feat(activities): payment-rails pattern + provider-aware checkout #68

Merged
padreug merged 7 commits from payment-rails-pattern into dev 2026-05-23 21:18:54 +00:00

7 commits

Author SHA1 Message Date
663e32e7a4 fix(activities): normalize 'sat' vs 'sats' across fiat conditionals
TicketApiService.getCurrencies() returns 'sats' (plural) while the
schema, initialValues, and existing comparisons used 'sat' (singular)
— a pre-existing inconsistency in the events extension surface. The
new payment-rails conditionals tripped on it: as soon as the user
picked the populated 'sats' option from the price-currency dropdown,
form.values.currency became 'sats', the `=== 'sat'` check failed, and
the Fiat currency dropdown stayed hidden even with the toggle on.

Normalize all the new comparisons to accept both spellings:

- FiatToggleField: isSatDenomination(d) helper drives both the
  v-show and the auto-mirror watch.
- CreateEventDialog Zod superRefine: same accept-both rule on the
  require-fiat_currency branch.
- PurchaseTicketDialog: isPriceInSats computed drives the
  Lightning-sats badge AND the PriceConversionPreview render
  condition AND the inverse conversion watcher's bail-out.

Also flip FiatToggleField to drive dropdown visibility from the
outer FormField's slot value rather than useFormContext — slot
bindings are guaranteed reactive, sidesteps the public-form-context
indirection that earlier left allowFiat stale in the child's
template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:44:43 +02:00
b7b5a08594 fix(base): FiatToggleField reads form state via useFormContext
The previous version called useField directly with a getter for the
field name. That created a child-local field rather than connecting
to the parent form's allow_fiat / fiat_currency state — so the
Switch's on/off visually toggled but the form never knew, and the
conditional Fiat currency dropdown never appeared.

Rewrite around the proven pattern used elsewhere in the dialog: bind
the inputs through FormField (the shadcn-vue / vee-validate Field
component) and reach for cross-field state via useFormContext.
showCurrencyDropdown now reads form.values[allowFiatField] directly,
which mirrors the parent's actual state, and the denomination-mirror
watch goes through form.setFieldValue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:30:03 +02:00
99667add65 feat(activities): provider-aware checkout labels and conversion preview
The buyer-side payment-method block now surfaces one button per
configured fiat provider — Stripe, PayPal, Square, SEPA — rather than
a single bare "Fiat" catch-all. Buttons read in provider names so the
buyer never has to guess what rail backs each choice; the dispatch on
click forwards both `rail` and `provider` to the existing
`ticketApi.requestTicket` signature.

PaymentMethodSelector + useFiatProviders from the base module drive
the list. The Lightning button picks up a "≈ N sats" badge whenever
the event price is denominated in fiat, so the buyer sees the live
sat charge alongside the headline price. A new conversion-preview
line under the headline shows the sat→fiat estimate in the inverse
case (sat-denominated event with fiat enabled), giving the rail-vs-
unit asymmetry an explicit place in the UI.

Explanatory copy makes the equivalence explicit: both methods charge
the same amount, rates are estimates, exact amount locks in at
checkout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 18:40:50 +02:00
61665790b3 refactor(activities): adopt shared payment-rails pattern in CreateEventDialog
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>
2026-05-23 18:37:11 +02:00
a0087b6bf3 feat(base): payment-rails composables + components shared across modules
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>
2026-05-23 18:34:19 +02:00
f94ad30ac7 feat(activities): expose fiat checkout on event create + purchase
Both sides of the fiat-payment surface introduced by events v1.4.0:

CreateEventDialog — organizer-side opt-in:
- New "Accept fiat payments" switch (allow_fiat) + fiat currency
  picker (USD/EUR/GBP/CHF). Toggle is always shipped on
  create/edit so a true→false flip propagates correctly.
- Hint copy notes that the host's LNbits admin needs a configured
  fiat provider (Stripe etc.) for the toggle to actually work at
  purchase time.

PurchaseTicketDialog — buyer-side method selector:
- Two-button selector (Lightning / Fiat) shown only when the event
  has `allow_fiat=true`. Hidden entirely for Lightning-only events.
- Lightning path: unchanged (uses useTicketPurchase composable).
- Fiat path: posts to the API with `payment_method=fiat`, then
  surfaces a "Open <provider> checkout" button that opens the
  returned fiat_payment_request URL in a new tab. Payment
  confirmation happens via webhook on the backend; ticket appears
  in My Tickets on next reload.

EventsPage threads the new fiat fields through `selectedEvent` so
the dialog sees them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:47:39 +02:00
620919da58 feat(activities): align types + API service with events v1.6.1
The backend rebase brought in PR #50 (fiat checkout + email/Nostr
ticket notifications), v1.6.0 (custom notification subject/body), and
PR #51 (resend-email endpoint). The webapp types lagged.

Aligns the type surface in src/modules/activities/types/ticket.ts:

- EventExtra (with notification toggles + custom subject/body), promo
  codes, conditional event config.
- ActivityTicketExtra mirroring backend's TicketExtra (nostr_identifier,
  email/nostr notification_sent flags, refund state).
- TicketedEvent + CreateEventRequest gain allow_fiat, fiat_currency, extra.
- TicketPurchaseInvoice extended for fiat: paymentRequest now optional,
  fiatPaymentRequest + fiatProvider + isFiat added. **Closes a latent
  blocker**: a backend response with is_fiat=true would have lost the
  fiat URL during deserialization (silent crash on QR generation).
- New CreateTicketRequest type for the POST /tickets/{id} body, with
  the v1.6.1 payment_method + fiat_provider + nostr_identifier fields.

TicketApiService:
- requestTicket() accepts the new optional fields (paymentMethod,
  fiatProvider, promoCode, refundAddress, nostrIdentifier) and
  deserializes the full TicketPurchaseInvoice shape including fiat.
- fetchUserTickets() / validateTicket() / new resendTicketEmail()
  thread the extra metadata through.

useTicketPurchase composable rejects fiat responses with a clear
error (the QR-and-bolt11 path doesn't know fiat); the eventual UI
selector will live in PurchaseTicketDialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:47:39 +02:00