123import { createMemo, createSignal, Show } from "solid-js";45import { sendMessage } from "../App";6import { PeachWordmark } from "../components/PeachMark";7import {8 parseImportSource,9 type ImportParseResult,10} from "../../shared/import";11import {12 decodePeachScript,13 isPeachScript,14 normalisePeachScript,15} from "../../crypto/peachScript";16import { parsePayload } from "../../crypto/peachPayload";17import type { VaultEntry } from "../../shared/types";1819type ImportSubview = "menu" | "peachscript" | "file";20type ImportMergeMode = "add" | "replace";2122interface ImportProps {23 mode?: "existing" | "onboarding";24 replacesExistingVault?: boolean;25 26 initialView?: ImportSubview;27 onBack: () => void;28 onImport: () => void | Promise<void>;29}3031export function ImportView(props: ImportProps) {32 const [view, setView] = createSignal<ImportSubview>(props.initialView ?? "menu");33 34 35 const [peachScriptPrefill, setPeachScriptPrefill] = createSignal<string>("");36 const mode = () => props.mode ?? "existing";37 const isOnboarding = () => mode() === "onboarding";38 39 40 const subviewBack = () => (props.initialView ? props.onBack() : setView("menu"));4142 return (43 <div class={isOnboarding() ? "peach-lock-view" : "peach-overlay-view ov-enter-right"} style="display:flex">44 <Show when={view() === "menu"}>45 <ImportMenu46 mode={mode()}47 replacesExistingVault={props.replacesExistingVault}48 onBack={props.onBack}49 onPick={(v) => setView(v)}50 />51 </Show>52 <Show when={view() === "peachscript"}>53 <PeachScriptImport54 mode={mode()}55 replacesExistingVault={props.replacesExistingVault}56 initialScript={peachScriptPrefill()}57 onBack={() => {58 setPeachScriptPrefill("");59 subviewBack();60 }}61 onImport={props.onImport}62 />63 </Show>64 <Show when={view() === "file"}>65 <FileImport66 mode={mode()}67 replacesExistingVault={props.replacesExistingVault}68 onBack={subviewBack}69 onImport={props.onImport}70 onLoadPeachFile={(content) => {71 setPeachScriptPrefill(content);72 setView("peachscript");73 }}74 />75 </Show>76 </div>77 );78}798081828384function ImportMenu(props: {85 mode: "existing" | "onboarding";86 replacesExistingVault?: boolean;87 onBack: () => void;88 onPick: (view: ImportSubview) => void;89}) {90 const isOnboarding = props.mode === "onboarding";91 return (92 <>93 <Show94 when={isOnboarding}95 fallback={<OverlayHeader title="import" onBack={props.onBack} />}96 >97 <div class="peach-lock-view-header">98 <PeachWordmark class="peach-wordmark-wrap" />99 <button class="peach-lock-restore-link" onClick={props.onBack}>100 ‹ back101 </button>102 </div>103 </Show>104 <div class="peach-settings-body peach-export-menu">105 <Show when={isOnboarding && props.replacesExistingVault}>106 <div class="peach-settings-group">107 <div class="peach-restore-warning">108 Restoring now replaces the locked local vault on this device with109 a fresh one from your backup or export file.110 </div>111 </div>112 </Show>113114 <div class="peach-settings-group peach-export-card peach-export-card--primary">115 <p class="peach-settings-label">peachscript</p>116 <p class="peach-export-card-copy">117 Paste a PeachScript backup from another Peach device.118 {props.mode === "existing" ? " We'll merge it into your vault." : ""}119 </p>120 <button121 class="peach-btn peach-btn--subtle peach-btn--sm"122 onClick={() => props.onPick("peachscript")}123 >124 Paste PeachScript →125 </button>126 </div>127128 <div class="peach-settings-group peach-export-card">129 <p class="peach-settings-label">backup file</p>130 <p class="peach-export-card-copy">131 A Peach JSON export, or a file from Bitwarden, Proton Pass,132 1Password, or a browser CSV (Chrome, Safari, Edge).133 </p>134 <button135 class="peach-btn peach-btn--subtle peach-btn--sm"136 onClick={() => props.onPick("file")}137 >138 Choose file →139 </button>140 </div>141 </div>142 </>143 );144}145146147148149150function PeachScriptImport(props: {151 mode: "existing" | "onboarding";152 replacesExistingVault?: boolean;153 154155 initialScript?: string;156 onBack: () => void;157 onImport: () => void | Promise<void>;158}) {159 const [scriptInput, setScriptInput] = createSignal(props.initialScript ?? "");160 const [scriptPass, setScriptPass] = createSignal("");161 const [masterPassword, setMasterPassword] = createSignal("");162 const [masterConfirm, setMasterConfirm] = createSignal("");163 const [working, setWorking] = createSignal(false);164 const [error, setError] = createSignal<string | null>(null);165 const [result, setResult] = createSignal<{ entryCount: number } | null>(null);166167 const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));168 const isOnboarding = props.mode === "onboarding";169 const masterPasswordReady = createMemo(170 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()171 );172173 async function handleImport() {174 setError(null);175 setResult(null);176 if (!scriptLooksValid()) {177 setError("Paste your PeachScript first.");178 return;179 }180 if (!scriptPass().trim()) {181 setError("Enter the backup password.");182 return;183 }184 setWorking(true);185 try {186 const bytes = decodePeachScript(normalisePeachScript(scriptInput()));187 const payload = await parsePayload(bytes, scriptPass());188189 if (isOnboarding) {190 if (!masterPasswordReady()) {191 throw new Error("MASTER_PASSWORD_REQUIRED");192 }193 const createRes = await sendMessage({194 type: "CREATE_VAULT",195 payload: { password: masterPassword() },196 });197 if (!(createRes as { ok: boolean }).ok) throw new Error("CREATE_VAULT_FAILED");198 }199200 const restoreRes = await sendMessage({201 type: "RESTORE_ENTRIES",202 payload: {203 entries: payload.entries,204 mode: "merge",205 settings: payload.settings,206 },207 });208 const restoreEnvelope = restoreRes as { ok: boolean; error?: string };209 if (!restoreEnvelope.ok) {210 throw new Error(restoreEnvelope.error ?? "RESTORE_FAILED");211 }212213 if (payload.license?.key) {214 await sendMessage({215 type: "ACTIVATE_LICENSE",216 payload: { key: payload.license.key },217 }).catch(() => {});218 }219220 setResult({ entryCount: payload.entries.length });221 await props.onImport();222 } catch (err) {223 const code = err instanceof Error ? err.message : String(err);224 setError(getImportErrorCopy(code));225 } finally {226 setWorking(false);227 }228 }229230 return (231 <>232 <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />233 <div class="peach-settings-body">234 <Show when={isOnboarding && props.replacesExistingVault}>235 <div class="peach-settings-group">236 <div class="peach-restore-warning">237 Restoring now replaces the locked local vault on this device with238 a fresh one from your backup.239 </div>240 </div>241 </Show>242 <div class="peach-settings-group peach-export-card peach-export-card--primary peach-export-pass-group">243 <p class="peach-settings-label">paste peachscript</p>244 <p class="peach-export-card-copy">245 {isOnboarding246 ? "Paste a PeachScript backup from another Peach device to restore your vault — or load a .peach file from disk."247 : "We'll merge the entries into your existing vault. You can paste PeachScript or load a .peach file."}248 </p>249 <textarea250 class="peach-script-input"251 placeholder="paste PeachScript here…"252 value={scriptInput()}253 onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}254 />255 <label256 class="peach-btn peach-btn--ghost peach-btn--sm"257 style="display:inline-flex;align-items:center;justify-content:center;cursor:pointer;"258 >259 Load .peach file260 <input261 type="file"262 accept=".peach,application/x-peach,text/plain"263 style="display:none;"264 onChange={async (event) => {265 const input = event.currentTarget;266 const file = input.files?.[0];267 if (!file) return;268 try {269 const text = await file.text();270 setScriptInput(text);271 } catch (err) {272 setError(err instanceof Error ? err.message : "Couldn't read file.");273 } finally {274 input.value = "";275 }276 }}277 />278 </label>279 <div class="peach-settings-stack peach-export-password-stack">280 <input281 class="peach-settings-input"282 type="password"283 placeholder="PeachScript password"284 value={scriptPass()}285 onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}286 />287 </div>288 <Show when={isOnboarding}>289 <p class="peach-settings-label" style="margin-top:4px">new master password</p>290 <p class="peach-export-card-copy">291 This password unlocks Peach on this device after the restore.292 </p>293 <div class="peach-settings-stack peach-export-password-stack">294 <input295 class="peach-settings-input"296 type="password"297 placeholder={`master password (min 8 chars)`}298 value={masterPassword()}299 onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}300 />301 <input302 class="peach-settings-input"303 type="password"304 placeholder="confirm master password"305 value={masterConfirm()}306 onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}307 />308 <Show when={masterConfirm().length > 0 && masterPassword() !== masterConfirm()}>309 <p class="peach-export-note" style="color:var(--peach-t2)">310 Passwords don't match311 </p>312 </Show>313 </div>314 </Show>315 <button316 class="peach-btn peach-btn--subtle peach-btn--sm"317 onClick={() => void handleImport()}318 disabled={319 working() ||320 !scriptLooksValid() ||321 !scriptPass().trim() ||322 (isOnboarding && !masterPasswordReady())323 }324 >325 {working() ? "decoding…" : "Restore from PeachScript"}326 </button>327 <Show when={error()}>328 <div class="peach-restore-error">{error()}</div>329 </Show>330 <Show when={result()}>331 <div class="peach-export-success">332 <span class="peach-export-success-check">✓</span>333 <span>334 Restored {result()!.entryCount} entr335 {result()!.entryCount === 1 ? "y" : "ies"}.336 </span>337 </div>338 </Show>339 </div>340 </div>341 </>342 );343}344345346347348349function FileImport(props: {350 mode: "existing" | "onboarding";351 replacesExistingVault?: boolean;352 onBack: () => void;353 onImport: () => void | Promise<void>;354 355356 onLoadPeachFile?: (content: string) => void;357}) {358 const isOnboarding = props.mode === "onboarding";359 const needsMasterPassword = () => isOnboarding;360361 const [preview, setPreview] = createSignal<ImportParseResult | null>(null);362 const [rawImport, setRawImport] = createSignal<string | null>(null);363 const [backupPassword, setBackupPassword] = createSignal("");364 const [masterPassword, setMasterPassword] = createSignal("");365 const [masterConfirm, setMasterConfirm] = createSignal("");366 const [importing, setImporting] = createSignal(false);367 const [error, setError] = createSignal<string | null>(null);368 const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");369 const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);370 const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);371 const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);372 const [existingCount, setExistingCount] = createSignal<number>(0);373374 const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);375 const masterPasswordReady = createMemo(376 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()377 );378379 async function refreshExistingCount() {380 if (isOnboarding) return;381 const res = await sendMessage({ type: "GET_ENTRIES" });382 const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];383 setExistingCount(list.length);384 }385 void refreshExistingCount();386387 async function parseSource(text: string, password?: string) {388 try {389 const result = await parseImportSource(text, password);390 setPreview(result);391 setError(null);392 if (result.format === "unknown") {393 setError(394 "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."395 );396 }397 return result;398 } catch (err) {399 const message = err instanceof Error ? err.message : String(err);400 setPreview(null);401 setError(getImportErrorCopy(message));402 return null;403 }404 }405406 async function handleFileSelect(event: Event) {407 const input = event.target as HTMLInputElement;408 const file = input.files?.[0];409 if (!file) return;410411 setPreview(null);412 setRawImport(null);413 setBackupPassword("");414 setError(null);415 setAwaitingReplaceConfirm(false);416417 const text = await file.text();418419 420 421 422 423 424 const looksLikePeach =425 file.name.toLowerCase().endsWith(".peach") || isPeachScript(text);426 if (looksLikePeach) {427 if (props.onLoadPeachFile) {428 props.onLoadPeachFile(text);429 input.value = "";430 return;431 }432 setError("This is a .peach file. Open the PeachScript card to import it.");433 return;434 }435436 setRawImport(text);437 await parseSource(text);438 }439440 async function handleUnlockBackup() {441 const raw = rawImport();442 if (!raw || !backupPassword().trim()) return;443 await parseSource(raw, backupPassword());444 }445446 async function countDuplicates(incoming: ImportParseResult["entries"]): Promise<number> {447 if (incoming.length === 0) return 0;448 const res = await sendMessage({ type: "GET_ENTRIES" });449 const existing = ((res as { ok: boolean; data?: VaultEntry[] }).data ?? []).filter((e) => !e.deletedAt);450 const keyOf = (title?: string, url?: string, username?: string) =>451 `${(title ?? "").toLowerCase()}|${(url ?? "").toLowerCase()}|${(username ?? "").toLowerCase()}`;452 const existingKeys = new Set(existing.map((e) => keyOf(e.title, e.url, e.username)));453 let dupes = 0;454 for (const entry of incoming) {455 const u = "username" in entry ? entry.username : undefined;456 const url = "url" in entry ? entry.url : undefined;457 if (existingKeys.has(keyOf(entry.title, url, u))) dupes += 1;458 }459 return dupes;460 }461462 async function handleImport() {463 const currentPreview = preview();464 if (!currentPreview) return;465466 if (needsBackupPassword() && !backupPassword().trim()) {467 setError("Enter the backup password used for this export.");468 return;469 }470471 if (needsMasterPassword()) {472 if (masterPassword().length < 8) {473 setError("Master password must be at least 8 characters.");474 return;475 }476 if (!masterPasswordReady()) {477 setError("Master passwords do not match.");478 return;479 }480 }481482 if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {483 setAwaitingReplaceConfirm(true);484 return;485 }486487 setImporting(true);488 setError(null);489 setDuplicateWarning(0);490 setImportProgress(null);491492 try {493 if (currentPreview.entries.length === 0) {494 throw new Error("NO_ENTRIES_IMPORTED");495 }496497 if (needsMasterPassword()) {498 const createResponse = await sendMessage({499 type: "CREATE_VAULT",500 payload: { password: masterPassword() },501 });502 if (!(createResponse as { ok: boolean }).ok) {503 throw new Error("CREATE_VAULT_FAILED");504 }505 }506507 if (!isOnboarding && mergeMode() === "replace") {508 const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });509 if (!(clearRes as { ok: boolean }).ok) {510 throw new Error("CLEAR_VAULT_FAILED");511 }512 }513514 let dupes = 0;515 if (!isOnboarding && mergeMode() === "add") {516 dupes = await countDuplicates(currentPreview.entries);517 }518519 let imported = 0;520 setImportProgress({ current: 0, total: currentPreview.entries.length });521522 const batchRes = await sendMessage({523 type: "BATCH_ADD_ENTRIES",524 payload: { entries: currentPreview.entries },525 });526527 if ((batchRes as { ok: boolean }).ok) {528 imported = ((batchRes as { ok: boolean; data?: VaultEntry[] }).data ?? []).length;529 setImportProgress({ current: imported, total: currentPreview.entries.length });530 } else {531 for (const entry of currentPreview.entries) {532 const response = await sendMessage({533 type: "ADD_ENTRY",534 payload: entry,535 });536 if ((response as { ok: boolean }).ok) imported += 1;537 setImportProgress({ current: imported, total: currentPreview.entries.length });538 }539 }540541 if (imported === 0) {542 throw new Error("NO_ENTRIES_IMPORTED");543 }544545 if (currentPreview.licenseKey) {546 await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: currentPreview.licenseKey } }).catch(() => {});547 }548549 setDuplicateWarning(dupes);550 await props.onImport();551 } catch (err) {552 const message = err instanceof Error ? err.message : String(err);553 setError(getImportErrorCopy(message));554 setImporting(false);555 setImportProgress(null);556 return;557 }558559 setImporting(false);560 setImportProgress(null);561 }562563 return (564 <>565 <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />566 <div class="peach-settings-body">567 <Show when={isOnboarding && props.replacesExistingVault}>568 <div class="peach-settings-group">569 <div class="peach-restore-warning">570 Restoring now replaces the locked local vault on this device with571 a fresh one from your backup.572 </div>573 </div>574 </Show>575 <div class="peach-settings-group peach-export-card peach-export-card--primary">576 <p class="peach-settings-label">choose a file</p>577 <p class="peach-export-card-copy">578 Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO579 credential-exchange passkeys, or a Peach JSON backup.580 </p>581582 <div class="peach-provider-chips">583 <span class="peach-provider-chip">Peach JSON</span>584 <span class="peach-provider-chip">FIDO passkeys</span>585 <span class="peach-provider-chip">Proton Pass</span>586 <span class="peach-provider-chip">Bitwarden</span>587 <span class="peach-provider-chip">1Password</span>588 <span class="peach-provider-chip">Browser CSV</span>589 </div>590591 <input592 class="peach-restore-file"593 type="file"594 accept=".csv,.json,.peach,application/x-peach"595 onChange={(event) => void handleFileSelect(event)}596 />597 </div>598599 <Show when={needsBackupPassword()}>600 <div class="peach-settings-group">601 <p class="peach-settings-label">unlock backup</p>602 <p class="peach-export-card-copy">603 Enter the password used when you created this encrypted Peach JSON604 backup.605 </p>606 <input607 class="peach-restore-input"608 type="password"609 placeholder="backup password"610 value={backupPassword()}611 onInput={(event) =>612 setBackupPassword((event.target as HTMLInputElement).value)613 }614 onBlur={() => { if (backupPassword().trim()) void handleUnlockBackup(); }}615 onKeyDown={(event) => {616 if (event.key === "Enter") {617 event.preventDefault();618 void handleUnlockBackup();619 }620 }}621 />622 <div class="peach-settings-actions">623 <button624 class="peach-btn peach-btn--subtle peach-btn--sm"625 onClick={() => void handleUnlockBackup()}626 disabled={!backupPassword().trim()}627 >628 Decrypt →629 </button>630 </div>631 </div>632 </Show>633634 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>635 <div class="peach-settings-group">636 <p class="peach-settings-label">{preview()!.label}</p>637 <div class="peach-import-summary">638 Found <strong>{preview()!.entries.length}</strong>{" "}639 entr{preview()!.entries.length === 1 ? "y" : "ies"}640 {preview()!.skippedItems > 0641 ? `. ${preview()!.skippedItems} skipped because no password was present.`642 : "."}643 </div>644645 <div class="peach-import-preview">646 {preview()!.entries.slice(0, 8).map((entry) => (647 <div class="peach-import-preview-row">648 <span class="peach-import-preview-title">{entry.title}</span>649 <span class="peach-import-preview-meta">650 {entry.passkey651 ? `${entry.passkey.userName} • ${entry.passkey.rpId} (passkey)`652 : entry.type === "note"653 ? "secure note"654 : entry.username || entry.url || "password item"}655 </span>656 </div>657 ))}658 <Show when={preview()!.entries.length > 8}>659 <div class="peach-import-preview-more">660 ...and {preview()!.entries.length - 8} more661 </div>662 </Show>663 </div>664 </div>665 </Show>666667 <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>668 <div class="peach-settings-group">669 <p class="peach-settings-label">merge or replace</p>670 <label class="peach-import-choice">671 <input672 type="radio"673 name="merge-mode"674 value="add"675 checked={mergeMode() === "add"}676 onChange={() => { setMergeMode("add"); setAwaitingReplaceConfirm(false); }}677 />678 <span class="peach-import-choice-main">679 <span class="peach-import-choice-label">Add to existing vault</span>680 <span class="peach-import-choice-sub">681 This will add {preview()!.entries.length} entries to your existing {existingCount()}.682 </span>683 </span>684 </label>685 <label class="peach-import-choice">686 <input687 type="radio"688 name="merge-mode"689 value="replace"690 checked={mergeMode() === "replace"}691 onChange={() => setMergeMode("replace")}692 />693 <span class="peach-import-choice-main">694 <span class="peach-import-choice-label">Replace vault</span>695 <span class="peach-import-choice-sub">696 This will replace your {existingCount()} entries with {preview()!.entries.length} imported ones.697 </span>698 </span>699 </label>700 <Show when={awaitingReplaceConfirm()}>701 <div class="peach-restore-warning" style="margin-top:6px">702 Replace forever? This cannot be undone.703 </div>704 </Show>705 </div>706 </Show>707708 <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>709 <div class="peach-settings-group">710 <p class="peach-settings-label">new master password</p>711 <p class="peach-export-card-copy">712 This password unlocks the Peach vault created on this device after713 import.714 </p>715 <div class="peach-auth-grid">716 <input717 class="peach-restore-input"718 type="password"719 placeholder="new master password"720 value={masterPassword()}721 onInput={(event) =>722 setMasterPassword((event.target as HTMLInputElement).value)723 }724 />725 <input726 class="peach-restore-input"727 type="password"728 placeholder="confirm master password"729 value={masterConfirm()}730 onInput={(event) =>731 setMasterConfirm((event.target as HTMLInputElement).value)732 }733 />734 </div>735 </div>736 </Show>737738 <Show when={error()}>739 <div class="peach-settings-group">740 <div class="peach-restore-error">{error()}</div>741 </div>742 </Show>743744 <Show when={importing() && importProgress()}>745 <div class="peach-settings-group">746 <div class="peach-import-summary">747 Importing... [{importProgress()!.current} / {importProgress()!.total}]748 </div>749 </div>750 </Show>751752 <Show when={duplicateWarning() > 0 && !importing()}>753 <div class="peach-settings-group">754 <div class="peach-export-card-copy">755 {duplicateWarning()} {duplicateWarning() === 1 ? "entry appears" : "entries appear"} to be duplicates of existing ones. They were still imported — you can review and delete them in your vault.756 </div>757 </div>758 </Show>759760 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>761 <div class="peach-settings-group">762 <div class="peach-settings-actions">763 <button764 class="peach-btn peach-btn--subtle peach-btn--sm"765 onClick={() => void handleImport()}766 disabled={767 importing() ||768 preview()!.entries.length === 0 ||769 (needsMasterPassword() && !masterPasswordReady())770 }771 >772 {importing()773 ? "importing..."774 : isOnboarding775 ? `Create vault + import ${preview()!.entries.length}`776 : mergeMode() === "replace"777 ? awaitingReplaceConfirm()778 ? "Yes, replace forever"779 : `Replace vault with ${preview()!.entries.length}`780 : `Import ${preview()!.entries.length}`}781 </button>782 </div>783 </div>784 </Show>785 </div>786 </>787 );788}789790791792793794function OverlayHeader(props: { title: string; onBack: () => void }) {795 return (796 <div class="peach-overlay-header">797 <button798 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"799 onClick={props.onBack}800 aria-label="Back"801 >802 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">803 <path d="M6 1L1 6L6 11" />804 </svg>805 </button>806 <span class="peach-overlay-title">{props.title}</span>807 <div class="peach-overlay-spacer"></div>808 </div>809 );810}811812function SubviewHeader(props: {813 title: string;814 onBack: () => void;815 mode: "existing" | "onboarding";816}) {817 818 819 return (820 <Show821 when={props.mode === "onboarding"}822 fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}823 >824 <div class="peach-lock-view-header">825 <PeachWordmark class="peach-wordmark-wrap" />826 <button class="peach-lock-restore-link" onClick={props.onBack}>827 ‹ back828 </button>829 </div>830 </Show>831 );832}833834function getImportErrorCopy(error: string): string {835 switch (error) {836 case "UNSUPPORTED_IMPORT_FORMAT":837 return "Unrecognised file format. Use CSV, Bitwarden JSON, Proton Pass JSON/CSV, or a Peach encrypted backup.";838 case "INVALID_EXPORT_PASSWORD":839 case "INVALID_PASSPHRASE":840 return "Wrong backup password. This is the password you chose when you created the backup.";841 case "INVALID_PEACH_EXPORT":842 return "This Peach backup file looks corrupted or incomplete.";843 case "INVALID_PEACHSCRIPT_CHARACTER":844 return "This doesn't look like PeachScript. Make sure you copied the entire backup string.";845 case "PAYLOAD_TOO_SHORT":846 return "PeachScript looks truncated. Copy the full backup string and try again.";847 case "BAD_MAGIC":848 case "UNSUPPORTED_VERSION":849 return "This PeachScript was created by a different version of Peach.";850 case "CHECKSUM_FAILED":851 return "PeachScript appears corrupted. Re-copy from the source device.";852 case "CREATE_VAULT_FAILED":853 return "Peach couldn't create a new local vault before importing.";854 case "CLEAR_VAULT_FAILED":855 return "Peach couldn't clear the existing vault before replacing it.";856 case "MASTER_PASSWORD_REQUIRED":857 return "Choose a master password for your new vault first.";858 case "RESTORE_FAILED":859 return "Restore failed. Your existing vault is unchanged.";860 case "vault locked":861 return "Peach is locked. Open Peach with your master password and try again.";862 case "refuse to replace with empty entry set":863 return "This PeachScript decoded to zero entries — refusing to wipe the vault.";864 case "NO_ENTRIES_IMPORTED":865 return "No importable password entries were found in that file.";866 default:867 return error;868 }869}