'use client'; /** * Captures taken with no network, kept until they can be sent (FLOWS.md B2, SPEC.md * section 8). IndexedDB rather than localStorage because the payload is a photograph. * * The queue is per browser, not per account. A capture belongs to whoever was signed in * when it was taken, and the API decides that from the session on the way in, so a queued * item that syncs under a different session is refused there rather than misfiled here. */ const DB_NAME = 'impuestos-offline'; const STORE = 'captures'; const VERSION = 1; export interface QueuedCapture { id: string; blob: Blob; mime: string; qrPayload: string | null; queuedAt: string; } function open(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, VERSION); request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains(STORE)) { request.result.createObjectStore(STORE, { keyPath: 'id' }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function transact(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest): Promise { return open().then( (db) => new Promise((resolve, reject) => { const transaction = db.transaction(STORE, mode); const request = run(transaction.objectStore(STORE)); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); transaction.oncomplete = () => db.close(); }), ); } export async function enqueueCapture(file: File, qrPayload: string | null): Promise { const item: QueuedCapture = { id: crypto.randomUUID(), blob: file, mime: file.type || 'image/jpeg', qrPayload, queuedAt: new Date().toISOString(), }; await transact('readwrite', (store) => store.add(item)); notify(); } export async function listQueue(): Promise { try { return await transact('readonly', (store) => store.getAll()); } catch { // A browser with storage switched off has no queue, which is not an error the reader // needs to hear about: the capture simply failed to send and said so. return []; } } export async function removeCapture(id: string): Promise { await transact('readwrite', (store) => store.delete(id)); notify(); } /** Lets the shell re-read the count without polling. */ const CHANGED = 'impuestos:offline-queue'; export function notify(): void { window.dispatchEvent(new Event(CHANGED)); } export function subscribeToQueue(listener: () => void): () => void { window.addEventListener(CHANGED, listener); window.addEventListener('online', listener); window.addEventListener('offline', listener); return () => { window.removeEventListener(CHANGED, listener); window.removeEventListener('online', listener); window.removeEventListener('offline', listener); }; } export function fileFor(item: QueuedCapture): File { return new File([item.blob], `captura-${item.id}.jpg`, { type: item.mime }); }