diff --git a/src/adapters/auth/signOut.ts b/src/adapters/auth/signOut.ts index 001d9f73..d478b13b 100644 --- a/src/adapters/auth/signOut.ts +++ b/src/adapters/auth/signOut.ts @@ -5,15 +5,31 @@ type SignOutOptions = { callbackUrl?: string; }; +export type SessionClearedPayload = { + redirecting?: boolean; +}; + export async function signOut(options: SignOutOptions = {}) { + // Remove the persisted session first. This makes the user unauthenticated + // for the next page load and prevents the old access token from being used. clearSession(); - eventBus.emit(AppEvents.SessionCleared); if (options.callbackUrl && typeof window !== "undefined") { const currentPathname = window.location.pathname; if (currentPathname.startsWith("/admin")) { + // Start the redirect before notifying the current page. The event is + // still emitted, but the redirect flag tells mounted listeners to wait + // for the new page instead of rendering the login screen once here. window.location.href = options.callbackUrl; + console.log("[Auth] SessionCleared emitted from admin logout"); + eventBus.emit(AppEvents.SessionCleared, { redirecting: true }); + return; } } + + // When there is no full-page redirect, mounted components must receive this + // event so useSession can clear their in-memory user and status values. + console.log("[Auth] SessionCleared emitted from signOut"); + eventBus.emit(AppEvents.SessionCleared); return; } diff --git a/src/hooks/useSession.ts b/src/hooks/useSession.ts index 2c27ca8a..4f4946e6 100644 --- a/src/hooks/useSession.ts +++ b/src/hooks/useSession.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { loadSession } from "../adapters/auth/storage"; import { getSession } from "../adapters/auth/getSession"; import type { Session } from "../adapters/auth/types"; +import type { SessionClearedPayload } from "../adapters/auth/signOut"; import { eventBus, AppEvents } from "../helpers/eventBus"; type UseSessionResult = { @@ -38,7 +39,17 @@ export function useSession(): UseSessionResult { setData(session || null); setStatus(session?.user?.accessToken ? "authenticated" : "unauthenticated"); }); - const offClear = eventBus.on(AppEvents.SessionCleared, () => { + const offClear = eventBus.on(AppEvents.SessionCleared, (payload) => { + // For a normal sign-out, clear the session held by this React tree so + // route guards update immediately. During a page redirect, storage is + // already cleared and the new page will check it, so updating this old + // tree would briefly render the login screen before the redirect. + console.log( + payload?.redirecting + ? "[Auth] SessionCleared received; waiting for page redirect" + : "[Auth] SessionCleared listener received event" + ); + if (payload?.redirecting) return; setData(null); setStatus("unauthenticated"); });