diff --git a/src/components/frontend/session/SessionExpiryBanner.tsx b/src/components/frontend/session/SessionExpiryBanner.tsx index 3ff6e12..89682e4 100644 --- a/src/components/frontend/session/SessionExpiryBanner.tsx +++ b/src/components/frontend/session/SessionExpiryBanner.tsx @@ -16,6 +16,13 @@ import { } from "@/lib/session/countdown"; import { SESSION_EXTENDED_EVENT } from "@/hooks/useGameTick"; +/** + * Stable id for the one-shot "session expiring" heads-up toast. A fixed id lets + * us dismiss it deterministically (e.g. on sign-out) instead of relying on the + * opaque id returned by `toast.info`. Mirrors MissionReminderToasts. + */ +const SESSION_EXPIRY_TOAST_ID = "session-expiry-warning"; + interface SessionExpiryBannerProps { /** Absolute expiry (ms since epoch), server-computed from the JWT `exp`. */ sessionExp: number | null; @@ -70,6 +77,7 @@ export function SessionExpiryBanner({ toastFiredRef.current = true; const minutes = Math.round(warnMs / 60_000); toast.info(`Your session expires in about ${minutes} min.`, { + id: SESSION_EXPIRY_TOAST_ID, action: { label: "Extend", onClick: () => void extendSession(), @@ -83,6 +91,20 @@ export function SessionExpiryBanner({ if (phase === "warn") fireToast(); }, [phase, warnMs, fireToast]); + // The heads-up toast is only relevant while we're in the warn window. Dismiss it + // when the user extends (phase leaves "warn") or dismisses the banner, and again + // on unmount (sign-out swaps the shell for the landing page) — otherwise the + // duration:Infinity toast lingers for a signed-out user and its Extend button 403s. + useEffect(() => { + if (!visible) toast.dismiss(SESSION_EXPIRY_TOAST_ID); + }, [visible]); + + useEffect(() => { + return () => { + toast.dismiss(SESSION_EXPIRY_TOAST_ID); + }; + }, []); + const extendSession = useCallback(async () => { setExtending(true); setError(null);