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 76 try {77 await chrome.storage.local?.remove("peach_recovery_pending");78 } catch { }79 80 await new Promise((r) => setTimeout(r, 600));81 props.onDone();82 }8384 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());8586 return (87 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">88 <RecoveryRuneBackground />8990 <header class="peach-screen-header">91 <div class="peach-wordmark-stack">92 <PeachWordmark class="peach-wordmark-wrap" />93 </div>94 </header>9596 <div class="peach-recovery-header">97 <h1 class="peach-onboarding-headline">save your recovery words</h1>98 <p class="peach-hint peach-hint--warning">99 write these down — you will never see them again100 </p>101 </div>102103 <Show when={loading()}>104 <p class="peach-hint">loading...</p>105 </Show>106107 <Show when={error()}>108 <p class="peach-hint peach-hint-error">{error()}</p>109 </Show>110111 <Show when={!loading() && !error()}>112 <div class="peach-recovery-words-display">113 <div class="peach-recovery-words-label">your 12 recovery words</div>114 <ol class="peach-recovery-word-list" start={1}>115 <For each={words()}>116 {(word, i) => (117 <li class="peach-recovery-word-item">118 <span class="peach-recovery-word-num">{i() + 1}</span>119 <span class="peach-recovery-word-text">{word}</span>120 </li>121 )}122 </For>123 </ol>124 <button125 type="button"126 class="peach-recovery-download-btn"127 onClick={() => downloadRecoveryWords(words())}128 >129 download as .txt130 </button>131 </div>132133 {}134 <div class="peach-recovery-confirm">135 <Show when={!confirmed()}>136 <p class="peach-recovery-confirm-label">137 {countdown() > 0138 ? `type the highlighted words to confirm (${countdown()}s)`139 : "type the highlighted words to confirm"}140 </p>141 </Show>142 <Show when={confirmed()}>143 <p class="peach-hint peach-hint--success">✓ words verified</p>144 </Show>145146 <div class="peach-recovery-confirm-fields">147 <For each={CONFIRM_WORD_INDICES}>148 {(pos, i) => (149 <div class="peach-recovery-confirm-field">150 <span class="peach-recovery-confirm-num">151 word {pos + 1}152 </span>153 <input154 class="peach-recovery-word-input peach-recovery-confirm-input"155 type="text"156 autocomplete="off"157 autocorrect="off"158 autocapitalize="none"159 spellcheck={false}160 disabled={!canConfirm()}161 value={confirmWords()[i()]}162 onInput={(e) => {163 const updated = [...confirmWords()];164 updated[i()] = (e.target as HTMLInputElement).value;165 setConfirmWords(updated);166 if (confirmError()) setConfirmError("");167 }}168 onKeyDown={(e) => {169 if (e.key === "Enter" && canConfirm()) void handleConfirm();170 }}171 />172 </div>173 )}174 </For>175 </div>176177 <Show when={confirmError()}>178 <p class="peach-hint peach-hint-error">{confirmError()}</p>179 </Show>180181 <button182 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"183 disabled={!canConfirm() || confirmed()}184 onClick={() => void handleConfirm()}185 >186 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}187 </button>188 </div>189 </Show>190 </section>191 );192}193194function downloadRecoveryWords(words: string[]): void {195 const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n");196 const body =197 "PEACH RECOVERY WORDS\n" +198 "====================\n\n" +199 "Anyone with these 12 words can recover your vault.\n" +200 "Store this file somewhere only you can access.\n\n" +201 numbered + "\n";202 const blob = new Blob([body], { type: "text/plain" });203 const url = URL.createObjectURL(blob);204 const a = document.createElement("a");205 a.href = url;206 a.download = "peach-recovery-words.txt";207 document.body.appendChild(a);208 a.click();209 a.remove();210 URL.revokeObjectURL(url);211}