fix(portal): prevent premature redirect with sessionAttempted flag

Fixes E2E race condition where setSession and setInitComplete are batched
by React concurrent rendering, causing redirect to fire before session
is set. The sessionAttempted flag tracks "did we try" so redirect only
fires when there was NO attempt, not when the state update is pending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
groombook-engineer[bot]
2026-03-31 16:54:22 +00:00
committed by GitHub
parent 7443b66739
commit 49aa6ac989
+13 -4
View File
@@ -39,6 +39,9 @@ export function CustomerPortal() {
const [sessionExtended, setSessionExtended] = useState(false);
const [clientName, setClientName] = useState<string>("");
const [initComplete, setInitComplete] = useState(false);
// 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);
const { branding } = useBranding();
const [searchParams, setSearchParams] = useSearchParams();
@@ -60,6 +63,7 @@ export function CustomerPortal() {
.then((s) => {
if (s && s.status === "active") {
setSession(s);
setSessionAttempted(true);
fetch(`/api/portal/me`, { headers: { "X-Impersonation-Session-Id": s.id } })
.then(r => r.ok ? r.json() : null)
.then(data => { if (data?.name) setClientName(data.name); })
@@ -68,6 +72,7 @@ export function CustomerPortal() {
setSearchParams({}, { replace: true });
})
.catch(() => {
setSessionAttempted(true);
setSearchParams({}, { replace: true });
})
.finally(() => setInitComplete(true));
@@ -89,10 +94,11 @@ export function CustomerPortal() {
.then((s) => {
if (s && s.id) {
setSession(s);
setSessionAttempted(true);
setClientName(devUser.name);
}
})
.catch(() => {})
.catch(() => { setSessionAttempted(true); })
.finally(() => setInitComplete(true));
} else {
// No valid session: staff dev users and unauthenticated users fall through here
@@ -175,8 +181,11 @@ export function CustomerPortal() {
const avatarInitials = (clientName.split(" ")[0] || "G").charAt(0).toUpperCase();
// After init completes, redirect unauthenticated users to /login and staff to /admin
// The portal chrome must NEVER be visible to users without a valid client session
if (initComplete && !session) {
// The portal chrome must NEVER be visible to users without a valid client session.
// Only redirect if we have NOT attempted a session fetch yet — if a fetch is in-flight
// (E2E mock resolves synchronously, batched with setInitComplete), sessionAttempted
// is still false so we don't redirect prematurely.
if (initComplete && !session && !sessionAttempted) {
const devUser = getDevUser();
if (devUser && devUser.type === "staff") {
return <Navigate to="/admin" replace />;
@@ -251,7 +260,7 @@ export function CustomerPortal() {
<div className="hidden md:flex items-center gap-3 px-6 py-5 border-b border-stone-100">
{branding.logoBase64 && branding.logoMimeType ? (
<img
src={`data:${branding.logoMimeType};base64,${branding.logoBase64}`}
src={`data:${branding.logoMimeType};base64,${branding.logoBase64}}`}
alt=""
className="w-10 h-10 rounded-xl object-contain"
/>