"use client"; import { useEffect, useState, useSyncExternalStore } from "react"; import { Button } from "@/components/ui"; import { copyText } from "@/lib/clipboard"; import { haptic } from "@/lib/haptics"; import { useI18n } from "@/lib/i18n"; import { deleteSyncedData, enableSync, forgetDeviceKey, hasDeviceKey, joinSync, syncNow, unlockSync, type SyncOutcome } from "@/lib/sync"; import { syncStore } from "@/lib/sync-state"; /** * Settings → Sync (off by default). Enabling derives a key from the user's * passphrase, stores it as a non-extractable CryptoKey and uploads the first * encrypted blob. Joining needs the sync ID plus the passphrase. Conflicts * are surfaced with an explicit choice — never resolved silently. */ export function SyncSection() { const { t, locale } = useI18n(); const record = useSyncExternalStore(syncStore.subscribe, syncStore.getSnapshot, syncStore.getServerSnapshot); const [mode, setMode] = useState<"idle" | "enable" | "join">("idle"); const [pass, setPass] = useState(""); const [pass2, setPass2] = useState(""); const [joinId, setJoinId] = useState(""); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState(null); const [error, setError] = useState(null); const [locked, setLocked] = useState(null); const [confirmDelete, setConfirmDelete] = useState(false); useEffect(() => { if (!record.enabled) { setLocked(null); return; } void hasDeviceKey().then(setLocked); }, [record.enabled, record.id]); const statusFor = (outcome: SyncOutcome): string | null => { switch (outcome.status) { case "pushed": return t("settings.syncPushed"); case "pulled": return t("settings.syncPulled"); case "up-to-date": return t("settings.syncDone"); case "conflict": return t("settings.syncConflict"); case "locked": return t("settings.syncLocked"); case "error": return outcome.message ?? t("settings.syncError"); default: return null; } }; const reset = () => { setMode("idle"); setPass(""); setPass2(""); setJoinId(""); setError(null); }; const run = async (action: () => Promise) => { setBusy(true); setError(null); setNotice(null); try { const outcome = await action(); const text = statusFor(outcome); if (outcome.status === "error" || outcome.status === "locked") setError(text); else setNotice(text); if (outcome.status !== "error" && outcome.status !== "locked") reset(); } finally { setBusy(false); } }; const startEnable = () => { if (pass.length < 8) return setError(t("settings.syncPassphraseShort")); if (pass !== pass2) return setError(t("settings.syncPassphraseMismatch")); void run(() => enableSync(pass)); }; const startJoin = () => { if (!joinId.trim()) return setError(t("server.syncInvalidId")); if (pass.length < 8) return setError(t("settings.syncPassphraseShort")); void run(() => joinSync(joinId, pass)); }; const startUnlock = () => { if (!pass) return setError(t("settings.syncPassphraseShort")); void run(async () => { const outcome = await unlockSync(pass); if (outcome.status !== "error") setLocked(true); return outcome; }); }; const lastSynced = record.lastSyncedAt ? t("settings.syncLast", { when: new Date(record.lastSyncedAt).toLocaleString(locale) }) : t("settings.syncNever"); const fieldClass = "w-full rounded-xl border border-[var(--hairline)] bg-[var(--surface-strong)] px-3 py-2.5 text-sm text-frost-950 outline-none transition focus:border-accent-500 dark:text-frost-50"; /* Revoking the server copy must work even on a device that can't unlock (e.g. after the browser dropped the stored key). */ const deleteBlock = (
{confirmDelete ? ( {t("settings.syncDeleteConfirm")} ) : ( )}
); return (

{t("settings.sync")}

{t("settings.syncNote")}

{t("settings.syncNoteKey")}

{!record.enabled ? ( mode === "enable" ? (
setPass(event.target.value)} placeholder={t("settings.syncPassphrase")} autoComplete="new-password" spellCheck={false} className={fieldClass} /> setPass2(event.target.value)} placeholder={t("settings.syncPassphraseConfirm")} autoComplete="new-password" spellCheck={false} className={fieldClass} /> {error ?

{error}

: null}
) : mode === "join" ? (
setJoinId(event.target.value)} placeholder={t("settings.syncJoinId")} autoComplete="off" spellCheck={false} className={fieldClass} /> setPass(event.target.value)} placeholder={t("settings.syncPassphrase")} autoComplete="off" spellCheck={false} className={fieldClass} />

{t("settings.syncJoinNote")}

{error ?

{error}

: null}
) : (
) ) : locked === false ? (

{t("settings.syncLocked")}

setPass(event.target.value)} placeholder={t("settings.syncPassphrase")} autoComplete="off" spellCheck={false} className={fieldClass} /> {error ?

{error}

: null} {deleteBlock}
) : (
{t("settings.syncId")} {record.id}

{lastSynced}

{record.conflict ? (

{t("settings.syncConflict")}

) : ( )} {notice ?

{notice}

: null} {error ?

{error}

: null}
{deleteBlock}
)}
); }