import { createContext, useContext, useState, useCallback, useEffect, useRef, type ReactNode, } from 'react'; import { useIsMutating } from '@tanstack/react-query'; import { useSettings } from '@/hooks/useSettings'; import api from '@/lib/api'; interface LockContextValue { isLocked: boolean; lock: () => void; unlock: (password: string) => Promise; } const LockContext = createContext(null); const ACTIVITY_EVENTS = ['mousemove', 'keydown', 'click', 'scroll', 'touchstart'] as const; const THROTTLE_MS = 5_000; // only reset timer every 5s of activity export function LockProvider({ children }: { children: ReactNode }) { const [isLocked, setIsLocked] = useState(false); const { settings } = useSettings(); const activeMutations = useIsMutating(); const timerRef = useRef | null>(null); const lastActivityRef = useRef(Date.now()); const activeMutationsRef = useRef(activeMutations); activeMutationsRef.current = activeMutations; const lock = useCallback(() => { setIsLocked(true); }, []); const unlock = useCallback(async (password: string) => { const { data } = await api.post<{ verified: boolean }>('/auth/verify-password', { password }); if (data.verified) { setIsLocked(false); lastActivityRef.current = Date.now(); } }, []); // Auto-lock idle timer useEffect(() => { const enabled = settings?.auto_lock_enabled ?? false; const minutes = settings?.auto_lock_minutes ?? 5; if (!enabled || isLocked) { // Clear any existing timer when disabled or already locked if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } return; } const timeoutMs = minutes * 60_000; const resetTimer = () => { // Don't lock while TanStack mutations are in flight if (activeMutationsRef.current > 0) return; if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { lock(); }, timeoutMs); }; const handleActivity = () => { const now = Date.now(); if (now - lastActivityRef.current < THROTTLE_MS) return; lastActivityRef.current = now; resetTimer(); }; // Start the initial timer resetTimer(); // Attach throttled listeners for (const event of ACTIVITY_EVENTS) { document.addEventListener(event, handleActivity, { passive: true }); } return () => { for (const event of ACTIVITY_EVENTS) { document.removeEventListener(event, handleActivity); } if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }; }, [settings?.auto_lock_enabled, settings?.auto_lock_minutes, isLocked, lock]); return ( {children} ); } export function useLock(): LockContextValue { const ctx = useContext(LockContext); if (!ctx) throw new Error('useLock must be used within a LockProvider'); return ctx; }