From 891b5072f8e74d0da737168245ebbb9b56a54c84 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 6 Sep 2026 19:35:45 +0200 Subject: [PATCH] feat(events): guest checkout in the purchase dialog with delivery email Buyers no longer need an account to buy a paid ticket. The dialog gains a "Your details" block (name for guests, email + confirm-email for everyone, prefilled from the account email) as a vee-validate/zod form, and builds its rail buttons from the event's published payment methods so an organizer can run card-only events. - Fiat: same-tab redirect to the provider right after the POST, with the pending checkout (ticket ids, email) remembered in localStorage for the return page; the two-step "open in new tab" panel is gone. - Lightning: guests get the bolt11 QR + "Open in wallet" and settle via the existing poll; the LNbits-wallet button stays login-only. - Free tickets remain login-gated (events#29). - Guest success state renders the ticket QRs inline with the "emailed to" note, since guests have no My Tickets. i18n keys added under events.purchase / checkout / ticket (en, es, fr). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo --- src/i18n/locales/en.ts | 45 ++ src/i18n/locales/es.ts | 45 ++ src/i18n/locales/fr.ts | 45 ++ src/i18n/types.ts | 45 ++ .../components/PurchaseTicketDialog.vue | 419 ++++++++++++------ src/modules/events/lib/pendingCheckout.ts | 38 ++ 6 files changed, 498 insertions(+), 139 deletions(-) create mode 100644 src/modules/events/lib/pendingCheckout.ts diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index dbb408c..b718299 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -129,6 +129,51 @@ const messages: LocaleMessages = { pending: 'Pending', registered: 'Registered', }, + purchase: { + card: 'Card', + continueTo: 'Continue to {provider} checkout', + fiatRedirectHint: "You'll be taken to the provider's secure checkout and brought back here once paid.", + noCheckoutUrl: 'Fiat provider did not return a checkout URL.', + fiatFailed: 'Card checkout failed', + loginRequiredTitle: 'Login required', + loginRequiredFree: 'Free tickets need an account so seats cannot be claimed anonymously. Log in to get yours.', + contact: { + title: 'Your details', + name: 'Name', + namePlaceholder: 'Name on the ticket', + nameRequired: 'Name is required', + email: 'Email', + emailHint: 'Your ticket will be sent to this address.', + emailInvalid: 'Enter a valid email address', + confirmEmail: 'Confirm email', + emailMismatch: 'Email addresses do not match', + }, + }, + checkout: { + title: 'Confirming your payment…', + paidTitle: 'Payment received', + processing: 'Waiting for the payment to settle. This usually takes a few seconds.', + stillProcessing: 'Still processing. Your ticket will be emailed to {email} as soon as the payment settles.', + stillProcessingNoEmail: 'Still processing. Reload this page in a moment to check again.', + emailedTo: 'We emailed your ticket to {email}.', + cancelled: 'Payment cancelled — no seats were reserved.', + viewTicket: 'Open ticket page', + viewMyTickets: 'View My Tickets', + ticketN: 'Ticket {n} of {total}', + close: 'Close', + }, + ticket: { + title: 'Your ticket', + loading: 'Loading ticket…', + notFound: 'Ticket not found', + event: 'Event', + holder: 'Name', + paid: 'Paid', + pending: 'Payment pending', + registered: 'Checked in', + showAtDoor: 'Show this QR at the door.', + backToEvent: 'Back to event', + }, nav: { feed: 'Home', calendar: 'Calendar', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 9ca8958..7410846 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -129,6 +129,51 @@ const messages: LocaleMessages = { pending: 'Pendiente', registered: 'Registrado', }, + purchase: { + card: 'Tarjeta', + continueTo: 'Continuar al pago con {provider}', + fiatRedirectHint: 'Te llevaremos al pago seguro del proveedor y volverás aquí una vez pagado.', + noCheckoutUrl: 'El proveedor no devolvió una URL de pago.', + fiatFailed: 'El pago con tarjeta falló', + loginRequiredTitle: 'Inicio de sesión requerido', + loginRequiredFree: 'Los boletos gratuitos requieren una cuenta para evitar reservas anónimas. Inicia sesión para obtener el tuyo.', + contact: { + title: 'Tus datos', + name: 'Nombre', + namePlaceholder: 'Nombre en el boleto', + nameRequired: 'El nombre es obligatorio', + email: 'Correo electrónico', + emailHint: 'Tu boleto se enviará a esta dirección.', + emailInvalid: 'Introduce un correo electrónico válido', + confirmEmail: 'Confirmar correo', + emailMismatch: 'Los correos no coinciden', + }, + }, + checkout: { + title: 'Confirmando tu pago…', + paidTitle: 'Pago recibido', + processing: 'Esperando a que se liquide el pago. Suele tardar unos segundos.', + stillProcessing: 'Aún en proceso. Tu boleto se enviará a {email} en cuanto se liquide el pago.', + stillProcessingNoEmail: 'Aún en proceso. Recarga esta página en un momento.', + emailedTo: 'Enviamos tu boleto a {email}.', + cancelled: 'Pago cancelado — no se reservó ningún asiento.', + viewTicket: 'Abrir página del boleto', + viewMyTickets: 'Ver mis boletos', + ticketN: 'Boleto {n} de {total}', + close: 'Cerrar', + }, + ticket: { + title: 'Tu boleto', + loading: 'Cargando boleto…', + notFound: 'Boleto no encontrado', + event: 'Evento', + holder: 'Nombre', + paid: 'Pagado', + pending: 'Pago pendiente', + registered: 'Registrado', + showAtDoor: 'Muestra este QR en la entrada.', + backToEvent: 'Volver al evento', + }, nav: { feed: 'Inicio', calendar: 'Calendario', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index ef37aaa..21392ec 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -129,6 +129,51 @@ const messages: LocaleMessages = { pending: 'En attente', registered: 'Enregistré', }, + purchase: { + card: 'Carte', + continueTo: 'Continuer vers le paiement {provider}', + fiatRedirectHint: 'Vous serez redirigé vers le paiement sécurisé du prestataire, puis ramené ici une fois payé.', + noCheckoutUrl: "Le prestataire n'a pas renvoyé d'URL de paiement.", + fiatFailed: 'Le paiement par carte a échoué', + loginRequiredTitle: 'Connexion requise', + loginRequiredFree: 'Les billets gratuits nécessitent un compte pour éviter les réservations anonymes. Connectez-vous pour obtenir le vôtre.', + contact: { + title: 'Vos coordonnées', + name: 'Nom', + namePlaceholder: 'Nom sur le billet', + nameRequired: 'Le nom est obligatoire', + email: 'E-mail', + emailHint: 'Votre billet sera envoyé à cette adresse.', + emailInvalid: 'Saisissez une adresse e-mail valide', + confirmEmail: "Confirmer l'e-mail", + emailMismatch: 'Les adresses e-mail ne correspondent pas', + }, + }, + checkout: { + title: 'Confirmation de votre paiement…', + paidTitle: 'Paiement reçu', + processing: 'En attente du règlement du paiement. Cela prend généralement quelques secondes.', + stillProcessing: 'Toujours en cours. Votre billet sera envoyé à {email} dès que le paiement sera réglé.', + stillProcessingNoEmail: 'Toujours en cours. Rechargez cette page dans un instant.', + emailedTo: 'Nous avons envoyé votre billet à {email}.', + cancelled: 'Paiement annulé — aucune place réservée.', + viewTicket: 'Ouvrir la page du billet', + viewMyTickets: 'Voir mes billets', + ticketN: 'Billet {n} sur {total}', + close: 'Fermer', + }, + ticket: { + title: 'Votre billet', + loading: 'Chargement du billet…', + notFound: 'Billet introuvable', + event: 'Événement', + holder: 'Nom', + paid: 'Payé', + pending: 'Paiement en attente', + registered: 'Enregistré', + showAtDoor: "Présentez ce QR à l'entrée.", + backToEvent: "Retour à l'événement", + }, nav: { feed: 'Accueil', calendar: 'Calendrier', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index ccce8f7..61a8f32 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -104,6 +104,51 @@ export interface LocaleMessages { pending: string registered: string } + purchase: { + card: string + continueTo: string + fiatRedirectHint: string + noCheckoutUrl: string + fiatFailed: string + loginRequiredTitle: string + loginRequiredFree: string + contact: { + title: string + name: string + namePlaceholder: string + nameRequired: string + email: string + emailHint: string + emailInvalid: string + confirmEmail: string + emailMismatch: string + } + } + checkout: { + title: string + paidTitle: string + processing: string + stillProcessing: string + stillProcessingNoEmail: string + emailedTo: string + cancelled: string + viewTicket: string + viewMyTickets: string + ticketN: string + close: string + } + ticket: { + title: string + loading: string + notFound: string + event: string + holder: string + paid: string + pending: string + registered: string + showAtDoor: string + backToEvent: string + } nav: { feed: string calendar: string diff --git a/src/modules/events/components/PurchaseTicketDialog.vue b/src/modules/events/components/PurchaseTicketDialog.vue index a9543f1..53eac52 100644 --- a/src/modules/events/components/PurchaseTicketDialog.vue +++ b/src/modules/events/components/PurchaseTicketDialog.vue @@ -1,13 +1,26 @@