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 await refreshWords();4041 42 let remaining = 5;43 countdownTimer = setInterval(() => {44 remaining -= 1;45 setCountdown(remaining);46 if (remaining <= 0 && countdownTimer) {47 clearInterval(countdownTimer);48 countdownTimer = null;49 }50 }, 1000);51 });5253 async function refreshWords(): Promise<void> {54 55 const res = (await sendMessage({56 type: "GET_RECOVERY_WORDS",57 })) as { ok: boolean; data?: string[]; error?: string };5859 if (res.ok && res.data) {60 setWords(res.data);61 } else {62 setError("could not retrieve recovery words");63 }64 setLoading(false);65 }6667 68 69 70 71 const handleVisibilityChange = () => {72 if (document.visibilityState === "hidden") {73 setWords([]);74 } else if (!confirmed()) {75 void refreshWords();76 }77 };78 document.addEventListener("visibilitychange", handleVisibilityChange);7980 onCleanup(() => {81 if (countdownTimer) clearInterval(countdownTimer);82 document.removeEventListener("visibilitychange", handleVisibilityChange);83 });8485 async function handleConfirm() {86 const confirmed = confirmWords().map((w) => w.toLowerCase().trim());87 if (confirmed.some((w) => !w)) {88 setConfirmError("all three words are required");89 return;90 }9192 93 const stored = words();94 for (let i = 0; i < confirmIndices.length; i++) {95 const pos = confirmIndices[i]!;96 if (confirmed[i] !== stored[pos]) {97 setConfirmError(`word ${pos + 1} is incorrect`);98 return;99 }100 }101102 setConfirmed(true);103 setConfirmError("");104 105 106 107 108 try {109 await chrome.storage.local?.remove("peach_recovery_pending");110 } catch { }111 112 await new Promise((r) => setTimeout(r, 600));113 props.onDone();114 }115116 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());117118 return (119 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">120 <RecoveryRuneBackground />121122 <header class="peach-screen-header">123 <div class="peach-wordmark-stack">124 <PeachWordmark class="peach-wordmark-wrap" />125 </div>126 </header>127128 <div class="peach-recovery-header">129 <h1 class="peach-onboarding-headline">save your recovery words</h1>130 <p class="peach-hint peach-hint--warning">131 write these down — you can view them again in Settings with your master password132 </p>133 </div>134135 <Show when={loading()}>136 <p class="peach-hint">loading...</p>137 </Show>138139 <Show when={error()}>140 <p class="peach-hint peach-hint-error">{error()}</p>141 </Show>142143 <Show when={!loading() && !error()}>144 <div class="peach-recovery-words-display">145 <div class="peach-recovery-words-label">your 12 recovery words</div>146 <ol class="peach-recovery-word-list" start={1}>147 <For each={words()}>148 {(word, i) => (149 <li class="peach-recovery-word-item">150 <span class="peach-recovery-word-num">{i() + 1}</span>151 <span class="peach-recovery-word-text">{word}</span>152 </li>153 )}154 </For>155 </ol>156 <button157 type="button"158 class="peach-recovery-download-btn"159 onClick={() => void saveRecoveryWords(words())}160 >161 download as .txt162 </button>163 </div>164165 {}166 <div class="peach-recovery-confirm">167 <Show when={!confirmed()}>168 <p class="peach-recovery-confirm-label">169 {countdown() > 0170 ? `type the highlighted words to confirm (${countdown()}s)`171 : "type the highlighted words to confirm"}172 </p>173 </Show>174 <Show when={confirmed()}>175 <p class="peach-hint peach-hint--success">✓ words verified</p>176 </Show>177178 <div class="peach-recovery-confirm-fields">179 <For each={confirmIndices}>180 {(pos, i) => (181 <div class="peach-recovery-confirm-field">182 <span class="peach-recovery-confirm-num">183 word {pos + 1}184 </span>185 <input186 class="peach-recovery-word-input peach-recovery-confirm-input"187 type="text"188 autocomplete="off"189 autocorrect="off"190 autocapitalize="none"191 spellcheck={false}192 disabled={!canConfirm()}193 value={confirmWords()[i()]}194 onInput={(e) => {195 const updated = [...confirmWords()];196 updated[i()] = (e.target as HTMLInputElement).value;197 setConfirmWords(updated);198 if (confirmError()) setConfirmError("");199 }}200 onKeyDown={(e) => {201 if (e.key === "Enter" && canConfirm()) void handleConfirm();202 }}203 />204 </div>205 )}206 </For>207 </div>208209 <Show when={confirmError()}>210 <p class="peach-hint peach-hint-error">{confirmError()}</p>211 </Show>212213 <button214 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"215 disabled={!canConfirm() || confirmed()}216 onClick={() => void handleConfirm()}217 >218 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}219 </button>220 </div>221 </Show>222 </section>223 );224}225226function recoveryWordsText(words: string[]): string {227 const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n");228 return (229 "PEACH RECOVERY WORDS\n" +230 "====================\n\n" +231 "Anyone with these 12 words can recover your vault.\n" +232 "Store this somewhere only you can access.\n\n" +233 numbered + "\n"234 );235}236237238239240241function saveRecoveryWords(words: string[]): void {242 const blob = new Blob([recoveryWordsText(words)], { type: "text/plain" });243 const a = document.createElement("a");244 a.download = "peach-recovery-words.txt";245 a.href = URL.createObjectURL(blob);246 setTimeout(() => URL.revokeObjectURL(a.href), 1000);247 document.body.appendChild(a);248 a.click();249 a.remove();250}