import { useEffect, useRef, useState } from 'react' import { useNavigate, useSearchParams, Link } from 'react-router-dom' import { CheckCircle2, XCircle, Loader2, MailCheck } from 'lucide-react' import { authApi } from '@/api/auth' import { useAuthStore } from '@/store/authStore' import { PageMeta } from '@/components/common/PageMeta' import { toast } from '@/lib/toast' import { cn } from '@/lib/utils' type Status = 'loading' | 'success' | 'error' | 'already-verified' | 'no-token' const SUCCESS_REDIRECT_MS = 1200 /** * Standalone landing page for the email-verification link * (`/verify-email?token=...`). * * Behavior: * - If the user is already verified, short-circuit to a friendly * "Already verified" state. No API call. * - Else fire `POST /auth/email/verify` exactly once (a `useRef` guard keeps * React 19 strict-mode double-invoke from double-firing the call). On * success, refresh the auth store and bounce to `/home`. * - On error, show "Invalid or expired token" + a "Resend" CTA that calls * `POST /auth/email/send-verification`. */ export function VerifyEmailPage() { const [searchParams] = useSearchParams() const navigate = useNavigate() const token = searchParams.get('token') const alreadyVerified = useAuthStore( (s) => Boolean(s.user?.email_verified_at), ) const initialStatus: Status = alreadyVerified ? 'already-verified' : token ? 'loading' : 'no-token' const [status, setStatus] = useState(initialStatus) const [errorMessage, setErrorMessage] = useState('') const [isResending, setIsResending] = useState(false) // Single-fire guard: React 19 strict mode runs effects twice on mount. // Without this, the verify endpoint would burn the token on the first call // and then 400 on the second, flashing an error past the success state. const hasFiredRef = useRef(false) useEffect(() => { if (status !== 'loading') return if (!token) return if (hasFiredRef.current) return hasFiredRef.current = true let cancelled = false authApi .verifyEmail(token) .then(async () => { // Refresh user so `email_verified_at` is populated everywhere. try { await useAuthStore.getState().fetchUser() } catch { // Non-fatal: server confirmed verification, the local user object // will refresh on next page load. } if (cancelled) return setStatus('success') toast.success('Email verified') // Brief success state, then redirect to the dashboard. window.setTimeout(() => { navigate('/home', { replace: true }) }, SUCCESS_REDIRECT_MS) }) .catch((err) => { if (cancelled) return const detail = (err as { response?: { data?: { detail?: string } } }) .response?.data?.detail setErrorMessage(detail ?? 'Invalid or expired verification link') setStatus('error') }) return () => { cancelled = true } }, [status, token, navigate]) const handleResend = async () => { setIsResending(true) try { await authApi.sendVerificationEmail() toast.success('Verification email sent — check your inbox') } catch { toast.error('Failed to send verification email') } finally { setIsResending(false) } } return ( <>
{status === 'loading' && ( <>

Verifying your email…

)} {status === 'success' && ( <>

Email verified

Redirecting you to the dashboard…

Go to dashboard )} {status === 'already-verified' && ( <>

You're already verified

This account's email is already confirmed. No further action needed.

Go to dashboard )} {status === 'error' && ( <>

Verification failed

{errorMessage || 'Invalid or expired verification link'}

Go to dashboard
)} {status === 'no-token' && ( <>

Missing verification token

The link you used doesn't include a verification token. Try the link in your verification email again.

Go to dashboard )}
) } export default VerifyEmailPage