import { useEffect, useState } from "react"; import { api } from "../api/client"; import type { AuthResponse } from "../types/api"; import { SyncModal } from "./SyncModal"; export function GarminConnection({ onBeforeConnect }: { onBeforeConnect?: () => Promise }) { const [auth, setAuth] = useState(null); const [code, setCode] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // There's no real Garmin logout -- the backend session just sits idle. // "Disconnect" only hides that and shows Connect again locally; a real // login (connect()) clears it. const [disconnected, setDisconnected] = useState(false); const [showSyncModal, setShowSyncModal] = useState(false); // Auth status alone -- sync status is polled by SyncModal itself while // it's open, so this no longer needs the "poll faster while syncing" // dynamic interval it used to have. useEffect(() => { api.authStatus().then(setAuth).catch((e) => setError(String(e))); const interval = setInterval(() => { api.authStatus().then(setAuth).catch(() => {}); }, 6000); return () => clearInterval(interval); }, []); async function connect() { setBusy(true); setError(null); try { await onBeforeConnect?.(); setAuth(await api.login()); setDisconnected(false); } catch (e) { setError(String(e)); } finally { setBusy(false); } } function disconnect() { setDisconnected(true); } async function submitMFA() { if (!code.trim()) return; setBusy(true); setError(null); try { setAuth(await api.submitMFA(code.trim())); setCode(""); } catch (e) { setError(String(e)); } finally { setBusy(false); } } async function sync() { setBusy(true); setError(null); try { await api.syncRun(); setShowSyncModal(true); } catch (e) { setError(String(e)); } finally { setBusy(false); } } async function resetAll() { if (!window.confirm("This deletes every synced activity, lap, and kind assignment, then starts a fresh pull from Garmin next sync. This cannot be undone. Continue?")) { return; } setBusy(true); setError(null); try { await api.resetSync(); // Reset runs as a background sync job; wait for it to actually finish // before reloading, otherwise other pages (e.g. Activities) would // still show the just-deleted activities from their own stale state. let status = await api.syncStatus(); while (status.in_progress) { await new Promise((resolve) => setTimeout(resolve, 300)); status = await api.syncStatus(); } window.location.reload(); } catch (e) { setError(String(e)); setBusy(false); } } const status = disconnected ? "unknown" : auth?.status ?? "unknown"; return (
{status !== "authenticated" && status !== "mfa_required" && ( )} {status === "authenticated" && ( <> )} {/* Reset all only wipes local DB state (see handleSyncReset) -- it doesn't touch the Garmin session, so it stays available even while disconnected/not-yet-connected. */}
{/* Connected/not-connected is already conveyed by the Connect/Disconnect button itself -- only MFA/failed need a label, since no button distinguishes those from "not connected". */} {(status === "mfa_required" || status === "failed") && ( {status === "mfa_required" ? "MFA code required" : "Connection failed"} )}
{status === "mfa_required" && (
setCode(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submitMFA()} />
)} {!disconnected && auth?.message &&

{auth.message}

} {error &&

{error}

} {showSyncModal && setShowSyncModal(false)} />}
); }