import { createSignal, For, Match, onMount, Show, Switch } from "solid-js"; import { sendMessage } from "../App"; import { PeachMark } from "../components/PeachMark"; import { LockRuneCanvas } from "../components/LockRuneCanvas"; import { RecoveryWordDisplayView } from "./RecoveryWordDisplay"; import type { Canary } from "../../shared/types"; import { ACCENT_COLORS } from "../../shared/types"; import type { AccentColorName, PasskeyProviderState, ProFeature, VaultSnapshot, } from "../../shared/types"; import { DEFAULT_BINDINGS, findConflict, formatKeybinding, keybindingFromEvent, SHORTCUT_DESCRIPTORS, type Keybinding, type ShortcutAction, } from "../../shared/shortcuts"; import { isNativePlatform, nativePlugin } from "../../shared/native"; interface LicenseInfo { valid: boolean; plan: "free" | "pro"; issued?: string; } interface SettingsProps { entriesCount: number; breachMonitoring: boolean; lockOnClose: boolean; autoLockMinutes: number; trashRetentionDays: number; theme: "dark" | "light"; accentColor: AccentColorName; textSize: "small" | "medium" | "large"; reduceMotion: boolean; isPro?: boolean; initialSection?: string | null; upgradeHighlight?: ProFeature | null; 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; onAccentChange: (name: AccentColorName) => void | Promise; onTextSizeChange: (size: "small" | "medium" | "large") => void | Promise; onReduceMotionChange: (enabled: boolean) => void | Promise; shortcuts: Record; onShortcutsChange: (active: Record) => void | Promise; onLicenseChange?: () => void | Promise; onBack: () => void; onImport: () => void; onExport: () => void; onBreachCheckDone: () => void; onOpenHealth?: () => void; onOpenSyncPairing?: () => void; onOpenFileImport?: () => void; onOpenPanic?: () => void; onOpenCodexGenerate?: () => void; onOpenCodexRestore?: () => void; } type SectionName = | "security" | "vault" | "appearance" | "shortcuts" | "license" | "feedback"; const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"]; function formatActivatedSince(issued: string | undefined): string { if (!issued) return "Active"; try { const d = new Date(issued); if (Number.isNaN(d.getTime())) return "Active"; return `Active since ${d.toLocaleDateString(undefined, { month: "long", year: "numeric" })}`; } catch { return "Active"; } } function isSectionName(value: string): value is SectionName { return ( value === "security" || value === "vault" || value === "appearance" || value === "shortcuts" || value === "license" || value === "feedback" ); } export function SettingsView(props: SettingsProps) { const manifest = chrome.runtime.getManifest(); const version = manifest.version; const isAndroid = (manifest as any).version_name === 'mobile'; const initial = props.initialSection; const [activeSection, setActiveSection] = createSignal( initial && isSectionName(initial) ? (initial as SectionName) : null ); 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 [, setLicenseSuccess] = createSignal(null); const [activatingLicense, setActivatingLicense] = createSignal(false); const isPro = () => props.isPro ?? false; const [biometricSupported, setBiometricSupported] = createSignal(false); const [biometricStored, setBiometricStored] = createSignal(false); const [biometricBusy, setBiometricBusy] = createSignal(false); const [biometricError, setBiometricError] = createSignal(null); const [passkeyState, setPasskeyState] = createSignal(null); const [passkeySaving, setPasskeySaving] = createSignal(false); const [dedupCount, setDedupCount] = createSignal(null); const [dedupRunning, setDedupRunning] = createSignal(false); const [dedupDone, setDedupDone] = createSignal(null); const [declinedCardCount, setDeclinedCardCount] = createSignal(0); // Vault history (peach_vault_history) — last 10 snapshots, snapshotted // BEFORE every real-mode vault write so the user can roll back if a sync // or import overwrites the wrong state. const [historySnapshots, setHistorySnapshots] = createSignal([]); const [historyLoading, setHistoryLoading] = createSignal(false); const [historyRestoring, setHistoryRestoring] = createSignal(null); async function loadVaultHistory() { setHistoryLoading(true); try { const res = await sendMessage({ type: "GET_VAULT_HISTORY" }); const list = (res as { ok: boolean; data?: VaultSnapshot[] }).data ?? []; setHistorySnapshots(list); } finally { setHistoryLoading(false); } } async function handleRestoreVaultSnapshot(id: string) { if (historyRestoring()) return; setHistoryRestoring(id); const res = await sendMessage({ type: "RESTORE_VAULT_SNAPSHOT", payload: { snapshotId: id }, }); setHistoryRestoring(null); if ((res as { ok: boolean }).ok) { // Vault is now locked — let the parent close Settings; App.tsx's // VAULT_LOCKED listener will route back to the lock screen. props.onBack(); } } function formatSnapshotTime(savedAt: number): string { const diff = Date.now() - savedAt; if (diff < 60_000) return "Just now"; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; const d = new Date(savedAt); const isToday = new Date().toDateString() === d.toDateString(); const isYest = new Date(Date.now() - 86_400_000).toDateString() === d.toDateString(); const t = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); if (isToday) return `Today ${t}`; if (isYest) return `Yesterday ${t}`; return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${t}`; } function reasonLabel(reason: VaultSnapshot["reason"]): string { return ( { local_save: "saved", sync_received: "synced", import: "imported", restore: "restored", } as const )[reason] ?? reason; } const [showVaultAdvanced, setShowVaultAdvanced] = createSignal(false); 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] = createSignal(false); const [showNewRecoveryWords, setShowNewRecoveryWords] = createSignal(false); 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); const [canaryEntries, setCanaryEntries] = createSignal>([]); const [canaryTestingId, setCanaryTestingId] = createSignal(null); const [canaryTestResult, setCanaryTestResult] = createSignal<{id: string; ok: boolean} | null>(null); const [fbCategory, setFbCategory] = createSignal("Bug report"); const [fbMessage, setFbMessage] = createSignal(""); const [fbSubmitting, setFbSubmitting] = createSignal(false); const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null); onMount(async () => { const [licenseRes, passkeyRes, duressRes, settingsRes, keyRes] = await Promise.all([ sendMessage({ type: "GET_LICENSE" }), sendMessage({ type: "GET_PASSKEY_PROVIDER_STATE" }), sendMessage({ type: "IS_DURESS_ENABLED" }), sendMessage({ type: "GET_SETTINGS" }), sendMessage({ type: "GET_LICENSE_KEY" }), ]); // Vault history loads in the background — the Vault sub-view picks it up // when the user navigates in. Loading it lazily keeps Settings boot fast. void loadVaultHistory(); if ((licenseRes as { ok: boolean }).ok) { setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null); } 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); } if ((settingsRes as { ok: boolean }).ok) { const sData = (settingsRes as { ok: boolean; data?: { declinedCardFingerprints?: string[] } }).data; setDeclinedCardCount(sData?.declinedCardFingerprints?.length ?? 0); } if ((keyRes as { ok: boolean }).ok) { setLicenseKey((keyRes as { ok: boolean; data?: string }).data ?? null); } const entriesRes = await sendMessage({ type: "GET_ENTRIES" }); if ((entriesRes as { ok: boolean }).ok) { const entries = (entriesRes as { ok: boolean; data?: Array<{id: string; title: string; canary?: Canary}> }).data ?? []; setCanaryEntries( entries .filter((e) => e.canary?.webhookUrl) .map((e) => ({ id: e.id, title: e.title, canary: e.canary! })) ); } if (isNativePlatform()) { const biometric = nativePlugin<{ isAvailable: () => Promise<{ available: boolean }> }>("BiometricPlugin"); const keystore = nativePlugin<{ hasMasterKey: () => Promise<{ exists: boolean }> }>("KeystorePlugin"); try { if (biometric) { const a = await biometric.isAvailable(); setBiometricSupported(a.available); } if (keystore) { const k = await keystore.hasMasterKey(); setBiometricStored(k.exists); } } catch { /* missing plugins — leave defaults */ } } }); async function handleEnableBiometrics() { setBiometricBusy(true); setBiometricError(null); try { const keyRes = (await sendMessage({ type: "GET_DERIVED_KEY" })) as { ok: boolean; data?: { masterKeyHex: string }; error?: string; }; if (!keyRes.ok || !keyRes.data?.masterKeyHex) { setBiometricError(keyRes.error === "vault_locked" ? "unlock the vault first" : "couldn't read derived key"); return; } const keystore = nativePlugin<{ storeMasterKey: (p: { masterKeyHex: string }) => Promise }>("KeystorePlugin"); if (!keystore) { setBiometricError("biometric plugin unavailable"); return; } await keystore.storeMasterKey({ masterKeyHex: keyRes.data.masterKeyHex }); await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: true } }); setBiometricStored(true); } catch (e) { setBiometricError(e instanceof Error ? e.message : "failed to enable biometrics"); } finally { setBiometricBusy(false); } } async function handleDisableBiometrics() { setBiometricBusy(true); setBiometricError(null); try { const keystore = nativePlugin<{ clearMasterKey: () => Promise }>("KeystorePlugin"); if (keystore) await keystore.clearMasterKey(); await sendMessage({ type: "UPDATE_SETTINGS", payload: { biometricsEnabled: false } }); setBiometricStored(false); } catch (e) { setBiometricError(e instanceof Error ? e.message : "failed to disable biometrics"); } finally { setBiometricBusy(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) { setChangePwCurrent(""); setChangePwNew(""); setChangePwConfirm(""); setShowChangePw(false); setShowNewRecoveryWords(true); } else { setChangePwError(res.error ?? "failed to change password"); } setChangePwSaving(false); } const [copied, setCopied] = createSignal(false); async function copyLicenseKey(key: string) { await navigator.clipboard.writeText(key); setCopied(true); setTimeout(() => setCopied(false), 2000); } 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); setLicenseKey(key); 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 ); } } 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 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); 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 handleClearDeclinedCards() { await sendMessage({ type: "UPDATE_SETTINGS", payload: { declinedCardFingerprints: [] } }); setDeclinedCardCount(0); } 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"); } } async function handleSubmitFeedback() { if (!fbMessage().trim() || fbSubmitting()) return; setFbSubmitting(true); setFbResult(null); const res = await sendMessage({ type: "SUBMIT_FEEDBACK", payload: { category: fbCategory(), message: fbMessage().trim(), version, browser: navigator.userAgent, }, }) as { ok: boolean; error?: string }; setFbSubmitting(false); if (res.ok) { setFbResult("success"); setFbMessage(""); setTimeout(() => setFbResult(null), 4000); } else { setFbResult("error"); } } function handleHeaderBack() { if (activeSection() !== null) { setActiveSection(null); } else { props.onBack(); } } function currentTitle(): string { const s = activeSection(); if (s === null) return "settings"; if (s === "license") return "account"; return s; } function renderCatRow(opts: { section: SectionName; name: string; desc: string; icon: () => any; pro?: boolean; }) { return ( ); } const iconShield = () => ( ); const iconKey = () => ( ); const iconLock = () => ( ); const iconSync = () => ( ); const iconSun = () => ( ); const iconKeys = () => ( ); const iconBubble = () => ( ); function renderHome() { return (

PROTECTION

{renderCatRow({ section: "security", name: "Security", desc: "Locking, breach monitoring, passkeys", icon: iconShield })} {renderCatRow({ section: "license", name: "Peach Pro", desc: isPro() ? "Active · manage your license" : "Upgrade · $39 · yours forever", icon: iconKey })}

VAULT

{renderCatRow({ section: "vault", name: "Vault", desc: "Entries, health, import & export", icon: iconLock })}

PREFERENCES

{renderCatRow({ section: "appearance", name: "Appearance", desc: "Theme & accent color", icon: iconSun })} {renderCatRow({ section: "shortcuts", name: "Shortcuts", desc: "Keyboard reference", icon: iconKeys })}

SUPPORT

{renderCatRow({ section: "feedback", name: "Feedback", desc: "Report issues, suggest features", icon: iconBubble })}
); } function renderSecurity() { return (

LOCKING

Lock on close
Auto-lock after

BIOMETRICS

Unlock with biometrics
{biometricError()}

BREACH PROTECTION

Breach monitoring
Check all passwords
Last check {breachResult()}

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 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. The encrypted storage is identical in size whether a hidden vault exists or not, so the hidden vault cannot be proven or disproven without both passwords. This protects against a coercer who demands your password — not against device malware or someone who already has your device unlocked.
{duressSuccess()}
setDuressMainPassword(e.currentTarget.value)} /> setDuressPassword(e.currentTarget.value)} /> setDuressConfirm(e.currentTarget.value)} />

{duressError()}

CANARY ENTRIES

Canary entries Silently pings a webhook when an entry is accessed. Configure canary on any entry in your vault.
{canaryEntries().length > 0 ? `${canaryEntries().length} active` : "none"}
0}> {(entry) => (
{entry.title}
)}

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 ? " ✓" : " (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}
); } function renderVault() { return (

ENTRIES

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.

MAINTENANCE

Trash auto-delete
Vault Health
0}>
{declinedCardCount()} card{declinedCardCount() === 1 ? '' : 's'} set to never save

DATA

Import passwords
Export backup
Peach Codex Pro
{/* Restore from Codex: intentionally never gated — a printed Codex must remain restorable regardless of license state. */}
Restore from Codex

VAULT HISTORY

Loading…

No snapshots yet. Peach saves automatically before every change — saves, syncs, and imports.

0}>

Restoring locks your vault. Unlock with the password that encrypted that snapshot to confirm.

{(snap) => (
{formatSnapshotTime(snap.savedAt)} {[ snap.entryCount !== null ? `${snap.entryCount} ${snap.entryCount === 1 ? "entry" : "entries"}` : null, reasonLabel(snap.reason), ] .filter(Boolean) .join(" · ")}
)}

Replace vault from backup

Wipes the current vault and restores from a backup file. Recoverable only if your backup is good.

Destroy vault

Permanently wipes every entry, setting, and license on this device. There is no recovery.

); } function renderAppearance() { const sizeOptions: Array<{ id: "small" | "medium" | "large"; label: string }> = [ { id: "small", label: "Small" }, { id: "medium", label: "Default" }, { id: "large", label: "Large" }, ]; return (

THEME

Theme

ACCENT

Accent
{(color) => (

TEXT SIZE

Size
{(opt) => ( )}

MOTION

Reduce motion
); } function renderShortcuts() { const [capturingAction, setCapturingAction] = createSignal(null); const [conflictMsg, setConflictMsg] = createSignal(null); function startCapture(action: ShortcutAction) { setConflictMsg(null); setCapturingAction(action); } function cancelCapture() { setCapturingAction(null); setConflictMsg(null); } function commitBinding(action: ShortcutAction, binding: Keybinding) { const conflict = findConflict(props.shortcuts, binding, action); if (conflict) { const other = SHORTCUT_DESCRIPTORS.find((d) => d.action === conflict); setConflictMsg(`Already bound to "${other?.label ?? conflict}"`); return; } const next = { ...props.shortcuts, [action]: binding }; setCapturingAction(null); setConflictMsg(null); void props.onShortcutsChange(next); } function resetAction(action: ShortcutAction) { const next = { ...props.shortcuts, [action]: DEFAULT_BINDINGS[action] }; void props.onShortcutsChange(next); } function resetAll() { void props.onShortcutsChange({ ...DEFAULT_BINDINGS }); } // Capture-mode key handler — wired via ref onto the chip element so it // catches the next non-modifier keypress and ignores other bindings. function handleCaptureKey(e: KeyboardEvent) { e.preventDefault(); e.stopPropagation(); if (e.key === "Escape") { cancelCapture(); return; } const binding = keybindingFromEvent(e); if (!binding) return; // bare modifier — keep waiting const action = capturingAction(); if (action) commitBinding(action, binding); } return (

ENTRY VIEW

d.scope === "entry")}> {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}

ANYWHERE IN POPUP

d.scope === "global")}> {(desc) => renderRow(desc, capturingAction, startCapture, cancelCapture, resetAction, handleCaptureKey)}
{conflictMsg()}
); function renderRow( desc: typeof SHORTCUT_DESCRIPTORS[number], capturing: () => ShortcutAction | null, onStart: (a: ShortcutAction) => void, onCancel: () => void, onReset: (a: ShortcutAction) => void, onKey: (e: KeyboardEvent) => void, ) { const isActive = () => capturing() === desc.action; const binding = () => props.shortcuts[desc.action]; const isCustom = () => { const b = binding(); const d = DEFAULT_BINDINGS[desc.action]; return b.key !== d.key || Boolean(b.ctrl) !== Boolean(d.ctrl) || Boolean(b.meta) !== Boolean(d.meta) || Boolean(b.shift) !== Boolean(d.shift) || Boolean(b.alt) !== Boolean(d.alt); }; return (
{desc.label}
); } } const PRO_FEATURES: Array<{ feature: ProFeature; name: string; desc: string; }> = [ { feature: "sync", name: "Peach Sync", desc: "Encrypted · peer-to-peer" }, { feature: "codex", name: "Peach Codex", desc: "Restore from paper" }, { feature: "passkeys", name: "Passkeys", desc: "Zero-knowledge WebAuthn" }, { feature: "ios_app", name: "Mobile apps", desc: "Android · iOS soon" }, { feature: "advanced_health", name: "Vault health", desc: "Reuse · age · exposure" }, { feature: "smart_password_change", name: "Smart reset", desc: "Guided password change" }, ]; const [licenseKey, setLicenseKey] = createSignal(null); const [showKey, setShowKey] = createSignal(false); const [showKeyEntry, setShowKeyEntry] = createSignal(false); const [deactivating, setDeactivating] = createSignal(false); async function handleDeactivateLicense() { setDeactivating(true); await sendMessage({ type: "DEACTIVATE_LICENSE" }); setLicenseInfo(null); await props.onLicenseChange?.(); setDeactivating(false); } async function handleTestCanary(entry: {id: string; title: string; canary: Canary}) { setCanaryTestingId(entry.id); setCanaryTestResult(null); const res = await sendMessage({ type: "FIRE_CANARY_PING", payload: { webhookUrl: entry.canary.webhookUrl, entryTitle: entry.title, trigger: "test" }, }) as { ok: boolean }; setCanaryTestResult({ id: entry.id, ok: res.ok }); setCanaryTestingId(null); setTimeout(() => setCanaryTestResult((r) => r?.id === entry.id ? null : r), 3000); } function renderLicense() { async function onActivate() { await handleActivateLicense(); if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") { await props.onLicenseChange?.(); } } return (

PEACH PRO · ONE-TIME

Everything,
everywhere.

One key unlocks every Pro feature — for the lifetime of the app.

{(item, i) => (
{String(i() + 1).padStart(2, "0")} {item.name} {item.desc}
)}

$39 once · yours forever

Get Peach Pro
ALREADY HAVE A KEY
setLicenseKeyInput(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") void onActivate(); }} autocomplete="off" spellcheck={false} />

{licenseError()}

} >

Peach Pro

{formatActivatedSince(licenseInfo()?.issued)}

LICENSE KEY
•••••••••••••••••••••••••••• } > {licenseKey()!}
{(item) => (
✦ {item.name}
)}
); } function renderFeedback() { return (

Help us improve Peach. Every report goes directly to our inbox.

TYPE

{(cat) => ( )}

MESSAGE