Skip to content

Conversation

@ajey35
Copy link
Contributor

@ajey35 ajey35 commented Jan 10, 2026

Fix: Add Session Storage Persistence for Invoice Forms

Issue

Form data was lost on page refresh in CreateInvoice and CreateInvoicesBatch pages, causing users to lose their work.

Solution

  • Created Session Storage utility with safe get/set/clear functions
  • Added auto-save on form field changes (500ms debounce)
  • Restored saved data on component mount
  • Cleared storage after successful submission
  • Made all form inputs controlled components

Changes

  • src/utils/session-storage.js - New utility module
  • src/page/CreateInvoice.jsx - Added persistence for all form fields
  • src/page/CreateInvoicesBatch.jsx - Added persistence for batch invoice data

Result

Users can now refresh the page without losing their form data. Data is automatically cleared after successful invoice creation.

Summary by CodeRabbit

  • New Features

    • Single, batch and link forms now auto-save and restore drafts (fields, dates, items, token choices).
    • New client-side storage utilities added for safe draft persistence with TTL, PII sanitization, and a max-draft limit.
  • Improvements

    • Debounced persistence with an initial-load guard to avoid premature saves.
    • URL parameters can override saved drafts; verified custom-token flow supported.
    • Drafts cleared after successful submission; form fields fully controlled and preserved.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

Adds TTL-backed sessionStorage/localStorage draft persistence with PII sanitization for single, batch, and link flows; restores drafts on mount, debounced saves after initial load, integrates URL-driven token/client overrides and token verification, and clears drafts on successful submission.

Changes

Cohort / File(s) Summary
Storage utilities
frontend/src/utils/localStorage.js, frontend/src/utils/sessionStorage.js
New TTL-aware storage modules exporting getFromStorage, saveToStorage, clearStorage, sanitizeDataForStorage, getMaxPersistedInvoices, and StorageKeys; enforce expiry, PII sanitization, quota handling, cleanup on load, and MAX_PERSISTED_INVOICES (10).
Single Invoice Form
frontend/src/page/CreateInvoice.jsx
Restore sanitized CREATE_INVOICE draft on mount; add isInitialLoad guard; convert user/client fields to controlled state; add debounced persistence and URL clientAddress/tokenAddress overrides; integrate custom-token flow and token verification; clear storage on successful submit.
Batch Invoice Form
frontend/src/page/CreateInvoicesBatch.jsx
Restore/persist CREATE_INVOICES_BATCH draft on mount with isInitialLoad guard and 500ms debounce; persist dates, userInfo, invoiceRows, token selection/verification; honor max persisted invoices limit; clear storage after successful batch submission.
Generate Link Form
frontend/src/page/GenerateLink.jsx
Restore/persist generate-link draft (amount, description, token selection, custom-token state, verification) with isInitialLoad guard and debounced saves; validate token/chain compatibility on restore.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Browser UI
  participant Storage as Storage Util
  participant Token as Token Verifier
  participant Backend as Submit Service

  UI->>Storage: load sanitized draft (key)
  Storage-->>UI: return draft or null
  UI->>UI: restore fields (dates, user/client, items, token state)
  alt tokenAddress or custom token present
    UI->>Token: verify token address / resolve decimals
    Token-->>UI: verification result
  end
  UI->>Storage: debounce save sanitized draft
  Storage-->>Storage: persist {data, expiresAt, createdAt}
  UI->>Backend: submit invoice(s) (payload includes user/client, items, token)
  Backend-->>UI: success
  UI->>Storage: clear relevant storage key
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • adityabhattad2021

Poem

🐰 I nibble bytes and tuck drafts tight,

Dates and tokens saved throughout the night.
Items return when you open the door,
Cleared on submit — no drafts anymore.
🥕 — From a rabbit who saves your store.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding persistent session storage for form inputs across multiple pages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/page/CreateInvoice.jsx (2)

166-204: Normalize token state to avoid “selectedToken + customToken” stale mixes.

In the URL override path you may set selectedToken without clearing customTokenAddress/verifiedToken (and vice versa). That can rehydrate later into a confusing state.

Proposed diff
          if (
            token.address?.toLowerCase() === urlTokenAddress.toLowerCase()
          ) {
            setSelectedToken(token);
            setUseCustomToken(false);
+           setCustomTokenAddress("");
+           setVerifiedToken(null);
+           setTokenVerificationState("idle");
            return;
          }
...
        setUseCustomToken(true);
        setCustomTokenAddress(urlTokenAddress);
+       setSelectedToken(null);
        verifyToken(urlTokenAddress);

Also applies to: 238-295


483-490: Guard chainId explicitly before env lookup for clearer failures.

Right now an undefined chainId becomes VITE_CONTRACT_ADDRESS_undefined and surfaces as “Unsupported network” without explaining what’s missing.

Proposed diff
-      const chainId = account?.chainId;
+      const chainId = account?.chainId;
+      if (!chainId) {
+        throw new Error("Wallet network not detected");
+      }
       const contractAddress = import.meta.env[
         `VITE_CONTRACT_ADDRESS_${chainId}`
       ];
frontend/src/page/CreateInvoicesBatch.jsx (1)

563-570: Add explicit chainId guard before env lookup.

Same as CreateInvoice—fail with a clearer message when chainId isn’t available.

🤖 Fix all issues with AI agents
In @frontend/src/page/CreateInvoice.jsx:
- Around line 49-54: The code is persisting PII (names, emails, addresses) to
localStorage via saveToStorage/getFromStorage/StorageKeys in CreateInvoice.jsx
(also appearing in the other ranges); change the storage strategy to avoid
long-lived localStorage by either switching these calls to sessionStorage,
adding a TTL wrapper that expires entries on read, and/or stop persisting email
fields altogether. Update the localStorage helper functions (getFromStorage,
saveToStorage, clearStorage) or add new session/ttl variants so reads check
expiry and deletes expired entries, change callers in CreateInvoice.jsx (the
places using StorageKeys around lines 49, 111-165, 238-295) to use the new safe
API, and ensure any persisted invoice data is minimized (no emails) or encrypted
before persisting.

In @frontend/src/page/CreateInvoicesBatch.jsx:
- Around line 57-62: The component currently persists large batches of client
PII using getFromStorage/saveToStorage (StorageKeys) which can hit localStorage
quotas and expose sensitive data; change persistence to use session-scoped
storage with TTL (implement new saveToSession/getFromSession helpers or extend
existing utils to accept a storage type and expiresAt), avoid storing email
fields and other direct PII (strip email/name/country from the saved batch
payload before calling saveToStorage/saveToSession), and enforce a cap on
persisted batch size (e.g., truncate to N items) when preparing data in
CreateInvoicesBatch.jsx and where the component calls
saveToStorage/getFromStorage/clearStorage so the persisted object is smaller and
ephemeral.
- Around line 142-177: When restoring token state in the useEffect, make the
branches mutually exclusive: if savedData.useCustomToken is true prefer the
custom path (call setUseCustomToken(true),
setCustomTokenAddress(savedData.customTokenAddress),
setVerifiedToken(savedData.verifiedToken ?? null) and
setTokenVerificationState(savedData.verifiedToken ? "success" : "idle")), and
explicitly clear the selected-token fields (setSelectedToken(null)); otherwise,
prefer the saved selectedToken (setSelectedToken(savedData.selectedToken) and
setUseCustomToken(false)) and explicitly clear custom-token fields
(setCustomTokenAddress("") , setVerifiedToken(null),
setTokenVerificationState("idle")) so mixed/ambiguous saved payloads cannot
produce hybrid state.
🧹 Nitpick comments (3)
frontend/src/utils/localStorage.js (2)

5-8: Freeze exported keys to prevent accidental mutation.

Right now StorageKeys can be mutated by consumers.

Proposed diff
 const STORAGE_KEYS = {
   CREATE_INVOICE: 'chainvoice_create_invoice',
   CREATE_INVOICES_BATCH: 'chainvoice_create_invoices_batch',
 };
+Object.freeze(STORAGE_KEYS);

 /**
  * Storage keys
  */
 export const StorageKeys = STORAGE_KEYS;

Also applies to: 56-57


27-39: Make quota detection more robust + consider defining clearStorage before saveToStorage.

QuotaExceededError isn’t consistent across browsers; also calling saveToStorage during a circular import can theoretically hit clearStorage before initialization.

Proposed diff
+/**
+ * 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);
+  }
+};
+
 /**
  * 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') {
+    const isQuotaExceeded =
+      error?.name === 'QuotaExceededError' ||
+      error?.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
+      error?.code === 22 ||
+      error?.code === 1014;
+    if (isQuotaExceeded) {
       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);
-  }
-};

Also applies to: 44-51

frontend/src/page/CreateInvoice.jsx (1)

516-538: Remove unused FormData (you’re fully controlled now).

const formData = new FormData(e.target); is dead code.

Proposed diff
   const handleSubmit = async (e) => {
     e.preventDefault();
-    const formData = new FormData(e.target);
-
     const data = {
       userAddress: account?.address?.toString(),
       userFname,
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a04a338 and abd0a9b.

📒 Files selected for processing (3)
  • frontend/src/page/CreateInvoice.jsx
  • frontend/src/page/CreateInvoicesBatch.jsx
  • frontend/src/utils/localStorage.js
🔇 Additional comments (3)
frontend/src/page/CreateInvoice.jsx (2)

503-506: Clearing persisted draft only on confirmed success looks correct.


661-747: Controlled inputs conversion looks solid.

Good: avoids uncontrolled→controlled warnings via || "" on item fields and keeps state as source of truth.

Also applies to: 769-855, 1109-1169, 1229-1271

frontend/src/page/CreateInvoicesBatch.jsx (1)

585-587: Clearing persisted batch draft after confirmation is correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
frontend/src/page/CreateInvoicesBatch.jsx (2)

142-179: Add error handling for data restoration.

The restoration logic lacks error handling for corrupted localStorage data. Invalid date strings, malformed JSON, or incompatible schema changes could cause runtime errors.

🛡️ Proposed fix with error handling
  // Load data from localStorage on mount
  useEffect(() => {
+   try {
      const savedData = getFromStorage(StorageKeys.CREATE_INVOICES_BATCH);
      if (savedData) {
        // Restore dates
        if (savedData.dueDate) {
-         setDueDate(new Date(savedData.dueDate));
+         const dueDate = new Date(savedData.dueDate);
+         if (!isNaN(dueDate.getTime())) {
+           setDueDate(dueDate);
+         }
        }
        if (savedData.issueDate) {
-         setIssueDate(new Date(savedData.issueDate));
+         const issueDate = new Date(savedData.issueDate);
+         if (!isNaN(issueDate.getTime())) {
+           setIssueDate(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");
          }
        }
      }
+   } catch (error) {
+     console.error("Failed to restore form data:", error);
+     // Clear corrupted data
+     clearStorage(StorageKeys.CREATE_INVOICES_BATCH);
+   }
    setIsInitialLoad(false);
  }, []);

200-233: Optimize dependency array to avoid unnecessary effect evaluations.

Using JSON.stringify(userInfo) and JSON.stringify(invoiceRows) in the dependency array creates new string values on every render, causing React to re-evaluate the effect even when the underlying data hasn't changed. While the debounce prevents excessive saves, the effect closure and timeout are recreated unnecessarily.

Consider using a custom deep-comparison hook or separate the persistence logic:

♻️ Alternative approach with useRef
+ const prevDataRef = useRef();
+
  // Save form data to localStorage (debounced)
  useEffect(() => {
    if (isInitialLoad) return;

+   const currentData = {
+     dueDate: dueDate?.toISOString(),
+     issueDate: issueDate?.toISOString(),
+     userInfo,
+     invoiceRows,
+     selectedToken,
+     customTokenAddress,
+     useCustomToken,
+     verifiedToken,
+   };
+
+   // Skip if data hasn't actually changed
+   if (JSON.stringify(currentData) === JSON.stringify(prevDataRef.current)) {
+     return;
+   }
+   prevDataRef.current = currentData;
+
    const saveData = () => {
-     const dataToSave = {
-       dueDate: dueDate?.toISOString(),
-       issueDate: issueDate?.toISOString(),
-       userInfo,
-       invoiceRows,
-       selectedToken,
-       customTokenAddress,
-       useCustomToken,
-       verifiedToken,
-     };
-
-     saveToStorage(StorageKeys.CREATE_INVOICES_BATCH, dataToSave);
+     saveToStorage(StorageKeys.CREATE_INVOICES_BATCH, currentData);
    };

    // Debounce save operations
    const timeoutId = setTimeout(saveData, 500);
    return () => clearTimeout(timeoutId);
  }, [
    dueDate,
    issueDate,
-   // Use JSON.stringify to detect deep changes in nested objects
-   JSON.stringify(userInfo),
-   JSON.stringify(invoiceRows),
+   userInfo,
+   invoiceRows,
    selectedToken,
    customTokenAddress,
    useCustomToken,
    verifiedToken,
    isInitialLoad,
  ]);

Note: This still has the issue of object reference changes, but moves the deep comparison inside the effect where it only runs when shallow dependencies change.

frontend/src/page/CreateInvoice.jsx (3)

112-164: Add error handling and validate restored dates.

Same as batch file: restoration lacks error handling for corrupted data. Additionally, the logic correctly prioritizes URL parameters over saved client data.

🛡️ Proposed fix with error handling
  // Load data from localStorage on mount
  useEffect(() => {
+   try {
      const savedData = getFromStorage(StorageKeys.CREATE_INVOICE);
      if (savedData) {
        // Restore dates
        if (savedData.dueDate) {
-         setDueDate(new Date(savedData.dueDate));
+         const dueDate = new Date(savedData.dueDate);
+         if (!isNaN(dueDate.getTime())) {
+           setDueDate(dueDate);
+         }
        }
        if (savedData.issueDate) {
-         setIssueDate(new Date(savedData.issueDate));
+         const issueDate = new Date(savedData.issueDate);
+         if (!isNaN(issueDate.getTime())) {
+           setIssueDate(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);
        }
      }
+   } catch (error) {
+     console.error("Failed to restore form data:", error);
+     clearStorage(StorageKeys.CREATE_INVOICE);
+   }
    setIsInitialLoad(false);
  }, []);

179-201: Redundant localStorage read for token matching.

Lines 186-196 re-read from localStorage to check if savedData.selectedToken matches the URL token, but selectedToken is already available in state from the restoration effect (lines 145-148). This is redundant and could use the state directly.

♻️ Proposed refactor using existing state
    if (urlTokenAddress) {
      if (isCustomFromURL) {
        setUseCustomToken(true);
        setCustomTokenAddress(urlTokenAddress);
        verifyToken(urlTokenAddress);
      } else {
-       // 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;
-         }
+       // Check if current selectedToken matches URL token
+       if (
+         selectedToken?.address?.toLowerCase() === urlTokenAddress.toLowerCase()
+       ) {
+         // Already set from localStorage, no need to update
+         setUseCustomToken(false);
+         return;
        }
        // If not found, try custom token verification
        setUseCustomToken(true);
        setCustomTokenAddress(urlTokenAddress);
        verifyToken(urlTokenAddress);
      }
    }

238-295: Optimize dependency array to avoid unnecessary effect evaluations.

Same issue as batch file: JSON.stringify(itemData) in the dependency array creates new strings on every render, causing unnecessary effect re-evaluation. The large dependency array with many individual fields also triggers frequently.

Consider the same useRef-based approach as suggested for the batch file, or explore using a library like use-deep-compare-effect for cleaner deep comparison.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abd0a9b and 2bf85d5.

📒 Files selected for processing (2)
  • frontend/src/page/CreateInvoice.jsx
  • frontend/src/page/CreateInvoicesBatch.jsx
🔇 Additional comments (8)
frontend/src/page/CreateInvoicesBatch.jsx (4)

564-567: LGTM! Network-specific contract resolution.

Correctly derives chainId from the connected account for network-specific contract address resolution.


586-587: LGTM! Proper cleanup on success.

Correctly clears persisted form data after successful batch submission to reset the draft state.


84-84: LGTM! Proper guard pattern.

The isInitialLoad flag correctly prevents saving to storage during the initial data restoration phase.


57-62: localStorage utilities already implement comprehensive error handling.

The imported utilities properly handle quota exceeded errors (with QuotaExceededError detection and old data clearing), JSON parse failures (graceful fallback to defaultValue), and SecurityErrors from private browsing (caught by general error handlers). No changes required.

frontend/src/page/CreateInvoice.jsx (4)

71-83: LGTM! Proper controlled component state.

User and client form fields are now explicit state variables with controlled inputs, enabling proper persistence and state management.


484-487: LGTM! Network-specific contract resolution.

Correctly derives chainId from the connected account, matching the pattern in the batch file.


505-506: LGTM! Proper cleanup on success.

Correctly clears persisted form data after successful invoice creation.


667-668: LGTM! Properly implemented controlled components.

All form inputs are correctly wired as controlled components with explicit value and onChange props, enabling reliable persistence and state management.

Also applies to: 680-681, 696-697, 731-732, 744-745, 775-776, 788-789, 804-805, 839-840, 852-853, 1111-1270

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @frontend/src/page/CreateInvoice.jsx:
- Around line 215-268: The useEffect debounced auto-save is missing userEmail
and clientEmail in its dependency array causing stale saves; update the
dependency array for the effect that defines saveData (the useEffect wrapping
saveToStorage/ sanitizeDataForStorage and StorageKeys.CREATE_INVOICE) to include
userEmail and clientEmail so changes to those fields trigger the debounced save
along with the existing dependencies.
🧹 Nitpick comments (3)
frontend/src/utils/localStorage.js (1)

55-78: Consider retrying the save operation after cleanup.

When QuotaExceededError occurs, the function clears the specific key and expired entries but doesn't retry saving. This means the user's data won't be persisted even after freeing space. Consider adding a retry mechanism or at least logging a warning that the save failed.

💾 Proposed retry logic
 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();
+      
+      // Retry once after cleanup
+      try {
+        const expiresAt = Date.now() + ttl;
+        const dataToStore = {
+          data: value,
+          expiresAt,
+          createdAt: Date.now(),
+        };
+        storage.setItem(key, JSON.stringify(dataToStore));
+      } catch (retryError) {
+        console.error('Failed to save after cleanup:', retryError);
+      }
     }
   }
 };
frontend/src/page/CreateInvoicesBatch.jsx (1)

138-167: Fix formatting issue.

Line 166 has trailing whitespace: setIsInitialLoad(false );

🧹 Fix formatting
-    setIsInitialLoad(false    );
+    setIsInitialLoad(false);
frontend/src/page/CreateInvoice.jsx (1)

476-478: Update comment to reference sessionStorage.

The comment on line 477 mentions "localStorage" but the code uses sessionStorage throughout the application.

📝 Fix comment
-      // Clear localStorage on successful submission
+      // Clear sessionStorage on successful submission
       clearStorage(StorageKeys.CREATE_INVOICE);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf85d5 and 9bba82f.

📒 Files selected for processing (3)
  • frontend/src/page/CreateInvoice.jsx
  • frontend/src/page/CreateInvoicesBatch.jsx
  • frontend/src/utils/localStorage.js
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/page/CreateInvoice.jsx (1)
frontend/src/utils/localStorage.js (11)
  • getFromStorage (26-49)
  • getFromStorage (26-49)
  • StorageKeys (194-194)
  • StorageKeys (194-194)
  • sanitized (136-136)
  • sanitizeDataForStorage (133-184)
  • sanitizeDataForStorage (133-184)
  • saveToStorage (55-78)
  • saveToStorage (55-78)
  • clearStorage (83-91)
  • clearStorage (83-91)
🔇 Additional comments (16)
frontend/src/utils/localStorage.js (7)

1-20: LGTM!

The use of sessionStorage with browser environment checks and TTL-based expiry is appropriate for temporary draft persistence. The 24-hour TTL provides a good balance between persistence and automatic cleanup.


26-49: LGTM!

The TTL expiry check with automatic deletion of expired entries on read is well-implemented. The fallback to return parsed directly (line 44) provides backward compatibility for data stored without the TTL wrapper.


83-91: LGTM!

The clearStorage function is straightforward with appropriate error handling.


96-124: LGTM!

The cleanup function correctly collects keys to remove first (lines 101-117) before removing them (line 120), avoiding iteration issues when modifying storage during traversal.


133-184: Verify that persisting wallet addresses aligns with privacy requirements.

The sanitization removes most PII (emails, names, addresses, countries, cities, postal codes) but retains clientAddress (wallet addresses) as noted in line 178. While wallet addresses are necessary for invoice functionality, they could be considered user identifiers under privacy regulations like GDPR.

Please confirm this is an intentional design decision and that wallet addresses are acceptable to persist in sessionStorage.


189-194: LGTM!

The exported getters provide clean access to internal constants.


196-199: LGTM!

Automatic cleanup on module load helps maintain sessionStorage hygiene. The browser environment check ensures it doesn't run in SSR contexts.

frontend/src/page/CreateInvoicesBatch.jsx (3)

56-63: LGTM!

The storage utility imports are clean and include all necessary functions for the persistence feature.


187-225: LGTM!

The debounced persistence with isInitialLoad guard prevents overwriting during initial data restoration. The warning when batch size exceeds the persistence limit (lines 204-208) provides helpful feedback.


567-567: LGTM!

Clearing the persisted draft after successful submission prevents stale data from being restored on future visits.

frontend/src/page/CreateInvoice.jsx (6)

49-55: LGTM!

The storage utility imports include all necessary functions for single invoice persistence.


72-84: LGTM!

The controlled component states for user and client fields are properly initialized, and the isInitialLoad guard flag is correctly set up.

Also applies to: 110-110


112-145: LGTM!

The data restoration logic correctly:

  • Restores dates, itemData, and token state
  • Skips restoring clientAddress when provided via URL (lines 122-125)
  • Doesn't restore PII fields (user/client names, emails) as they're sanitized before storage

147-180: LGTM!

The URL parameter handling with isInitialLoad guard ensures saved data is restored before URL params are applied. The logic to reuse saved token data when it matches the URL token (lines 164-174) is efficient and preserves additional token metadata.


494-510: LGTM!

The form data collection correctly gathers all state variables for submission, matching the payload structure expected by createInvoiceRequest.


639-640: LGTM!

All form inputs have been properly converted to controlled components with correct value bindings and onChange handlers. This ensures the persistence feature works correctly for all fields.

Also applies to: 652-653, 668-669, 703-704, 716-717, 747-748, 760-761, 776-777, 811-812, 824-825, 1081-1241

@ajey35 ajey35 force-pushed the feat/added-persistant-storage-inputs branch from 9bba82f to 2bf85d5 Compare January 10, 2026 06:38
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @frontend/src/utils/sessionStorage.js:
- Around line 131-186: The docstring for sanitizeDataForStorage claims addresses
are removed but the function still preserves clientAddress in invoiceRows;
either update the docstring/AI-summary to state clientAddress is intentionally
retained, or actually strip/pseudonymize it: in sanitizeDataForStorage (and
where invoiceRows is handled) remove sanitizedRow.clientAddress (or replace it
with a minimal non-PII surrogate like clientAddressHash) and update any callers
that rely on clientAddress; keep MAX_PERSISTED_INVOICES and invoiceRows logic
intact and ensure userInfo and top-level address fields are treated consistently
with the chosen option.
- Around line 27-50: getFromStorage currently leaves corrupted JSON in storage
and treats stored undefined ambiguously; update getFromStorage to catch
JSON.parse errors and in that catch call storage.removeItem(key) before logging
and returning defaultValue, and when parsed === undefined treat that as missing
by returning defaultValue (or parsed.data if defined) so saving undefined
doesn’t falsely round-trip; use getStorage(), storage.getItem(key),
storage.removeItem(key) and ensure the function returns parsed.data when
present, otherwise defaultValue for undefined or corrupted entries.
🧹 Nitpick comments (1)
frontend/src/utils/sessionStorage.js (1)

6-20: Make StorageKeys immutable + make getStorage() resilient to blocked storage.
Today, any consumer can mutate StorageKeys, and in some browser/privacy modes accessing window.sessionStorage can throw.

Proposed diff
 const STORAGE_KEYS = {
   CREATE_INVOICE: 'chainvoice_create_invoice',
   CREATE_INVOICES_BATCH: 'chainvoice_create_invoices_batch',
 };
+Object.freeze(STORAGE_KEYS);

 // 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;
+  try {
+    return window.sessionStorage;
+  } catch {
+    return null;
+  }
 };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08f36f3 and 6618a15.

📒 Files selected for processing (1)
  • frontend/src/utils/sessionStorage.js
🔇 Additional comments (2)
frontend/src/utils/sessionStorage.js (2)

98-126: Cleanup logic is solid (collect-then-remove avoids iteration pitfalls).
No concerns with the current approach.


191-201: Module-load cleanup guard looks good for SSR.
With Object.freeze(STORAGE_KEYS) above, the exported StorageKeys surface is safer too.

@ajey35 ajey35 changed the title Feat/added persistant storage (localstorage) Inputs Feat/added persistant storage (session-storage) Inputs Jan 10, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
frontend/src/utils/sessionStorage.js (3)

105-155: Refactor to eliminate code duplication and reconsider retry strategy.

The data preparation logic (lines 111-121) is duplicated in the retry block (lines 138-147). Additionally, line 130 calls clearStorage(key) before retrying, which will delete the existing data for that key. If the retry fails, the user's previous data is lost.

Consider:

  1. Extract the data preparation logic into a helper function
  2. Verify whether clearing the current key before retry is intentional—it may be better to only clean expired entries and retry without deleting the target key
♻️ Proposed refactor to eliminate duplication
+/**
+ * Prepare data for storage with TTL
+ */
+const prepareDataForStorage = (value, ttl) => {
+  const validTtl = typeof ttl === 'number' && ttl > 0 && isFinite(ttl) ? ttl : DEFAULT_TTL;
+  const expiresAt = Date.now() + validTtl;
+  return {
+    data: value,
+    expiresAt,
+    createdAt: Date.now(),
+  };
+};
+
 export const saveToStorage = (key, value, ttl = DEFAULT_TTL) => {
   try {
     const storage = getStorage();
     if (!storage) return;

-    // 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(),
-    };
-
+    const dataToStore = prepareDataForStorage(value, ttl);
     const serialized = JSON.stringify(dataToStore);
     storage.setItem(key, serialized);
   } catch (error) {
     console.error(`Error saving to sessionStorage (${key}):`, error);
     
     // 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);
+      // Clean up expired entries only (consider not clearing the current key)
       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 dataToStore = prepareDataForStorage(value, ttl);
         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);
       }
     }
   }
 };

27-71: Consider simplifying undefined handling logic.

The function has defensive checks for undefined values at two levels:

  • Lines 47-50: Remove entry if parsed === undefined
  • Lines 61-66: Check if parsed.data !== undefined before returning

While this handles edge cases thoroughly, the logic could be streamlined. The current implementation is functionally correct but slightly verbose.

♻️ Optional simplification
 export const getFromStorage = (key, defaultValue = null) => {
   try {
     const storage = getStorage();
     if (!storage) return defaultValue;

     const item = storage.getItem(key);
     if (!item) return defaultValue;

     // 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()) {
       // Entry expired, delete it
       storage.removeItem(key);
       return defaultValue;
     }

-    // 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;
+    // Return parsed.data if it exists, otherwise return defaultValue
+    // Note: This handles both missing data property and undefined data values
+    return 'data' in parsed ? parsed.data : defaultValue;
   } catch (error) {
     console.error(`Error reading from sessionStorage (${key}):`, error);
     return defaultValue;
   }
 };

105-121: Add serialization validation for stored values.

The function doesn't validate whether value can be safely serialized to JSON. Non-serializable values (like functions, symbols, or circular references) will cause JSON.stringify to throw or produce unexpected results.

♻️ Optional enhancement to catch serialization errors early
 export const saveToStorage = (key, value, ttl = DEFAULT_TTL) => {
   try {
     const storage = getStorage();
     if (!storage) return;

+    // Validate that value is serializable
+    if (value !== null && typeof value === 'object') {
+      // Quick check for circular references and non-serializable types
+      try {
+        JSON.stringify(value);
+      } catch (e) {
+        console.error(`Value for key "${key}" is not serializable:`, e);
+        return;
+      }
+    }
+
     // 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(),
     };

     const serialized = JSON.stringify(dataToStore);
     storage.setItem(key, serialized);
   } catch (error) {
     console.error(`Error saving to sessionStorage (${key}):`, error);
     // ... rest of error handling
   }
 };

Note: This does stringify twice, so it's a trade-off between validation and performance.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6618a15 and 40501d2.

📒 Files selected for processing (1)
  • frontend/src/utils/sessionStorage.js
🔇 Additional comments (1)
frontend/src/utils/sessionStorage.js (1)

210-262: Verify PII retention policy for clientAddress field.

The function intentionally retains clientAddress in both single and batch invoice data (noted in comments at lines 208, 216, and 256). Depending on your jurisdiction and privacy requirements, client addresses may be considered PII.

Additionally, the function uses shallow copying (line 213) and only handles specific nested structures (userInfo, invoiceRows). If the data contains other nested objects with PII fields outside these known structures, they won't be sanitized.

Verify:

  1. Whether retaining clientAddress complies with your privacy policy and data protection requirements (GDPR, CCPA, etc.)
  2. Whether there are other nested data structures in the form data that might contain PII fields not covered by this sanitization logic

Consider documenting the rationale for retaining clientAddress if it's a deliberate decision for functional requirements.

@kumawatkaran523
Copy link
Contributor

@ajey35 First of try to resolve conflict.
Secondly can we add data persistence feature in link generation part as well??

@ajey35 ajey35 force-pushed the feat/added-persistant-storage-inputs branch from 40501d2 to 4f7d3ea Compare January 20, 2026 17:01
@ajey35
Copy link
Contributor Author

ajey35 commented Jan 20, 2026

hey @kumawatkaran523 Resolved Conflicts and Added Session storage for the GenerateLink Component As Welll

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/page/GenerateLink.jsx`:
- Around line 74-99: The restored token fields in the useEffect that reads
getFromStorage(StorageKeys.GENERATE_LINK) must be validated against the current
chain/token list before calling setSelectedToken, setUseCustomToken,
setCustomTokenAddress, setVerifiedToken, or setTokenVerificationState; update
the logic in the useEffect (and/or add a follow-up effect keyed on tokens and
chainId) to check that savedData.selectedToken exists in the current tokens list
for the active chain (and that a saved customTokenAddress matches the current
chainId format or lookup), and if it does not match clear the token-related
state instead of restoring it, then call setIsInitialLoad(false) as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants