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>
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>
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>
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>
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>
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>
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>