import { useEffect, useRef, useState } from "react"; import { api } from "./api/client"; import "./OnboardingWizard.css"; import type { AuthResponse } from "./types/api"; type Step = "name" | "garmin"; // Replaces the old CreateProfile.tsx + ConnectGarmin.tsx: a single wizard, // since nothing is persisted to the database until Garmin actually // connects (see // docs/superpowers/specs/2026-07-26-onboarding-wizard-deferred-commit-design.md). // Quitting partway through (closing the tab) leaves no trace in the // database -- there's no half-created account to clean up or re-prompt // for later, so there's deliberately no "Log out" escape hatch anywhere in // this flow: closing the tab *is* the escape hatch. export function OnboardingWizard({ onCreated }: { onCreated: (displayName: string) => void }) { const [step, setStep] = useState("name"); const [displayName, setDisplayName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [code, setCode] = useState(""); const [auth, setAuth] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const completeTriggered = useRef(false); function submitName(e: React.FormEvent) { e.preventDefault(); if (!displayName.trim()) { setError("Please enter a display name."); return; } setError(null); setStep("garmin"); } async function submitGarmin(e: React.FormEvent) { e.preventDefault(); if (!email.trim() || !password) { setError("Please enter your Garmin email and password."); return; } setBusy(true); setError(null); try { setAuth(await api.setupGarminLogin(email.trim(), password)); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong, please try again."); } finally { setBusy(false); } } async function submitMFA() { if (!code.trim()) return; setBusy(true); setError(null); try { setAuth(await api.setupGarminMFA(code.trim())); setCode(""); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong, please try again."); } finally { setBusy(false); } } async function complete() { setBusy(true); setError(null); try { const result = await api.setupComplete(displayName.trim()); onCreated(result.display_name); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong, please try again."); setBusy(false); } } // Once Garmin reports success (whether MFA was needed or not), finish // setup immediately -- no "Continue to geniusrun" click required. Guarded // by a ref (not just the effect's dependency array) so this can never // fire twice, e.g. under React StrictMode's double-invoked effects in // development. useEffect(() => { if (auth?.status === "authenticated" && !completeTriggered.current) { completeTriggered.current = true; complete(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [auth?.status]); if (step === "name") { return (

🧞‍♀️ Welcome to geniusrun

Let's set up your profile. You'll connect your Garmin account next.

setDisplayName(e.target.value)} autoFocus />
{error &&

{error}

}
); } return (

🧞‍♀️ Connect your Garmin account

geniusrun needs your Garmin credentials to sync your activities.

{auth?.status === "authenticated" ? ( <>

Connected to Garmin — finishing setup…

{error && ( <>

{error}

)} ) : auth?.status === "mfa_required" ? (
setCode(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submitMFA()} disabled={busy} autoFocus /> {auth.message &&

{auth.message}

} {error &&

{error}

}
) : (
setEmail(e.target.value)} disabled={busy} autoFocus /> setPassword(e.target.value)} disabled={busy} /> {auth?.message &&

{auth.message}

} {error &&

{error}

}
)}
); }