Skip to content

fix: cross-region KYC locks previously unlocked regions#1872

Merged
Hugo0 merged 1 commit intomainfrom
fix/cross-region-lock
Apr 5, 2026
Merged

fix: cross-region KYC locks previously unlocked regions#1872
Hugo0 merged 1 commit intomainfrom
fix/cross-region-lock

Conversation

@kushagrasarathe
Copy link
Copy Markdown
Contributor

@kushagrasarathe kushagrasarathe commented Apr 4, 2026

problem

after PR #641 (moveToLevel for cross-region KYC), users who complete verification for a second region lose access to their first region. this affected users

root cause: isRegionUnlocked in useIdentityVerification.tsx scoped region access by sumsubVerificationRegionIntent. when the intent was LATAM, it returned false for bridge regions without checking hasProviderAccess('BRIDGE') — even when bridge rails were ENABLED. same issue in reverse: STANDARD intent never checked manteca for LATAM regions.

// before (broken)
if (sumsubVerificationRegionIntent === 'LATAM') {
    if (MANTECA_SUPPORTED_REGIONS.includes(regionName)) return true
    return false  // ← locks bridge regions even if rails are ENABLED
}
return hasProviderAccess('BRIDGE') && BRIDGE_SUPPORTED_REGIONS.includes(regionName)
// ← never checks manteca for LATAM regions

fix

check provider access independently of sumsub regionIntent:

  • bridge regions: unlock based on hasProviderAccess('BRIDGE') regardless of intent
  • latam: unlock if sumsubVerificationRegionIntent === 'LATAM' (immediate, while manteca rails still pending) OR isMantecaApproved (after manteca submission completes)
  • rest of world: always unlocked with any sumsub approval

existing affected users will auto-fix on deploy — their bridge/manteca rails are already ENABLED.

test scenarios

  • user with SUMSUB APPROVED (LATAM) + bridge rails ENABLED → europe unlocked ✓
  • user with SUMSUB APPROVED (STANDARD) + manteca ACTIVE → latam unlocked ✓
  • first-time user with SUMSUB APPROVED (STANDARD) + no manteca → latam locked ✓
  • first-time user with SUMSUB APPROVED (LATAM) + no bridge rails → europe locked ✓

when sumsubVerificationRegionIntent was LATAM, isRegionUnlocked returned
false for bridge regions without checking hasProviderAccess('BRIDGE').
this locked users out of europe/NA after completing cross-region kyc
even when their bridge rails were ENABLED.

now checks each provider's rails independently: bridge regions unlock
based on bridge rail status, latam unlocks based on sumsub intent or
manteca approval. fixes the region-lock reported for fabio, federico,
and matias after PR #641 (moveToLevel) was merged.
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Apr 4, 2026 5:35pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Walkthrough

The change modifies the isRegionUnlocked function within a custom React hook to alter how Sumsub approval unlocks regions. When Sumsub approval is active, REST OF WORLD regions remain unconditionally unlocked, BRIDGE-supported regions now unlock purely based on BRIDGE provider access regardless of intent, and MANTECA-supported regions unlock if LATAM intent is set or Manteca approval is granted.

Changes

Cohort / File(s) Summary
Region Unlock Logic Update
src/hooks/useIdentityVerification.tsx
Modified conditional logic for isRegionUnlocked when isSumsubApproved is true. Decoupled BRIDGE region unlocking from sumsubVerificationRegionIntent and added combined condition for MANTECA regions (LATAM intent OR Manteca approval). Non-Sumsub approval pathways remain unchanged.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: restoring access to previously unlocked regions affected by cross-region KYC changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description clearly explains the problem (users losing access to first region after cross-region KYC), root cause (regionIntent scoping bug), fix (independent provider access checking), and includes specific test scenarios.

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

✨ 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 fix/cross-region-lock

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

@kushagrasarathe
Copy link
Copy Markdown
Contributor Author

@coderabbitai review in depth.

Evaluate:

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

Also, write a very concise testing guide for QA

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Tip

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

🧩 Analysis chain

Let me gather the PR diff and relevant file contents first.


🏁 Script executed:

#!/bin/bash
# Fetch PR details and diff
gh pr view 1872 --repo peanutprotocol/peanut-ui --json title,body,files,additions,deletions

echo "=== PR DIFF ==="
gh pr diff 1872 --repo peanutprotocol/peanut-ui

Length of output: 4222


🏁 Script executed:

#!/bin/bash
# Read the full file
cat src/hooks/useIdentityVerification.tsx

Length of output: 11815


@kushagrasarathe Here's a thorough review of PR #1872.


🔍 In-Depth Review

Bug Fix Correctness ✅

The restructuring of isRegionUnlocked correctly addresses the root cause. I traced all four test scenarios against the new logic:

Scenario Key Vars LATAM Bridge Regions Result
LATAM-intent + bridge rails ENABLED isSumsubApproved=true, intent='LATAM', hasProviderAccess('BRIDGE')=true ✅ unlocked ✅ unlocked
STANDARD-intent + manteca ACTIVE isSumsubApproved=true, intent='STANDARD', isMantecaApproved=true ✅ unlocked ✅ unlocked
STANDARD-intent + no manteca isSumsubApproved=true, intent='STANDARD', isMantecaApproved=false 🔒 locked depends on rails
LATAM-intent + no bridge rails isSumsubApproved=true, intent='LATAM', hasProviderAccess('BRIDGE')=false ✅ unlocked 🔒 locked

The old code used sumsubVerificationRegionIntent as a gate before checking provider access, which was the bug. The new code decouples them correctly — each provider is checked independently.


Risk Assessment — Low 🟢

1. Regression: non-sumsub users (legacy Bridge-only)
The fallback path is unchanged:

return (
    (isBridgeApproved && BRIDGE_SUPPORTED_REGIONS.includes(regionName)) ||
    (isMantecaApproved && MANTECA_SUPPORTED_REGIONS.includes(regionName))
)

Legacy bridge-KYC users who never went through the sumsub flow are unaffected.

2. Regression: QR-only LATAM access

if (isBridgeApproved && !isMantecaApproved && !isSumsubApproved) {
    unlocked.push(...MANTECA_QR_ONLY_REGIONS, ...BRIDGE_SUPPORTED_LATAM_COUNTRIES)
}

This block is unchanged — Bridge users without Manteca still get Argentina/Brazil QR access.

3. One nuance worth noting: sumsubVerificationRegionIntent === 'LATAM' unlocks the LATAM region immediately, even before Manteca rails are active. This is intentional (QR payments work via superuser fallback per prior behavior), but it means a LATAM-intent sumsub user sees LATAM unlocked even if they never complete Manteca onboarding. This is a pre-existing design decision, not a regression.

4. return false at the bottom of the isSumsubApproved block is unreachable for the current 4 regions (all handled above it), but acts as a safe guard for any future region additions. No issue.


Performance Impact — None 🟢

  • The change is a pure logic refactor inside an existing useMemo
  • hasProviderAccess (a .filter() + .some() on user?.rails) is called the same or fewer times
  • useMemo dependency array is unchanged: [isUserBridgeKycApproved, isUserMantecaKycApproved, isUserSumsubKycApproved, sumsubVerificationRegionIntent, user?.rails]
  • Zero new API calls, state, or subscriptions

Code Quality / DRY ✅

The new structure is strictly better:

  • Flatter — removed nesting that hid the bridge lockout bug
  • More readable — each provider has an isolated, self-contained check
  • Old misleading comment removed — the removed comment said "sumsub approval scoped by regionIntent," which was no longer true after the fix

Minor nit: MANTECA_SUPPORTED_REGIONS = ['LATAM'] is a single-element array used in .includes(). It reads fine but a named constant const LATAM_REGION_NAME = 'LATAM' would be slightly more direct. Not a blocker.


⚠️ One Thing to Verify

isMantecaApproved comes from isUserMantecaKycApproved in useKycStatus. Separately, isVerifiedForCountry uses the stricter check v.status === MantecaKycStatus.ACTIVE. If isUserMantecaKycApproved in useKycStatus accepts a broader set of statuses (e.g. PENDING), a user with a pending manteca application could briefly see LATAM unlocked. This is pre-existing behavior unrelated to this PR, but worth a quick confirm.


🧪 Concise QA Testing Guide

Setup: Have test accounts in each state below; deploy to staging.

# Account State Expected Result
1 Sumsub APPROVED (LATAM intent) + Bridge rails ENABLED Europe & North America regions show as unlocked
2 Sumsub APPROVED (STANDARD intent) + Manteca ACTIVE LATAM region shows as unlocked
3 Sumsub APPROVED (STANDARD intent) + no Manteca LATAM region shows as locked
4 Sumsub APPROVED (LATAM intent) + no Bridge rails Europe & North America show as locked
5 Bridge KYC only (no Sumsub) + no Manteca Argentina & Brazil QR-only access still shows; LATAM full access locked
6 Regression: Previously affected user (LATAM intent + Bridge rails ENABLED) On page load/refresh, Europe/North America auto-unlock without any action

Check the region grid on the identity verification page (/profile/identity-verification) for each account state. No API calls needed — the fix is purely client-side state evaluation.


🧠 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.

Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 0
File: :0-0
Timestamp: 2026-03-04T18:32:51.654Z
Learning: In peanutprotocol/peanut-ui, Bridge-specific add-money and withdraw flows (`src/app/(mobile-ui)/add-money/[country]/bank/page.tsx`, `src/components/AddWithdraw/AddWithdrawCountriesList.tsx`, `src/components/Claim/Link/views/BankFlowManager.view.tsx`) have been migrated (PR `#1713`) to use the unified `isUserKycApproved` from `useKycStatus()` hook instead of per-component `liveKycStatus` + WebSocket state. The migration is complete; no more direct `bridgeKycStatus === 'approved'` checks in those files.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1654
File: src/components/Card/CardGeoScreen.tsx:41-45
Timestamp: 2026-02-02T06:03:48.192Z
Learning: In peanutprotocol/peanut-ui, KYC 2.0 uses the unified entry route '/profile/identity-verification'. Components (e.g., src/components/Card/CardGeoScreen.tsx) should avoid region-specific paths like '/profile/identity-verification/europe/bridge' and navigate to the unified route instead.

Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 1693
File: src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:206-207
Timestamp: 2026-03-04T18:35:17.673Z
Learning: In Bridge-specific add-money and withdraw flows (e.g., src/app/(mobile-ui)/add-money/[country]/bank/page.tsx), liveKycStatus from WebSocket tracks real-time bridge KYC status changes, and bridgeKycStatus is the persisted status. These flows use direct bridgeKycStatus === 'approved' checks instead of the unified isUserKycVerified helper. Migrating to the unified verifier requires careful testing of WebSocket-based real-time status tracking and is deferred to follow-up work.

Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 1683
File: src/components/Kyc/SumsubKycFlow.tsx:218-230
Timestamp: 2026-02-23T16:00:24.665Z
Learning: In src/components/Kyc/SumsubKycFlow.tsx, the ToS confirmation flow in handleTosIframeClose intentionally continues to fetchUser() even when confirmBridgeTos() fails after retry. The phase-transition effect (which monitors rail status via useRailStatusTracking) serves as the actual validation mechanism, making the flow resilient to transient confirmation API failures while still ensuring correct state progression.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/hooks/useIdentityVerification.tsx`:
- Around line 182-185: The hook currently restores top-level Bridge regions via
BRIDGE_SUPPORTED_REGIONS and hasProviderAccess('BRIDGE') but still suppresses
BRIDGE_SUPPORTED_LATAM_COUNTRIES and MANTECA_QR_ONLY_REGIONS when
isSumsubApproved is true, causing Sumsub-approved Bridge users to miss Mexico
and QR-only countries in unlockedRegions; update the country-level conditional
that currently checks isSumsubApproved to instead (or additionally) consult
hasProviderAccess('BRIDGE') so that BRIDGE_SUPPORTED_LATAM_COUNTRIES and
MANTECA_QR_ONLY_REGIONS are included in unlockedRegions when
hasProviderAccess('BRIDGE') returns true (use the same predicate used for
BRIDGE_SUPPORTED_REGIONS and reference variables
BRIDGE_SUPPORTED_LATAM_COUNTRIES, MANTECA_QR_ONLY_REGIONS, isSumsubApproved,
hasProviderAccess('BRIDGE'), and unlockedRegions to locate and change the
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: e278a8a4-f092-4fa1-bc52-3455041f4030

📥 Commits

Reviewing files that changed from the base of the PR and between 74488af and 812dbf3.

📒 Files selected for processing (1)
  • src/hooks/useIdentityVerification.tsx

Comment on lines +182 to +185
// bridge regions: check provider rails regardless of sumsub regionIntent
if (BRIDGE_SUPPORTED_REGIONS.includes(regionName)) {
return hasProviderAccess('BRIDGE')
}
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.

⚠️ Potential issue | 🟠 Major

Reuse the new Bridge-access predicate for the country-level Bridge unlocks.

Lines 182-185 now restore the top-level Bridge regions from hasProviderAccess('BRIDGE'), but Lines 206-208 still suppress BRIDGE_SUPPORTED_LATAM_COUNTRIES and MANTECA_QR_ONLY_REGIONS whenever isSumsubApproved is true. Sumsub-approved Bridge users will therefore still miss Mexico and the Bridge QR-only countries in unlockedRegions, even though this hook still advertises them as Bridge unlocks at Lines 250-283.

Suggested alignment
         const hasProviderAccess = (providerCode: string) => {
             const providerRails = user?.rails?.filter((r) => r.rail.provider.code === providerCode) ?? []
             if (providerRails.length === 0) return false
             return providerRails.some(
                 (r) =>
                     r.status === 'ENABLED' ||
                     r.status === 'REQUIRES_INFORMATION' ||
                     r.status === 'REQUIRES_EXTRA_INFORMATION'
             )
         }
+        const hasBridgeRailAccess = hasProviderAccess('BRIDGE')

         // helper to check if a region should be unlocked
         const isRegionUnlocked = (regionName: string) => {
             if (isSumsubApproved) {
                 if (regionName === 'Rest of the world') return true

                 // bridge regions: check provider rails regardless of sumsub regionIntent
                 if (BRIDGE_SUPPORTED_REGIONS.includes(regionName)) {
-                    return hasProviderAccess('BRIDGE')
+                    return hasBridgeRailAccess
                 }
@@
-        if (isBridgeApproved && !isMantecaApproved && !isSumsubApproved) {
+        if (((!isSumsubApproved && isBridgeApproved) || (isSumsubApproved && hasBridgeRailAccess)) && !isMantecaApproved) {
             unlocked.push(...MANTECA_QR_ONLY_REGIONS, ...BRIDGE_SUPPORTED_LATAM_COUNTRIES)
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// bridge regions: check provider rails regardless of sumsub regionIntent
if (BRIDGE_SUPPORTED_REGIONS.includes(regionName)) {
return hasProviderAccess('BRIDGE')
}
const hasProviderAccess = (providerCode: string) => {
const providerRails = user?.rails?.filter((r) => r.rail.provider.code === providerCode) ?? []
if (providerRails.length === 0) return false
return providerRails.some(
(r) =>
r.status === 'ENABLED' ||
r.status === 'REQUIRES_INFORMATION' ||
r.status === 'REQUIRES_EXTRA_INFORMATION'
)
}
const hasBridgeRailAccess = hasProviderAccess('BRIDGE')
// helper to check if a region should be unlocked
const isRegionUnlocked = (regionName: string) => {
if (isSumsubApproved) {
if (regionName === 'Rest of the world') return true
// bridge regions: check provider rails regardless of sumsub regionIntent
if (BRIDGE_SUPPORTED_REGIONS.includes(regionName)) {
return hasBridgeRailAccess
}
}
// ... rest of isRegionUnlocked logic
}
// ... middle code ...
if (((!isSumsubApproved && isBridgeApproved) || (isSumsubApproved && hasBridgeRailAccess)) && !isMantecaApproved) {
unlocked.push(...MANTECA_QR_ONLY_REGIONS, ...BRIDGE_SUPPORTED_LATAM_COUNTRIES)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useIdentityVerification.tsx` around lines 182 - 185, The hook
currently restores top-level Bridge regions via BRIDGE_SUPPORTED_REGIONS and
hasProviderAccess('BRIDGE') but still suppresses
BRIDGE_SUPPORTED_LATAM_COUNTRIES and MANTECA_QR_ONLY_REGIONS when
isSumsubApproved is true, causing Sumsub-approved Bridge users to miss Mexico
and QR-only countries in unlockedRegions; update the country-level conditional
that currently checks isSumsubApproved to instead (or additionally) consult
hasProviderAccess('BRIDGE') so that BRIDGE_SUPPORTED_LATAM_COUNTRIES and
MANTECA_QR_ONLY_REGIONS are included in unlockedRegions when
hasProviderAccess('BRIDGE') returns true (use the same predicate used for
BRIDGE_SUPPORTED_REGIONS and reference variables
BRIDGE_SUPPORTED_LATAM_COUNTRIES, MANTECA_QR_ONLY_REGIONS, isSumsubApproved,
hasProviderAccess('BRIDGE'), and unlockedRegions to locate and change the
logic).

@Hugo0 Hugo0 merged commit 865fec7 into main Apr 5, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants