@supunlakmal/hooks
Version:
A collection of reusable React hooks
156 lines • 7.03 kB
JavaScript
import { useState, useEffect, useCallback, useRef } from 'react';
const isBrowser = typeof window !== 'undefined';
const IdleDetector = isBrowser ? window.IdleDetector : undefined;
/**
* Hook to detect user idle state and screen lock status using the Idle Detection API.
*
* @param options Configuration options for the idle detector.
* @returns State and controls for idle detection.
*/
export function useIdleDetection(options = {}) {
const isSupported = !!IdleDetector;
const [userState, setUserState] = useState(null);
const [screenState, setScreenState] = useState(null);
const [permissionState, setPermissionState] = useState(null);
const [isActive, setIsActive] = useState(false);
const [error, setError] = useState(null);
const detectorRef = useRef(null);
const abortControllerRef = useRef(null);
const optionsRef = useRef(options);
// Keep options ref updated
useEffect(() => {
optionsRef.current = options;
}, [options]);
const requestPermission = useCallback(async () => {
var _a, _b;
if (!isSupported) {
setError(new Error('Idle Detection API not supported.'));
setPermissionState('denied');
return 'denied';
}
try {
const status = await IdleDetector.requestPermission();
setPermissionState(status);
setError(null);
return status;
}
catch (err) {
console.error('Error requesting idle detection permission:', err);
const currentError = err instanceof Error
? err
: new Error('Failed to request idle permission');
setError(currentError);
(_b = (_a = optionsRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, currentError);
setPermissionState('denied');
return 'denied';
}
}, [isSupported]);
const startDetector = useCallback(async () => {
var _a, _b, _c, _d, _e, _f, _g;
if (!isSupported || isActive)
return;
if (permissionState !== 'granted') {
const status = await requestPermission();
if (status !== 'granted') {
setError(new Error('Idle detection permission not granted.'));
(_b = (_a = optionsRef.current).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error('Idle detection permission not granted.'));
return;
}
}
if (detectorRef.current)
return; // Already started or failed previously
try {
abortControllerRef.current = new AbortController();
const detector = new IdleDetector();
detectorRef.current = detector;
detector.onchange = () => {
var _a, _b;
setUserState(detector.userState);
setScreenState(detector.screenState);
setIsActive(true); // Keep active as long as detector exists
(_b = (_a = optionsRef.current).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, {
userState: detector.userState,
screenState: detector.screenState,
});
};
await detector.start({
threshold: optionsRef.current.threshold,
signal: abortControllerRef.current.signal,
});
// Initial state check after start
setUserState(detector.userState);
setScreenState(detector.screenState);
setIsActive(true);
setError(null);
(_d = (_c = optionsRef.current).onChange) === null || _d === void 0 ? void 0 : _d.call(_c, {
userState: detector.userState,
screenState: detector.screenState,
});
}
catch (err) {
console.error('Error starting idle detector:', err);
const currentError = err instanceof Error ? err : new Error('Failed to start idle detector');
setError(currentError);
(_f = (_e = optionsRef.current).onError) === null || _f === void 0 ? void 0 : _f.call(_e, currentError);
setIsActive(false);
detectorRef.current = null;
(_g = abortControllerRef.current) === null || _g === void 0 ? void 0 : _g.abort(); // Clean up abort controller
abortControllerRef.current = null;
}
}, [isSupported, isActive, permissionState, requestPermission]);
// Auto-start logic
useEffect(() => {
var _a;
if (isSupported && optionsRef.current.autoStart) {
// Check initial permission state without prompting
(_a = navigator.permissions) === null || _a === void 0 ? void 0 : _a.query({ name: 'idle-detection' }).then((status) => {
setPermissionState(status.state);
if (status.state === 'granted') {
startDetector();
}
status.onchange = () => {
var _a;
setPermissionState(status.state);
if (status.state !== 'granted') {
// Stop detector if permission is revoked
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
detectorRef.current = null;
setIsActive(false);
}
};
}).catch((err) => {
console.warn('Could not query idle-detection permission state:', err);
// Fallback to trying to start, which will request permission if needed
startDetector();
});
}
}, [isSupported, startDetector]); // Dependencies: isSupported, startDetector (which depends on permissionState)
// Cleanup on unmount or when signal is aborted
useEffect(() => {
const externalSignal = optionsRef.current.signal;
const handleAbort = () => {
var _a;
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort();
detectorRef.current = null;
setIsActive(false);
};
externalSignal === null || externalSignal === void 0 ? void 0 : externalSignal.addEventListener('abort', handleAbort);
return () => {
var _a;
externalSignal === null || externalSignal === void 0 ? void 0 : externalSignal.removeEventListener('abort', handleAbort);
(_a = abortControllerRef.current) === null || _a === void 0 ? void 0 : _a.abort(); // Abort internal controller on unmount
detectorRef.current = null;
};
}, []); // Empty array: run only once on mount/unmount
return {
userState,
screenState,
permissionState,
isActive,
error,
requestPermission,
startDetector,
isSupported,
};
}
//# sourceMappingURL=useIdleDetection.js.map