From 61665790b348371b16e6e7060542c09916167c6b Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 23 May 2026 18:37:11 +0200 Subject: [PATCH] refactor(activities): adopt shared payment-rails pattern in CreateEventDialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) ← 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) --- .../components/CreateEventDialog.vue | 183 +++++++++--------- 1 file changed, 94 insertions(+), 89 deletions(-) diff --git a/src/modules/activities/components/CreateEventDialog.vue b/src/modules/activities/components/CreateEventDialog.vue index c2c1f45..5f9d45b 100644 --- a/src/modules/activities/components/CreateEventDialog.vue +++ b/src/modules/activities/components/CreateEventDialog.vue @@ -24,7 +24,6 @@ import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { Switch } from '@/components/ui/switch' import { ScrollArea } from '@/components/ui/scroll-area' import { Select, @@ -33,12 +32,13 @@ import { SelectTrigger, SelectValue, } 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 { injectService, SERVICE_TOKENS } from '@/core/di-container' import ImageUpload from '@/modules/base/components/ImageUpload.vue' import DatePicker from '@/modules/base/components/DatePicker.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 type { ImageUploadService, UploadedImage } from '@/modules/base/services/ImageUploadService' import type { TicketApiService } from '../services/TicketApiService' @@ -129,14 +129,25 @@ const formSchema = toTypedSchema( .superRefine((v, ctx) => { // End must not precede start. Compare on the folded date+time // string so equal-date / later-time is enforced too. - if (!v.event_end_date) return - const start = foldDateTime(v.event_start_date, v.event_start_time) - const end = foldDateTime(v.event_end_date, v.event_end_time) - if (start && end && end < start) { + if (v.event_end_date) { + const start = foldDateTime(v.event_start_date, v.event_start_time) + const end = foldDateTime(v.event_end_date, v.event_end_time) + if (start && end && end < start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['event_end_date'], + 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: ['event_end_date'], - message: 'End must be on or after start', + 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 } if (formValues.currency) eventData.currency = formValues.currency - // allow_fiat / fiat_currency: always send so the toggle reads - // both directions on edit (a true→false flip must propagate). + // allow_fiat always sends so a true→false flip propagates on edit; + // 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 - 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.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket if (selectedCategories.value.length > 0) eventData.categories = selectedCategories.value @@ -558,88 +572,79 @@ const handleOpenChange = (open: boolean) => { /> - -
- - - Tickets - - - - 0 = unlimited - - - + +
+
+

Pricing

+

+ Set what buyers see. Lightning charges happen in sats; + fiat amounts convert at checkout using current rates. +

+
+
+ + + Tickets + + + + 0 = unlimited + + + - - - Price - - - - 0 = free - - - + + + Price + + + + 0 = free + + + - - - Currency - - - - - - + + + Price currency + + + + + + +
- -
- - -
- Accept fiat payments - - Buyers can pay with the LNbits instance's configured fiat provider (e.g. Stripe). - -
- - - -
-
- - - - Fiat currency - - - - - - + +
+
+

Payment methods

+

+ Lightning is always available. Enable fiat to also accept + card and bank payments through your configured provider. +

+
+
+ + Lightning — always on +
+