import { Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, } from "solid-js"; import { sendMessage } from "../App"; import { RuneBackground } from "../components/RuneBackground"; import { PairingQrCode } from "../components/PairingQrCode"; import { RELAY_DISCLOSURE } from "../components/SyncModeSelector"; type AdoptPhase = "password" | "pair"; export interface AdoptVaultViewProps { onCancel(): void; onDone(): void | 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 [receiverInvitation, setReceiverInvitation] = createSignal(""); const [receiverError, setReceiverError] = createSignal(""); onCleanup(() => { // Tell the background to release the relay room if we still hold it. void sendMessage({ type: "CANCEL_ADOPTION_SOURCE" }).catch(() => undefined); }); // When the user enters the pair phase, the empty extension becomes the // source of a "pairing request" QR. The populated phone scans it and the // phone is responsible for sending the vault. Until the phone-side reversal // lands, this is a UI-only change — the invitation is a valid rendezvous // that the existing relay dialer can reach, but no vault crosses the wire // yet. The SAS / vault-receive handlers are wired up to the same signals // the receiver flow always used, so the phone-side code can plug straight // in once it ships. createEffect(() => { if (phase() !== "pair") return; if (receiverInvitation() || receiverError()) return; void (async () => { const response = (await sendMessage({ type: "START_ADOPTION_RECEIVER", })) as { ok: boolean; data?: { invitation?: string }; error?: string }; if (response.ok && response.data?.invitation) { setReceiverInvitation(response.data.invitation); } else { setReceiverError(response.error ?? "adoption_receiver_failed"); } })(); }); 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; }); 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.

{/* The browser is empty — the phone (which holds the vault) is the one that scans. This QR is the initiation point; the phone propagates the vault to the extension once the connection completes. */}

Scan this code on the device that holds your vault. That device will send the vault to this extension once the codes match.

{RELAY_DISCLOSURE}

Preparing secure pairing room…

} >

{receiverError()}

} >
); }