Skip to content

Card history#1909

Merged
jjramirezn merged 12 commits intodevfrom
feat/card-activity-history
Apr 28, 2026
Merged

Card history#1909
jjramirezn merged 12 commits intodevfrom
feat/card-activity-history

Conversation

@jjramirezn
Copy link
Copy Markdown
Contributor

No description provided.

Card transactions in the activity feed have been showing a literal
"Card Payment" label because the transformer ignored the merchant
fields the backend was already persisting on the intent. The drawer
had no card-specific block at all. The activity-history spec calls
for merchant name + category + location, cross-currency display,
"Adjusted from \$X" notes on settlement deltas, friendly decline
reasons in the feed row, an auto-close note for stale auths, and a
"Report a problem" link to Crisp.

transactionTransformer.ts:
  - CARD_SPEND case reads extraData.merchantName and uses it as the
    feed-row title. Falls back to "Card payment" only when Rain didn't
    return a name (rare).
  - Card refunds (REFUND-kind intents with extraData.parentRainTxId)
    detected in the default branch and rendered as receive/credit
    rows titled "Refund from {Merchant}".
  - Failed card spends set memo to friendlyDeclineReason(code) so the
    feed row carries a one-line "why" without requiring a tap into
    the drawer (spec §4.4).
  - Pass the full card-payment bag through extraDataForDrawer so the
    receipt component can render the detail block without re-querying.

TransactionDetailsReceipt.tsx:
  - New card-payment Card block rendered when extraDataForDrawer.cardPayment
    is populated. Shows merchant header (icon + name + category),
    optional location, cross-currency line ("Charged in {amount} {code}"),
    "Adjusted from \$X" settlement note, decline reason for failed,
    auto-close note for cancelled-via-stale-sweep, and a "Report a
    problem" button that opens Crisp.

TransactionCard.tsx:
  - Spec §4.4 visual de-emphasis: declined card spends render at
    opacity-60 in the feed but stay clickable.

cardDeclineReason.ts (new):
  - Maps raw Rain decline codes (INSUFFICIENT_FUNDS,
    card_spending_limit_exceeded, blocked_merchant/mcc, card_locked,
    invalid_pin) to user-friendly copy. Unknown codes fall back to
    "Transaction declined". Backend persists the raw code on intent
    metadata; this mapping lives on the FE for easy tweaking and
    future i18n.
Backend now exposes enrichedMerchantIcon (logo URL) and merchantId
on the card-payment extraData. Plumb both through the drawer's
TransactionDetails type so a later UI iteration can swap the generic
card icon for a real brand logo without another backend change.

No visual change in v1 — the receipt still renders the generic credit-
card icon per the spec's v1 decision. Just adding the wiring.
The receipt drawer (TransactionDetailsReceipt.tsx) had grown to 1603
lines with a TODO at the top calling for DRY refactoring. Each
transaction provider's bespoke rendering — Manteca's country-specific
deposit info, Bridge's multi-country bank fields, Perk's separate
layout, Card's merchant block — lived inline alongside type-discriminator
checks scattered across ~20 sites.

Post-M3 the backend reads everything from a single transaction_intents
table with provider-specific shape stamped onto extraData. That means
the FE doesn't need a per-provider read path — just per-provider
*rendering*. Time to actually let provider-specific code live in
provider-specific files.

Extracted provider modules:

  provider-rows/
    CardPaymentRows.tsx — merchant category, location, cross-currency
      "charged in", "Original amount" (settlement-adjusted), decline
      reason, auto-close note. Replaces the standalone Card block I
      added in the previous commit; rows now live inline in the
      receipt's details Card via a new `cardPayment` rowVisibilityConfig
      key. One unified details Card, no more parallel structure.
    BridgeDepositInstructions.tsx — multi-country bank fields (US
      routing/account/beneficiary, EU IBAN/BIC, UK sort code) plus
      the "See bank details" toggle. Owns its own showBankDetails
      state — that's local UI state, doesn't need to leak to parent.
    MantecaDepositInfo.tsx — Manteca onramp deposit address (label
      country-specific), alias, Argentina-only Razón Social + CUIT.

  provider-actions/
    CancelDepositActions.tsx — consolidates three near-identical
      cancel-deposit buttons (Bridge cancelOnramp, Manteca
      cancelDeposit, REQUEST + bridge fulfillment cancelOnramp+
      chargesApi.cancel) into one component that picks the right
      flow based on transaction shape.

  provider-receipts/
    PerkRewardReceipt.tsx — Perk's full-replacement layout (separate
      header card, processing pill, reason row + payment-UUID strip
      hack) lifted from the early-return branch into a self-contained
      component.

  transaction-predicates.ts — `isQRPayment`, `hasShareableReceipt`,
    `usesCompletedTimestampLabel`, `isCardPaymentEntry`, `isPerkReward`.
    Replaces multi-element `[X, Y, Z, ...].includes(originalType)`
    arrays scattered through the receipt. Adding a new type now means
    extending one Set, not grep-and-edit.

Memo handling: dropped the friendly-decline-reason injection in the
transformer. Decline reasons render in CardPaymentRows now (drawer-
level), and memo never reached the feed row anyway, so the indirection
was dead code. Card spends suppress memo entirely (backend follow-up),
which collapses the redundant Comment row.

Receipt size: 1603 → 1113 lines (-31%). Provider modules add 676 lines
across 6 new files but they're each focused and self-contained.

Deferred: SEND_LINK / REQUEST extraction (peer-to-peer Peanut feature,
not provider-specific, tightly coupled to QR-share + cancelLinkAndClaim
on-chain flow — own focused PR). useReceiptViewModel hook (gold-plating
after the provider extractions; remaining JSX is fine).
CardPaymentRows internally renders 0–6 sub-rows depending on whether
the merchant data, cross-currency, settlement adjustment, decline
reason, or auto-close note are present. For a card spend with all
of those absent (USD merchant, blank category/location, normal
settlement, completed status) the component returns null — but the
receipt's rowVisibilityConfig.cardPayment was true based on
`isCardPaymentEntry(transaction)` alone, leaving the slot visible-
but-empty.

That tripped shouldHideBorder: it counted cardPayment as the last
visible row, so the preceding `createdAt` row kept its bottom border,
which then dangled into the empty space inside the Card.

Adds hasCardPaymentRowsContent() — exact mirror of the component's
sub-row gating — and uses it as a second condition on the slot's
visibility. Slot collapses, createdAt becomes the last visible row,
border suppresses correctly. Visible at the bottom of the receipt
when there IS data.
Rain's sandbox payloads arrive whitespace-padded ("             " for
city, "  " for country, " - " for category) and legacy intents in the
DB pre-date the backend cleanField pass, so they keep their original
padding. Truthiness checks like `if (card.merchantCity)` match those
strings (any non-empty string is truthy in JS), so the slot was being
counted as "has content" — the receipt rendered a Location row with a
visible-but-blank value, leaving a dangling border under the preceding
"Created" row.

`nonBlank()` trims and treats whitespace-only or single/double-dash
placeholder strings as null. Used both in `hasCardPaymentRowsContent`
(slot visibility) and inside the component (sub-row gating + display
value), so even data that slips past the predicate renders a clean
row rather than empty padding.

Covers existing intents in DB without requiring a backfill.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Apr 28, 2026 6:57pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds card-payment metadata and decline detection; visually de-emphasizes declined card spends. Extracts provider-specific receipt UI and cancellation workflows into new components. Introduces a receipt view-model hook, transaction predicates, a card-decline utility, and enriches the transaction transformer with a cardPayment payload.

Changes

Cohort / File(s) Summary
Transaction card
src/components/TransactionDetails/TransactionCard.tsx
Adds isDeclinedCardSpend and conditionally applies opacity-60 to the Card for declined card spend entries.
Receipt refactor & view model
src/components/TransactionDetails/TransactionDetailsReceipt.tsx, src/components/TransactionDetails/useReceiptViewModel.ts, src/components/TransactionDetails/transaction-details.utils.ts
Refactors receipt rendering to use useReceiptViewModel; moves large memo/row logic out of component; adds 'cardPayment' to row keys and ordering.
Provider-specific receipt components
src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx, src/components/TransactionDetails/provider-rows/MantecaDepositInfo.tsx, src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx, src/components/TransactionDetails/provider-receipts/PerkRewardReceipt.tsx
Introduces CardPaymentRows (+predicate), MantecaDepositInfo, BridgeDepositInstructions (expandable bank details + copy controls), and PerkRewardReceipt (reward UI + support modal trigger).
Cancel deposit actions
src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
New CancelDepositActions client component consolidating cancel/reject workflows (bridge, Manteca, bank requests), handling loading, API calls, query invalidation, and error capture.
Transaction predicates & transformer
src/components/TransactionDetails/transaction-predicates.ts, src/components/TransactionDetails/transactionTransformer.ts
Adds predicates (isQRPayment, hasShareableReceipt, usesCompletedTimestampLabel, isCardPaymentEntry, isPerkReward) and enriches drawer mapping to expose kind and extraDataForDrawer.cardPayment including isRefund.
Card decline utility
src/utils/cardDeclineReason.ts
Adds friendlyDeclineReason(code) mapping raw decline codes to user-friendly messages for receipts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, making it impossible to assess whether the description relates to the changeset. Add a pull request description explaining the objectives, main changes, and reasoning behind the refactoring and new components introduced.
Docstring Coverage ⚠️ Warning Docstring coverage is 73.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Card history' is vague and does not clearly summarize the main changes; it lacks specificity about the refactoring and new components being introduced. Consider a more descriptive title that captures the primary change, such as 'Refactor receipt logic and add card payment details components' or 'Extract receipt view model and add card activity components'.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

@jjramirezn
Copy link
Copy Markdown
Contributor Author

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Code-analysis diff

Painscore total: 5402.52 → 5542.29 (+139.77)
Findings: +19 net (+40 new, -21 resolved)

🆕 New findings (40)

  • critical complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 158, MI 53.14, SLOC 342
  • critical complexity — src/components/TransactionDetails/transactionTransformer.ts — CC 139, MI 28.57, SLOC 429
  • critical method-complexity — src/components/TransactionDetails/transactionTransformer.ts:194 — mapTransactionDataForDrawer CC 133 SLOC 419
  • critical method-complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:63 — CC 111 SLOC 165
  • critical complexity — src/components/TransactionDetails/useReceiptViewModel.ts — CC 88, MI 62.3, SLOC 157
  • critical complexity — src/components/TransactionDetails/TransactionCard.tsx — CC 56, MI 50.86, SLOC 137
  • high method-complexity — src/components/TransactionDetails/useReceiptViewModel.ts:136 — CC 39 SLOC 28
  • high complexity — src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx — CC 37, MI 53.48, SLOC 114
  • high hotspot — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — 36 commits, +455/-1039 lines since 6 months ago
  • high method-complexity — src/components/TransactionDetails/TransactionCard.tsx:72 — CC 30 SLOC 65
  • medium high-mdd — src/components/TransactionDetails/transactionTransformer.ts:194 — mapTransactionDataForDrawer: MDD 203.6 (uses across many lines from declarations)
  • medium high-mdd — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:63 — TransactionDetailsReceipt: MDD 175.1 (uses across many lines from declarations)
  • medium high-mdd — src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx:23 — BridgeDepositInstructions: MDD 105.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/TransactionDetails/useReceiptViewModel.ts:76 — useReceiptViewModel: MDD 84.5 (uses across many lines from declarations)
  • medium high-dlt — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:63 — TransactionDetailsReceipt: DLT 75 (calls 75 distinct functions — high context load)
  • medium high-mdd — src/components/TransactionDetails/TransactionCard.tsx:72 — TransactionCard: MDD 51.9 (uses across many lines from declarations)
  • medium complexity — src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx — CC 26, MI 67.21, SLOC 68
  • medium high-mdd — src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx:70 — CardPaymentRows: MDD 24.7 (uses across many lines from declarations)
  • medium hotspot — src/components/TransactionDetails/TransactionCard.tsx — 22 commits, +154/-62 lines since 6 months ago
  • medium complexity — src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx — CC 17, MI 66.76, SLOC 13

…and 20 more.

✅ Resolved (21)

  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 287, MI 55.89, SLOC 538
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:77 — CC 143 SLOC 209
  • src/components/TransactionDetails/transactionTransformer.ts — CC 129, MI 26.25, SLOC 393
  • src/components/TransactionDetails/transactionTransformer.ts:164 — mapTransactionDataForDrawer CC 127 SLOC 389
  • src/components/TransactionDetails/TransactionCard.tsx — CC 53, MI 51.17, SLOC 134
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:146 — CC 38 SLOC 27
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — 30 commits, +290/-162 lines since 6 months ago
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:77 — TransactionDetailsReceipt: MDD 433.2 (uses across many lines from declarations)
  • src/components/TransactionDetails/transactionTransformer.ts:164 — mapTransactionDataForDrawer: MDD 179.4 (uses across many lines from declarations)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:77 — TransactionDetailsReceipt: DLT 82 (calls 82 distinct functions — high context load)
  • src/components/TransactionDetails/TransactionCard.tsx:71 — TransactionCard: MDD 47.5 (uses across many lines from declarations)
  • src/components/TransactionDetails/TransactionCard.tsx:71 — CC 27 SLOC 62
  • src/hooks/query/user.ts — 19 commits, +120/-86 lines since 6 months ago
  • src/components/TransactionDetails/TransactionCard.tsx:257 — getActionIcon CC 15 SLOC 39
  • src/components/TransactionDetails/TransactionCard.tsx:71 — TransactionCard: DLT 20 (calls 20 distinct functions — high context load)
  • src/components/TransactionDetails/TransactionCard.tsx:257 — getActionIcon: MDD 17.8 (uses across many lines from declarations)
  • src/components/TransactionDetails/transactionTransformer.ts:164 — mapTransactionDataForDrawer: DLT 15 (calls 15 distinct functions — high context load)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:449 — getLabelText: MDD 10.8 (uses across many lines from declarations)
  • src/components/TransactionDetails/transaction-details.utils.ts:48 — getBankAccountLabel: exported fn missing return type annotation
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:77 — TransactionDetailsReceipt: exported fn missing return type annotation

…and 1 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx 0.0 10.0 +10.0
src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx 0.0 9.5 +9.5
src/components/TransactionDetails/useReceiptViewModel.ts 0.0 8.1 +8.1
src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx 0.0 6.3 +6.3
src/utils/cardDeclineReason.ts 0.0 5.0 +5.0
src/components/TransactionDetails/provider-rows/MantecaDepositInfo.tsx 0.0 4.8 +4.8
src/components/TransactionDetails/transaction-predicates.ts 0.0 4.6 +4.6
src/components/TransactionDetails/provider-receipts/PerkRewardReceipt.tsx 0.0 4.4 +4.4
src/components/TransactionDetails/TransactionCard.tsx 11.5 12.5 +1.0
src/app/quests/page.tsx 8.3 9.3 +1.0
src/components/TransactionDetails/TransactionAvatarBadge.tsx 11.1 12.0 +1.0
src/app/lp/card/CardLandingPage.tsx 14.9 15.8 +0.9
src/components/Profile/components/ProfileMenuItem.tsx 7.6 8.5 +0.9
src/utils/passkeyPreflight.ts 15.6 16.4 +0.9
src/app/api/health/zerodev/route.ts 12.9 13.7 +0.8
src/components/IdentityVerification/StartVerificationModal.tsx 8.3 9.0 +0.7
src/app/[...recipient]/payment-layout-wrapper.tsx 7.5 8.1 +0.7
src/components/Card/CardGeoScreen.tsx 8.7 9.3 +0.6
src/components/Global/StatusPill/index.tsx 7.5 8.1 +0.6
src/components/Profile/AvatarWithBadge.tsx 8.9 9.4 +0.6

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

🧪 UI test report — ✅ all green

Suites

  • unit: 932 ran, 0 failed, 0 skipped, 11.7s

📊 Coverage (unit)

metric %
statements 42.9%
branches 22.4%
functions 22.4%
lines 42.6%
⏱ 10 slowest test cases
time test
0.2s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.1s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces 2
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (invalid characters)
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ENS name
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid Italian IBAN
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR details
gh pr view 1909 --repo peanutprotocol/peanut-ui --json title,body,files,additions,deletions,commits

# Get PR diff
gh pr diff 1909 --repo peanutprotocol/peanut-ui

Length of output: 93666


@jjramirezn — deep review below. Overall this is a well-structured refactor with thoughtful commit decomposition. I found a few concrete issues worth addressing before merge.


🐛 Bug Risk

1. Memo not actually suppressed for card spends (medium risk)

transactionTransformer.ts has a comment saying card spends suppress the memo, but the code doesn't do it:

// comment says "Card spends suppress this" but:
memo: isTestDeposit ? 'Your peanut wallet is ready to use!' : entry.memo?.trim(),

If Rain ever returns a memo on a CARD_SPEND intent, the receipt would render a redundant "Comment" row alongside the already-correct merchant name. Fix:

memo: isTestDeposit
    ? 'Your peanut wallet is ready to use!'
    : entry.kind === 'CARD_SPEND'
      ? undefined        // merchant/decline rows own the narrative; memo adds nothing
      : entry.memo?.trim(),

2. Non-null assertion on bridgeTransferId (medium risk)

In CancelDepositActions.tsx:

await cancelOnramp(transaction.extraDataForDrawer?.bridgeTransferId!)

The ! silences TypeScript but if bridgeTransferId is absent at runtime (malformed intent, legacy record), cancelOnramp receives undefined and the behaviour is provider-defined. Add a guard:

const bridgeId = transaction.extraDataForDrawer?.bridgeTransferId
if (!bridgeId) throw new Error('Missing bridgeTransferId for REQUEST cancel')
await cancelOnramp(bridgeId)

3. localAmount division without numeric guard (low-medium risk)

In CardPaymentRows.tsx:

const localFormatted = (Number(card.localAmount) / 100).toFixed(2)

Number(null)0, Number('abc')NaN. A row showing "NaN EUR" would be confusing. Simple guard:

const raw = Number(card.localAmount)
if (!Number.isFinite(raw)) { /* skip or log */ }
const localFormatted = (raw / 100).toFixed(2)

Same issue applies to authAmount in the settlementAdjusted block.

4. Card-refund detection in default branch (low-medium risk)

In transactionTransformer.ts, the default branch runs for SEND_LINK, REQUEST, DEPOSIT, WITHDRAW, etc. If any of those ever receive a parentRainTxId field from the backend (even accidentally), they'd be silently classified as card refunds. The check should be scoped to REFUND kind:

default:
    if (entry.kind === 'REFUND' && entry.extraData?.parentRainTxId) {
        // card refund path
    }

5. isDeclinedCardSpend in TransactionCard.tsx duplicates predicate (low risk)

const isDeclinedCardSpend =
    status === 'failed' && transaction.extraDataForDrawer?.cardPayment != null

isCardPaymentEntry already exists in transaction-predicates.ts. Importing it keeps the definition in one place:

import { isCardPaymentEntry } from './transaction-predicates'
const isDeclinedCardSpend = status === 'failed' && isCardPaymentEntry(transaction)

⚡ Performance

No concerns. All new predicates use ReadonlySet.has() (O(1)). The hasCardPaymentRowsContent guard runs synchronously inside the existing rowVisibilityConfig useMemo, so it adds negligible cost. No new network calls introduced. The BridgeDepositInstructions component correctly owns its own local showBankDetails state (no lift to parent, no extra renders).


🧹 Code Quality / DRY

What's good:

  • The transaction-predicates.ts approach is correct and extensible. Adding a new provider is now one Set entry, not a grep-and-edit.
  • Extracted components (PerkRewardReceipt, BridgeDepositInstructions, MantecaDepositInfo, CancelDepositActions) are each well-scoped and self-contained.
  • nonBlank() is a good defensive utility for Rain's sandbox padding problem.
  • Receipt shrinkage from 1,603 → 1,113 lines (-31%) is meaningful.

Remaining DRY concern:
hasCardPaymentRowsContent in CardPaymentRows.tsx mirrors the sub-row gating inside CardPaymentRows. Every new sub-row requires updating both functions. This is acknowledged in comments but is still a maintenance trap. A pragmatic mitigation:

// Instead of two parallel functions, CardPaymentRows could return a 
// typed result and the parent checks `.length > 0`:
export function buildCardPaymentRows(transaction): SubRow[] { ... }
// Receipt: rowVisibilityConfig.cardPayment = buildCardPaymentRows(transaction).length > 0
// CardPaymentRows just calls buildCardPaymentRows and renders

Not a blocker, but worth a follow-up issue.

Minor:
Import aliases isQRPayment as isQRPaymentTransaction and isPerkReward as isPerkRewardTransaction in TransactionDetailsReceipt.tsx are a symptom of local naming conflicts. Renaming the predicate exports (e.g. isQRPaymentEntry, isPerkRewardEntry) would eliminate the alias.


🧪 QA Testing Guide

Scenario Where Expected
Card spend – merchant known Activity feed + drawer Feed row shows merchant name; drawer shows Category/Location rows if populated
Card spend – merchant unknown Activity feed Feed row shows "Card payment" fallback
Declined card spend Activity feed + drawer Row is opacity-60; drawer shows "Decline reason" row with friendly copy (e.g. "Insufficient balance")
Cross-currency card spend (e.g. EUR merchant) Drawer "Charged in X EUR" row visible; USD-only spends do not show the row
Settlement-adjusted spend (tip / partial capture) Drawer "Original amount" row visible with auth amount
Auto-closed auth Drawer "Note: Automatically cancelled..." row visible
Card refund Activity feed + drawer Feed row shows "Refund from {Merchant}" with receive direction (green); drawer renders card block
Whitespace/placeholder merchant fields Drawer Category / Location rows must NOT appear for blank/dash values
Bridge onramp pending Drawer Deposit message + "See bank details" toggle works; "Cancel deposit" button present; button triggers cancellation and closes drawer
Manteca onramp pending (AR) Drawer Deposit address, alias, Razón Social + CUIT all visible; "Cancel deposit" button works
Pending bank request (sender role) Drawer "Cancel Request" button (not "Cancel deposit") present; cancels both onramp and charge
Perk reward Drawer Separate Perk layout renders (not the generic receipt); reason strips UUID suffix
Non-card transactions Activity feed + drawer No regression: QR payments, Bridge offramps, sends/receives render correctly without card block

🧠 Learnings used
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 852
File: src/hooks/useTransactionHistory.ts:177-239
Timestamp: 2025-05-14T21:16:06.369Z
Learning: When reviewing code, pay attention to and resolve CodeRabbit comments as they can contain useful refactoring suggestions and insights.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 594
File: src/components/Request/Pay/Views/Initial.view.tsx:188-213
Timestamp: 2024-12-31T15:08:15.640Z
Learning: Always resolve coderabbit comments when reviewing code, since they can contain useful suggestions.

Copy link
Copy Markdown
Contributor

@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: 8

Caution

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

⚠️ Outside diff range comments (1)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)

232-245: ⚠️ Potential issue | 🟠 Major

Include isPublic in these memo dependencies.

Line 233 and Line 333 now branch on isPublic, but both memos omit it from their dependency arrays. If this component is reused while switching between private/public receipt contexts, mantecaDepositInfo and the share-receipt button can stay stale and expose private-only receipt content in a public view.

Suggested fix
-    }, [transaction, isPendingBankRequest])
+    }, [transaction, isPendingBankRequest, isPublic, isPeanutWalletToken])
...
-    }, [transaction, isPendingSentLink, isPendingRequester, isPendingRequestee])
+    }, [transaction, isPendingSentLink, isPendingRequester, isPendingRequestee, isPublic])

Also applies to: 332-338

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TransactionDetails/TransactionDetailsReceipt.tsx` around lines
232 - 245, The memo hooks that compute mantecaDepositInfo and the
share-receipt/button state omit isPublic from their dependency arrays, causing
stale private/public-derived values; update the useMemo dependencies that
include mantecaDepositInfo and the memo controlling the share-receipt visibility
to add isPublic (alongside transaction and isPendingBankRequest) so changes in
isPublic re-run the memoized calculations; look for useMemo blocks referencing
mantecaDepositInfo, isCardPaymentEntry/hasCardPaymentRowsContent, and closed to
add isPublic to their dependency lists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx`:
- Around line 102-116: The current flow in CancelDepositActions (inside the
wrapAction callback) calls
cancelOnramp(transaction.extraDataForDrawer?.bridgeTransferId!) and immediately
calls chargesApi.cancel(transaction.id) without checking the bridge cancel
result or guarding against a missing bridgeTransferId; change this so you first
validate transaction.extraDataForDrawer?.bridgeTransferId exists, await
cancelOnramp(...) and inspect its return (e.g., check for an error field or
falsy result), and only call chargesApi.cancel(transaction.id) when cancelOnramp
indicates success; if cancelOnramp fails or bridgeTransferId is missing, throw
or return early so the local charge is not deleted.

In
`@src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx`:
- Around line 37-40: The displayed deposit reference and copied text are
truncated to 10 characters; update the BridgeDepositInstructions component to
use the full instructions.deposit_message instead of
instructions.deposit_message.slice(0, 10) for both the visible span and the
CopyToClipboard prop (textToCopy) so the exact reference is shown and copied;
locate the JSX fragment that renders the span and CopyToClipboard and remove the
slice() calls so both use the complete instructions.deposit_message.
- Around line 92-192: The component BridgeDepositInstructions.tsx fails to
handle Mexican SPEI because it never checks instructions.clabe, causing Mexico
entries to fall through to the US branch; update the conditional rendering logic
in BridgeDepositInstructions (the JSX block using instructions.iban /
instructions.sort_code / US fallback) to add a dedicated branch that detects
instructions.clabe and renders a PaymentInfoRow for "CLABE" (and any related
beneficiary fields) with copy support (similar to how
bank_account_number/bank_routing_number are rendered); refer to the
instructions.clabe property and the PaymentInfoRow and CopyToClipboard usage
when adding this branch so Mexico SPEI shows a CLABE row instead of the US
fallback.

In `@src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx`:
- Around line 7-15: The code compares raw card.localCurrency to "USD" (and
similar checks elsewhere) without normalizing padded or placeholder values; use
the existing nonBlank helper to normalize the value (e.g., const localCurrency =
nonBlank(card.localCurrency)) and then perform the USD check and any rendering
decisions against that normalized variable, and use that normalized value when
rendering the "Charged in" row (and replicate the same change for the other
occurrences around the file where card.localCurrency is compared or rendered).

In `@src/components/TransactionDetails/transaction-predicates.ts`:
- Around line 46-53: Update TransactionDetails and the predicates so QR_INTENT
entries are detected: add a transformed flag or kind property on
TransactionDetails (e.g., qrPayKind or transformedKind) when
mapTransactionDataForDrawer encounters EHistoryEntryType.TRANSACTION_INTENT with
kind === 'QR_PAY' (in mapTransactionDataForDrawer in transactionTransformer.ts),
and set it alongside extraDataForDrawer.originalType; then change isQRPayment
and hasShareableReceipt to first check that new transformed property (falling
back to extraDataForDrawer.originalType) so QR_PAY intents are treated the same
as direct QR payment types.

In `@src/components/TransactionDetails/TransactionCard.tsx`:
- Around line 133-142: The current isDeclinedCardSpend condition dims any failed
transaction with cardPayment (including refunds); update the predicate used
where isDeclinedCardSpend is defined so it also checks that the cardPayment is
not a refund (i.e. include
!transaction.extraDataForDrawer?.cardPayment?.isRefund), keeping the rest of the
logic (status === 'failed' and cardPayment present) and leave the Card usage
(className={twMerge('cursor-pointer', isDeclinedCardSpend && 'opacity-60')})
unchanged.

In `@src/components/TransactionDetails/transactionTransformer.ts`:
- Around line 670-691: The current conditional only sets cardPayment when
entry.extraData?.merchantName or parentRainTxId exists, dropping cardPayment for
unenriched card spends; change the logic in the cardPayment construction so that
for card spend entries you always build the cardPayment object (use
entry.extraData properties with safe null/coerce casts as currently done)
instead of returning undefined based on merchantName; specifically update the
cardPayment assignment in transactionTransformer (where cardPayment: ... is
defined) to unconditionally create the same shape using
entry.extraData?.merchantName, entry.extraData?.merchantCategory, etc., and keep
isRefund derived from !!entry.extraData?.parentRainTxId so
isCardPaymentEntry(...) and downstream styling/drawer behavior still work.
- Around line 645-649: The memo is still being copied for card transactions;
change the memo assignment in transactionTransformer.ts so card spends do not
carry entry.memo. Instead of always using entry.memo?.trim(), set memo to the
test-deposit string if isTestDeposit, otherwise null/undefined for card spends
and only use entry.memo?.trim() for non-card spends — detect card spends using
the same flag/field the CardPaymentRows rendering uses (e.g.,
entry.payment_method_type or entry.isCard or the property used elsewhere to
decide to render CardPaymentRows) so the generic "Comment" row is not
duplicated.

---

Outside diff comments:
In `@src/components/TransactionDetails/TransactionDetailsReceipt.tsx`:
- Around line 232-245: The memo hooks that compute mantecaDepositInfo and the
share-receipt/button state omit isPublic from their dependency arrays, causing
stale private/public-derived values; update the useMemo dependencies that
include mantecaDepositInfo and the memo controlling the share-receipt visibility
to add isPublic (alongside transaction and isPendingBankRequest) so changes in
isPublic re-run the memoized calculations; look for useMemo blocks referencing
mantecaDepositInfo, isCardPaymentEntry/hasCardPaymentRowsContent, and closed to
add isPublic to their dependency lists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2fbf2ce-e29f-48d1-ba8c-bbf3c7800ade

📥 Commits

Reviewing files that changed from the base of the PR and between 9b490c9 and c7a4b27.

📒 Files selected for processing (11)
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
  • src/components/TransactionDetails/provider-receipts/PerkRewardReceipt.tsx
  • src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx
  • src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx
  • src/components/TransactionDetails/provider-rows/MantecaDepositInfo.tsx
  • src/components/TransactionDetails/transaction-details.utils.ts
  • src/components/TransactionDetails/transaction-predicates.ts
  • src/components/TransactionDetails/transactionTransformer.ts
  • src/utils/cardDeclineReason.ts

Comment thread src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx Outdated
Comment thread src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx Outdated
Comment thread src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx
Comment thread src/components/TransactionDetails/transaction-predicates.ts Outdated
Comment thread src/components/TransactionDetails/TransactionCard.tsx
Comment thread src/components/TransactionDetails/transactionTransformer.ts Outdated
Comment thread src/components/TransactionDetails/transactionTransformer.ts
Copy link
Copy Markdown
Contributor

@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: 2

♻️ Duplicate comments (4)
src/components/TransactionDetails/transactionTransformer.ts (2)

668-689: ⚠️ Potential issue | 🟠 Major

Build cardPayment for all card intents, not only enriched ones.

This guard still drops CARD_SPEND entries when Rain has no merchantName yet. Those transactions stop matching isCardPaymentEntry(...), so the drawer loses card-specific rows/styling exactly for the generic “Card payment” fallback case. Key this object off the intent kind, and derive isRefund from the refund kind instead of parentRainTxId alone.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TransactionDetails/transactionTransformer.ts` around lines 668
- 689, The cardPayment object is only built when extraData has
merchantName/parentRainTxId, which drops CARD_SPEND entries and breaks
isCardPaymentEntry matching; always construct cardPayment based on the
transaction intent (e.g., check entry.intent.kind or the enum used for intent)
instead of extraData presence, populate merchant fields from entry.extraData
when present, and compute isRefund from the intent/refund kind (not solely
parentRainTxId) so isCardPaymentEntry and drawer styling work for generic "Card
payment" fallbacks.

643-647: ⚠️ Potential issue | 🟡 Minor

The card-memo suppression is still only documented, not implemented.

This still copies entry.memo?.trim() for card entries, so card spends/refunds can render both CardPaymentRows and the generic Comment row. Suppress memo for card-payment intents before trimming.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TransactionDetails/transactionTransformer.ts` around lines 643
- 647, The memo field is still populated for card payments causing duplicate
Comment rows; update the memo assignment in transactionTransformer.ts to
suppress memo for card-payment intents before trimming: when building the object
that sets memo (currently using isTestDeposit and entry.memo?.trim()), add a
check for the entry's intent/payment type (the same predicate used to decide
rendering with CardPaymentRows) so that for card spends/refunds memo is set to
undefined (or an empty value), otherwise use entry.memo?.trim(), preserving the
isTestDeposit override.
src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx (1)

112-115: ⚠️ Potential issue | 🔴 Critical

Don’t cancel the charge before Bridge cancellation succeeds.

This branch still trusts bridgeTransferId! and ignores the { error?: string } result from cancelOnramp(). A missing/failed Bridge cancel can still delete the local charge and orphan the onramp.

🔒 Suggested fix
                 <CancelButton
                     label="Cancel Request"
                     disabled={!!isLoading}
                     onClick={() =>
                         wrapAction(async () => {
-                            await cancelOnramp(transaction.extraDataForDrawer?.bridgeTransferId!)
+                            const bridgeTransferId = transaction.extraDataForDrawer?.bridgeTransferId
+                            if (!bridgeTransferId) {
+                                throw new Error('Missing bridge transfer ID for request cancellation')
+                            }
+
+                            const result = await cancelOnramp(bridgeTransferId)
+                            if (result.error) {
+                                throw new Error(result.error)
+                            }
+
                             await chargesApi.cancel(transaction.id)
                         })
                     }
                 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx`
around lines 112 - 115, The current flow calls
cancelOnramp(transaction.extraDataForDrawer?.bridgeTransferId!) and then
unconditionally calls chargesApi.cancel, which both force-unwraps
bridgeTransferId and ignores cancelOnramp's { error?: string } result; change
wrapAction to first validate that
transaction.extraDataForDrawer?.bridgeTransferId exists, await
cancelOnramp(bridgeTransferId) and check its returned result for an error, and
only call chargesApi.cancel(transaction.id) if cancelOnramp succeeded (no
error); surface or throw the cancelOnramp error instead of deleting the local
charge when Bridge cancellation fails.
src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx (1)

31-31: ⚠️ Potential issue | 🟡 Minor

Normalize localCurrency before the USD check.

Both the predicate and renderer still compare raw card.localCurrency to 'usd'. Values like ' USD ' or '-' will incorrectly surface a “Charged in” row. Reuse nonBlank(card.localCurrency) for the gate and the rendered value.

Also applies to: 86-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx` at line
31, The USD-check and displayed "Charged in" value use raw card.localCurrency,
so values like " USD " or "-" slip through; normalize and reuse
nonBlank(card.localCurrency) instead: update the predicate that currently reads
if (card.localAmount && card.localCurrency && card.localCurrency.toLowerCase()
!== 'usd') to compute a const normalizedCurrency = nonBlank(card.localCurrency)
and use normalizedCurrency && normalizedCurrency.toLowerCase() !== 'usd' for the
gate, and use normalizedCurrency as the rendered value in the renderer (the same
change also needs to be applied to the second occurrence handling the charged-in
row around the renderer logic).
🧹 Nitpick comments (1)
src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx (1)

25-35: Derive visibility and rendered rows from the same builder.

hasCardPaymentRowsContent(...) and CardPaymentRows(...) maintain two copies of the same presence checks. Extract a shared buildCardPaymentSubRows(...) helper and have the visibility predicate use rows.length > 0; it will keep the border logic and row rendering from drifting apart again.

Also applies to: 64-127

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx` around
lines 25 - 35, Extract a single builder function (e.g.,
buildCardPaymentSubRows(transaction: TransactionDetails)) that performs all
current presence checks and returns the list of sub-row descriptors used by
CardPaymentRows; update CardPaymentRows to render rows from
buildCardPaymentSubRows and move any border/formatting decisions to operate on
that returned array; replace hasCardPaymentRowsContent with a simple predicate
that calls buildCardPaymentSubRows(transaction) and returns rows.length > 0 so
visibility is derived from the same source of truth; ensure the helper preserves
existing checks (merchantCategory, merchantCity/merchantCountry,
localAmount/localCurrency != 'usd', settlementAdjusted/authAmount,
failed+declineReason, cancellationReason === 'auto_closed') and any identifiers
used in the CardPaymentRows rendering logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx`:
- Around line 89-100: When formatting card amounts in CardPaymentRows (e.g., the
localAmount and authAmount paths that compute localFormatted and
adjustedFromUsd), guard against Number(...) returning NaN by parsing the cents
into a numeric variable and only calling toFixed(2) and pushing the subRows
entry when Number.isFinite(parsedCents) is true; if the parsed value is not
finite, skip creating that row so the UI never displays "NaN" (apply this check
for both card.localAmount and card.authAmount usages).

In `@src/components/TransactionDetails/transactionTransformer.ts`:
- Around line 446-457: The refund-handling branch in transactionTransformer.ts
currently triggers for any entry with extraData.parentRainTxId; restrict it to
only run for explicit refund intents by adding a check that entry.kind ===
'REFUND' (i.e., change the condition that guards setting direction,
transactionCardType, nameForDetails and isPeerActuallyUser to require both
parentRainTxId and kind === 'REFUND'), so reversals or settlement adjustments
that carry parentRainTxId won't be misclassified as refunds.

---

Duplicate comments:
In `@src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx`:
- Around line 112-115: The current flow calls
cancelOnramp(transaction.extraDataForDrawer?.bridgeTransferId!) and then
unconditionally calls chargesApi.cancel, which both force-unwraps
bridgeTransferId and ignores cancelOnramp's { error?: string } result; change
wrapAction to first validate that
transaction.extraDataForDrawer?.bridgeTransferId exists, await
cancelOnramp(bridgeTransferId) and check its returned result for an error, and
only call chargesApi.cancel(transaction.id) if cancelOnramp succeeded (no
error); surface or throw the cancelOnramp error instead of deleting the local
charge when Bridge cancellation fails.

In `@src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx`:
- Line 31: The USD-check and displayed "Charged in" value use raw
card.localCurrency, so values like " USD " or "-" slip through; normalize and
reuse nonBlank(card.localCurrency) instead: update the predicate that currently
reads if (card.localAmount && card.localCurrency &&
card.localCurrency.toLowerCase() !== 'usd') to compute a const
normalizedCurrency = nonBlank(card.localCurrency) and use normalizedCurrency &&
normalizedCurrency.toLowerCase() !== 'usd' for the gate, and use
normalizedCurrency as the rendered value in the renderer (the same change also
needs to be applied to the second occurrence handling the charged-in row around
the renderer logic).

In `@src/components/TransactionDetails/transactionTransformer.ts`:
- Around line 668-689: The cardPayment object is only built when extraData has
merchantName/parentRainTxId, which drops CARD_SPEND entries and breaks
isCardPaymentEntry matching; always construct cardPayment based on the
transaction intent (e.g., check entry.intent.kind or the enum used for intent)
instead of extraData presence, populate merchant fields from entry.extraData
when present, and compute isRefund from the intent/refund kind (not solely
parentRainTxId) so isCardPaymentEntry and drawer styling work for generic "Card
payment" fallbacks.
- Around line 643-647: The memo field is still populated for card payments
causing duplicate Comment rows; update the memo assignment in
transactionTransformer.ts to suppress memo for card-payment intents before
trimming: when building the object that sets memo (currently using isTestDeposit
and entry.memo?.trim()), add a check for the entry's intent/payment type (the
same predicate used to decide rendering with CardPaymentRows) so that for card
spends/refunds memo is set to undefined (or an empty value), otherwise use
entry.memo?.trim(), preserving the isTestDeposit override.

---

Nitpick comments:
In `@src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx`:
- Around line 25-35: Extract a single builder function (e.g.,
buildCardPaymentSubRows(transaction: TransactionDetails)) that performs all
current presence checks and returns the list of sub-row descriptors used by
CardPaymentRows; update CardPaymentRows to render rows from
buildCardPaymentSubRows and move any border/formatting decisions to operate on
that returned array; replace hasCardPaymentRowsContent with a simple predicate
that calls buildCardPaymentSubRows(transaction) and returns rows.length > 0 so
visibility is derived from the same source of truth; ensure the helper preserves
existing checks (merchantCategory, merchantCity/merchantCountry,
localAmount/localCurrency != 'usd', settlementAdjusted/authAmount,
failed+declineReason, cancellationReason === 'auto_closed') and any identifiers
used in the CardPaymentRows rendering logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02f12123-32d4-4320-9274-9ee96b7ac15e

📥 Commits

Reviewing files that changed from the base of the PR and between c7a4b27 and 2d6fa1e.

📒 Files selected for processing (6)
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
  • src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx
  • src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx
  • src/components/TransactionDetails/transactionTransformer.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx

Comment thread src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx Outdated
Comment thread src/components/TransactionDetails/transactionTransformer.ts Outdated
The receipt component was still doing a lot of pure derivation inline
after the provider extractions: 7 type/status predicates, the 21-key
rowVisibilityConfig, the visible-rows + last-in-group helpers, the
share-receipt gate, the country lookup, the contributor list, the
formatted total. ~220 lines of useMemos and helper functions before
any JSX or side-effect callback ran.

Moved all of it into useReceiptViewModel(transaction, { isPublic }) —
pure derivation, no IO, no refs, no effects. Receipt destructures the
view model in one block and stays focused on what it actually owns:

  - Local UI state (showCancelLinkModal, tokenData)
  - Effects (fetch token data, sync modal state to parent)
  - Side-effect callbacks (closeRequestLink, the cancel-link flow)
  - JSX that consumes view-model fields

Receipt size: 1113 → 900 lines (-19%, cumulative -44% from the original
1603). Cyclomatic load on the component drops sharply because the big
useMemos no longer count toward TransactionDetailsReceipt's CC.

Hook is 284 lines but every export is independently reasonable about —
6 small useMemos, one big rowVisibilityConfig switch (unchanged),
two border helpers, one share gate. Each derivation has its own
docstring tying it to spec or behavior.
Round of correctness + UX fixes from automated review.

CancelDepositActions:
  REQUEST cancel path no longer fires chargesApi.cancel before
  confirming the bridge cancel succeeded. Previously it ignored
  cancelOnramp's { error } return shape and used a non-null
  assertion on bridgeTransferId, which could orphan an onramp on
  Bridge's side while marking the request as cancelled locally.
  Now: explicit guard for missing bridgeTransferId + throws on
  bridgeResult.error before touching the charge.

BridgeDepositInstructions:
  - Deposit Message no longer truncates display + clipboard text
    to 10 chars. Bridge requires the exact reference for
    reconciliation; truncation could break otherwise-valid
    deposits silently. Display gets break-all wrap; copy gets the
    full string.
  - Adds the Mexico SPEI (CLABE) branch. The component header
    already advertises Mexico support and the depositInstructions
    type carries `clabe`, but the render tree was falling through
    to the US fallback and showing blank account/routing rows.

transaction-predicates:
  - isQRPayment / hasShareableReceipt now also recognize the post-
    M3 shape: TRANSACTION_INTENT entries with extraData.kind ===
    'QR_PAY'. Pre-M3 rows still in the feed used dedicated
    originalType values; both shapes coexist. The transformer now
    threads `kind` into extraDataForDrawer so predicates can read
    it cleanly.

transactionTransformer:
  - cardPayment block builds for any CARD_SPEND-kind intent, not
    just enriched ones. Previously gated on merchantName presence,
    which dropped the entire card-specific drawer chrome for
    unknown-merchant spends.
  - Refund branch in the default case scoped to kind === 'OTHER'
    or 'REFUND' (REFUND-kind intents surface as 'OTHER' from
    toLegacyKindLabel today). Guarding on parentRainTxId alone
    risked misrouting any future intent that carries the linkage.
  - Memo actually does what its comment claimed — suppressed for
    card spends + Rain refunds. Backend already sets memo=undefined
    for these, but defending in depth.

CardPaymentRows:
  - parseCents() rejects NaN / Infinity / null up-front so the
    drawer never renders "Charged in NaN EUR" or "$NaN" rows.
    Used by both the predicate and the renderer.
  - hasCrossCurrencyCharge() normalizes localCurrency through
    nonBlank + lower-case-equals-usd before deciding to emit the
    "Charged in" row. Padded placeholder values from Rain's
    sandbox no longer slip past.

TransactionCard:
  - Imports isCardPaymentEntry from transaction-predicates instead
    of duplicating the cardPayment-presence check.
  - Declined-card-spend opacity-60 now scoped to spends specifically;
    failed REFUND entries (which also populate cardPayment) won't
    get greyed.
Copy link
Copy Markdown
Contributor

@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: 2

🧹 Nitpick comments (1)
src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx (1)

52-61: Expose the collapse state on the bank-details toggle.

Adding aria-expanded (and optionally aria-controls) here would make the expand/collapse state understandable to screen readers.

♿ Small accessibility tweak
                 <button
                     onClick={() => setShowBankDetails(!showBankDetails)}
+                    aria-expanded={showBankDetails}
                     className="flex w-full items-center justify-between py-3 text-left text-sm font-normal text-black underline transition-colors"
                 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx`
around lines 52 - 61, The toggle button in BridgeDepositInstructions.tsx that
uses showBankDetails and setShowBankDetails should expose its expanded state to
assistive tech: add aria-expanded={showBankDetails} to the <button> and
optionally add aria-controls pointing to the id of the collapsible bank details
container (create or reference an id on the details container). Keep the
existing onClick, label text and Icon logic unchanged so only accessibility
attributes are added to the button.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx`:
- Around line 150-168: The US-format branch in BridgeDepositInstructions.tsx
renders Account and Routing Number even when instructions may be partial; update
the branch selection to only render the US block when both
instructions.bank_account_number and instructions.bank_routing_number are
present (e.g., change the conditional that falls through to the US block to
check these two fields), and for PaymentInfoRow/CopyToClipboard uses ensure
non-null values (or render a fallback message) so CopyToClipboard is never
passed undefined; reference PaymentInfoRow, CopyToClipboard, and the
instructions object (and the transactionTransformer optional fields) when making
this check.

In `@src/components/TransactionDetails/transaction-predicates.ts`:
- Around line 16-44: EHistoryEntryType is undefined at module load because it’s
imported from a hook module, causing QR_PAYMENT_TYPES, SHAREABLE_RECEIPT_TYPES
and COMPLETED_LABEL_TYPES to throw on import; fix by removing the import-time
dependency: either re-source EHistoryEntryType from a non-hook/shared runtime
module (export the enum from a plain/shared file and import that instead) or
convert the const Sets into lazily-initialized getters/functions (e.g.,
getQrPaymentTypes(), getShareableReceiptTypes(), getCompletedLabelTypes()) that
import or reference EHistoryEntryType at runtime so the enum is resolved after
module load; update all usages to call the getters if you choose lazy init.

---

Nitpick comments:
In
`@src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx`:
- Around line 52-61: The toggle button in BridgeDepositInstructions.tsx that
uses showBankDetails and setShowBankDetails should expose its expanded state to
assistive tech: add aria-expanded={showBankDetails} to the <button> and
optionally add aria-controls pointing to the id of the collapsible bank details
container (create or reference an id on the details container). Keep the
existing onClick, label text and Icon logic unchanged so only accessibility
attributes are added to the button.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 602a0cdc-15ed-4369-813e-5602551a59d6

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6fa1e and b24d076.

📒 Files selected for processing (8)
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/provider-actions/CancelDepositActions.tsx
  • src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx
  • src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx
  • src/components/TransactionDetails/transaction-predicates.ts
  • src/components/TransactionDetails/transactionTransformer.ts
  • src/components/TransactionDetails/useReceiptViewModel.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/provider-rows/CardPaymentRows.tsx
  • src/components/TransactionDetails/transactionTransformer.ts

Comment thread src/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsx Outdated
Comment thread src/components/TransactionDetails/transaction-predicates.ts Outdated
EHistoryEntryType was being imported from @/hooks/useTransactionHistory
(which re-exports it from utils/history.utils). When tests mock the
hook (send-states.test.tsx does), the re-exported enum collapses to
undefined at module-load time and the predicate file's top-level Sets
throw "Cannot read properties of undefined (reading 'MANTECA_QR_PAYMENT')".

The transformer hits the same symbol but only inside function bodies
(executed at call time, after the test has set up the real hook),
so it didn't surface there.

Switch the predicates to import from @/utils/history.utils directly —
canonical source, no hook indirection, doesn't break under mocking.
`bank_account_number` and `bank_routing_number` are typed optional in
the depositInstructions interface, but the US fallback branch was
rendering both unconditionally — with non-null assertions on the copy
text. A partial Bridge payload (e.g. routing-only, or a future rail
that doesn't fit CLABE/IBAN/UK/US shapes and falls through to the US
branch) would render `undefined` and copy the literal string
"undefined" to the clipboard.

Each row now gates on its own value, mirroring the pattern already
used for the optional `bank_beneficiary_name` / `bank_beneficiary_address`
rows below. Drops the non-null assertions in the process.
The earlier fix swapped the EHistoryEntryType import path from the
hook re-export to utils/history.utils direct, but that didn't escape
the cycle. The ts-jest emit was still threading a runtime require
through this chain:

  utils/history.utils → transactionTransformer  (line 2: TransactionDetails)
                      → TransactionCard
                      → transaction-predicates
                      → utils/history.utils      (mid-evaluation, enum undef)

The inline `type` modifier on history.utils:2 should have been erased
under verbatimModuleSyntax + isolatedModules, but ts-jest with
useESM: true wasn't reliably stripping it.

Real fix: drop the runtime enum dep in this file entirely. Switch to
a type-only import (purely erased) and use string literals in the
Sets / equality checks. EHistoryEntryType is a string enum so the
runtime values are identical to their literal forms — same wire-level
behavior, no runtime require chain.

Verified: send-states.test.tsx passes after the change. Tradeoff: a
future rename of an enum value won't be caught by tsc since the
literals are cast to the type. Worth the unblock.
@jjramirezn jjramirezn merged commit da39205 into dev Apr 28, 2026
14 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