diff --git a/frontend/src/OnboardingWizard.css b/frontend/src/OnboardingWizard.css index fecba5a..6d5208e 100644 --- a/frontend/src/OnboardingWizard.css +++ b/frontend/src/OnboardingWizard.css @@ -55,11 +55,20 @@ max-width: 320px; } -.onboarding-wizard-actions { +.onboarding-wizard-name-row { display: flex; - gap: 0.75rem; + gap: 0.5rem; + align-items: stretch; } -.onboarding-wizard-actions button { +.onboarding-wizard-name-row input { flex: 1; + min-width: 0; +} + +.onboarding-wizard-icon-button { + flex: 0 0 auto; + padding: 0.6rem 0.9rem; + font-size: 1.1rem; + line-height: 1; } diff --git a/frontend/src/OnboardingWizard.tsx b/frontend/src/OnboardingWizard.tsx index 5799584..8366e62 100644 --- a/frontend/src/OnboardingWizard.tsx +++ b/frontend/src/OnboardingWizard.tsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import { api, BASE_URL } from "./api/client"; +import { useEffect, useRef, useState } from "react"; +import { api } from "./api/client"; import "./OnboardingWizard.css"; import type { AuthResponse } from "./types/api"; @@ -9,9 +9,10 @@ type Step = "name" | "garmin"; // 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, logging out) leaves no trace -// in the database -- there's no half-created account to clean up or -// re-prompt for later. +// 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(""); @@ -21,6 +22,7 @@ export function OnboardingWizard({ onCreated }: { onCreated: (displayName: strin 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(); @@ -75,26 +77,38 @@ export function OnboardingWizard({ onCreated }: { onCreated: (displayName: strin } } + // 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 - /> +
+ setDisplayName(e.target.value)} + autoFocus + /> + +
{error &&

{error}

} - -
-
-
); @@ -107,10 +121,15 @@ export function OnboardingWizard({ onCreated }: { onCreated: (displayName: strin {auth?.status === "authenticated" ? ( <> -

Connected to Garmin.

- +

Connected to Garmin — finishing setup…

+ {error && ( + <> +

{error}

+ + + )} ) : auth?.status === "mfa_required" ? (
@@ -124,21 +143,9 @@ export function OnboardingWizard({ onCreated }: { onCreated: (displayName: strin /> {auth.message &&

{auth.message}

} {error &&

{error}

} -
- - -
+
) : (
@@ -159,29 +166,11 @@ export function OnboardingWizard({ onCreated }: { onCreated: (displayName: strin /> {auth?.message &&

{auth.message}

} {error &&

{error}

} -
- - -
+
)} - -
- -
); }