onboarding-adopt-path.tsx

6.6 KB · raw

1// DRAFT DESTINATION: src/popup/views/AdoptVaultView.tsx plus small hunks in2// src/popup/views/Onboarding.tsx, src/popup/views/Lock.tsx, and src/popup/App.tsx.3// Phase 5: "I already have Peach" receiver path.45import { Match, Show, Switch, createMemo, createSignal } from "solid-js";6import { sendMessage } from "../App";7import { RuneBackground } from "../components/RuneBackground";89type AdoptPhase = "password" | "pair" | "sas" | "success" | "error";1011interface AdoptVaultViewProps {12  onCancel(): void;13  onDone(): void | Promise<void>;14  startAdoption(args: {15    password: string;16    confirmSAS(sas: string): Promise<boolean>;17    createVault(rootKeyHex: string): Promise<{ vaultId: string }>;18  }): Promise<void>;19}2021const MIN_LENGTH = 12;2223export function AdoptVaultView(props: AdoptVaultViewProps) {24  const [phase, setPhase] = createSignal<AdoptPhase>("password");25  const [password, setPassword] = createSignal("");26  const [confirmPassword, setConfirmPassword] = createSignal("");27  const [sas, setSas] = createSignal("");28  const [sasResolve, setSasResolve] = createSignal<((ok: boolean) => void) | null>(null);29  const [error, setError] = createSignal("");3031  const canContinue = createMemo(() =>32    password().length >= MIN_LENGTH &&33    confirmPassword().length >= MIN_LENGTH &&34    password() === confirmPassword()35  );3637  function blocker(): string | null {38    if (!password()) return null;39    if (password().length < MIN_LENGTH) return `need ${MIN_LENGTH - password().length} more characters`;40    if (!confirmPassword()) return "confirm your password";41    if (password() !== confirmPassword()) return "passwords don't match";42    return null;43  }4445  async function beginPairing() {46    setPhase("pair");47    setError("");48    try {49      await props.startAdoption({50        password: password(),51        confirmSAS: (code) => new Promise<boolean>((resolve) => {52          setSas(code);53          setSasResolve(() => resolve);54          setPhase("sas");55        }),56        createVault: async (rootKeyHex) => {57          const res = await sendMessage({58            type: "CREATE_VAULT_FROM_ROOT_KEY",59            payload: { password: password(), rootKeyHex },60          }) as { ok: boolean; vaultId?: string; error?: string };61          if (!res.ok || !res.vaultId) throw new Error(res.error ?? "adoption_create_failed");62          return { vaultId: res.vaultId };63        },64      });65      setPhase("success");66    } catch (err) {67      setError(err instanceof Error ? err.message : String(err));68      setPhase("error");69    }70  }7172  function confirmSas(ok: boolean) {73    const resolve = sasResolve();74    setSasResolve(null);75    resolve?.(ok);76    if (!ok) {77      setPhase("password");78      setPassword("");79      setConfirmPassword("");80    }81  }8283  return (84    <section class="peach-screen peach-theme peach-adopt-vault">85      <RuneBackground opacity={0.07} />86      <Switch>87        <Match when={phase() === "password"}>88          <h1>Set a password for this device.</h1>89          <p>This protects your vault on this device only. It can be different from your other devices.</p>90          <label class="peach-label" for="adopt-password">password</label>91          <input92            id="adopt-password"93            class="peach-underline-input"94            type="password"95            autocomplete="new-password"96            value={password()}97            onInput={(event) => setPassword((event.target as HTMLInputElement).value)}98          />99          <label class="peach-label" for="adopt-confirm-password">confirm password</label>100          <input101            id="adopt-confirm-password"102            class="peach-underline-input"103            type="password"104            autocomplete="new-password"105            value={confirmPassword()}106            onInput={(event) => setConfirmPassword((event.target as HTMLInputElement).value)}107          />108          <Show when={blocker()}>109            <p class="peach-hint">{blocker()}</p>110          </Show>111          <button class="peach-btn peach-btn--accent peach-btn--md" disabled={!canContinue()} onClick={() => setPhase("pair")}>112            Continue ->113          </button>114          <button class="peach-btn peach-btn--ghost peach-btn--md" onClick={props.onCancel}>Cancel</button>115        </Match>116117        <Match when={phase() === "pair"}>118          <div class="peach-adopt-birds" aria-hidden="true" />119          <h1>Pair with your other device.</h1>120          <p>Open Peach on the device that has your vault. Bring both devices close together.</p>121          <button class="peach-btn peach-btn--accent peach-btn--md" onClick={() => void beginPairing()}>122            Pair a device ->123          </button>124          <button class="peach-btn peach-btn--ghost peach-btn--md" onClick={props.onCancel}>Cancel</button>125        </Match>126127        <Match when={phase() === "sas"}>128          <h1>Confirm this code matches on your other device:</h1>129          <div class="peach-adopt-sas">{sas().replace(/(\d)(?=\d)/g, "$1 ")}</div>130          <button class="peach-btn peach-btn--accent peach-btn--md" onClick={() => confirmSas(true)}>Confirm</button>131          <button class="peach-btn peach-btn--ghost peach-btn--md" onClick={() => confirmSas(false)}>Cancel</button>132        </Match>133134        <Match when={phase() === "success"}>135          <div class="peach-adopt-birds" aria-hidden="true" />136          <h1>Joined your vault.</h1>137          <p>Your passwords will sync shortly.</p>138          <button class="peach-btn peach-btn--accent peach-btn--md" onClick={() => void props.onDone()}>139            Open Peach ->140          </button>141        </Match>142143        <Match when={phase() === "error"}>144          <h1>Couldn't join your vault.</h1>145          <p class="peach-hint peach-hint-error">{error()}</p>146          <button class="peach-btn peach-btn--accent peach-btn--md" onClick={() => setPhase("password")}>Try again</button>147          <button class="peach-btn peach-btn--ghost peach-btn--md" onClick={props.onCancel}>Cancel</button>148        </Match>149      </Switch>150    </section>151  );152}153154// DRAFT HUNK: src/popup/views/Onboarding.tsx155// - Extend props with `onAdopt?: () => void | Promise<void>`.156// - On the final card, render a third action button:157//     <button class="peach-onboarding-adopt" onClick={props.onAdopt}>158//       I already have Peach ->159//     </button>160//   Keep Create vault and Restore from backup visible in LockView as they are161//   today; this is an additional path, not a replacement.162163// DRAFT HUNK: src/popup/App.tsx164// - Add `| "adopting"` to `View`.165// - Lazy-load `AdoptVaultView`.166// - Pass `onAdopt={() => setView("adopting")}` into Onboarding.167// - Render AdoptVaultView while hasVault() is false.168// - On success, refresh HAS_VAULT, mark onboarding complete, call handleUnlock.169