1import { createSignal, createMemo, For, onCleanup, onMount, Show } from "solid-js";2import { sendMessage } from "../App";3import { PeachWordmark } from "../components/PeachMark";4import { RecoveryRuneBackground } from "../components/RecoveryRuneBackground";56interface RecoveryWordDisplayViewProps {7 onDone: () => void;8}9101112function pickConfirmIndices(): number[] {13 const pool = Array.from({ length: 12 }, (_, i) => i);14 const rand = new Uint32Array(pool.length);15 crypto.getRandomValues(rand);16 for (let i = pool.length - 1; i > 0; i--) {17 const j = (rand[i] ?? 0) % (i + 1);18 const tmp = pool[i]!;19 pool[i] = pool[j]!;20 pool[j] = tmp;21 }22 return pool.slice(0, 3).sort((a, b) => a - b);23}2425export function RecoveryWordDisplayView(props: RecoveryWordDisplayViewProps) {26 27 const confirmIndices = pickConfirmIndices();28 const [words, setWords] = createSignal<string[]>([]);29 const [error, setError] = createSignal("");30 const [loading, setLoading] = createSignal(true);31 const [confirmWords, setConfirmWords] = createSignal(["", "", ""]);32 const [confirmError, setConfirmError] = createSignal("");33 const [confirmed, setConfirmed] = createSignal(false);34 35 const [countdown, setCountdown] = createSignal(5);36 let countdownTimer: ReturnType<typeof setInterval> | null = null;3738 onMount(async () => {39 40 const res = (await sendMessage({41 type: "GET_RECOVERY_WORDS",42 })) as { ok: boolean; data?: string[]; error?: string };4344 if (res.ok && res.data) {45 setWords(res.data);46 } else {47 setError("could not retrieve recovery words");48 }49 setLoading(false);5051 52 let remaining = 5;53 countdownTimer = setInterval(() => {54 remaining -= 1;55 setCountdown(remaining);56 if (remaining <= 0 && countdownTimer) {57 clearInterval(countdownTimer);58 countdownTimer = null;59 }60 }, 1000);61 });6263 onCleanup(() => {64 if (countdownTimer) clearInterval(countdownTimer);65 });6667 async function handleConfirm() {68 const confirmed = confirmWords().map((w) => w.toLowerCase().trim());69 if (confirmed.some((w) => !w)) {70 setConfirmError("all three words are required");71 return;72 }7374 75 const stored = words();76 for (let i = 0; i < confirmIndices.length; i++) {77 const pos = confirmIndices[i]!;78 if (confirmed[i] !== stored[pos]) {79 setConfirmError(`word ${pos + 1} is incorrect`);80 return;81 }82 }8384 setConfirmed(true);85 setConfirmError("");86 87 88 89 90 try {91 await chrome.storage.local?.remove("peach_recovery_pending");92 } catch { }93 94 await new Promise((r) => setTimeout(r, 600));95 props.onDone();96 }9798 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());99100 return (101 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">102 <RecoveryRuneBackground />103104 <header class="peach-screen-header">105 <div class="peach-wordmark-stack">106 <PeachWordmark class="peach-wordmark-wrap" />107 </div>108 </header>109110 <div class="peach-recovery-header">111 <h1 class="peach-onboarding-headline">save your recovery words</h1>112 <p class="peach-hint peach-hint--warning">113 write these down — you can view them again in Settings with your master password114 </p>115 </div>116117 <Show when={loading()}>118 <p class="peach-hint">loading...</p>119 </Show>120121 <Show when={error()}>122 <p class="peach-hint peach-hint-error">{error()}</p>123 </Show>124125 <Show when={!loading() && !error()}>126 <div class="peach-recovery-words-display">127 <div class="peach-recovery-words-label">your 12 recovery words</div>128 <ol class="peach-recovery-word-list" start={1}>129 <For each={words()}>130 {(word, i) => (131 <li class="peach-recovery-word-item">132 <span class="peach-recovery-word-num">{i() + 1}</span>133 <span class="peach-recovery-word-text">{word}</span>134 </li>135 )}136 </For>137 </ol>138 <button139 type="button"140 class="peach-recovery-download-btn"141 onClick={() => void saveRecoveryWords(words())}142 >143 download as .txt144 </button>145 </div>146147 {}148 <div class="peach-recovery-confirm">149 <Show when={!confirmed()}>150 <p class="peach-recovery-confirm-label">151 {countdown() > 0152 ? `type the highlighted words to confirm (${countdown()}s)`153 : "type the highlighted words to confirm"}154 </p>155 </Show>156 <Show when={confirmed()}>157 <p class="peach-hint peach-hint--success">✓ words verified</p>158 </Show>159160 <div class="peach-recovery-confirm-fields">161 <For each={confirmIndices}>162 {(pos, i) => (163 <div class="peach-recovery-confirm-field">164 <span class="peach-recovery-confirm-num">165 word {pos + 1}166 </span>167 <input168 class="peach-recovery-word-input peach-recovery-confirm-input"169 type="text"170 autocomplete="off"171 autocorrect="off"172 autocapitalize="none"173 spellcheck={false}174 disabled={!canConfirm()}175 value={confirmWords()[i()]}176 onInput={(e) => {177 const updated = [...confirmWords()];178 updated[i()] = (e.target as HTMLInputElement).value;179 setConfirmWords(updated);180 if (confirmError()) setConfirmError("");181 }}182 onKeyDown={(e) => {183 if (e.key === "Enter" && canConfirm()) void handleConfirm();184 }}185 />186 </div>187 )}188 </For>189 </div>190191 <Show when={confirmError()}>192 <p class="peach-hint peach-hint-error">{confirmError()}</p>193 </Show>194195 <button196 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"197 disabled={!canConfirm() || confirmed()}198 onClick={() => void handleConfirm()}199 >200 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}201 </button>202 </div>203 </Show>204 </section>205 );206}207208function recoveryWordsText(words: string[]): string {209 const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n");210 return (211 "PEACH RECOVERY WORDS\n" +212 "====================\n\n" +213 "Anyone with these 12 words can recover your vault.\n" +214 "Store this somewhere only you can access.\n\n" +215 numbered + "\n"216 );217}218219220221222223function saveRecoveryWords(words: string[]): void {224 const blob = new Blob([recoveryWordsText(words)], { type: "text/plain" });225 const a = document.createElement("a");226 a.download = "peach-recovery-words.txt";227 a.href = URL.createObjectURL(blob);228 setTimeout(() => URL.revokeObjectURL(a.href), 1000);229 document.body.appendChild(a);230 a.click();231 a.remove();232}