A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { getSetupStatus, setSetupIdentity } from "@/lib/api";import { SetupIdentityMode } from "./setup-identity-mode";import { SetupConfigure } from "./setup-configure";import { SetupAttachAuth } from "./setup-attach-auth";import { SetupVerify } from "./setup-verify";import { SetupTelemetry } from "./setup-telemetry";import { SetupComplete } from "./setup-complete";import { Button } from "@/components/ui/button";import { Skeleton } from "@/components/ui/skeleton";import { Stepper, StepperItem, StepperList, StepperIndicator, StepperSeparator, StepperTitle, StepperTrigger,} from "@/components/ui/stepper";
type SetupStep = | "mode" | "configure" | "attach-auth" | "verify" | "telemetry" | "complete";
export function SetupWizard() { const [currentStep, setCurrentStep] = useState<SetupStep>("mode"); const [identityMode, setIdentityMode] = useState<string | null>(null); const [attachedDid, setAttachedDid] = useState<string | null>(null); const [attachedHandle, setAttachedHandle] = useState<string | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [lastFailedMode, setLastFailedMode] = useState<string | null>(null); const stepContentRef = useRef<HTMLDivElement>(null);
const initialLoadRef = useRef(true); useEffect(() => { if (initialLoadRef.current) { initialLoadRef.current = false; return; } stepContentRef.current?.focus(); }, [currentStep]);
useEffect(() => { getSetupStatus() .then((status) => { if (status.setup_complete) { setCurrentStep("complete"); } else if (status.plc_verified) { setCurrentStep("complete"); } else if (status.identity_configured) { setIdentityMode(status.identity_mode); setCurrentStep("verify"); } else if ( status.identity_mode && status.identity_mode !== "not_exposed" ) { setIdentityMode(status.identity_mode);
const pendingAuth = localStorage.getItem("happyview_attach_auth"); if (status.identity_mode === "attach_account" && pendingAuth) { try { const payload = JSON.parse(pendingAuth) as { attachedDid: string; }; setAttachedDid(payload.attachedDid); setCurrentStep("attach-auth"); } catch { setCurrentStep("configure"); } } else { setCurrentStep("configure"); } } }) .catch(() => {}) .finally(() => setLoading(false)); }, []);
const handleModeSelected = useCallback(async (mode: string) => { setIdentityMode(mode); setError(null); setLastFailedMode(null); if (mode === "not_exposed") { try { await setSetupIdentity({ mode: "not_exposed" }); // Route through telemetry too: the ask is the feature, and skipping // identity setup is no reason to skip the only prompt an operator // will ever see for it. setCurrentStep("telemetry"); } catch (e) { setLastFailedMode(mode); setError( e instanceof Error ? e.message : "Failed to save configuration. Check that your backend is running and try again.", ); } } else if (mode === "attach_account") { setCurrentStep("configure"); } else { try { await setSetupIdentity({ mode }); setCurrentStep("verify"); } catch (e) { setLastFailedMode(mode); setError( e instanceof Error ? e.message : "Failed to configure identity. Check that your backend is running and try again.", ); } } }, []);
const handleGoBack = useCallback(() => { setError(null); switch (currentStep) { case "configure": setCurrentStep("mode"); break; case "attach-auth": setCurrentStep("configure"); break; case "verify": setCurrentStep( identityMode === "attach_account" ? "configure" : "mode", ); break; } }, [currentStep, identityMode]);
const handleConfigureComplete = useCallback( (opts?: { attachedDid?: string; attachedHandle?: string | null }) => { if (opts?.attachedDid) { setAttachedDid(opts.attachedDid); setAttachedHandle(opts.attachedHandle ?? null); setCurrentStep("attach-auth"); } else { setCurrentStep("verify"); } }, [], );
const handleAttachAuthComplete = useCallback(() => { setCurrentStep("verify"); }, []);
const handleVerifyComplete = useCallback(() => { setCurrentStep("telemetry"); }, []);
const handleTelemetryComplete = useCallback(() => { setCurrentStep("complete"); }, []);
const stepOrder = useMemo<SetupStep[]>( () => identityMode === "attach_account" ? ["mode", "configure", "attach-auth", "verify", "telemetry", "complete"] : ["mode", "verify", "telemetry", "complete"], [identityMode], );
const handleStepperNav = useCallback( (value: string) => { const target = value as SetupStep; const currentIndex = stepOrder.indexOf(currentStep); const targetIndex = stepOrder.indexOf(target); if (targetIndex < 0 || targetIndex > currentIndex) return; if (target === "mode") { setIdentityMode(null); setAttachedDid(null); setAttachedHandle(null); setError(null); } setCurrentStep(target); }, [currentStep, stepOrder], );
if (loading) { return ( <div className="space-y-4" role="status" aria-label="Loading setup"> <Skeleton className="h-10 w-full" /> <Skeleton className="h-64 w-full rounded-xl" /> </div> ); }
return ( <Stepper value={currentStep} onValueChange={handleStepperNav}> <StepperList> <StepperItem value="mode"> <StepperTrigger> <StepperIndicator /> <StepperTitle>Identity</StepperTitle> </StepperTrigger> <StepperSeparator /> </StepperItem> {identityMode === "attach_account" && ( <> <StepperItem value="configure"> <StepperTrigger> <StepperIndicator /> <StepperTitle>Account</StepperTitle> </StepperTrigger> <StepperSeparator /> </StepperItem> <StepperItem value="attach-auth"> <StepperTrigger> <StepperIndicator /> <StepperTitle>Sign In</StepperTitle> </StepperTrigger> <StepperSeparator /> </StepperItem> </> )} <StepperItem value="verify"> <StepperTrigger> <StepperIndicator /> <StepperTitle> {identityMode === "did_plc" ? "Key Backup" : identityMode === "attach_account" ? "Verify" : "Review"} </StepperTitle> </StepperTrigger> <StepperSeparator /> </StepperItem> <StepperItem value="telemetry"> <StepperTrigger> <StepperIndicator /> <StepperTitle>Telemetry</StepperTitle> </StepperTrigger> <StepperSeparator /> </StepperItem> <StepperItem value="complete"> <StepperTrigger> <StepperIndicator /> <StepperTitle>Done</StepperTitle> </StepperTrigger> </StepperItem> </StepperList>
{error && ( <div role="alert" className="mt-4 rounded-lg border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive flex items-center justify-between gap-3" > <span>{error}</span> {lastFailedMode && ( <Button variant="ghost" size="sm" className="text-destructive shrink-0" onClick={() => { if (lastFailedMode) handleModeSelected(lastFailedMode); }} > Try again </Button> )} </div> )}
<div ref={stepContentRef} tabIndex={-1} className="mt-8 outline-none"> {currentStep === "mode" && ( <SetupIdentityMode onComplete={handleModeSelected} /> )} {currentStep === "configure" && identityMode && ( <SetupConfigure mode={identityMode} onComplete={handleConfigureComplete} onBack={handleGoBack} /> )} {currentStep === "attach-auth" && attachedDid && ( <SetupAttachAuth attachedDid={attachedDid} attachedHandle={attachedHandle} onComplete={handleAttachAuthComplete} onBack={handleGoBack} /> )} {currentStep === "verify" && identityMode && ( <SetupVerify mode={identityMode} onComplete={handleVerifyComplete} onBack={handleGoBack} /> )} {currentStep === "telemetry" && ( <SetupTelemetry onNext={handleTelemetryComplete} /> )} {currentStep === "complete" && ( <SetupComplete identityMode={identityMode} /> )} </div> </Stepper> );}