Skip to content

feat(card): geo-blocked state — stop prohibited-country users before the apply funnel - #2620

Open
jjramirezn wants to merge 6 commits into
devfrom
feat/card-geo-gate-2
Open

feat(card): geo-blocked state — stop prohibited-country users before the apply funnel#2620
jjramirezn wants to merge 6 commits into
devfrom
feat/card-geo-gate-2

Conversation

@jjramirezn

@jjramirezn jjramirezn commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Successor of #2387 (same tree merged with dev — re-pushed as a new branch because dev's unsigned commits trip the signed-commits repo rule on an existing branch).

Summary

The /card state machine ignored country eligibility: users from Rain's prohibited-issuance countries were teased with the press-and-hold "see if you qualify" gate and could run the whole KYC funnel before Rain rejected them at the end.

  • computeCardState: new geo-blocked state, keyed on the BE geoProhibited field (country KNOWN and prohibited — never blocks on unknown; missing field ⇒ not blocked, covered by tests).
  • ApplicationStatusScreen: new geo-blocked variant — regional copy (no country names, per Rain marketing-compliance), crying mascot, NO contact-support CTA (regulation is not a support case), and a link to the Prohibited Activities Policy (the one compliant place the country list is published). Localized en / es-419 / pt-BR.
  • card/page.tsx: a mid-funnel geo-blocked apply response renders the terminal screen and clears pending overlays.

⚠️ Contract gap — needs a BE companion before this is live

The current peanut-api-ts dev does NOT emit geoProhibited (the geo gate that landed server-side blocks at POST /rain/cards + lifecycle, but exposes no read-model field). As shipped, this PR is inert-but-safe: the field is absent ⇒ every state resolves exactly as before (tested). The funnel gate only activates when a BE companion adds geoProhibited to the card read model.

Consequences to be aware of:

  • sync-openapi churn: the weekday sync workflow regenerates src/types/ from staging and will open a PR stripping the hand-declared geoProhibited until the BE ships it. Don't merge that sync PR blindly, or coordinate this merge with the BE companion.
  • The spec declares geoProhibited optional (matches services/card.ts), so FE code is honest about the field's absence.

Design notes / accepted trade-offs

  • Policy link hardcodes /en/ — the content page exists English-only (mirrors CardTermsScreen's absolute-URL pattern).
  • geoBlocked overlay state is useState, not URL state — it's a server verdict, not shareable navigation state.
  • Follow-up (not this PR): handleSumsubRefreshToken re-implements advanceFromApplyResponse's status fan-out; third copy grew here — worth routing through one function.

Screenshots

⚠️ NONE — the geo-blocked state is unreachable against any real backend today (see contract gap; no BE emits the field, and dev cheats can't shape it). Rendering is covered by 12 jest tests asserting copy, CTA absence, and the policy link for the new variant.

QA

npm test (203 suites), typecheck, prettier green locally; the one failing local suite (add-money-states) is untouched by this branch and passes in CI. Mobile-first layout per Bruddle rules.

Docs follow-up (separate, via update-content — never bundled here)

The geo-block changes what card availability copy can say. Stale after this ships with its BE companion: product/card.md, product/quick-ref.md, product/countries.md, product/support-answers/card-country-restrictions.md (53-country list diverges from the 17 enforced), card-not-showing-after-kyc.md, content/_system/context/messaging.md (guardrail 12), content/help/peanut-card/*, card-payments/*, supported-geographies/* (all locales), content/countries/en.md. Note: content/legal/card-prohibited-activities/en.md lists the same 17 countries but omits the UK block.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added regional eligibility checks for card applications.
    • Applicants in prohibited regions now see a dedicated restriction screen with localized messaging and a link to the prohibited-activities policy.
    • Added support for English, Latin American Spanish, and Brazilian Portuguese.
  • Bug Fixes
    • Geo-blocked applications now correctly clear pending overlays and refresh card status.
    • Existing cardholders and re-issue flows remain unaffected.

…the apply funnel

The /card state machine ignored country eligibility: users from Rain's
prohibited-issuance countries were teased with the press-and-hold
"see if you qualify" gate and could run the whole KYC funnel before Rain
rejected them.

- computeCardState: new 'geo-blocked' state, keyed on BE's new
  geoProhibited field (country KNOWN and prohibited — never blocks on
  unknown, so pre-KYC users still enter). Scoped to users with no
  existing application: in-flight/rejected rails keep their truthful
  state and the re-issue path stays untouched, mirroring the BE gate.
- ApplicationStatusScreen: 'geo-blocked' variant — regulatory dead end,
  no support CTA (support can't override regulation).
- api.openapi.json patched surgically with just the new field (the
  committed copy has pre-existing drift; full re-sync left for its own PR).
…unnel

/code-review findings:

- A Sumsub-only user (country unknown up front, so geoProhibited couldn't
  block them) who completes KYC with a prohibited-country address got a
  generic applyError on add-card — an apply button that can never
  succeed. The BE now answers a typed 'geo-blocked' status; handle it in
  the apply/poll/refresh paths with a local flag that renders the
  terminal screen immediately, plus a cardInfo refetch so the state
  machine owns the block on later visits.
- Component test pinning the geo-blocked no-support-CTA contract
  (support cannot override regulation; guards SUPPORT_VARIANTS drift).
- Straight apostrophe in the geo-blocked body copy (matched the rest of
  the COPY table).
…d screen

A geo-blocked user had no way to see WHICH regions are restricted: Rain's
marketing-compliance rules ban country names on card-marketing surfaces
(help articles included), so the full issuance denylist is published in
exactly one compliant place — the legal Prohibited Activities Policy
(§1 Restricted Countries). Link it from the geo-blocked screen, the one
surface where a user actually needs it. Tests pin the href and that the
link stays off other variants.
pre-commit scan false positive: flagged value is a hardcoded test fixture constant from dev's side of the merge, already in dev history.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 5, 2026 8:26pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62662d6b-5c6c-4968-adb8-8d0892117cf0

📥 Commits

Reviewing files that changed from the base of the PR and between 74c5985 and a97af70.

⛔ Files ignored due to path filters (1)
  • src/types/api.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (11)
  • src/app/(mobile-ui)/card/page.tsx
  • src/components/Card/ApplicationStatusScreen.tsx
  • src/components/Card/__tests__/ApplicationStatusScreen.test.tsx
  • src/components/Card/__tests__/cardState.utils.test.ts
  • src/components/Card/cardState.utils.ts
  • src/i18n/app/messages/en.json
  • src/i18n/app/messages/es-419.json
  • src/i18n/app/messages/pt-BR.json
  • src/services/card.ts
  • src/services/rain.ts
  • src/types/api.openapi.json

📝 Walkthrough

Walkthrough

The card flow now detects prohibited residence countries from card and application responses. It exposes a geo-blocked state, prioritizes its regulatory screen, refetches card data, and displays localized messaging with a policy link.

Changes

Geo-blocked card flow

Layer / File(s) Summary
Geo-blocked contracts and card state
src/types/api.openapi.json, src/services/card.ts, src/services/rain.ts, src/components/Card/cardState.utils.ts, src/components/Card/__tests__/cardState.utils.test.ts
The API and service types expose geoProhibited and the geo-blocked response. Card state returns geo-blocked before eligibility checks when no application rail exists. Tests cover blocking precedence and existing application paths.
Application response handling
src/app/(mobile-ui)/card/page.tsx
The card page handles geo-blocked application and Sumsub responses, clears pending overlays, refetches card information, and renders the blocked state before other overlays.
Localized geo-blocked status presentation
src/components/Card/ApplicationStatusScreen.tsx, src/components/Card/__tests__/ApplicationStatusScreen.test.tsx, src/i18n/app/messages/*.json
The status screen adds localized geo-blocked copy, the prohibited-activities policy link, crying mascot rendering, and no support CTA. Tests cover the rendered content and link behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CardPage
  participant Rain
  participant Sumsub
  participant CardInfo
  participant ApplicationStatusScreen
  Rain->>CardPage: return geo-blocked application response
  CardPage->>CardPage: clear pending overlays and activate geoBlocked
  CardPage->>CardInfo: refetch card information
  Sumsub->>CardPage: return geo-blocked token response
  CardPage->>CardInfo: refetch card information
  CardPage->>ApplicationStatusScreen: render geo-blocked status
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: innolope-dev, hugo0, kushagrasarathe

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the geo-blocked card state and its purpose of preventing prohibited-country users from entering the application funnel.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/card-geo-gate-2

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7055.64 → 7057.91 (+2.27)
Findings: 0 net (+159 new, -159 resolved)

🆕 New findings (159)

  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 132, MI 56.28, SLOC 513
  • critical complexity — src/services/rain.ts — CC 67, MI 59.71, SLOC 268
  • high structural-dup — types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9524
  • high structural-dup — types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9610
  • high structural-dup — types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9201
  • high structural-dup — types/api.generated.ts:9146 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9298
  • high structural-dup — types/api.generated.ts:9146 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9469
  • high structural-dup — types/api.generated.ts:9146 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9727
  • high hotspot — src/app/(mobile-ui)/card/page.tsx — 45 commits, +1098/-487 lines since 6 months ago
  • high hotspot — src/services/rain.ts — 30 commits, +907/-126 lines since 6 months ago
  • medium high-mdd — src/app/(mobile-ui)/card/page.tsx:56 — CardPage: MDD 167.7 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/card/page.tsx:56 — CardPage: DLT 61 (calls 61 distinct functions — high context load)
  • medium structural-dup — types/api.generated.ts:3207 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6591
  • medium structural-dup — types/api.generated.ts:3207 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7166
  • medium structural-dup — types/api.generated.ts:7243 — 40 duplicate lines / 108 tokens with types/api.generated.ts:7315
  • medium structural-dup — types/api.generated.ts:9161 — 40 duplicate lines / 120 tokens with types/api.generated.ts:9398
  • medium structural-dup — types/api.generated.ts:7243 — 39 duplicate lines / 109 tokens with types/api.generated.ts:7389
  • medium structural-dup — types/api.generated.ts:8788 — 39 duplicate lines / 116 tokens with types/api.generated.ts:8938
  • medium structural-dup — types/api.generated.ts:9825 — 39 duplicate lines / 114 tokens with types/api.generated.ts:9910
  • medium structural-dup — types/api.generated.ts:139 — 38 duplicate lines / 147 tokens with types/api.generated.ts:9438

…and 139 more.

✅ Resolved (159)

  • src/app/(mobile-ui)/card/page.tsx — CC 128, MI 56.73, SLOC 495
  • src/services/rain.ts — CC 67, MI 59.72, SLOC 268
  • types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9519
  • types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9605
  • types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9196
  • types/api.generated.ts:9141 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9293
  • types/api.generated.ts:9141 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9464
  • types/api.generated.ts:9141 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9722
  • src/app/(mobile-ui)/card/page.tsx — 43 commits, +1064/-485 lines since 6 months ago
  • src/app/(mobile-ui)/card/page.tsx:56 — CardPage: MDD 156.3 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:56 — CardPage: DLT 60 (calls 60 distinct functions — high context load)
  • types/api.generated.ts:3207 — 49 duplicate lines / 139 tokens with types/api.generated.ts:6586
  • types/api.generated.ts:3207 — 49 duplicate lines / 137 tokens with types/api.generated.ts:7161
  • types/api.generated.ts:7238 — 40 duplicate lines / 108 tokens with types/api.generated.ts:7310
  • types/api.generated.ts:9156 — 40 duplicate lines / 120 tokens with types/api.generated.ts:9393
  • types/api.generated.ts:7238 — 39 duplicate lines / 109 tokens with types/api.generated.ts:7384
  • types/api.generated.ts:8783 — 39 duplicate lines / 116 tokens with types/api.generated.ts:8933
  • types/api.generated.ts:9820 — 39 duplicate lines / 114 tokens with types/api.generated.ts:9905
  • types/api.generated.ts:139 — 38 duplicate lines / 147 tokens with types/api.generated.ts:9433
  • types/api.generated.ts:1716 — 38 duplicate lines / 126 tokens with types/api.generated.ts:7053

…and 139 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Card/ApplicationStatusScreen.tsx 10.7 11.6 +0.9
src/app/(mobile-ui)/card/page.tsx 17.1 17.6 +0.5

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2645 ran, 0 failed, 0 skipped, 47.3s

📊 Coverage (unit)

metric %
statements 64.0%
branches 47.7%
functions 53.6%
lines 64.6%
⏱ 10 slowest test cases
time test
3.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/utils/__tests__/auth-token.test.ts › removes the guarded item and marker and releases parked callers
0.3s src/utils/__tests__/auth-token.test.ts › returns the token hydrated from Preferences after authReady
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@jjramirezn

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…sive service type

The hand-declared spec marked geoProhibited required while src/services/card.ts declares it optional — and the current BE does not emit it yet (FE-first deploy). Optional is the honest contract and reduces sync-openapi churn direction.
@jjramirezn
jjramirezn marked this pull request as ready for review August 5, 2026 20:46
@jjramirezn

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@jjramirezn
jjramirezn requested a review from Hugo0 August 5, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant