feat(activities): align types + API service with events v1.6.1
The backend rebase brought in PR #50 (fiat checkout + email/Nostr ticket notifications), v1.6.0 (custom notification subject/body), and PR #51 (resend-email endpoint). The webapp types lagged. Aligns the type surface in src/modules/activities/types/ticket.ts: - EventExtra (with notification toggles + custom subject/body), promo codes, conditional event config. - ActivityTicketExtra mirroring backend's TicketExtra (nostr_identifier, email/nostr notification_sent flags, refund state). - TicketedEvent + CreateEventRequest gain allow_fiat, fiat_currency, extra. - TicketPurchaseInvoice extended for fiat: paymentRequest now optional, fiatPaymentRequest + fiatProvider + isFiat added. **Closes a latent blocker**: a backend response with is_fiat=true would have lost the fiat URL during deserialization (silent crash on QR generation). - New CreateTicketRequest type for the POST /tickets/{id} body, with the v1.6.1 payment_method + fiat_provider + nostr_identifier fields. TicketApiService: - requestTicket() accepts the new optional fields (paymentMethod, fiatProvider, promoCode, refundAddress, nostrIdentifier) and deserializes the full TicketPurchaseInvoice shape including fiat. - fetchUserTickets() / validateTicket() / new resendTicketEmail() thread the extra metadata through. useTicketPurchase composable rejects fiat responses with a clear error (the QR-and-bolt11 path doesn't know fiat); the eventual UI selector will live in PurchaseTicketDialog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6cd420d9cb
commit
73aee75b5b
3 changed files with 178 additions and 10 deletions
|
|
@ -98,16 +98,31 @@ export function useTicketPurchase() {
|
||||||
currentUser.value!.id,
|
currentUser.value!.id,
|
||||||
accessToken
|
accessToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Backend now returns either a Lightning invoice or a fiat
|
||||||
|
// checkout URL (post-events-v1.4.0). This composable only knows
|
||||||
|
// how to drive the Lightning path; fiat would need a separate
|
||||||
|
// redirect-to-provider flow that lives in PurchaseTicketDialog
|
||||||
|
// (it has the user-visible payment-method selector). Reject the
|
||||||
|
// fiat response here so callers get a clear error instead of a
|
||||||
|
// silent broken QR.
|
||||||
|
if (invoice.isFiat || !invoice.paymentRequest) {
|
||||||
|
throw new Error(
|
||||||
|
'This event uses fiat checkout. Use the purchase dialog ' +
|
||||||
|
'to follow the provider link.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const bolt11: string = invoice.paymentRequest
|
||||||
paymentHash.value = invoice.paymentHash
|
paymentHash.value = invoice.paymentHash
|
||||||
paymentRequest.value = invoice.paymentRequest
|
paymentRequest.value = bolt11
|
||||||
|
|
||||||
// Generate QR code for payment
|
// Generate QR code for payment
|
||||||
await generateQRCode(invoice.paymentRequest)
|
await generateQRCode(bolt11)
|
||||||
|
|
||||||
// Try to pay with wallet if available
|
// Try to pay with wallet if available
|
||||||
if (hasWalletWithBalance.value) {
|
if (hasWalletWithBalance.value) {
|
||||||
try {
|
try {
|
||||||
await payWithWallet(invoice.paymentRequest)
|
await payWithWallet(bolt11)
|
||||||
await startPaymentStatusCheck(eventId, invoice.paymentHash)
|
await startPaymentStatusCheck(eventId, invoice.paymentHash)
|
||||||
} catch (walletError) {
|
} catch (walletError) {
|
||||||
console.log('Wallet payment failed, falling back to manual payment:', walletError)
|
console.log('Wallet payment failed, falling back to manual payment:', walletError)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import type {
|
import type {
|
||||||
ActivityTicket,
|
ActivityTicket,
|
||||||
|
ActivityTicketExtra,
|
||||||
|
CreateTicketRequest,
|
||||||
|
PaymentMethod,
|
||||||
TicketPurchaseInvoice,
|
TicketPurchaseInvoice,
|
||||||
TicketPaymentStatus,
|
TicketPaymentStatus,
|
||||||
TicketedEvent,
|
TicketedEvent,
|
||||||
|
|
@ -49,14 +52,38 @@ export class TicketApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a ticket purchase (creates a Lightning invoice).
|
* Request a ticket purchase. Returns either a Lightning invoice
|
||||||
* Uses POST /tickets/{event_id} with user_id in body (upstream API).
|
* (`paymentRequest` = bolt11) or a fiat invoice (`fiatPaymentRequest`
|
||||||
|
* = follow-the-URL string from the configured fiat provider). The
|
||||||
|
* `isFiat` flag is the discriminator.
|
||||||
|
*
|
||||||
|
* `paymentMethod` defaults to "lightning"; pass "fiat" to opt into
|
||||||
|
* the fiat path (requires the event to have `allow_fiat=true`).
|
||||||
|
* `fiatProvider` is optional — backend picks the user's configured
|
||||||
|
* default when omitted.
|
||||||
|
*
|
||||||
|
* Additional ticket metadata (promo code, refund address, nostr
|
||||||
|
* identifier for DM delivery) can be supplied via `options`.
|
||||||
*/
|
*/
|
||||||
async requestTicket(
|
async requestTicket(
|
||||||
eventId: string,
|
eventId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
accessToken: string
|
accessToken: string,
|
||||||
|
options: {
|
||||||
|
paymentMethod?: PaymentMethod
|
||||||
|
fiatProvider?: string
|
||||||
|
promoCode?: string
|
||||||
|
refundAddress?: string
|
||||||
|
nostrIdentifier?: string
|
||||||
|
} = {},
|
||||||
): Promise<TicketPurchaseInvoice> {
|
): Promise<TicketPurchaseInvoice> {
|
||||||
|
const body: CreateTicketRequest = { user_id: userId }
|
||||||
|
if (options.paymentMethod) body.payment_method = options.paymentMethod
|
||||||
|
if (options.fiatProvider) body.fiat_provider = options.fiatProvider
|
||||||
|
if (options.promoCode) body.promo_code = options.promoCode
|
||||||
|
if (options.refundAddress) body.refund_address = options.refundAddress
|
||||||
|
if (options.nostrIdentifier) body.nostr_identifier = options.nostrIdentifier
|
||||||
|
|
||||||
const data = await this.request(
|
const data = await this.request(
|
||||||
`/events/api/v1/tickets/${eventId}`,
|
`/events/api/v1/tickets/${eventId}`,
|
||||||
{
|
{
|
||||||
|
|
@ -65,13 +92,16 @@ export class TicketApiService {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ user_id: userId }),
|
body: JSON.stringify(body),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paymentHash: data.payment_hash,
|
paymentHash: data.payment_hash,
|
||||||
paymentRequest: data.payment_request,
|
paymentRequest: data.payment_request ?? undefined,
|
||||||
|
fiatPaymentRequest: data.fiat_payment_request ?? undefined,
|
||||||
|
fiatProvider: data.fiat_provider ?? undefined,
|
||||||
|
isFiat: Boolean(data.is_fiat),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,6 +151,7 @@ export class TicketApiService {
|
||||||
paid: t.paid,
|
paid: t.paid,
|
||||||
time: t.time,
|
time: t.time,
|
||||||
regTimestamp: t.reg_timestamp,
|
regTimestamp: t.reg_timestamp,
|
||||||
|
extra: t.extra as ActivityTicketExtra | undefined,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,6 +175,7 @@ export class TicketApiService {
|
||||||
paid: t.paid,
|
paid: t.paid,
|
||||||
time: t.time,
|
time: t.time,
|
||||||
regTimestamp: t.reg_timestamp,
|
regTimestamp: t.reg_timestamp,
|
||||||
|
extra: t.extra as ActivityTicketExtra | undefined,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +215,39 @@ export class TicketApiService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend the ticket confirmation email for a paid ticket. Requires
|
||||||
|
* the event's wallet admin key (organizer-only). Returns the updated
|
||||||
|
* Ticket with the `email_notification_sent` flag refreshed.
|
||||||
|
*
|
||||||
|
* Endpoint added upstream in v1.6.1 (PR #51).
|
||||||
|
*/
|
||||||
|
async resendTicketEmail(
|
||||||
|
ticketId: string,
|
||||||
|
adminKey: string,
|
||||||
|
): Promise<ActivityTicket> {
|
||||||
|
const t = await this.request(
|
||||||
|
`/events/api/v1/tickets/${ticketId}/resend-email`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-API-KEY': adminKey },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
id: t.id,
|
||||||
|
wallet: t.wallet,
|
||||||
|
activityId: t.event,
|
||||||
|
name: t.name,
|
||||||
|
email: t.email,
|
||||||
|
userId: t.user_id,
|
||||||
|
registered: t.registered,
|
||||||
|
paid: t.paid,
|
||||||
|
time: t.time,
|
||||||
|
regTimestamp: t.reg_timestamp,
|
||||||
|
extra: t.extra as ActivityTicketExtra | undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Probe whether the current user has LNbits admin privileges. The
|
* Probe whether the current user has LNbits admin privileges. The
|
||||||
* `/all` endpoint is `check_admin`-gated, so a 200 means "admin",
|
* `/all` endpoint is `check_admin`-gated, so a 200 means "admin",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,44 @@
|
||||||
/**
|
/**
|
||||||
* Database-backed ticket types (via LNbits events extension)
|
* Database-backed ticket types (via LNbits events extension).
|
||||||
|
*
|
||||||
|
* Wire-format types — names match the snake_case fields the events
|
||||||
|
* extension serves over HTTP. Camel-cased aliases (e.g. ActivityTicket
|
||||||
|
* below) are the webapp-internal view models after adapter conversion.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export interface PromoCode {
|
||||||
|
code: string
|
||||||
|
discount_percent: number
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventExtra mirrors the EventExtra Pydantic model in
|
||||||
|
* `events/models.py`. Carries promo codes, conditional-event config,
|
||||||
|
* and the per-event notification toggles + custom subject/body added
|
||||||
|
* in upstream v1.4.0 (PR #50) and v1.6.0.
|
||||||
|
*/
|
||||||
|
export interface EventExtra {
|
||||||
|
promo_codes: PromoCode[]
|
||||||
|
conditional: boolean
|
||||||
|
min_tickets: number
|
||||||
|
email_notifications: boolean
|
||||||
|
nostr_notifications: boolean
|
||||||
|
notification_subject: string
|
||||||
|
notification_body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityTicketExtra {
|
||||||
|
applied_promo_code?: string | null
|
||||||
|
sats_paid?: number | null
|
||||||
|
refund_address?: string | null
|
||||||
|
nostr_identifier?: string | null
|
||||||
|
ticket_base_url?: string | null
|
||||||
|
email_notification_sent: boolean
|
||||||
|
nostr_notification_sent: boolean
|
||||||
|
refunded: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActivityTicket {
|
export interface ActivityTicket {
|
||||||
id: string
|
id: string
|
||||||
wallet: string
|
wallet: string
|
||||||
|
|
@ -21,19 +58,40 @@ export interface ActivityTicket {
|
||||||
time: string
|
time: string
|
||||||
/** Registration/scan timestamp */
|
/** Registration/scan timestamp */
|
||||||
regTimestamp: string
|
regTimestamp: string
|
||||||
|
/** Optional metadata — promo code applied, sats paid, notification
|
||||||
|
* delivery flags, refund state. May be absent on older tickets. */
|
||||||
|
extra?: ActivityTicketExtra
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TicketStatus = 'pending' | 'paid' | 'registered' | 'cancelled'
|
export type TicketStatus = 'pending' | 'paid' | 'registered' | 'cancelled'
|
||||||
|
|
||||||
|
export type PaymentMethod = 'lightning' | 'fiat'
|
||||||
|
|
||||||
export interface TicketPurchaseRequest {
|
export interface TicketPurchaseRequest {
|
||||||
activityId: string
|
activityId: string
|
||||||
userId: string
|
userId: string
|
||||||
accessToken: string
|
accessToken: string
|
||||||
|
/** Lightning (default) or fiat. Only meaningful if the event has
|
||||||
|
* `allow_fiat=true` on the backend; otherwise the backend coerces
|
||||||
|
* to lightning. */
|
||||||
|
paymentMethod?: PaymentMethod
|
||||||
|
/** Specific fiat provider id (e.g. "stripe"). Backend picks the
|
||||||
|
* user's default if omitted. */
|
||||||
|
fiatProvider?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server response from `POST /tickets/{event_id}`. Either Lightning
|
||||||
|
* (`paymentRequest` = bolt11) or fiat (`fiatPaymentRequest` = a URL
|
||||||
|
* the buyer follows to complete payment with `fiatProvider`).
|
||||||
|
* `isFiat` is the discriminator.
|
||||||
|
*/
|
||||||
export interface TicketPurchaseInvoice {
|
export interface TicketPurchaseInvoice {
|
||||||
paymentHash: string
|
paymentHash: string
|
||||||
paymentRequest: string
|
paymentRequest?: string
|
||||||
|
fiatPaymentRequest?: string
|
||||||
|
fiatProvider?: string
|
||||||
|
isFiat: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketPaymentStatus {
|
export interface TicketPaymentStatus {
|
||||||
|
|
@ -58,6 +116,10 @@ export interface TicketedEvent {
|
||||||
event_start_date: string
|
event_start_date: string
|
||||||
event_end_date: string | null
|
event_end_date: string | null
|
||||||
currency: string
|
currency: string
|
||||||
|
/** Whether the event accepts fiat payments. Upstream v1.4.0+. */
|
||||||
|
allow_fiat: boolean
|
||||||
|
/** Fiat currency code (ISO 4217). Defaults to "GBP" upstream. */
|
||||||
|
fiat_currency: string
|
||||||
amount_tickets: number
|
amount_tickets: number
|
||||||
price_per_ticket: number
|
price_per_ticket: number
|
||||||
time: string
|
time: string
|
||||||
|
|
@ -65,6 +127,7 @@ export interface TicketedEvent {
|
||||||
banner: string | null
|
banner: string | null
|
||||||
location: string | null
|
location: string | null
|
||||||
categories: string[]
|
categories: string[]
|
||||||
|
extra: EventExtra
|
||||||
status: string
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,9 +139,34 @@ export interface CreateEventRequest {
|
||||||
event_start_date: string
|
event_start_date: string
|
||||||
event_end_date?: string
|
event_end_date?: string
|
||||||
currency?: string
|
currency?: string
|
||||||
|
allow_fiat?: boolean
|
||||||
|
fiat_currency?: string
|
||||||
amount_tickets?: number
|
amount_tickets?: number
|
||||||
price_per_ticket?: number
|
price_per_ticket?: number
|
||||||
banner?: string | null
|
banner?: string | null
|
||||||
location?: string | null
|
location?: string | null
|
||||||
categories?: string[]
|
categories?: string[]
|
||||||
|
/** Optional — notification toggles + custom subject/body, promo
|
||||||
|
* codes, conditional-event config. Backend defaults to a fresh
|
||||||
|
* EventExtra if omitted. */
|
||||||
|
extra?: Partial<EventExtra>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for `POST /tickets/{event_id}`. Either `user_id` OR the
|
||||||
|
* `name`+`email` pair is required (backend root_validator enforces
|
||||||
|
* mutual exclusion). `nostr_identifier` opts the buyer into Nostr DM
|
||||||
|
* delivery when the event has nostr_notifications enabled. The
|
||||||
|
* `payment_method` + `fiat_provider` pair selects between Lightning
|
||||||
|
* and fiat checkout.
|
||||||
|
*/
|
||||||
|
export interface CreateTicketRequest {
|
||||||
|
name?: string
|
||||||
|
email?: string
|
||||||
|
user_id?: string
|
||||||
|
promo_code?: string
|
||||||
|
refund_address?: string
|
||||||
|
nostr_identifier?: string
|
||||||
|
payment_method?: PaymentMethod
|
||||||
|
fiat_provider?: string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue