import { useState } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Lock, ShieldCheck } from "lucide-react"; import { api, HttpError } from "../lib/api"; import * as e2ee from "../lib/e2ee"; const inputCls = "w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent"; // Secure-messaging key gate: first-run setup (choose a passphrase) or unlock on this device. // Self-contained — on success it updates the shared e2ee unlock state, so any observing view // (the Messages page, a dock window) swaps out of the gate automatically. In unlock mode it also // offers a forgotten-passphrase reset (the only escape from the set-once wall). export default function KeyGate({ meId, compact = false }: { meId: number; compact?: boolean }) { const { t } = useTranslation(); const qc = useQueryClient(); const keyQ = useQuery({ queryKey: qk.messageKey(), queryFn: api.messageMyKey, staleTime: 60_000, }); const configured = keyQ.data?.configured ?? false; // Password accounts must re-verify to reset (guards a hijacked session); Google-only accounts // have no password to check. Reactive passive observer of the App-owned qk.me() query (enabled: // false — App does the fetching), so hasPassword stays correct even if me resolves after mount. const meQ = useQuery({ queryKey: qk.me(), queryFn: api.me, enabled: false }); const hasPassword = meQ.data?.has_password ?? false; const [view, setView] = useState<"gate" | "reset">("gate"); const [pass, setPass] = useState(""); const [confirm, setConfirm] = useState(""); const [curPass, setCurPass] = useState(""); const [err, setErr] = useState(null); const [busy, setBusy] = useState(false); async function submit() { setErr(null); if (pass.length < 6) return setErr(t("messages.passTooShort")); if (!configured && pass !== confirm) return setErr(t("messages.passMismatch")); setBusy(true); try { if (configured) { await e2ee.unlock(meId, pass, keyQ.data!); } else { const payload = await e2ee.setup(meId, pass); await api.setupMessageKey(payload); } qc.invalidateQueries({ queryKey: qk.messageKey() }); } catch { setErr(configured ? t("messages.wrongPass") : t("messages.setupFailed")); } finally { setBusy(false); } } function toReset() { setErr(null); setPass(""); setConfirm(""); setView("reset"); } function cancelReset() { setErr(null); setCurPass(""); setView("gate"); } async function doReset() { setErr(null); if (hasPassword && !curPass) return setErr(t("messages.resetNeedPass")); setBusy(true); try { await api.resetMessageKey(hasPassword ? curPass : undefined); // Server key + dead history are gone; wipe this device's identity too so the gate re-appears // in setup mode. Invalidate last so the refetch sees configured:false. await e2ee.resetLocal(meId); setCurPass(""); setView("gate"); qc.invalidateQueries({ queryKey: qk.messageKey() }); } catch (e) { setErr(e instanceof HttpError && e.detail ? e.detail : t("messages.resetFailed")); } finally { setBusy(false); } } if (keyQ.isLoading) return
{t("common.loading")}
; if (view === "reset") return (
{t("messages.resetTitle")}

{t("messages.resetWarn")}

{hasPassword && ( setCurPass(e.target.value)} placeholder={t("messages.resetCurrentPass")} className={inputCls} /> )} {err &&
{err}
}
); return (
{configured ? t("messages.unlockTitle") : t("messages.setupTitle")}
{!compact && (

{configured ? t("messages.unlockBody") : t("messages.setupBody")}

)} setPass(e.target.value)} onKeyDown={(e) => e.key === "Enter" && configured && submit()} placeholder={t("messages.passphrase")} className={inputCls} /> {!configured && ( setConfirm(e.target.value)} placeholder={t("messages.passphraseConfirm")} className={inputCls} /> )} {err &&
{err}
} {configured && ( )} {!configured && !compact &&

{t("messages.setupWarn")}

}
); }