1import { createSignal, createMemo, For, onCleanup, onMount, Show } from "solid-js";2import { sendMessage } from "../App";3import { PeachWordmark } from "../components/PeachMark";4import { RecoveryRuneBackground } from "../components/RecoveryRuneBackground";5import { isNativePlatform } from "../../shared/native";67interface RecoveryWordDisplayViewProps {8 onDone: () => void;9}10111213function pickConfirmIndices(): number[] {14 const pool = Array.from({ length: 12 }, (_, i) => i);15 const rand = new Uint32Array(pool.length);16 crypto.getRandomValues(rand);17 for (let i = pool.length - 1; i > 0; i--) {18 const j = (rand[i] ?? 0) % (i + 1);19 const tmp = pool[i]!;20 pool[i] = pool[j]!;21 pool[j] = tmp;22 }23 return pool.slice(0, 3).sort((a, b) => a - b);24}2526export function RecoveryWordDisplayView(props: RecoveryWordDisplayViewProps) {27 28 const confirmIndices = pickConfirmIndices();29 const [words, setWords] = createSignal<string[]>([]);30 const [error, setError] = createSignal("");31 const [loading, setLoading] = createSignal(true);32 const [confirmWords, setConfirmWords] = createSignal(["", "", ""]);33 const [confirmError, setConfirmError] = createSignal("");34 const [confirmed, setConfirmed] = createSignal(false);35 36 const [countdown, setCountdown] = createSignal(5);37 const [copied, setCopied] = createSignal(false);3839 async function copyRecoveryWords() {40 try {41 await navigator.clipboard.writeText(recoveryWordsText(words()));42 setCopied(true);43 setTimeout(() => setCopied(false), 1800);44 } catch {45 46 }47 }48 let countdownTimer: ReturnType<typeof setInterval> | null = null;4950 onMount(async () => {51 52 const res = (await sendMessage({53 type: "GET_RECOVERY_WORDS",54 })) as { ok: boolean; data?: string[]; error?: string };5556 if (res.ok && res.data) {57 setWords(res.data);58 } else {59 setError("could not retrieve recovery words");60 }61 setLoading(false);6263 64 let remaining = 5;65 countdownTimer = setInterval(() => {66 remaining -= 1;67 setCountdown(remaining);68 if (remaining <= 0 && countdownTimer) {69 clearInterval(countdownTimer);70 countdownTimer = null;71 }72 }, 1000);73 });7475 onCleanup(() => {76 if (countdownTimer) clearInterval(countdownTimer);77 });7879 async function handleConfirm() {80 const confirmed = confirmWords().map((w) => w.toLowerCase().trim());81 if (confirmed.some((w) => !w)) {82 setConfirmError("all three words are required");83 return;84 }8586 87 const stored = words();88 for (let i = 0; i < confirmIndices.length; i++) {89 const pos = confirmIndices[i]!;90 if (confirmed[i] !== stored[pos]) {91 setConfirmError(`word ${pos + 1} is incorrect`);92 return;93 }94 }9596 setConfirmed(true);97 setConfirmError("");98 99 100 101 102 try {103 await chrome.storage.local?.remove("peach_recovery_pending");104 } catch { }105 106 await new Promise((r) => setTimeout(r, 600));107 props.onDone();108 }109110 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());111112 return (113 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">114 <RecoveryRuneBackground />115116 <header class="peach-screen-header">117 <div class="peach-wordmark-stack">118 <PeachWordmark class="peach-wordmark-wrap" />119 </div>120 </header>121122 <div class="peach-recovery-header">123 <h1 class="peach-onboarding-headline">save your recovery words</h1>124 <p class="peach-hint peach-hint--warning">125 write these down — you can view them again in Settings with your master password126 </p>127 </div>128129 <Show when={loading()}>130 <p class="peach-hint">loading...</p>131 </Show>132133 <Show when={error()}>134 <p class="peach-hint peach-hint-error">{error()}</p>135 </Show>136137 <Show when={!loading() && !error()}>138 <div class="peach-recovery-words-display">139 <div class="peach-recovery-words-label">your 12 recovery words</div>140 <ol class="peach-recovery-word-list" start={1}>141 <For each={words()}>142 {(word, i) => (143 <li class="peach-recovery-word-item">144 <span class="peach-recovery-word-num">{i() + 1}</span>145 <span class="peach-recovery-word-text">{word}</span>146 </li>147 )}148 </For>149 </ol>150 {151152153154}155 <Show156 when={isNativePlatform()}157 fallback={158 <button159 type="button"160 class="peach-recovery-download-btn"161 onClick={() => void saveRecoveryWords(words())}162 >163 download as .txt164 </button>165 }166 >167 <button168 type="button"169 class="peach-recovery-download-btn"170 onClick={() => void copyRecoveryWords()}171 >172 {copied() ? "copied ✓ — paste somewhere safe" : "copy words"}173 </button>174 </Show>175 </div>176177 {}178 <div class="peach-recovery-confirm">179 <Show when={!confirmed()}>180 <p class="peach-recovery-confirm-label">181 {countdown() > 0182 ? `type the highlighted words to confirm (${countdown()}s)`183 : "type the highlighted words to confirm"}184 </p>185 </Show>186 <Show when={confirmed()}>187 <p class="peach-hint peach-hint--success">✓ words verified</p>188 </Show>189190 <div class="peach-recovery-confirm-fields">191 <For each={confirmIndices}>192 {(pos, i) => (193 <div class="peach-recovery-confirm-field">194 <span class="peach-recovery-confirm-num">195 word {pos + 1}196 </span>197 <input198 class="peach-recovery-word-input peach-recovery-confirm-input"199 type="text"200 autocomplete="off"201 autocorrect="off"202 autocapitalize="none"203 spellcheck={false}204 disabled={!canConfirm()}205 value={confirmWords()[i()]}206 onInput={(e) => {207 const updated = [...confirmWords()];208 updated[i()] = (e.target as HTMLInputElement).value;209 setConfirmWords(updated);210 if (confirmError()) setConfirmError("");211 }}212 onKeyDown={(e) => {213 if (e.key === "Enter" && canConfirm()) void handleConfirm();214 }}215 />216 </div>217 )}218 </For>219 </div>220221 <Show when={confirmError()}>222 <p class="peach-hint peach-hint-error">{confirmError()}</p>223 </Show>224225 <button226 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"227 disabled={!canConfirm() || confirmed()}228 onClick={() => void handleConfirm()}229 >230 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}231 </button>232 </div>233 </Show>234 </section>235 );236}237238function recoveryWordsText(words: string[]): string {239 const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n");240 return (241 "PEACH RECOVERY WORDS\n" +242 "====================\n\n" +243 "Anyone with these 12 words can recover your vault.\n" +244 "Store this somewhere only you can access.\n\n" +245 numbered + "\n"246 );247}248249250251252253function saveRecoveryWords(words: string[]): void {254 const blob = new Blob([recoveryWordsText(words)], { type: "text/plain" });255 const a = document.createElement("a");256 a.download = "peach-recovery-words.txt";257 a.href = URL.createObjectURL(blob);258 setTimeout(() => URL.revokeObjectURL(a.href), 1000);259 document.body.appendChild(a);260 a.click();261 a.remove();262}