feat(activities): payment-rails pattern + provider-aware checkout #68
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!68
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "payment-rails-pattern"
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
Resolves the clunky payment surface introduced by the v1.6.1 fiat
exposure: two "Currency" selectors with no relationship, a bare
fiat button on the buyer side that conflicts with the price unit,
and no live conversion preview. Reshapes the dialogs around one
canonical vocabulary and extracts the cross-cutting bits into the
base module so restaurant + marketplace can adopt the pattern as
their backends gain fiat support.
currency), "Fiat currency"(
fiat_currency), "Payment method" (rail), "Also accept fiat"(
allow_fiat). The bare word "Currency" is banned inpayment-context labels; the buyer-side "Pay in fiat" / "Fiat"
button is replaced by per-provider buttons (
Stripe,PayPal,Square,SEPA).src/modules/base/:useFiatProvidersreads the per-sessionUser.fiat_providersfrom
GET /api/v1/auth(today the same list for organizer +buyer because LNbits configures providers globally).
usePriceConversionover the existing/api/v1/conversionendpoint with a 60s cache; best-effort, null on failure.
PaymentMethodSelector— rail picker with rail+providerdispatch fields and label+icon+badge display fields.
FiatToggleField— owner-side switch + conditionalfiat-currency dropdown; auto-disabled with a setup-instructions
tooltip when the user has no providers; silently mirrors
fiat_currency = denominationfor non-sat price units.PriceConversionPreview— muted "≈ X.XX USD" line.two semantic sections (Pricing · Payment methods). Lightning
shows as an informational chip; the fiat block is now the
shared
<FiatToggleField>. Zod superRefine requiresfiat_currencyonly on the surface where it's actuallyexposed (
allow_fiat && currency === 'sat'). Submit dropsfiat_currencywhen fiat is off so the backend payload stayshonest.
<PaymentMethodSelector>driven byuseFiatProviders, so amulti-provider instance shows one button per provider. The
Lightning button picks up a "≈ N sats" badge whenever the price
is denominated in fiat. A new
<PriceConversionPreview>lineunder the headline price shows the inverse case (sat-denominated
event with fiat enabled). Explanatory copy makes the equivalence
explicit: same amount via different rails, rates are estimates,
exact amount locks in at checkout.
documenting the vocabulary, primitives, fiat-providers-are-global
architecture, and the "adding a new provider" recipe.
This branch depends on the v1.6.1 types/API alignment commit
(
620919d) it builds on top of. The siblingnotification-configbranch also carries that commit; whichever PR merges first lands
it on
dev, the other rebases trivially.Test plan
npm run buildclean (vue-tsc + vite) — passes locally.fiat_providers=['stripe']:sections;
Price currency=sat+ toggle "Also accept fiat"on →
Fiat currencydropdown appears and is required.Price currencytoUSDhides theFiat currencydropdown; submit payload containsfiat_currency: 'USD'.is disabled with the setup-instructions tooltip.
stripeandsquareenabled,viewing a
currency=USD, allow_fiat=trueevent: purchasedialog shows Lightning (with "≈ N sats" badge) + one Stripe
button + one Square button; clicking each forwards
payment_method: 'fiat', fiat_provider: 'stripe'/'square'.currency=sat, allow_fiat=true, fiat_currency=EURevent: headline "N sats" + secondary"Equivalent ~€X.XX EUR if paid in fiat".
correctly; toggling "Also accept fiat" off and saving sends
allow_fiat: false.🤖 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>Smoke-tested locally and caught two bugs — pushed
b7b5a08+663e32e.Bug 1 —
FiatToggleFieldnot bound to parent form. First version calleduseField()directly inside the child component with a reactive name getter; that created a child-local field instead of connecting to the parent form'sallow_fiatstate. The Switch visually toggled butform.values.allow_fiatnever changed, soshowCurrencyDropdownstayed false. Final structure (commit663e32e) wraps everything in the outer<FormField name="allow_fiat">and readsallowFiatfrom its scoped slot — slot bindings are reactive by construction, so nouseFormContextindirection.Bug 2 —
'sat'vs'sats'normalization. Pre-existing inconsistency I hit by introducing thecurrency === 'sat'conditional.TicketApiService.getCurrencies()returns'sats'(plural) but the schema/initialValues/comparisons in this codebase use'sat'(singular). As soon as the user picked from the populated price-currency dropdown,form.values.currencybecame'sats', the conditional failed, and the Fiat currency dropdown stayed hidden even with the toggle on. Normalized to accept both spellings everywhere the new code paths read it:FiatToggleField.isSatDenomination(d)helper drives both thev-showand the auto-mirror watch.superRefineaccepts both on the require-fiat_currencybranch.isPriceInSatscomputed drives the Lightning-sats badge, the<PriceConversionPreview>render condition, AND the inverse conversion watcher's bail-out.End-to-end standardization on one spelling tracked as a follow-up in #70 — left out of this PR to keep the fix narrow.