import { Match, Show, Switch, createMemo, createSignal } from "solid-js"; import { sendMessage } from "../App"; import { RuneBackground } from "../components/RuneBackground"; type AdoptPhase = "password" | "pair" | "sas" | "success" | "error"; export interface AdoptVaultViewProps { onCancel(): void; onDone(): void | Promise; startAdoption(args: { password: string; confirmSAS(sas: string): Promise; createVault(rootKeyHex: string): Promise<{ vaultId: string }>; }): Promise; } const MIN_LENGTH = 12; const MIN_ZXCVBN_SCORE = 2; let zxcvbnLoader: ((password: string) => { score: number }) | null = null; async function getZxcvbn() { if (zxcvbnLoader) return zxcvbnLoader; const [{ zxcvbn, zxcvbnOptions }, common, en] = await Promise.all([ import("@zxcvbn-ts/core"), import("@zxcvbn-ts/language-common"), import("@zxcvbn-ts/language-en"), ]); zxcvbnOptions.setOptions({ translations: en.translations, graphs: common.adjacencyGraphs, dictionary: { ...common.dictionary, ...en.dictionary }, }); zxcvbnLoader = (password: string) => zxcvbn(password); return zxcvbnLoader; } export function AdoptVaultView(props: AdoptVaultViewProps) { const [phase, setPhase] = createSignal("password"); const [password, setPassword] = createSignal(""); const [confirmPassword, setConfirmPassword] = createSignal(""); const [strengthScore, setStrengthScore] = createSignal(null); const [sas, setSas] = createSignal(""); const [sasResolve, setSasResolve] = createSignal<((ok: boolean) => void) | null>(null); const [error, setError] = createSignal(""); createMemo(() => { const value = password(); if (!value) { setStrengthScore(null); return; } getZxcvbn().then((zxcvbn) => { if (password() === value) setStrengthScore(zxcvbn(value).score); }).catch(() => { if (password() === value) setStrengthScore(value.length >= 16 ? 2 : 1); }); }); const canContinue = createMemo(() => password().length >= MIN_LENGTH && confirmPassword().length >= MIN_LENGTH && password() === confirmPassword() && strengthScore() !== null && strengthScore()! >= MIN_ZXCVBN_SCORE ); const createBlocker = createMemo(() => { if (!password()) return null; if (password().length < MIN_LENGTH) return `need ${MIN_LENGTH - password().length} more characters`; if (!confirmPassword()) return "confirm your password"; if (password() !== confirmPassword()) return "passwords don't match"; if (strengthScore() === null) return "checking strength..."; if (strengthScore()! < MIN_ZXCVBN_SCORE) return "password too weak - use a longer passphrase"; return null; }); async function beginPairing() { setPhase("pair"); setError(""); try { await props.startAdoption({ password: password(), confirmSAS: (code) => new Promise((resolve) => { setSas(code); setSasResolve(() => resolve); setPhase("sas"); }), createVault: async (rootKeyHex) => { const res = await sendMessage({ type: "CREATE_VAULT_FROM_ROOT_KEY", payload: { password: password(), rootKeyHex }, }) as { ok: boolean; vaultId?: string; error?: string }; if (!res.ok || !res.vaultId) throw new Error(res.error ?? "adoption_create_failed"); return { vaultId: res.vaultId }; }, }); setPhase("success"); } catch (err) { if (err instanceof Error && err.message === "sas_rejected") return; setError(err instanceof Error ? err.message : String(err)); setPhase("error"); } } function confirmSas(ok: boolean) { const resolve = sasResolve(); setSasResolve(null); resolve?.(ok); if (!ok) { setPhase("password"); setPassword(""); setConfirmPassword(""); setStrengthScore(null); } } return (

Set a password for this device.

This protects your vault on this device only. It can be different from your other devices.

setPassword((event.target as HTMLInputElement).value)} /> setConfirmPassword((event.target as HTMLInputElement).value)} />

{createBlocker()}

Pair with your other device.

Open Peach on the device that has your vault. Bring both devices close together.

Confirm this code matches on your other device.

{sas().replace(/(\d)(?=\d)/g, "$1 ")}

Joined your vault.

Your passwords will sync shortly.

Could not join your vault.

{error()}

); }