RecoveryWordDisplay.tsx

6.5 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    // Small pause so user sees the success state, then proceed.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        {/* Confirmation step */}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}