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>
This commit is contained in:
parent
caec8eddcc
commit
985c10939d
1 changed files with 96 additions and 91 deletions
|
|
@ -24,7 +24,6 @@ import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -33,12 +32,13 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Calendar, Loader2, MapPin, AlertCircle } from 'lucide-vue-next'
|
import { Calendar, Loader2, MapPin, AlertCircle, Zap } from 'lucide-vue-next'
|
||||||
import { toastService } from '@/core/services/ToastService'
|
import { toastService } from '@/core/services/ToastService'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import ImageUpload from '@/modules/base/components/ImageUpload.vue'
|
import ImageUpload from '@/modules/base/components/ImageUpload.vue'
|
||||||
import DatePicker from '@/modules/base/components/DatePicker.vue'
|
import DatePicker from '@/modules/base/components/DatePicker.vue'
|
||||||
import TimePicker from '@/modules/base/components/TimePicker.vue'
|
import TimePicker from '@/modules/base/components/TimePicker.vue'
|
||||||
|
import FiatToggleField from '@/modules/base/components/payments/FiatToggleField.vue'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import type { ImageUploadService, UploadedImage } from '@/modules/base/services/ImageUploadService'
|
import type { ImageUploadService, UploadedImage } from '@/modules/base/services/ImageUploadService'
|
||||||
import type { TicketApiService } from '../services/TicketApiService'
|
import type { TicketApiService } from '../services/TicketApiService'
|
||||||
|
|
@ -129,7 +129,7 @@ const formSchema = toTypedSchema(
|
||||||
.superRefine((v, ctx) => {
|
.superRefine((v, ctx) => {
|
||||||
// End must not precede start. Compare on the folded date+time
|
// End must not precede start. Compare on the folded date+time
|
||||||
// string so equal-date / later-time is enforced too.
|
// string so equal-date / later-time is enforced too.
|
||||||
if (!v.event_end_date) return
|
if (v.event_end_date) {
|
||||||
const start = foldDateTime(v.event_start_date, v.event_start_time)
|
const start = foldDateTime(v.event_start_date, v.event_start_time)
|
||||||
const end = foldDateTime(v.event_end_date, v.event_end_time)
|
const end = foldDateTime(v.event_end_date, v.event_end_time)
|
||||||
if (start && end && end < start) {
|
if (start && end && end < start) {
|
||||||
|
|
@ -139,6 +139,17 @@ const formSchema = toTypedSchema(
|
||||||
message: 'End must be on or after start',
|
message: 'End must be on or after start',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// When the price is in sats and the organizer also accepts fiat,
|
||||||
|
// they MUST choose a settle currency. Other price denominations
|
||||||
|
// mirror themselves into fiat_currency automatically.
|
||||||
|
if (v.allow_fiat && v.currency === 'sat' && !v.fiat_currency) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ['fiat_currency'],
|
||||||
|
message: 'Pick a fiat currency for buyers paying by card',
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -325,10 +336,13 @@ const onSubmit = form.handleSubmit(async (formValues) => {
|
||||||
eventData.banner = null
|
eventData.banner = null
|
||||||
}
|
}
|
||||||
if (formValues.currency) eventData.currency = formValues.currency
|
if (formValues.currency) eventData.currency = formValues.currency
|
||||||
// allow_fiat / fiat_currency: always send so the toggle reads
|
// allow_fiat always sends so a true→false flip propagates on edit;
|
||||||
// both directions on edit (a true→false flip must propagate).
|
// fiat_currency only sends when fiat is on (no point persisting a
|
||||||
|
// rail-currency the backend won't use).
|
||||||
eventData.allow_fiat = formValues.allow_fiat
|
eventData.allow_fiat = formValues.allow_fiat
|
||||||
if (formValues.fiat_currency) eventData.fiat_currency = formValues.fiat_currency
|
if (formValues.allow_fiat && formValues.fiat_currency) {
|
||||||
|
eventData.fiat_currency = formValues.fiat_currency
|
||||||
|
}
|
||||||
if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets
|
if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets
|
||||||
if (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket
|
if (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket
|
||||||
if (selectedCategories.value.length > 0) eventData.categories = selectedCategories.value
|
if (selectedCategories.value.length > 0) eventData.categories = selectedCategories.value
|
||||||
|
|
@ -558,7 +572,15 @@ const handleOpenChange = (open: boolean) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tickets (optional, visible) -->
|
<!-- ── Pricing ──────────────────────────────────────────── -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">Pricing</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Set what buyers see. Lightning charges happen in sats;
|
||||||
|
fiat amounts convert at checkout using current rates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-3">
|
<div class="grid grid-cols-3 gap-3">
|
||||||
<FormField v-slot="{ componentField }" name="amount_tickets">
|
<FormField v-slot="{ componentField }" name="amount_tickets">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
|
|
@ -584,7 +606,7 @@ const handleOpenChange = (open: boolean) => {
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="currency">
|
<FormField v-slot="{ componentField }" name="currency">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Currency</FormLabel>
|
<FormLabel>Price currency</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select v-bind="componentField">
|
<Select v-bind="componentField">
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -601,45 +623,28 @@ const handleOpenChange = (open: boolean) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fiat checkout (organizer opts in). Backend requires the
|
|
||||||
host's LNbits admin to have configured a fiat provider
|
|
||||||
(Stripe etc.) under settings.fiat_providers; toggling
|
|
||||||
this on without one will fail at purchase time. -->
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end">
|
|
||||||
<FormField v-slot="{ value, handleChange }" name="allow_fiat">
|
|
||||||
<FormItem class="sm:col-span-2 flex flex-row items-center justify-between rounded-md border p-3">
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
<FormLabel>Accept fiat payments</FormLabel>
|
|
||||||
<FormDescription class="text-xs">
|
|
||||||
Buyers can pay with the LNbits instance's configured fiat provider (e.g. Stripe).
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
|
||||||
<Switch :model-value="value as boolean" @update:model-value="handleChange" />
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="fiat_currency">
|
<!-- ── Payment methods ──────────────────────────────────── -->
|
||||||
<FormItem v-show="form.values.allow_fiat">
|
<div class="space-y-3">
|
||||||
<FormLabel>Fiat currency</FormLabel>
|
<div>
|
||||||
<FormControl>
|
<p class="text-sm font-medium">Payment methods</p>
|
||||||
<Select v-bind="componentField">
|
<p class="text-xs text-muted-foreground">
|
||||||
<SelectTrigger>
|
Lightning is always available. Enable fiat to also accept
|
||||||
<SelectValue placeholder="USD" />
|
card and bank payments through your configured provider.
|
||||||
</SelectTrigger>
|
</p>
|
||||||
<SelectContent>
|
</div>
|
||||||
<SelectItem value="USD">USD</SelectItem>
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<SelectItem value="EUR">EUR</SelectItem>
|
<Zap class="w-4 h-4" />
|
||||||
<SelectItem value="GBP">GBP</SelectItem>
|
<span>Lightning — always on</span>
|
||||||
<SelectItem value="CHF">CHF</SelectItem>
|
</div>
|
||||||
</SelectContent>
|
<FiatToggleField
|
||||||
</Select>
|
allow-fiat-field="allow_fiat"
|
||||||
</FormControl>
|
fiat-currency-field="fiat_currency"
|
||||||
<FormMessage />
|
:denomination="form.values.currency ?? 'sat'"
|
||||||
</FormItem>
|
:available-fiat-currencies="availableCurrencies.filter(c => c !== 'sat')"
|
||||||
</FormField>
|
:disabled="isLoading"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue