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 let countdownTimer: ReturnType<typeof setInterval> | null = null;2425 onMount(async () => {26 27 const res = (await sendMessage({28 type: "GET_RECOVERY_WORDS",29 })) as { ok: boolean; data?: string[]; error?: string };3031 if (res.ok && res.data) {32 setWords(res.data);33 } else {34 setError("could not retrieve recovery words");35 }36 setLoading(false);3738 39 let remaining = 5;40 countdownTimer = setInterval(() => {41 remaining -= 1;42 setCountdown(remaining);43 if (remaining <= 0 && countdownTimer) {44 clearInterval(countdownTimer);45 countdownTimer = null;46 }47 }, 1000);48 });4950 onCleanup(() => {51 if (countdownTimer) clearInterval(countdownTimer);52 });5354 async function handleConfirm() {55 const confirmed = confirmWords().map((w) => w.toLowerCase().trim());56 if (confirmed.some((w) => !w)) {57 setConfirmError("all three words are required");58 return;59 }6061 62 const stored = words();63 for (let i = 0; i < CONFIRM_WORD_INDICES.length; i++) {64 const pos = CONFIRM_WORD_INDICES[i];65 if (confirmed[i] !== stored[pos]) {66 setConfirmError(`word ${pos + 1} is incorrect`);67 return;68 }69 }7071 setConfirmed(true);72 setConfirmError("");73 74 75 76 77 try {78 await chrome.storage.local?.remove("peach_recovery_pending");79 } catch { }80 81 await new Promise((r) => setTimeout(r, 600));82 props.onDone();83 }8485 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());8687 return (88 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">89 <RecoveryRuneBackground />9091 <header class="peach-screen-header">92 <div class="peach-wordmark-stack">93 <PeachWordmark class="peach-wordmark-wrap" />94 </div>95 </header>9697 <div class="peach-recovery-header">98 <h1 class="peach-onboarding-headline">save your recovery words</h1>99 <p class="peach-hint peach-hint--warning">100 write these down — you will never see them again101 </p>102 </div>103104 <Show when={loading()}>105 <p class="peach-hint">loading...</p>106 </Show>107108 <Show when={error()}>109 <p class="peach-hint peach-hint-error">{error()}</p>110 </Show>111112 <Show when={!loading() && !error()}>113 <div class="peach-recovery-words-display">114 <div class="peach-recovery-words-label">your 12 recovery words</div>115 <ol class="peach-recovery-word-list" start={1}>116 <For each={words()}>117 {(word, i) => (118 <li class="peach-recovery-word-item">119 <span class="peach-recovery-word-num">{i() + 1}</span>120 <span class="peach-recovery-word-text">{word}</span>121 </li>122 )}123 </For>124 </ol>125 <button126 type="button"127 class="peach-recovery-download-btn"128 onClick={() => void saveRecoveryWords(words())}129 >130 {isNativePlatform() ? "share recovery words" : "download as .txt"}131 </button>132 </div>133134 {}135 <div class="peach-recovery-confirm">136 <Show when={!confirmed()}>137 <p class="peach-recovery-confirm-label">138 {countdown() > 0139 ? `type the highlighted words to confirm (${countdown()}s)`140 : "type the highlighted words to confirm"}141 </p>142 </Show>143 <Show when={confirmed()}>144 <p class="peach-hint peach-hint--success">✓ words verified</p>145 </Show>146147 <div class="peach-recovery-confirm-fields">148 <For each={CONFIRM_WORD_INDICES}>149 {(pos, i) => (150 <div class="peach-recovery-confirm-field">151 <span class="peach-recovery-confirm-num">152 word {pos + 1}153 </span>154 <input155 class="peach-recovery-word-input peach-recovery-confirm-input"156 type="text"157 autocomplete="off"158 autocorrect="off"159 autocapitalize="none"160 spellcheck={false}161 disabled={!canConfirm()}162 value={confirmWords()[i()]}163 onInput={(e) => {164 const updated = [...confirmWords()];165 updated[i()] = (e.target as HTMLInputElement).value;166 setConfirmWords(updated);167 if (confirmError()) setConfirmError("");168 }}169 onKeyDown={(e) => {170 if (e.key === "Enter" && canConfirm()) void handleConfirm();171 }}172 />173 </div>174 )}175 </For>176 </div>177178 <Show when={confirmError()}>179 <p class="peach-hint peach-hint-error">{confirmError()}</p>180 </Show>181182 <button183 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"184 disabled={!canConfirm() || confirmed()}185 onClick={() => void handleConfirm()}186 >187 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}188 </button>189 </div>190 </Show>191 </section>192 );193}194195async function saveRecoveryWords(words: string[]): Promise<void> {196 const numbered = words.map((w, i) => `${String(i + 1).padStart(2, " ")}. ${w}`).join("\n");197 const body =198 "PEACH RECOVERY WORDS\n" +199 "====================\n\n" +200 "Anyone with these 12 words can recover your vault.\n" +201 "Store this file somewhere only you can access.\n\n" +202 numbered + "\n";203204 205 206 207 208 if (isNativePlatform() && typeof navigator !== "undefined" && typeof navigator.share === "function") {209 try {210 await navigator.share({211 title: "Peach recovery words",212 text: body,213 });214 return;215 } catch (e) {216 217 if (e instanceof Error && /abort/i.test(e.message)) return;218 }219 }220221 const blob = new Blob([body], { type: "text/plain" });222 const url = URL.createObjectURL(blob);223 const a = document.createElement("a");224 a.href = url;225 a.download = "peach-recovery-words.txt";226 document.body.appendChild(a);227 a.click();228 a.remove();229 URL.revokeObjectURL(url);230}