// Persistence for the desktop shell. // // Two tiers, both different from the mobile client: // * KeyFile lives in the OS keychain (via Electron safeStorage in main.ts, // exposed as `window.dchain.keyfile`). We never touch it here from // renderer code except through that IPC. // * Everything else — settings, contacts, chat cache, "this device was // registered" marker — lives in localStorage. It's synchronous, // origin-isolated inside the renderer, and plenty durable for // per-install state. A future polish could move chats to IndexedDB // for streaming writes, but localStorage is fine for v2.2.0. import type { KeyFile, NodeSettings, Contact, Message } from './types'; import type { DChainAPI } from '../../electron/preload'; declare global { interface Window { dchain: DChainAPI; } } // ─── KeyFile (safeStorage-backed via IPC) ──────────────────────────────── // // All keyfile operations go through window.dchain.keyfile — the preload // script bridges them to Electron's safeStorage. If preload failed to // load (dev misconfig, broken build), we surface a loud error rather // than silently failing, since a missing keyfile layer means nothing // else in the app can work. function requireDchain() { if (typeof window === 'undefined' || !window.dchain) { throw new Error( 'window.dchain is not available — the Electron preload failed to ' + 'load. Check dist-electron/preload.js exists and that main.ts is ' + 'pointing at it.', ); } return window.dchain; } export async function loadKeyFile(): Promise { const raw = await requireDchain().keyfile.load(); if (!raw) return null; try { return JSON.parse(raw) as KeyFile; } catch { return null; } } export async function saveKeyFile(kf: KeyFile): Promise { await requireDchain().keyfile.save(JSON.stringify(kf)); } export async function deleteKeyFile(): Promise { await requireDchain().keyfile.delete(); } // ─── Settings ───────────────────────────────────────────────────────────── const SETTINGS_KEY = 'dchain_settings'; const DEFAULT_SETTINGS: NodeSettings = { nodeUrl: 'http://localhost:8080', contractId: '', }; export function loadSettings(): NodeSettings { const raw = localStorage.getItem(SETTINGS_KEY); if (!raw) return DEFAULT_SETTINGS; try { return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) }; } catch { return DEFAULT_SETTINGS; } } export function saveSettings(s: Partial): void { const cur = loadSettings(); localStorage.setItem(SETTINGS_KEY, JSON.stringify({ ...cur, ...s })); } // ─── Contacts ───────────────────────────────────────────────────────────── const CONTACTS_KEY = 'dchain_contacts'; export function loadContacts(): Contact[] { const raw = localStorage.getItem(CONTACTS_KEY); if (!raw) return []; try { return JSON.parse(raw) as Contact[]; } catch { return []; } } export function saveContacts(list: Contact[]): void { localStorage.setItem(CONTACTS_KEY, JSON.stringify(list)); } export function upsertContact(c: Contact): void { const cs = loadContacts(); const i = cs.findIndex(x => x.address === c.address); if (i >= 0) cs[i] = c; else cs.push(c); saveContacts(cs); } // ─── Chat cache (per-conversation, capped) ─────────────────────────────── const CHATS_PREFIX = 'dchain_chats_'; const CHAT_CAP = 500; export function loadMessages(chatAddr: string): Message[] { const raw = localStorage.getItem(CHATS_PREFIX + chatAddr); if (!raw) return []; try { return JSON.parse(raw) as Message[]; } catch { return []; } } /** * Append + persist. Deduplicates by id, trims to CHAT_CAP newest. Callers * in the UI should prefer zustand's store.appendMessage for reactivity * and call this from effects to flush to disk. */ export function appendMessage(chatAddr: string, m: Message): void { const cur = loadMessages(chatAddr); if (cur.some(x => x.id === m.id)) return; cur.push(m); const trimmed = cur.slice(-CHAT_CAP); localStorage.setItem(CHATS_PREFIX + chatAddr, JSON.stringify(trimmed)); } // ─── Multi-device bookkeeping (shared semantic with mobile client) ─────── const DEVICE_REGISTERED_KEY = 'dchain_device_registered'; export function isDeviceRegistered(): boolean { return localStorage.getItem(DEVICE_REGISTERED_KEY) === '1'; } export function markDeviceRegistered(): void { localStorage.setItem(DEVICE_REGISTERED_KEY, '1'); } export async function wipeAllLocalState(): Promise { await deleteKeyFile(); // Everything else in localStorage we control; iterate + clear our prefix. const ours = [ SETTINGS_KEY, CONTACTS_KEY, DEVICE_REGISTERED_KEY, ]; for (const key of Object.keys(localStorage)) { if (ours.includes(key) || key.startsWith('dchain_chats_')) { localStorage.removeItem(key); } } }