docs(events): guest checkout, fiat return flow and ticket delivery contract
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
This commit is contained in:
parent
89a1fcb030
commit
cbd4159f72
2 changed files with 83 additions and 0 deletions
76
docs/02-modules/events-module/guest-checkout.md
Normal file
76
docs/02-modules/events-module/guest-checkout.md
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# 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: {...} }`).
|
||||||
|
|
@ -182,6 +182,13 @@ interface ModulePlugin {
|
||||||
|
|
||||||
**See:** [[market-module/index|📖 Market Module Documentation]]
|
**See:** [[market-module/index|📖 Market Module Documentation]]
|
||||||
|
|
||||||
|
### **Events Module** 🎟️
|
||||||
|
**Purpose:** Ticketed events (Nostr NIP-52 feed + LNbits events extension)
|
||||||
|
**Location:** `src/modules/events/`
|
||||||
|
**Dependencies:** `['base']`
|
||||||
|
|
||||||
|
**See:** [[events-module/guest-checkout|📖 Guest checkout, fiat return flow and ticket delivery]]
|
||||||
|
|
||||||
## Module Development
|
## Module Development
|
||||||
|
|
||||||
### **Creating a New Module**
|
### **Creating a New Module**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue