Card history#1909
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
@coderabbitai review in depth. Evaluate:
Also, write a very concise testing guide for QA |
Code-analysis diffPainscore total: 5402.52 → 5542.29 (+139.77) 🆕 New findings (40)
…and 20 more. ✅ Resolved (21)
…and 1 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 93666
🐛 Bug Risk1. Memo not actually suppressed for card spends (medium risk)
// 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 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
|
| 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.
There was a problem hiding this comment.
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 | 🟠 MajorInclude
isPublicin 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,mantecaDepositInfoand 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
📒 Files selected for processing (11)
src/components/TransactionDetails/TransactionCard.tsxsrc/components/TransactionDetails/TransactionDetailsReceipt.tsxsrc/components/TransactionDetails/provider-actions/CancelDepositActions.tsxsrc/components/TransactionDetails/provider-receipts/PerkRewardReceipt.tsxsrc/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsxsrc/components/TransactionDetails/provider-rows/CardPaymentRows.tsxsrc/components/TransactionDetails/provider-rows/MantecaDepositInfo.tsxsrc/components/TransactionDetails/transaction-details.utils.tssrc/components/TransactionDetails/transaction-predicates.tssrc/components/TransactionDetails/transactionTransformer.tssrc/utils/cardDeclineReason.ts
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/components/TransactionDetails/transactionTransformer.ts (2)
668-689:⚠️ Potential issue | 🟠 MajorBuild
cardPaymentfor all card intents, not only enriched ones.This guard still drops
CARD_SPENDentries when Rain has nomerchantNameyet. Those transactions stop matchingisCardPaymentEntry(...), so the drawer loses card-specific rows/styling exactly for the generic “Card payment” fallback case. Key this object off the intent kind, and deriveisRefundfrom the refund kind instead ofparentRainTxIdalone.🤖 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 | 🟡 MinorThe card-memo suppression is still only documented, not implemented.
This still copies
entry.memo?.trim()for card entries, so card spends/refunds can render bothCardPaymentRowsand the genericCommentrow. Suppressmemofor 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 | 🔴 CriticalDon’t cancel the charge before Bridge cancellation succeeds.
This branch still trusts
bridgeTransferId!and ignores the{ error?: string }result fromcancelOnramp(). 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 | 🟡 MinorNormalize
localCurrencybefore the USD check.Both the predicate and renderer still compare raw
card.localCurrencyto'usd'. Values like' USD 'or'-'will incorrectly surface a “Charged in” row. ReusenonBlank(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(...)andCardPaymentRows(...)maintain two copies of the same presence checks. Extract a sharedbuildCardPaymentSubRows(...)helper and have the visibility predicate userows.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
📒 Files selected for processing (6)
src/components/TransactionDetails/TransactionCard.tsxsrc/components/TransactionDetails/TransactionDetailsReceipt.tsxsrc/components/TransactionDetails/provider-actions/CancelDepositActions.tsxsrc/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsxsrc/components/TransactionDetails/provider-rows/CardPaymentRows.tsxsrc/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
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.
There was a problem hiding this comment.
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 optionallyaria-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
📒 Files selected for processing (8)
src/components/TransactionDetails/TransactionCard.tsxsrc/components/TransactionDetails/TransactionDetailsReceipt.tsxsrc/components/TransactionDetails/provider-actions/CancelDepositActions.tsxsrc/components/TransactionDetails/provider-rows/BridgeDepositInstructions.tsxsrc/components/TransactionDetails/provider-rows/CardPaymentRows.tsxsrc/components/TransactionDetails/transaction-predicates.tssrc/components/TransactionDetails/transactionTransformer.tssrc/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
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.
No description provided.