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 66a7bf9c3..7d9cd691a 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 @@ -93,23 +93,16 @@ export default function ConfirmationPage() { data: _booking, refetch: refetchBooking, isLoading: isBookingLoading, + isError: isBookingError, } = useQuery({ queryKey: ["booking", bookingId], - queryFn: async (): Promise => { - try { - return await apiClient.get(`/bookings/${bookingId}`); - } catch (error) { - return { - id: bookingId || "", - pnr: pnr || undefined, - status: "PENDING_PAYMENT", - totalMinor: passengers.reduce( - (sum) => sum + (selectedSchedule?.baseFareAdult || 0), - 0, - ), - }; - } - }, + // Let a real fetch failure surface as a real error (React Query's global retry:1 + // default then retries once automatically) instead of silently returning a + // fabricated "PENDING_PAYMENT" object — that used to mask genuine failures (a + // transient blip right after a cross-domain redirect from the payment gateway is + // common) as normal pending state forever, since a caught error that returns data + // looks like a success to React Query and never gets retried. + queryFn: (): Promise => apiClient.get(`/bookings/${bookingId}`), // Payment status must never be served from a stale cache — the app-wide default // (providers.tsx) is a 60s staleTime, which would otherwise block React Query's // own refetch-on-window-focus from firing (it only refetches stale data). Without @@ -333,6 +326,30 @@ export default function ConfirmationPage() { ); } + // A real fetch failure (not just "still pending") — surface it honestly instead of + // silently pretending the booking is pending, and let the user retry the check + // without needing a full page refresh. + if (isBookingError) { + return ( +
+
+
+

+ We're having trouble loading your booking status right + now. This is usually temporary — tap below to try again. +

+ +
+
+
+ ); + } + return (
diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/dmoney/success/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/dmoney/success/page.tsx index cf132bc7f..c63cea9aa 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/dmoney/success/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/dmoney/success/page.tsx @@ -55,13 +55,20 @@ function DmoneySuccessContent() { // Manage Booking sessions don't carry a bookingId in the client store — the detail page // it lands on re-fetches the booking's real status itself, so there's nothing to verify // client-side here; just hand off without claiming an outcome we can't confirm. - if (!bookingId) { - if (!cancelled) { - router.push(target); - } + if (manageBookingRef) { + router.push(target); return; } + // Normal booking flow: bookingId comes from a Zustand store persisted to localStorage. + // This page is always reached via a real cross-domain redirect from the payment gateway + // (a full page load, not an in-app navigation), so that store has to rehydrate from + // localStorage asynchronously — bookingId can read as empty on the first render or two. + // Wait for it instead of treating an empty first-render value as "nothing to verify", + // which would silently skip this page's whole verification step and hand off to + // /booking/confirmation without ever having checked payment status here. + if (!bookingId) return; + verifyBookingPaid(bookingId).then((result) => { if (cancelled) return; if (result === 'SUCCEEDED') { @@ -82,8 +89,7 @@ function DmoneySuccessContent() { return () => { cancelled = true; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [bookingId, router, updateStatus]); return (
diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/success/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/success/page.tsx index a47fdd07c..94d66121b 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/success/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/telebirr/success/page.tsx @@ -55,13 +55,20 @@ function TelebirrSuccessContent() { // Manage Booking sessions don't carry a bookingId in the client store — the detail page // it lands on re-fetches the booking's real status itself, so there's nothing to verify // client-side here; just hand off without claiming an outcome we can't confirm. - if (!bookingId) { - if (!cancelled) { - router.push(target); - } + if (manageBookingRef) { + router.push(target); return; } + // Normal booking flow: bookingId comes from a Zustand store persisted to localStorage. + // This page is always reached via a real cross-domain redirect from the payment gateway + // (a full page load, not an in-app navigation), so that store has to rehydrate from + // localStorage asynchronously — bookingId can read as empty on the first render or two. + // Wait for it instead of treating an empty first-render value as "nothing to verify", + // which would silently skip this page's whole verification step and hand off to + // /booking/confirmation without ever having checked payment status here. + if (!bookingId) return; + verifyBookingPaid(bookingId).then((result) => { if (cancelled) return; if (result === 'SUCCEEDED') { @@ -82,8 +89,7 @@ function TelebirrSuccessContent() { return () => { cancelled = true; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [bookingId, router, updateStatus]); return (
diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/waafi/success/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/waafi/success/page.tsx index 89a8b3098..a33eaec41 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/waafi/success/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/waafi/success/page.tsx @@ -55,13 +55,20 @@ function WaafiSuccessContent() { // Manage Booking sessions don't carry a bookingId in the client store — the detail page // it lands on re-fetches the booking's real status itself, so there's nothing to verify // client-side here; just hand off without claiming an outcome we can't confirm. - if (!bookingId) { - if (!cancelled) { - router.push(target); - } + if (manageBookingRef) { + router.push(target); return; } + // Normal booking flow: bookingId comes from a Zustand store persisted to localStorage. + // This page is always reached via a real cross-domain redirect from the payment gateway + // (a full page load, not an in-app navigation), so that store has to rehydrate from + // localStorage asynchronously — bookingId can read as empty on the first render or two. + // Wait for it instead of treating an empty first-render value as "nothing to verify", + // which would silently skip this page's whole verification step and hand off to + // /booking/confirmation without ever having checked payment status here. + if (!bookingId) return; + verifyBookingPaid(bookingId).then((result) => { if (cancelled) return; if (result === 'SUCCEEDED') { @@ -82,8 +89,7 @@ function WaafiSuccessContent() { return () => { cancelled = true; }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [bookingId, router, updateStatus]); return (