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}101112const CONFIRM_WORD_INDICES = [2, 6, 10]; 1314export function RecoveryWordDisplayView(props: RecoveryWordDisplayViewProps) {15 const [words, setWords] = createSignal<string[]>([]);16 const [error, setError] = createSignal("");17 const [loading, setLoading] = createSignal(true);18 const [confirmWords, setConfirmWords] = createSignal(["", "", ""]);19 const [confirmError, setConfirmError] = createSignal("");20 const [confirmed, setConfirmed] = createSignal(false);21 22 const [countdown, setCountdown] = createSignal(5);23 const [copied, setCopied] = createSignal(false);2425 async function copyRecoveryWords() {26 try {27 await navigator.clipboard.writeText(recoveryWordsText(words()));28 setCopied(true);29 setTimeout(() => setCopied(false), 1800);30 } catch {31 32 }33 }34 let countdownTimer: ReturnType<typeof setInterval> | null = null;3536 onMount(async () => {37 38 const res = (await sendMessage({39 type: "GET_RECOVERY_WORDS",40 })) as { ok: boolean; data?: string[]; error?: string };4142 if (res.ok && res.data) {43 setWords(res.data);44 } else {45 setError("could not retrieve recovery words");46 }47 setLoading(false);4849 50 let remaining = 5;51 countdownTimer = setInterval(() => {52 remaining -= 1;53 setCountdown(remaining);54 if (remaining <= 0 && countdownTimer) {55 clearInterval(countdownTimer);56 countdownTimer = null;57 }58 }, 1000);59 });6061 onCleanup(() => {62 if (countdownTimer) clearInterval(countdownTimer);63 });6465 async function handleConfirm() {66 const confirmed = confirmWords().map((w) => w.toLowerCase().trim());67 if (confirmed.some((w) => !w)) {68 setConfirmError("all three words are required");69 return;70 }7172 73 const stored = words();74 for (let i = 0; i < CONFIRM_WORD_INDICES.length; i++) {75 const pos = CONFIRM_WORD_INDICES[i];76 if (confirmed[i] !== stored[pos]) {77 setConfirmError(`word ${pos + 1} is incorrect`);78 return;79 }80 }8182 setConfirmed(true);83 setConfirmError("");84 85 86 87 88 try {89 await chrome.storage.local?.remove("peach_recovery_pending");90 } catch { }91 92 await new Promise((r) => setTimeout(r, 600));93 props.onDone();94 }9596 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());9798 return (99 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">100 <RecoveryRuneBackground />101102 <header class="peach-screen-header">103 <div class="peach-wordmark-stack">104 <PeachWordmark class="peach-wordmark-wrap" />105 </div>106 </header>107108 <div class="peach-recovery-header">109 <h1 class="peach-onboarding-headline">save your recovery words</h1>110 <p class="peach-hint peach-hint--warning">111 write these down — you will never see them again112 </p>113 </div>114115 <Show when={loading()}>116 <p class="peach-hint">loading...</p>117 </Show>118119 <Show when={error()}>120 <p class="peach-hint peach-hint-error">{error()}</p>121 </Show>122123 <Show when={!loading() && !error()}>124 <div class="peach-recovery-words-display">125 <div class="peach-recovery-words-label">your 12 recovery words</div>126 <ol class="peach-recovery-word-list" start={1}>127 <For each={words()}>128 {(word, i) => (129 <li class="peach-recovery-word-item">130 <span class="peach-recovery-word-num">{i() + 1}</span>131 <span class="peach-recovery-word-text">{word}</span>132 </li>133 )}134 </For>135 </ol>136 {137138139140}141 <Show142 when={isNativePlatform()}143 fallback={144 <button145 type="button"146 class="peach-recovery-download-btn"147 onClick={() => void saveRecoveryWords(words())}148 >149 download as .txt150 </button>151 }152 >153 <button154 type="button"155 class="peach-recovery-download-btn"156 onClick={() => void copyRecoveryWords()}157 >158 {copied() ? "copied ✓ — paste somewhere safe" : "copy words"}159 </button>160 </Show>161 </div>162163 {}164 <div class="peach-recovery-confirm">165 <Show when={!confirmed()}>166 <p class="peach-recovery-confirm-label">167 {countdown() > 0168 ? `type the highlighted words to confirm (${countdown()}s)`169 : "type the highlighted words to confirm"}170 </p>171 </Show>172 <Show when={confirmed()}>173 <p class="peach-hint peach-hint--success">✓ words verified</p>174 </Show>175176 <div class="peach-recovery-confirm-fields">177 <For each={CONFIRM_WORD_INDICES}>178 {(pos, i) => (179 <div class="peach-recovery-confirm-field">180 <span class="peach-recovery-confirm-num">181 word {pos + 1}182 </span>183 <input184 class="peach-recovery-word-input peach-recovery-confirm-input"185 type="text"186 autocomplete="off"187 autocorrect="off"188 autocapitalize="none"189 spellcheck={false}190 disabled={!canConfirm()}191 value={confirmWords()[i()]}192 onInput={(e) => {193 const updated = [...confirmWords()];194 updated[i()] = (e.target as HTMLInputElement).value;195 setConfirmWords(updated);196 if (confirmError()) setConfirmError("");197 }}198 onKeyDown={(e) => {199 if (e.key === "Enter" && canConfirm()) void handleConfirm();200 }}201 />202 </div>203 )}204 </For>205 </div>206207 <Show when={confirmError()}>208 <p class="peach-hint peach-hint-error">{confirmError()}</p>209 </Show>210211 <button212 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"213 disabled={!canConfirm() || confirmed()}214 onClick={() => void handleConfirm()}215 >216 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}217 </button>218 </div>219 </Show>220 </section>221 );222}223224function recoveryWordsText(words: string[]): string {225 const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n");226 return (227 "PEACH RECOVERY WORDS\n" +228 "====================\n\n" +229 "Anyone with these 12 words can recover your vault.\n" +230 "Store this somewhere only you can access.\n\n" +231 numbered + "\n"232 );233}234235236237238239function saveRecoveryWords(words: string[]): void {240 const blob = new Blob([recoveryWordsText(words)], { type: "text/plain" });241 const a = document.createElement("a");242 a.download = "peach-recovery-words.txt";243 a.href = URL.createObjectURL(blob);244 setTimeout(() => URL.revokeObjectURL(a.href), 1000);245 document.body.appendChild(a);246 a.click();247 a.remove();248}