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 await new Promise((r) => setTimeout(r, 600));74 props.onDone();75 }7677 const canConfirm = createMemo(() => countdown() <= 0 && !confirmed());7879 return (80 <section class="peach-screen peach-theme lock-screen lock-screen--recovery lock-screen--recovery-display">81 <RecoveryRuneBackground />8283 <header class="peach-screen-header">84 <div class="peach-wordmark-stack">85 <PeachWordmark class="peach-wordmark-wrap" />86 </div>87 </header>8889 <div class="peach-recovery-header">90 <h1 class="peach-onboarding-headline">save your recovery words</h1>91 <p class="peach-hint peach-hint--warning">92 write these down — you will never see them again93 </p>94 </div>9596 <Show when={loading()}>97 <p class="peach-hint">loading...</p>98 </Show>99100 <Show when={error()}>101 <p class="peach-hint peach-hint-error">{error()}</p>102 </Show>103104 <Show when={!loading() && !error()}>105 <div class="peach-recovery-words-display">106 <div class="peach-recovery-words-label">your 12 recovery words</div>107 <ol class="peach-recovery-word-list" start={1}>108 <For each={words()}>109 {(word, i) => (110 <li class="peach-recovery-word-item">111 <span class="peach-recovery-word-num">{toRoman(i() + 1)}</span>112 <span class="peach-recovery-word-text">{word}</span>113 </li>114 )}115 </For>116 </ol>117 </div>118119 {}120 <div class="peach-recovery-confirm">121 <Show when={!confirmed()}>122 <p class="peach-recovery-confirm-label">123 {countdown() > 0124 ? `type the highlighted words to confirm (${countdown()}s)`125 : "type the highlighted words to confirm"}126 </p>127 </Show>128 <Show when={confirmed()}>129 <p class="peach-hint peach-hint--success">✓ words verified</p>130 </Show>131132 <div class="peach-recovery-confirm-fields">133 <For each={CONFIRM_WORD_INDICES}>134 {(pos, i) => (135 <div class="peach-recovery-confirm-field">136 <span class="peach-recovery-confirm-num">137 word {pos + 1}138 </span>139 <input140 class="peach-recovery-word-input peach-recovery-confirm-input"141 type="text"142 autocomplete="off"143 autocorrect="off"144 autocapitalize="none"145 spellcheck={false}146 disabled={!canConfirm()}147 value={confirmWords()[i()]}148 onInput={(e) => {149 const updated = [...confirmWords()];150 updated[i()] = (e.target as HTMLInputElement).value;151 setConfirmWords(updated);152 if (confirmError()) setConfirmError("");153 }}154 onKeyDown={(e) => {155 if (e.key === "Enter" && canConfirm()) void handleConfirm();156 }}157 />158 </div>159 )}160 </For>161 </div>162163 <Show when={confirmError()}>164 <p class="peach-hint peach-hint-error">{confirmError()}</p>165 </Show>166167 <button168 class="peach-btn peach-btn--accent peach-btn--md peach-recovery-confirm-btn"169 disabled={!canConfirm() || confirmed()}170 onClick={() => void handleConfirm()}171 >172 {confirmed() ? "verified ✓" : countdown() > 0 ? `wait ${countdown()}s` : "i wrote them down"}173 </button>174 </div>175 </Show>176 </section>177 );178}179180function toRoman(n: number): string {181 const map: [number, string][] = [182 [1000, "M"], [900, "CM"], [500, "D"], [400, "CD"],183 [100, "C"], [90, "XC"], [50, "L"], [40, "XL"],184 [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"],185 ];186 let out = "";187 for (const [value, numeral] of map) {188 while (n >= value) {189 out += numeral;190 n -= value;191 }192 }193 return out;194}