// Sign in (SD-0001 §5.2) — the single email + one-time-code flow behind both doors. Step 1 // requests a code; step 2 verifies it. Honest copy for every §5.2 state; storefront routing // is handled by App on success. Visuals per the ui/designs export (hf-signin). import { useEffect, useState } from "react"; import { requestCode, verifyCode, type ApiError, type VerifyResult } from "../api"; import { cooldownAppliesTo } from "../cooldown"; import CodeInput from "../ui/CodeInput"; import { AuthCard, Banner, Field, Footer, PrimaryButton, Screen, TopBar, Wordmark } from "../ui/kit"; type Door = "signup" | "login"; interface Props { door: Door; onAuthed: (result: VerifyResult) => void; onBack: () => void; } function useCooldown(): [number, (s: number) => void] { const [left, setLeft] = useState(0); const ticking = left > 0; useEffect(() => { if (!ticking) return; const t = setInterval(() => setLeft((v) => Math.max(0, v - 1)), 1000); return () => clearInterval(t); }, [ticking]); return [left, setLeft]; } export default function SignIn({ door, onAuthed, onBack }: Props) { const [step, setStep] = useState<"email" | "code">("email"); const [email, setEmail] = useState(""); const [code, setCode] = useState(""); const [busy, setBusy] = useState(false); const [fieldError, setFieldError] = useState(null); const [codeError, setCodeError] = useState(null); const [bannerError, setBannerError] = useState(null); const [cooldown, setCooldown] = useCooldown(); // The address the running cooldown belongs to — the server's cooldown is per-address // (INV-3), so a different address must not be blocked by it (bug #20). const [cooldownFor, setCooldownFor] = useState(null); function applyError(err: ApiError) { setFieldError(null); setCodeError(null); setBannerError(null); if (err.code === "invalid_email") setFieldError(err.message); else if (err.code === "resend_cooldown") setCooldown(err.retry_after_s ?? 60); else if (err.code === "code_mismatch" || err.code === "code_expired") setCodeError(err.message); else setBannerError(err); // delivery_failed, code_exhausted, unexpected } async function sendCode(): Promise { setBusy(true); setFieldError(null); setBannerError(null); const err = await requestCode(email); setBusy(false); if (err) { applyError(err); if (err.code === "resend_cooldown") setCooldownFor(email); return err.code === "resend_cooldown"; // a cooldown still means a code is out there } setCooldown(60); setCooldownFor(email); return true; } async function submitEmail(e: React.FormEvent) { e.preventDefault(); if (await sendCode()) { setCode(""); setCodeError(null); setStep("code"); } } async function resend() { setCodeError(null); setCode(""); await sendCode(); } async function submitCode(e: React.FormEvent) { e.preventDefault(); setBusy(true); setCodeError(null); setBannerError(null); const res = await verifyCode(email, code); setBusy(false); if (!res.ok) { applyError(res.error); return; } onAuthed(res.result); } const heading = door === "signup" ? "Create your storefront" : "Log in"; const sub = door === "signup" ? "Enter your email to begin. There's no password to create." : "Enter your email and we'll send a one-time code to sign in."; return ( } right={ } />
{step === "email" ? (

{heading}

{sub}

{bannerError && ( The email didn't go out. Try again in a moment — nothing was lost. )} setEmail(ev.target.value), }} />
{busy ? "Sending…" : cooldownAppliesTo(email, cooldownFor, cooldown) ? `Resend in ${cooldown}s` : "Send code"}

We'll email you a one-time code. That's all we need — no password, ever.

) : (

Check your email

We sent a 6-digit code to {email}.{" "}

{bannerError && ( {bannerError.code === "code_exhausted" ? "That code is no longer valid. Request a fresh one to keep going." : bannerError.message} )}
{codeError && (

{codeError}

)}
{busy ? "Checking…" : "Continue"}

Good for 10 minutes.

{cooldown > 0 ? ( Resend in {cooldown}s ) : ( )}
)}