Skip to content

Commit f849681

Browse files
Auto logout progress bar (#55)
* Auto logout progress bar * global listeners removed from submodules * added prevent logout * Label send as param * Test * test * test * test * test * test * test * test * PR comments * PR comments * PR comments * PR comments
1 parent 1e64240 commit f849681

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { FetchType, jsonFetchWrapper } from "@/submodules/javascript-functions/basic-fetch";
2+
import { formatTimeDigitalClock } from "@/submodules/javascript-functions/date-parser";
3+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
4+
5+
type AutoLogoutProgressBarProps = {
6+
autoLogoutMinutes: number | null;
7+
label: string;
8+
comesFromEntry?: boolean;
9+
className?: string;
10+
}
11+
12+
function isReload() {
13+
if ("performance" in window) {
14+
const navEntries = performance.getEntriesByType("navigation") as PerformanceNavigationTiming[];
15+
if (navEntries.length > 0) {
16+
return navEntries[0].type === "reload";
17+
}
18+
return (performance as any).navigation?.type === 1;
19+
}
20+
return false;
21+
};
22+
23+
const AUTH_BASE_URI = '/.ory/kratos/public/self-service/';
24+
function logout() {
25+
const url = `${AUTH_BASE_URI}logout/browser`;
26+
jsonFetchWrapper(url, FetchType.GET, (result) => { window.location.href = result.logout_url });
27+
}
28+
29+
export default function AutoLogoutProgressBar(props: AutoLogoutProgressBarProps) {
30+
const [showProgressBar, setShowProgressBar] = useState(false);
31+
const [remainingMinutes, setRemainingMinutes] = useState(0);
32+
const [completeCalled, setCompleteCalled] = useState(false);
33+
const progressBarRef = useRef(null);
34+
35+
const lastInteractionRef = useRef(Date.now());
36+
37+
useEffect(() => {
38+
const resetTimer = () => {
39+
lastInteractionRef.current = Date.now();
40+
progressBarRef.current?.resetTimer();
41+
}
42+
43+
const onKeyDownEvent = (e: KeyboardEvent) => {
44+
const target = e.target as HTMLElement;
45+
// used for the chat input (we want to trigger rest on typing)
46+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
47+
resetTimer();
48+
}
49+
};
50+
window.addEventListener("click", resetTimer);
51+
window.addEventListener("keydown", onKeyDownEvent)
52+
return () => {
53+
window.removeEventListener("click", resetTimer);
54+
window.removeEventListener("keydown", onKeyDownEvent)
55+
};
56+
}, []);
57+
58+
useEffect(() => {
59+
const autoLogoutMinutes = props?.autoLogoutMinutes;
60+
if (!autoLogoutMinutes) {
61+
setShowProgressBar(false);
62+
setRemainingMinutes(0);
63+
return;
64+
}
65+
66+
setRemainingMinutes(autoLogoutMinutes <= 5 ? autoLogoutMinutes : 5);
67+
68+
const checkInactivity = () => {
69+
const now = Date.now();
70+
const inactiveMinutes = (now - lastInteractionRef.current) / 1000 / 60;
71+
const minutesLeft = autoLogoutMinutes - inactiveMinutes;
72+
setShowProgressBar(minutesLeft <= 5);
73+
};
74+
75+
checkInactivity();
76+
const interval = setInterval(checkInactivity, 1000);
77+
return () => clearInterval(interval);
78+
}, [props?.autoLogoutMinutes]);
79+
80+
const logoutUser = useCallback(() => {
81+
localStorage.removeItem("lastClosedAt");
82+
logout();
83+
}, []);
84+
85+
const onCompleteFunc = useCallback(() => {
86+
setCompleteCalled(true);
87+
logoutUser();
88+
setTimeout(() => {
89+
setCompleteCalled(false);
90+
}, 1000);
91+
}, []);
92+
93+
useEffect(() => {
94+
if (isReload()) return;
95+
if (localStorage.getItem("comesFromEntry") === "true" && props.autoLogoutMinutes) {
96+
localStorage.removeItem("lastClosedAt");
97+
if (!props.comesFromEntry) {
98+
localStorage.setItem("comesFromEntry", "false");
99+
}
100+
return;
101+
}
102+
const handleBeforeUnload = () => {
103+
const nowIso = new Date().toISOString();
104+
if (!localStorage.getItem("lastClosedAt") && !completeCalled) localStorage.setItem("lastClosedAt", nowIso);
105+
};
106+
window.addEventListener("beforeunload", handleBeforeUnload);
107+
const stored = localStorage.getItem("lastClosedAt");
108+
if (stored && props.autoLogoutMinutes) {
109+
window.removeEventListener("beforeunload", handleBeforeUnload);
110+
logoutUser();
111+
}
112+
return () => {
113+
window.removeEventListener("beforeunload", handleBeforeUnload);
114+
};
115+
}, [completeCalled, props.comesFromEntry]);
116+
117+
return <>
118+
{showProgressBar && <ReverseProgressBar ref={progressBarRef} duration={remainingMinutes * 60} className={props.className} label={props.label} onComplete={onCompleteFunc} />}
119+
</>
120+
};
121+
122+
type ReverseProgressBarProps = {
123+
duration: number;
124+
onComplete: () => void;
125+
label: string;
126+
className?: string;
127+
};
128+
129+
const ReverseProgressBar = forwardRef((props: ReverseProgressBarProps, ref) => {
130+
const [remaining, setRemaining] = useState<number>(props.duration);
131+
132+
useEffect(() => {
133+
setRemaining(props.duration);
134+
}, [props.duration]);
135+
136+
useImperativeHandle(ref, () => ({
137+
resetTimer: () => {
138+
setRemaining(props.duration);
139+
}
140+
}));
141+
142+
useEffect(() => {
143+
setRemaining(props.duration);
144+
145+
const tick = () => {
146+
setRemaining(prev => {
147+
if (prev - 1 <= 0) {
148+
if (props.onComplete) props.onComplete();
149+
clearInterval(interval);
150+
return 0;
151+
}
152+
return prev - 1;
153+
});
154+
};
155+
const interval = setInterval(tick, 1000);
156+
return () => {
157+
clearInterval(interval);
158+
};
159+
}, [props.duration]);
160+
161+
const progressPercent = useMemo(() => {
162+
return Math.max(0, Math.min(100, (remaining / Math.max(1, props.duration)) * 100));
163+
}, [remaining, props.duration]);
164+
165+
return (
166+
<div className={`w-full max-w-3xs ml-auto ${props.className}`}>
167+
<div className="relative w-full h-5 bg-gray-200 rounded-md overflow-hidden">
168+
<div className="absolute inset-0 bg-gradient-to-r from-red-500 via-yellow-400 to-green-500" />
169+
<div
170+
style={{ width: `${100 - progressPercent}%` }}
171+
className="absolute top-0 bottom-0 right-0 bg-gray-200"
172+
/>
173+
<div className="absolute inset-0 flex items-center justify-center text-sm font-medium tabular-nums">
174+
{formatTimeDigitalClock(remaining)}
175+
</div>
176+
</div>
177+
{props.label && <div className="mt-2 text-xs text-gray-500 italic text-center">{props.label}</div>}
178+
</div>
179+
);
180+
});

0 commit comments

Comments
 (0)