assessment-model/src/app/auth/verify-code/VerifyCodeForm.tsx
Khalim Conn-Kowlessar 47efd1bd5a Drop useEffect from VerifyCodeForm; replace tick-based countdown
The per-second countdown was the only thing pulling useEffect into this
component, in violation of the project rule (CLAUDE.md: avoid useEffect,
prefer event handlers). The hook was driving a tick purely so we could
show "Resend in 28s" / "27s" / ... — none of which is load-bearing.

Replace it with a single setTimeout fired from the resend event handler
that flips resendStatus from "cooldown" back to "idle" after 30 seconds.
The button stays disabled with "Code sent — wait a moment" instead of
showing a live countdown. Same blocking signal, no hook needed.

While here, split the single status enum into verifyStatus + resendStatus
so the verify button no longer wrongly disables during a resend cooldown
(latent bug from the previous shape).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:50:05 +00:00

136 lines
4 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { Button } from "@/app/shadcn_components/ui/button";
import { Input } from "@/app/shadcn_components/ui/input";
const RESEND_COOLDOWN_MS = 30_000;
type VerifyStatus = "idle" | "verifying";
type ResendStatus = "idle" | "resending" | "cooldown";
export default function VerifyCodeForm({ email }: { email: string }) {
const router = useRouter();
const [code, setCode] = useState("");
const [verifyStatus, setVerifyStatus] = useState<VerifyStatus>("idle");
const [resendStatus, setResendStatus] = useState<ResendStatus>("idle");
const [error, setError] = useState<string | null>(null);
const [resendNotice, setResendNotice] = useState<string | null>(null);
const inFlightRef = useRef(false);
const isWorking =
verifyStatus === "verifying" || resendStatus === "resending";
async function submitCode(value: string) {
if (inFlightRef.current) return;
if (!/^\d{6}$/.test(value)) return;
inFlightRef.current = true;
setVerifyStatus("verifying");
setError(null);
const res = await signIn("email-code", {
email,
code: value,
redirect: false,
});
inFlightRef.current = false;
if (res?.ok) {
router.push("/home");
return;
}
setVerifyStatus("idle");
setCode("");
setError(
"That code didn't match. Check the latest email — older codes stop working as soon as you request a new one.",
);
}
function handleChange(next: string) {
const digits = next.replace(/\D/g, "").slice(0, 6);
setCode(digits);
if (error) setError(null);
if (resendNotice) setResendNotice(null);
if (digits.length === 6) void submitCode(digits);
}
async function handleResend() {
if (isWorking || resendStatus === "cooldown" || !email) return;
setResendStatus("resending");
setError(null);
setResendNotice(null);
const res = await signIn("email", { email, redirect: false });
if (res?.error) {
setResendStatus("idle");
setError(
"We couldn't send a new code right now. Wait a minute and try again.",
);
return;
}
setResendNotice("A new code is on its way. Older codes stop working.");
setResendStatus("cooldown");
setTimeout(() => setResendStatus("idle"), RESEND_COOLDOWN_MS);
}
const resendLabel =
resendStatus === "resending"
? "Sending…"
: resendStatus === "cooldown"
? "Code sent — wait a moment"
: "Resend code";
return (
<div className="space-y-4 text-left">
<div>
<label
htmlFor="verify-code"
className="block text-xs font-medium text-gray-600 mb-2 text-center"
>
Sign-in code
</label>
<Input
id="verify-code"
inputMode="numeric"
autoComplete="one-time-code"
value={code}
onChange={(e) => handleChange(e.target.value)}
placeholder="••••••"
className="h-12 text-center text-2xl tracking-[0.5em] font-mono"
disabled={verifyStatus === "verifying"}
autoFocus
/>
</div>
<Button
type="button"
onClick={() => submitCode(code)}
disabled={isWorking || code.length !== 6}
className="bg-brandbrown hover:bg-hoverblue w-full text-base py-3"
>
{verifyStatus === "verifying" ? "Verifying…" : "Sign in"}
</Button>
<div className="text-center text-sm space-y-2">
{error && <p className="text-red-500">{error}</p>}
{resendNotice && !error && (
<p className="text-green-600">{resendNotice}</p>
)}
<button
type="button"
onClick={handleResend}
disabled={isWorking || resendStatus === "cooldown" || !email}
className="text-brandblue hover:underline disabled:text-gray-400 disabled:no-underline"
>
{resendLabel}
</button>
</div>
</div>
);
}