"use client"; import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuCloud, LuEye, LuEyeOff, LuLogOut, LuRefreshCw, LuUser, } from "react-icons/lu"; import { LoadingButton } from "@/components/loading-button"; import { AnimatedTabs, AnimatedTabsContent, AnimatedTabsList, AnimatedTabsTrigger, } from "@/components/ui/animated-tabs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useCloudAuth } from "@/hooks/use-cloud-auth"; import { translateBackendError } from "@/lib/backend-errors"; import { getEntitlements } from "@/lib/entitlements"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import { cn } from "@/lib/utils"; import type { SyncSettings } from "@/types"; interface AccountPageProps { isOpen: boolean; onClose: () => void; subPage?: boolean; onOpenSignIn: () => void; } type ConnectionStatus = "unknown" | "testing" | "connected" | "error"; export function AccountPage({ isOpen, onClose, subPage, onOpenSignIn, }: AccountPageProps) { const { t } = useTranslation(); const { user, isLoggedIn, isLoading: isCloudLoading, logout, refreshProfile, } = useCloudAuth(); const [isRefreshing, setIsRefreshing] = useState(false); const [isLoggingOut, setIsLoggingOut] = useState(false); // Self-hosted server state. Loaded once when the dialog opens and persisted // via `save_sync_settings` so the rest of the app picks up the new URL/token // from `SettingsManager`. const [serverUrl, setServerUrl] = useState(""); const [token, setToken] = useState(""); const [showToken, setShowToken] = useState(false); const [isSavingSelfHosted, setIsSavingSelfHosted] = useState(false); const [isTestingConnection, setIsTestingConnection] = useState(false); const [connectionStatus, setConnectionStatus] = useState("unknown"); const hasConfig = Boolean(serverUrl && token); // Self-hosted and cloud are mutually exclusive — both share the same sync // engine and a profile can't be sync'd to two backends. The tab trigger is // disabled here AND the backend rejects mixed state (see `save_sync_settings` // / `cloud_logout`), so even if someone bypasses the UI we don't end up // with split-brain. const selfHostedDisabled = isLoggedIn || isCloudLoading; const handleRefresh = async () => { setIsRefreshing(true); try { await refreshProfile(); showSuccessToast(t("account.refreshed")); } catch (e) { showErrorToast(String(e)); } finally { setIsRefreshing(false); } }; const handleLogout = async () => { setIsLoggingOut(true); try { await logout(); // The backend wipes sync URL + token as part of cloud_logout (see // `cloud_auth::cloud_logout`); pull the now-empty settings back into // the form so a user who flips to the Self-hosted tab doesn't see the // pre-logout production URL still sitting there. await loadSelfHostedSettings(); showSuccessToast(t("account.loggedOut")); } catch (e) { showErrorToast(String(e)); } finally { setIsLoggingOut(false); } }; const loadSelfHostedSettings = useCallback(async () => { try { const settings = await invoke("get_sync_settings"); setServerUrl(settings.sync_server_url ?? ""); setToken(settings.sync_token ?? ""); setConnectionStatus( settings.sync_server_url && settings.sync_token ? "unknown" : "unknown", ); } catch (error) { console.error("Failed to load sync settings:", error); } }, []); useEffect(() => { if (isOpen) { void loadSelfHostedSettings(); } }, [isOpen, loadSelfHostedSettings]); const handleTestConnection = useCallback(async () => { if (!serverUrl) { showErrorToast(t("sync.config.serverUrlRequired")); return; } setIsTestingConnection(true); setConnectionStatus("testing"); try { const healthUrl = `${serverUrl.replace(/\/$/, "")}/health`; const response = await fetch(healthUrl); if (response.ok) { setConnectionStatus("connected"); showSuccessToast(t("sync.config.connectionSuccess")); } else { setConnectionStatus("error"); showErrorToast(t("sync.config.serverError")); } } catch { setConnectionStatus("error"); showErrorToast(t("sync.config.connectFailed")); } finally { setIsTestingConnection(false); } }, [serverUrl, t]); const handleSaveSelfHosted = useCallback(async () => { setIsSavingSelfHosted(true); try { await invoke("save_sync_settings", { syncServerUrl: serverUrl || null, syncToken: token || null, }); try { await invoke("restart_sync_service"); } catch (e) { console.error("Failed to restart sync service:", e); } showSuccessToast(t("sync.config.settingsSaved")); } catch (error) { console.error("Failed to save sync settings:", error); // Use the structured backend-error translator so the cloud-vs-self- // hosted mutex (`SELF_HOSTED_REQUIRES_LOGOUT`) shows a clear message // instead of the generic "save failed" toast. showErrorToast(translateBackendError(t as never, error)); } finally { setIsSavingSelfHosted(false); } }, [serverUrl, token, t]); const handleDisconnectSelfHosted = useCallback(async () => { setIsSavingSelfHosted(true); try { await invoke("save_sync_settings", { syncServerUrl: null, syncToken: null, }); try { await invoke("restart_sync_service"); } catch (e) { console.error("Failed to restart sync service:", e); } setServerUrl(""); setToken(""); setConnectionStatus("unknown"); showSuccessToast(t("sync.config.disconnected")); } catch (error) { console.error("Failed to disconnect:", error); showErrorToast(t("sync.config.disconnectFailed")); } finally { setIsSavingSelfHosted(false); } }, [t]); return (
{t("account.tabs.account")} {t("account.tabs.selfHosted")}
{isLoggedIn && user ? ( <>

{user.email}

{t("account.plan", { plan: user.plan, period: user.planPeriod ?? "—", })}

) : ( <>

{t("account.signedOut")}

{t("account.signedOutDescription")}

)}
{isLoggedIn && user && (

{t("account.fields.plan")}

{user.plan}

{t("account.fields.status")}

{user.subscriptionStatus ?? "—"}

{user.teamRole && (

{t("account.fields.teamRole")}

{user.teamRole}

)} {user.planPeriod && (

{t("account.fields.period")}

{user.planPeriod}

)} {typeof user.deviceOrdinal === "number" && (

{t("account.fields.device")}

{t("account.deviceOrdinal", { ordinal: user.deviceOrdinal, count: user.deviceCount ?? user.deviceOrdinal, })}

)}
)} {isLoggedIn && user && getEntitlements(user).browserAutomation && user.isPrimaryDevice === false && (

{t("account.automationPrimaryOnly")}

)} {isLoggedIn && user && getEntitlements(user).browserAutomation && user.isPrimaryDevice === true && (user.deviceCount ?? 1) > 1 && (

{t("account.automationActiveHere")}

)}
{isLoggedIn ? ( <> { void handleLogout(); }} className="h-8 gap-1.5 text-xs" > {t("account.logout")} ) : ( )}
{selfHostedDisabled ? ( // Defensive: the tab trigger is disabled while the user is // logged in, so this branch shouldn't be reachable via UI — // but if state flips mid-render (e.g. a cloud login finishes // while the tab is open), show the explanation instead of // a silent empty card.

{t("account.selfHosted.disabledWhileLoggedIn")}

) : (

{t("account.selfHosted.title")}

{t("account.selfHosted.description")}

{ setServerUrl(e.target.value); setConnectionStatus("unknown"); }} autoComplete="off" spellCheck={false} />
{ setToken(e.target.value); setConnectionStatus("unknown"); }} autoComplete="off" spellCheck={false} className="pr-9" />
{t("account.selfHosted.connectionStatus")} {connectionStatus === "connected" && ( {t("sync.status.connected")} )} {connectionStatus === "error" && ( {t("sync.status.error")} )} {connectionStatus === "testing" && ( {t("sync.status.syncing")} )} {connectionStatus === "unknown" && ( {t("account.selfHosted.statusUnknown")} )}
void handleTestConnection()} className="h-8 text-xs" > {t("account.selfHosted.testConnection")} void handleSaveSelfHosted()} className="h-8 text-xs" > {t("common.buttons.save")} {hasConfig && ( )}
)}
); }