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:
committed by
GitHub
parent
7443b66739
commit
49aa6ac989
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user