Guard referral storage access - #126
Conversation
Greptile SummaryThis PR guards all
Confidence Score: 4/5Safe to merge — the production guard is correct and handles all documented failure modes without altering the happy path. The implementation is sound and the happy path is unchanged. The two open points are minor: the outer try/catch in getStoredReferral is unreachable dead code, and the "does not throw" test cases don't await effects so they may not exercise the guard inside useEffect if effects flush asynchronously in this environment. src/components/referral/ReferralTracker.test.tsx — the synchronous "does not throw" assertions may not be verifying the useEffect guard. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[useEffect fires / getStoredReferral / clearStoredReferral] --> B[getLocalStorage]
B --> C{typeof window === 'undefined'?}
C -- Yes --> D[return null]
C -- No --> E[try: return window.localStorage]
E -- throws SecurityError --> F[catch: return null]
E -- ok --> G[Storage object]
G --> H{caller operation}
D --> I[null?.op = no-op]
F --> I
H -- setItem --> J[try: storage?.setItem]
J -- throws QuotaExceededError --> K[catch: silently ignore]
J -- ok --> L[value stored]
H -- getItem --> M[storage?.getItem ?? null]
H -- removeItem --> N[try: storage?.removeItem]
N -- throws --> O[catch: silently ignore]
Reviews (1): Last reviewed commit: "Guard referral tracking storage access" | Re-trigger Greptile |
| it("does not throw when localStorage is unavailable", () => { | ||
| Object.defineProperty(window, "localStorage", { | ||
| configurable: true, | ||
| get() { | ||
| throw new DOMException("Storage is blocked", "SecurityError"); | ||
| }, | ||
| }); | ||
| searchParams = new URLSearchParams("ref=invite-789"); | ||
|
|
||
| expect(() => render(<ReferralTracker />)).not.toThrow(); | ||
| expect(getStoredReferral()).toBeNull(); | ||
| expect(() => clearStoredReferral()).not.toThrow(); | ||
| }); |
There was a problem hiding this comment.
Effect guard not awaited in "does not throw" tests
Both "does not throw" cases are synchronous and don't await the useEffect. RTL's render() wraps in a synchronous act(), but the first test in this suite still uses await waitFor() to observe the effect's side effect — which suggests effects aren't reliably flushed by synchronous render() here. If the effect is scheduled after render() returns, the not.toThrow() assertion passes regardless of whether the guard fires, meaning a future regression that removes the guard could go undetected by these tests. Making the tests async and adding an await act(async () => { render(...) }) (or checking via waitFor) would ensure the effect actually runs before the assertion.
| export function getStoredReferral(): string | null { | ||
| if (typeof window === "undefined") return null; | ||
| return localStorage.getItem(REFERRAL_KEY); | ||
| try { | ||
| return getLocalStorage()?.getItem(REFERRAL_KEY) ?? null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Outer try/catch in
getStoredReferral is unreachable
getLocalStorage() already catches any access-time throw and returns null. The optional-chaining call null?.getItem(...) produces undefined without throwing, and ?? null converts that to null. The outer try/catch block can therefore never be triggered by the getLocalStorage() path. It would only fire if getItem() itself throws, which isn't a documented browser behaviour. Consider removing it to keep the error-handling surface consistent with clearStoredReferral, or add a comment explaining the specific edge case it guards.
Summary
localStoragereads, writes, and cleanup so blocked storage does not break invite signup flowsFixes #125
Validation
pnpm test:run src/components/referral/ReferralTracker.test.tsxpnpm type-checkpnpm exec eslint src/components/referral/ReferralTracker.tsx src/components/referral/ReferralTracker.test.tsxgit diff --checkBounty / payment
Submitted for the active uGig affiliate-program testing task. SOL receive address:
27sdMYXofqoM9qR13bZhccRNYeEgYn5EoHXTSJn4QWKP.Payment fallback: PayPal cultofrozen@gmail.com