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>
This commit is contained in:
Padreug 2026-05-23 19:44:43 +02:00
commit 663e32e7a4
3 changed files with 53 additions and 44 deletions

View file

@ -142,8 +142,10 @@ const formSchema = toTypedSchema(
} }
// When the price is in sats and the organizer also accepts fiat, // When the price is in sats and the organizer also accepts fiat,
// they MUST choose a settle currency. Other price denominations // they MUST choose a settle currency. Other price denominations
// mirror themselves into fiat_currency automatically. // mirror themselves into fiat_currency automatically. The events
if (v.allow_fiat && v.currency === 'sat' && !v.fiat_currency) { // extension uses 'sat' and 'sats' interchangeably accept both.
const isSat = v.currency === 'sat' || v.currency === 'sats'
if (v.allow_fiat && isSat && !v.fiat_currency) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
path: ['fiat_currency'], path: ['fiat_currency'],
@ -642,7 +644,7 @@ const handleOpenChange = (open: boolean) => {
allow-fiat-field="allow_fiat" allow-fiat-field="allow_fiat"
fiat-currency-field="fiat_currency" fiat-currency-field="fiat_currency"
:denomination="form.values.currency ?? 'sat'" :denomination="form.values.currency ?? 'sat'"
:available-fiat-currencies="availableCurrencies.filter(c => c !== 'sat')" :available-fiat-currencies="availableCurrencies.filter(c => c !== 'sat' && c !== 'sats')"
:disabled="isLoading" :disabled="isLoading"
/> />
</div> </div>

View file

@ -66,6 +66,9 @@ const isFiatPending = ref(false)
const fiatError = ref<string | null>(null) const fiatError = ref<string | null>(null)
const canChooseFiat = computed(() => Boolean(props.event.allow_fiat)) const canChooseFiat = computed(() => Boolean(props.event.allow_fiat))
const isPriceInSats = computed(
() => props.event.currency === 'sat' || props.event.currency === 'sats',
)
// Lightning-button badge: when the price is denominated in fiat, show // Lightning-button badge: when the price is denominated in fiat, show
// the live sat equivalent so the buyer knows roughly what their wallet // the live sat equivalent so the buyer knows roughly what their wallet
@ -74,7 +77,7 @@ const lightningSats = ref<number | null>(null)
watch( watch(
() => [props.event.currency, props.event.price_per_ticket, props.isOpen] as const, () => [props.event.currency, props.event.price_per_ticket, props.isOpen] as const,
async ([cur, amt, open]) => { async ([cur, amt, open]) => {
if (!open || cur === 'sat' || !amt) { if (!open || !amt || cur === 'sat' || cur === 'sats') {
lightningSats.value = null lightningSats.value = null
return return
} }
@ -97,7 +100,7 @@ const paymentMethods = computed<PaymentMethodEntry[]>(() => {
icon: Zap, icon: Zap,
available: true, available: true,
badge: badge:
props.event.currency !== 'sat' && lightningSats.value !isPriceInSats.value && lightningSats.value
? `${Math.round(lightningSats.value).toLocaleString()} sats` ? `${Math.round(lightningSats.value).toLocaleString()} sats`
: undefined, : undefined,
} }
@ -304,7 +307,7 @@ onUnmounted(() => {
<span class="text-sm font-medium">{{ formatEventPrice(event.price_per_ticket, event.currency) }}</span> <span class="text-sm font-medium">{{ formatEventPrice(event.price_per_ticket, event.currency) }}</span>
</div> </div>
<PriceConversionPreview <PriceConversionPreview
v-if="canChooseFiat && event.currency === 'sat' && event.fiat_currency" v-if="canChooseFiat && isPriceInSats && event.fiat_currency"
:amount="event.price_per_ticket" :amount="event.price_per_ticket"
from="sat" from="sat"
:to="event.fiat_currency" :to="event.fiat_currency"

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch } from 'vue' import { watch } from 'vue'
import { useFormContext } from 'vee-validate' import { useFormContext } from 'vee-validate'
import { import {
FormControl, FormControl,
@ -38,27 +38,31 @@ const props = defineProps<{
disabled?: boolean disabled?: boolean
}>() }>()
const form = useFormContext()
const { hasAnyProvider, refresh } = useFiatProviders() const { hasAnyProvider, refresh } = useFiatProviders()
const form = useFormContext()
// Refresh once on mount so the disabled-state reflects providers the // Refresh once on mount so the disabled-state reflects providers the
// user may have just configured in another tab. // user may have just configured in another tab.
refresh() refresh()
const allowFiatValue = computed(() => // "sat" / "sats" appear interchangeably across the LNbits events
Boolean(form.values[props.allowFiatField as keyof typeof form.values]), // extension and the webapp's currency lists treat both as the
) // BTC-denominated case for the conditional + auto-mirror.
function isSatDenomination(d: string): boolean {
const showCurrencyDropdown = computed( return d === 'sat' || d === 'sats'
() => allowFiatValue.value && props.denomination === 'sat', }
)
// When the price is denominated in a fiat currency, the rail currency // When the price is denominated in a fiat currency, the rail currency
// MUST match it silently mirror so backend payload stays consistent. // MUST match it silently mirror so backend payload stays consistent.
watch( watch(
() => props.denomination, () => props.denomination,
(d) => { (d) => {
if (d && d !== 'sat' && form.values[props.fiatCurrencyField as keyof typeof form.values] !== d) { if (!form) return
if (
d &&
!isSatDenomination(d) &&
form.values[props.fiatCurrencyField as keyof typeof form.values] !== d
) {
form.setFieldValue(props.fiatCurrencyField, d) form.setFieldValue(props.fiatCurrencyField, d)
} }
}, },
@ -67,8 +71,8 @@ watch(
</script> </script>
<template> <template>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end"> <FormField v-slot="{ value: allowFiat, handleChange: setAllowFiat }" :name="allowFiatField">
<FormField v-slot="{ value, handleChange }" :name="allowFiatField"> <div class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end">
<FormItem class="sm:col-span-2 flex flex-row items-center justify-between rounded-md border p-3"> <FormItem class="sm:col-span-2 flex flex-row items-center justify-between rounded-md border p-3">
<div class="space-y-0.5"> <div class="space-y-0.5">
<FormLabel>Also accept fiat</FormLabel> <FormLabel>Also accept fiat</FormLabel>
@ -93,35 +97,35 @@ watch(
</TooltipProvider> </TooltipProvider>
<Switch <Switch
v-else v-else
:model-value="value as boolean" :model-value="allowFiat as boolean"
:disabled="disabled" :disabled="disabled"
@update:model-value="handleChange" @update:model-value="setAllowFiat"
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>
</FormField>
<FormField v-slot="{ componentField }" :name="fiatCurrencyField"> <FormField v-slot="{ componentField }" :name="fiatCurrencyField">
<FormItem v-show="showCurrencyDropdown"> <FormItem v-show="(allowFiat as boolean) && isSatDenomination(denomination)">
<FormLabel>Fiat currency</FormLabel> <FormLabel>Fiat currency</FormLabel>
<FormControl> <FormControl>
<Select v-bind="componentField" :disabled="disabled"> <Select v-bind="componentField" :disabled="disabled">
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="USD" /> <SelectValue placeholder="USD" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem <SelectItem
v-for="c in availableFiatCurrencies" v-for="c in availableFiatCurrencies"
:key="c" :key="c"
:value="c" :value="c"
> >
{{ c }} {{ c }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
</div> </div>
</FormField>
</template> </template>