import { useEffect, useRef, useState, type FormEvent } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { authApi, safeNext, type LoginResult, type Providers } from "../api/auth"; import { ApiError } from "../api/client"; import Logo from "../components/Logo"; import SkyCanvas from "../components/SkyCanvas"; import { useAuth } from "../hooks/useAuth"; /** Mirrors validate_password on the backend. */ const MIN_PASSWORD = 8; type Mode = "signin" | "signup" | "verify" | "forgot" | "reset"; declare global { interface Window { google?: { accounts: { id: { initialize: (cfg: { client_id: string; callback: (r: { credential: string }) => void }) => void; renderButton: (el: HTMLElement, opts: Record) => void; }; }; }; } } const GOOGLE_CLIENT_ID = (import.meta.env.VITE_GOOGLE_CLIENT_ID || import.meta.env.GOOGLE_CLIENT_ID) as | string | undefined; function GoogleRedirectButton({ next }: { next: string }) { return ( Continue with Google ); } function GoogleButton({ onCredential }: { onCredential: (c: string) => void }) { const ref = useRef(null); useEffect(() => { if (!GOOGLE_CLIENT_ID || !ref.current) return; const render = () => { if (!window.google || !ref.current) return; window.google.accounts.id.initialize({ client_id: GOOGLE_CLIENT_ID, callback: (r) => onCredential(r.credential) }); window.google.accounts.id.renderButton(ref.current, { theme: "filled_black", size: "large", shape: "pill", text: "continue_with", width: 320, }); }; if (window.google) return render(); const script = document.createElement("script"); script.src = "https://accounts.google.com/gsi/client"; script.async = true; script.onload = render; document.head.appendChild(script); }, [onCredential]); if (!GOOGLE_CLIENT_ID) { return null; } return
; } export default function AuthPage() { const [params] = useSearchParams(); const [mode, setMode] = useState(params.get("mode") === "signup" ? "signup" : "signin"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [name, setName] = useState(""); const [code, setCode] = useState(""); const [showPassword, setShowPassword] = useState(false); // The current code is out of tries or expired: the only ways forward are a new code or a new email. const [codeDead, setCodeDead] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [busy, setBusy] = useState(false); const [providers, setProviders] = useState(null); const [isNew, setIsNew] = useState(false); const { complete, user } = useAuth(); const navigate = useNavigate(); const next = safeNext(params.get("next")); useEffect(() => { // New accounts get onboarding, except people who signed up to use a free tool: send them straight back to it. if (user) navigate(isNew && !next.startsWith("/tools/") ? "/welcome" : next, { replace: true }); }, [user, navigate, next, isNew]); useEffect(() => { authApi .providers() .then(setProviders) .catch(() => setProviders({ google: GOOGLE_CLIENT_ID ? "popup" : null })); }, []); // The Google redirect flow lands back here with #error=... when it fails. useEffect(() => { const failure = new URLSearchParams(window.location.hash.slice(1)).get("error"); if (!failure) return; setError(failure); window.history.replaceState(null, "", window.location.pathname + window.location.search); }, []); const run = async (fn: () => Promise) => { setBusy(true); setError(null); try { const result = await fn(); if (result) { if (result.created) setIsNew(true); complete(result); } } catch (e) { setError(e instanceof ApiError ? e.message : "Something went wrong. Try again."); if (!(e instanceof ApiError)) return; // A dead code cannot be retried; clear it so the next step is clearly "Resend code". if (e.code === "too_many_attempts" || e.code === "expired") { setCode(""); setCodeDead(true); } // Registered with the other method: back to sign in, where the Google button sits. if (e.code === "wrong_provider") setMode("signin"); } finally { setBusy(false); } }; useEffect(() => setCodeDead(false), [mode]); const resend = () => run(async () => { if (mode === "verify") await authApi.registerResend(email); else await authApi.forgotStart(email); setCodeDead(false); setNotice("New code sent."); }); const tryAnotherEmail = () => { setCode(""); setError(null); setNotice(null); setMode(mode === "verify" ? "signup" : "forgot"); }; const onGoogle = (credential: string) => run(() => authApi.google(credential)); const submit = (e: FormEvent) => { e.preventDefault(); if (mode === "signin") return run(() => authApi.login(email, password)); if (mode === "signup") return run(async () => { await authApi.registerStart(email, password, name); setMode("verify"); setNotice(`We sent a 6 digit code to ${email}.`); }); if (mode === "verify") return run(() => authApi.registerVerify(email, code)); if (mode === "forgot") return run(async () => { await authApi.forgotStart(email); setMode("reset"); setNotice(`We sent a reset code to ${email}.`); }); return run(() => authApi.forgotComplete(email, code, password)); }; // New passwords (sign up, reset) must meet the length rule before the form can be sent. const passwordTooShort = (mode === "signup" || mode === "reset") && password.length < MIN_PASSWORD; const titles: Record = { signin: "Welcome back", signup: "Create your account", verify: "Check your email", forgot: "Reset your password", reset: "Choose a new password", }; const cta: Record = { signin: "Sign in", signup: "Send my code", verify: "Verify and continue", forgot: "Send reset code", reset: "Save and sign in", }; return (

{titles[mode]}

{notice &&

{notice}

} {(mode === "signin" || mode === "signup") && ( <> {providers?.google === "redirect" && } {providers?.google === "popup" && } {!providers &&
}
or with email
)}
{mode === "signup" && ( )} {mode !== "verify" && mode !== "reset" && ( )} {(mode === "verify" || mode === "reset") && ( )} {(mode === "signin" || mode === "signup" || mode === "reset") && ( )} {error && (

{error}

)} {codeDead ? ( ) : ( )}
{mode === "signin" && ( <> New here?{" "} )} {mode === "signup" && ( Have an account?{" "} )} {(mode === "verify" || mode === "reset") && (codeDead ? ( ) : ( ))} {(mode === "forgot" || mode === "reset") && ( )}
); }