From 991660405d75082ac4fd6cbe80c4777934026e4e Mon Sep 17 00:00:00 2001 From: Barkley Trimsworth Date: Tue, 31 Mar 2026 17:43:00 +0000 Subject: [PATCH] fix(portal): prevent Dashboard redirect during impersonation session load When navigating to /?sessionId=xxx, Dashboard would immediately redirect to /login because sessionId was null before the fetch completed. The impersonation banner never rendered. Add isImpersonating state: true while impersonation fetch is in-flight, prevents Dashboard from redirecting until session loads. Co-Authored-By: Paperclip --- apps/web/src/portal/CustomerPortal.tsx | 8 ++++++-- apps/web/src/portal/sections/Dashboard.tsx | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/web/src/portal/CustomerPortal.tsx b/apps/web/src/portal/CustomerPortal.tsx index 2a5d424..2547664 100644 --- a/apps/web/src/portal/CustomerPortal.tsx +++ b/apps/web/src/portal/CustomerPortal.tsx @@ -42,6 +42,9 @@ export function CustomerPortal() { // Track whether we've attempted to fetch a session — used to prevent premature redirect // when a session fetch is in-flight (E2E mocks resolve synchronously, batched with setInitComplete) const [sessionAttempted, setSessionAttempted] = useState(false); + // Track whether an impersonation session fetch from URL param is in-flight + // Dashboard will not redirect while this is true, allowing the session to load + const [isImpersonating, setIsImpersonating] = useState(false); const { branding } = useBranding(); const [searchParams, setSearchParams] = useSearchParams(); @@ -54,6 +57,7 @@ export function CustomerPortal() { const sessionId = searchParams.get("sessionId"); if (sessionId) { + setIsImpersonating(true); // Real impersonation session from URL param fetch(`/api/impersonation/sessions/${sessionId}`) .then((r) => { @@ -75,7 +79,7 @@ export function CustomerPortal() { setSessionAttempted(true); setSearchParams({}, { replace: true }); }) - .finally(() => setInitComplete(true)); + .finally(() => { setInitComplete(true); setIsImpersonating(false); }); return; } @@ -162,7 +166,7 @@ export function CustomerPortal() { const sessionId = session?.id ?? null; switch (activeSection) { case "dashboard": - return ; + return ; case "appointments": return ; case "pets": diff --git a/apps/web/src/portal/sections/Dashboard.tsx b/apps/web/src/portal/sections/Dashboard.tsx index f252e8a..bf6f0e4 100644 --- a/apps/web/src/portal/sections/Dashboard.tsx +++ b/apps/web/src/portal/sections/Dashboard.tsx @@ -8,6 +8,8 @@ interface DashboardProps { onNavigate: (section: "appointments" | "pets" | "billing" | "reports") => void; readOnly: boolean; onReschedule: (appointmentId: string) => void; + /** True when a sessionId param was in the URL and the session is still loading */ + isImpersonating?: boolean; } interface Appointment { @@ -73,6 +75,7 @@ export function Dashboard({ onNavigate, readOnly, onReschedule, + isImpersonating, }: DashboardProps) { const [appointments, setAppointments] = useState([]); const [pets, setPets] = useState([]); @@ -183,7 +186,7 @@ export function Dashboard({ ); } - if (!sessionId) { + if (!sessionId && !isImpersonating) { return ; }