6c6828006b
Adds opt-in (default OFF) auto-lock: after X minutes of no user input the app calls signOut() (full memory wipe + PIN re-entry on next open). Settings dropdown (Aus / 5 / 15 / 30 / 60 min) lives in SecurityCenter below the existing wipe-on-close toggle. The idle timer is mounted in AppShell via useIdleAutoLock; activity events are throttled to 1 Hz to avoid timer thrash on rapid mouse movement. The localStorage key is added to PRESERVE_LOCAL_STORAGE so a wipe never silently disables the feature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
80 lines
2.2 KiB
TypeScript
80 lines
2.2 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
|
|
import { useAuth } from '../context/AuthContext';
|
|
import {
|
|
getAutoLockMinutes,
|
|
subscribeAutoLockSetting,
|
|
type AutoLockMinutes,
|
|
} from '../lib/autoLockSettings';
|
|
|
|
const ACTIVITY_EVENTS: Array<keyof WindowEventMap> = [
|
|
'keydown',
|
|
'mousedown',
|
|
'pointermove',
|
|
'touchstart',
|
|
'wheel',
|
|
];
|
|
|
|
// Throttle activity-event resets to once per second to avoid thrashing the
|
|
// timer on rapid mouse movement.
|
|
const RESET_THROTTLE_MS = 1000;
|
|
|
|
export function useIdleAutoLock(): void {
|
|
const { session, signOut } = useAuth();
|
|
const minutesRef = useRef<AutoLockMinutes>(getAutoLockMinutes());
|
|
const timerRef = useRef<number | null>(null);
|
|
const lastResetAtRef = useRef<number>(0);
|
|
|
|
// Keep minutesRef live to the setting.
|
|
useEffect(() => {
|
|
const unsub = subscribeAutoLockSetting((v) => {
|
|
minutesRef.current = v;
|
|
scheduleNext();
|
|
});
|
|
return unsub;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// Helper: schedule the lock based on the current setting.
|
|
function scheduleNext(): void {
|
|
if (timerRef.current !== null) {
|
|
window.clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
const min = minutesRef.current;
|
|
if (min === 0) return; // disabled
|
|
timerRef.current = window.setTimeout(() => {
|
|
timerRef.current = null;
|
|
// Fire the lock. signOut wipes local state and navigates to /device
|
|
// (PIN re-entry screen).
|
|
void signOut().catch((err) => console.warn('auto-lock signOut failed', err));
|
|
}, min * 60 * 1000);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!session) return;
|
|
scheduleNext();
|
|
|
|
const onActivity = () => {
|
|
const now = Date.now();
|
|
if (now - lastResetAtRef.current < RESET_THROTTLE_MS) return;
|
|
lastResetAtRef.current = now;
|
|
scheduleNext();
|
|
};
|
|
|
|
for (const ev of ACTIVITY_EVENTS) {
|
|
window.addEventListener(ev, onActivity, { passive: true });
|
|
}
|
|
return () => {
|
|
for (const ev of ACTIVITY_EVENTS) {
|
|
window.removeEventListener(ev, onActivity);
|
|
}
|
|
if (timerRef.current !== null) {
|
|
window.clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [session]);
|
|
}
|