import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { InlineView } from "@/components/surface/surface-scope"; import { toast } from "sonner"; import { AlertCircle, Loader2, X } from "lucide-react"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { Switch } from "@/components/switch"; import { ElectronLoginForm } from "@/auth/ElectronLoginForm"; import { completeLink, getLinkPreview, probeServer, reloginLink, type ProbeProblem, type ServerTarget, type SyncStatus, } from "@/api/sync-api"; import { ProxySettingsFields } from "./ProxySettingsFields"; import { forgetServerUrl, getSavedServerUrls, rememberServerUrl, } from "./saved-servers"; import { DocsLink } from "@/components/docs-link"; type Step = "server" | "signin" | "choose" | "finishing"; interface LinkServerDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onLinked: (status: SyncStatus) => void; /** Sign back in to the server this desktop is already linked to. */ relogin?: { serverUrl: string }; } function problemText( t: (key: string, options?: Record) => string, problem: ProbeProblem | undefined, message: string | undefined, ): string { switch (problem) { case "invalid_url": return t("sync.problems.invalidUrl"); case "tls_untrusted": return t("sync.problems.tlsUntrusted"); case "basic_auth_required": return t("sync.problems.basicAuthRequired"); case "proxy_login": return t("sync.problems.proxyLogin"); case "not_termix": return t("sync.problems.notTermix"); case "version_mismatch": return t("sync.problems.versionMismatch"); case "different_account": return t("sync.problems.differentAccount"); default: return message ? t("sync.problems.unreachableWithReason", { reason: message }) : t("sync.problems.unreachable"); } } export function LinkServerDialog({ open, onOpenChange, onLinked, relogin, }: LinkServerDialogProps) { const { t } = useTranslation(); const [step, setStep] = useState(relogin ? "signin" : "server"); const [savedUrls, setSavedUrls] = useState(getSavedServerUrls); const [target, setTarget] = useState({ serverUrl: relogin?.serverUrl ?? "", customHeaders: [], basicAuth: null, allowInvalidCertificate: false, }); const [showAdvanced, setShowAdvanced] = useState(false); const [checking, setChecking] = useState(false); const [problem, setProblem] = useState(null); const [problemKind, setProblemKind] = useState(null); const [serverName, setServerName] = useState(); const [token, setToken] = useState(null); const [counts, setCounts] = useState>({}); const [mode, setMode] = useState<"merge" | "replace">("merge"); const [finishError, setFinishError] = useState(null); useEffect(() => { if (!open) return; setStep(relogin ? "signin" : "server"); setProblem(null); setProblemKind(null); setToken(null); setFinishError(null); setMode("merge"); if (relogin) { setTarget((current) => ({ ...current, serverUrl: relogin.serverUrl })); return; } // An older version of the app may have saved a server address. window.electronAPI ?.invoke?.("get-previous-server-url") .then((result) => { const url = (result as { serverUrl?: string | null } | null)?.serverUrl; if (url) { setTarget((current) => current.serverUrl ? current : { ...current, serverUrl: url }, ); } }) .catch(() => {}); }, [open, relogin]); const handleCheck = async () => { setChecking(true); setProblem(null); setProblemKind(null); try { const result = await probeServer(target); if (!result.ok) { setProblemKind(result.problem ?? null); setProblem(problemText(t, result.problem, result.message)); if ( result.problem === "basic_auth_required" || result.problem === "proxy_login" ) { setShowAdvanced(true); } if (result.problem === "basic_auth_required" && !target.basicAuth) { setTarget((current) => ({ ...current, basicAuth: { username: "", password: "" }, })); } return; } setTarget((current) => ({ ...current, serverUrl: result.serverUrl ?? current.serverUrl, })); setServerName(result.name); setStep("signin"); } catch { setProblem(problemText(t, "unreachable", undefined)); } finally { setChecking(false); } }; const finish = async (sessionToken: string, chosen: "merge" | "replace") => { setStep("finishing"); setFinishError(null); try { const status = await completeLink({ ...target, token: sessionToken, mode: chosen, serverName, }); rememberServerUrl(target.serverUrl); toast.success(t("sync.wizard.linked")); onLinked(status); onOpenChange(false); } catch (error) { const data = ( error as { response?: { data?: { problem?: ProbeProblem; message?: string } }; } )?.response?.data; setFinishError(problemText(t, data?.problem, data?.message)); setStep("choose"); } }; const handleSignedIn = async (sessionToken: string | null) => { if (!sessionToken) return; if (relogin) { try { const status = await reloginLink(sessionToken); toast.success(t("sync.wizard.signedInAgain")); onLinked(status); onOpenChange(false); } catch (error) { const data = ( error as { response?: { data?: { error?: string; problem?: ProbeProblem; message?: string; }; }; } )?.response?.data; throw new Error( data?.error === "different_account" ? t("sync.problems.differentAccount") : problemText(t, data?.problem, data?.message), ); } return; } setToken(sessionToken); const preview = await getLinkPreview().catch(() => ({})); setCounts(preview); const total = Object.values(preview).reduce((sum, count) => sum + count, 0); if (total === 0) { await finish(sessionToken, "merge"); return; } setStep("choose"); }; const localTotal = Object.values(counts).reduce( (sum, count) => sum + count, 0, ); const wide = step === "signin"; const title = relogin ? t("sync.wizard.reloginTitle") : t("sync.wizard.title"); const description = step === "server" ? t("sync.wizard.serverDescription") : step === "signin" ? t("sync.wizard.signinDescription", { url: target.serverUrl }) : step === "choose" ? t("sync.wizard.chooseDescription") : t("sync.wizard.finishingDescription"); const content = ( <> {step === "server" && (
{ event.preventDefault(); void handleCheck(); }} >
setTarget({ ...target, serverUrl: event.target.value }) } />

{t("sync.wizard.serverUrlHint")}

{!relogin && savedUrls.length > 0 && (
{t("sync.wizard.savedServers")} {savedUrls.map((url) => (
))}
)}
{problem && (
{problem}
)} {(problemKind === "tls_untrusted" || target.allowInvalidCertificate) && (
{t("sync.proxy.allowInvalidCertificate")} {t("sync.proxy.allowInvalidCertificateHint")}
setTarget({ ...target, allowInvalidCertificate: checked }) } />
)} {showAdvanced && ( setTarget({ ...target, ...next })} /> )}
)} {step === "signin" && (
relogin ? onOpenChange(false) : setStep("server") } />
)} {step === "choose" && (
{finishError && (
{finishError}
)} {( [ { value: "merge", title: t("sync.wizard.mergeTitle"), body: t("sync.wizard.mergeBody", { count: localTotal }), }, { value: "replace", title: t("sync.wizard.replaceTitle"), body: t("sync.wizard.replaceBody", { count: localTotal }), }, ] as const ).map((option) => ( ))}
)} {step === "finishing" && (
{t("sync.wizard.linking")}
)} ); // The wizard takes over the panel, tab or onboarding step it opens from. return ( {!wide && (

{description} {step === "server" && }

)} {content}
); }