diff --git a/components/AutoLogoutProgressBar.tsx b/components/AutoLogoutProgressBar.tsx new file mode 100644 index 0000000..f616972 --- /dev/null +++ b/components/AutoLogoutProgressBar.tsx @@ -0,0 +1,180 @@ +import { FetchType, jsonFetchWrapper } from "@/submodules/javascript-functions/basic-fetch"; +import { formatTimeDigitalClock } from "@/submodules/javascript-functions/date-parser"; +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; + +type AutoLogoutProgressBarProps = { + autoLogoutMinutes: number | null; + label: string; + comesFromEntry?: boolean; + className?: string; +} + +function isReload() { + if ("performance" in window) { + const navEntries = performance.getEntriesByType("navigation") as PerformanceNavigationTiming[]; + if (navEntries.length > 0) { + return navEntries[0].type === "reload"; + } + return (performance as any).navigation?.type === 1; + } + return false; +}; + +const AUTH_BASE_URI = '/.ory/kratos/public/self-service/'; +function logout() { + const url = `${AUTH_BASE_URI}logout/browser`; + jsonFetchWrapper(url, FetchType.GET, (result) => { window.location.href = result.logout_url }); +} + +export default function AutoLogoutProgressBar(props: AutoLogoutProgressBarProps) { + const [showProgressBar, setShowProgressBar] = useState(false); + const [remainingMinutes, setRemainingMinutes] = useState(0); + const [completeCalled, setCompleteCalled] = useState(false); + const progressBarRef = useRef(null); + + const lastInteractionRef = useRef(Date.now()); + + useEffect(() => { + const resetTimer = () => { + lastInteractionRef.current = Date.now(); + progressBarRef.current?.resetTimer(); + } + + const onKeyDownEvent = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + // used for the chat input (we want to trigger rest on typing) + if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") { + resetTimer(); + } + }; + window.addEventListener("click", resetTimer); + window.addEventListener("keydown", onKeyDownEvent) + return () => { + window.removeEventListener("click", resetTimer); + window.removeEventListener("keydown", onKeyDownEvent) + }; + }, []); + + useEffect(() => { + const autoLogoutMinutes = props?.autoLogoutMinutes; + if (!autoLogoutMinutes) { + setShowProgressBar(false); + setRemainingMinutes(0); + return; + } + + setRemainingMinutes(autoLogoutMinutes <= 5 ? autoLogoutMinutes : 5); + + const checkInactivity = () => { + const now = Date.now(); + const inactiveMinutes = (now - lastInteractionRef.current) / 1000 / 60; + const minutesLeft = autoLogoutMinutes - inactiveMinutes; + setShowProgressBar(minutesLeft <= 5); + }; + + checkInactivity(); + const interval = setInterval(checkInactivity, 1000); + return () => clearInterval(interval); + }, [props?.autoLogoutMinutes]); + + const logoutUser = useCallback(() => { + localStorage.removeItem("lastClosedAt"); + logout(); + }, []); + + const onCompleteFunc = useCallback(() => { + setCompleteCalled(true); + logoutUser(); + setTimeout(() => { + setCompleteCalled(false); + }, 1000); + }, []); + + useEffect(() => { + if (isReload()) return; + if (localStorage.getItem("comesFromEntry") === "true" && props.autoLogoutMinutes) { + localStorage.removeItem("lastClosedAt"); + if (!props.comesFromEntry) { + localStorage.setItem("comesFromEntry", "false"); + } + return; + } + const handleBeforeUnload = () => { + const nowIso = new Date().toISOString(); + if (!localStorage.getItem("lastClosedAt") && !completeCalled) localStorage.setItem("lastClosedAt", nowIso); + }; + window.addEventListener("beforeunload", handleBeforeUnload); + const stored = localStorage.getItem("lastClosedAt"); + if (stored && props.autoLogoutMinutes) { + window.removeEventListener("beforeunload", handleBeforeUnload); + logoutUser(); + } + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [completeCalled, props.comesFromEntry]); + + return <> + {showProgressBar && } + > +}; + +type ReverseProgressBarProps = { + duration: number; + onComplete: () => void; + label: string; + className?: string; +}; + +const ReverseProgressBar = forwardRef((props: ReverseProgressBarProps, ref) => { + const [remaining, setRemaining] = useState(props.duration); + + useEffect(() => { + setRemaining(props.duration); + }, [props.duration]); + + useImperativeHandle(ref, () => ({ + resetTimer: () => { + setRemaining(props.duration); + } + })); + + useEffect(() => { + setRemaining(props.duration); + + const tick = () => { + setRemaining(prev => { + if (prev - 1 <= 0) { + if (props.onComplete) props.onComplete(); + clearInterval(interval); + return 0; + } + return prev - 1; + }); + }; + const interval = setInterval(tick, 1000); + return () => { + clearInterval(interval); + }; + }, [props.duration]); + + const progressPercent = useMemo(() => { + return Math.max(0, Math.min(100, (remaining / Math.max(1, props.duration)) * 100)); + }, [remaining, props.duration]); + + return ( + + + + + + {formatTimeDigitalClock(remaining)} + + + {props.label && {props.label}} + + ); +});