import { Match, Show, Switch, createMemo, createSignal, onMount } from "solid-js"; import { Capacitor } from "@capacitor/core"; import { sendMessage } from "../App"; import { LockRuneCanvas } from "../components/LockRuneCanvas"; import { PeachMark, PeachWordmark } from "../components/PeachMark"; import { ImportView } from "./Import"; import { biometricUnlock, biometricAvailable } from "../../shared/biometric"; // Lazy-load zxcvbn — it's heavy (~800 kB) and only needed during vault creation let _zxcvbn: ((password: string) => { score: number }) | null = null; async function getZxcvbn() { if (_zxcvbn) return _zxcvbn; const mod = await import("zxcvbn"); _zxcvbn = mod.default; return _zxcvbn; } interface LockProps { hasVault: boolean; ready: boolean; isPro?: boolean; onUnlock: () => void | Promise; /** Optional escape hatch: rewind from the create-vault screen back to * Onboarding. Only meaningful when the user reached create-vault from * Onboarding in the first place. */ onBackToOnboarding?: () => void; } // "restore" = picker, "peachscript" / "file" = detail flows, // "mayan" = Mayan-code import (decoder not shipped yet — picker exposes a "soon" card). type RestoreSurface = "restore" | "peachscript" | "file" | "mayan" | null; export function LockView(props: LockProps) { const [password, setPassword] = createSignal(""); const [confirmPassword, setConfirmPassword] = createSignal(""); const [error, setError] = createSignal(""); const [loading, setLoading] = createSignal(false); const [restoreSurface, setRestoreSurface] = createSignal(null); const [showPassword, setShowPassword] = createSignal(false); const MIN_LENGTH = 12; const MIN_ZXCVBN_SCORE = 2; const [strengthScore, setStrengthScore] = createSignal(null); const canCreate = createMemo( () => password().length >= MIN_LENGTH && confirmPassword().length >= MIN_LENGTH && password() === confirmPassword() && strengthScore() !== null && strengthScore()! >= MIN_ZXCVBN_SCORE ); const passwordStrength = createMemo(() => { const score = strengthScore(); if (score === null) return null; if (score <= 1) return "weak"; if (score === 2) return "fair"; if (score === 3) return "good"; return "strong"; }); const createBlockerMessage = createMemo(() => { if (!password()) return null; if (password().length < MIN_LENGTH) return `need ${MIN_LENGTH - password().length} more characters`; if (!confirmPassword()) return "confirm your password"; if (password() !== confirmPassword()) return "passwords don't match"; if (strengthScore() === null) return "checking strength…"; if (strengthScore()! < MIN_ZXCVBN_SCORE) return "password too weak — add words or length"; return null; }); // Update strength asynchronously when password changes createMemo(() => { const value = password(); if (!value) { setStrengthScore(null); return; } getZxcvbn().then((zxcvbn) => { if (password() === value) setStrengthScore(zxcvbn(value).score); }).catch(() => { // zxcvbn unavailable — fall back to length-only heuristic if (password() === value) setStrengthScore(value.length >= 16 ? 3 : value.length >= 12 ? 2 : 1); }); }); async function handleUnlock() { if (!password().trim()) return; setLoading(true); setError(""); const response = await sendMessage({ type: "UNLOCK", payload: { password: password() }, }); const result = response as { ok: boolean; error?: string }; if (result.ok) { await props.onUnlock(); setLoading(false); return; } setError(getDisplayError(result.error, "incorrect password")); setPassword(""); const passwordField = document.querySelector(".lock-password-input") as HTMLInputElement | undefined; passwordField?.classList.add("shake"); setTimeout(() => passwordField?.classList.remove("shake"), 300); setLoading(false); } async function handleCreate() { if (!password().trim()) return; if (password().length < MIN_LENGTH) { setError(`password must be at least ${MIN_LENGTH} characters`); return; } if (password() !== confirmPassword()) { setError("passwords do not match"); return; } const score = strengthScore(); if (score === null || score < MIN_ZXCVBN_SCORE) { setError("password is too weak — try a longer or more varied passphrase"); return; } setLoading(true); setError(""); const response = await sendMessage({ type: "CREATE_VAULT", payload: { password: password() }, }); const result = response as { ok: boolean; error?: string }; if (result.ok) { await props.onUnlock(); setLoading(false); return; } setError(getDisplayError(result.error, "failed to create vault")); setLoading(false); } function openRestore(surface: Exclude) { if (!props.ready || loading()) return; setPassword(""); setConfirmPassword(""); setError(""); setRestoreSurface(surface); } // ── All surfaces routed reactively via Switch/Match ─────────────────── return ( {/* ── Restore picker ────────────────────────────────────────── */} setRestoreSurface(null)} onPickPeachScript={() => setRestoreSurface("peachscript")} onPickMayan={() => setRestoreSurface("mayan")} onPickCSV={() => setRestoreSurface("file")} /> {/* ── Mayan code restore (decoder coming soon) ──────────────── */} setRestoreSurface("restore")} /> {/* ── PeachScript restore ───────────────────────────────────── */} setRestoreSurface("restore")} onImport={async () => { setRestoreSurface(null); await props.onUnlock(); }} /> {/* ── File restore ──────────────────────────────────────────── */} setRestoreSurface("restore")} onImport={async () => { setRestoreSurface(null); await props.onUnlock(); }} /> {/* ── Unlock (hasVault = true) ───────────────────────────────── */}
pro
vault locked

unlock
your vault.

{ if (props.ready) setTimeout(() => el.focus(), 80); }} onInput={(event) => setPassword((event.target as HTMLInputElement).value)} onKeyDown={(event) => { if (event.key !== "Enter" || !props.ready) return; void handleUnlock(); }} />

{error()}

{/* ── Onboarding (hasVault = false) ─────────────────────────── */}
pro

{"keep your\nsecrets."}


setPassword((event.target as HTMLInputElement).value)} onKeyDown={(event) => { if (event.key !== "Enter" || !props.ready) return; void handleCreate(); }} /> 0 && passwordStrength() !== null} fallback={

at least 12 characters

} >
{passwordStrength()}
setConfirmPassword((event.target as HTMLInputElement).value)} onKeyDown={(event) => { if (event.key === "Enter" && props.ready) void handleCreate(); }} />

{error()}

If you forget it, your vault cannot be opened. There is no reset.

); } // ── Restore method picker ────────────────────────────────────────────────── function RestorePickerView(props: { onBack: () => void; onPickPeachScript: () => void; onPickMayan: () => void; onPickCSV: () => void; }) { return (
restore

bring back
your vault.

); } // ── Mayan code (decoder coming soon) ─────────────────────────────────────── function MayanComingSoonView(props: { onBack: () => void }) { return (
mayan code

decoder
on its way.

Not yet — but soon.


Mayan codes encode the same backup as PeachScript, but printable on a single page. The phone-camera decoder is the next thing to land for this format.

Until then, restore your vault using the PeachScript that came with this Mayan code, or open a .peach file backup.

); } // ── Biometric unlock ──────────────────────────────────────────────────────── async function handleBiometricUnlock(props: { onUnlock: () => void | Promise }) { const unlocked = await biometricUnlock(); if (unlocked) { await props.onUnlock(); } } function BiometricUnlockButton(props: { ready: boolean; loading: () => boolean; onUnlock: () => void | Promise }) { const [bioAvailable, setBioAvailable] = createSignal(false); onMount(async () => { setBioAvailable(await biometricAvailable()); }); return ( ); } // ── Helpers ──────────────────────────────────────────────────────────────── function getDisplayError(error: string | undefined, fallback: string): string { if (!error) return fallback; if ( error === "SERVICE_WORKER_UNAVAILABLE" || error.includes("Receiving end does not exist") || error.includes("message port closed") ) { return "background service unavailable"; } return error; }