Skip to content

fix(claim): stop claim page crashing on links with no events relation - #2893

Merged
innolope-dev merged 2 commits into
devfrom
fix/claim-cancelled-events-crash-dev
Aug 31, 2026
Merged

fix(claim): stop claim page crashing on links with no events relation#2893
innolope-dev merged 2 commits into
devfrom
fix/claim-cancelled-events-crash-dev

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Re-targets #2889 (merged to main) and #2892 onto dev. dev never received the fix — the crashing line is still live there:

// src/components/Claim/Claim.tsx (dev)
cancelledDate: status === 'cancelled' ? new Date(claimLinkData.events[0]?.timestamp) : undefined,

Without this, the next devmain release re-introduces the crash. Carries both the fix and its regression test, since dev has neither.

The bug

Opening a cancelled send link showed "Application error: a client-side exception has occurred"TypeError: Cannot read properties of undefined (reading '0') thrown inside a render-phase useMemo (Sentry PEANUT-UI-SJ0).

The optional chain sits on the wrong side of the subscript: events[0]?.timestamp still throws when events itself is missing.

Why events goes missing

GET /send-links/:pubKey has two response paths. The DB path returns sanitizeSendLink(sendLink), which projects intents into the legacy claim + events shape. The cache path (30s TTL) returns the raw Prisma row, which has neither. So the crash needed a cache hit on a cancelled link — which is why it was intermittent.

The backend half is peanutprotocol/peanut-api-ts#1473 (also re-targeted to dev). This PR is still worth landing on its own: the guard is correct regardless of which shape the API sends.

Changes

  • Guard the array, not the element.
  • Mark events optional on the SendLink type. It was declared required, which is exactly why the compiler never flagged the dereference — and why the existing test fixture couldn't be corrected.

The test

dev already had the right test — "CANCELLED link shows ClaimedView", rendering the real Claim component. It passed anyway because the fixture hard-codes events: [], and [][0]?.timestamp is safe where undefined[0] throws.

The new case builds the cache-hit shape by stripping claim and events. It gates on the receipt rather than ClaimedView: claimLinkData is populated by an async effect that awaits fetchTokenDetails, so the view settles before the memo runs — asserting on ClaimedView alone passes even against the crashing code.

Note this gate differs from #2892's, which used the transaction-details drawer. That hook doesn't exist on dev; here the same memo feeds selectedTransaction and the receipt instead.

Verified on this base

  • Against origin/dev's Claim.tsx: fails with TypeError: Cannot read properties of undefined (reading '0').
  • With the fix: 16/16 pass.
  • tsc --noEmit reports only the pre-existing src/utils/tw.ts(58,5) error, which is present on clean origin/dev.

The default fixture keeps events: [] so the projected shape stays covered too.

The API stopped selecting the SendLink `events` relation post-ledger-collapse,
so `claimLinkData.events` is now always undefined. Claim.tsx dereferenced
`events[0]` with the optional chain on the wrong side of the index, throwing
"Cannot read properties of undefined (reading '0')" inside a render-phase
useMemo — which Next.js surfaces as "Application error: a client-side
exception has occurred" (Sentry PEANUT-UI-SJ0).

Guard the array itself and mark `events` optional on the SendLink type so the
compiler catches the next orphaned access.
… claim page

GET /send-links/:pubKey answers a cache hit (30s TTL) with the raw Prisma
row, which carries `intents` instead of the `claim` + `events` shape
`sanitizeSendLink` projects on the DB path. The existing CANCELLED test
never caught PEANUT-UI-SJ0 because its fixture hard-codes `events: []`,
and `[][0]?.timestamp` is safe where `undefined[0]` throws.

Add a case built from the cache-hit shape. It gates on the drawer effect
rather than the rendered view: the view settles before the transaction
memo runs, so asserting on it alone passes even against the crashing code.
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 31, 2026 5:37pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2243e901-b3fc-45a8-90ec-353cc84ce78b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7107.2 → 7107.44 (+0.24)
Findings: 0 net (+12 new, -12 resolved)

🆕 New findings (12)

  • critical complexity — src/components/Claim/Claim.tsx — CC 100, MI 53.65, SLOC 399
  • medium high-mdd — src/components/Claim/Claim.tsx:50 — Claim: MDD 103.1 (uses across many lines from declarations)
  • medium high-mdd — src/components/Claim/Claim.tsx:122 — : MDD 26.8 (uses across many lines from declarations)
  • medium method-complexity — src/components/Claim/Claim.tsx:122 — CC 16 SLOC 79
  • medium react-effect-derives-state — src/components/Claim/Claim.tsx:421 — small useEffect that only sets state from deps
  • medium react-effect-derives-state — src/components/Claim/Claim.tsx:427 — useEffect with empty deps + setState — derived state anti-pattern
  • medium react-effect-derives-state — src/components/Claim/Claim.tsx:437 — small useEffect that only sets state from deps
  • low high-dlt — src/components/Claim/Claim.tsx:287 — : DLT 16 (calls 16 distinct functions — high context load)
  • low high-dlt — src/components/Claim/Claim.tsx:291 — processLink: DLT 15 (calls 15 distinct functions — high context load)
  • low high-mdd — src/components/Claim/Claim.tsx:287 — : MDD 10.5 (uses across many lines from declarations)
  • low enum-usage — src/services/services.types.ts:384 — enum EInviteType — prefer as const object literals
  • low enum-usage — src/services/services.types.ts:443 — enum PointsAction — prefer as const object literals

✅ Resolved (12)

  • src/components/Claim/Claim.tsx — CC 99, MI 53.67, SLOC 399
  • src/components/Claim/Claim.tsx:50 — Claim: MDD 102.1 (uses across many lines from declarations)
  • src/components/Claim/Claim.tsx:122 — : MDD 25.9 (uses across many lines from declarations)
  • src/components/Claim/Claim.tsx:122 — CC 15 SLOC 79
  • src/components/Claim/Claim.tsx:418 — small useEffect that only sets state from deps
  • src/components/Claim/Claim.tsx:424 — useEffect with empty deps + setState — derived state anti-pattern
  • src/components/Claim/Claim.tsx:434 — small useEffect that only sets state from deps
  • src/components/Claim/Claim.tsx:284 — : DLT 16 (calls 16 distinct functions — high context load)
  • src/components/Claim/Claim.tsx:288 — processLink: DLT 15 (calls 15 distinct functions — high context load)
  • src/components/Claim/Claim.tsx:284 — : MDD 10.5 (uses across many lines from declarations)
  • src/services/services.types.ts:383 — enum EInviteType — prefer as const object literals
  • src/services/services.types.ts:442 — enum PointsAction — prefer as const object literals

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 4579 ran, 0 failed, 0 skipped, 1.4m

📊 Coverage (unit)

metric %
statements 72.5%
branches 58.2%
functions 63.7%
lines 73.5%
⏱ 10 slowest test cases
time test
3.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.8s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › the change cooldown shows its date and blocks changing to another country, not re-saving
1.5s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › saves the declared residence, refetches, and closes
1.2s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › prefers the server value over a stale device mirror
1.2s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › moving to a country in neither slot leaves the second document alone
1.1s src/components/Setup/Views/__tests__/Residence.test.tsx › reveals the second selector via the multi-doc link
1.1s src/app/receipt/[entryId]/pdf/__tests__/receipt-pdf-render.test.ts › produces a real, non-trivial PDF document
1.1s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › swaps from the server value with no device mirror at all
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
0.9s src/components/Profile/views/__tests__/ResidenceChangeModal.test.tsx › promoting the second document country swaps the pair instead of dropping one
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Chip review — no blocking findings — this is not an approval

Clean: the guard safely handles omitted events, the type now matches both API response shapes, and the regression test exercises the previously crashing receipt path.

Checked clean

  • Correctness: verified that a cancelled raw-cache payload without claim or events no longer throws while deriving receipt data.
  • API contract: confirmed database responses project events while the cache path can return the raw shape without that field.
  • Security: no authorization, trust-boundary, secret-handling, injection, or workflow-permission behavior changed.
  • Adversarial: reconstructed the missing-events render failure and confirmed all remaining claim and event reads on this path are guarded.
  • Slop: found no duplicate abstraction, dead code, misleading behavior, or architecture drift in the scoped change.
  • CI: exact-head unit, typecheck, eslint, format, analyze, and aggregate ci-success checks passed.

Second opinion by moonshotai/kimi-k3: 0 finding(s), marked with the model name. It reads the diff only, so treat its findings as advice.

Exact head: 7e3ee6dc9ce6 · Context: repo, engineering

@innolope-dev
innolope-dev merged commit 40d866c into dev Aug 31, 2026
20 checks passed
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.

1 participant