Files
pfm/PostFundManagement.PartnerUI/src/components/auth/SessionExpiredModal.tsx
T

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;
};