"use client"; import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuEye, LuEyeOff } from "react-icons/lu"; import { LoadingButton } from "@/components/loading-button"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ProBadge } from "@/components/ui/pro-badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useCloudAuth } from "@/hooks/use-cloud-auth"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import type { SyncSettings } from "@/types"; const DEVICE_LINK_URL = "https://donutbrowser.com/auth/link"; interface SyncConfigDialogProps { isOpen: boolean; onClose: (loginOccurred?: boolean) => void; /** * Called after the user clicks "Login" so the parent can open the * device-code verify dialog as a separate step. Implementations should * close this dialog and open the verify one — that keeps the verify * step visually independent and avoids stacking on top of other * dialogs (e.g. the profile selector triggered by deep links). */ onLoginStarted?: () => void; } interface ProxyUsage { used_mb: number; limit_mb: number; remaining_mb: number; recurring_limit_mb: number; extra_limit_mb: number; } export function SyncConfigDialog({ isOpen, onClose, onLoginStarted, }: SyncConfigDialogProps) { const { t } = useTranslation(); // Self-hosted state const [serverUrl, setServerUrl] = useState(""); const [token, setToken] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isTesting, setIsTesting] = useState(false); const [showToken, setShowToken] = useState(false); // Cloud auth state const { user, isLoggedIn, isLoading: isCloudLoading, logout, } = useCloudAuth(); const [activeTab, setActiveTab] = useState("cloud"); const [, setLiveProxyUsage] = useState(null); const [connectionStatus, setConnectionStatus] = useState< "unknown" | "testing" | "connected" | "error" >("unknown"); const hasConfig = Boolean(serverUrl && token); const testConnection = useCallback(async (url: string) => { setConnectionStatus("testing"); try { const healthUrl = `${url.replace(/\/$/, "")}/health`; const response = await fetch(healthUrl); setConnectionStatus(response.ok ? "connected" : "error"); } catch { setConnectionStatus("error"); } }, []); const loadSettings = useCallback(async () => { setIsLoading(true); try { const settings = await invoke("get_sync_settings"); setServerUrl(settings.sync_server_url ?? ""); setToken(settings.sync_token ?? ""); if (settings.sync_server_url && settings.sync_token) { void testConnection(settings.sync_server_url); } } catch (error) { console.error("Failed to load sync settings:", error); } finally { setIsLoading(false); } }, [testConnection]); useEffect(() => { if (isOpen) { setConnectionStatus("unknown"); void loadSettings(); void invoke("cloud_get_proxy_usage") .then(setLiveProxyUsage) .catch(() => { setLiveProxyUsage(null); }); } }, [isOpen, loadSettings]); // Auto-select the appropriate tab based on connection state useEffect(() => { if (isCloudLoading) return; if (isLoggedIn) { setActiveTab("cloud"); } else if (serverUrl && token) { setActiveTab("self-hosted"); } else { setActiveTab("cloud"); } }, [isCloudLoading, isLoggedIn, serverUrl, token]); const handleTestConnection = useCallback(async () => { if (!serverUrl) { showErrorToast(t("sync.config.serverUrlRequired")); return; } setIsTesting(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 { setIsTesting(false); } }, [serverUrl, t]); const handleSave = useCallback(async () => { setIsSaving(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")); onClose(); } catch (error) { console.error("Failed to save sync settings:", error); showErrorToast(t("sync.config.saveFailed")); } finally { setIsSaving(false); } }, [serverUrl, token, onClose, t]); const handleDisconnect = useCallback(async () => { setIsSaving(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 { setIsSaving(false); } }, [t]); const handleOpenLogin = useCallback(async () => { try { await invoke("handle_url_open", { url: DEVICE_LINK_URL }); // Hand off the verify step to its own dialog so the user has a // focused place to paste the code, and so it doesn't visually // stack with this dialog or any other modal currently on screen. onLoginStarted?.(); } catch (error) { console.error("Failed to open login link:", error); showErrorToast(String(error)); } }, [onLoginStarted]); const handleCloudLogout = useCallback(async () => { try { await logout(); showSuccessToast(t("sync.cloud.logoutSuccess")); setServerUrl(""); setToken(""); try { await invoke("restart_sync_service"); } catch (e) { console.error("Failed to restart sync service:", e); } } catch (error) { console.error("Failed to logout:", error); showErrorToast(String(error)); } }, [logout, t]); const cloudBlocked = !isLoggedIn && hasConfig; const selfHostedBlocked = isLoggedIn; return ( {t("sync.title")} {t("sync.description")} {isLoggedIn && user ? (
{t("sync.cloud.connected")}
{t("sync.cloud.email")} {user.email}
{t("sync.cloud.plan")} {user.plan} {user.planPeriod ? ` (${user.planPeriod})` : ""}
{t("sync.cloud.profiles")} {t("sync.cloud.profileUsage", { used: user.cloudProfilesUsed, limit: user.profileLimit, })}
{user.teamName && ( <>
{t("sync.team.name")} {user.teamName}
{t("sync.team.role")} {user.teamRole === "owner" ? t("sync.team.roleOwner") : user.teamRole === "admin" ? t("sync.team.roleAdmin") : t("sync.team.roleMember")}

{t("sync.team.manageOnWeb")}

)}
) : ( {t("sync.cloud.tabLabel")} {cloudBlocked && } {t("sync.cloud.selfHostedTabLabel")} {selfHostedBlocked && } {isCloudLoading ? (
) : (

{t("sync.cloud.deviceLinkInstructions")}

)} {isLoading ? (
) : (
{ setServerUrl(e.target.value); }} />
{ setToken(e.target.value); }} className="pr-10" /> {showToken ? "Hide token" : "Show token"}
{connectionStatus === "testing" && (
{t("sync.status.syncing")}
)} {connectionStatus === "connected" && (
{t("sync.status.connected")}
)} {connectionStatus === "error" && (
{t("sync.status.disconnected")}
)}
)} {hasConfig && ( )} void handleSave()} isLoading={isSaving} disabled={!serverUrl || !token} > Save )}
); }