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}91011const CONFIRM_WORD_INDICES = [2, 6, 10]; 1213export function RecoveryWordDisplayView(props: RecoveryWordDisplayViewProps) {14 const [words, setWords] = createSignal<string[]>([]);15 const [error, setError] = createSignal("");16 const [loading, setLoading] = createSignal(true);17 const [confirmWords, setConfirmWords] = createSignal(["", "", ""]);18 const [confirmError, setConfirmError] = createSignal("");19 const [confirmed, setConfirmed] = createSignal(false);20 21 const [countdown, setCountdown] = createSignal(5);22 let countdownTimer: ReturnType<typeof setInterval> | null = null;2324 onMount(async () => {25 26 const res = (await sendMessage({27 type: "GET_RECOVERY_WORDS",28 })) as { ok: boolean; data?: string[]; error?: string };2930 if (res.ok && res.data) {31 setWords(res.data);32 } else {33 setError("could not retrieve recovery words");34 }35 setLoading(false);3637 38 let remaining = 5;39 countdownTimer = setInterval(() => {40 remaining -= 1;41 setCountdown(remaining);42 if (remaining <= 0 && countdownTimer) {43 clearInterval(countdownTimer);44 countdownTimer = null;45 }46 }, 1000);47 });4849 onCleanup(() => {50 if (countdownTimer) clearInterval(countdownTimer);51 });5253 async function handleConfirm() {54 const confirmed = confirmWords().map((w) => w.toLowerCase().trim());55 if (confirmed.some((w) => !w)) {56 setConfirmError("all three words are required");57 return;58 }5960 61 const stored = words();62 for (let i = 0; i < CONFIRM_WORD_INDICES.length; i++) {63 const pos = CONFIRM_WORD_INDICES[i];64 if (confirmed[i] !== stored[pos]) {65 setConfirmError(`word ${pos + 1} is incorrect`);66 return;67 }68 }6970 setConfirmed(true);71 setConfirmError("");72 73 74 75 try {76 await chrome.storage.session?.remove("peach_recovery_pending");77 } catch { }78 79 await new Promise((r) => setTimeout(r, 600));80 props.onDone();81 }8283 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());8485 return (86 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">87 <RecoveryRuneBackground />8889 <header class="peach-screen-header">90 <div class="peach-wordmark-stack">91 <PeachWordmark class="peach-wordmark-wrap" />92 </div>93 </header>9495 <div class="peach-recovery-header">96 <h1 class="peach-onboarding-headline">save your recovery words</h1>97 <p class="peach-hint peach-hint--warning">98 write these down — you will never see them again99 </p>100 </div>101102 <Show when={loading()}>103 <p class="peach-hint">loading...</p>104 </Show>105106 <Show when={error()}>107 <p class="peach-hint peach-hint-error">{error()}</p>108 </Show>109110 <Show when={!loading() && !error()}>111 <div class="peach-recovery-words-display">112 <div class="peach-recovery-words-label">your 12 recovery words</div>113 <ol class="peach-recovery-word-list" start={1}>114 <For each={words()}>115 {(word, i) => (116 <li class="peach-recovery-word-item">117 <span class="peach-recovery-word-num">{i() + 1}</span>118 <span class="peach-recovery-word-text">{word}</span>119 </li>120 )}121 </For>122 </ol>123 <button124 type="button"125 class="peach-recovery-download-btn"126 onClick={() => downloadRecoveryWords(words())}127 >128 download as .txt129 </button>130 </div>131132 {}133 <div class="peach-recovery-confirm">134 <Show when={!confirmed()}>135 <p class="peach-recovery-confirm-label">136 {countdown() > 0137 ? `type the highlighted words to confirm (${countdown()}s)`138 : "type the highlighted words to confirm"}139 </p>140 </Show>141 <Show when={confirmed()}>142 <p class="peach-hint peach-hint--success">✓ words verified</p>143 </Show>144145 <div class="peach-recovery-confirm-fields">146 <For each={CONFIRM_WORD_INDICES}>147 {(pos, i) => (148 <div class="peach-recovery-confirm-field">149 <span class="peach-recovery-confirm-num">150 word {pos + 1}151 </span>152 <input153 class="peach-recovery-word-input peach-recovery-confirm-input"154 type="text"155 autocomplete="off"156 autocorrect="off"157 autocapitalize="none"158 spellcheck={false}159 disabled={!canConfirm()}160 value={confirmWords()[i()]}161 onInput={(e) => {162 const updated = [...confirmWords()];163 updated[i()] = (e.target as HTMLInputElement).value;164 setConfirmWords(updated);165 if (confirmError()) setConfirmError("");166 }}167 onKeyDown={(e) => {168 if (e.key === "Enter" && canConfirm()) void handleConfirm();169 }}170 />171 </div>172 )}173 </For>174 </div>175176 <Show when={confirmError()}>177 <p class="peach-hint peach-hint-error">{confirmError()}</p>178 </Show>179180 <button181 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"182 disabled={!canConfirm() || confirmed()}183 onClick={() => void handleConfirm()}184 >185 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}186 </button>187 </div>188 </Show>189 </section>190 );191}192193function downloadRecoveryWords(words: string[]): void {194 const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n");195 const body =196 "PEACH RECOVERY WORDS\n" +197 "====================\n\n" +198 "Anyone with these 12 words can recover your vault.\n" +199 "Store this file somewhere only you can access.\n\n" +200 numbered + "\n";201 const blob = new Blob([body], { type: "text/plain" });202 const url = URL.createObjectURL(blob);203 const a = document.createElement("a");204 a.href = url;205 a.download = "peach-recovery-words.txt";206 document.body.appendChild(a);207 a.click();208 a.remove();209 URL.revokeObjectURL(url);210}