RecoveryWordDisplay.tsx

7.3 KB · raw

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}910// After showing the words, user must type the 3 words at these positions11const CONFIRM_WORD_INDICES = [2, 6, 10]; // 0-indexed → positions 3, 7, 111213export 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  // 5-second countdown before confirmation input is enabled21  const [countdown, setCountdown] = createSignal(5);22  let countdownTimer: ReturnType<typeof setInterval> | null = null;2324  onMount(async () => {25    // Fetch the recovery words from the background (vault must be unlocked).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    // Start 5-second countdown38    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    // Verify against stored words61    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    // Clear the recovery-pending flag so lock-on-close / auto-lock resume73    // their normal behavior and future unlocks no longer re-route here.74    // Once the user has confirmed the words, losing the in-memory key is75    // no longer a data-loss event.76    try {77      await chrome.storage.local?.remove("peach_recovery_pending");78    } catch { /* best-effort */ }79    // Small pause so user sees the success state, then proceed.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        {/* Confirmation step */}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}