154 lines
5.6 KiB
TypeScript
154 lines
5.6 KiB
TypeScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { Clock, LogOut, RefreshCw } from 'lucide-react';
|
|
import { PfmPartnerLogo } from '../common/PfmPartnerLogo';
|
|
import { clearSessionAndRedirectToLanding, dismissSubordinateModals } from '../../services/sessionExpiration';
|
|
|
|
interface SessionExpiredModalProps {
|
|
isOpen: boolean;
|
|
onDismiss?: () => void;
|
|
reason?: string | null;
|
|
countdownSeconds?: number;
|
|
}
|
|
|
|
export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
|
|
isOpen,
|
|
onDismiss,
|
|
reason,
|
|
countdownSeconds = 8,
|
|
}) => {
|
|
const [timeLeftMs, setTimeLeftMs] = useState(countdownSeconds * 1000);
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
const totalDurationMs = countdownSeconds * 1000;
|
|
const isClearingRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
dismissSubordinateModals();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
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]);
|
|
|
|
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));
|
|
|
|
const modalContent = (
|
|
<div
|
|
id="session-expired-modal-backdrop"
|
|
className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-slate-950/85 backdrop-blur-2xl animate-in fade-in duration-300 overflow-y-auto"
|
|
onMouseEnter={() => setIsPaused(true)}
|
|
onMouseLeave={() => setIsPaused(false)}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="session-expired-title"
|
|
>
|
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[450px] h-[450px] bg-amber-500/10 blur-[120px] rounded-full" />
|
|
</div>
|
|
|
|
<div
|
|
id="session-expired-modal-content"
|
|
className="relative w-full max-w-md rounded-3xl border border-white/10 bg-slate-900/85 backdrop-blur-xl shadow-[0_25px_60px_rgba(0,0,0,0.85),inset_0_1px_1px_rgba(255,255,255,0.15)] overflow-hidden flex flex-col items-center text-center p-8 sm:p-10 my-auto space-y-6"
|
|
>
|
|
<div className="relative flex items-center justify-center pt-2">
|
|
<div className="absolute -inset-3 rounded-full bg-amber-500/20 blur-xl" />
|
|
<PfmPartnerLogo className="h-16 w-16 drop-shadow-[0_8px_24px_rgba(245,158,11,0.35)] relative z-10" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/20 text-[11px] font-semibold text-amber-300">
|
|
<Clock className="w-3.5 h-3.5 text-amber-400" />
|
|
<span>Session Expired</span>
|
|
</div>
|
|
<h2 id="session-expired-title" className="text-2xl font-bold tracking-tight text-white">
|
|
Security Session Ended
|
|
</h2>
|
|
</div>
|
|
|
|
<p className="text-sm text-slate-300 leading-relaxed max-w-sm">
|
|
{reason
|
|
? reason.replace(/network\s*error/gi, 'Permission Check')
|
|
: 'Your security credentials have expired for your protection. Please sign in again to continue.'}
|
|
</p>
|
|
|
|
{/* Smooth Countdown Bar */}
|
|
<div className="w-full space-y-2">
|
|
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-emerald-400 to-teal-400 transition-all duration-75 ease-linear rounded-full"
|
|
style={{ width: `${progressPercent}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-[11px] text-slate-400 font-mono">
|
|
Redirecting in {secondsRemaining}s
|
|
</span>
|
|
</div>
|
|
|
|
<div className="w-full pt-1">
|
|
<button
|
|
onClick={handleAction}
|
|
type="button"
|
|
className="w-full py-3.5 px-6 rounded-2xl bg-gradient-to-r from-emerald-600 via-teal-600 to-emerald-500 hover:from-emerald-500 hover:to-teal-500 text-white font-bold text-sm tracking-wide shadow-[0_12px_30px_rgba(16,185,129,0.35),inset_0_1px_1px_rgba(255,255,255,0.3)] hover:shadow-[0_16px_36px_rgba(16,185,129,0.5)] transition-all flex items-center justify-center gap-2 cursor-pointer"
|
|
>
|
|
<RefreshCw className="w-4 h-4 text-emerald-100" />
|
|
<span>Sign In Again Now</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return typeof document !== 'undefined'
|
|
? createPortal(modalContent, document.body)
|
|
: modalContent;
|
|
};
|