// 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, normalisePeachScript, } from "../../crypto/peachScript"; import { parsePayload } from "../../crypto/peachPayload"; import type { VaultEntry } from "../../shared/types"; import { CJKImportView } from "./CJKImport"; interface ImportProps { mode?: "existing" | "onboarding"; replacesExistingVault?: boolean; onBack: () => void; onImport: () => void | Promise; } type ImportMergeMode = "add" | "replace"; export function ImportView(props: ImportProps) { const [showCjkImport, setShowCjkImport] = createSignal(false); return ( setShowCjkImport(true)} /> } > setShowCjkImport(false)} onDone={async () => { setShowCjkImport(false); await props.onImport(); }} /> ); } function MainImportView(props: ImportProps & { onOpenCjk: () => void }) { const mode = () => props.mode ?? "existing"; // PeachScript paste flow const [scriptInput, setScriptInput] = createSignal(""); const [scriptPass, setScriptPass] = createSignal(""); const [scriptWorking, setScriptWorking] = createSignal(false); const [scriptError, setScriptError] = createSignal(null); const [scriptResult, setScriptResult] = createSignal<{ entryCount: number } | null>(null); // File flow (existing format detection) const [preview, setPreview] = createSignal(null); const [rawImport, setRawImport] = createSignal(null); const [backupPassword, setBackupPassword] = createSignal(""); const [masterPassword, setMasterPassword] = createSignal(""); const [masterConfirm, setMasterConfirm] = createSignal(""); const [importing, setImporting] = createSignal(false); const [error, setError] = createSignal(null); const [mergeMode, setMergeMode] = createSignal("add"); const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false); const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null); const [duplicateWarning, setDuplicateWarning] = createSignal(0); const [existingCount, setExistingCount] = createSignal(0); const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false); const needsMasterPassword = createMemo(() => mode() === "onboarding"); const masterPasswordReady = createMemo( () => masterPassword().length >= 8 && masterPassword() === masterConfirm() ); const scriptLooksValid = createMemo(() => isPeachScript(scriptInput())); async function refreshExistingCount() { if (mode() !== "existing") return; const res = await sendMessage({ type: "GET_ENTRIES" }); const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? []; setExistingCount(list.length); } void refreshExistingCount(); // --------------------------------------------------------------------- // PeachScript flow // --------------------------------------------------------------------- async function handlePeachScriptImport() { setScriptError(null); setScriptResult(null); if (!scriptLooksValid()) { setScriptError("Paste your PeachScript first."); return; } if (!scriptPass().trim()) { setScriptError("Enter the backup password."); return; } setScriptWorking(true); try { const bytes = decodePeachScript(normalisePeachScript(scriptInput())); const payload = await parsePayload(bytes, scriptPass()); // Onboarding: create a fresh vault first. if (mode() === "onboarding") { 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, }, }); if (!(restoreRes as { ok: boolean }).ok) throw new Error("RESTORE_FAILED"); if (payload.license?.key) { await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: payload.license.key }, }).catch(() => {}); } setScriptResult({ entryCount: payload.entries.length }); await props.onImport(); } catch (err) { const code = err instanceof Error ? err.message : String(err); setScriptError(getImportErrorCopy(code)); } finally { setScriptWorking(false); } } // --------------------------------------------------------------------- // File flow (Bitwarden / 1Password / etc.) // --------------------------------------------------------------------- async function parseSource(text: string, password?: string) { try { const result = await parseImportSource(text, password); setPreview(result); setError(null); if (result.format === "unknown") { setError( "We couldn't recognise this file format. Peach supports: Peach JSON backups, Bitwarden JSON/CSV, Proton Pass JSON/CSV, 1Password JSON, and generic browser CSV exports." ); } return result; } catch (err) { const message = err instanceof Error ? err.message : String(err); setPreview(null); setError(getImportErrorCopy(message)); return null; } } async function handleFileSelect(event: Event) { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; setPreview(null); setRawImport(null); setBackupPassword(""); setError(null); setAwaitingReplaceConfirm(false); const text = await file.text(); setRawImport(text); await parseSource(text); } async function handleUnlockBackup() { const raw = rawImport(); if (!raw || !backupPassword().trim()) return; await parseSource(raw, backupPassword()); } async function countDuplicates(incoming: ImportParseResult["entries"]): Promise { if (incoming.length === 0) return 0; const res = await sendMessage({ type: "GET_ENTRIES" }); const existing = ((res as { ok: boolean; data?: VaultEntry[] }).data ?? []).filter((e) => !e.deletedAt); const keyOf = (title?: string, url?: string, username?: string) => `${(title ?? "").toLowerCase()}|${(url ?? "").toLowerCase()}|${(username ?? "").toLowerCase()}`; const existingKeys = new Set(existing.map((e) => keyOf(e.title, e.url, e.username))); let dupes = 0; for (const entry of incoming) { const u = "username" in entry ? entry.username : undefined; const url = "url" in entry ? entry.url : undefined; if (existingKeys.has(keyOf(entry.title, url, u))) dupes += 1; } return dupes; } async function handleImport() { const currentPreview = preview(); if (!currentPreview) return; if (needsBackupPassword() && !backupPassword().trim()) { setError("Enter the backup password used for this export."); return; } if (needsMasterPassword()) { if (masterPassword().length < 8) { setError("Master password must be at least 8 characters."); return; } if (!masterPasswordReady()) { setError("Master passwords do not match."); return; } } if (mode() === "existing" && mergeMode() === "replace" && !awaitingReplaceConfirm()) { setAwaitingReplaceConfirm(true); return; } setImporting(true); setError(null); setDuplicateWarning(0); setImportProgress(null); try { if (currentPreview.entries.length === 0) { throw new Error("NO_ENTRIES_IMPORTED"); } if (needsMasterPassword()) { const createResponse = await sendMessage({ type: "CREATE_VAULT", payload: { password: masterPassword() }, }); if (!(createResponse as { ok: boolean }).ok) { throw new Error("CREATE_VAULT_FAILED"); } } if (mode() === "existing" && mergeMode() === "replace") { const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" }); if (!(clearRes as { ok: boolean }).ok) { throw new Error("CLEAR_VAULT_FAILED"); } } let dupes = 0; if (mode() === "existing" && mergeMode() === "add") { dupes = await countDuplicates(currentPreview.entries); } let imported = 0; setImportProgress({ current: 0, total: currentPreview.entries.length }); const batchRes = await sendMessage({ type: "BATCH_ADD_ENTRIES", payload: { entries: currentPreview.entries }, }); if ((batchRes as { ok: boolean }).ok) { imported = ((batchRes as { ok: boolean; data?: VaultEntry[] }).data ?? []).length; setImportProgress({ current: imported, total: currentPreview.entries.length }); } else { for (const entry of currentPreview.entries) { const response = await sendMessage({ type: "ADD_ENTRY", payload: entry, }); if ((response as { ok: boolean }).ok) imported += 1; setImportProgress({ current: imported, total: currentPreview.entries.length }); } } if (imported === 0) { throw new Error("NO_ENTRIES_IMPORTED"); } if (currentPreview.licenseKey) { await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: currentPreview.licenseKey } }).catch(() => {}); } setDuplicateWarning(dupes); await props.onImport(); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(getImportErrorCopy(message)); setImporting(false); setImportProgress(null); return; } setImporting(false); setImportProgress(null); } return (
import
} >
Restoring now replaces the locked local vault on this device with a fresh one from your backup or export file.
{/* PeachScript paste — primary path */}

paste peachscript

{mode() === "onboarding" ? "Paste a PeachScript backup from another Peach device to restore your vault." : "Paste a PeachScript backup from another Peach device. We'll merge it into your vault."}