12345import { 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}153154155156157158159160161162163164165166167168169