Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
76 lines
3.6 KiB
Markdown
76 lines
3.6 KiB
Markdown
# 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` → opens `CheckoutReturnDialog`, which polls the anonymous
|
|
`GET /events/api/v1/tickets/{ticket_id}` for every id every 2 s (60 s budget,
|
|
retry button) until all are `paid`, 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 get `useOwnedTickets().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: {...} }`).
|