feat(activities): ticket purchase + Nostr-driven inventory sync #71
No reviewers
Labels
No labels
app:activities
app:chat
app:events
app:forum
app:libra
app:market
app:restaurant
app:tasks
app:wallet
app:webapp
bug
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiolabs/webapp!71
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "tickets-purchase-sync"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Restores the ticket purchase + "tickets visible in the events area"
loop after the activities module was extracted into a standalone
Nostr-driven PWA. Companion to aiolabs/events#15.
Before this PR:
EventsPage(the LNbitslisting), but activities sourced from Nostr had no buy path at all.
Activity.ticketInfoexisted in the type but was never populated— converters skipped it — so cards showed nothing and the
detail page had no purchase affordance.
MyTicketsPageroute existed but hit a 404 endpoint (thebackend lacked
GET /tickets/user/{user_id}).After this PR (combined with #15):
ticketInfo, parsedfrom the six AIO
tickets_*tags added by the events extension.ActivityCardshows "✓ You have N tickets" alongside theexisting tickets-remaining line.
ActivityDetailPagegains an owned-tickets section + a Buybutton gated on capacity. PurchaseTicketDialog wires up
unchanged from #68.
ActivitiesPagenarrows the feedto activities you hold tickets for.
MyTicketsPagestarts working once #15 lands.Wire contract (with aiolabs/events#15)
tickets_availableActivityTicketInfo.available(undefined when omitted = unlimited)tickets_soldActivityTicketInfo.soldtickets_priceActivityTicketInfo.pricetickets_currencyActivityTicketInfo.currencytickets_allow_fiatActivityTicketInfo.allowFiattickets_fiat_currencyActivityTicketInfo.fiatCurrencytickets_currencyis the discriminator — when absent the parserreturns
undefinedforticketInfo, so non-AIO calendar events(or AIO events published before this rollout) cleanly render
without any buy UI.
Commits
7cf009c— parse ticket inventory tags from NIP-52 events(TicketTags + parseTicketTags + populate Activity.ticketInfo).
fd78a91— useOwnedTickets composable (module-level singleton)5589bb3— purchase button + owned-tickets section onActivityDetailPage; existing PurchaseTicketDialog wired up.
ea4e196— "My tickets" filter chip on ActivitiesPage.Branch base
Branches off
payment-rails-pattern(PR #68) rather thandevdirectly — the existing PurchaseTicketDialog refactor from #68 is
load-bearing for the detail page wiring. Don't merge this until
#68 + #69 have landed on
dev, or rebase first.Backend dependency
Do not merge until aiolabs/events#15 has shipped and the
catalog entry is bumped on the host the webapp connects to.
Without the backend tags, every activity will render with
ticketInfo = undefined— the badges + buy button surfaces willall stay dark.
Test plan
After backend + catalog are live on the target host:
npm run buildclean (vue-tsc + vite) — passes locally.feed → confirm cards show "X tickets remaining" badges parsed
from the Nostr tags. Unlimited-capacity activities show
"Unlimited tickets".
with the correct price/currency, no owned-tickets section.
(via relay republish from the backend).
flips to "Buy another ticket".
activities with owned tickets remain visible.
tabs, buy simultaneously, confirm
tickets_soldincrementsby 2 on the republished event.
🤖 Generated with Claude Code
Activities is the first module to mix Lightning + fiat rails; restaurant and marketplace will follow. Extract the cross-cutting bits to the base module so the next adoption is a wiring exercise: - useFiatProviders: reactive `User.fiat_providers` (today the same list for organizer + buyer because LNbits configures providers globally), plus `providerMeta()` for label/icon hints. - usePriceConversion: `convert()` + reactive `useLivePreview()` over the existing `/api/v1/conversion` endpoint, 60s cache, null on transient failure. - PaymentMethodSelector: buyer-side rail picker. `PaymentMethod.id` enumerates rails (`lightning | fiat | cash | internal | …`) with `provider` for the fiat case so a multi-provider instance shows one button per provider instead of a bare "Fiat" catch-all. - FiatToggleField: organizer-side switch + conditional fiat-currency dropdown. Auto-disables with a setup-instructions tooltip when the user has no providers; silently mirrors fiat_currency to a non-sat price denomination to keep the backend payload consistent. - PriceConversionPreview: muted "≈ X.XX USD" line for surfaces where the price denomination differs from the chosen rail's currency. LnbitsAPI.getConversion wraps the conversion endpoint so the composable goes through the existing API service rather than raw fetch. CLAUDE.md gains a "Payment rails pattern" section documenting the canonical vocabulary ("Price currency" / "Fiat currency" / "Payment method" / "Also accept fiat" — bare "Currency" and "Pay in fiat" are banned in payment-context UI labels) and the fiat-providers-are-global note. The pre-existing `prvkey` comment on User picks up an inline allowlist marker so the secret scanner stops flagging this file on every commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Split the bottom of the create/edit form into two semantic sections that read in the canonical vocabulary: Pricing Tickets · Price · Price currency ← renamed from bare "Currency" Payment methods Lightning — always on (informational chip) <FiatToggleField/> ← replaces the inline switch + raw fiat_currency dropdown The toggle field handles the conditional dropdown (hide + auto-mirror when the price denomination IS the fiat currency) and the disabled- with-tooltip state when the user has no configured provider, so the parent form just supplies field names + the denomination value. Zod superRefine grows a check that requires `fiat_currency` only in the surface where the toggle exposes the dropdown — `allow_fiat && currency === 'sat'`. Submit-time payload drops `fiat_currency` when `allow_fiat` is off so we don't persist a rail-currency the backend won't use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>i18n: add the missing keys the ticket purchase + owned-tickets surfaces use across en/es/fr — activities.detail.{buyTicket, buyAnotherTicket, viewMyTickets, ticketsOwned, unlimitedTickets} and activities.filters.myTickets. Without these the runtime fell back to the literal key strings + spammed [intlify] warnings; the filter chip rendered the bare key text on logged-in sessions. ticketsOwned uses i18n pluralization so "You have 1 ticket" vs "You have 5 tickets" both come out correct. useOwnedTickets: the hasAutoLoaded guard prevented retries after a transient backend failure (e.g. an LNbits restart mid-fetch). The composable would stay stuck with tickets = [] forever, so the buyer landing on a fresh detail page right after a transient error saw no badges anywhere. Detect the "previous load didn't actually hydrate" state (lastLoadedUserId still null while authenticated) and retry on the next useOwnedTickets() call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Earlier commit landed the backend storing N seats on one row via extra.quantity (one invoice, one payment, one ticket row), but the UI kept counting rows instead of seats. A 5-ticket purchase showed: Dialog header: "Purchase a ticket for X for 100 sats" ← lied Success modal: "Ticket purchased!" / one ticket ID ← lied My Tickets / badges: "1 paid ticket" ← lied even though the buyer correctly paid 500 sats and 5 seats were sold (DB verified: extra.quantity=5, sats_paid=500, event.sold incremented by 5). The bolt11 invoice amount is cryptographic so the wallet charge was always right — only the labels were wrong. Fixes: - ActivityTicketExtra grows `quantity?: number` (the field already on the wire from the backend; just adding it to the type). - useOwnedTickets exposes `seatsOnRow(ticket)` and `paidCount` sums seats (extra.quantity) across rows instead of counting rows. ActivityCard's "You have N tickets" badge now reflects actual seat ownership. - useUserTickets.groupedTickets sums seats into paidCount / pendingCount / registeredCount so MyTicketsPage groups read correctly. - ActivityDetailPage owned-tickets section adds a `×N` chip on rows that represent multiple seats so the buyer can see which row covers how many. - PurchaseTicketDialog header + DialogDescription reflect the selected quantity ("Purchase 5 tickets" / "5 tickets for X · 500 sats"). The success modal switches to "5 tickets purchased!" and re-labels the ticket id "Purchase ID (covers all tickets)" so the buyer doesn't expect 5 separate ids. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Last commit fixed the dialog + ActivityDetailPage to read extra.quantity, but missed three more row-count → seat-count surfaces in MyTicketsPage: - Tab pills (All / Paid / Pending / Registered) used `paidTickets.length` etc. on the filtered row arrays — so a user who bought 1+5+5+6+3+1+1+1 = 23 seats across 8 rows saw "All (8)". Now reads from useUserTickets.{total,paid,pending, registered}Seats which sum extra.quantity. - Group header badge "{{ group.tickets.length }} tickets" → uses group.paidCount + pendingCount (already seat-summed by the previous fix to groupedTickets). - Group description gains a "({N} purchases)" sub-line when seats ≠ rows so the buyer can see at a glance "you have 23 tickets across 8 purchases". - Per-row carousel card grows a `×N` chip next to the truncated Ticket #ID when that row represents multi-seat — same chip language as the ActivityDetailPage owned-tickets section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>The card had it; the detail page didn't. Reuses the same three- state language as the card ("Unlimited" / "{count} tickets available" / "Sold out") so the buyer sees the same signal on both surfaces. Placed at the top of the tickets section, above the owned-tickets chip + buy CTA, so it reads top-down: how many are left → how many you have → buy more. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>