import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; import { GraduationCap, Loader2 } from "lucide-react"; import { useResendOtp, useVerifyEmail } from "@/hooks/queries/useSignup"; import { ApiError } from "@/lib/api-client"; const RESEND_LIMIT = 3; const COOLDOWN_SEC = 60; function errorMessage(err: unknown): string { if (err instanceof ApiError) return err.message; if (err instanceof Error) return err.message; return "Verification failed"; } export default function EmailVerification() { const [searchParams] = useSearchParams(); const email = searchParams.get("email")?.trim() || ""; const navigate = useNavigate(); const verify = useVerifyEmail(); const resend = useResendOtp(); const [otp, setOtp] = useState(""); const [error, setError] = useState(null); const [resendAttempts, setResendAttempts] = useState(0); const [cooldown, setCooldown] = useState(0); const submittingRef = useRef(false); const lastSubmittedOtp = useRef(null); useEffect(() => { if (otp.length < 6) lastSubmittedOtp.current = null; }, [otp]); useEffect(() => { if (cooldown <= 0) return; const t = window.setInterval(() => { setCooldown((c) => (c <= 1 ? 0 : c - 1)); }, 1000); return () => window.clearInterval(t); }, [cooldown]); const runVerify = useCallback( async (code: string) => { if (!email || code.length !== 6) return; setError(null); submittingRef.current = true; try { await verify.mutateAsync({ email, otp: code }); navigate("/onboarding", { replace: true }); } catch (e) { lastSubmittedOtp.current = null; setError(errorMessage(e)); } finally { submittingRef.current = false; } }, [email, navigate, verify], ); useEffect(() => { if (otp.length !== 6 || submittingRef.current || verify.isPending) return; if (lastSubmittedOtp.current === otp) return; lastSubmittedOtp.current = otp; void runVerify(otp); }, [otp, runVerify, verify.isPending]); const handleResend = async () => { if (!email || cooldown > 0 || resendAttempts >= RESEND_LIMIT || resend.isPending) return; setError(null); try { await resend.mutateAsync(email); setResendAttempts((n) => n + 1); setCooldown(COOLDOWN_SEC); } catch (e) { setError(errorMessage(e)); } }; if (!email) { return (
Missing email Add an email query parameter or return to registration.
); } return (

Verify your email

Check your inbox We've sent a 6-digit verification code to{" "} {email}
{verify.isPending && (
Verifying…
)}
{error && (

{error}

)}

Resend attempts:{" "} {resendAttempts}/{RESEND_LIMIT}

Wrong address?{" "} Change email

); }