import { createSignal, createMemo, For, onCleanup, onMount, Show } from "solid-js"; import { sendMessage } from "../App"; import { PeachWordmark } from "../components/PeachMark"; import { RecoveryRuneBackground } from "../components/RecoveryRuneBackground"; import { isNativePlatform } from "../../shared/native"; interface RecoveryWordDisplayViewProps { onDone: () => void; } // After showing the words, user must type the 3 words at these positions const CONFIRM_WORD_INDICES = [2, 6, 10]; // 0-indexed → positions 3, 7, 11 export function RecoveryWordDisplayView(props: RecoveryWordDisplayViewProps) { const [words, setWords] = createSignal([]); const [error, setError] = createSignal(""); const [loading, setLoading] = createSignal(true); const [confirmWords, setConfirmWords] = createSignal(["", "", ""]); const [confirmError, setConfirmError] = createSignal(""); const [confirmed, setConfirmed] = createSignal(false); // 5-second countdown before confirmation input is enabled const [countdown, setCountdown] = createSignal(5); let countdownTimer: ReturnType | null = null; onMount(async () => { // Fetch the recovery words from the background (vault must be unlocked). const res = (await sendMessage({ type: "GET_RECOVERY_WORDS", })) as { ok: boolean; data?: string[]; error?: string }; if (res.ok && res.data) { setWords(res.data); } else { setError("could not retrieve recovery words"); } setLoading(false); // Start 5-second countdown let remaining = 5; countdownTimer = setInterval(() => { remaining -= 1; setCountdown(remaining); if (remaining <= 0 && countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; } }, 1000); }); onCleanup(() => { if (countdownTimer) clearInterval(countdownTimer); }); async function handleConfirm() { const confirmed = confirmWords().map((w) => w.toLowerCase().trim()); if (confirmed.some((w) => !w)) { setConfirmError("all three words are required"); return; } // Verify against stored words const stored = words(); for (let i = 0; i < CONFIRM_WORD_INDICES.length; i++) { const pos = CONFIRM_WORD_INDICES[i]; if (confirmed[i] !== stored[pos]) { setConfirmError(`word ${pos + 1} is incorrect`); return; } } setConfirmed(true); setConfirmError(""); // Clear the recovery-pending flag so lock-on-close / auto-lock resume // their normal behavior and future unlocks no longer re-route here. // Once the user has confirmed the words, losing the in-memory key is // no longer a data-loss event. try { await chrome.storage.local?.remove("peach_recovery_pending"); } catch { /* best-effort */ } // Small pause so user sees the success state, then proceed. await new Promise((r) => setTimeout(r, 600)); props.onDone(); } const canConfirm = createMemo(() => countdown() <= 0 && !confirmed()); return (

save your recovery words

write these down — you will never see them again

loading...

{error()}

your 12 recovery words
    {(word, i) => (
  1. {i() + 1} {word}
  2. )}
{/* Confirmation step */}

{countdown() > 0 ? `type the highlighted words to confirm (${countdown()}s)` : "type the highlighted words to confirm"}

✓ words verified

{(pos, i) => (
word {pos + 1} { const updated = [...confirmWords()]; updated[i()] = (e.target as HTMLInputElement).value; setConfirmWords(updated); if (confirmError()) setConfirmError(""); }} onKeyDown={(e) => { if (e.key === "Enter" && canConfirm()) void handleConfirm(); }} />
)}

{confirmError()}

); } function saveRecoveryWords(words: string[]): void { const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n"); const body = "PEACH RECOVERY WORDS\n" + "====================\n\n" + "Anyone with these 12 words can recover your vault.\n" + "Store this file somewhere only you can access.\n\n" + numbered + "\n"; const a = document.createElement("a"); a.download = "peach-recovery-words.txt"; if (isNativePlatform()) { // Capacitor WebView: blob: URLs are opaque to native, but data: URLs go // through MainActivity.setDownloadListener which writes them to public // Downloads via MediaStore. Encode as base64 to keep the data URL // intact through the WebView's URL plumbing. const b64 = btoa(unescape(encodeURIComponent(body))); a.href = `data:text/plain;base64,${b64}`; } else { const blob = new Blob([body], { type: "text/plain" }); a.href = URL.createObjectURL(blob); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } document.body.appendChild(a); a.click(); a.remove(); }