import { useEffect, useState } from "react"; import { api, BASE_URL, NetworkError } from "./api/client"; import { showError } from "./banner"; import { BannerStack } from "./components/BannerStack"; import "./LoginGate.css"; import App from "./App"; import { OnboardingWizard } from "./OnboardingWizard"; import type { SessionInfo } from "./types/api"; type Status = "loading" | "authenticated" | "unauthenticated"; const AUTH_ERROR_MESSAGES: Record = { forbidden: "Your account isn't authorized for geniusrun.", failed: "Login failed, please try again.", }; // Wraps App: on mount, asks the backend whether this browser already has a // valid session (GET /api/session/me). geniusrun has no anonymous view, so // this is the first fork -- "show the login screen" vs "show the app." A // second fork, once authenticated, is whether the session's account has a // provisioned profile yet (session.has_profile) -- a brand-new OIDC login // sees OnboardingWizard instead of App until it completes one. Nothing is // persisted until the wizard's Garmin-connect step actually succeeds (see // docs/superpowers/specs/2026-07-26-onboarding-wizard-deferred-commit-design.md), // so has_profile alone is always sufficient here -- there's no separate // "provisioned but not connected" state to gate on. export function LoginGate() { const [status, setStatus] = useState("loading"); const [session, setSession] = useState(null); useEffect(() => { api .getSessionInfo() .then((s) => { setSession(s); setStatus("authenticated"); }) .catch((e) => { // A NetworkError here means the backend itself isn't reachable, not // that this browser is genuinely logged out -- worth a banner, since // otherwise this is the one place in the app where "backend not // here" would show no feedback at all. Either way the screen still // falls through to "unauthenticated" (showing the Log in button), // since there's no session to trust regardless of why the check // failed. if (e instanceof NetworkError) showError(e.message); setStatus("unauthenticated"); }); }, []); // Keycloak redirects back here with ?auth_error= when the // login-gate's own role check (not Keycloak's own authentication) rejects // a session -- surfaced once, as a banner, the moment this screen is // reached. useEffect(() => { if (status !== "unauthenticated") return; const authError = new URLSearchParams(window.location.search).get("auth_error"); if (authError) showError(AUTH_ERROR_MESSAGES[authError] ?? "Login failed, please try again."); }, [status]); if (status === "loading") { return ( <>
Loading…
); } if (status === "unauthenticated") { return ( <>

🧞‍♀️ geniusrun

Log in
); } if (!session!.has_profile) { return ( setSession((s) => (s ? { ...s, has_profile: true, display_name: displayName } : s))} /> ); } return ; }