"use client"; /** * LoginForm — pure form widget reused by both the dedicated /login page and * the AuthGate modal that pops up when an unauthenticated user tries to use * a protected feature. * * Two stages: * 1. Password — email + password. If the BFF signals `otp_required` * (account unverified for >30 days), the form advances to stage 2. * 2. OTP — 6-digit code emailed to the user. Submitting verifies the * code, marks the email verified, and completes the session. * * `onSuccess` fires after the session is finalised (either at stage 1 for * verified accounts or stage 2 for the OTP-gated path). Callers decide * what happens next (page redirect, modal close, etc.) — the form itself * doesn't navigate. */ import { useState } from "react"; import Link from "next/link"; import { useAuth } from "@/components/auth/auth-provider"; import { OAuthButtons } from "@/components/auth/oauth-buttons"; import { AuthError } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; export function LoginForm({ onSuccess, forgotPasswordHref = "/forgot-password", showForgotLink = true, oauthNext, }: { onSuccess?: () => void; forgotPasswordHref?: string; showForgotLink?: boolean; /** Post-OAuth redirect path on the frontend. */ oauthNext?: string; }) { const { logIn, verifyLoginOTP } = useAuth(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [otp, setOTP] = useState(""); // Two-stage state machine. "password" is the default; we advance to // "otp" only after the BFF signals otp_required. const [stage, setStage] = useState<"password" | "otp">("password"); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); async function onPasswordSubmit(e: React.SyntheticEvent) { e.preventDefault(); setError(null); setSubmitting(true); try { const res = await logIn(email, password); if (res.kind === "otp_required") { setStage("otp"); return; } onSuccess?.(); } catch (err) { setError(err instanceof AuthError ? err.message : "Login failed."); } finally { setSubmitting(false); } } async function onOTPSubmit(e: React.SyntheticEvent) { e.preventDefault(); setError(null); setSubmitting(true); try { await verifyLoginOTP(email, password, otp); onSuccess?.(); } catch (err) { setError( err instanceof AuthError ? err.message : "Could not verify the code.", ); } finally { setSubmitting(false); } } if (stage === "otp") { return (
setOTP(e.target.value.replace(/\D/g, "").slice(0, 6)) } placeholder="123456" className="tracking-[0.4em] text-center text-lg" />

We sent a code to {email}. Your account has been unverified for a while, so we need this extra step. The code expires in 10 minutes.

{error ?

{error}

: null}
); } return (
setEmail(e.target.value)} />
{showForgotLink ? ( Forgot? ) : null}
setPassword(e.target.value)} />
{error ?

{error}

: null}
); }