diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index b718299..19becc3 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -159,6 +159,7 @@ const messages: LocaleMessages = { cancelled: 'Payment cancelled — no seats were reserved.', viewTicket: 'Open ticket page', viewMyTickets: 'View My Tickets', + retry: 'Check again', ticketN: 'Ticket {n} of {total}', close: 'Close', }, diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 7410846..7bb588b 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -159,6 +159,7 @@ const messages: LocaleMessages = { cancelled: 'Pago cancelado — no se reservó ningún asiento.', viewTicket: 'Abrir página del boleto', viewMyTickets: 'Ver mis boletos', + retry: 'Comprobar de nuevo', ticketN: 'Boleto {n} de {total}', close: 'Cerrar', }, diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 21392ec..28dd564 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -159,6 +159,7 @@ const messages: LocaleMessages = { cancelled: 'Paiement annulé — aucune place réservée.', viewTicket: 'Ouvrir la page du billet', viewMyTickets: 'Voir mes billets', + retry: 'Vérifier à nouveau', ticketN: 'Billet {n} sur {total}', close: 'Fermer', }, diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 61a8f32..1dbb181 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -134,6 +134,7 @@ export interface LocaleMessages { cancelled: string viewTicket: string viewMyTickets: string + retry: string ticketN: string close: string } diff --git a/src/modules/events/components/CheckoutReturnDialog.vue b/src/modules/events/components/CheckoutReturnDialog.vue new file mode 100644 index 0000000..c09571b --- /dev/null +++ b/src/modules/events/components/CheckoutReturnDialog.vue @@ -0,0 +1,182 @@ + + + diff --git a/src/modules/events/views/EventDetailPage.vue b/src/modules/events/views/EventDetailPage.vue index 10fdbc8..ee08ae8 100644 --- a/src/modules/events/views/EventDetailPage.vue +++ b/src/modules/events/views/EventDetailPage.vue @@ -14,6 +14,8 @@ import { useEventDetail } from '../composables/useEventDetail' import BookmarkButton from '../components/BookmarkButton.vue' import OrganizerCard from '../components/OrganizerCard.vue' import PurchaseTicketDialog from '../components/PurchaseTicketDialog.vue' +import CheckoutReturnDialog from '../components/CheckoutReturnDialog.vue' +import { clearPendingCheckout, readPendingCheckout } from '../lib/pendingCheckout' import { useAuth } from '@/composables/useAuthService' import { useEventsStore } from '../stores/events' import { useOwnedTickets } from '../composables/useOwnedTickets' @@ -116,6 +118,7 @@ const purchaseEvent = computed(() => { currency: a.ticketInfo.currency, allow_fiat: a.ticketInfo.allowFiat, fiat_currency: a.ticketInfo.fiatCurrency, + payment_methods: a.ticketInfo.paymentMethods, } }) @@ -142,7 +145,10 @@ const isPast = computed(() => { const showPurchaseDialog = ref(false) function openPurchaseDialog() { - if (!isAuthenticated.value) { + // Paid tickets are open to guests (name + email identity). Free + // claims still need an account so seats can't be drained anonymously. + const isFreeEvent = (purchaseEvent.value?.price_per_ticket ?? 0) <= 0 + if (!isAuthenticated.value && isFreeEvent) { toastService.info(t('events.detail.loginToBuyTickets'), { action: { label: t('events.detail.logIn'), @@ -154,6 +160,43 @@ function openPurchaseDialog() { showPurchaseDialog.value = true } +// --- Return from the fiat provider --------------------------------- +// Stripe sends the buyer back here with ?checkout=success&tickets=a,b +// (ids pre-generated by the extension before the session was created) +// or ?checkout=cancelled. The query is stripped immediately so the +// ticket ids don't linger in history; the localStorage record written +// before the redirect is the fallback source and carries the email. +const showReturnDialog = ref(false) +const returnTicketIds = ref([]) +const returnEmail = ref(undefined) + +function handleCheckoutReturn() { + const status = route.query.checkout + if (status !== 'success' && status !== 'cancelled') return + + const pending = readPendingCheckout() + const samePurchase = pending?.eventId === eventId + void router.replace({ query: {} }) + + if (status === 'cancelled') { + clearPendingCheckout() + toastService.info(t('events.checkout.cancelled')) + return + } + + const fromQuery = String(route.query.tickets ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + const ids = fromQuery.length > 0 ? fromQuery : samePurchase ? pending!.ticketIds : [] + if (ids.length === 0) return + returnTicketIds.value = ids + returnEmail.value = samePurchase ? pending?.email : undefined + showReturnDialog.value = true +} + +onMounted(handleCheckoutReturn) + // Re-fetch the user's tickets when the purchase dialog closes (the // buyer may have just paid). The inventory side updates automatically // via the relay republish from the events extension. @@ -366,6 +409,14 @@ function goToMyTickets() { @update:is-open="showPurchaseDialog = $event" /> + +