import React, { useEffect, useState, useRef } from 'react'; import { ShieldAlert, Clock, ArrowRight, X, AlertTriangle, LogOut } from 'lucide-react'; import { clearSessionAndRedirectToLanding, dismissSubordinateModals } from '../../services/sessionExpiration'; interface SessionExpiredModalProps { isOpen: boolean; onDismiss?: () => void; reason?: string | null; /** Total countdown time in seconds (defaults to 8 seconds) */ countdownSeconds?: number; } export const SessionExpiredModal: React.FC = ({ isOpen, onDismiss, reason, countdownSeconds = 8, }) => { const [timeLeftMs, setTimeLeftMs] = useState(countdownSeconds * 1000); const [isPaused, setIsPaused] = useState(false); const totalDurationMs = countdownSeconds * 1000; const isClearingRef = useRef(false); // When session expiration modal is displayed, dismiss all subordinate modals useEffect(() => { if (isOpen) { dismissSubordinateModals(); } }, [isOpen]); // Listen for cancellation event (e.g., when activation lockout takes priority) useEffect(() => { const handleCancelled = () => { setTimeLeftMs(countdownSeconds * 1000); setIsPaused(false); isClearingRef.current = false; }; window.addEventListener('pfm:session-expired-cancelled', handleCancelled); return () => { window.removeEventListener('pfm:session-expired-cancelled', handleCancelled); }; }, [countdownSeconds]); // Reset or start countdown when modal opens useEffect(() => { if (!isOpen) { setTimeLeftMs(countdownSeconds * 1000); setIsPaused(false); isClearingRef.current = false; return; } const intervalMs = 50; const interval = setInterval(() => { setTimeLeftMs((prev) => { if (isPaused) return prev; const next = prev - intervalMs; if (next <= 0) { clearInterval(interval); handleAction(); return 0; } return next; }); }, intervalMs); return () => clearInterval(interval); }, [isOpen, isPaused, countdownSeconds]); const handleAction = () => { if (isClearingRef.current) return; isClearingRef.current = true; if (onDismiss) { onDismiss(); } else { clearSessionAndRedirectToLanding(); } }; if (!isOpen) return null; const secondsRemaining = Math.max(0, Math.ceil(timeLeftMs / 1000)); const progressPercent = Math.max(0, Math.min(100, (timeLeftMs / totalDurationMs) * 100)); return (
setIsPaused(true)} onMouseLeave={() => setIsPaused(false)} role="dialog" aria-modal="true" aria-labelledby="session-expired-title" >
{/* Subtle decorative top accent bar */}
{/* Top bar with close button */}

Session Expired

HTTP 401

Authorization token invalidated

{/* Informative Explanation (non-technical and clear) */}

Your active working session has ended or your security credentials have expired. To protect your data and prevent unauthorized changes, your session has been closed.

{reason && (
{reason}
)}
{/* Progress Bar Countdown */}
Returning to landing page in{' '} {secondsRemaining}s {isPaused && ( Paused on hover )}
{/* Progress bar line */}
{/* Actions */}
); };