fix and update

This commit is contained in:
lendry
2026-07-02 00:10:22 +03:00
parent bcdfbc3861
commit 4306d0ce37
11 changed files with 375 additions and 41 deletions

View 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]);
}