- SessionExpiryBanner: live countdown, warn 15min early, one-shot toast, Extend button - countdown.ts: pure countdown helpers (timeUntilExpiry, sessionExpiryPhase, formatCountdown) - useSessionExpired: added unlock handler for SESSION_EXTENDED_EVENT - useGameTick: exported SESSION_EXTENDED_EVENT for session extension - unit tests: 14/14 pass for countdown helpers - e2e test: banner renders with countdown + extend button
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { SESSION_EXPIRED_EVENT, SESSION_EXTENDED_EVENT } from "@/hooks/useGameTick";
|
|
|
|
/**
|
|
* Tracks whether the session has expired (per the client-side watchdog).
|
|
*
|
|
* Returns both a reactive `expired` boolean (for rendering) and an
|
|
* `expiredRef` (for use inside callbacks / the game loop without stale
|
|
* closures). Consumers should gate start/retry/input and skip server saves
|
|
* on `expiredRef.current`.
|
|
*/
|
|
export function useSessionExpired(): {
|
|
expired: boolean;
|
|
expiredRef: React.RefObject<boolean>;
|
|
} {
|
|
const [expired, setExpired] = useState(false);
|
|
const expiredRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
const lock = () => {
|
|
expiredRef.current = true;
|
|
setExpired(true);
|
|
};
|
|
const unlock = () => {
|
|
expiredRef.current = false;
|
|
setExpired(false);
|
|
};
|
|
window.addEventListener(SESSION_EXPIRED_EVENT, lock);
|
|
window.addEventListener(SESSION_EXTENDED_EVENT, unlock);
|
|
return () => {
|
|
window.removeEventListener(SESSION_EXPIRED_EVENT, lock);
|
|
window.removeEventListener(SESSION_EXTENDED_EVENT, unlock);
|
|
};
|
|
}, []);
|
|
|
|
return { expired, expiredRef };
|
|
}
|