// Signal-conditioned views in this file must use , never a // signal-conditioned early return — see Projects.md §4.1. import { createMemo, createSignal, Show } from "solid-js"; import { sendMessage } from "../App"; import { PeachWordmark } from "../components/PeachMark"; import { parseImportSource, type ImportParseResult, } from "../../shared/import"; import { decodePeachScript, isPeachScript, } from "../../crypto/peachScript"; import { parsePayload } from "../../crypto/peachPayload"; import { IMPORT_LIMITS } from "../../shared/import-limits"; import type { VaultEntry } from "../../shared/types"; type ImportSubview = "menu" | "peachscript" | "file"; type ImportMergeMode = "add" | "replace"; interface ImportProps { mode?: "existing" | "onboarding"; replacesExistingVault?: boolean; /** Skip the menu and open straight into a specific subview. */ initialView?: ImportSubview; onBack: () => void; onImport: () => void | Promise; } function OnboardingMasterPasswordFields(props: { idPrefix: string; copy: string; masterPlaceholder: string; password: string; confirm: string; onPasswordInput: (value: string) => void; onConfirmInput: (value: string) => void; }) { const [showPassword, setShowPassword] = createSignal(false); return ( <>

{props.copy}

props.onPasswordInput((event.target as HTMLInputElement).value)} /> props.onConfirmInput((event.target as HTMLInputElement).value)} /> 0 && props.password !== props.confirm}>

Passwords don't match

); } export function ImportView(props: ImportProps) { const [view, setView] = createSignal(props.initialView ?? "menu"); // When FileImport detects a .peach file, it stashes the contents here and // routes the view to "peachscript" so the existing decode flow handles it. const [peachScriptPrefill, setPeachScriptPrefill] = createSignal(""); const mode = () => props.mode ?? "existing"; const isOnboarding = () => mode() === "onboarding"; // When the caller deep-links into a subview, the back arrow exits the // ImportView entirely. Otherwise it returns to the import menu. const subviewBack = () => (props.initialView ? props.onBack() : setView("menu")); return (
setView(v)} /> { setPeachScriptPrefill(""); subviewBack(); }} onImport={props.onImport} /> { setPeachScriptPrefill(content); setView("peachscript"); }} />
); } // --------------------------------------------------------------------------- // Menu // --------------------------------------------------------------------------- function ImportMenu(props: { mode: "existing" | "onboarding"; replacesExistingVault?: boolean; onBack: () => void; onPick: (view: ImportSubview) => void; }) { const isOnboarding = props.mode === "onboarding"; return ( <> } >
Restoring now replaces the locked local vault on this device with a fresh one from your backup or export file.

peachscript

Paste a PeachScript backup from another Peach device. {props.mode === "existing" ? " We'll merge it into your vault." : ""}

backup file

A file from Bitwarden, Proton Pass, 1Password, or a browser CSV (Chrome, Safari, Edge).

); } // --------------------------------------------------------------------------- // PeachScript paste flow // --------------------------------------------------------------------------- function PeachScriptImport(props: { mode: "existing" | "onboarding"; replacesExistingVault?: boolean; /** Optional initial PeachScript content — set when the user loaded a * .peach file from the CSV/file card and was routed here. */ initialScript?: string; onBack: () => void; onImport: () => void | Promise; }) { const [scriptInput, setScriptInput] = createSignal(props.initialScript ?? ""); const [scriptPass, setScriptPass] = createSignal(""); const [masterPassword, setMasterPassword] = createSignal(""); const [masterConfirm, setMasterConfirm] = createSignal(""); const [working, setWorking] = createSignal(false); const [error, setError] = createSignal(null); const [result, setResult] = createSignal<{ entryCount: number } | null>(null); const scriptLooksValid = createMemo(() => isPeachScript(scriptInput())); const isOnboarding = props.mode === "onboarding"; const masterPasswordReady = createMemo( () => masterPassword().length >= 8 && masterPassword() === masterConfirm() ); async function handleImport() { setError(null); setResult(null); if (!scriptLooksValid()) { setError("Paste your PeachScript first."); return; } if (!scriptPass().trim()) { setError("Enter the backup password."); return; } setWorking(true); try { // PB-26-208: decodePeachScript skips whitespace internally — no // separate normalise pass. const bytes = decodePeachScript(scriptInput()); const payload = await parsePayload(bytes, scriptPass()); if (isOnboarding) { if (!masterPasswordReady()) { throw new Error("MASTER_PASSWORD_REQUIRED"); } const createRes = await sendMessage({ type: "CREATE_VAULT", payload: { password: masterPassword() }, }); if (!(createRes as { ok: boolean }).ok) throw new Error("CREATE_VAULT_FAILED"); } const restoreRes = await sendMessage({ type: "RESTORE_ENTRIES", payload: { entries: payload.entries, mode: "merge", settings: payload.settings, }, }); const restoreEnvelope = restoreRes as { ok: boolean; error?: string }; if (!restoreEnvelope.ok) { throw new Error(restoreEnvelope.error ?? "RESTORE_FAILED"); } if (payload.license?.key) { await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: payload.license.key }, }).catch(() => {}); } setResult({ entryCount: payload.entries.length }); await props.onImport(); } catch (err) { const code = err instanceof Error ? err.message : String(err); setError(getImportErrorCopy(code)); } finally { setWorking(false); } } return ( <>
Restoring now replaces the locked local vault on this device with a fresh one from your backup.

paste peachscript

{isOnboarding ? "Paste a PeachScript backup from another Peach device to restore your vault — or load a .peach file from disk." : "We'll merge the entries into your existing vault. You can paste PeachScript or load a .peach file."}