feat(events): guest checkout with email delivery, Stripe return flow, public ticket page #144

Merged
padreug merged 7 commits from feat/events-guest-checkout into dev 2026-09-08 10:25:57 +00:00
Owner

Webapp half of the guest-checkout work (backend: aiolabs/events feat/guest-checkout-email → v1.6.1-aio.8; lnbits: aiolabs/lnbits#66). Ship only after the extension is deployed — an older extension 422s on user_id + email.

What changes

  • Guests can buy paid tickets (name + email + confirm-email, Shadcn Form + vee-validate + zod). Logged-in buyers also get the email field, prefilled from their account, so every ticket carries a delivery address. Free tickets stay login-gated (events#29).
  • Fiat checkout redirects in the same tab and comes back to /events/:id?checkout=success&tickets=… / ?checkout=cancelled; the query is stripped on arrival and a return dialog polls the anonymous ticket read until paid, then shows one QR per ticket and "we emailed your ticket to …". A localStorage record (events:pending-checkout) is the fallback source.
  • Guest Lightning works with the same identity (QR + "Open in wallet"; wallet-pay only when logged in).
  • Per-event payment methods: rails come from the new NIP-52 tag tickets_payment_methods (fallback to the allow_fiat rule); CreateEventDialog gets a Lightning / Card checkbox group replacing the fiat toggle, Card disabled when the organizer has no fiat provider.
  • Public ticket page /events/ticket/:ticketId — target of the emailed link; event name via the Nostr store, then a public-event fetch.
  • frontend_url = origin + BASE_URL (so https://demo.aiolabs.dev/events / https://app.ariege.io/oyez) is sent on every purchase.
  • Shared ticket:// QR renderer with the brand logo overlay (error-correction H) used by the return dialog, ticket page and My Tickets.
  • i18n en/es/fr for the new strings; docs/02-modules/events-module/guest-checkout.md documents the contract.

Verification

vue-tsc -b clean; hub and events standalone builds pass; vitest run src/modules/events → 26 tests passing (API body shapes for guest vs logged-in, NIP-52 tag parsing, frontend root, payment-method resolution).

Note: the "deep link shows the hub / login" report was an nginx fallback bug fixed in server-deploy (d5f5a19) and already deployed to aio-demo.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo

Webapp half of the guest-checkout work (backend: aiolabs/events `feat/guest-checkout-email` → v1.6.1-aio.8; lnbits: aiolabs/lnbits#66). **Ship only after the extension is deployed** — an older extension 422s on `user_id + email`. ## What changes - **Guests can buy paid tickets** (name + email + confirm-email, Shadcn Form + vee-validate + zod). Logged-in buyers also get the email field, prefilled from their account, so every ticket carries a delivery address. Free tickets stay login-gated (events#29). - **Fiat checkout** redirects in the same tab and comes back to `/events/:id?checkout=success&tickets=…` / `?checkout=cancelled`; the query is stripped on arrival and a return dialog polls the anonymous ticket read until paid, then shows one QR per ticket and "we emailed your ticket to …". A localStorage record (`events:pending-checkout`) is the fallback source. - **Guest Lightning** works with the same identity (QR + "Open in wallet"; wallet-pay only when logged in). - **Per-event payment methods**: rails come from the new NIP-52 tag `tickets_payment_methods` (fallback to the `allow_fiat` rule); CreateEventDialog gets a Lightning / Card checkbox group replacing the fiat toggle, Card disabled when the organizer has no fiat provider. - **Public ticket page** `/events/ticket/:ticketId` — target of the emailed link; event name via the Nostr store, then a public-event fetch. - `frontend_url` = `origin + BASE_URL` (so `https://demo.aiolabs.dev/events` / `https://app.ariege.io/oyez`) is sent on every purchase. - Shared `ticket://` QR renderer with the brand logo overlay (error-correction H) used by the return dialog, ticket page and My Tickets. - i18n en/es/fr for the new strings; `docs/02-modules/events-module/guest-checkout.md` documents the contract. ## Verification `vue-tsc -b` clean; hub and events standalone builds pass; `vitest run src/modules/events` → 26 tests passing (API body shapes for guest vs logged-in, NIP-52 tag parsing, frontend root, payment-method resolution). Note: the "deep link shows the hub / login" report was an nginx fallback bug fixed in server-deploy (d5f5a19) and already deployed to aio-demo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Prepare the webapp for account-less ticket purchases against events ext
v1.6.1-aio.8:

- requestTicket takes an options object: user_id (+ optional email) for
  logged-in buyers, name + email for guests, Authorization only when a
  token exists, and frontend_url so the backend builds the Stripe return
  URLs and the emailed ticket link under this app's root.
- getTicket (anonymous PublicTicket) and getPublicEvent for the return
  dialog and the public ticket page.
- resendTicketEmail maps the structured TicketResendResult (legacy bare
  ticket tolerated).
- NIP-52 tickets_payment_methods tag parsed into ticketInfo.paymentMethods;
  effectivePaymentMethods() mirrors the backend rule.
- renderTicketQr: level-H ticket:// QR with the brand logo centred, shared
  by every ticket surface; useTicketPurchase accepts a guest contact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Buyers no longer need an account to buy a paid ticket. The dialog gains a
"Your details" block (name for guests, email + confirm-email for everyone,
prefilled from the account email) as a vee-validate/zod form, and builds
its rail buttons from the event's published payment methods so an
organizer can run card-only events.

- Fiat: same-tab redirect to the provider right after the POST, with the
  pending checkout (ticket ids, email) remembered in localStorage for the
  return page; the two-step "open in new tab" panel is gone.
- Lightning: guests get the bolt11 QR + "Open in wallet" and settle via
  the existing poll; the LNbits-wallet button stays login-only.
- Free tickets remain login-gated (events#29).
- Guest success state renders the ticket QRs inline with the "emailed
  to" note, since guests have no My Tickets.

i18n keys added under events.purchase / checkout / ticket (en, es, fr).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Stripe now brings the buyer back to the event page with
?checkout=success&tickets=<ids> or ?checkout=cancelled. The page strips
the query, then opens CheckoutReturnDialog, which polls the anonymous
ticket endpoint until every row is paid (60 s budget with a retry), and
renders one logo QR per ticket with a link to its public ticket page and
the "emailed to" note from the pending-checkout record. Logged-in buyers
also get their owned tickets refreshed and a My Tickets shortcut.

The login toast on "Buy ticket" now only fires for free events; paid
events open the dialog for guests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Target of the emailed ticket link the extension now builds under the
webapp root. Anonymous, like the LNbits UI's own /events/ticket/{id}:
reads the PublicTicket, resolves the event name (Nostr store → scoped
relay query → public REST as a last resort), and renders the logo QR
with paid / pending / checked-in state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Replace the "Also accept fiat" switch with a Lightning / Card checkbox
group so an organizer can switch Lightning off for a card-only sale.
Sends extra.payment_methods (upstream v2 shape) and keeps allow_fiat in
sync (fiat in list ⇒ allow_fiat=true) for older readers. Card stays
disabled with a tooltip when the organizer has no fiat provider; the
fiat-currency picker appears when Card is on and the price is in sats.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
padreug deleted branch feat/events-guest-checkout 2026-09-08 10:25:57 +00:00
Sign in to join this conversation.
No description provided.