diff --git a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx index b10109c0f..c48e30177 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx @@ -68,7 +68,7 @@ export default function ConfirmationPage() { import("@/lib/generate-voucher"); }, []); - const { data: _booking } = useQuery({ + const { data: _booking, refetch: refetchBooking } = useQuery({ queryKey: ["booking", bookingId], queryFn: async (): Promise => { try { @@ -88,6 +88,23 @@ export default function ConfirmationPage() { enabled: !!bookingId, }); + // Poll the payment intent every 10 s while the booking is PENDING_PAYMENT. + // The backend auto-confirms (and generates tickets) when the payment-api reports + // SUCCEEDED, so detecting that here means the booking is now CONFIRMED — refetch + // to update the UI without requiring the user to refresh. + const { data: intentStatus } = useQuery({ + queryKey: ["payment-intent-status", bookingId], + queryFn: () => apiClient.get(`/payments/intents/${bookingId}`), + enabled: _booking?.status === "PENDING_PAYMENT" && !!bookingId, + refetchInterval: 10_000, + }); + + useEffect(() => { + if (intentStatus?.status === "SUCCEEDED") { + refetchBooking(); + } + }, [intentStatus?.status]); + // Only trust an actually-confirmed booking to show ticket numbers / a "CONFIRMED" badge — // a gateway redirect back here does not mean payment succeeded (see payment return pages). // Ticket generation itself is never triggered from this page — the payment webhook diff --git a/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx index a54ce795b..82d357a28 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx @@ -125,6 +125,22 @@ function BookingDetailContent() { booking?.status === "PENDING_PAYMENT" || booking?.status === "DRAFT", }); + // When the booking is PENDING_PAYMENT, poll the payment intent endpoint every 10 s. + // The backend auto-confirms the booking when it finds a SUCCEEDED intent, so detecting + // SUCCEEDED here means the booking is now CONFIRMED — refetch to update the UI. + const { data: intentStatus } = useQuery({ + queryKey: ["payment-intent-status", booking?.id], + queryFn: () => apiClient.get(`/payments/intents/${booking!.id}`), + enabled: booking?.status === "PENDING_PAYMENT" && !!booking?.id, + refetchInterval: 10_000, + }); + + useEffect(() => { + if (intentStatus?.status === "SUCCEEDED") { + refetch(); + } + }, [intentStatus?.status]); + const selectedPaymentMethod = (paymentMethods || []).find((m: any) => m.type === selectedMethod) || null;