Summary
useActivityTimer is otherwise a clean, portable timer hook — local tick loop, optimistic state, server sync, auto-stop-at-limit logic. Its one browser dependency is a beforeunload listener that warns the user before closing the tab mid-activity. Extract that into a separate web-only hook so the timer logic itself is platform-neutral.
Part of #569.
Relevant code
frontend/src/hooks/useActivityTimer.ts:336-342:
// Block tab close / refresh / external navigation while timer is active
useEffect(() => {
if (status !== 'active') return;
const handler = (e: BeforeUnloadEvent): void => { e.preventDefault(); e.returnValue = ''; };
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [status]);
Proposed fix
Move this into its own hook (e.g. hooks/useUnloadWarning.ts) taking an active: boolean, and call it from the component that owns the timer UI rather than from inside useActivityTimer. This leaves useActivityTimer with zero window references.
Acceptance criteria
Notes
There's no native equivalent of beforeunload — protecting an in-progress timer on Android is an AppState / background-task concern and will need designing separately. Extracting the web behaviour now keeps the two concerns from being conflated later.
⚠️ Touches the timer hook, so coordinate with the in-progress timer redesign to avoid conflicting edits. The change is small and additive, but it's in a file that redesign work is likely to be moving around.
Summary
useActivityTimeris otherwise a clean, portable timer hook — local tick loop, optimistic state, server sync, auto-stop-at-limit logic. Its one browser dependency is abeforeunloadlistener that warns the user before closing the tab mid-activity. Extract that into a separate web-only hook so the timer logic itself is platform-neutral.Part of #569.
Relevant code
frontend/src/hooks/useActivityTimer.ts:336-342:Proposed fix
Move this into its own hook (e.g.
hooks/useUnloadWarning.ts) taking anactive: boolean, and call it from the component that owns the timer UI rather than from insideuseActivityTimer. This leavesuseActivityTimerwith zerowindowreferences.Acceptance criteria
windowreference remains inuseActivityTimer.tsactive, and does not warn when it isn'twindowuseActivityTimer.test.tsxstill passesNotes
There's no native equivalent of
beforeunload— protecting an in-progress timer on Android is an AppState / background-task concern and will need designing separately. Extracting the web behaviour now keeps the two concerns from being conflated later.