Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
3.6 KiB
Events — guest checkout, fiat return flow and ticket delivery
Buyers can purchase paid tickets without an LNbits account. This note is
the wire contract between the webapp and the aiolabs/events extension
(≥ v1.6.1-aio.8) for that flow. Free tickets still require a login
(events#29: no anonymous free issuance).
Identity on POST /events/api/v1/tickets/{event_id}
| Buyer | Body fields | Auth header |
|---|---|---|
| Logged in | user_id + email (delivery address, prefilled from the account, editable) |
Authorization: Bearer <token> |
| Guest | name + email |
none (the endpoint is anonymous; the instance X-API-KEY is always sent) |
Every purchase also sends frontend_url — this app's root
(window.location.origin + import.meta.env.BASE_URL, no trailing slash;
src/modules/events/lib/frontendRoot.ts). The extension only accepts it when
its origin is on the LNbits LNBITS_CORS_ALLOWED_ORIGINS allowlist (or the
LNbits base / custom-frontend origin); otherwise the request fails with
frontend_url origin is not allowed. and the dialog shows that message.
payment_method is lightning or fiat. The event's enabled rails come from
the NIP-52 tag tickets_payment_methods ("lightning,fiat"); when the tag is
absent the legacy rule applies (Lightning always, fiat iff
tickets_allow_fiat). See lib/paymentMethods.ts. A rail that is off is
rejected by the backend with Payment method not enabled for this event.
Return from the fiat provider
The extension composes both Stripe URLs under frontend_url:
| Purpose | URL |
|---|---|
| success | <root>/events/{event_id}?checkout=success&tickets=<id1,id2> |
| cancel | <root>/events/{event_id}?checkout=cancelled |
Before redirecting (same tab, window.location.assign), the purchase dialog
writes localStorage['events:pending-checkout'] =
{ eventId, paymentHash, ticketIds, email, ts } (lib/pendingCheckout.ts).
EventDetailPage strips the query on arrival, then:
success→ opensCheckoutReturnDialog, which polls the anonymousGET /events/api/v1/tickets/{ticket_id}for every id every 2 s (60 s budget, retry button) until all arepaid, then renders one QR per ticket, the "emailed to" note (email from the localStorage record) and a link to the ticket page. Logged-in buyers also getuseOwnedTickets().refresh().cancelled→ info toast, record cleared. Nothing was charged; the extension purges unpaid rows after 24 h.
Public ticket page
/events/ticket/:ticketId (views/TicketPage.vue, requiresAuth: false) is
where the emailed link lands. It reads the PublicTicket (no id / email /
user_id in the payload; the id in the URL is the door credential, exactly as
in the LNbits UI), resolves the event name (Nostr store → scoped relay query
→ public REST, which 410s on sold-out events and is treated as non-fatal) and
renders the QR.
QR rendering
All ticket://<id> QRs (purchase success, return dialog, ticket page, My
Tickets) go through lib/ticketQr.ts: error-correction level H with the
brand logo (@brand/logo.png) centred at ≤ 20 % width on a white pad, so the
in-app image matches the PNG the extension embeds in the ticket email
(GET /events/api/v1/qr/{ticket_id}). Canvas failures fall back to the plain
QR.
Organizer side
CreateEventDialog publishes extra.payment_methods (Lightning / Card
checkboxes; at least one) and keeps allow_fiat in sync for older readers.
Card is disabled with a tooltip when the organizer's LNbits user has no fiat
provider (useFiatProviders().hasAnyProvider).
resendTicketEmail returns the structured TicketResendResult
({ ticket, email: {attempted, sent, error}, nostr: {...} }).