1import { createMemo, createSignal, Show } from "solid-js";2import { sendMessage } from "../App";3import type { VaultEntry } from "../../shared/types";4import { exportEntriesToPeachJson } from "../../shared/backup";5import { CJKExportView } from "./CJKExport";67interface ExportProps {8 onBack: () => void;9}1011const MIN_EXPORT_PASSWORD = 12;1213export function ExportView(props: ExportProps) {14 const [showCjkExport, setShowCjkExport] = createSignal(false);15 return (16 <Show when={showCjkExport()} fallback={<MainExportView onBack={props.onBack} onOpenCjk={() => setShowCjkExport(true)} />}>17 <CJKExportView onBack={() => setShowCjkExport(false)} />18 </Show>19 );20}2122function MainExportView(props: { onBack: () => void; onOpenCjk: () => void }) {23 const [exportPassword, setExportPassword] = createSignal("");24 const [exporting, setExporting] = createSignal(false);25 const [result, setResult] = createSignal<string | null>(null);26 const [error, setError] = createSignal<string | null>(null);27 const [showCsvWarning, setShowCsvWarning] = createSignal(false);2829 const passwordIsValid = createMemo(30 () => exportPassword().length >= MIN_EXPORT_PASSWORD31 );3233 async function handleEncryptedExport() {34 setError(null);35 setResult(null);3637 if (exportPassword().length < MIN_EXPORT_PASSWORD) {38 setError(`Export password must be at least ${MIN_EXPORT_PASSWORD} characters.`);39 return;40 }4142 setExporting(true);4344 const response = await sendMessage({ type: "GET_ENTRIES" });45 const entries = (response as { ok: boolean; data?: VaultEntry[] }).data ?? [];46 const active = entries.filter((e) => !e.deletedAt);4748 if (active.length === 0) {49 setError("Your vault has no entries to export.");50 setExporting(false);51 return;52 }5354 try {55 const blob = await exportEntriesToPeachJson(active, exportPassword());56 const filename = `peach-backup-${dateStamp()}.json`;57 downloadBlob(58 JSON.stringify(blob, null, 2),59 "application/json",60 filename61 );62 setResult(63 `Exported ${active.length} entr${active.length === 1 ? "y" : "ies"}. Filename: ${filename}. Store this file and your export password somewhere safe — neither can be recovered.`64 );65 setExportPassword("");66 } catch (err) {67 setError(err instanceof Error ? err.message : "Export failed.");68 } finally {69 setExporting(false);70 }71 }7273 async function handleCSVExport() {74 setShowCsvWarning(false);75 setExporting(true);76 setResult(null);77 setError(null);7879 const response = await sendMessage({ type: "GET_ENTRIES" });80 const entries = (response as { ok: boolean; data?: VaultEntry[] }).data ?? [];81 const active = entries.filter((e) => e.deletedAt === undefined && e.type !== "passkey");82 const excludedPasskeys = entries.filter((e) => e.deletedAt === undefined && e.type === "passkey").length;8384 if (active.length === 0) {85 setError("Your vault has no entries to export.");86 setExporting(false);87 return;88 }8990 const headers = ["type", "name", "url", "username", "password", "totp", "tags", "notes"];91 const rows = active.map((e) => {92 const type = e.type === "note" ? "note" : "login";93 return [94 type,95 e.title,96 e.url ?? "",97 e.username ?? "",98 e.password ?? "",99 e.totp ?? "",100 Array.isArray(e.tags) ? e.tags.join("|") : "",101 e.notes ?? "",102 ]103 104 105 106 .map((v) =>107 `"${String(v).replace(/\r?\n/g, " ").replace(/"/g, '""')}"`108 )109 .join(",");110 });111 const csv = [headers.join(","), ...rows].join("\n");112113 const filename = `peach-passwords-${dateStamp()}.csv`;114 try {115 downloadBlob(csv, "text/csv", filename);116 const suffix = excludedPasskeys > 0117 ? ` ${excludedPasskeys} passkey${excludedPasskeys === 1 ? "" : "s"} excluded from plaintext export.`118 : "";119 setResult(120 `Exported ${active.length} entr${active.length === 1 ? "y" : "ies"} as CSV. Filename: ${filename}.${suffix} Store this file somewhere safe — the passwords inside are in plain text.`121 );122 } catch (err) {123 setError(err instanceof Error ? err.message : "Export failed.");124 } finally {125 setExporting(false);126 }127 }128129 return (130 <div class="peach-overlay-view ov-enter-right" style="display:flex">131 <div class="peach-overlay-header">132 <button133 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"134 onClick={props.onBack}135 aria-label="Back"136 >137 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">138 <path d="M6 1L1 6L6 11" />139 </svg>140 </button>141 <span class="peach-overlay-title">export</span>142 <div class="peach-overlay-spacer"></div>143 </div>144145 <div class="peach-settings-body">146147 {}148 <div class="peach-settings-group peach-export-card peach-export-card--primary">149 <p class="peach-settings-label">backup phrase</p>150 <p class="peach-export-card-copy">151 Your vault as a string of characters you can copy anywhere — no file needed. Paste it into Peach on any device to restore.152 </p>153 <button154 class="peach-btn peach-btn--subtle peach-btn--sm"155 onClick={props.onOpenCjk}156 >157 Create backup phrase158 </button>159 </div>160161 {}162 <div class="peach-settings-group peach-export-card">163 <p class="peach-settings-label">encrypted file</p>164 <p class="peach-export-card-copy">165 Download a file backup. Set a password to lock it — you'll need that password to restore.166 </p>167 <div class="peach-settings-stack peach-export-password-stack">168 <input169 class="peach-settings-input"170 type="password"171 placeholder={`backup password (min ${MIN_EXPORT_PASSWORD} chars)`}172 value={exportPassword()}173 onInput={(e) => setExportPassword((e.target as HTMLInputElement).value)}174 />175 <Show when={exportPassword().length > 0 && exportPassword().length < MIN_EXPORT_PASSWORD}>176 <p class="peach-export-note" style="color:var(--peach-t2)">177 At least {MIN_EXPORT_PASSWORD} characters178 </p>179 </Show>180 </div>181 <button182 class="peach-btn peach-btn--subtle peach-btn--sm"183 onClick={() => void handleEncryptedExport()}184 disabled={exporting() || !passwordIsValid()}185 >186 {exporting() ? "saving…" : "Download backup file"}187 </button>188 </div>189190 {}191 <div class="peach-settings-group peach-export-card">192 <p class="peach-settings-label">CSV for other apps</p>193 <p class="peach-export-card-copy">194 Plain-text export for importing into another password manager. Passwords will be readable — delete the file once you're done.195 </p>196 <Show197 when={!showCsvWarning()}198 fallback={199 <div class="peach-settings-actions">200 <button201 class="peach-btn peach-btn--ghost peach-btn--sm"202 onClick={() => void handleCSVExport()}203 disabled={exporting()}204 >205 {exporting() ? "saving…" : "Download CSV"}206 </button>207 <button208 class="peach-btn peach-btn--ghost peach-btn--sm"209 onClick={() => setShowCsvWarning(false)}210 >211 cancel212 </button>213 </div>214 }215 >216 <button217 class="peach-btn peach-btn--ghost peach-btn--sm"218 onClick={() => setShowCsvWarning(true)}219 >220 Export as CSV221 </button>222 </Show>223 </div>224225 <Show when={error()}>226 <div class="peach-settings-group">227 <div class="peach-restore-error">{error()}</div>228 </div>229 </Show>230231 <Show when={result()}>232 <div class="peach-settings-group">233 <div class="peach-export-success">234 <span class="peach-export-success-check">✓</span>235 <span>{result()}</span>236 </div>237 </div>238 </Show>239 </div>240 </div>241 );242}243244function dateStamp(): string {245 return new Date().toISOString().split("T")[0];246}247248function downloadBlob(content: string, mimeType: string, filename: string): void {249 try {250 const url = URL.createObjectURL(new Blob([content], { type: mimeType }));251 const a = document.createElement("a");252 a.href = url;253 a.download = filename;254 document.body.appendChild(a);255 a.click();256 document.body.removeChild(a);257 URL.revokeObjectURL(url);258 } catch (err) {259 console.error("[Peach] Download failed:", err);260 throw err;261 }262}