All files / src/utils/components/idle-checker IdleTimer.jsx

91.83% Statements 45/49
58.33% Branches 7/12
100% Functions 10/10
93.33% Lines 42/45

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100                                              4x 4x 4x 4x   4x 14x 14x 14x 14x           4x 7x 7x           4x 6x   6x 6x   6x 1x 1x 1x 1x     6x 14x   14x       14x 14x 6x 6x   8x     6x 2x       6x   36x 6x 6x   6x   6x 36x 6x 6x 6x       6x        
import React, { useEffect } from "react";
 
/**
 * Idle detector for the inactivity auto-logout.
 *
 * The previous implementation armed a single `setTimeout(idleTime)` and relied
 * on it firing on time. Browsers heavily throttle — and after a few minutes can
 * freeze — timers in backgrounded/occluded tabs, so a user who switched away and
 * came back was often NOT logged out on time (the timer was delayed or never
 * fired). That is the "idle logout doesn't work" symptom.
 *
 * This version is **wall-clock based** instead:
 *  - every (throttled) user-activity event stores `Date.now()` in localStorage;
 *  - a periodic poll AND `visibilitychange`/`focus` recompute the *real* elapsed
 *    time from that timestamp and flip to idle once it exceeds `idleTime`.
 *
 * Because each check derives elapsed time from a stored timestamp rather than
 * trusting the timer to fire exactly, it survives background-tab throttling: even
 * if the poll is throttled to once a minute (or frozen), the moment the tab is
 * refocused `visibilitychange` runs the check immediately. localStorage also
 * makes activity/idle consistent across tabs of the same origin.
 */
 
export const ACTIVITY_KEY = "eruditiontx_last_activity";
const ACTIVITY_EVENTS = ["mousedown", "mousemove", "keydown", "scroll", "touchstart", "click"];
const POLL_INTERVAL_MS = 5000;
const WRITE_THROTTLE_MS = 1000;
 
const readLastActivity = () => {
    try {
        const raw = window.localStorage.getItem(ACTIVITY_KEY);
        const ts = raw ? parseInt(raw, 10) : NaN;
        return Number.isFinite(ts) ? ts : null;
    } catch {
        return null;
    }
};
 
const writeLastActivity = (ts) => {
    try {
        window.localStorage.setItem(ACTIVITY_KEY, String(ts));
    } catch {
        /* private mode / quota — ignore; the check falls back to seeding "now" */
    }
};
 
const IdleTimer = ({ idleTime, setIsIdle }) => {
    const idleMs = idleTime * 1000;
 
    useEffect(() => {
        let lastWrite = 0;
 
        const markActivity = () => {
            const now = Date.now();
            Iif (now - lastWrite < WRITE_THROTTLE_MS) return; // throttle localStorage writes
            lastWrite = now;
            writeLastActivity(now);
        };
 
        const checkIdle = () => {
            const last = readLastActivity();
            // No stored timestamp yet → treat now as activity (don't false-trip).
            Iif (last === null) {
                writeLastActivity(Date.now());
                return;
            }
            const remainingMs = idleMs - (Date.now() - last);
            if (remainingMs <= 0) {
                setIsIdle(true);
                return;
            }
            console.log(`[IdleTimer] idle in ${Math.ceil(remainingMs / 1000)}s`);
        };
 
        const onVisibility = () => {
            Eif (document.visibilityState === "visible") checkIdle();
        };
 
        // Seed activity on mount so a fresh session starts the clock now.
        writeLastActivity(Date.now());
 
        ACTIVITY_EVENTS.forEach((event) => window.addEventListener(event, markActivity, { passive: true }));
        document.addEventListener("visibilitychange", onVisibility);
        window.addEventListener("focus", checkIdle);
 
        const interval = setInterval(checkIdle, POLL_INTERVAL_MS);
 
        return () => {
            ACTIVITY_EVENTS.forEach((event) => window.removeEventListener(event, markActivity));
            document.removeEventListener("visibilitychange", onVisibility);
            window.removeEventListener("focus", checkIdle);
            clearInterval(interval);
        };
    }, [idleMs, setIsIdle]);
 
    return null;
};
 
export default IdleTimer;