fix and update
This commit is contained in:
192
apps/frontend/hooks/use-pin-idle-lock.ts
Normal file
192
apps/frontend/hooks/use-pin-idle-lock.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
AUTH_SESSION_KEY,
|
||||
AUTH_TOKEN_KEY,
|
||||
fetchAuthSession,
|
||||
isPinRequiredError,
|
||||
} from '@/lib/api';
|
||||
|
||||
const ACTIVITY_EVENTS = ['mousedown', 'keydown', 'touchstart', 'scroll', 'wheel', 'pointerdown', 'click'] as const;
|
||||
const ACTIVITY_DEBOUNCE_MS = 250;
|
||||
const SERVER_TOUCH_INTERVAL_MS = 30_000;
|
||||
|
||||
interface UsePinIdleLockOptions {
|
||||
enabled: boolean;
|
||||
timeoutMinutes: number;
|
||||
accessToken: string | null;
|
||||
onLockRequired: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
export function usePinIdleLock({
|
||||
enabled,
|
||||
timeoutMinutes,
|
||||
accessToken,
|
||||
onLockRequired,
|
||||
}: UsePinIdleLockOptions) {
|
||||
const onLockRequiredRef = useRef(onLockRequired);
|
||||
const accessTokenRef = useRef(accessToken);
|
||||
const timeoutMsRef = useRef(Math.max(1, timeoutMinutes) * 60_000);
|
||||
const idleDeadlineRef = useRef<number>(Date.now() + timeoutMsRef.current);
|
||||
const idleTimerRef = useRef<number | null>(null);
|
||||
const activityDebounceRef = useRef<number | null>(null);
|
||||
const lastServerTouchRef = useRef(0);
|
||||
const checkInFlightRef = useRef(false);
|
||||
const enabledRef = useRef(enabled);
|
||||
|
||||
useEffect(() => {
|
||||
onLockRequiredRef.current = onLockRequired;
|
||||
}, [onLockRequired]);
|
||||
|
||||
useEffect(() => {
|
||||
accessTokenRef.current = accessToken;
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
enabledRef.current = enabled;
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
timeoutMsRef.current = Math.max(1, timeoutMinutes) * 60_000;
|
||||
if (enabledRef.current) {
|
||||
idleDeadlineRef.current = Date.now() + timeoutMsRef.current;
|
||||
}
|
||||
}, [timeoutMinutes]);
|
||||
|
||||
const clearIdleTimer = useCallback(() => {
|
||||
if (idleTimerRef.current !== null) {
|
||||
window.clearTimeout(idleTimerRef.current);
|
||||
idleTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const syncServerActivity = useCallback(async () => {
|
||||
const token = accessTokenRef.current ?? window.localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
if (!token) return;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastServerTouchRef.current < SERVER_TOUCH_INTERVAL_MS) return;
|
||||
lastServerTouchRef.current = now;
|
||||
|
||||
try {
|
||||
await fetchAuthSession(token);
|
||||
} catch (error) {
|
||||
if (isPinRequiredError(error)) {
|
||||
onLockRequiredRef.current(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkIdleLock = useCallback(async () => {
|
||||
if (!enabledRef.current || checkInFlightRef.current) return;
|
||||
|
||||
const remainingMs = idleDeadlineRef.current - Date.now();
|
||||
if (remainingMs > 0) {
|
||||
clearIdleTimer();
|
||||
idleTimerRef.current = window.setTimeout(() => {
|
||||
void checkIdleLock();
|
||||
}, remainingMs);
|
||||
return;
|
||||
}
|
||||
|
||||
checkInFlightRef.current = true;
|
||||
clearIdleTimer();
|
||||
|
||||
const token = accessTokenRef.current ?? window.localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
if (!token) {
|
||||
checkInFlightRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await fetchAuthSession(token);
|
||||
if (session.requiresPin) {
|
||||
onLockRequiredRef.current(session.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
return;
|
||||
}
|
||||
|
||||
idleDeadlineRef.current = Date.now() + timeoutMsRef.current;
|
||||
clearIdleTimer();
|
||||
idleTimerRef.current = window.setTimeout(() => {
|
||||
void checkIdleLock();
|
||||
}, timeoutMsRef.current);
|
||||
} catch (error) {
|
||||
if (isPinRequiredError(error)) {
|
||||
onLockRequiredRef.current(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
|
||||
return;
|
||||
}
|
||||
|
||||
idleDeadlineRef.current = Date.now() + timeoutMsRef.current;
|
||||
clearIdleTimer();
|
||||
idleTimerRef.current = window.setTimeout(() => {
|
||||
void checkIdleLock();
|
||||
}, timeoutMsRef.current);
|
||||
} finally {
|
||||
checkInFlightRef.current = false;
|
||||
}
|
||||
}, [clearIdleTimer]);
|
||||
|
||||
const registerActivity = useCallback(() => {
|
||||
if (!enabledRef.current) return;
|
||||
|
||||
idleDeadlineRef.current = Date.now() + timeoutMsRef.current;
|
||||
clearIdleTimer();
|
||||
idleTimerRef.current = window.setTimeout(() => {
|
||||
void checkIdleLock();
|
||||
}, timeoutMsRef.current);
|
||||
|
||||
void syncServerActivity();
|
||||
}, [checkIdleLock, clearIdleTimer, syncServerActivity]);
|
||||
|
||||
const scheduleActivityReset = useCallback(() => {
|
||||
if (activityDebounceRef.current !== null) {
|
||||
window.clearTimeout(activityDebounceRef.current);
|
||||
}
|
||||
activityDebounceRef.current = window.setTimeout(() => {
|
||||
activityDebounceRef.current = null;
|
||||
registerActivity();
|
||||
}, ACTIVITY_DEBOUNCE_MS);
|
||||
}, [registerActivity]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
clearIdleTimer();
|
||||
if (activityDebounceRef.current !== null) {
|
||||
window.clearTimeout(activityDebounceRef.current);
|
||||
activityDebounceRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
idleDeadlineRef.current = Date.now() + timeoutMsRef.current;
|
||||
lastServerTouchRef.current = Date.now();
|
||||
void checkIdleLock();
|
||||
|
||||
for (const eventName of ACTIVITY_EVENTS) {
|
||||
window.addEventListener(eventName, scheduleActivityReset, { passive: true });
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
clearIdleTimer();
|
||||
return;
|
||||
}
|
||||
void checkIdleLock();
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
clearIdleTimer();
|
||||
if (activityDebounceRef.current !== null) {
|
||||
window.clearTimeout(activityDebounceRef.current);
|
||||
activityDebounceRef.current = null;
|
||||
}
|
||||
for (const eventName of ACTIVITY_EVENTS) {
|
||||
window.removeEventListener(eventName, scheduleActivityReset);
|
||||
}
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [checkIdleLock, clearIdleTimer, enabled, scheduleActivityReset]);
|
||||
}
|
||||
Reference in New Issue
Block a user