From a78032eeca03c5565e073b26dfd9080acc88f960 Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Mon, 31 Aug 2026 16:43:23 +0100 Subject: [PATCH 1/2] fix(claim): stop claim page crashing on links with no events relation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/Claim/Claim.tsx | 5 ++++- src/services/services.types.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index 88c9452358..2fc5ea0bb1 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -179,7 +179,10 @@ export const Claim = ({}) => { initials: getInitialsFromName(recipientName), memo: claimLinkData.textContent, attachmentUrl: claimLinkData.fileUrl, - cancelledDate: status === 'cancelled' ? new Date(claimLinkData.events[0]?.timestamp) : undefined, + cancelledDate: + status === 'cancelled' && claimLinkData.events?.[0] + ? new Date(claimLinkData.events[0].timestamp) + : undefined, txHash: claimLinkData.claim?.txHash, extraDataForDrawer: { isLinkTransaction: true, diff --git a/src/services/services.types.ts b/src/services/services.types.ts index 4cc97869fe..99be569dcc 100644 --- a/src/services/services.types.ts +++ b/src/services/services.types.ts @@ -374,7 +374,8 @@ export type SendLink = { }[] } } - events: { + /** Absent post-ledger-collapse: the API no longer selects the `events` relation. */ + events?: { timestamp: Date status: SendLinkStatus reason?: string From 7e3ee6dc9ce65ad91eae8c91df481ce2a8228784 Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Mon, 31 Aug 2026 17:34:30 +0100 Subject: [PATCH 2/2] test(claim): cover the unprojected send-link payload that crashed the 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. --- .../Claim/__tests__/claim-states.test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/Claim/__tests__/claim-states.test.tsx b/src/components/Claim/__tests__/claim-states.test.tsx index 231b20f5e2..944e23dcfc 100644 --- a/src/components/Claim/__tests__/claim-states.test.tsx +++ b/src/components/Claim/__tests__/claim-states.test.tsx @@ -385,6 +385,36 @@ describe('GROUP 3: Already Claimed / Cancelled', () => { }) }) + // GET /send-links/:pubKey answers a cache hit with the raw Prisma row, which + // carries `intents` instead of the projected `claim` + `events`. Claim.tsx read + // `events[0]` unguarded and took the whole page down (PEANUT-UI-SJ0). + test('CANCELLED link renders when the API omits claim/events (cache-hit shape)', async () => { + mockUseAuth.mockReturnValue({ + user: { user: { userId: 'sender-123' } }, + isFetchingUser: false, + fetchUser: jest.fn(), + }) + + const { + events: _events, + claim: _claim, + ...unprojected + } = makeSendLink({ + status: 'CANCELLED', + sender: { userId: 'sender-123', username: 'alice' }, + }) + mockSendLinksApi.get.mockResolvedValue(unprojected) + + renderClaim() + + // Gates on the receipt, which only renders once the transaction memo has + // produced a value — asserting on ClaimedView settles too early to catch + // a throw inside the memo. + await waitFor(() => { + expect(screen.getByTestId('transaction-details-receipt')).toBeInTheDocument() + }) + }) + test('CLAIMING link (in progress) shows as already claimed', async () => { const link = makeSendLink({ status: 'CLAIMING' }) mockSendLinksApi.get.mockResolvedValue(link)