import { createSignal, onMount, Show } from "solid-js"; import { sendMessage } from "../App"; import type { PasskeyProviderState } from "../../shared/types"; import type { PairedDevice, TrustedNetwork, SyncStatus } from "../../shared/sync-types"; interface LicenseInfo { valid: boolean; plan: "free" | "pro"; issued?: string; } interface SettingsProps { entriesCount: number; breachMonitoring: boolean; lockOnClose: boolean; autoLockMinutes: number; trashRetentionDays: number; theme: "dark" | "light"; onBreachMonitoringChange: (enabled: boolean) => void | Promise; onLockOnCloseChange: (enabled: boolean) => void | Promise; onAutoLockMinutesChange: (minutes: number) => void | Promise; onTrashRetentionChange: (days: number) => void | Promise; onThemeChange: (theme: "dark" | "light") => void; onBack: () => void; onImport: () => void; onExport: () => void; onBreachCheckDone: () => void; onOpenHealth?: () => void; onOpenSyncPairing?: (mode: "initiate" | "accept") => void; onOpenCJKImport?: () => void; onOpenFileImport?: () => void; } export function SettingsView(props: SettingsProps) { const version = chrome.runtime.getManifest().version; const [checkingBreaches, setCheckingBreaches] = createSignal(false); const [breachResult, setBreachResult] = createSignal(null); const [licenseInfo, setLicenseInfo] = createSignal(null); const [licenseKeyInput, setLicenseKeyInput] = createSignal(""); const [licenseError, setLicenseError] = createSignal(null); const [licenseSuccess, setLicenseSuccess] = createSignal(null); const [activatingLicense, setActivatingLicense] = createSignal(false); const [pairedDevices, setPairedDevices] = createSignal([]); const [trustedNetworks, setTrustedNetworks] = createSignal([]); const [syncStatus, setSyncStatus] = createSignal("offline"); const [lastSyncAt, setLastSyncAt] = createSignal(undefined); const [isPro, setIsPro] = createSignal(false); const [syncingNow, setSyncingNow] = createSignal(false); const [passkeyState, setPasskeyState] = createSignal(null); const [passkeySaving, setPasskeySaving] = createSignal(false); // Deduplication state const [dedupCount, setDedupCount] = createSignal(null); const [dedupRunning, setDedupRunning] = createSignal(false); const [dedupDone, setDedupDone] = createSignal(null); // Change master password state const [showChangePw, setShowChangePw] = createSignal(false); const [changePwCurrent, setChangePwCurrent] = createSignal(""); const [changePwNew, setChangePwNew] = createSignal(""); const [changePwConfirm, setChangePwConfirm] = createSignal(""); const [changePwSaving, setChangePwSaving] = createSignal(false); const [changePwError, setChangePwError] = createSignal(null); const [changePwSuccess, setChangePwSuccess] = createSignal(false); async function handleChangePassword() { setChangePwError(null); if (!changePwCurrent().trim()) { setChangePwError("enter your current password"); return; } if (changePwNew().length < 12) { setChangePwError("new password must be at least 12 characters"); return; } if (changePwNew() !== changePwConfirm()) { setChangePwError("new passwords do not match"); return; } if (changePwNew() === changePwCurrent()) { setChangePwError("new password must differ from current"); return; } setChangePwSaving(true); const res = await sendMessage({ type: "CHANGE_PASSWORD", payload: { currentPassword: changePwCurrent(), newPassword: changePwNew() }, }) as { ok: boolean; error?: string }; if (res.ok) { setChangePwSuccess(true); setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm(""); setTimeout(() => { setChangePwSuccess(false); setShowChangePw(false); }, 2200); } else { setChangePwError(res.error ?? "failed to change password"); } setChangePwSaving(false); } // Duress password state const [duressEnabled, setDuressEnabled] = createSignal(false); const [showDuressSetup, setShowDuressSetup] = createSignal(false); const [duressPassword, setDuressPassword] = createSignal(""); const [duressConfirm, setDuressConfirm] = createSignal(""); const [duressMainPassword, setDuressMainPassword] = createSignal(""); const [duressSaving, setDuressSaving] = createSignal(false); const [duressError, setDuressError] = createSignal(null); const [duressSuccess, setDuressSuccess] = createSignal(null); onMount(async () => { const [licenseRes, syncRes, passkeyRes, duressRes] = await Promise.all([ sendMessage({ type: "GET_LICENSE" }), sendMessage({ type: "GET_SYNC_STATE" }), sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }), sendMessage({ type: "IS_DURESS_ENABLED" }), ]); if ((licenseRes as { ok: boolean }).ok) { setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null); } if ((syncRes as { ok: boolean }).ok) { const data = syncRes as { ok: boolean; data?: { pairedDevices?: PairedDevice[]; trustedNetworks?: TrustedNetwork[]; status?: SyncStatus; lastSyncAt?: number; isPro?: boolean; }}; if (data.data) { setPairedDevices(data.data.pairedDevices ?? []); setTrustedNetworks(data.data.trustedNetworks ?? []); setSyncStatus(data.data.status ?? "offline"); setLastSyncAt(data.data.lastSyncAt); setIsPro(data.data.isPro ?? false); } } if ((passkeyRes as { ok: boolean }).ok) { setPasskeyState( (passkeyRes as { ok: boolean; data?: PasskeyProviderState }).data ?? null ); } if ((duressRes as { ok: boolean }).ok) { setDuressEnabled((duressRes as { ok: boolean; data?: boolean }).data ?? false); } chrome.runtime.onMessage.addListener((msg) => { if (msg.type === "SYNC_STATE_UPDATE") { setPairedDevices(msg.pairedDevices ?? []); setTrustedNetworks(msg.trustedNetworks ?? []); setSyncStatus(msg.status ?? "offline"); setLastSyncAt(msg.lastSyncAt); setIsPro(msg.isPro ?? false); } }); }); async function handleActivateLicense() { const key = licenseKeyInput().trim(); if (!key) return; setActivatingLicense(true); setLicenseError(null); setLicenseSuccess(null); const res = await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key } }); setActivatingLicense(false); if ((res as { ok: boolean }).ok) { const data = (res as { ok: boolean; data?: LicenseInfo }).data; if (data?.valid) { setLicenseInfo(data); setLicenseSuccess("License activated!"); setLicenseKeyInput(""); } else { setLicenseError("Invalid license key"); } } else { setLicenseError("Activation failed"); } } async function handleTogglePasskeys() { const current = passkeyState(); if (!current || !current.supported) return; setPasskeySaving(true); const res = await sendMessage({ type: "SET_PASSKEY_PROVIDER_ENABLED", payload: { enabled: !current.enabled }, }); setPasskeySaving(false); if ((res as { ok: boolean }).ok) { setPasskeyState( (res as { ok: boolean; data?: PasskeyProviderState }).data ?? null ); } } function maskLicenseKey(key: string): string { if (key.length < 10) return key.slice(0, 4) + "****"; return key.slice(0, 6) + "****" + key.slice(-4); } async function handleCheckAllBreaches() { setCheckingBreaches(true); setBreachResult(null); const res = await sendMessage({ type: "CHECK_ALL_BREACHES" }); setCheckingBreaches(false); if ((res as { ok: boolean }).ok) { const data = (res as { ok: boolean; data?: Record }).data ?? {}; const breached = Object.values(data).filter((count) => count > 0).length; setBreachResult( breached > 0 ? `${breached} password${breached > 1 ? "s" : ""} found in breaches` : "All passwords clean" ); props.onBreachCheckDone(); } } async function handleSyncNow() { if (syncStatus() === "not_trusted") return; setSyncingNow(true); await sendMessage({ type: "TRIGGER_SYNC" }); setTimeout(() => setSyncingNow(false), 3000); } async function handleRemoveDevice(deviceId: string) { await sendMessage({ type: "REMOVE_PAIRED_DEVICE", payload: { deviceId } }); } async function handleRemoveNetwork(ssid?: string, bssid?: string, isTailscale?: boolean) { await sendMessage({ type: "REMOVE_TRUSTED_NETWORK", payload: { ssid, bssid, isTailscale } }); } async function handleMarkNetworkTrusted() { await sendMessage({ type: "MARK_NETWORK_TRUSTED" }); } function formatRelativeTime(timestamp?: number): string { if (!timestamp) return "Never"; const diff = Date.now() - timestamp; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return "Just now"; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; return `${days}d ago`; } function getStatusColor(status: SyncStatus): string { switch (status) { case "synced": return "var(--peach-sync-ok)"; case "syncing": return "var(--peach-sync-busy)"; case "no_peers": return "var(--peach-sync-off)"; case "not_trusted": return "var(--peach-sync-err)"; case "offline": return "var(--peach-sync-off)"; default: return "var(--peach-sync-off)"; } } function getStatusLabel(status: SyncStatus): string { switch (status) { case "synced": return "Synced"; case "syncing": return "Syncing"; case "no_peers": return "No peers visible"; case "not_trusted": return "Not on a trusted network"; case "offline": return "Offline"; default: return "Unknown"; } } function isCurrentNetworkTrusted(): boolean { // Show the "mark trusted" button only if there are no trusted networks at all // (Tailscale is always trusted implicitly; any explicit networks count too) return trustedNetworks().length > 0; } async function handleSetupDuress() { const mainPw = duressMainPassword().trim(); const duressPw = duressPassword().trim(); const confirmPw = duressConfirm().trim(); if (!mainPw) { setDuressError("enter your main password to confirm"); return; } if (duressPw.length < 8) { setDuressError("duress password must be at least 8 characters"); return; } if (duressPw === mainPw) { setDuressError("duress password must differ from your main password"); return; } if (duressPw !== confirmPw) { setDuressError("passwords do not match"); return; } setDuressSaving(true); setDuressError(null); // Verify main password first const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean }; if (!verifyRes.ok || !verifyRes.data) { setDuressError("incorrect main password"); setDuressSaving(false); return; } const res = await sendMessage({ type: "SETUP_DURESS", payload: { duressPassword: duressPw } }) as { ok: boolean; error?: string }; setDuressSaving(false); if (res.ok) { setDuressEnabled(true); setShowDuressSetup(false); setDuressPassword(""); setDuressConfirm(""); setDuressMainPassword(""); setDuressSuccess("Duress password set. If entered, a decoy vault will be shown."); setTimeout(() => setDuressSuccess(null), 5000); } else { setDuressError(res.error ?? "failed to set duress password"); } } async function handleCountDuplicates() { setDedupRunning(true); setDedupDone(null); const res = await sendMessage({ type: "COUNT_DUPLICATES" }) as { ok: boolean; data?: number }; setDedupRunning(false); if (res.ok) setDedupCount(res.data ?? 0); } async function handleDeduplicate() { setDedupRunning(true); const res = await sendMessage({ type: "DEDUPLICATE_ENTRIES" }) as { ok: boolean; data?: number }; setDedupRunning(false); if (res.ok) { setDedupDone(res.data ?? 0); setDedupCount(0); } } async function handleDisableDuress() { const mainPw = duressMainPassword().trim(); if (!mainPw) { setDuressError("enter your main password to confirm"); return; } setDuressSaving(true); setDuressError(null); const verifyRes = await sendMessage({ type: "VERIFY_MAIN_PASSWORD", payload: { password: mainPw } }) as { ok: boolean; data?: boolean }; if (!verifyRes.ok || !verifyRes.data) { setDuressError("incorrect main password"); setDuressSaving(false); return; } const res = await sendMessage({ type: "DISABLE_DURESS" }) as { ok: boolean; error?: string }; setDuressSaving(false); if (res.ok) { setDuressEnabled(false); setShowDuressSetup(false); setDuressMainPassword(""); setDuressSuccess("Duress password disabled."); setTimeout(() => setDuressSuccess(null), 4000); } else { setDuressError(res.error ?? "failed to disable duress password"); } } function renderSyncSection() { if (!isPro()) { return (

sync

Status {getStatusLabel(syncStatus())}
Last sync {lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}
); } return (

sync

Status {getStatusLabel(syncStatus())}
Last sync {lastSyncAt() ? formatRelativeTime(lastSyncAt()) : "Never synced"}
{pairedDevices().map((device) => (
{device.displayName} Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}
))}
{trustedNetworks().filter(n => !n.isTailscale).map((network) => (
{network.ssid ?? "Unknown network"}
))}
{!isCurrentNetworkTrusted() && ( )}
Tailscale is always trusted
); } return (
settings

security

Lock on close
Auto-lock after
Breach monitoring
Check all passwords
{breachResult() && (
Last check {breachResult()}
)} {/* ── Change master password ──────────────────────────────────── */}
Master password
setChangePwCurrent((e.target as HTMLInputElement).value)} autocomplete="current-password" /> setChangePwNew((e.target as HTMLInputElement).value)} autocomplete="new-password" /> setChangePwConfirm((e.target as HTMLInputElement).value)} autocomplete="new-password" onKeyDown={(e) => { if (e.key === "Enter") void handleChangePassword(); }} />

{changePwError()}

password changed ✓

{/* ── Duress password ─────────────────────────────────────────────── */}

duress password

Duress password If entered under coercion, shows a decoy empty vault instead of your real one.
{duressEnabled() ? "on" : "off"}
If someone forces you to unlock Peach, enter the duress password instead. A decoy empty vault opens; your real vault stays hidden.
{duressSuccess()}
setDuressMainPassword(e.currentTarget.value)} /> setDuressPassword(e.currentTarget.value)} /> setDuressConfirm(e.currentTarget.value)} />

{duressError()}

passkeys

Intercept passkey requests Peach can handle passkey sign-in and creation on supported Chromium browsers.
Status {passkeyState()?.statusLabel ?? "Unavailable"}
Browser {passkeyState()?.browserLabel ?? "Unknown"} {passkeyState()?.supported ? " \u2713" : " (interception unavailable)"}
Only one passkey provider can be active at a time. Disable Bitwarden or 1Password passkey interception if Peach reports a conflict.
{passkeyState()?.conflict && (
{passkeyState()!.conflict}
)}
{renderSyncSection()}

vault

Entries {props.entriesCount}
Duplicates void handleCountDuplicates()} disabled={dedupRunning()} > {dedupRunning() ? "scanning..." : dedupCount() === null ? "scan" : dedupCount() === 0 ? "none found" : `${dedupCount()} found — remove?`} }> {dedupDone() === 0 ? "already clean" : `removed ${dedupDone()}`}
0 && dedupDone() === null}>
{dedupCount()} duplicate {dedupCount() === 1 ? "entry" : "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.
Theme
Trash auto-delete
Import passwords
Export backup
Vault Health
Replace vault from backup
This will permanently replace your current vault with the backup.

license

{licenseInfo()?.valid && licenseInfo()?.plan === "pro" ? (
Peach Pro activated {licenseInfo()?.issued ? new Date(licenseInfo()!.issued!).toLocaleDateString() : ""}
) : ( <>
setLicenseKeyInput(e.currentTarget.value)} style="width:100%;box-sizing:border-box" />
{licenseError() && (
{licenseError()}
)} {licenseSuccess() && (
{licenseSuccess()}
)} )}
{/* ── Keyboard shortcuts ────────────────────────────────────── */}

keyboard shortcuts

Ccopy password Ucopy username /search vault ↑ ↓navigate entries Escback / close ⌘⇧Pautofill on page
); }