All files / layouts SessionTimeoutModal.tsx

48.36% Statements 59/122
35% Branches 14/40
70.37% Functions 19/27
63.75% Lines 51/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95          39x 1x 39x 39x 39x   96x 96x 96x 281x 281x 283x 96x 96x 96x 78x 78x                 78x           96x 79x 36x 32x 32x   4x 4x 3x 3x 3x 3x 3x 3x   1x 1x         79x                             96x 2x 2x 2x 2x                                    
import { useEffect, useState, useRef } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useUiStore } from './uiStore'
import { useAuthStore } from '@/features/auth/authStore'
import { Button } from '@/components/ui/button'
 
const WARNING_BEFORE = 5 * 60 * 1000
const CHECK_INTERVAL = 1000
 
export function SessionTimeoutModal() {
  const navigate = useNavigate()
  const location = useLocation()
  const sessionTimeoutAt = useUiStore((s) => s.sessionTimeoutAt)
  const setSessionTimeoutAt = useUiStore((s) => s.setSessionTimeoutAt)
  const clearAuth = useAuthStore((s) => s.clearAuth)
  const [remaining, setRemaining] = useState<number | null>(null)
  const channelRef = useRef<BroadcastChannel | null>(null)
 
  useEffect(() => {
    channelRef.current = new BroadcastChannel('session-timeout')
    channelRef.current.onmessage = (event) => {
      if (event.data.type === 'extend') {
        setSessionTimeoutAt(event.data.timeoutAt)
      }
      if (event.data.type === 'logout') {
        clearAuth()
        navigate('/login', { replace: true })
      }
    }
    return () => channelRef.current?.close()
  }, [setSessionTimeoutAt, clearAuth, navigate])
 
  useEffect(() => {
    const interval = setInterval(() => {
      if (!sessionTimeoutAt) {
        setRemaining(null)
        return
      }
      const diff = sessionTimeoutAt - Date.now()
      if (diff <= 0) {
        sessionStorage.setItem('returnUrl', location.pathname + location.search)
        clearAuth()
        navigate('/login', { replace: true })
        channelRef.current?.postMessage({ type: 'logout' })
        clearInterval(interval)
        return
      }
      if (diff <= WARNING_BEFORE) {
        setRemaining(diff)
      } else {
        setRemaining(null)
      }
    }, CHECK_INTERVAL)
    returnE () => clearInterval(interval)
  }, [sessionTimeoutAt, clearAuth, navigate])
 
  function extendSession() {
    const newTimeout = Date.now() + 30 * 60 * 1000
    setSessionTimeoutAt(newTimeout)
    setRemaining(null)
    channelRef.current?.postMessage({ type: 'extend', timeoutAt: newTimeout })
  }
 
  if (remaining === null || remaining > WARNING_BEFORE) return null

  const seconds = Math.ceil(remaining / 1000)
  const minutes = Math.floor(seconds / 60)
  const secs = seconds % 60
 
  return (
    <div
      role="dialog"
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
    >
      <div className="w-80 rounded-lg border border-border bg-card p-6 shadow-lg">
        <h2 className="mb-2 text-lg font-semibold">Session Timeout</h2>
        <p className="mb-4 text-sm text-muted-foreground">
          Your session will expire in {minutes}:{secs.toString().padStart(2, '0')}
        </p>
        <div className="mb-4 h-2 w-full overflow-hidden rounded-full bg-muted">
          <div
            className="h-full rounded-full bg-primary transition-all duration-1000"
            style={{
              width: `${(remaining / WARNING_BEFORE) * 100}%`,
            }}
          />
        </div>
        <Button className="w-full" onClick={extendSession}>
          Stay logged in
        </Button>
      </div>
    </div>
  )
}