'use client' import { useState, useEffect, useCallback, useRef } from 'react' import PageLayout from '../../components/PageLayout' import BlueBlurryBackground from '../../components/background/blueblurry' // NEW import useAuthStore from '../../store/authStore' import { useUserStatus } from '../../hooks/useUserStatus' import { useRouter } from 'next/navigation' import { useToast } from '../../components/toast/toastComponent' export default function EmailVerifyPage() { const user = useAuthStore(s => s.user) const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW const token = useAuthStore(s => s.accessToken) const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED const [code, setCode] = useState(['', '', '', '', '', '']) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState('') const [success, setSuccess] = useState(false) const [resendCooldown, setResendCooldown] = useState(0) const [initialEmailSent, setInitialEmailSent] = useState(false) const inputsRef = useRef>([]) const emailSentRef = useRef(false) const router = useRouter() const { showToast } = useToast() // NEW: resend and validity windows const RESEND_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes const CODE_VALIDITY_MS = 15 * 60 * 1000 // 15 minutes (informational) // NEW: helpers to persist per-user last sent timestamp const getStorageKey = (email?: string | null) => `emailVerify:lastSent:${email || 'anon'}` const getLastSentAt = (email?: string | null) => { try { return parseInt(localStorage.getItem(getStorageKey(email)) || '0', 10) || 0 } catch { return 0 } } const setLastSentAt = (ts: number, email?: string | null) => { try { localStorage.setItem(getStorageKey(email), String(ts)) } catch {} } // Send verification email automatically on page load (respect cooldown) useEffect(() => { if (!token || emailSentRef.current) return const now = Date.now() const last = getLastSentAt(user?.email) const remaining = RESEND_INTERVAL_MS - (now - last) if (last && remaining > 0) { // Already sent recently: honor cooldown, don't resend setInitialEmailSent(true) setResendCooldown(Math.ceil(remaining / 1000)) return } const sendInitialEmail = async () => { emailSentRef.current = true try { const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-verification-email`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }) const data = await response.json() if (response.ok && data.success) { setInitialEmailSent(true) setLastSentAt(Date.now(), user?.email) setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000)) showToast({ variant: 'success', title: 'Verification email sent', message: `We sent a verification email to ${user?.email || 'your email'}.` }) } else { const msg = data?.message || 'Error sending the verification email.' setError(msg) emailSentRef.current = false showToast({ variant: 'error', title: 'Email not sent', message: msg }) } } catch (err) { console.error('Error sending initial verification email:', err) const msg = 'Network error while sending the verification email.' setError(msg) emailSentRef.current = false showToast({ variant: 'error', title: 'Network error', message: msg }) } } sendInitialEmail() }, [token, user, showToast]) // Cooldown timer useEffect(() => { if (!resendCooldown) return const t = setInterval(() => { setResendCooldown(c => (c > 0 ? c - 1 : 0)) }, 1000) return () => clearInterval(t) }, [resendCooldown]) // Replace handleChange to support multi-digit input distribution const handleChange = (idx: number, val: string) => { const digits = (val || '').replace(/\D/g, '') if (digits.length === 0) { const next = [...code] next[idx] = '' setCode(next) setError('') return } // Distribute digits across fields starting from idx const next = [...code] let i = idx for (const ch of digits) { if (i > 5) break next[i] = ch i++ } setCode(next) setError('') // Focus next empty or last filled const focusTo = Math.min(i, 5) inputsRef.current[focusTo]?.focus() } // New: paste handler to fill all fields const handlePaste = (idx: number, e: React.ClipboardEvent) => { e.preventDefault() const pasted = e.clipboardData.getData('text') || '' const digits = pasted.replace(/\D/g, '').slice(0, 6) if (!digits) return const next = [...code] let i = idx for (const ch of digits) { if (i > 5) break next[i] = ch i++ } setCode(next) // Move focus to last populated field const focusTo = Math.min(i - 1, 5) if (focusTo >= 0) inputsRef.current[focusTo]?.focus() } const handleKeyDown = (idx: number, e: React.KeyboardEvent) => { if (e.key === 'Backspace') { e.preventDefault() const next = [...code] if (next[idx]) { // If current has a value, clear it and stay next[idx] = '' setCode(next) setError('') inputsRef.current[idx]?.focus() } else if (idx > 0) { // If empty, move to previous and clear it const prev = idx - 1 next[prev] = '' setCode(next) setError('') inputsRef.current[prev]?.focus() } return } if (e.key === 'ArrowLeft' && idx > 0) { e.preventDefault() inputsRef.current[idx - 1]?.focus() return } if (e.key === 'ArrowRight' && idx < 5) { e.preventDefault() inputsRef.current[idx + 1]?.focus() return } } const fullCode = code.join('') const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (fullCode.length !== 6) { const msg = 'Please enter the 6-digit code.' setError(msg) showToast({ variant: 'error', title: 'Invalid code', message: msg }) return } if (!token) { const msg = 'Not authenticated. Please log in again.' setError(msg) showToast({ variant: 'error', title: 'Authentication error', message: msg }) return } setSubmitting(true) setError('') try { const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/verify-email-code`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ code: fullCode }) }) const data = await response.json() if (response.ok && data.success) { setSuccess(true) showToast({ variant: 'success', title: 'Email verified', message: 'Your email has been verified successfully.' }) await refreshStatus() // Guests go directly to dashboard after email verification const isGuest = user?.role === 'guest' window.location.href = isGuest ? '/dashboard' : '/quickaction-dashboard?tutorial=true' } else { const msg = data.error || 'Verification failed. Please try again.' setError(msg) showToast({ variant: 'error', title: 'Verification failed', message: msg }) } } catch (err) { console.error('Email verification error:', err) const msg = 'Network error. Please try again.' setError(msg) showToast({ variant: 'error', title: 'Network error', message: msg }) } finally { setSubmitting(false) } } // Resend (respect 10min interval even across navigation) const handleResend = useCallback(async () => { if (submitting || success) return const now = Date.now() const last = getLastSentAt(user?.email) const remaining = RESEND_INTERVAL_MS - (now - last) if (remaining > 0) { setResendCooldown(Math.ceil(remaining / 1000)) return } if (!token) { const msg = 'Not authenticated. Please log in again.' setError(msg) showToast({ variant: 'error', title: 'Authentication error', message: msg }) return } setError('') try { const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-verification-email`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }) const data = await response.json() if (response.ok && data.success) { setLastSentAt(Date.now(), user?.email) setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000)) if (!initialEmailSent) setInitialEmailSent(true) showToast({ variant: 'success', title: 'Verification email sent', message: `We sent a new verification email to ${user?.email || 'your email'}.` }) } else { const msg = data?.message || 'Error sending the email.' setError(msg) showToast({ variant: 'error', title: 'Email not sent', message: msg }) } } catch (err) { console.error('Resend email error:', err) const msg = 'Network error while sending the email.' setError(msg) showToast({ variant: 'error', title: 'Network error', message: msg }) } }, [token, submitting, success, user, initialEmailSent, showToast]) // NEW: format seconds to m:ss const formatMmSs = (total: number) => { const m = Math.floor(total / 60) const s = total % 60 return `${m}:${String(s).padStart(2, '0')}` } // NEW: hard block if step already done OR all steps done const [redirectTo, setRedirectTo] = useState(null) const redirectOnceRef = useRef(false) const smoothReplace = useCallback((to: string) => { if (redirectOnceRef.current) return redirectOnceRef.current = true setRedirectTo(to) window.setTimeout(() => router.replace(to), 200) }, [router]) useEffect(() => { if (statusLoading || !userStatus) return const isGuest = user?.role === 'guest' const allDone = isGuest ? !!userStatus.email_verified : !!userStatus.email_verified && !!userStatus.documents_uploaded && !!userStatus.profile_completed && !!userStatus.contract_signed if (allDone) { smoothReplace('/dashboard') } else if (userStatus.email_verified) { // Regular users go back to quickaction dashboard for remaining steps // Guests should never reach here since allDone covers them smoothReplace('/quickaction-dashboard') } }, [statusLoading, userStatus, user, smoothReplace]) // NEW: must be logged in useEffect(() => { if (!isAuthReady) return if (!user || !token) smoothReplace('/login') }, [isAuthReady, user, token, smoothReplace]) return ( {/* NEW: smooth redirect overlay */} {redirectTo && (
Redirecting…
Please wait
)}

Verify your email

{initialEmailSent ? ( <> We sent a 6-digit code to{' '} {user?.email || 'your email'} . Enter it below. ) : ( <> Sending verification email to{' '} {user?.email || 'your email'} ... )}

{code.map((v, i) => ( { inputsRef.current[i] = el }} inputMode="numeric" aria-label={`Code digit ${i + 1}`} autoComplete="one-time-code" maxLength={1} value={v} onChange={e => handleChange(i, e.target.value)} onKeyDown={e => handleKeyDown(i, e)} onPaste={e => handlePaste(i, e)} className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none ${v ? 'border-[#8D6B1D] ring-2 ring-[#8D6B1D]/25 bg-white text-gray-900' : 'border-gray-300 bg-white/80 text-gray-700'} focus:ring-2 focus:ring-[#8D6B1D] focus:border-[#8D6B1D]`} /> ))}
{error && (
{error}
)} {success && (
Verified! Redirecting shortly...
)}
Didn’t receive the email? Please check your junk/spam folder. Still having issues?{' '} Contact support .
) }