From ceee33d6a98aad50cdb2ba752a77074203909fcc Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 11:12:47 +0530 Subject: [PATCH 01/11] implemented-fncs --- frontend/src/utils/localStorage.js | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 frontend/src/utils/localStorage.js diff --git a/frontend/src/utils/localStorage.js b/frontend/src/utils/localStorage.js new file mode 100644 index 00000000..3287105f --- /dev/null +++ b/frontend/src/utils/localStorage.js @@ -0,0 +1,57 @@ +/** + * Utility functions for localStorage operations with error handling + */ + +const STORAGE_KEYS = { + CREATE_INVOICE: 'chainvoice_create_invoice', + CREATE_INVOICES_BATCH: 'chainvoice_create_invoices_batch', +}; + +/** + * Safe localStorage getter + */ +export const getFromStorage = (key, defaultValue = null) => { + try { + if (typeof window === 'undefined') return defaultValue; + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (error) { + console.error(`Error reading from localStorage (${key}):`, error); + return defaultValue; + } +}; + +/** + * Safe localStorage setter + */ +export const saveToStorage = (key, value) => { + try { + if (typeof window === 'undefined') return; + window.localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.error(`Error saving to localStorage (${key}):`, error); + // Handle quota exceeded error + if (error.name === 'QuotaExceededError') { + console.warn('localStorage quota exceeded. Clearing old data...'); + clearStorage(key); + } + } +}; + +/** + * Clear specific key from localStorage + */ +export const clearStorage = (key) => { + try { + if (typeof window === 'undefined') return; + window.localStorage.removeItem(key); + } catch (error) { + console.error(`Error clearing localStorage (${key}):`, error); + } +}; + +/** + * Storage keys + */ +export const StorageKeys = STORAGE_KEYS; + From e5766a8151306d1ae9766e99143805f1185c3a8e Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 11:13:42 +0530 Subject: [PATCH 02/11] added-localstorage-impl-invoice --- frontend/src/page/CreateInvoice.jsx | 326 ++++++++++++++++++++++------ 1 file changed, 263 insertions(+), 63 deletions(-) diff --git a/frontend/src/page/CreateInvoice.jsx b/frontend/src/page/CreateInvoice.jsx index ba9eb779..c89ff69b 100644 --- a/frontend/src/page/CreateInvoice.jsx +++ b/frontend/src/page/CreateInvoice.jsx @@ -48,6 +48,12 @@ import TokenPicker, { ToggleSwitch } from "@/components/TokenPicker"; import { CopyButton } from "@/components/ui/copyButton"; import CountryPicker from "@/components/CountryPicker"; import { useTokenList } from "@/hooks/useTokenList"; +import { + getFromStorage, + saveToStorage, + clearStorage, + StorageKeys, +} from "@/utils/localStorage"; function CreateInvoice() { const { data: walletClient } = useWalletClient(); @@ -65,6 +71,20 @@ function CreateInvoice() { const [userCountry, setUserCountry] = useState(""); const [clientCountry, setClientCountry] = useState(""); + // User form fields + const [userFname, setUserFname] = useState(""); + const [userLname, setUserLname] = useState(""); + const [userEmail, setUserEmail] = useState(""); + const [userCity, setUserCity] = useState(""); + const [userPostalcode, setUserPostalcode] = useState(""); + + // Client form fields + const [clientFname, setClientFname] = useState(""); + const [clientLname, setClientLname] = useState(""); + const [clientEmail, setClientEmail] = useState(""); + const [clientCity, setClientCity] = useState(""); + const [clientPostalcode, setClientPostalcode] = useState(""); + // Token selection state const [selectedToken, setSelectedToken] = useState(null); const [customTokenAddress, setCustomTokenAddress] = useState(""); @@ -89,8 +109,67 @@ function CreateInvoice() { ]); const [totalAmountDue, setTotalAmountDue] = useState(0); + const [isInitialLoad, setIsInitialLoad] = useState(true); + // Load data from localStorage on mount useEffect(() => { + const savedData = getFromStorage(StorageKeys.CREATE_INVOICE); + if (savedData) { + // Restore dates + if (savedData.dueDate) { + setDueDate(new Date(savedData.dueDate)); + } + if (savedData.issueDate) { + setIssueDate(new Date(savedData.issueDate)); + } + + // Restore user info + if (savedData.userFname) setUserFname(savedData.userFname); + if (savedData.userLname) setUserLname(savedData.userLname); + if (savedData.userEmail) setUserEmail(savedData.userEmail); + if (savedData.userCountry) setUserCountry(savedData.userCountry); + if (savedData.userCity) setUserCity(savedData.userCity); + if (savedData.userPostalcode) setUserPostalcode(savedData.userPostalcode); + + // Restore client info (only if no URL params) + const urlClientAddress = searchParams.get("clientAddress"); + if (!urlClientAddress) { + if (savedData.clientAddress) setClientAddress(savedData.clientAddress); + if (savedData.clientFname) setClientFname(savedData.clientFname); + if (savedData.clientLname) setClientLname(savedData.clientLname); + if (savedData.clientEmail) setClientEmail(savedData.clientEmail); + if (savedData.clientCountry) setClientCountry(savedData.clientCountry); + if (savedData.clientCity) setClientCity(savedData.clientCity); + if (savedData.clientPostalcode) + setClientPostalcode(savedData.clientPostalcode); + } + + // Restore token selection + if (savedData.selectedToken) { + setSelectedToken(savedData.selectedToken); + setUseCustomToken(false); + } + if (savedData.useCustomToken && savedData.customTokenAddress) { + setUseCustomToken(true); + setCustomTokenAddress(savedData.customTokenAddress); + if (savedData.verifiedToken) { + setVerifiedToken(savedData.verifiedToken); + setTokenVerificationState("success"); + } + } + + // Restore item data + if (savedData.itemData && savedData.itemData.length > 0) { + setItemData(savedData.itemData); + } + } + setIsInitialLoad(false); + }, []); + + // Handle URL params (should override localStorage) + useEffect(() => { + if (isInitialLoad) return; + console.log("account address : ", account.address); const urlClientAddress = searchParams.get("clientAddress"); const urlTokenAddress = searchParams.get("tokenAddress"); @@ -101,63 +180,93 @@ function CreateInvoice() { } const processUrlToken = async () => { - if (urlTokenAddress && !loadingTokens) { - if (isCustomFromURL) { - setUseCustomToken(true); - setCustomTokenAddress(urlTokenAddress); - verifyToken(urlTokenAddress); - } else { - const preselectedToken = tokens.find( - (token) => - (token.contract_address || token.address).toLowerCase() === urlTokenAddress.toLowerCase() - ); + if (!urlTokenAddress || loadingTokens) return; - if (preselectedToken) { - let decimals = preselectedToken.decimals; - - // If decimals are missing/null, try to fetch them from chain - if (decimals === undefined || decimals === null) { - try { - if (typeof window !== "undefined" && window.ethereum) { - const provider = new BrowserProvider(window.ethereum); - const contract = new ethers.Contract(urlTokenAddress, ERC20_ABI, provider); - // Try to fetch decimals - decimals = await contract.decimals(); - } - } catch (err) { - console.warn("Failed to fetch decimals for preselected token:", err); - } - } + if (isCustomFromURL) { + setUseCustomToken(true); + setCustomTokenAddress(urlTokenAddress); + verifyToken(urlTokenAddress); + return; + } + + // Try to find token in saved selection first + const savedData = getFromStorage(StorageKeys.CREATE_INVOICE); + if (savedData?.selectedToken) { + const token = savedData.selectedToken; + if ( + token.address?.toLowerCase() === urlTokenAddress.toLowerCase() + ) { + setSelectedToken(token); + setUseCustomToken(false); + return; + } + } - // If we successfully resolved decimals (from list or chain) - if (decimals !== undefined && decimals !== null) { - setSelectedToken({ - address: preselectedToken.contract_address || preselectedToken.address, - symbol: preselectedToken.symbol, - name: preselectedToken.name, - logo: preselectedToken.image, - decimals: Number(decimals) - }); - setUseCustomToken(false); - } else { - // Fallback to manual verification if we can't determine decimals safely - console.warn("Could not determine token decimals, falling back to manual verification."); - setUseCustomToken(true); - setCustomTokenAddress(urlTokenAddress); - verifyToken(urlTokenAddress); + // Try to find from tokens list + const preselectedToken = tokens.find( + (token) => + (token.contract_address || token.address).toLowerCase() === + urlTokenAddress.toLowerCase() + ); + + if (preselectedToken) { + let decimals = preselectedToken.decimals; + + // If decimals are missing/null, try to fetch them from chain + if (decimals === undefined || decimals === null) { + try { + if (typeof window !== "undefined" && window.ethereum) { + const provider = new BrowserProvider(window.ethereum); + const contract = new ethers.Contract( + urlTokenAddress, + ERC20_ABI, + provider + ); + decimals = await contract.decimals(); } - } else { - // Not in list, treat as custom - setUseCustomToken(true); - setCustomTokenAddress(urlTokenAddress); - verifyToken(urlTokenAddress); + } catch (err) { + console.warn( + "Failed to fetch decimals for preselected token:", + err + ); } } + + if (decimals !== undefined && decimals !== null) { + setSelectedToken({ + address: + preselectedToken.contract_address || preselectedToken.address, + symbol: preselectedToken.symbol, + name: preselectedToken.name, + logo: preselectedToken.image, + decimals: Number(decimals), + }); + setUseCustomToken(false); + } else { + // Fallback to manual verification if we can't determine decimals safely + console.warn( + "Could not determine token decimals, falling back to manual verification." + ); + setUseCustomToken(true); + setCustomTokenAddress(urlTokenAddress); + verifyToken(urlTokenAddress); + } + } else { + // Not in list, treat as custom + setUseCustomToken(true); + setCustomTokenAddress(urlTokenAddress); + verifyToken(urlTokenAddress); } }; processUrlToken(); - }, [searchParams, walletClient, tokens, loadingTokens, account.address]); + }, [ + isInitialLoad, + searchParams, + tokens, + loadingTokens, + account.address, + ]); useEffect(() => { const total = itemData.reduce((sum, item) => { @@ -192,6 +301,64 @@ function CreateInvoice() { setShowWalletAlert(!isConnected); }, [isConnected]); + // Save form data to localStorage (debounced) + useEffect(() => { + if (isInitialLoad) return; + + const saveData = () => { + const dataToSave = { + dueDate: dueDate?.toISOString(), + issueDate: issueDate?.toISOString(), + clientAddress, + userFname, + userLname, + userEmail, + userCountry, + userCity, + userPostalcode, + clientFname, + clientLname, + clientEmail, + clientCountry, + clientCity, + clientPostalcode, + selectedToken, + customTokenAddress, + useCustomToken, + verifiedToken, + itemData, + }; + + saveToStorage(StorageKeys.CREATE_INVOICE, dataToSave); + }; + + // Debounce save operations + const timeoutId = setTimeout(saveData, 500); + return () => clearTimeout(timeoutId); + }, [ + dueDate, + issueDate, + clientAddress, + userFname, + userLname, + userEmail, + userCountry, + userCity, + userPostalcode, + clientFname, + clientLname, + clientEmail, + clientCountry, + clientCity, + clientPostalcode, + selectedToken, + customTokenAddress, + useCustomToken, + verifiedToken, + itemData, + isInitialLoad, + ]); + const handleItemData = (e, index) => { const { name, value } = e.target; @@ -416,7 +583,6 @@ const verifyToken = async (address, targetChainId = null) => { if (!account?.chainId) { throw new Error("Missing chainId: wallet connected but chain not configured"); } - const contractAddress = import.meta.env[ `VITE_CONTRACT_ADDRESS_${account.chainId}` ]; @@ -436,6 +602,10 @@ const verifyToken = async (address, targetChainId = null) => { ); const receipt = await tx.wait(); + + // Clear localStorage on successful submission + clearStorage(StorageKeys.CREATE_INVOICE); + setTimeout(() => navigate("/dashboard/sent"), 4000); } catch (err) { console.error("Encryption or transaction failed:", err); @@ -450,20 +620,20 @@ const verifyToken = async (address, targetChainId = null) => { const formData = new FormData(e.target); const data = { - userAddress: formData.get("userAddress"), - userFname: formData.get("userFname"), - userLname: formData.get("userLname"), - userEmail: formData.get("userEmail"), - userCountry: userCountry || formData.get("userCountry") || "", - userCity: formData.get("userCity"), - userPostalcode: formData.get("userPostalcode"), - clientAddress: formData.get("clientAddress"), - clientFname: formData.get("clientFname"), - clientLname: formData.get("clientLname"), - clientEmail: formData.get("clientEmail"), - clientCountry: clientCountry || formData.get("clientCountry") || "", - clientCity: formData.get("clientCity"), - clientPostalcode: formData.get("clientPostalcode"), + userAddress: account?.address?.toString(), + userFname, + userLname, + userEmail, + userCountry, + userCity, + userPostalcode, + clientAddress, + clientFname, + clientLname, + clientEmail, + clientCountry, + clientCity, + clientPostalcode, itemData, }; await createInvoiceRequest(data); @@ -595,6 +765,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Your First Name" className="w-full mt-1 border-gray-300 text-black " name="userFname" + value={userFname} + onChange={(e) => setUserFname(e.target.value)} />
@@ -606,6 +778,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Your Last Name" className="w-full mt-1 border-gray-300 text-black" name="userLname" + value={userLname} + onChange={(e) => setUserLname(e.target.value)} />
@@ -620,6 +794,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Email" className="w-full mt-1 border-gray-300 text-black" name="userEmail" + value={userEmail} + onChange={(e) => setUserEmail(e.target.value)} />
@@ -653,6 +829,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="City" className="w-full mt-1 border-gray-300 text-black" name="userCity" + value={userCity} + onChange={(e) => setUserCity(e.target.value)} />
@@ -664,6 +842,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Postal Code" className="w-full mt-1 border-gray-300 text-black" name="userPostalcode" + value={userPostalcode} + onChange={(e) => setUserPostalcode(e.target.value)} />
@@ -693,6 +873,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Client First Name" className="w-full mt-1 border-gray-300 text-black" name="clientFname" + value={clientFname} + onChange={(e) => setClientFname(e.target.value)} />
@@ -704,6 +886,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Client Last Name" className="w-full mt-1 border-gray-300 text-black" name="clientLname" + value={clientLname} + onChange={(e) => setClientLname(e.target.value)} />
@@ -718,6 +902,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Email" className="w-full mt-1 border-gray-300 text-black" name="clientEmail" + value={clientEmail} + onChange={(e) => setClientEmail(e.target.value)} />
@@ -751,6 +937,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="City" className="w-full mt-1 border-gray-300 text-black" name="clientCity" + value={clientCity} + onChange={(e) => setClientCity(e.target.value)} />
@@ -762,6 +950,8 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Postal Code" className="w-full mt-1 border-gray-300 text-black" name="clientPostalcode" + value={clientPostalcode} + onChange={(e) => setClientPostalcode(e.target.value)} />
@@ -1025,6 +1215,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Enter Description" className="w-full border-gray-300 text-black" name="description" + value={itemData[index].description || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1039,6 +1230,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black" name="qty" + value={itemData[index].qty || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1051,6 +1243,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black" name="unitPrice" + value={itemData[index].unitPrice || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1066,6 +1259,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black" name="discount" + value={itemData[index].discount || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1078,6 +1272,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black" name="tax" + value={itemData[index].tax || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1139,6 +1334,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="Enter Description" className="w-full border-gray-300 text-black" name="description" + value={itemData[index].description || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1148,6 +1344,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black py-2" name="qty" + value={itemData[index].qty || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1157,6 +1354,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black py-2" name="unitPrice" + value={itemData[index].unitPrice || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1166,6 +1364,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black py-2" name="discount" + value={itemData[index].discount || ""} onChange={(e) => handleItemData(e, index)} /> @@ -1175,6 +1374,7 @@ const verifyToken = async (address, targetChainId = null) => { placeholder="0" className="w-full border-gray-300 text-black py-2" name="tax" + value={itemData[index].tax || ""} onChange={(e) => handleItemData(e, index)} /> From 6ad3d4c046c46da802c9669463be44b38b0ac853 Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 11:14:18 +0530 Subject: [PATCH 03/11] added-local-storage-impl-for-batch-invoices --- frontend/src/page/CreateInvoicesBatch.jsx | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/frontend/src/page/CreateInvoicesBatch.jsx b/frontend/src/page/CreateInvoicesBatch.jsx index fb1c9ddf..00f8c15c 100644 --- a/frontend/src/page/CreateInvoicesBatch.jsx +++ b/frontend/src/page/CreateInvoicesBatch.jsx @@ -54,6 +54,12 @@ import WalletConnectionAlert from "../components/WalletConnectionAlert"; import TokenPicker, { ToggleSwitch } from "@/components/TokenPicker"; import { CopyButton } from "@/components/ui/copyButton"; import CountryPicker from "@/components/CountryPicker"; +import { + getFromStorage, + saveToStorage, + clearStorage, + StorageKeys, +} from "@/utils/localStorage"; function CreateInvoicesBatch() { const { data: walletClient } = useWalletClient(); @@ -75,6 +81,7 @@ function CreateInvoicesBatch() { // UI state for collapsible invoices const [expandedInvoice, setExpandedInvoice] = useState(0); + const [isInitialLoad, setIsInitialLoad] = useState(true); // Batch invoice data const [invoiceRows, setInvoiceRows] = useState([ @@ -132,6 +139,45 @@ function CreateInvoicesBatch() { ); }, [invoiceRows.map((r) => JSON.stringify(r.itemData)).join(",")]); + // Load data from localStorage on mount + useEffect(() => { + const savedData = getFromStorage(StorageKeys.CREATE_INVOICES_BATCH); + if (savedData) { + // Restore dates + if (savedData.dueDate) { + setDueDate(new Date(savedData.dueDate)); + } + if (savedData.issueDate) { + setIssueDate(new Date(savedData.issueDate)); + } + + // Restore user info + if (savedData.userInfo) { + setUserInfo(savedData.userInfo); + } + + // Restore invoice rows + if (savedData.invoiceRows && savedData.invoiceRows.length > 0) { + setInvoiceRows(savedData.invoiceRows); + } + + // Restore token selection + if (savedData.selectedToken) { + setSelectedToken(savedData.selectedToken); + setUseCustomToken(false); + } + if (savedData.useCustomToken && savedData.customTokenAddress) { + setUseCustomToken(true); + setCustomTokenAddress(savedData.customTokenAddress); + if (savedData.verifiedToken) { + setVerifiedToken(savedData.verifiedToken); + setTokenVerificationState("success"); + } + } + } + setIsInitialLoad(false); + }, []); + // Initialize Lit useEffect(() => { const initLit = async () => { @@ -151,6 +197,40 @@ function CreateInvoicesBatch() { setShowWalletAlert(!isConnected); }, [isConnected]); + // Save form data to localStorage (debounced) + useEffect(() => { + if (isInitialLoad) return; + + const saveData = () => { + const dataToSave = { + dueDate: dueDate?.toISOString(), + issueDate: issueDate?.toISOString(), + userInfo, + invoiceRows, + selectedToken, + customTokenAddress, + useCustomToken, + verifiedToken, + }; + + saveToStorage(StorageKeys.CREATE_INVOICES_BATCH, dataToSave); + }; + + // Debounce save operations + const timeoutId = setTimeout(saveData, 500); + return () => clearTimeout(timeoutId); + }, [ + dueDate, + issueDate, + userInfo, + invoiceRows, + selectedToken, + customTokenAddress, + useCustomToken, + verifiedToken, + isInitialLoad, + ]); + // Invoice management const addInvoiceRow = () => { const newIndex = invoiceRows.length; @@ -480,6 +560,7 @@ function CreateInvoicesBatch() { toast.info("Submitting batch transaction to blockchain..."); // Send to contract + const chainId = account?.chainId; const contractAddress = import.meta.env[ `VITE_CONTRACT_ADDRESS_${chainId}` ]; @@ -501,6 +582,9 @@ function CreateInvoicesBatch() { toast.info("Transaction submitted! Waiting for confirmation..."); const receipt = await tx.wait(); + // Clear localStorage on successful submission + clearStorage(StorageKeys.CREATE_INVOICES_BATCH); + toast.success( `Successfully created ${validInvoices.length} invoices in batch!` ); From 3f736f7adef7021fb703211cdf980b86f68b5f5d Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 11:45:01 +0530 Subject: [PATCH 04/11] fixed-small-issue --- frontend/src/page/CreateInvoice.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/page/CreateInvoice.jsx b/frontend/src/page/CreateInvoice.jsx index c89ff69b..ee5f760f 100644 --- a/frontend/src/page/CreateInvoice.jsx +++ b/frontend/src/page/CreateInvoice.jsx @@ -355,7 +355,8 @@ function CreateInvoice() { customTokenAddress, useCustomToken, verifiedToken, - itemData, + // Use JSON.stringify to detect deep changes in itemData + JSON.stringify(itemData), isInitialLoad, ]); From ec7fc1a07ee16ccf98c2e603886f3f4d36eac834 Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 11:45:22 +0530 Subject: [PATCH 05/11] fixed-small-issue --- frontend/src/page/CreateInvoicesBatch.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/page/CreateInvoicesBatch.jsx b/frontend/src/page/CreateInvoicesBatch.jsx index 00f8c15c..7a3385b1 100644 --- a/frontend/src/page/CreateInvoicesBatch.jsx +++ b/frontend/src/page/CreateInvoicesBatch.jsx @@ -222,8 +222,9 @@ function CreateInvoicesBatch() { }, [ dueDate, issueDate, - userInfo, - invoiceRows, + // Use JSON.stringify to detect deep changes in nested objects + JSON.stringify(userInfo), + JSON.stringify(invoiceRows), selectedToken, customTokenAddress, useCustomToken, From fd03af1fc27d530f059c45b02f36b65e607b12b9 Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 12:36:47 +0530 Subject: [PATCH 06/11] persistant-session-storge-updated --- frontend/src/utils/localStorage.js | 177 ++++++++++++++++++++++++++--- 1 file changed, 161 insertions(+), 16 deletions(-) diff --git a/frontend/src/utils/localStorage.js b/frontend/src/utils/localStorage.js index 3287105f..6037a164 100644 --- a/frontend/src/utils/localStorage.js +++ b/frontend/src/utils/localStorage.js @@ -1,5 +1,6 @@ /** - * Utility functions for localStorage operations with error handling + * Utility functions for sessionStorage operations with TTL and PII protection + * Uses sessionStorage (session-only, cleared on tab close) with TTL expiry */ const STORAGE_KEYS = { @@ -7,51 +8,195 @@ const STORAGE_KEYS = { CREATE_INVOICES_BATCH: 'chainvoice_create_invoices_batch', }; +// Default TTL: 24 hours (in milliseconds) +const DEFAULT_TTL = 24 * 60 * 60 * 1000; + +/** + * Get storage instance (sessionStorage for session-only storage) + */ +const getStorage = () => { + if (typeof window === 'undefined') return null; + return window.sessionStorage; +}; + /** - * Safe localStorage getter + * Safe sessionStorage getter with TTL expiry check + * Automatically deletes expired entries on read + * Session-only: cleared when browser tab closes */ export const getFromStorage = (key, defaultValue = null) => { try { - if (typeof window === 'undefined') return defaultValue; - const item = window.localStorage.getItem(key); - return item ? JSON.parse(item) : defaultValue; + const storage = getStorage(); + if (!storage) return defaultValue; + + const item = storage.getItem(key); + if (!item) return defaultValue; + + const parsed = JSON.parse(item); + + // Check if entry has TTL and if it's expired + if (parsed.expiresAt && parsed.expiresAt < Date.now()) { + // Entry expired, delete it + storage.removeItem(key); + return defaultValue; + } + + // Return the actual data (without TTL metadata) + return parsed.data !== undefined ? parsed.data : parsed; } catch (error) { - console.error(`Error reading from localStorage (${key}):`, error); + console.error(`Error reading from sessionStorage (${key}):`, error); return defaultValue; } }; /** - * Safe localStorage setter + * Safe sessionStorage setter with TTL + * Wraps data with expiry timestamp + * Session-only: cleared when browser tab closes */ -export const saveToStorage = (key, value) => { +export const saveToStorage = (key, value, ttl = DEFAULT_TTL) => { try { - if (typeof window === 'undefined') return; - window.localStorage.setItem(key, JSON.stringify(value)); + const storage = getStorage(); + if (!storage) return; + + const expiresAt = Date.now() + ttl; + const dataToStore = { + data: value, + expiresAt, + createdAt: Date.now(), + }; + + storage.setItem(key, JSON.stringify(dataToStore)); } catch (error) { - console.error(`Error saving to localStorage (${key}):`, error); + console.error(`Error saving to sessionStorage (${key}):`, error); // Handle quota exceeded error if (error.name === 'QuotaExceededError') { - console.warn('localStorage quota exceeded. Clearing old data...'); + console.warn('sessionStorage quota exceeded. Clearing old data...'); clearStorage(key); + // Try to clean up expired entries + cleanupExpiredEntries(); } } }; /** - * Clear specific key from localStorage + * Clear specific key from sessionStorage */ export const clearStorage = (key) => { try { - if (typeof window === 'undefined') return; - window.localStorage.removeItem(key); + const storage = getStorage(); + if (!storage) return; + storage.removeItem(key); + } catch (error) { + console.error(`Error clearing sessionStorage (${key}):`, error); + } +}; + +/** + * Cleanup expired entries from sessionStorage + */ +const cleanupExpiredEntries = () => { + try { + const storage = getStorage(); + if (!storage) return; + + const keysToRemove = []; + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + if (key && key.startsWith('chainvoice_')) { + try { + const item = storage.getItem(key); + if (item) { + const parsed = JSON.parse(item); + if (parsed.expiresAt && parsed.expiresAt < Date.now()) { + keysToRemove.push(key); + } + } + } catch (e) { + // Invalid entry, remove it + keysToRemove.push(key); + } + } + } + + keysToRemove.forEach((key) => storage.removeItem(key)); } catch (error) { - console.error(`Error clearing localStorage (${key}):`, error); + console.error('Error cleaning up expired entries:', error); } }; +// Maximum number of invoice rows to persist (to prevent quota issues) +const MAX_PERSISTED_INVOICES = 10; + +/** + * Remove PII (emails, names, addresses, countries) from data object before saving + * Only keeps essential non-PII data needed for form restoration + */ +export const sanitizeDataForStorage = (data) => { + if (!data || typeof data !== 'object') return data; + + const sanitized = { ...data }; + + // Remove top-level PII fields + delete sanitized.userEmail; + delete sanitized.clientEmail; + delete sanitized.userFname; + delete sanitized.userLname; + delete sanitized.userCountry; + delete sanitized.userCity; + delete sanitized.userPostalcode; + delete sanitized.clientFname; + delete sanitized.clientLname; + delete sanitized.clientCountry; + delete sanitized.clientCity; + delete sanitized.clientPostalcode; + + // Handle nested userInfo object (for batch invoices) + if (sanitized.userInfo && typeof sanitized.userInfo === 'object') { + sanitized.userInfo = { ...sanitized.userInfo }; + // Remove all PII from userInfo - only keep non-PII fields if any + delete sanitized.userInfo.userEmail; + delete sanitized.userInfo.userFname; + delete sanitized.userInfo.userLname; + delete sanitized.userInfo.userCountry; + delete sanitized.userInfo.userCity; + delete sanitized.userInfo.userPostalcode; + } + + // Handle invoiceRows array (for batch invoices) + if (sanitized.invoiceRows && Array.isArray(sanitized.invoiceRows)) { + // Limit batch size to prevent quota issues + const limitedRows = sanitized.invoiceRows.slice(0, MAX_PERSISTED_INVOICES); + + sanitized.invoiceRows = limitedRows.map((row) => { + const sanitizedRow = { ...row }; + // Remove all client PII - only keep essential non-PII data + delete sanitizedRow.clientEmail; + delete sanitizedRow.clientFname; + delete sanitizedRow.clientLname; + delete sanitizedRow.clientCountry; + delete sanitizedRow.clientCity; + delete sanitizedRow.clientPostalcode; + // Keep: clientAddress (needed for functionality), itemData, totalAmountDue + return sanitizedRow; + }); + } + + return sanitized; +}; + +/** + * Get maximum number of invoices that can be persisted + */ +export const getMaxPersistedInvoices = () => MAX_PERSISTED_INVOICES; + /** * Storage keys */ export const StorageKeys = STORAGE_KEYS; +// Cleanup expired entries on module load +if (typeof window !== 'undefined') { + cleanupExpiredEntries(); +} + From 5a90d1c6de05ad8b512c766ef5341622c89b1918 Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 12:41:51 +0530 Subject: [PATCH 07/11] file-name-changed --- frontend/src/utils/sessionStorage.js | 202 +++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 frontend/src/utils/sessionStorage.js diff --git a/frontend/src/utils/sessionStorage.js b/frontend/src/utils/sessionStorage.js new file mode 100644 index 00000000..6037a164 --- /dev/null +++ b/frontend/src/utils/sessionStorage.js @@ -0,0 +1,202 @@ +/** + * Utility functions for sessionStorage operations with TTL and PII protection + * Uses sessionStorage (session-only, cleared on tab close) with TTL expiry + */ + +const STORAGE_KEYS = { + CREATE_INVOICE: 'chainvoice_create_invoice', + CREATE_INVOICES_BATCH: 'chainvoice_create_invoices_batch', +}; + +// Default TTL: 24 hours (in milliseconds) +const DEFAULT_TTL = 24 * 60 * 60 * 1000; + +/** + * Get storage instance (sessionStorage for session-only storage) + */ +const getStorage = () => { + if (typeof window === 'undefined') return null; + return window.sessionStorage; +}; + +/** + * Safe sessionStorage getter with TTL expiry check + * Automatically deletes expired entries on read + * Session-only: cleared when browser tab closes + */ +export const getFromStorage = (key, defaultValue = null) => { + try { + const storage = getStorage(); + if (!storage) return defaultValue; + + const item = storage.getItem(key); + if (!item) return defaultValue; + + const parsed = JSON.parse(item); + + // Check if entry has TTL and if it's expired + if (parsed.expiresAt && parsed.expiresAt < Date.now()) { + // Entry expired, delete it + storage.removeItem(key); + return defaultValue; + } + + // Return the actual data (without TTL metadata) + return parsed.data !== undefined ? parsed.data : parsed; + } catch (error) { + console.error(`Error reading from sessionStorage (${key}):`, error); + return defaultValue; + } +}; + +/** + * Safe sessionStorage setter with TTL + * Wraps data with expiry timestamp + * Session-only: cleared when browser tab closes + */ +export const saveToStorage = (key, value, ttl = DEFAULT_TTL) => { + try { + const storage = getStorage(); + if (!storage) return; + + const expiresAt = Date.now() + ttl; + const dataToStore = { + data: value, + expiresAt, + createdAt: Date.now(), + }; + + storage.setItem(key, JSON.stringify(dataToStore)); + } catch (error) { + console.error(`Error saving to sessionStorage (${key}):`, error); + // Handle quota exceeded error + if (error.name === 'QuotaExceededError') { + console.warn('sessionStorage quota exceeded. Clearing old data...'); + clearStorage(key); + // Try to clean up expired entries + cleanupExpiredEntries(); + } + } +}; + +/** + * Clear specific key from sessionStorage + */ +export const clearStorage = (key) => { + try { + const storage = getStorage(); + if (!storage) return; + storage.removeItem(key); + } catch (error) { + console.error(`Error clearing sessionStorage (${key}):`, error); + } +}; + +/** + * Cleanup expired entries from sessionStorage + */ +const cleanupExpiredEntries = () => { + try { + const storage = getStorage(); + if (!storage) return; + + const keysToRemove = []; + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + if (key && key.startsWith('chainvoice_')) { + try { + const item = storage.getItem(key); + if (item) { + const parsed = JSON.parse(item); + if (parsed.expiresAt && parsed.expiresAt < Date.now()) { + keysToRemove.push(key); + } + } + } catch (e) { + // Invalid entry, remove it + keysToRemove.push(key); + } + } + } + + keysToRemove.forEach((key) => storage.removeItem(key)); + } catch (error) { + console.error('Error cleaning up expired entries:', error); + } +}; + +// Maximum number of invoice rows to persist (to prevent quota issues) +const MAX_PERSISTED_INVOICES = 10; + +/** + * Remove PII (emails, names, addresses, countries) from data object before saving + * Only keeps essential non-PII data needed for form restoration + */ +export const sanitizeDataForStorage = (data) => { + if (!data || typeof data !== 'object') return data; + + const sanitized = { ...data }; + + // Remove top-level PII fields + delete sanitized.userEmail; + delete sanitized.clientEmail; + delete sanitized.userFname; + delete sanitized.userLname; + delete sanitized.userCountry; + delete sanitized.userCity; + delete sanitized.userPostalcode; + delete sanitized.clientFname; + delete sanitized.clientLname; + delete sanitized.clientCountry; + delete sanitized.clientCity; + delete sanitized.clientPostalcode; + + // Handle nested userInfo object (for batch invoices) + if (sanitized.userInfo && typeof sanitized.userInfo === 'object') { + sanitized.userInfo = { ...sanitized.userInfo }; + // Remove all PII from userInfo - only keep non-PII fields if any + delete sanitized.userInfo.userEmail; + delete sanitized.userInfo.userFname; + delete sanitized.userInfo.userLname; + delete sanitized.userInfo.userCountry; + delete sanitized.userInfo.userCity; + delete sanitized.userInfo.userPostalcode; + } + + // Handle invoiceRows array (for batch invoices) + if (sanitized.invoiceRows && Array.isArray(sanitized.invoiceRows)) { + // Limit batch size to prevent quota issues + const limitedRows = sanitized.invoiceRows.slice(0, MAX_PERSISTED_INVOICES); + + sanitized.invoiceRows = limitedRows.map((row) => { + const sanitizedRow = { ...row }; + // Remove all client PII - only keep essential non-PII data + delete sanitizedRow.clientEmail; + delete sanitizedRow.clientFname; + delete sanitizedRow.clientLname; + delete sanitizedRow.clientCountry; + delete sanitizedRow.clientCity; + delete sanitizedRow.clientPostalcode; + // Keep: clientAddress (needed for functionality), itemData, totalAmountDue + return sanitizedRow; + }); + } + + return sanitized; +}; + +/** + * Get maximum number of invoices that can be persisted + */ +export const getMaxPersistedInvoices = () => MAX_PERSISTED_INVOICES; + +/** + * Storage keys + */ +export const StorageKeys = STORAGE_KEYS; + +// Cleanup expired entries on module load +if (typeof window !== 'undefined') { + cleanupExpiredEntries(); +} + From 824e40443a6353731ba628cd9c83ef6363b3bb23 Mon Sep 17 00:00:00 2001 From: ajey35 Date: Sat, 10 Jan 2026 13:10:54 +0530 Subject: [PATCH 08/11] improved-err-handling --- frontend/src/utils/sessionStorage.js | 98 ++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 11 deletions(-) diff --git a/frontend/src/utils/sessionStorage.js b/frontend/src/utils/sessionStorage.js index 6037a164..2eac3cb2 100644 --- a/frontend/src/utils/sessionStorage.js +++ b/frontend/src/utils/sessionStorage.js @@ -32,7 +32,22 @@ export const getFromStorage = (key, defaultValue = null) => { const item = storage.getItem(key); if (!item) return defaultValue; - const parsed = JSON.parse(item); + // Parse JSON with error handling - remove corrupted entries + let parsed; + try { + parsed = JSON.parse(item); + } catch (parseError) { + // Corrupted JSON - remove it from storage + storage.removeItem(key); + console.error(`Corrupted JSON in sessionStorage (${key}), removed:`, parseError); + return defaultValue; + } + + // Treat undefined as missing data + if (parsed === undefined) { + storage.removeItem(key); + return defaultValue; + } // Check if entry has TTL and if it's expired if (parsed.expiresAt && parsed.expiresAt < Date.now()) { @@ -41,40 +56,100 @@ export const getFromStorage = (key, defaultValue = null) => { return defaultValue; } - // Return the actual data (without TTL metadata) - return parsed.data !== undefined ? parsed.data : parsed; + // Return parsed.data if present, otherwise defaultValue + // Treat undefined data as missing to prevent false round-trip of undefined + if (parsed.data !== undefined) { + return parsed.data; + } + + // If parsed.data is undefined, treat as missing + return defaultValue; } catch (error) { console.error(`Error reading from sessionStorage (${key}):`, error); return defaultValue; } }; +/** + * Check if an error is a quota exceeded error + * Handles various browser implementations and error formats + */ +const isQuotaExceededError = (error) => { + if (!error) return false; + + // Check error name (standard) + if (error.name === 'QuotaExceededError' || error.name === 'NS_ERROR_DOM_QUOTA_REACHED') { + return true; + } + + // Check error code (some browsers) + if (error.code === 22 || error.code === 1014) { + return true; + } + + // Check error message (fallback for edge cases) + const message = String(error.message || '').toLowerCase(); + if (message.includes('quota') || message.includes('storage') || message.includes('exceeded')) { + return true; + } + + return false; +}; + /** * Safe sessionStorage setter with TTL * Wraps data with expiry timestamp * Session-only: cleared when browser tab closes + * Includes retry logic for quota exceeded errors */ export const saveToStorage = (key, value, ttl = DEFAULT_TTL) => { try { const storage = getStorage(); if (!storage) return; - const expiresAt = Date.now() + ttl; + // Validate TTL: ensure it's a positive number + const validTtl = typeof ttl === 'number' && ttl > 0 && isFinite(ttl) ? ttl : DEFAULT_TTL; + + const expiresAt = Date.now() + validTtl; const dataToStore = { data: value, expiresAt, createdAt: Date.now(), }; - storage.setItem(key, JSON.stringify(dataToStore)); + const serialized = JSON.stringify(dataToStore); + storage.setItem(key, serialized); } catch (error) { console.error(`Error saving to sessionStorage (${key}):`, error); - // Handle quota exceeded error - if (error.name === 'QuotaExceededError') { - console.warn('sessionStorage quota exceeded. Clearing old data...'); + + // Handle quota exceeded error with retry + if (isQuotaExceededError(error)) { + console.warn('sessionStorage quota exceeded. Clearing old data and retrying...'); + + // Clean up: remove current key and expired entries clearStorage(key); - // Try to clean up expired entries cleanupExpiredEntries(); + + // Retry the write once after cleanup + try { + const storage = getStorage(); + if (!storage) return; + + const validTtl = typeof ttl === 'number' && ttl > 0 && isFinite(ttl) ? ttl : DEFAULT_TTL; + const expiresAt = Date.now() + validTtl; + const dataToStore = { + data: value, + expiresAt, + createdAt: Date.now(), + }; + + const serialized = JSON.stringify(dataToStore); + storage.setItem(key, serialized); + console.info('Successfully saved to sessionStorage after quota cleanup'); + } catch (retryError) { + // If retry also fails, log but don't throw (graceful degradation) + console.error(`Failed to save to sessionStorage after cleanup (${key}):`, retryError); + } } } }; @@ -129,8 +204,8 @@ const cleanupExpiredEntries = () => { const MAX_PERSISTED_INVOICES = 10; /** - * Remove PII (emails, names, addresses, countries) from data object before saving - * Only keeps essential non-PII data needed for form restoration + * Remove *some* PII (emails, names, countries, etc.) from data object before saving. + * Note: `clientAddress` is intentionally retained for form restoration. */ export const sanitizeDataForStorage = (data) => { if (!data || typeof data !== 'object') return data; @@ -138,6 +213,7 @@ export const sanitizeDataForStorage = (data) => { const sanitized = { ...data }; // Remove top-level PII fields + // Note: clientAddress is intentionally retained for form restoration delete sanitized.userEmail; delete sanitized.clientEmail; delete sanitized.userFname; From 4f7d3ea4c6b3a8fa6b8ee6d9a01bbe5fd576d9ae Mon Sep 17 00:00:00 2001 From: ajey35 Date: Tue, 20 Jan 2026 22:29:49 +0530 Subject: [PATCH 09/11] resolved-conflicts --- frontend/src/page/CreateInvoice.jsx | 131 +++++++++++----------------- 1 file changed, 51 insertions(+), 80 deletions(-) diff --git a/frontend/src/page/CreateInvoice.jsx b/frontend/src/page/CreateInvoice.jsx index ee5f760f..93d6100b 100644 --- a/frontend/src/page/CreateInvoice.jsx +++ b/frontend/src/page/CreateInvoice.jsx @@ -180,93 +180,63 @@ function CreateInvoice() { } const processUrlToken = async () => { - if (!urlTokenAddress || loadingTokens) return; - - if (isCustomFromURL) { - setUseCustomToken(true); - setCustomTokenAddress(urlTokenAddress); - verifyToken(urlTokenAddress); - return; - } - - // Try to find token in saved selection first - const savedData = getFromStorage(StorageKeys.CREATE_INVOICE); - if (savedData?.selectedToken) { - const token = savedData.selectedToken; - if ( - token.address?.toLowerCase() === urlTokenAddress.toLowerCase() - ) { - setSelectedToken(token); - setUseCustomToken(false); - return; - } - } + if (urlTokenAddress && !loadingTokens) { + if (isCustomFromURL) { + setUseCustomToken(true); + setCustomTokenAddress(urlTokenAddress); + verifyToken(urlTokenAddress); + } else { + const preselectedToken = tokens.find( + (token) => + (token.contract_address || token.address).toLowerCase() === urlTokenAddress.toLowerCase() + ); - // Try to find from tokens list - const preselectedToken = tokens.find( - (token) => - (token.contract_address || token.address).toLowerCase() === - urlTokenAddress.toLowerCase() - ); + if (preselectedToken) { + let decimals = preselectedToken.decimals; + + // If decimals are missing/null, try to fetch them from chain + if (decimals === undefined || decimals === null) { + try { + if (typeof window !== "undefined" && window.ethereum) { + const provider = new BrowserProvider(window.ethereum); + const contract = new ethers.Contract(urlTokenAddress, ERC20_ABI, provider); + // Try to fetch decimals + decimals = await contract.decimals(); + } + } catch (err) { + console.warn("Failed to fetch decimals for preselected token:", err); + } + } - if (preselectedToken) { - let decimals = preselectedToken.decimals; - - // If decimals are missing/null, try to fetch them from chain - if (decimals === undefined || decimals === null) { - try { - if (typeof window !== "undefined" && window.ethereum) { - const provider = new BrowserProvider(window.ethereum); - const contract = new ethers.Contract( - urlTokenAddress, - ERC20_ABI, - provider - ); - decimals = await contract.decimals(); + // If we successfully resolved decimals (from list or chain) + if (decimals !== undefined && decimals !== null) { + setSelectedToken({ + address: preselectedToken.contract_address || preselectedToken.address, + symbol: preselectedToken.symbol, + name: preselectedToken.name, + logo: preselectedToken.image, + decimals: Number(decimals) + }); + setUseCustomToken(false); + } else { + // Fallback to manual verification if we can't determine decimals safely + console.warn("Could not determine token decimals, falling back to manual verification."); + setUseCustomToken(true); + setCustomTokenAddress(urlTokenAddress); + verifyToken(urlTokenAddress); } - } catch (err) { - console.warn( - "Failed to fetch decimals for preselected token:", - err - ); + } else { + // Not in list, treat as custom + setUseCustomToken(true); + setCustomTokenAddress(urlTokenAddress); + verifyToken(urlTokenAddress); } } - - if (decimals !== undefined && decimals !== null) { - setSelectedToken({ - address: - preselectedToken.contract_address || preselectedToken.address, - symbol: preselectedToken.symbol, - name: preselectedToken.name, - logo: preselectedToken.image, - decimals: Number(decimals), - }); - setUseCustomToken(false); - } else { - // Fallback to manual verification if we can't determine decimals safely - console.warn( - "Could not determine token decimals, falling back to manual verification." - ); - setUseCustomToken(true); - setCustomTokenAddress(urlTokenAddress); - verifyToken(urlTokenAddress); - } - } else { - // Not in list, treat as custom - setUseCustomToken(true); - setCustomTokenAddress(urlTokenAddress); - verifyToken(urlTokenAddress); } }; processUrlToken(); - }, [ - isInitialLoad, - searchParams, - tokens, - loadingTokens, - account.address, - ]); + }, [searchParams, walletClient, tokens, loadingTokens, account.address, isInitialLoad]); useEffect(() => { const total = itemData.reduce((sum, item) => { @@ -355,8 +325,7 @@ function CreateInvoice() { customTokenAddress, useCustomToken, verifiedToken, - // Use JSON.stringify to detect deep changes in itemData - JSON.stringify(itemData), + itemData, isInitialLoad, ]); @@ -584,6 +553,8 @@ const verifyToken = async (address, targetChainId = null) => { if (!account?.chainId) { throw new Error("Missing chainId: wallet connected but chain not configured"); } + + const chainId = account?.chainId; const contractAddress = import.meta.env[ `VITE_CONTRACT_ADDRESS_${account.chainId}` ]; From 04178c90fd02762d4079eb2b5c89c63703600bfa Mon Sep 17 00:00:00 2001 From: ajey35 Date: Tue, 20 Jan 2026 22:56:06 +0530 Subject: [PATCH 10/11] added-persitant-storage-for-generatelink-comp --- frontend/src/page/GenerateLink.jsx | 65 ++++++++++++++++++++++++++++ frontend/src/utils/sessionStorage.js | 1 + 2 files changed, 66 insertions(+) diff --git a/frontend/src/page/GenerateLink.jsx b/frontend/src/page/GenerateLink.jsx index 40b02e9b..d945142c 100644 --- a/frontend/src/page/GenerateLink.jsx +++ b/frontend/src/page/GenerateLink.jsx @@ -24,6 +24,12 @@ import { Badge } from "@/components/ui/badge"; import { BrowserProvider, ethers } from "ethers"; import { ERC20_ABI } from "@/contractsABI/ERC20_ABI"; import { useTokenList } from "../hooks/useTokenList"; +import { + getFromStorage, + saveToStorage, + StorageKeys, + sanitizeDataForStorage, +} from "@/utils/sessionStorage"; const GenerateLink = () => { const { address, isConnected, chainId } = useAccount(); @@ -43,6 +49,7 @@ const GenerateLink = () => { const [tokenVerificationState, setTokenVerificationState] = useState("idle"); const [verifiedToken, setVerifiedToken] = useState(null); const [loading, setLoading] = useState(false); + const [isInitialLoad, setIsInitialLoad] = useState(true); // Set default token when tokens are loaded useEffect(() => { @@ -64,6 +71,64 @@ const GenerateLink = () => { setShowWalletAlert(!isConnected); }, [isConnected]); + // Load persisted data from sessionStorage on mount + useEffect(() => { + const savedData = getFromStorage(StorageKeys.GENERATE_LINK); + + if (savedData) { + if (savedData.amount) setAmount(savedData.amount); + if (savedData.description) setDescription(savedData.description); + + if (savedData.selectedToken) { + setSelectedToken(savedData.selectedToken); + setUseCustomToken(false); + } + + if (savedData.useCustomToken && savedData.customTokenAddress) { + setUseCustomToken(true); + setCustomTokenAddress(savedData.customTokenAddress); + + if (savedData.verifiedToken) { + setVerifiedToken(savedData.verifiedToken); + setTokenVerificationState("success"); + } + } + } + + setIsInitialLoad(false); + }, []); + + // Persist form state to sessionStorage (debounced) + useEffect(() => { + if (isInitialLoad) return; + + const saveData = () => { + const dataToSave = sanitizeDataForStorage({ + amount, + description, + selectedToken, + customTokenAddress, + useCustomToken, + verifiedToken, + chainId, + }); + + saveToStorage(StorageKeys.GENERATE_LINK, dataToSave); + }; + + const timeoutId = setTimeout(saveData, 500); + return () => clearTimeout(timeoutId); + }, [ + amount, + description, + selectedToken, + customTokenAddress, + useCustomToken, + verifiedToken, + chainId, + isInitialLoad, + ]); + const generateLink = () => { const tokenToUse = useCustomToken ? verifiedToken : selectedToken; diff --git a/frontend/src/utils/sessionStorage.js b/frontend/src/utils/sessionStorage.js index 2eac3cb2..00184728 100644 --- a/frontend/src/utils/sessionStorage.js +++ b/frontend/src/utils/sessionStorage.js @@ -6,6 +6,7 @@ const STORAGE_KEYS = { CREATE_INVOICE: 'chainvoice_create_invoice', CREATE_INVOICES_BATCH: 'chainvoice_create_invoices_batch', + GENERATE_LINK: 'chainvoice_generate_link', }; // Default TTL: 24 hours (in milliseconds) From 5deb16585ce4423a71a34c4598e1a247a5728403 Mon Sep 17 00:00:00 2001 From: ajey35 Date: Tue, 20 Jan 2026 23:15:16 +0530 Subject: [PATCH 11/11] Resolved-code-rabbit-suggested --- frontend/src/page/GenerateLink.jsx | 92 +++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/frontend/src/page/GenerateLink.jsx b/frontend/src/page/GenerateLink.jsx index d945142c..c19266aa 100644 --- a/frontend/src/page/GenerateLink.jsx +++ b/frontend/src/page/GenerateLink.jsx @@ -50,6 +50,7 @@ const GenerateLink = () => { const [verifiedToken, setVerifiedToken] = useState(null); const [loading, setLoading] = useState(false); const [isInitialLoad, setIsInitialLoad] = useState(true); + const [savedTokenState, setSavedTokenState] = useState(null); // Set default token when tokens are loaded useEffect(() => { @@ -79,24 +80,99 @@ const GenerateLink = () => { if (savedData.amount) setAmount(savedData.amount); if (savedData.description) setDescription(savedData.description); - if (savedData.selectedToken) { - setSelectedToken(savedData.selectedToken); + // Defer token restoration until we can validate against current chain/token list + const tokenState = { + selectedToken: savedData.selectedToken || null, + useCustomToken: !!savedData.useCustomToken, + customTokenAddress: savedData.customTokenAddress || "", + verifiedToken: savedData.verifiedToken || null, + savedChainId: savedData.chainId ?? null, + }; + setSavedTokenState(tokenState); + } + + setIsInitialLoad(false); + }, []); + + // Validate and restore token state against current chain/token list + useEffect(() => { + if (!savedTokenState) return; + + const clearTokenState = () => { + setSelectedToken(null); + setUseCustomToken(false); + setCustomTokenAddress(""); + setVerifiedToken(null); + setTokenVerificationState("idle"); + }; + + const currentChainId = chainId || 1; + const chainMatches = + !savedTokenState.savedChainId || + savedTokenState.savedChainId === currentChainId; + + // If the saved data was for a different chain, do not restore token state + if (!chainMatches) { + clearTokenState(); + return; + } + + // 1. Try to restore a listed token, but only if it still exists for this chain + if (savedTokenState.selectedToken) { + const savedAddress = + (savedTokenState.selectedToken.address || + savedTokenState.selectedToken.contract_address || + "").toLowerCase(); + + const matchingToken = tokens.find((token) => { + const tokenAddress = + (token.contract_address || token.address || "").toLowerCase(); + return tokenAddress === savedAddress; + }); + + if (matchingToken) { setUseCustomToken(false); + setSelectedToken({ + address: matchingToken.contract_address, + symbol: matchingToken.symbol, + name: matchingToken.name, + logo: matchingToken.image, + decimals: matchingToken.decimals ?? 18, + }); + return; } - if (savedData.useCustomToken && savedData.customTokenAddress) { + // Saved selectedToken no longer valid on this chain + clearTokenState(); + return; + } + + // 2. Try to restore a custom token if the address is valid + if (savedTokenState.useCustomToken && savedTokenState.customTokenAddress) { + const addr = savedTokenState.customTokenAddress; + + if (ethers.isAddress(addr)) { setUseCustomToken(true); - setCustomTokenAddress(savedData.customTokenAddress); + setCustomTokenAddress(addr); - if (savedData.verifiedToken) { - setVerifiedToken(savedData.verifiedToken); + if (savedTokenState.verifiedToken) { + setVerifiedToken(savedTokenState.verifiedToken); setTokenVerificationState("success"); + } else { + setVerifiedToken(null); + setTokenVerificationState("idle"); } + return; } + + // Invalid custom token address for this chain; clear state + clearTokenState(); + return; } - setIsInitialLoad(false); - }, []); + // No valid token information to restore + clearTokenState(); + }, [savedTokenState, tokens, chainId]); // Persist form state to sessionStorage (debounced) useEffect(() => {