Skip to content

Send the selected destination rail as destination.paymentRail - #776

Merged
shreyav merged 1 commit into
mainfrom
claude/visualizer-destination-payment-rail
Jul 31, 2026
Merged

Send the selected destination rail as destination.paymentRail#776
shreyav merged 1 commit into
mainfrom
claude/visualizer-destination-payment-rail

Conversation

@shreyav

@shreyav shreyav commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

Makes the Grid Visualizer's payment-rail selection functional. Today the rail is decorative — the user picks one from a dropdown and it only changes UI captions. This threads the choice into the generated POST /quotes request as destination.paymentRail.

Supersedes #351.

Why the currencies.ts rename is a prerequisite, not a cosmetic change

instantRails / allRails currently hold human-readable strings ('FedNow', 'SEPA Instant', 'Bank Transfer', …). Those are not values the API accepts, so no rail could be sent at all while the data held display strings. Converting them to PaymentRail enum members (FEDNOW, SEPA_INSTANT, BANK_TRANSFER, …) is what makes the feature possible.

Because the enum values would otherwise render as SEPA_INSTANT at users, this adds an exported railDisplayName() covering all 23 enum members (with an underscores-to-spaces fallback) and applies it at the four sites that surface a rail:

  1. code-generator.tsgetJitPaymentMethod(), the "sending a FedNow transfer" prose note
  2. CurrencyPicker.tsxgetRailsText() and the keywords array in buildFiatItem (keywords keep both enum and display forms, so search still matches either)
  3. InputCard.tsx — dropdown button label, dropdown menu items, static fiatRail row
  4. flow-path.ts — the "Funds in via X" / "Funds out via X" captions

The enum value is the data; railDisplayName() is presentation only. No display string reaches a request body or onSelect/state.

The rename + display map is carried over from the closing PR on docs/sync-20260418 (e3191b4). Two currencies added to main since that branch (El Salvador SLV_ACCOUNT, CNY_ACCOUNT) were converted here as well.

paymentRail vs paymentRails — the distinction #351 got wrong

paymentRail (singular) Request field, on quote destinations only (quotes/AccountDestination.yaml). Optional — omitted, Grid selects a default.
paymentRails (plural) Response-only. Lives in *AccountInfo.yaml wrappers, reporting what an account supports. Absent from *ExternalAccountCreateInfo / *AccountInfoBase.

#351 injected the plural into POST /customers/external-accounts bodies — a field that endpoint does not accept. This PR introduces no paymentRails anywhere (grep -rn paymentRails src/ in the visualizer returns nothing), and buildAccountInfoBody has no rail logic at all. A short WHY comment at the emission site records the distinction.

The source/destination asymmetry

The spec puts paymentRail on the destination, but the visualizer's only rail selector was on the Source card. So this adds a destination-side selection rather than reusing the source's:

  • useFlowBuilder.ts: destRail state, SET_DEST_RAIL action, setDestRail callback — mirroring sourceRail exactly. Defaults via the existing getDefaultRail() on SET_RECEIVE, and recomputed on SWAP (matching how sourceRail already behaves there).
  • page.tsx: Destination InputCard now gets rail / onRailChange. InputCard already supported these generically, so no component changes were needed beyond the display-name calls.
  • Options stay constrained to the currency's allRails via the existing getFiatRailOptions, per "must be one of the rails supported by the destination account."

The existing source rail is untouched and keeps its display purposes.

flow-path.ts's "Funds out via X" caption now uses the selected destination rail instead of always defaulting to instantRails[0] — same user choice, so it should reflect it.

Judgment call worth a second opinion

paymentRail is emitted only for external destinations, in addition to the "fiat and a rail selected" conditions. Funds credited to a Grid internal balance never traverse an external payment rail, so sending one there would generate an example the API would likely reject. This also matches how flow-path.ts already treats rails (the "Funds out via" caption only renders for !next.isInternal). A destination can become internal via Swap. Happy to drop the gate if reviewers disagree.

Note the UI still shows a Rail row on an internal fiat destination card even though it no longer affects the body — consistent with how the Source card already behaves for internal sources. Hiding it felt like scope creep; flagging rather than fixing.

Verification — please read

I could not run the real build. npm ci in components/grid-visualizer fails because @central-icons-react requires a CENTRAL_LICENSE_KEY env var that is not available in this environment. The Vercel preview build on this PR is the real checknext build / next lint and all .tsx typechecking are unverified here.

What I was able to verify locally:

  • Strict typecheck of every dependency-free modulecode-generator.ts, flow-path.ts, currencies.ts, settlement-rails.ts, account-types.ts, crypto.ts under strict: true with the @/* path mapping: clean. The .tsx files could not be checked (missing react / next / icon types).
  • Behavioral smoke test of the compiled pure logic:
    • paymentRail appears in the quote body with the enum value, and changing the selection changes it (SEPA_INSTANTSEPA)
    • key omitted entirely (not null/undefined) when no rail is selected, when the destination is crypto, and when the destination is internal
    • across every rail of every currency, no non-enum-looking value is ever emitted
    • "Funds out via" follows the selection, falls back to the default, and renders display names
    • all 23 PaymentRail members are covered by the display map, and every rail value in currencies.ts is a valid enum member
  • Spec claims re-verified against openapi/components/schemas/quotes/AccountDestination.yaml, common/PaymentRail.yaml, and the *AccountInfo / *ExternalAccountCreateInfo split.

There is no test suite in grid-visualizer (no test script, no test files), so no tests were added or updated.

Not verified: actual rendering, dropdown interaction, and that CodePanel's stepsKey remount behaves as intended in the browser (destRail was added to both the memo deps and the key, so stale code should be impossible, but this is untested at runtime).

Files changed

  • src/data/currencies.ts — enum rename + railDisplayName()
  • src/hooks/useFlowBuilder.tsdestRail state/action/setter
  • src/lib/code-generator.tsdestRail param, paymentRail emission, display names in the JIT note
  • src/lib/flow-path.tsdestRail param, "Funds out via" uses it, display names
  • src/app/page.tsx — wires destRail to the Destination card and both panels
  • src/components/CodePanel/CodePanel.tsxdestRail prop, memo deps, stepsKey
  • src/components/FlowPanel/FlowPanel.tsxdestRail prop, memo deps
  • src/components/InputCard/InputCard.tsx — display names
  • src/components/CurrencyPicker/CurrencyPicker.tsx — display names

Generated by Claude Code

The Grid Visualizer's rail selector was decorative: picking a rail only
changed UI captions and never reached a request body. The quote
destination accepts an optional paymentRail, so the user's choice can be
real.

Three parts, in dependency order:

Convert currencies.ts instantRails/allRails to PaymentRail enum members.
This is the prerequisite, not a cosmetic rename - 'SEPA Instant' is not a
value the API accepts, so no rail could be sent while the data held
display strings. Add railDisplayName() so the four render sites (JIT
payment note, currency picker rail list, rail dropdown, flow-path
captions) keep reading as prose. The enum value is the data; the display
name is presentation only.

Add destRail state mirroring sourceRail, and pass rail/onRailChange to
the Destination InputCard. The spec puts paymentRail on the destination,
but the only rail selector was on the Source card, so the destination
needed its own selection rather than a reuse of the source's. Options
stay constrained to the currency's allRails, since the rail must be one
the destination account supports.

Emit it in the generated POST /quotes body, and use it for the
"Funds out via X" caption instead of defaulting to instantRails[0].

Supersedes #351, which injected the plural, response-only paymentRails
into external-account create bodies. Singular paymentRail on a quote
destination is a request field; plural paymentRails only ever appears in
*AccountInfo response wrappers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMnjJ8yWJsui7jLqqrDrL4
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
grid-flow-builder Ready Ready Preview Jul 30, 2026 8:24pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
grid-wallet-demo Ignored Ignored Jul 30, 2026 8:24pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes the destination payment-rail selector functional throughout the Grid Visualizer.

  • Converts configured rail labels to valid PaymentRail enum values while preserving human-readable UI labels.
  • Tracks destination-rail state across destination selection and swaps.
  • Emits the selected rail as destination.paymentRail for external fiat quote destinations.
  • Keeps the generated flow diagram and code panel synchronized with the selection.

Confidence Score: 5/5

The PR appears safe to merge, with destination-rail state, presentation, and generated quote output consistently wired to the documented API contract.

The selectable values match the PaymentRail enum, normal destination transitions keep the selected rail compatible with the current currency, and paymentRail is emitted only for external fiat account destinations.

Important Files Changed

Filename Overview
components/grid-visualizer/src/data/currencies.ts Replaces display-form rail strings with valid PaymentRail enum values and centralizes presentation labels.
components/grid-visualizer/src/hooks/useFlowBuilder.ts Adds destination-rail state and consistently initializes or recomputes it when selecting or swapping destinations.
components/grid-visualizer/src/lib/code-generator.ts Threads the destination rail into external fiat quote destinations using the schema-compatible singular paymentRail field.
components/grid-visualizer/src/lib/flow-path.ts Uses the selected destination rail in flow captions while formatting enum values for display.
components/grid-visualizer/src/app/page.tsx Connects destination-rail state to the destination card, flow panel, and code panel.
components/grid-visualizer/src/components/CodePanel/CodePanel.tsx Regenerates and remounts generated steps when the destination rail changes.
components/grid-visualizer/src/components/FlowPanel/FlowPanel.tsx Recomputes the visualized flow whenever the destination rail changes.
components/grid-visualizer/src/components/InputCard/InputCard.tsx Displays readable rail labels while preserving enum values in selection callbacks.
components/grid-visualizer/src/components/CurrencyPicker/CurrencyPicker.tsx Presents readable rail names and retains both enum and display forms as search keywords.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Destination rail dropdown] --> B[useFlowBuilder destRail]
  B --> C[FlowPanel]
  B --> D[CodePanel]
  C --> E[Funds out via display label]
  D --> F[generateSteps]
  F --> G{External fiat destination?}
  G -- Yes --> H[destination.paymentRail enum value]
  G -- No --> I[Omit paymentRail]
Loading

Reviews (1): Last reviewed commit: "Send the selected destination rail as de..." | Re-trigger Greptile

@shreyav
shreyav requested a review from patcapulong July 30, 2026 21:46
@shreyav
shreyav merged commit 5ebe971 into main Jul 31, 2026
8 checks passed
@shreyav
shreyav deleted the claude/visualizer-destination-payment-rail branch July 31, 2026 22:12
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.

3 participants