From 159c8df10152e2b672792ea3b155ec20b6855847 Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Wed, 24 Jun 2026 18:33:59 +0530 Subject: [PATCH 1/3] feat: migrate storage to IndexedDB and standardize react-hot-toast UX --- frontend/package.json | 3 +- frontend/src/hooks/useInvoiceExport.js | 4 +- frontend/src/hooks/useInvoiceStorage.js | 53 ++++++ frontend/src/page/BatchPayment.jsx | 3 +- frontend/src/page/CreateInvoice.jsx | 38 ++++ frontend/src/page/CreateInvoicesBatch.jsx | 42 +++++ frontend/src/page/ReceivedInvoice.jsx | 51 ++++- frontend/src/page/SentInvoice.jsx | 51 ++++- frontend/src/services/invoiceStorage/index.js | 15 ++ .../services/invoiceStorage/invoiceBackup.js | 56 ++++++ .../src/services/invoiceStorage/invoiceDB.js | 174 ++++++++++++++++++ 11 files changed, 474 insertions(+), 16 deletions(-) create mode 100644 frontend/src/hooks/useInvoiceStorage.js create mode 100644 frontend/src/services/invoiceStorage/index.js create mode 100644 frontend/src/services/invoiceStorage/invoiceBackup.js create mode 100644 frontend/src/services/invoiceStorage/invoiceDB.js diff --git a/frontend/package.json b/frontend/package.json index 29c88fe0..f6831202 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@aossie-org/idb-backup": "^1.0.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/icons-material": "^6.4.6", @@ -30,6 +31,7 @@ "ethers": "^6.13.5", "framer-motion": "^12.23.12", "html2canvas": "^1.4.1", + "idb": "^8.0.3", "idb-keyval": "^6.2.1", "jspdf": "^3.0.0", "lucide-react": "^0.471.1", @@ -41,7 +43,6 @@ "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-router-dom": "^7.1.1", - "react-toastify": "^11.0.5", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "viem": "^2.22.9", diff --git a/frontend/src/hooks/useInvoiceExport.js b/frontend/src/hooks/useInvoiceExport.js index b0c61803..cdb21369 100644 --- a/frontend/src/hooks/useInvoiceExport.js +++ b/frontend/src/hooks/useInvoiceExport.js @@ -1,5 +1,5 @@ -import { useCallback } from "react"; -import { toast } from "react-toastify"; +import { useState, useCallback } from "react"; +import toast from "react-hot-toast"; import { downloadInvoiceCSV } from "@/utils/generateInvoiceCSV"; import { downloadInvoiceJSON } from "@/utils/generateInvoiceJSON"; diff --git a/frontend/src/hooks/useInvoiceStorage.js b/frontend/src/hooks/useInvoiceStorage.js new file mode 100644 index 00000000..d7776633 --- /dev/null +++ b/frontend/src/hooks/useInvoiceStorage.js @@ -0,0 +1,53 @@ +import { useState, useCallback } from 'react'; +import { + storeInvoice, + getSentInvoices, + getReceivedInvoices, + getAllInvoices, + getInvoiceById, + updateInvoiceStatus, +} from '../services/invoiceStorage/invoiceDB.js'; +import { + exportInvoiceBackup, + importInvoiceBackup, +} from '../services/invoiceStorage/invoiceBackup.js'; + +/** + * React hook for interacting with local IndexedDB invoice storage. + * Provides CRUD operations and backup/restore functionality. + */ +export function useInvoiceStorage() { + const [isExporting, setIsExporting] = useState(false); + const [isImporting, setIsImporting] = useState(false); + + const handleExport = useCallback(async () => { + setIsExporting(true); + try { + await exportInvoiceBackup(); + } finally { + setIsExporting(false); + } + }, []); + + const handleImport = useCallback(async (file) => { + setIsImporting(true); + try { + await importInvoiceBackup(file); + } finally { + setIsImporting(false); + } + }, []); + + return { + storeInvoice, + getSentInvoices, + getReceivedInvoices, + getAllInvoices, + getInvoiceById, + updateInvoiceStatus, + exportBackup: handleExport, + importBackup: handleImport, + isExporting, + isImporting, + }; +} diff --git a/frontend/src/page/BatchPayment.jsx b/frontend/src/page/BatchPayment.jsx index 6e7feef4..70e47c41 100644 --- a/frontend/src/page/BatchPayment.jsx +++ b/frontend/src/page/BatchPayment.jsx @@ -7,8 +7,7 @@ import SwipeableDrawer from "@mui/material/SwipeableDrawer"; import html2canvas from "html2canvas"; import { ERC20_ABI } from "../contractsABI/ERC20_ABI"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; +import toast from "react-hot-toast"; import { CheckCircle2, Loader2, diff --git a/frontend/src/page/CreateInvoice.jsx b/frontend/src/page/CreateInvoice.jsx index 2c31896e..368ff475 100644 --- a/frontend/src/page/CreateInvoice.jsx +++ b/frontend/src/page/CreateInvoice.jsx @@ -42,6 +42,7 @@ import { CopyButton } from "@/components/ui/copyButton"; import CountryPicker from "@/components/CountryPicker"; import { useTokenList } from "@/hooks/useTokenList"; import toast from "react-hot-toast"; +import { storeInvoice } from "../services/invoiceStorage/invoiceDB.js"; import ProductCatalogImport from "@/components/ProductCatalogImport"; import ProductAutocompleteInput from "@/components/ProductAutocompleteInput"; @@ -432,6 +433,43 @@ const validateClientAddress = useCallback((value) => { ); const receipt = await tx.wait(); + + const iface = new ethers.Interface(ChainvoiceABI); + let invoiceId = null; + for (const log of receipt.logs) { + try { + const parsed = iface.parseLog(log); + if (parsed?.name === 'InvoiceCreated') { + invoiceId = parsed.args[0].toString(); + break; + } + } catch (e) { + // ignore + } + } + + if (invoiceId) { + try { + await storeInvoice({ + invoiceId, + chainId: account.chainId, + from: account.address.toLowerCase(), + to: data.clientAddress.toLowerCase(), + isPaid: false, + isCancelled: false, + wakuDelivered: false, + invoiceDataHash: dataToEncryptHash, + data: invoicePayload, + }); + } catch (storageErr) { + console.error("Invoice created, but local persistence failed:", storageErr); + toast.error( + "Invoice was created on-chain, but could not be saved locally. Please do not leave this page until you back up the invoice details." + ); + return; + } + } + setTimeout(() => navigate("/dashboard/sent"), 4000); } catch (err) { console.error("Encryption or transaction failed:", err); diff --git a/frontend/src/page/CreateInvoicesBatch.jsx b/frontend/src/page/CreateInvoicesBatch.jsx index bbf0eaea..0157120e 100644 --- a/frontend/src/page/CreateInvoicesBatch.jsx +++ b/frontend/src/page/CreateInvoicesBatch.jsx @@ -37,6 +37,7 @@ import { format } from "date-fns"; import { Label } from "@/components/ui/label"; import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; +import { storeInvoice } from "../services/invoiceStorage/invoiceDB.js"; @@ -337,6 +338,8 @@ function CreateInvoicesBatch() { toast(`Processing ${validInvoices.length} invoices...`); + const invoicePayloads = []; + // Process each invoice for (const [index, row] of validInvoices.entries()) { toast( @@ -381,6 +384,7 @@ function CreateInvoicesBatch() { }; const invoiceString = JSON.stringify(invoicePayload); + invoicePayloads.push(invoicePayload); const encryptedStringBase64 = btoa(invoiceString); const dataToEncryptHash = ""; @@ -422,6 +426,44 @@ function CreateInvoicesBatch() { toast("Transaction submitted! Waiting for confirmation..."); const receipt = await tx.wait(); + const iface = new ethers.Interface(ChainvoiceABI); + const invoiceIds = []; + for (const log of receipt.logs) { + try { + const parsed = iface.parseLog(log); + if (parsed?.name === 'InvoiceCreated') { + invoiceIds.push(parsed.args[0].toString()); + } + } catch (e) { + // ignore + } + } + + if (invoiceIds.length !== invoicePayloads.length) { + console.warn(`Expected ${invoicePayloads.length} InvoiceCreated events, got ${invoiceIds.length}`); + } + + for (const [eventIndex, invoiceId] of invoiceIds.entries()) { + const payload = invoicePayloads[eventIndex]; + if (!payload) continue; + + try { + await storeInvoice({ + invoiceId, + chainId: account.chainId, + from: account.address.toLowerCase(), + to: payload.client.address.toLowerCase(), + isPaid: false, + isCancelled: false, + wakuDelivered: false, + invoiceDataHash: "", + data: payload, + }); + } catch (err) { + console.error(`Failed to store invoice ${invoiceId} locally:`, err); + } + } + toast.success( `Successfully created ${validInvoices.length} invoices in batch!` ); diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 6d2ad289..520c15b1 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -16,10 +16,10 @@ import { useRef } from "react"; import { generateInvoicePDF } from "@/utils/generateInvoicePDF"; import { formatInvoiceTotal } from "@/utils/invoiceExportHelpers"; import { useInvoiceExport } from "@/hooks/useInvoiceExport"; +import { getReceivedInvoices as getLocalReceivedInvoices, storeInvoice, updateInvoiceStatus } from "../services/invoiceStorage/invoiceDB.js"; import { ERC20_ABI } from "@/contractsABI/ERC20_ABI"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; +import toast from "react-hot-toast"; import CancelIcon from "@mui/icons-material/Cancel"; import { @@ -661,9 +661,18 @@ function ReceivedInvoice() { const decryptedInvoices = []; + // 1. Fetch local invoices + const localInvoices = await getLocalReceivedInvoices(address); + const localInvoiceMap = new Map(); + for (const local of localInvoices) { + if (String(local.chainId) === String(chainId)) { + localInvoiceMap.set(String(local.invoiceId), local); + } + } + for (const invoice of res) { try { - const id = invoice[0]; + const id = invoice[0].toString(); const from = invoice[1].toLowerCase(); const to = invoice[2].toLowerCase(); const isPaid = invoice[5]; @@ -678,10 +687,40 @@ function ReceivedInvoice() { continue; } - const decryptedString = atob(encryptedStringBase64); + const localInv = localInvoiceMap.get(id); + let parsed; + + if (localInv && localInv.data) { + parsed = { ...localInv.data }; + + // Update local status if it changed + if (localInv.isPaid !== isPaid || localInv.isCancelled !== isCancelled) { + await updateInvoiceStatus(chainId, id, { isPaid, isCancelled }); + } + } else { + if (!encryptedStringBase64) continue; + const decryptedString = atob(encryptedStringBase64); + parsed = JSON.parse(decryptedString); + + // Cache it locally + try { + await storeInvoice({ + invoiceId: id, + chainId, + from, + to, + isPaid, + isCancelled, + wakuDelivered: false, + invoiceDataHash: dataToEncryptHash, + data: parsed + }); + } catch (err) { + console.warn(`Failed to cache invoice ${id} locally`, err); + } + } - const parsed = JSON.parse(decryptedString); - parsed["id"] = id; + parsed["id"] = BigInt(id); parsed["isPaid"] = isPaid; parsed["isCancelled"] = isCancelled; diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index 0c01dbb2..64324ba6 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -16,9 +16,10 @@ import { useRef } from "react"; import { generateInvoicePDF } from "@/utils/generateInvoicePDF"; import { formatInvoiceTotal } from "@/utils/invoiceExportHelpers"; import { useInvoiceExport } from "@/hooks/useInvoiceExport"; +import { getSentInvoices as getLocalSentInvoices, storeInvoice, updateInvoiceStatus } from "../services/invoiceStorage/invoiceDB.js"; import { ERC20_ABI } from "@/contractsABI/ERC20_ABI"; -import { toast } from "react-toastify"; +import toast from "react-hot-toast"; import { CircularProgress, Skeleton, @@ -156,9 +157,19 @@ function SentInvoice() { const decryptedInvoices = []; + // 1. Fetch local invoices + const localInvoices = await getLocalSentInvoices(address); + const localInvoiceMap = new Map(); + for (const local of localInvoices) { + if (String(local.chainId) === String(chainId)) { + localInvoiceMap.set(String(local.invoiceId), local); + } + } + + // 2. Process on-chain invoices and merge for (const invoice of res) { try { - const id = invoice[0]; + const id = invoice[0].toString(); const from = invoice[1].toLowerCase(); const to = invoice[2].toLowerCase(); const isPaid = invoice[5]; @@ -174,10 +185,40 @@ function SentInvoice() { continue; } - const decryptedString = atob(encryptedStringBase64); + const localInv = localInvoiceMap.get(id); + let parsed; + + if (localInv && localInv.data) { + parsed = { ...localInv.data }; + + // Update local status if it changed + if (localInv.isPaid !== isPaid || localInv.isCancelled !== isCancelled) { + await updateInvoiceStatus(chainId, id, { isPaid, isCancelled }); + } + } else { + if (!encryptedStringBase64) continue; + const decryptedString = atob(encryptedStringBase64); + parsed = JSON.parse(decryptedString); + + // Cache it locally + try { + await storeInvoice({ + invoiceId: id, + chainId, + from, + to, + isPaid, + isCancelled, + wakuDelivered: false, + invoiceDataHash: dataToEncryptHash, + data: parsed + }); + } catch (err) { + console.warn(`Failed to cache invoice ${id} locally`, err); + } + } - const parsed = JSON.parse(decryptedString); - parsed["id"] = id; + parsed["id"] = BigInt(id); parsed["isPaid"] = isPaid; parsed["isCancelled"] = isCancelled; diff --git a/frontend/src/services/invoiceStorage/index.js b/frontend/src/services/invoiceStorage/index.js new file mode 100644 index 00000000..4d4ab351 --- /dev/null +++ b/frontend/src/services/invoiceStorage/index.js @@ -0,0 +1,15 @@ +export { + storeInvoice, + getInvoiceById, + getSentInvoices, + getReceivedInvoices, + getAllInvoices, + updateInvoiceStatus, + deleteInvoice, + DB_NAME, +} from './invoiceDB.js'; +export { + exportInvoiceBackup, + importInvoiceBackup, + getLocalInvoiceCount, +} from './invoiceBackup.js'; diff --git a/frontend/src/services/invoiceStorage/invoiceBackup.js b/frontend/src/services/invoiceStorage/invoiceBackup.js new file mode 100644 index 00000000..9fef4d52 --- /dev/null +++ b/frontend/src/services/invoiceStorage/invoiceBackup.js @@ -0,0 +1,56 @@ +import { + exportDB, + importDB, + downloadJSON, + readFileAsJSON, +} from '@aossie-org/idb-backup'; +import { DB_NAME } from './invoiceDB.js'; + +/** + * Export all local invoice data as a downloadable JSON backup file. + * Uses @aossie-org/idb-backup for type-safe IndexedDB export. + * @returns {Promise} the backup data object + */ +export async function exportInvoiceBackup() { + const backup = await exportDB({ dbName: DB_NAME }); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + downloadJSON(backup, `chainvoice-backup-${timestamp}.json`); + return backup; +} + +/** + * Import invoice data from a backup JSON file. + * Uses 'merge' strategy to avoid overwriting existing data. + * + * @param {File} file - the uploaded backup file + */ +export async function importInvoiceBackup(file) { + const backupData = await readFileAsJSON(file); + + // Validate it's a valid backup + if (!backupData || backupData.databaseName !== DB_NAME) { + throw new Error( + 'Invalid backup file format. Please select a valid ChainVoice backup.' + ); + } + + await importDB({ + dbName: DB_NAME, + backupData, + strategy: 'merge', + }); +} + +/** + * Get the count of locally stored invoices (for UI display). + * @returns {Promise} + */ +export async function getLocalInvoiceCount() { + try { + const { getAllInvoices } = await import('./invoiceDB.js'); + const all = await getAllInvoices(); + return all.length; + } catch { + return 0; + } +} diff --git a/frontend/src/services/invoiceStorage/invoiceDB.js b/frontend/src/services/invoiceStorage/invoiceDB.js new file mode 100644 index 00000000..2a8b1409 --- /dev/null +++ b/frontend/src/services/invoiceStorage/invoiceDB.js @@ -0,0 +1,174 @@ +import { openDB } from 'idb'; + +const DB_NAME = 'chainvoice-invoices'; +const DB_VERSION = 2; // Bumped to force re-creation of stores +const STORE_NAME = 'invoices'; + +/** + * Open (or create) the IndexedDB database for local invoice storage. + * If the database exists but is missing the required store (e.g. from a + * previous version or corrupted state), we delete and recreate it. + * @returns {Promise} + */ +async function getDB() { + try { + const db = await openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + let store; + if (!db.objectStoreNames.contains(STORE_NAME)) { + store = db.createObjectStore(STORE_NAME, { keyPath: 'compositeKey' }); + } else { + store = db.transaction.objectStore(STORE_NAME); + } + + if (!store.indexNames.contains('by-sender')) store.createIndex('by-sender', 'from'); + if (!store.indexNames.contains('by-receiver')) store.createIndex('by-receiver', 'to'); + if (!store.indexNames.contains('by-chain')) store.createIndex('by-chain', 'chainId'); + if (!store.indexNames.contains('by-invoiceId')) store.createIndex('by-invoiceId', 'invoiceId'); + }, + }); + return db; + } catch (err) { + console.error('[invoiceDB] DB open failed:', err); + throw err; + } +} + +/** + * Build a composite key from chainId and invoiceId. + * @param {number|string} chainId + * @param {number|string} invoiceId + * @returns {string} + */ +function makeKey(chainId, invoiceId) { + return `${chainId}-${invoiceId}`; +} + +/** + * Store an invoice in IndexedDB. + * The invoice object must contain: invoiceId, chainId, from, to. + * + * @param {Object} invoice - invoice record with metadata + data + */ +export async function storeInvoice(invoice) { + if (invoice.chainId === undefined || invoice.invoiceId === undefined) { + throw new Error('storeInvoice: chainId and invoiceId are required'); + } + + const db = await getDB(); + const record = { + ...invoice, + compositeKey: makeKey(invoice.chainId, invoice.invoiceId), + from: invoice.from?.toLowerCase(), + to: invoice.to?.toLowerCase(), + storedAt: Date.now(), + }; + await db.put(STORE_NAME, record); +} + +/** + * Get an invoice by chain and ID. + * @param {number|string} chainId + * @param {number|string} invoiceId + * @returns {Promise} + */ +export async function getInvoiceById(chainId, invoiceId) { + try { + const db = await getDB(); + return db.get(STORE_NAME, makeKey(chainId, invoiceId)); + } catch (err) { + console.warn('[invoiceDB] getInvoiceById failed:', err); + return undefined; + } +} + +/** + * Get all invoices sent by an address (across all chains). + * @param {string} senderAddress + * @returns {Promise} + */ +export async function getSentInvoices(senderAddress) { + try { + const db = await getDB(); + return db.getAllFromIndex( + STORE_NAME, + 'by-sender', + senderAddress.toLowerCase() + ); + } catch (err) { + console.warn('[invoiceDB] getSentInvoices failed:', err); + return []; + } +} + +/** + * Get all invoices received by an address (across all chains). + * @param {string} receiverAddress + * @returns {Promise} + */ +export async function getReceivedInvoices(receiverAddress) { + try { + const db = await getDB(); + return db.getAllFromIndex( + STORE_NAME, + 'by-receiver', + receiverAddress.toLowerCase() + ); + } catch (err) { + console.warn('[invoiceDB] getReceivedInvoices failed:', err); + return []; + } +} + +/** + * Get all invoices stored locally. + * @returns {Promise} + */ +export async function getAllInvoices() { + try { + const db = await getDB(); + return db.getAll(STORE_NAME); + } catch (err) { + console.warn('[invoiceDB] getAllInvoices failed:', err); + return []; + } +} + +/** + * Update invoice status fields (isPaid, isCancelled). + * @param {number|string} chainId + * @param {number|string} invoiceId + * @param {Object} updates - { isPaid?: boolean, isCancelled?: boolean } + * @returns {Promise} + */ +export async function updateInvoiceStatus(chainId, invoiceId, updates) { + try { + const db = await getDB(); + const key = makeKey(chainId, invoiceId); + const existing = await db.get(STORE_NAME, key); + if (!existing) return null; + const updated = { ...existing, ...updates, updatedAt: Date.now() }; + await db.put(STORE_NAME, updated); + return updated; + } catch (err) { + console.warn('[invoiceDB] updateInvoiceStatus failed:', err); + return null; + } +} + +/** + * Delete an invoice from local storage. + * @param {number|string} chainId + * @param {number|string} invoiceId + */ +export async function deleteInvoice(chainId, invoiceId) { + try { + const db = await getDB(); + await db.delete(STORE_NAME, makeKey(chainId, invoiceId)); + } catch (err) { + console.warn('[invoiceDB] deleteInvoice failed:', err); + } +} + +export { DB_NAME, STORE_NAME }; + From 11c62dda14f4a2fa08788dee53ca6917c6c917fa Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Fri, 10 Jul 2026 11:43:12 +0530 Subject: [PATCH 2/3] fix: remove unused useState import --- frontend/src/hooks/useInvoiceExport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hooks/useInvoiceExport.js b/frontend/src/hooks/useInvoiceExport.js index cdb21369..0ee6a0c8 100644 --- a/frontend/src/hooks/useInvoiceExport.js +++ b/frontend/src/hooks/useInvoiceExport.js @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react"; +import { useCallback } from "react"; import toast from "react-hot-toast"; import { downloadInvoiceCSV } from "@/utils/generateInvoiceCSV"; import { downloadInvoiceJSON } from "@/utils/generateInvoiceJSON"; From ec158deab90294eab54967f5fee15fda000eb1ce Mon Sep 17 00:00:00 2001 From: Atharva0506 Date: Fri, 10 Jul 2026 12:05:16 +0530 Subject: [PATCH 3/3] fix: address CodeRabbit review suggestions - Replace all toast.info() with toast() (react-hot-toast compatibility) - Cache IndexedDB connection to avoid reopening on every call - Add from/to validation in storeInvoice to prevent silent index failures - Use atomic readwrite transaction in updateInvoiceStatus - Switch dynamic import to static import in invoiceBackup.js - Warn user when InvoiceCreated event is not found in tx logs --- frontend/src/page/BatchPayment.jsx | 6 ++-- frontend/src/page/CreateInvoice.jsx | 5 +++ frontend/src/page/ReceivedInvoice.jsx | 20 ++++++------ frontend/src/page/SentInvoice.jsx | 2 +- .../services/invoiceStorage/invoiceBackup.js | 3 +- .../src/services/invoiceStorage/invoiceDB.js | 31 ++++++++++++------- 6 files changed, 40 insertions(+), 27 deletions(-) diff --git a/frontend/src/page/BatchPayment.jsx b/frontend/src/page/BatchPayment.jsx index 70e47c41..95dbfec7 100644 --- a/frontend/src/page/BatchPayment.jsx +++ b/frontend/src/page/BatchPayment.jsx @@ -227,7 +227,7 @@ function BatchPayment() { } setSelectedInvoices(new Set(batchInvoices.map((inv) => inv.id))); - toast.info( + toast( `Selected ${batchInvoices.length} invoices from batch #${batchId}` ); @@ -458,7 +458,7 @@ function BatchPayment() { const grouped = getGroupedInvoices(); // PRE-CHECK ALL BALANCES BEFORE ANY TRANSACTIONS - toast.info("Checking balances..."); + toast("Checking balances..."); const errors = []; for (const [tokenKey, group] of grouped.entries()) { @@ -524,7 +524,7 @@ function BatchPayment() { ); if (currentAllowance < totalAmount) { - toast.info(`Approving ${symbol} for spending...`); + toast(`Approving ${symbol} for spending...`); const approveTx = await tokenContract.approve( contractAddress, totalAmount diff --git a/frontend/src/page/CreateInvoice.jsx b/frontend/src/page/CreateInvoice.jsx index 368ff475..218e10f5 100644 --- a/frontend/src/page/CreateInvoice.jsx +++ b/frontend/src/page/CreateInvoice.jsx @@ -468,6 +468,11 @@ const validateClientAddress = useCallback((value) => { ); return; } + } else { + console.warn("InvoiceCreated event not found in transaction logs"); + toast.error( + "Invoice was created on-chain, but could not be detected in the transaction. Please check your Sent Invoices page." + ); } setTimeout(() => navigate("/dashboard/sent"), 4000); diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 520c15b1..40b5deb4 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -358,7 +358,7 @@ function ReceivedInvoice() { } setSelectedInvoices(new Set(batchInvoices.map((inv) => inv.id))); - toast.info( + toast( `Selected ${batchInvoices.length} invoices from batch #${batchId}` ); @@ -431,7 +431,7 @@ function ReceivedInvoice() { const amountDueInWei = ethers.parseUnits(String(amountDue), decimals); if (currentAllowance < amountDueInWei) { - toast.info(`Requesting approval for ${tokenSymbol}...`); + toast(`Requesting approval for ${tokenSymbol}...`); const contractAddress = import.meta.env[ `VITE_CONTRACT_ADDRESS_${chainId}` ]; @@ -443,16 +443,16 @@ function ReceivedInvoice() { contractAddress, amountDueInWei ); - toast.info("Approval transaction submitted. Please wait..."); + toast("Approval transaction submitted. Please wait..."); await approveTx.wait(); toast.success(`${tokenSymbol} approval completed successfully!`); } - toast.info("Submitting payment transaction..."); + toast("Submitting payment transaction..."); const tx = await contract.payInvoice(BigInt(invoiceId), { value: fee, }); - toast.info( + toast( "Payment transaction submitted. Please wait for confirmation..." ); await tx.wait(); @@ -461,11 +461,11 @@ function ReceivedInvoice() { const amountDueInWei = ethers.parseUnits(String(amountDue), 18); const total = amountDueInWei + BigInt(fee); - toast.info("Submitting payment transaction..."); + toast("Submitting payment transaction..."); const tx = await contract.payInvoice(BigInt(invoiceId), { value: total, }); - toast.info( + toast( "Payment transaction submitted. Please wait for confirmation..." ); await tx.wait(); @@ -511,7 +511,7 @@ function ReceivedInvoice() { const grouped = getGroupedInvoices(); // BALANCE CHECK (same as individual) - toast.info("Checking balances..."); + toast("Checking balances..."); for (const [tokenKey, group] of grouped.entries()) { try { @@ -574,7 +574,7 @@ function ReceivedInvoice() { ); if (currentAllowance < totalAmount) { - toast.info(`Approving ${symbol} for spending...`); + toast(`Approving ${symbol} for spending...`); const approveTx = await tokenContract.approve( contractAddress, totalAmount @@ -836,7 +836,7 @@ function ReceivedInvoice() { } try { - toast.info("Generating PDF..."); + toast("Generating PDF..."); const pdf = await generateInvoicePDF(drawerState.selectedInvoice, fee); const fileName = `invoice-${drawerState.selectedInvoice.id.toString().padStart(6, "0")}.pdf`; pdf.save(fileName); diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index 64324ba6..74d32f44 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -332,7 +332,7 @@ function SentInvoice() { } try { - toast.info("Generating PDF..."); + toast("Generating PDF..."); const pdf = await generateInvoicePDF(drawerState.selectedInvoice, fee); const fileName = `invoice-${drawerState.selectedInvoice.id.toString().padStart(6, "0")}.pdf`; pdf.save(fileName); diff --git a/frontend/src/services/invoiceStorage/invoiceBackup.js b/frontend/src/services/invoiceStorage/invoiceBackup.js index 9fef4d52..24d32d8e 100644 --- a/frontend/src/services/invoiceStorage/invoiceBackup.js +++ b/frontend/src/services/invoiceStorage/invoiceBackup.js @@ -4,7 +4,7 @@ import { downloadJSON, readFileAsJSON, } from '@aossie-org/idb-backup'; -import { DB_NAME } from './invoiceDB.js'; +import { DB_NAME, getAllInvoices } from './invoiceDB.js'; /** * Export all local invoice data as a downloadable JSON backup file. @@ -47,7 +47,6 @@ export async function importInvoiceBackup(file) { */ export async function getLocalInvoiceCount() { try { - const { getAllInvoices } = await import('./invoiceDB.js'); const all = await getAllInvoices(); return all.length; } catch { diff --git a/frontend/src/services/invoiceStorage/invoiceDB.js b/frontend/src/services/invoiceStorage/invoiceDB.js index 2a8b1409..898401f6 100644 --- a/frontend/src/services/invoiceStorage/invoiceDB.js +++ b/frontend/src/services/invoiceStorage/invoiceDB.js @@ -10,9 +10,11 @@ const STORE_NAME = 'invoices'; * previous version or corrupted state), we delete and recreate it. * @returns {Promise} */ +let dbPromise = null; + async function getDB() { - try { - const db = await openDB(DB_NAME, DB_VERSION, { + if (!dbPromise) { + dbPromise = openDB(DB_NAME, DB_VERSION, { upgrade(db) { let store; if (!db.objectStoreNames.contains(STORE_NAME)) { @@ -20,18 +22,19 @@ async function getDB() { } else { store = db.transaction.objectStore(STORE_NAME); } - + if (!store.indexNames.contains('by-sender')) store.createIndex('by-sender', 'from'); if (!store.indexNames.contains('by-receiver')) store.createIndex('by-receiver', 'to'); if (!store.indexNames.contains('by-chain')) store.createIndex('by-chain', 'chainId'); if (!store.indexNames.contains('by-invoiceId')) store.createIndex('by-invoiceId', 'invoiceId'); }, + }).catch((err) => { + dbPromise = null; // Reset so next call retries + console.error('[invoiceDB] DB open failed:', err); + throw err; }); - return db; - } catch (err) { - console.error('[invoiceDB] DB open failed:', err); - throw err; } + return dbPromise; } /** @@ -54,13 +57,16 @@ export async function storeInvoice(invoice) { if (invoice.chainId === undefined || invoice.invoiceId === undefined) { throw new Error('storeInvoice: chainId and invoiceId are required'); } + if (!invoice.from || !invoice.to) { + throw new Error('storeInvoice: from and to addresses are required'); + } const db = await getDB(); const record = { ...invoice, compositeKey: makeKey(invoice.chainId, invoice.invoiceId), - from: invoice.from?.toLowerCase(), - to: invoice.to?.toLowerCase(), + from: invoice.from.toLowerCase(), + to: invoice.to.toLowerCase(), storedAt: Date.now(), }; await db.put(STORE_NAME, record); @@ -145,10 +151,13 @@ export async function updateInvoiceStatus(chainId, invoiceId, updates) { try { const db = await getDB(); const key = makeKey(chainId, invoiceId); - const existing = await db.get(STORE_NAME, key); + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const existing = await store.get(key); if (!existing) return null; const updated = { ...existing, ...updates, updatedAt: Date.now() }; - await db.put(STORE_NAME, updated); + await store.put(updated); + await tx.done; return updated; } catch (err) { console.warn('[invoiceDB] updateInvoiceStatus failed:', err);