From 47efd1bd5aef4de2fe77987825b0c8eb9173c551 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 27 May 2026 14:50:05 +0000 Subject: [PATCH] Drop useEffect from VerifyCodeForm; replace tick-based countdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/app/auth/verify-code/VerifyCodeForm.tsx | 58 ++++++++++----------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/app/auth/verify-code/VerifyCodeForm.tsx b/src/app/auth/verify-code/VerifyCodeForm.tsx index a38ee9a..c0e42c1 100644 --- a/src/app/auth/verify-code/VerifyCodeForm.tsx +++ b/src/app/auth/verify-code/VerifyCodeForm.tsx @@ -1,36 +1,33 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +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_SECONDS = 30; +const RESEND_COOLDOWN_MS = 30_000; -type Status = "idle" | "verifying" | "resending"; +type VerifyStatus = "idle" | "verifying"; +type ResendStatus = "idle" | "resending" | "cooldown"; export default function VerifyCodeForm({ email }: { email: string }) { const router = useRouter(); const [code, setCode] = useState(""); - const [status, setStatus] = useState("idle"); + const [verifyStatus, setVerifyStatus] = useState("idle"); + const [resendStatus, setResendStatus] = useState("idle"); const [error, setError] = useState(null); - const [resendCountdown, setResendCountdown] = useState(0); const [resendNotice, setResendNotice] = useState(null); const inFlightRef = useRef(false); - // Resend cooldown timer - useEffect(() => { - if (resendCountdown <= 0) return; - const id = setTimeout(() => setResendCountdown((s) => s - 1), 1000); - return () => clearTimeout(id); - }, [resendCountdown]); + const isWorking = + verifyStatus === "verifying" || resendStatus === "resending"; async function submitCode(value: string) { if (inFlightRef.current) return; if (!/^\d{6}$/.test(value)) return; inFlightRef.current = true; - setStatus("verifying"); + setVerifyStatus("verifying"); setError(null); const res = await signIn("email-code", { @@ -46,7 +43,7 @@ export default function VerifyCodeForm({ email }: { email: string }) { return; } - setStatus("idle"); + 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.", @@ -62,25 +59,33 @@ export default function VerifyCodeForm({ email }: { email: string }) { } async function handleResend() { - if (resendCountdown > 0 || status === "resending") return; - if (!email) return; - setStatus("resending"); + if (isWorking || resendStatus === "cooldown" || !email) return; + setResendStatus("resending"); setError(null); setResendNotice(null); const res = await signIn("email", { email, redirect: false }); - setStatus("idle"); 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."); - setResendCountdown(RESEND_COOLDOWN_SECONDS); + setResendStatus("cooldown"); + setTimeout(() => setResendStatus("idle"), RESEND_COOLDOWN_MS); } + const resendLabel = + resendStatus === "resending" + ? "Sending…" + : resendStatus === "cooldown" + ? "Code sent — wait a moment" + : "Resend code"; + return (
@@ -98,7 +103,7 @@ export default function VerifyCodeForm({ email }: { email: string }) { onChange={(e) => handleChange(e.target.value)} placeholder="••••••" className="h-12 text-center text-2xl tracking-[0.5em] font-mono" - disabled={status === "verifying"} + disabled={verifyStatus === "verifying"} autoFocus />
@@ -106,10 +111,10 @@ export default function VerifyCodeForm({ email }: { email: string }) {
@@ -120,19 +125,12 @@ export default function VerifyCodeForm({ email }: { email: string }) {
-
); }