Skip to content

fix(withdraw): UK GBP + GB-IBAN-EUR + intra-SEPA bank withdrawals (PR B — frontend)#2263

Merged
kushagrasarathe merged 61 commits into
devfrom
hotfix/uk-gbp-withdraw-flow
Jun 24, 2026
Merged

fix(withdraw): UK GBP + GB-IBAN-EUR + intra-SEPA bank withdrawals (PR B — frontend)#2263
kushagrasarathe merged 61 commits into
devfrom
hotfix/uk-gbp-withdraw-flow

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Hotfix to main · pairs with peanut-api-ts #1042 (BANK_GB type — deploy BE first). Notion: https://app.notion.com/p/3848381175798172a9fbeca0b71a3f20

Ready for review. Core fix complete (T1.2 / T1.3 / T1.4 + tests). T2 (rail picker) deferred — not required for the money path.

Summary — the why

The withdraw flow keys the destination rail off country, hardcoding one rail per country. The UK has two (GBP Faster Payments + EUR SEPA), so UK users get stranded; ~1,266 users are enrolled in both. Customer bobbyfresco (~17d), also kamilagod96.

Supersedes #2173

Cherry-picked @Hugo0's #2173 commit (its getOfframpConfigFromAccount helper + the Claim-flow fix + test) — authorship preserved. #2173 only patched the Claim flow and its GB branch was dead (no account ever surfaced as 'gb' until the paired BANK_GB BE PR). Close #2173 as superseded by this PR.

Landed

  • fix(offramp): derive destination rail from account type, not country (PEANUT-API-5P/5M/5N) #2173's helper + Claim fix (cherry-picked).
  • T1.3 — drop the IBAN.country === selectedCountry false-reject; gate on real support (validateBankAccount → BE allowedCountries). Fixes intra-SEPA (German IBAN / Spain selected) and GB-IBAN-EUR.
  • T1.4 (92f078b5) — withdraw destinationDetails now derives currency/rail via getOfframpConfigFromAccount(account) (GB → GBP, tolerates the Prisma-shaped BANK_GB); removes the empty-default branch that produced "External account ID is missing.".
  • T1.2 (92f078b5) — the freshly-added-account fallback guards on bridgeAccountId, surfacing a retryable error instead of navigating to a confirm screen that dead-ends on "Bank account is missing.". Root cause was on the BE: Copy changes for deposit hotfix from prod #1042 now returns the projected wire shape (bridgeAccountId + legacy type + details/sortCode), commit 210bd6ea, so a fresh account carries what this flow reads.
  • ✅ Tests: getOfframpConfigFromAccount (projected + Prisma-shaped BANK_GB + Manteca-throw + country fallback); withdraw.utils IBAN→country; intra-SEPA / GB-IBAN cases.

Deferred

  • T2 — explicit "GBP · UK bank" / "EUR · SEPA" choice on the GB step. Not required for the money path (GBP is reached via sort-code entry, EUR-to-GB-IBAN via IBAN entry). Tracked as a follow-up.

Risk / deploy

  • Cross-repo deploy order (hard-gated): BE Copy changes for deposit hotfix from prod #1042 (+ migration add_bank_gb_account_type + backfill --apply, dry-run prod cohort = 4) first, then this. Until BANK_GB exists + the backfill runs, UK accounts still type as iban.
  • CI: required checks green (typecheck / unit / e2e / format / CodeQL / ci-success). The red eslint is pre-existing/advisory — every error is on lines this PR didn't touch (@/interfaces barrels, prior anys, unused rawData).

0xkkonrad and others added 30 commits June 16, 2026 15:58
Bumps src/content f204f50..5cbbe4f (peanut-content main). Headline change:
scrub the Argentina cardless cash-withdrawal ("Red ATM") marketing across
all 5 locales — it was promoted as a live feature but is being pulled. The
RedATM withdrawal flow in the app is untouched; this is marketing copy only.

Competitor/pain-point ATM copy is intentionally preserved (foreign-ATM fee
comparisons, Western Union, Revolut/BLIK).

Content-only: zero code paths touched. Supersedes the handrolled #2226 and
the stale auto/update-content PRs, which were based off main and showed
phantom code diffs against dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rogress bar)

Two cosmetic-but-confusing display bugs in the bill-split / request-pot UI,
both reported by Konrad:

1. Slider "snap to 50% → 49.98%": the slider snaps the thumb to a clean
   percentage on drag, but AmountInput re-derives the thumb position from a
   cent-floored amount ($33.37 pot → 50% = $16.685 → floored $16.68 → 49.98%),
   and the Slider's sync effect then clobbered the snapped value. The amount is
   correct (you can't pay half a cent); only the label drifted. The sync effect
   now keeps the thumb on a snap point through sub-cent drift.

2. Request-pot progress bar blank for pots that collected >= $1,000: the receipt
   passed the comma-grouped display string to Number() (Number("1,234.56") ===
   NaN), blanking the bar. Use the raw numeric totalAmountCollected, matching
   how `goal` already uses transaction.amount.

Display-only; no amounts or money paths change. Complements api-ts #1027 (the
send-link $0 phantom) as part of the same P2P display-correctness sweep.
Card payments couldn't be split. The "Split this bill" CTA + /request prefill +
auto-create already ship for QR pays (gated on isQRPayment); this extends the
gate to card spends so a Peanut-card purchase can seed a bill-split request
(amount = the spend's USD value, comment = "Bill split for {merchant}").

- isCardSpend predicate: CARD_SPEND_AUTH/CLEAR only — refunds + auth reversals
  excluded (you didn't pay those).
- Threaded through useReceiptViewModel; CTA gate is now (isQRPayment || isCardSpend)
  and also excludes failed/declined spends.
- Omit the merchant param when userName is the "Card payment" fallback so the
  comment isn't "Bill split for Card payment".

FE-only — reuses the existing /request creation + POST /requests.
…eRabbit)

A merchant name with reserved characters (e.g. "Tigers & Lions") would break the
`/request?amount=…&merchant=…` query string and corrupt the request prefill.
encodeURIComponent the merchant value. Addresses CodeRabbit's inline finding on
#2235.
fix(p2p): request-pot display bugs — slider 49.98% snap + $1k+ progress bar NaN
normalizePixPhoneNumber returned the raw input untouched when the cleaned value
already started with '+', so '+55-11-99999-9999' kept its separators and stayed
non-canonical — conflicting with the helper's contract and risking PIX-key
mismatches. Always return the separator-free +55 form. From release-PR #2236
CodeRabbit triage; adds the separator-with-+ regression cases the existing
+-prefix tests never exercised.
…zation

fix(pix): canonicalize +55 phone keys that already carry a + prefix
…uilder

The CTA's onClick inlined the /request URL build, including the two bug-prone
bits — the "Card payment" merchant fallback and the URL-encode CodeRabbit caught
on #2235. Extract to buildSplitBillRequestUrl and lock it with unit tests
(encode reserved chars, omit fallback, omit empty merchant). Pure refactor +
tests; no behavior change.
Fast-forward the Red ATM scrub bump from the intermediate 5cbbe4f to
the current peanut-content main tip 358b6a6, so dev matches mono-main
content (and converges with the main-targeted prod bump #2242 — keeps
the next dev->main release from re-reverting the Red ATM removal).

Content-only, no code paths touched.
Symmetric with the merchant encode — the param type allows string, so encode
amount defensively. No behavior change for the numeric amounts callers pass
(encodeURIComponent("12.5") === "12.5"); existing assertions unchanged.
test(split-bill): lock the Split-this-bill CTA URL builder
Mirrors the main-targeted companions (#2242). The 358b6a6 content bump
removes the avalanche deposit + withdraw marketing pages (avalanche is
advertised in SEO + DEPOSIT_RAILS but never wired into rhino.consts —
a false claim). Sync the code so the content gate passes:

- DEPOSIT_RAILS / verify-content RAIL_SLUGS: drop avalanche so the SEO
  route + sitemap stop emitting /deposit/via-avalanche
- redirects.json: 301 old via-avalanche + withdraw/avalanche -> /supported-networks
- .verify-content-baseline: 745 -> 740

ETH untouched (we support it). Deeper deposit-flow avalanche/scroll
cleanup stays with #2216 (overlaps these files -> trivial rebase).
The rail list lived in two places — DEPOSIT_RAILS (exchanges.ts) and a
hand-copied RAIL_SLUGS Set in scripts/verify-content.ts, guarded only by
a 'keep in sync' comment. Removing avalanche meant editing both; the
lists had already drifted before.

Extract to src/data/seo/deposit-rails.ts — an import-free module both
the app (routes + sitemap via exchanges.ts) and the standalone content
gate (verify-content.ts) import. Add/remove a rail in one place now.

No behaviour change: same 9 rails, same classification. (Registering
faster-payments + spei as rails is a follow-up — it's a content-input
regeneration job, not a code add: the from-→via- flip breaks generated
RelatedLinks that must be fixed at the mono input layer and re-mirrored.)
Missing `name` falls back to title-casing; missing `recommended_network`
falls back to 'arbitrum' (pickRecommendedNetwork), not title-casing.
Per CodeRabbit review.
…-atm

chore(content): bump src/content — remove Red ATM from marketing
Faster Payments (UK/GBP) and SPEI (MX/MXN) are live deposit rails but were
served as /deposit/from-{slug} (exchange framing) — inconsistent with
SEPA/ACH/wire. Register both in DEPOSIT_RAILS so the route + sitemap emit
/deposit/via-{slug}, bump src/content to the matching content fix (the 14
RelatedLinks now point to via-), and 301 the old from- URLs to via- for any
indexed/shared links.

Content side: peanut-content 358b6a6 -> 5eb8453 (mono@8a061d5) — flips the
links + adds FP/SPEI to the fiat-rail list in intent-taxonomy.md so
regeneration keeps them as rails. Verified: content gate green, typecheck +
src/data tests pass.
llms.txt was too short and undersold the product. Rewrite (GA-informed:
Argentina + Brazil dominate; mercadopago + pix are the top public pages) to
lead with the differentiators: pay PIX in Brazil with no CPF, pay MercadoPago
in Argentina with no DNI, the Peanut Card, then send/receive/deposit/withdraw,
how-it-works, and where-it-works (live vs roadmap).

Also fixes stale facts in llms-full.txt: smart accounts are on Arbitrum (not
'Base/Ethereum L2'); the card is virtual Visa only (not physical). No backend
provider names; 'ATM cash withdrawal' omitted (Red ATM was just killed from
marketing).
…allowlist

The card moved from a CARD_PIONEER allowlist (US+18 LATAM+10 African) to a
denylist: eligible everywhere except the issuer's ~17 prohibited-issuance
countries (China, India, Russia, Turkey, Vietnam, Iran, Israel, ...). Source of
truth: peanut-api-ts/src/card/geo-eligibility.ts. The earlier llms copy came
from product/card.md which is stale post-rehaul (flagged for update).
feat(seo): register Faster Payments + SPEI as deposit rails (via-, not from-)
…ale Avalanche FAQ

Carries forward the still-needed deposit-flow half of #2216 (the avalanche
*marketing* half already shipped via #2232):
- rhino.consts: remove SCROLL from SUPPORTED_EVM_CHAINS — Rhino's live config
  (getSupportedConfigs) no longer returns an SDA for it (audit 2026-06-11), and
  a deposit on an unsupported chain is silently lost. The chainId map keeps
  534352->SCROLL so any stray deposit stays identifiable for recovery.
- TokenAndNetworkConfirmationModal: compute the '+N EVM' overflow from the list
  length (was hardcoded '+4') so it stays correct, and drop the now-unused
  RHINO_SUPPORTED_CHAINS import.
- merchants.ts: drop Avalanche from the Argentina funding FAQ (false claim).

Supersedes #2216.
…removal

fix(deposit): stop offering Scroll deposits (silently lost) + drop stale Avalanche FAQ
Claim-link previews fell back to the generic Peanut image instead of
"X sent you $Y via Peanut". The claim page's generateMetadata +
force-dynamic were stripped for the native static-export build
(d204944), but scripts/native-build.js already strips them at build
time and restores the source afterwards — so the source-level removal
was redundant and silently degraded only the web build.

Restore generateMetadata, extracted into a unit-tested helper
(buildClaimMetadata) so the title/OG-image logic can't silently regress
again. Native build is unaffected: its P0_TRANSFORMS replaces this page
by path.
…y assertion

- import Claim from '@/components/Claim' (specific file) instead of the
  '@/components' barrel (eslint no-restricted-imports)
- clear the ENS resolution timeout after Promise.race settles so no timer
  is left pending per request (CodeRabbit)
- assert sendLinksApi.getByParams is never called on the early-return path (CodeRabbit)
fix(claim): restore dynamic social-preview metadata for claim links
The /api/og query-param contract (type/username/amount/token/isReceipt/
isPeanutUsername/isInvite) was hand-built in four places — claim, the
[...recipient] payment/profile page, receipts, and invites — so a renamed
or added param meant editing N sites with nothing catching a miss.

Extract one typed, unit-tested buildOgImageUrl() and route all four through
it. Claim + [...recipient] URLs are byte-identical to before; receipt +
invite produce the same params (order differs, semantically identical).
This also gives the previously-untested catch-all OG logic coverage via the
shared helper's tests. Follow-up to #2249.
refactor(og): single buildOgImageUrl() helper for /api/og links
Carries the Sumsub es-es correction into dev so the release ships fully-current
content. Clean dev-based bump (the auto-PR #2247 branched off main and couldn't
merge — that workflow bug is being fixed separately).
The content auto-bump opened PRs but never merged them — 9 piled up since
June 3, and they were branched off the default branch (main), which (a) let
code drift pollute them (#2226) and (b) made them conflict with dev once dev's
content moved (#2247).

Fix:
- Branch the bump off **dev** (the PR base) so the PR is content-only by
  construction and always cleanly mergeable.
- Auto-merge the PR, guarded so it only fires when the diff is *solely* the
  src/content pointer (anything else → left for human review).
- Create the PR with CONTENT_BOT_TOKEN (PAT) when present so CI triggers and
  auto-merge can gate on green checks; falls back to GITHUB_TOKEN (no regression).

Requires (repo admin, one-time): set CONTENT_BOT_TOKEN secret (PAT w/ peanut-ui
contents+PR write) + enable 'Allow auto-merge'. Takes effect once this reaches
main (repository_dispatch runs the workflow from the default branch).
…erge

ci(content): base auto-bump on dev + content-only auto-merge (stop the pile-up)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/__tests__/withdraw.utils.test.ts (1)

35-40: ⚡ Quick win

Strengthen IBAN-prefix assertions to truly lock the regression.

Line 35-40 currently checks only not.toBeNull(), which can still pass if prefixes map to the wrong country. Add a prefix-distinction assertion (e.g., GB result differs from DE result) so the test actually verifies derivation behavior.

Suggested test tightening
-        it('resolves the country from the IBAN prefix (GB/DE) and is whitespace-tolerant', () => {
-            // GB IBAN (Revolut-style) — the GB-IBAN-EUR case (Ibrahima).
-            expect(getCountryFromIban('GB33BUKB20201555555555')).not.toBeNull()
-            // DE IBAN with spaces — the intra-SEPA mismatch case.
-            expect(getCountryFromIban('DE89 3704 0044 0532 0130 00')).not.toBeNull()
-        })
+        it('resolves country from IBAN prefix (GB/DE) and is whitespace-tolerant', () => {
+            const gb = getCountryFromIban('GB33BUKB20201555555555')
+            const de = getCountryFromIban('DE89 3704 0044 0532 0130 00')
+            expect(gb).not.toBeNull()
+            expect(de).not.toBeNull()
+            expect(gb).not.toBe(de)
+        })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/__tests__/withdraw.utils.test.ts` around lines 35 - 40, The test
assertions in the IBAN prefix test only verify that getCountryFromIban returns
non-null values for both GB and DE IBANs, but do not actually validate that the
correct country codes are returned for each prefix. Replace the not.toBeNull()
assertions with explicit equality checks that verify
getCountryFromIban('GB33BUKB20201555555555') returns 'GB' and
getCountryFromIban('DE89 3704 0044 0532 0130 00') returns 'DE', ensuring the
test truly validates that different prefixes are correctly distinguished rather
than just confirming the function doesn't fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/AddWithdraw/DynamicBankAccountForm.tsx`:
- Around line 229-233: The country derivation logic at lines 229-233 uses
cleanedAccountNumber as the IBAN source, but the actual IBAN input can come from
data.iban or getValues('iban') at line 265, causing a mismatch. Ensure a single
normalized IBAN source is used consistently throughout the component when
deriving ibanCountryCode, ibanCountryName, and resolvedCountryCode. Determine
whether to use cleanedAccountNumber or the form value
(data.iban/getValues('iban')) as the authoritative IBAN source, and apply it
uniformly in all country derivation operations to prevent resolvedCountryCode
from becoming empty or incorrect.

---

Nitpick comments:
In `@src/utils/__tests__/withdraw.utils.test.ts`:
- Around line 35-40: The test assertions in the IBAN prefix test only verify
that getCountryFromIban returns non-null values for both GB and DE IBANs, but do
not actually validate that the correct country codes are returned for each
prefix. Replace the not.toBeNull() assertions with explicit equality checks that
verify getCountryFromIban('GB33BUKB20201555555555') returns 'GB' and
getCountryFromIban('DE89 3704 0044 0532 0130 00') returns 'DE', ensuring the
test truly validates that different prefixes are correctly distinguished rather
than just confirming the function doesn't fail.
🪄 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: 7f770d9c-6dea-4fc8-8f51-8107eaa23ba5

📥 Commits

Reviewing files that changed from the base of the PR and between 334d5b7 and 6a47aad.

📒 Files selected for processing (2)
  • src/components/AddWithdraw/DynamicBankAccountForm.tsx
  • src/utils/__tests__/withdraw.utils.test.ts

Comment thread src/components/AddWithdraw/DynamicBankAccountForm.tsx
@Hugo0

Hugo0 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — both addressed in 14188f5:

  • Preserve account.type through bank details (Claim flow): added type to all three bankDetails objects (new-account → response type, saved-account → account.type, guest → derived from response shape iban/clabe/sort_code→gb/account→us). getOfframpConfigFromAccount now routes by rail instead of falling back to country.
  • Single normalized IBAN source: normalizedIban = cleanedAccountNumber || (iban||'').replace(/\\s/g,''), used for all country derivation — no empty countryCode to Bridge.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@Hugo0 Thanks for the detailed breakdown — both fixes look well-reasoned:

  1. account.type propagation — covering all three bankDetails code paths (new-account uses response type, saved-account uses account.type, guest derives from response shape) ensures getOfframpConfigFromAccount always has the signal it needs to pick the correct rail without falling back to country.

  2. Normalized IBAN sourcenormalizedIban = cleanedAccountNumber || (iban||'').replace(/\s/g,'') as a single authoritative source is clean and prevents the empty countryCode edge case.

jjramirezn and others added 7 commits June 20, 2026 15:02
Advisory Complete now called handleStartAction (start-action), whose
submission never round-trips to Bridge. Switch both add-money and withdraw
bank pages to handleSelfHealResubmit('BRIDGE', advisory.requirementKey),
the path whose webhook completion relays answers to Bridge. Thread an
optional requirementKey through initiateSelfHealResubmission and
handleSelfHealResubmit to target the future-dated advisory requirement.

handleStartAction/startKycAction are now unused (kept for a focused
follow-up cleanup to avoid the isActionFlow cascade in this diff).
…bbit)

onCompleteNow now fires a real network call (self-heal resubmit), so rapid
clicks before isLoading disables the CTA could launch duplicate requests.
Add a completingRef in-flight guard.
…copy

fix(kyc): correct DUPLICATE_EMAIL copy — sign in / contact support
…empt

feat(kyc): skippable advisory pre-empt at add/withdraw (Bridge future-dated requirements)
The registered-name line added height to the bottom-pinned (mt-auto) value
block, pushing the PAN up into the hand artwork. Removing the two-line
Expiry/CVV labels (revealed + loading skeleton) reclaims that height; flexbox
shifts PAN + name back down. Values and ph-no-capture PII guards untouched.
Addresses CodeRabbit nit. The name is PII; asserting only the text would let a
future refactor move it outside ph-no-capture without failing a test. Lock the
class so the session-recording guard is regression-proof.
feat(card): show registered cardholder name on reveal
Hugo0 and others added 2 commits June 23, 2026 13:05
…o-dev-20260623

chore: back-merge main → dev (pre Sprint 148 release)
@Hugo0 Hugo0 changed the title fix(withdraw): UK GBP + GB-IBAN-EUR + intra-SEPA bank withdrawals (PR B — frontend) [WIP] fix(withdraw): UK GBP + GB-IBAN-EUR + intra-SEPA bank withdrawals (PR B — frontend) Jun 23, 2026
Hugo0 added 5 commits June 23, 2026 22:42
2026-06-02 21:24 UTC, PEANUT-API-5P/5M/5N. A user with a UK GBP bank
account tried to offramp USDC → EUR via SEPA. Bridge rejected with
"country is not supported for SEPA" — SEPA only credits EUR-denominated
Eurozone accounts, not GBP/UK.

Root cause was in BankFlowManager.handleCreateOfframpAndClaim:
    const destination = getOfframpCurrencyConfig(
        account.country ?? selectedCountry!.id
    )

When `account.country` is missing, the picker falls back to
`selectedCountry`. If `selectedCountry` doesn't match the saved-account's
actual type (e.g. user picks a random country, or country resolution is
buggy), the helper's "everything unknown → EUR/SEPA" default fires —
even for a clearly GBP/UK account.

The account's own `type` already carries the right answer for every
Bridge destination we support (us/gb/clabe/iban). Adding a tiny helper
`getOfframpConfigFromAccount` that derives currency+rail directly from
account.type closes this class of bug.

- `gb`  → gbp/faster_payments
- `us`  → usd/ach
- `clabe` → mxn/spei
- `iban` → eur/sepa
- `manteca` → throws (must use the Manteca offramp path, not Bridge)
- type missing → falls back to country-based picking (prior behavior)

The helper also accepts BE Prisma-shape suffixes (`BANK_IBAN`,
`BANK_ACCOUNT_GB`, etc.) so it doesn't matter which side of the API the
account row came from.

7 new unit tests in bridge.utils.test.ts cover:
- The GB regression (was EUR/SEPA, now GBP/faster_payments)
- All 4 supported account types map to the right rail
- BE Prisma-suffix shapes (BANK_IBAN, BANK_ACCOUNT_GB)
- Manteca throws to surface a wrong-path call early
- Missing-type fallback to country picking still works

All 71 bridge.utils tests passing.

Companion PR: peanut-api-ts #964 — surfaces Bridge's actual error
message in Sentry + stops Discord-paging on user-input 4xx errors.
Even after this FE fix, other user-input mistakes (e.g. a Bridge
customer status change mid-flow) shouldn't page on-call.
The bank-details form rejected any IBAN whose country didn't equal the
country selected on the previous screen ("IBAN does not match the selected
country"). SEPA routes by IBAN, so the dropdown is cosmetic for a EUR payout —
this gate blocked two legit cases: a German IBAN with Spain selected, and a UK
user withdrawing EUR to a GB IBAN (Ibrahima / bobbyfresco).

Gate on actual support instead: validateBankAccount() hits the BE
allowedCountries check (SEPA/US/CA), so structurally-valid-but-unsupported
IBANs are still rejected — just with an honest "not supported" message rather
than a false country-mismatch. Drops the now-orphaned getCountryFromIban import.
…ropdown

Follow-on to dropping the IBAN-country gate: the add-bank-account payload set
countryCode/countryName/address.country from the country picked on the previous
screen. With the gate gone, a German IBAN entered under "Spain" would reach
Bridge with countryCode=ESP and 400. SEPA routes by IBAN, so derive all three
country fields from the IBAN itself for IBAN accounts — keeping the payload
internally consistent exactly as the old equality gate guaranteed, just sourced
from the IBAN. Unblocks the GB-IBAN-EUR case (GB IBAN -> GBR) too.

Adds unit coverage for getCountryCodeForWithdraw (GB->GBR/DE->DEU/ES->ESP/US->USA,
idempotent on ISO-3) and getCountryFromIban, the derivation the fix relies on.
…IBAN source

CodeRabbit (2 major):
- BankFlowManager: the bankDetails objects (new-account, saved-account, guest)
  dropped `type`, so getOfframpConfigFromAccount() fell back to country routing —
  defeating the derive-from-type fix. Carry `type` through all three (guest path
  derives it from the response shape: iban/clabe/sort_code→gb/account→us).
- DynamicBankAccountForm: derive the IBAN country from a single normalized source
  (cleanedAccountNumber, falling back to the `iban` form value) so countryCode is
  never derived off an empty string → no empty countryCode to Bridge.
T1.4: destinationDetails derived the currency/rail from a country switch
whose default returned an empty rail, so a UK account typed anything but GB
(pre-BANK_GB mistype, or a Prisma-shaped 'BANK_GB') dead-ended on 'External
account ID is missing.'. Derive from the account via getOfframpConfigFromAccount
(GB->GBP), consistent with the claim flow.

T1.2: harden the freshly-added-account fallback — if the add-bank-account
response lacks bridgeAccountId, surface a retryable error instead of
navigating to a confirm screen that dead-ends on 'Bank account is missing.'.
@Hugo0 Hugo0 changed the base branch from main to dev June 24, 2026 05:42
@Hugo0 Hugo0 force-pushed the hotfix/uk-gbp-withdraw-flow branch from 92f078b to 48e749d Compare June 24, 2026 05:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (1)
src/components/Card/CardFace.tsx (1)

141-150: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Preserve semantic labels for expiry/CVV with sr-only text.

Dropping visual labels is okay, but adding hidden labels keeps screen-reader context intact without changing layout.

Suggested patch
-                                        <div className="ph-no-capture font-bold">
+                                        <div className="ph-no-capture font-bold">
+                                            <span className="sr-only">Expiry </span>
                                             {String(revealed.expiryMonth).padStart(2, '0')}/
                                             {String(revealed.expiryYear).slice(-2)}
                                         </div>
@@
-                                            <div className="ph-no-capture font-bold">{revealed.cvv}</div>
+                                            <div className="ph-no-capture font-bold">
+                                                <span className="sr-only">CVV </span>
+                                                {revealed.cvv}
+                                            </div>

Also applies to: 183-189

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Card/CardFace.tsx` around lines 141 - 150, The expiry and CVV
fields in CardFace should keep accessible context while remaining visually
label-free; update the rendered expiry/CVV sections to include hidden sr-only
text instead of removing the labels entirely. Use the existing CardFace markup
around the expiry block and the CVV block so screen readers still announce
meaningful labels without affecting layout, and apply the same fix to both
occurrences referenced in the component.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/update-content.yml:
- Around line 21-23: The checkout step in update-content workflow still uses a
mutable actions/checkout version and persists the default token. Update the
actions/checkout reference in the workflow to a pinned audited commit SHA, and
add persist-credentials: false so the checkout step does not leave write-capable
credentials behind. Keep the change focused on the existing checkout block that
currently sets ref: dev.

In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 121-129: The bank flow replays a deferred advisory confirmation
without re-validating the current gate state, so a stale ready+advisory snapshot
can still reach createOnramp() after the gate changes. Update
handleWarningConfirm() (and the callback path coming from useAdvisoryPreempt) to
re-check gate before proceeding, and if the gate is no longer ready, route back
through the warning/modal state instead of calling createOnramp(). Use the same
defensive pattern as the withdraw flow’s proceedWithOfframp() so the replayed
callback cannot bypass newly blocking KYC/ToS conditions.

In `@src/app/`(mobile-ui)/home/page.tsx:
- Around line 97-101: Gate the celebration modal in the home page effect on the
user record being loaded: in the useEffect around setShowKycModal, do not treat
user === undefined as “not celebrated yet” while fetchUser() is pending. Update
the logic in home/page.tsx so it only opens the modal when user is present and
isKycApproved is true and activationCelebratedAt is still empty, and also derive
the modal state from that condition on each run instead of only calling
setShowKycModal(true) so it can stay closed once activationCelebratedAt arrives.

In `@src/app/`[...recipient]/page.tsx:
- Around line 103-115: The receipt OG image generation in the page component is
using recipient even after chargeDetails has already resolved the correct
username for paid charges. Update the buildOgImageUrl call in the page route to
pass the resolved username variable for receipt cases (especially when chargeId
and isPaid are set), while preserving recipient for non-receipt request cards.
Use the existing username resolution logic in the same component to locate the
right value.

In `@src/app/receipt/`[entryId]/page.tsx:
- Around line 143-158: The OG preview label is hard-coded to USDC in the receipt
page while the displayed amount is derived from the transaction data, so the
asset name can drift from what generateReceiptTitle() uses. Update the
buildOgImageUrl call in the receipt page to pass the same currency/token label
source used by the title logic (for example the transaction’s currency.code or
tokenSymbol), and only fall back to USDC when that source is unavailable.

In `@src/components/TransactionDetails/TransactionDetailsReceipt.tsx`:
- Around line 321-324: The progress value in TransactionDetailsReceipt should
preserve the zero fallback for empty request pots instead of converting the raw
field directly. Update the progress calculation near the request-pot UI to use
the existing normalized total amount value or otherwise default missing
totalAmountCollected to 0 before calling Number, so brand-new pots do not
produce NaN and blank the progress bar.

In `@src/hooks/useAdvisoryPreempt.ts`:
- Around line 43-53: Keep the advisory retryable if onCompleteNow() fails by
deferring the dismissal and cleanup in completeNow until after the await
succeeds, or restoring visible/dismissed state in the catch path. Update the
useCallback in useAdvisoryPreempt so setDismissed, setVisible, and
pendingProceed.current = null only happen after a successful onCompleteNow()
call, ensuring a failed network request doesn’t permanently skip the advisory
for the session.

In `@src/hooks/useSumsubKycFlow.ts`:
- Around line 410-420: Persist the targeted action context used by
handleSelfHealResubmit so refreshToken() can reuse it after expiry. Store the
incoming requirementKey (and any action key derived in the same flow) in a ref
or state alongside selfHealProviderRef, then update refreshToken() to pass that
saved context into initiateSelfHealResubmission instead of always falling back
to the generic provider-only call or initiateSumsubKyc. Make sure the Sumsub
start flow and the Bridge/self-heal path both read from the same persisted
context so the session refreshes back into the same advisory/action.

---

Nitpick comments:
In `@src/components/Card/CardFace.tsx`:
- Around line 141-150: The expiry and CVV fields in CardFace should keep
accessible context while remaining visually label-free; update the rendered
expiry/CVV sections to include hidden sr-only text instead of removing the
labels entirely. Use the existing CardFace markup around the expiry block and
the CVV block so screen readers still announce meaningful labels without
affecting layout, and apply the same fix to both occurrences referenced in the
component.
🪄 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: 4cac8b3f-32ce-40a6-8e69-0374963ac3f6

📥 Commits

Reviewing files that changed from the base of the PR and between 92f078b and 48e749d.

📒 Files selected for processing (60)
  • .github/workflows/update-content.yml
  • .verify-content-baseline
  • e2e/flows/add-money.spec.ts
  • eslint.config.js
  • public/llms-full.txt
  • public/llms.txt
  • redirects.json
  • scripts/verify-content.ts
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/claim/page.tsx
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/[...recipient]/page.tsx
  • src/app/actions/sumsub.ts
  • src/app/invite/page.tsx
  • src/app/m/[slug]/merchants.ts
  • src/app/receipt/[entryId]/page.tsx
  • src/components/AddMoney/components/MantecaAddMoney.tsx
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx
  • src/components/AddWithdraw/DynamicBankAccountForm.tsx
  • src/components/Card/CardFace.tsx
  • src/components/Card/__tests__/CardFace.test.tsx
  • src/components/Claim/Link/views/BankFlowManager.view.tsx
  • src/components/Global/NavHeader/index.tsx
  • src/components/Global/Slider/__tests__/index.test.tsx
  • src/components/Global/Slider/index.tsx
  • src/components/Global/TokenAndNetworkConfirmationModal/index.tsx
  • src/components/Kyc/AdvisoryPreemptModal.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/__tests__/splitBill.utils.test.ts
  • src/components/TransactionDetails/__tests__/transaction-predicates.test.ts
  • src/components/TransactionDetails/splitBill.utils.ts
  • src/components/TransactionDetails/transaction-predicates.ts
  • src/components/TransactionDetails/useReceiptViewModel.ts
  • src/constants/rhino.consts.ts
  • src/constants/sumsub-reject-labels.consts.ts
  • src/content
  • src/data/seo/deposit-rails.ts
  • src/data/seo/exchanges.ts
  • src/data/seo/index.ts
  • src/hooks/useAdvisoryPreempt.test.ts
  • src/hooks/useAdvisoryPreempt.ts
  • src/hooks/useMultiPhaseKycFlow.ts
  • src/hooks/useSumsubKycFlow.ts
  • src/interfaces/interfaces.ts
  • src/services/rain.ts
  • src/types/api.generated.ts
  • src/types/api.openapi.json
  • src/types/capabilities.ts
  • src/utils/__tests__/bridge.utils.test.ts
  • src/utils/__tests__/claim-metadata.utils.test.ts
  • src/utils/__tests__/og.utils.test.ts
  • src/utils/__tests__/withdraw.utils.test.ts
  • src/utils/bridge.utils.ts
  • src/utils/capability-gate.test.ts
  • src/utils/capability-gate.ts
  • src/utils/claim-metadata.utils.ts
  • src/utils/og.utils.ts
  • src/utils/sentry.utils.ts
  • src/utils/withdraw.utils.ts
✅ Files skipped from review due to trivial changes (8)
  • src/components/Global/Slider/tests/index.test.tsx
  • .verify-content-baseline
  • src/types/api.generated.ts
  • src/data/seo/deposit-rails.ts
  • src/data/seo/index.ts
  • src/constants/sumsub-reject-labels.consts.ts
  • src/app/m/[slug]/merchants.ts
  • src/components/Global/NavHeader/index.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/utils/tests/withdraw.utils.test.ts
  • src/utils/tests/bridge.utils.test.ts
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx
  • src/utils/bridge.utils.ts
  • src/components/AddWithdraw/DynamicBankAccountForm.tsx
  • src/components/Claim/Link/views/BankFlowManager.view.tsx

Comment thread .github/workflows/update-content.yml
Comment thread src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Comment thread src/app/(mobile-ui)/home/page.tsx
Comment thread src/app/[...recipient]/page.tsx
Comment thread src/app/receipt/[entryId]/page.tsx
Comment thread src/components/TransactionDetails/TransactionDetailsReceipt.tsx
Comment thread src/hooks/useAdvisoryPreempt.ts
Comment thread src/hooks/useSumsubKycFlow.ts
@kushagrasarathe kushagrasarathe merged commit 9a3f743 into dev Jun 24, 2026
23 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants