Skip to content

fix: bridge KYC state machine — unified gate, inline ToS, rejection handling#1943

Merged
Hugo0 merged 10 commits into
mainfrom
fix/bridge-kyc-state-machine
May 7, 2026
Merged

fix: bridge KYC state machine — unified gate, inline ToS, rejection handling#1943
Hugo0 merged 10 commits into
mainfrom
fix/bridge-kyc-state-machine

Conversation

@kushagrasarathe

Copy link
Copy Markdown
Contributor

Summary

  • Unified pre-transfer gate (useBridgeTransferReadiness): single hook enforcing correct priority across all Bridge bank flows — hard rejection → ToS acceptance → fixable rejection → enrollment → ready
  • Expanded ToS detection (useBridgeTosStatus): now checks tos_acceptance/tos_v2_acceptance in rail additionalRequirements metadata, not just REQUIRES_INFORMATION status
  • Inline KYC in QR pay: replaced redirect to /profile/identity-verification with inline Sumsub SDK (handleInitiateKyc('LATAM'))
  • Rejection handling: withdraw/deposit/claim flows now show correct modals — "Contact support" for hard rejections, "Upload document" for fixable rejections (self-heal via applicant action)
  • BridgeTosStep UX: shows confirmation modal before iframe, prevents modal flash during ToS confirmation
  • Fixed support CTAs: replaced broken $crisp.push with setIsSupportModalOpen in KycProviderRejection, ActivationCTAs, RegionsVerification
  • Fixed SDK auto-close: race condition in useSumsubKycFlow where fetchCurrentStatus closed the SDK wrapper on first attempt
  • Error display: InitiateKycModal now shows error state with "Contact support" when self-heal/KYC initiation fails

Files changed (15)

File Change
hooks/useBridgeTransferReadiness.ts NEW — unified gate hook
hooks/useBridgeTosStatus.ts Check additionalRequirements for ToS
hooks/useSumsubKycFlow.ts Fix race condition (guard fetchCurrentStatus with showWrapperRef)
hooks/useProviderRejectionStatus.ts Generic message for permanent rejections
components/Kyc/InitiateKycModal.tsx Added blocked variant, error prop
components/Kyc/BridgeTosStep.tsx Confirmation modal + isConfirming state
components/Kyc/states/KycProviderRejection.tsx $crispsetIsSupportModalOpen
components/Home/ActivationCTAs.tsx $crispsetIsSupportModalOpen
components/Profile/views/RegionsVerification.view.tsx $crispsetIsSupportModalOpen
app/(mobile-ui)/withdraw/[country]/bank/page.tsx Unified gate, rejection handling
app/(mobile-ui)/add-money/[country]/bank/page.tsx Unified gate, rejection handling
app/(mobile-ui)/qr-pay/page.tsx Inline KYC (no redirect), gate blocks query
components/AddWithdraw/AddWithdrawCountriesList.tsx Unified gate, __silent__ sentinel
components/AddWithdraw/DynamicBankAccountForm.tsx Skip __silent__ error display
components/Claim/Link/views/BankFlowManager.view.tsx ToS guard + rejection for claims

Backend dependency

Requires peanutprotocol/peanut-api-ts#fix/bridge-tos-transfer-guard for:

  • Transfer creation ToS guard (fetchCustomerById check)
  • confirm-bridge-tos full status processor (correct rail state after ToS)
  • get-bridge-tos-link fallback for non-REQUIRES_INFORMATION rails
  • initiate-kyc regionIntent overwrite for non-approved users

Testing done

Tested locally with FE pointed to local BE + staging DB:

  1. Hard-rejected user (duplicate_customer_detected, PROVIDER_FINAL): sees "Verification issue" + "Contact support" modal → opens support drawer ✅
  2. Fixable-rejected user (government_id_verification_failed, PROVIDER_FIXABLE): sees "We need extra documents" + "Upload document" → Sumsub SDK opens inline → self-heal webhook fires → rails transition to REQUIRES_INFORMATION ✅
  3. Self-heal error state: set unrecognized reject label → classifier defaults to CONTACT_SUPPORT → modal shows "Something went wrong" + "Contact support" ✅
  4. ToS acceptance (withdraw): user with REQUIRES_INFORMATION rails → "Accept Terms of Service" modal → iframe opens → accept → confirmation without modal flash ✅
  5. QR pay (unverified user): scan QR → inline KYC modal (not redirect) → "Verify now" → Sumsub SDK opens inline ✅
  6. QR pay (verified user): scan QR → proceeds to payment directly ✅
  7. Identity verification page: SDK no longer auto-closes on first click (race condition fix) ✅
  8. Contact support CTAs: all "Contact support" buttons across KYC modals now open the support drawer ✅

Test plan

  • Verify withdraw flow for hard-rejected Bridge user → sees "Contact support" not "Start verification"
  • Verify withdraw flow for fixable-rejected Bridge user → self-heal opens Sumsub SDK inline
  • Verify deposit flow shows ToS modal before proceeding when ToS not accepted
  • Verify QR pay shows inline KYC modal (not redirect) for unverified users
  • Verify identity verification page SDK stays open on first click
  • Verify claim link flow handles ToS + rejection states
  • Regression: happy path withdraw/deposit/claim for fully approved users still works

…andling

the bridge KYC state handling in transfer flows (withdraw, deposit, claim, qr-pay)
had multiple issues: wrong priority order, missing ToS checks, redirects instead of
inline flows, and broken rejection handling.

changes:
- new `useBridgeTransferReadiness` hook: unified pre-transfer gate with correct
  priority (hard rejection → tos → fixable rejection → enrollment → ready)
- `useBridgeTosStatus`: expanded to check `tos_acceptance`/`tos_v2_acceptance` in
  rail `additionalRequirements` metadata, not just `REQUIRES_INFORMATION` status
- `InitiateKycModal`: added `blocked` variant (contact support CTA), `error` prop
  for failed self-heal attempts
- withdraw/bank, add-money/bank, AddWithdrawCountriesList: replaced fragmented
  `needsBridgeEnrollment` + `guardWithTos` + `bridgeRejection` checks with unified gate
- BankFlowManager (claim flow): added ToS guard + rejection handling for non-guest paths
- qr-pay: replaced redirect-based KYC (`router.push('/profile/identity-verification')`)
  with inline sumsub SDK initiation (`handleInitiateKyc('LATAM')`)
- BridgeTosStep: show confirmation modal before iframe, prevent modal flash during
  confirmation with `isConfirming` state
- KycProviderRejection, ActivationCTAs, RegionsVerification: replaced broken
  `$crisp.push` calls with `setIsSupportModalOpen`
- useSumsubKycFlow: fixed race condition where `fetchCurrentStatus` closes SDK wrapper
  on first attempt by guarding with `showWrapperRef`
- useProviderRejectionStatus: permanent rejections now show generic support message
  instead of misleading "upload a clearer photo"
- DynamicBankAccountForm: `__silent__` sentinel to prevent form navigation when gate blocks
- close kyc modal via `sumsubFlow.showWrapper` effect instead of in `onVerify` callback
@vercel

vercel Bot commented May 6, 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, Comment May 7, 2026 11:42am

Request Review

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62e8ed14-d808-45de-a809-2724f2f58821

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd359e and 5689481.

📒 Files selected for processing (1)
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx

Walkthrough

This PR consolidates KYC and bridge transfer gating into a single prioritized BridgeGateAction type, integrates Sumsub multi-phase flows across onramp/offramp/QR-pay pages, updates KYC UI components to support gate-driven variants and contact-support actions, and replaces Crisp chat with an in-app support modal throughout the application.

Changes

Bridge gate + Sumsub KYC integration

Layer / File(s) Summary
Data shape / Public API contract
src/hooks/useBridgeTransferReadiness.ts
Exports BridgeGateAction type with five priority states (accept_tos, fixable_rejection, blocked_rejection, needs_enrollment, ready), useBridgeTransferReadiness() hook that computes prioritized gate, and helpers getKycModalVariant() and getGateProviderMessage().
Supporting hooks
src/hooks/useBridgeTosStatus.ts, src/hooks/useProviderRejectionStatus.ts, src/hooks/useSumsubKycFlow.ts
useBridgeTosStatus now detects ToS needs from both REQUIRES_INFORMATION rail status and metadata.additionalRequirements. useProviderRejectionStatus adds fixable-vs-blocked rejection logic to select messaging. useSumsubKycFlow guards status recovery and restores prior state on cross-region initiation failures.
KYC modal component
src/components/Kyc/InitiateKycModal.tsx
Props expanded with onContactSupport, error, and blocked variant; computes title/description/CTA dynamically based on error/blocked/variant state; uses gate-derived variants and provider messages.
Bridge ToS step
src/components/Kyc/BridgeTosStep.tsx
Adds isConfirming state to handle iframe-to-rails confirmation lifecycle; replaces separate error modal with unified ActionModal controlled by iframe/confirming/error states.
Support modal wiring
src/components/Kyc/states/KycProviderRejection.tsx, src/components/Profile/views/RegionsVerification.view.tsx, src/components/Home/ActivationCTAs.tsx
useModalsContext replaces Crisp chat; contact-support buttons now open in-app support modal via setIsSupportModalOpen().
QR pay Sumsub integration
src/app/(mobile-ui)/qr-pay/page.tsx
Initializes useMultiPhaseKycFlow and tracks Sumsub approval; wires provider-rejection "Upload document" action to self-heal resubmit; Verify/Continue CTAs trigger multi-phase initiation; renders SumsubKycModals.
Onramp/Offramp pages (gate-driven)
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Replace legacy KYC/enrollment checks with useBridgeTransferReadiness; branch continuation/creation on gate.type (TOS guard vs KYC modal); auto-close KYC modal when Sumsub SDK wrapper opens; derive modal variant/message from gate.
Form submission gating
src/components/AddWithdraw/AddWithdrawCountriesList.tsx, src/components/AddWithdraw/DynamicBankAccountForm.tsx
AddWithdrawCountriesList blocks submission unless gate is ready; DynamicBankAccountForm extends onSuccess return type with optional silent flag and includes user name in rawData.
Claim flow integration
src/components/Claim/Link/views/BankFlowManager.view.tsx
Pre-gates off-ramp creation on bridge readiness; wires BridgeTosStep and InitiateKycModal into claim view; handles Sumsub self-heal for fixable_rejection; returns explicit error for missing account IDs.
Tests & formatting
src/hooks/__tests__/useBridgeTransferReadiness.test.ts, src/app/actions/sumsub.ts, src/app/(mobile-ui)/withdraw/manteca/page.tsx
Adds 151 lines of Jest tests validating gate prioritization and helper function mapping; minor error assignment and ErrorAlert formatting changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • peanutprotocol/peanut-ui#1932: Modifies the same KYC/bridge gating hooks and InitiateKycModal to drive provider self-heal and enrollment flows.
  • peanutprotocol/peanut-ui#1713: Updates KYC gating and Sumsub flows across onramp/withdraw pages, AddWithdrawCountriesList, BankFlowManager, and useSumsubKycFlow.
  • peanutprotocol/peanut-ui#1690: Replaces legacy KYC modal flows with Sumsub-driven multi-phase flows in the same components and pages.

Suggested reviewers

  • Hugo0
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely and accurately reflects the main change: a unified bridge KYC state machine with inline ToS handling and improved rejection handling.
Description check ✅ Passed The description comprehensively documents the PR objectives, changes, testing, and backend dependencies, directly related to the changeset across all modified files.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

Let me read all the changed files to do a thorough review.


🏁 Script executed:

#!/bin/bash
echo "=== hooks/useBridgeTransferReadiness.ts ==="
fd "useBridgeTransferReadiness.ts" --exec cat {}

echo ""
echo "=== hooks/useBridgeTosStatus.ts ==="
fd "useBridgeTosStatus.ts" --exec cat {}

echo ""
echo "=== hooks/useSumsubKycFlow.ts ==="
fd "useSumsubKycFlow.ts" --exec cat {}

echo ""
echo "=== hooks/useProviderRejectionStatus.ts ==="
fd "useProviderRejectionStatus.ts" --exec cat {}

Length of output: 23384


🏁 Script executed:

#!/bin/bash
echo "=== components/Kyc/InitiateKycModal.tsx ==="
fd "InitiateKycModal.tsx" --exec cat {}

echo ""
echo "=== components/Kyc/BridgeTosStep.tsx ==="
fd "BridgeTosStep.tsx" --exec cat {}

echo ""
echo "=== components/Kyc/states/KycProviderRejection.tsx ==="
fd "KycProviderRejection.tsx" --exec cat {}

echo ""
echo "=== components/Home/ActivationCTAs.tsx ==="
fd "ActivationCTAs.tsx" --exec cat {}

echo ""
echo "=== components/Profile/views/RegionsVerification.view.tsx ==="
fd "RegionsVerification.view.tsx" --exec cat {}

Length of output: 28640


🏁 Script executed:

#!/bin/bash
echo "=== app/(mobile-ui)/withdraw/[country]/bank/page.tsx ==="
fd "page.tsx" --search-path "app/(mobile-ui)/withdraw" --exec cat {}

echo ""
echo "=== app/(mobile-ui)/add-money/[country]/bank/page.tsx ==="
fd "page.tsx" --search-path "app/(mobile-ui)/add-money" --exec cat {}

echo ""
echo "=== app/(mobile-ui)/qr-pay/page.tsx ==="
fd "page.tsx" --search-path "app/(mobile-ui)/qr-pay" --exec cat {}

Length of output: 492


🏁 Script executed:

#!/bin/bash
echo "=== components/AddWithdraw/AddWithdrawCountriesList.tsx ==="
fd "AddWithdrawCountriesList.tsx" --exec cat {}

echo ""
echo "=== components/AddWithdraw/DynamicBankAccountForm.tsx ==="
fd "DynamicBankAccountForm.tsx" --exec cat {}

echo ""
echo "=== components/Claim/Link/views/BankFlowManager.view.tsx ==="
fd "BankFlowManager.view.tsx" --exec cat {}

Length of output: 76931


@kushagrasarathe Here is the in-depth review.


🐛 Bug Risks & Breakage

1. BridgeTosStep — unguarded await leaves isConfirming stuck

In handleIframeClose, when source === 'tos_accepted', confirmBridgeTosAndAwaitRails is awaited but not wrapped in try/catch. If it throws (network error, BE guard failure, etc.), setIsConfirming(false) is never called and the component is permanently stuck in the confirming state with no visible UI — the ActionModal is hidden (!isConfirming) and the iframe is also hidden (!showIframe).

// BridgeTosStep.tsx — handleIframeClose
if (source === 'tos_accepted') {
    setIsConfirming(true)
    setShowIframe(false)
    // ⚠️ if this throws, isConfirming stays true forever
    await confirmBridgeTosAndAwaitRails(fetchUser)
    setIsConfirming(false)
    onComplete()
}

Fix:

if (source === 'tos_accepted') {
    setIsConfirming(true)
    setShowIframe(false)
    try {
        await confirmBridgeTosAndAwaitRails(fetchUser)
        onComplete()
    } catch {
        setError('Something went wrong confirming your terms. Please try again.')
    } finally {
        setIsConfirming(false)
    }
}

2. BankFlowManagerisProcessingKycSuccess never reset on error

setIsProcessingKycSuccess(false) is only set at the end of the happy path in handleSuccess. If addBankAccount throws an exception before reaching it, the flag stays true and all future KYC success callbacks become no-ops.

// BankFlowManager.view.tsx
try {
    const addBankAccountResponse = await addBankAccount(payload)
    // ...
    // setIsProcessingKycSuccess(false) is only called here (happy path)
} finally {
    setIsProcessingKycSuccess(false)  // ← should be here
}

3. BankFlowManager — silent no-op when guardWithTos() returns false

In handleCreateOfframpAndClaim:

if (gate.type === 'accept_tos') {
    if (guardWithTos()) return  // shows ToS
    // ← if guardWithTos() returns false, falls through silently
    // no return here → proceeds past the gate check
}

If guardWithTos() returns false (ToS not shown for any reason), execution continues past the gate, potentially initiating an offramp for a user who hasn't accepted ToS.


4. useBridgeTosStatus — unsafe metadata cast

const reqs = r.metadata?.additionalRequirements as string[] | undefined
return reqs?.some((req) => req === 'tos_acceptance' || req === 'tos_v2_acceptance')

If the backend ever returns additionalRequirements as an object (e.g., { "0": "tos_acceptance" }) instead of an array, .some() will throw at runtime. A safer guard:

const reqs = Array.isArray(r.metadata?.additionalRequirements)
    ? (r.metadata.additionalRequirements as string[])
    : []

5. useSumsubKycFlow — cross-region prevStatusRef pre-set on failed initiation

In handleInitiateKyc, when crossRegion === true:

if (crossRegion) {
    prevStatusRef.current = 'APPROVED'  // set before the API call
}
try {
    const response = await initiateSumsubKyc(...)
    if (response.error) {
        setError(response.error)
        return  // ← prevStatusRef is already 'APPROVED', not restored
    }

If the API call fails, prevStatusRef is left pointing to 'APPROVED', which suppresses future APPROVED transition detection for this session. A real approval event would be silently swallowed.


6. AddWithdrawCountriesListneeds_enrollment crossRegion flag expression

gate.type === 'needs_enrollment' || undefined

This evaluates to true | undefined (boolean true when enrollment is needed), which is passed as the crossRegion argument. While functionally correct (truthy), it is highly misleading — a reader would expect a boolean true or explicit undefined, not a conditional || undefined that always resolves to the boolean. This is likely to produce unintended behavior if the logic around crossRegion ever changes.


⚡ Performance & Latency

1. fetchUser() on every form submit (medium impact)

In AddWithdrawCountriesList.handleFormSubmit:

const handleFormSubmit = async (payload, rawData) => {
    await fetchUser()  // ← network request on EVERY submit attempt
    if (gate.type !== 'ready') { ... return { error: '__silent__' } }
    ...
}

fetchUser() is called unconditionally before even checking the gate, meaning it fires on every submit including gate-blocked ones and validation-failed ones. For users with a stable KYC state, this is a needless round-trip that adds latency to every bank form submission. Consider calling it only when actually needed (e.g., after a KYC flow completes).

2. useProviderRejectionStatus — inline .sort() on every render

kycVerifications
    .filter((v) => v.provider === providerCode)
    .sort((a, b) => new Date(b.updatedAt ?? 0).getTime() - ...)

This is inside a useMemo([rails, kycVerifications]), so it only recomputes when those change — acceptable. However, kycVerifications is derived from user?.user?.kycVerifications outside the memo, so a new array reference on every useAuth() re-render (even with identical content) would bust the memo. Worth checking if useAuth stabilizes the user reference.

3. Triple hook composition in useBridgeTransferReadiness

The hook composes three hooks (useBridgeTosStatus, useProviderRejectionStatus, useKycStatus), each running their own useMemo. This adds three separate subscription points and three separate reconciliation passes on every user state update. It is not expensive in absolute terms, but every component that calls useBridgeTransferReadiness incurs this cost — currently at least 3 consumer components.


🔧 Code Quality & DRY

1. InitiateKycModal variant mapping is copy-pasted in two places

The gate.type → variant derivation is identical in both AddWithdrawCountriesList.tsx and BankFlowManager.view.tsx:

// Duplicated in both files:
variant={
    gate.type === 'blocked_rejection'
        ? 'blocked'
        : gate.type === 'fixable_rejection'
          ? 'provider_rejection'
          : gate.type === 'needs_enrollment'
            ? 'cross_region'
            : 'default'
}

This should be extracted to a utility function, e.g.:

function gateTypeToModalVariant(gate: BridgeGateAction): InitiateKycModalProps['variant'] {
    switch (gate.type) {
        case 'blocked_rejection': return 'blocked'
        case 'fixable_rejection': return 'provider_rejection'
        case 'needs_enrollment': return 'cross_region'
        default: return 'default'
    }
}

2. accountOwnerName validation logic duplicated 3× in DynamicBankAccountForm

The same validation rule appears three times (claim-guest, claim-logged-in-without-fullName, withdraw flow). Extract to a named rule object:

const accountOwnerNameRules = {
    required: 'Account owner name is required',
    validate: (value: string) => {
        const parts = value.trim().split(/\s+/)
        return parts.length >= 2 || 'Please enter both first and last name'
    },
}

3. isUs/isMx/isUk/isIban recalculated inside onSubmit

These four flags are already computed at the top of the component, yet they are recalculated identically inside onSubmit. The inner declarations shadow the outer ones unnecessarily.

4. __silent__ magic string is a code smell

The sentinel value '__silent__' is returned from handleFormSubmit, passed through onSuccess, and then filtered in DynamicBankAccountForm. A magic string in an error pipeline is fragile — a typo anywhere breaks the silencing silently. A cleaner design would be a typed result:

type FormSubmitResult = { error?: string; silent?: boolean }

5. console.log debug statement in production path

// DynamicBankAccountForm.tsx — onSubmit
console.log('Validation still checking, skipping submission')

Should be removed before merge.

6. Inconsistent user data source across hooks

useBridgeTosStatus reads from useUserStore() (Redux), while useProviderRejectionStatus reads from useAuth() (context). Both are ultimately derived from the same user object, but they could have different staleness if one is updated without the other. This should be standardized to a single source of truth.


✅ QA Testing Guide

# Scenario Steps Expected
1 Hard rejection Login as Bridge-hard-rejected user → attempt withdraw "Verification issue" modal → "Contact support" opens support drawer
2 Fixable rejection Login as Bridge-fixable-rejected user → attempt withdraw "We need extra documents" modal → "Upload document" → Sumsub SDK opens inline
3 Self-heal error state Fixable-rejected user with unrecognized reject label → attempt upload Modal shows "Something went wrong" + "Contact support"
4 ToS acceptance (withdraw) User with REQUIRES_INFORMATION rails → attempt withdraw ToS modal → iframe opens → accept → no modal flash → form re-submits
5 ToS acceptance (deposit) Same user → attempt deposit Same ToS gate triggers before proceeding
6 ToS acceptance (claim) Claim link as unaccepted-ToS user ToS gate triggers at confirm step, not at bank form
7 QR pay — unverified Scan QR as unverified user Inline KYC modal (not redirect to /profile/identity-verification) → "Verify now" → SDK opens
8 QR pay — verified Scan QR as fully approved user Proceeds directly to payment, no KYC prompt
9 SDK race condition Navigate to identity verification → click "Start Verification" immediately SDK stays open, does not auto-close on first attempt
10 Contact support CTAs Click "Contact support" in KycProviderRejection, ActivationCTAs, RegionsVerification Support drawer opens in all locations, no JS error
11 BridgeTosStep network error Accept ToS with BE unavailable Error displayed, component not stuck (not blank screen)
12 Happy path regression Fully approved user: withdraw, deposit, claim No KYC gates shown, full flows complete successfully
13 Form submit latency Submit bank form as KYC-approved user (devtools → network tab) Only one fetchUser call, not on gate-blocked attempts
14 needs_enrollment Sumsub-approved but Bridge not yet started → attempt bank transfer "cross_region" variant modal appears, enrollment initiates

…ata cast, gate fallthrough

- BridgeTosStep: wrap confirmBridgeTosAndAwaitRails in try/catch to prevent
  isConfirming getting stuck on error
- useBridgeTosStatus: use Array.isArray guard for additionalRequirements metadata
- all gate checks: always return when gate is not ready, even if guardWithTos
  returns false (defensive)
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai re-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

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== hooks/useBridgeTransferReadiness.ts ==="
fd "useBridgeTransferReadiness.ts" --exec cat {}

echo ""
echo "=== hooks/useBridgeTosStatus.ts ==="
fd "useBridgeTosStatus.ts" --exec cat {}

echo ""
echo "=== hooks/useSumsubKycFlow.ts ==="
fd "useSumsubKycFlow.ts" --exec cat {}

echo ""
echo "=== hooks/useProviderRejectionStatus.ts ==="
fd "useProviderRejectionStatus.ts" --exec cat {}

echo ""
echo "=== components/Kyc/BridgeTosStep.tsx ==="
fd "BridgeTosStep.tsx" --exec cat {}

echo ""
echo "=== components/Kyc/InitiateKycModal.tsx ==="
fd "InitiateKycModal.tsx" --exec cat {}

echo ""
echo "=== components/Kyc/states/KycProviderRejection.tsx ==="
fd "KycProviderRejection.tsx" --exec cat {}

echo ""
echo "=== components/Home/ActivationCTAs.tsx ==="
fd "ActivationCTAs.tsx" --exec cat {}

echo ""
echo "=== components/Profile/views/RegionsVerification.view.tsx ==="
fd "RegionsVerification.view.tsx" --exec cat {}

Length of output: 52205


🏁 Script executed:

#!/bin/bash
echo "=== components/AddWithdraw/AddWithdrawCountriesList.tsx ==="
fd "AddWithdrawCountriesList.tsx" --exec cat {}

echo ""
echo "=== components/AddWithdraw/DynamicBankAccountForm.tsx ==="
fd "DynamicBankAccountForm.tsx" --exec cat {}

echo ""
echo "=== components/Claim/Link/views/BankFlowManager.view.tsx ==="
fd "BankFlowManager.view.tsx" --exec cat {}

echo ""
echo "=== app/actions/sumsub.ts ==="
fd "sumsub.ts" --search-path "app/actions" --exec cat {}

Length of output: 77005


@kushagrasarathe Here is the updated in-depth re-review after reading all the latest code.


✅ Fixed Since Last Review

# Issue Status
1 BridgeTosStep stuck isConfirming on network error FixedhandleIframeClose now has try/catch/finally
2 useBridgeTosStatus unsafe .some() on non-array FixedArray.isArray guard added
3 BankFlowManager guardWithTos() fallthrough allowing proceeding without ToS Fixedreturn now covers the entire gate.type !== 'ready' block
4 BankFlowManager isProcessingKycSuccess never reset on error Fixed — now in a finally block
5 Broken $crisp.push support CTAs (3 locations) Fixed — all replaced with setIsSupportModalOpen

🐛 Remaining / New Bug Risks

1. useSumsubKycFlowprevStatusRef pre-set before confirming API success (unchanged)

prevStatusRef.current = 'APPROVED' is still set before the initiateSumsubKyc call when crossRegion === true. If the API returns response.error, the function returns early — but prevStatusRef is already 'APPROVED'. Any real APPROVED websocket event in that same session is silently swallowed.

if (crossRegion) {
    prevStatusRef.current = 'APPROVED'  // ⚠️ set before API call
}
const response = await initiateSumsubKyc(...)
if (response.error) {
    setError(response.error)
    return  // prevStatusRef stuck as 'APPROVED'
}

Fix: Move the pre-set to after confirming a successful response, or restore prevStatusRef if the call fails:

const previousRef = prevStatusRef.current
try {
    const response = await initiateSumsubKyc(...)
    if (response.error) {
        if (crossRegion) prevStatusRef.current = previousRef  // restore
        setError(response.error)
        return
    }
    if (crossRegion) prevStatusRef.current = 'APPROVED'  // only set on success
    ...
}

2. BankFlowManager.handleSuccess — silent no-op when bank account has no id (new)

In scenario 2 (UserBankClaim), when addBankAccountResponse.data exists but !addBankAccountResponse.data.id, the code falls through the entire if (addBankAccountResponse.data?.id) block with no error set and no state update. The user sees the form with no feedback and is stuck.

if (addBankAccountResponse.data?.id) {
    // happy path — sets state and navigates
}
// ⚠️ if data exists but id is missing: falls through silently
// finally runs → isProcessingKycSuccess reset
// function returns {} → DynamicBankAccountForm sees no error

Fix:

if (addBankAccountResponse.data?.id) {
    // ...
} else {
    return { error: 'Failed to process bank account. Please try again.' }
}

⚡ Performance Concerns (unchanged)

fetchUser() unconditional on every submit in AddWithdrawCountriesList

const handleFormSubmit = async (payload, rawData) => {
    await fetchUser()  // ⚠️ fires even when gate will block the action
    if (gate.type !== 'ready') { ... return { error: '__silent__' } }

fetchUser() adds a network round-trip on every submit, including gate-blocked ones. Consider moving it to only fire on actual gate transitions (e.g., after KYC or ToS completes) rather than on every submit.


🔧 Code Quality / DRY (unchanged)

1. Duplicated gate.type → variant mapping (2 locations)

The ternary mapping gate.type → InitiateKycModal variant is copy-pasted identically in AddWithdrawCountriesList (form view) and BankFlowManager (BankConfirmClaim render):

variant={
    gate.type === 'blocked_rejection'
        ? 'blocked'
        : gate.type === 'fixable_rejection'
          ? 'provider_rejection'
          : gate.type === 'needs_enrollment'
            ? 'cross_region'
            : 'default'
}

Extract to a shared utility in useBridgeTransferReadiness.ts or a new bridge-gate.utils.ts:

export function gateTypeToModalVariant(gate: BridgeGateAction): InitiateKycModalProps['variant'] {
    switch (gate.type) {
        case 'blocked_rejection': return 'blocked'
        case 'fixable_rejection': return 'provider_rejection'
        case 'needs_enrollment': return 'cross_region'
        default: return 'default'
    }
}

2. accountOwnerName validation rule duplicated 3× in DynamicBankAccountForm

The same { required, validate } object appears for claim (guest), claim (logged-in without fullName), and withdraw flows. Extract once:

const accountOwnerNameRules = {
    required: 'Account owner name is required',
    validate: (value: string) => {
        const parts = value.trim().split(/\s+/)
        return parts.length >= 2 || 'Please enter both first and last name'
    },
}

3. isUs/isMx/isUk/isIban shadowed inside onSubmit

These four flags are computed at the top of the component, then identically recomputed inside onSubmit, shadowing the outer declarations. Remove the inner redeclarations.

4. __silent__ magic string sentinel

The string '__silent__' is used as an out-of-band error signal. A typo anywhere in the pipeline breaks silencing silently. Replace with a typed result shape:

type FormSubmitResult = { error?: string; silent?: boolean }
// caller:  return { silent: true }
// handler: if (result.silent) return  // no UI error

5. console.log in production path

// DynamicBankAccountForm.tsx
console.log('Validation still checking, skipping submission')

Should be removed before merge.

6. Inconsistent user data source

useBridgeTosStatus reads from useUserStore() (Redux), while useProviderRejectionStatus reads from useAuth() (context). Both are consumed by useBridgeTransferReadiness. If the two stores update at different times, the gate could compute a stale state for one frame. Standardize to a single source.

7. Confusing || undefined pattern (2 locations)

// AddWithdrawCountriesList + BankFlowManager
gate.type === 'needs_enrollment' || undefined

This reads as "a fallback to undefined" but actually evaluates to true | undefined. Write explicitly:

gate.type === 'needs_enrollment' ? true : undefined

8. setIsSubmitting(false) called twice in error path (new)

In DynamicBankAccountForm.onSubmit, setIsSubmitting(false) is called both inside the if (result.error) branch and unconditionally in finally. The explicit calls inside the try block are redundant and can be removed, relying solely on finally.


✅ Concise QA Testing Guide

# Scenario Steps Expected
1 Hard rejection Log in as Bridge-hard-rejected user → attempt withdraw "Verification issue" modal → "Contact support" → support drawer opens
2 Fixable rejection Log in as Bridge-fixable-rejected user → attempt withdraw "We need extra documents" → "Upload document" → Sumsub SDK inline
3 Self-heal error Fixable user with unrecognized label → attempt upload "Something went wrong" modal + "Contact support"
4 ToS (withdraw/deposit/claim) User with REQUIRES_INFORMATION rails → trigger transfer ToS modal appears → iframe → accept → no modal flash → flow proceeds
5 QR pay — unverified Scan QR as unverified user Inline KYC modal (no redirect) → "Verify now" → SDK opens
6 QR pay — verified Scan QR as fully-approved user Proceeds to payment directly, no KYC prompt
7 SDK race condition Navigate to identity verification → click "Start Verification" immediately SDK stays open, does not auto-close
8 ToS network error Accept ToS with BE unavailable Error message shown, component not stuck/blank
9 Cross-region KYC failure Trigger needs_enrollment → API error on initiate Error shown, future APPROVED transitions still work (verify prevStatusRef restored)
10 Happy path regression Fully approved user: withdraw, deposit, claim No KYC/ToS gates shown, all flows complete

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai re-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

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== hooks/useBridgeTransferReadiness.ts ==="
fd "useBridgeTransferReadiness.ts" --exec cat {}

echo ""
echo "=== hooks/useBridgeTosStatus.ts ==="
fd "useBridgeTosStatus.ts" --exec cat {}

echo ""
echo "=== hooks/useSumsubKycFlow.ts ==="
fd "useSumsubKycFlow.ts" --exec cat {}

echo ""
echo "=== hooks/useProviderRejectionStatus.ts ==="
fd "useProviderRejectionStatus.ts" --exec cat {}

echo ""
echo "=== components/Kyc/BridgeTosStep.tsx ==="
fd "BridgeTosStep.tsx" --exec cat {}

echo ""
echo "=== components/Kyc/InitiateKycModal.tsx ==="
fd "InitiateKycModal.tsx" --exec cat {}

echo ""
echo "=== components/Kyc/states/KycProviderRejection.tsx ==="
fd "KycProviderRejection.tsx" --exec cat {}

echo ""
echo "=== components/Home/ActivationCTAs.tsx ==="
fd "ActivationCTAs.tsx" --exec cat {}

echo ""
echo "=== components/Profile/views/RegionsVerification.view.tsx ==="
fd "RegionsVerification.view.tsx" --exec cat {}

echo ""
echo "=== components/AddWithdraw/AddWithdrawCountriesList.tsx ==="
fd "AddWithdrawCountriesList.tsx" --exec cat {}

echo ""
echo "=== components/AddWithdraw/DynamicBankAccountForm.tsx ==="
fd "DynamicBankAccountForm.tsx" --exec cat {}

echo ""
echo "=== components/Claim/Link/views/BankFlowManager.view.tsx ==="
fd "BankFlowManager.view.tsx" --exec cat {}

echo ""
echo "=== app/actions/sumsub.ts ==="
fd "sumsub.ts" --search-path "src/app/actions" --exec cat {}

Length of output: 132633


@kushagrasarathe Here is the updated re-review after reading all latest code.


✅ Fixed Since Last Review

# Issue Status
1 BridgeTosStep stuck isConfirming on network error Fixedtry/catch/finally in handleIframeClose
2 useBridgeTosStatus unsafe .some() on non-array FixedArray.isArray guard present
3 BankFlowManager guardWithTos() fallthrough Fixedreturn covers entire gate.type !== 'ready' block
4 BankFlowManager isProcessingKycSuccess never reset on error Fixed — now in finally block
5 BankFlowManager silent no-op when addBankAccountResponse.data?.id is missing Fixed — now returns explicit error string
6 Broken $crisp.push support CTAs Fixed — all three replaced with setIsSupportModalOpen

🐛 Remaining / New Bug Risks

1. useSumsubKycFlowprevStatusRef pre-set before confirming API success (unchanged)

if (crossRegion) {
    prevStatusRef.current = 'APPROVED'  // ⚠️ set BEFORE the API call
}
const response = await initiateSumsubKyc(...)
if (response.error) {
    setError(response.error)
    return  // prevStatusRef already 'APPROVED' — real APPROVED events silently swallowed
}

Fix: Restore prevStatusRef on error, or only set it after a confirmed successful response:

const previousRef = prevStatusRef.current
const response = await initiateSumsubKyc(...)
if (response.error) {
    if (crossRegion) prevStatusRef.current = previousRef  // restore
    setError(response.error)
    return
}
if (crossRegion) prevStatusRef.current = 'APPROVED'  // only set on success

2. BankFlowManagerInitiateKycModal closes before error becomes visible (new)

In BankConfirmClaim, onVerify always calls setShowKycModal(false) after the awaited call:

onVerify={async () => {
    if (gate.type === 'fixable_rejection') {
        await sumsubFlow.handleSelfHealResubmit('BRIDGE')
    } else {
        await sumsubFlow.handleInitiateKyc(...)
    }
    setShowKycModal(false)  // ⚠️ closes even when API failed and error is set
}}

If handleSelfHealResubmit or handleInitiateKyc sets sumsubFlow.error, the error is passed to InitiateKycModal via error={sumsubFlow.error}. But setShowKycModal(false) hides the modal immediately after, so the error state is never shown to the user — they see a silent close with no feedback.

Fix: Only close the modal on success, similar to how AddWithdrawCountriesList uses a useEffect to close it:

// In BankFlowManager — add alongside the existing sumsubFlow hooks:
useEffect(() => {
    if (sumsubFlow.showWrapper) setShowKycModal(false)
}, [sumsubFlow.showWrapper])

// And in onVerify, remove the unconditional setShowKycModal(false):
onVerify={async () => {
    if (gate.type === 'fixable_rejection') {
        await sumsubFlow.handleSelfHealResubmit('BRIDGE')
    } else {
        await sumsubFlow.handleInitiateKyc('STANDARD', undefined, gate.type === 'needs_enrollment' ? true : undefined)
    }
    // do NOT close modal here — let the useEffect handle it when SDK opens
}}

⚡ Performance (unchanged)

fetchUser() unconditional on every submit in AddWithdrawCountriesList

const handleFormSubmit = async (payload, rawData) => {
    await fetchUser()  // ⚠️ fires even when gate is about to block the action
    if (gate.type !== 'ready') { ... return { error: '__silent__' } }

Every bank form submission — including gate-blocked ones — incurs a full fetchUser() network round-trip before the gate check even runs. Consider calling fetchUser() only when actually transitioning (e.g. after a KYC/ToS completion webhook), not on every submit attempt.


🔧 Code Quality / DRY (unchanged)

1. Duplicated gate.type → variant mapping (2 files)

Identical ternary appears in both AddWithdrawCountriesList (Line ~310) and BankFlowManager (BankConfirmClaim render). Extract once:

// bridge-gate.utils.ts
export function gateTypeToModalVariant(gate: BridgeGateAction): InitiateKycModalProps['variant'] {
    switch (gate.type) {
        case 'blocked_rejection': return 'blocked'
        case 'fixable_rejection': return 'provider_rejection'
        case 'needs_enrollment': return 'cross_region'
        default: return 'default'
    }
}

2. accountOwnerName validation duplicated 3× in DynamicBankAccountForm

The same { required, validate } object appears for claim guest, claim logged-in-without-fullName, and withdraw flows. Extract once:

const accountOwnerNameRules = {
    required: 'Account owner name is required',
    validate: (value: string) => {
        const parts = value.trim().split(/\s+/)
        return parts.length >= 2 || 'Please enter both first and last name'
    },
}

3. isUs/isMx/isUk/isIban shadowed inside onSubmit

These four flags are computed at the component's top scope and then identically redeclared inside onSubmit, shadowing the outer values unnecessarily.

4. __silent__ magic string sentinel

'__silent__' is returned from handleFormSubmit, threaded through onSuccess, and filtered in DynamicBankAccountForm. A typo anywhere silently breaks the behavior. Replace with a typed result:

type FormSubmitResult = { error?: string; silent?: boolean }
// producer: return { silent: true }
// consumer: if (result.silent) return

5. console.log statements in production paths

Two in DynamicBankAccountForm:

  • console.log('Validation still checking, skipping submission')
  • console.log('Could not fetch BIC automatically.') (in IBAN onBlur)

Both should be removed before merge.

6. Inconsistent user data source

useBridgeTosStatus reads from useUserStore() (Redux); useProviderRejectionStatus reads from useAuth() (context). Both are consumed by useBridgeTransferReadiness. If the two stores update at different render cycles, the gate can compute a stale mixed state for one frame. Standardize to one source.

7. Confusing || undefined expression (2 locations)

gate.type === 'needs_enrollment' || undefined

This evaluates to true | undefined — not boolean | undefined as intended. Write explicitly:

gate.type === 'needs_enrollment' ? true : undefined

8. setIsSubmitting(false) called in both branches and finally

In DynamicBankAccountForm.onSubmit, setIsSubmitting(false) is set in the error branch, the success branch, and unconditionally in finally. The explicit branch calls are redundant — finally alone is sufficient and cleaner.


✅ Concise QA Testing Guide

# Scenario Steps Expected
1 Hard rejection Login as Bridge-hard-rejected user → attempt withdraw "Verification issue" modal → "Contact support" → support drawer opens
2 Fixable rejection Login as Bridge-fixable-rejected user → attempt withdraw "We need extra documents" modal → "Upload document" → Sumsub SDK opens inline
3 Self-heal error (claim flow) Fixable user → attempt claim → "Upload document" → API fails Error should remain visible in modal (not silently closed)
4 ToS (withdraw/deposit/claim) User with REQUIRES_INFORMATION rails → trigger transfer ToS modal → iframe → accept → no modal flash → flow proceeds
5 ToS network error Accept ToS with BE unavailable Error message shown, component not stuck
6 QR pay — unverified Scan QR as unverified user Inline KYC modal (no redirect) → "Verify now" → SDK opens
7 QR pay — verified Scan QR as approved user Proceeds to payment directly
8 SDK race condition Navigate to identity verification → click "Start Verification" immediately SDK stays open, not auto-closed
9 Cross-region KYC API error Trigger needs_enrollment → API error on initiate Error shown in modal; future APPROVED events still fire correctly
10 Happy path regression Fully approved user: withdraw, deposit, claim No KYC/ToS gates, all flows complete successfully

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useSumsubKycFlow.ts (1)

141-143: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore prevStatusRef when cross-region initiation fails.

Line 141 pre-sets prevStatusRef.current = 'APPROVED' before initiation succeeds. If the request fails (Line 152 or Line 198), that value persists and can suppress later real APPROVED transition handling (onKycSuccess path).

💡 Proposed fix
 const handleInitiateKyc = useCallback(
     async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean) => {
+        const previousStatus = prevStatusRef.current
         userInitiatedRef.current = true
         initiatingRef.current = true
         selfHealProviderRef.current = null
         setIsLoading(true)
         setError(null)

         // for cross-region: pre-set prevStatusRef to APPROVED so the fetchCurrentStatus
         // effect (which also fires when regionIntent changes) doesn't trigger onKycSuccess
         // when it sees the existing APPROVED status.
         if (crossRegion) {
             prevStatusRef.current = 'APPROVED'
         }

         try {
             const response = await initiateSumsubKyc({
                 regionIntent: overrideIntent ?? regionIntent,
                 levelName,
                 crossRegion,
             })

             if (response.error) {
+                if (crossRegion) prevStatusRef.current = previousStatus
                 setError(response.error)
                 return
             }
             ...
         } catch (e: unknown) {
+            if (crossRegion) prevStatusRef.current = previousStatus
             const message = e instanceof Error ? e.message : 'An unexpected error occurred'
             setError(message)
         } finally {

Also applies to: 152-155, 198-200

🤖 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/hooks/useSumsubKycFlow.ts` around lines 141 - 143, When cross-region is
set the code unconditionally overwrites prevStatusRef.current to 'APPROVED'
before the initiation promise resolves; change this to save the original value
(e.g., const prev = prevStatusRef.current), set prevStatusRef.current =
'APPROVED' only for the pending initiation, and on every failure/early-return
path (the initiation catch/response-failure branches around the current
initiation and the later failure branches tied to crossRegion) restore
prevStatusRef.current = prev so the original status is preserved and
onKycSuccess can still run correctly.
🧹 Nitpick comments (2)
src/components/AddWithdraw/DynamicBankAccountForm.tsx (1)

252-254: 💤 Low value

Consider replacing magic string with typed result.

The '__silent__' sentinel works but is fragile. A typed return like { error?: string; silent?: boolean } would be safer and self-documenting.

Current approach functions correctly for this PR; flagging as optional improvement.

🤖 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/AddWithdraw/DynamicBankAccountForm.tsx` around lines 252 -
254, Replace the fragile magic string check in DynamicBankAccountForm: instead
of comparing result.error to '__silent__', change the API/handler that produces
result (the value used by the submission logic around setSubmissionError and
setIsSubmitting) to return a typed shape like { error?: string; silent?: boolean
} and update the submission code to check result.silent (or !result.error)
before calling setSubmissionError(result.error); also update any functions that
construct this result to set silent: true when you intend to suppress UI errors
so the logic in the submission handler (the block that currently checks
result.error and calls setSubmissionError/setIsSubmitting) becomes explicit and
type-safe.
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx (1)

400-435: ⚡ Quick win

Gate-to-variant mapping is duplicated across files.

This mapping logic (gate.type → InitiateKycModal variant) is repeated in BankFlowManager.view.tsx and AddWithdrawCountriesList.tsx. Consider extracting to a shared utility.

♻️ Suggested extraction
// utils/gate.utils.ts
export function getKycModalVariant(gateType: BridgeGateAction['type']): 'blocked' | 'provider_rejection' | 'cross_region' | 'default' {
  switch (gateType) {
    case 'blocked_rejection': return 'blocked'
    case 'fixable_rejection': return 'provider_rejection'
    case 'needs_enrollment': return 'cross_region'
    default: return 'default'
  }
}
🤖 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/app/`(mobile-ui)/add-money/[country]/bank/page.tsx around lines 400 -
435, The gate.type → InitiateKycModal variant mapping is duplicated; extract it
into a shared function (e.g., getKycModalVariant) that accepts
BridgeGateAction['type'] and returns 'blocked' | 'provider_rejection' |
'cross_region' | 'default', then replace the inline nested ternary in
InitiateKycModal's variant prop in Add-money page with a call to
getKycModalVariant(gate.type) and similarly import/use that function in
BankFlowManager.view.tsx and AddWithdrawCountriesList.tsx so all files share the
single mapping implementation.
🤖 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/app/`(mobile-ui)/withdraw/[country]/bank/page.tsx:
- Around line 172-179: The guard re-uses a stale render-time gate value and
re-triggers the ToS flow; modify handleCreateAndInitiateOfframp (and its
callers) to accept a bypass flag (e.g., bypassGate:boolean) or await gate
refresh: update BridgeTosStep.onComplete/confirmBridgeTosAndAwaitRails() to call
handleCreateAndInitiateOfframp(true) after confirming ToS, and inside
handleCreateAndInitiateOfframp check bypassGate before performing the gate.type
!== 'ready' early-return (instead of using the closed-over gate), or
alternatively re-read the current gate from Redux/store and wait until it
becomes 'ready' before proceeding; ensure guardWithTos and setShowKycModal are
only used when not bypassing.

In `@src/components/Claim/Link/views/BankFlowManager.view.tsx`:
- Around line 516-526: The modal is being closed unconditionally in the onVerify
handler which can hide errors from sumsubFlow (e.g., sumsubFlow.error) when
sumsubFlow.handleSelfHealResubmit('BRIDGE') or sumsubFlow.handleInitiateKyc(...)
fails; update the onVerify logic so setShowKycModal(false) is called only after
the async call succeeds (e.g., await the call inside try and call
setShowKycModal(false) in the try path), and in the catch path keep the modal
open and surface sumsubFlow.error (or display an inline error) so users see
failures instead of the modal disappearing. Ensure you update the branch
handling for gate.type === 'fixable_rejection' (handleSelfHealResubmit) and the
else branch (handleInitiateKyc) accordingly.

---

Outside diff comments:
In `@src/hooks/useSumsubKycFlow.ts`:
- Around line 141-143: When cross-region is set the code unconditionally
overwrites prevStatusRef.current to 'APPROVED' before the initiation promise
resolves; change this to save the original value (e.g., const prev =
prevStatusRef.current), set prevStatusRef.current = 'APPROVED' only for the
pending initiation, and on every failure/early-return path (the initiation
catch/response-failure branches around the current initiation and the later
failure branches tied to crossRegion) restore prevStatusRef.current = prev so
the original status is preserved and onKycSuccess can still run correctly.

---

Nitpick comments:
In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 400-435: The gate.type → InitiateKycModal variant mapping is
duplicated; extract it into a shared function (e.g., getKycModalVariant) that
accepts BridgeGateAction['type'] and returns 'blocked' | 'provider_rejection' |
'cross_region' | 'default', then replace the inline nested ternary in
InitiateKycModal's variant prop in Add-money page with a call to
getKycModalVariant(gate.type) and similarly import/use that function in
BankFlowManager.view.tsx and AddWithdrawCountriesList.tsx so all files share the
single mapping implementation.

In `@src/components/AddWithdraw/DynamicBankAccountForm.tsx`:
- Around line 252-254: Replace the fragile magic string check in
DynamicBankAccountForm: instead of comparing result.error to '__silent__',
change the API/handler that produces result (the value used by the submission
logic around setSubmissionError and setIsSubmitting) to return a typed shape
like { error?: string; silent?: boolean } and update the submission code to
check result.silent (or !result.error) before calling
setSubmissionError(result.error); also update any functions that construct this
result to set silent: true when you intend to suppress UI errors so the logic in
the submission handler (the block that currently checks result.error and calls
setSubmissionError/setIsSubmitting) becomes explicit and type-safe.
🪄 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: 732fdfd6-8386-4830-816e-80a449d4f094

📥 Commits

Reviewing files that changed from the base of the PR and between 9570cc0 and 5868e00.

📒 Files selected for processing (17)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/actions/sumsub.ts
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx
  • src/components/AddWithdraw/DynamicBankAccountForm.tsx
  • src/components/Claim/Link/views/BankFlowManager.view.tsx
  • src/components/Home/ActivationCTAs.tsx
  • src/components/Kyc/BridgeTosStep.tsx
  • src/components/Kyc/InitiateKycModal.tsx
  • src/components/Kyc/states/KycProviderRejection.tsx
  • src/components/Profile/views/RegionsVerification.view.tsx
  • src/hooks/useBridgeTosStatus.ts
  • src/hooks/useBridgeTransferReadiness.ts
  • src/hooks/useProviderRejectionStatus.ts
  • src/hooks/useSumsubKycFlow.ts

Comment thread src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Comment thread src/components/Claim/Link/views/BankFlowManager.view.tsx Outdated
Hugo0
Hugo0 previously approved these changes May 6, 2026
…lt, dedup variant mapping

- useSumsubKycFlow: save/restore prevStatusRef on crossRegion failure to prevent
  suppressing subsequent legitimate APPROVED transitions
- DynamicBankAccountForm: replace __silent__ magic string with typed { silent: true }
- useBridgeTransferReadiness: extract getKycModalVariant() and getGateProviderMessage()
  helpers to deduplicate gate→modal mapping across 4 files
- BankFlowManager: only close kyc modal if sdk opened (check showWrapper), preserving
  error visibility on failure

@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: 2

♻️ Duplicate comments (1)
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx (1)

448-454: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stale gate closure causes re-entry into TOS guard after completion.

After BridgeTosStep.onComplete calls confirmBridgeTosAndAwaitRails(), the gate value captured in the closure still holds 'accept_tos'. When handleCreateAndInitiateOfframp() is immediately called, the gate check at line 176 will re-trigger guardWithTos() instead of proceeding.

🐛 Suggested fix — add bypass flag
-    const handleCreateAndInitiateOfframp = async () => {
-        if (gate.type !== 'ready') {
+    const handleCreateAndInitiateOfframp = async (skipGateCheck = false) => {
+        if (!skipGateCheck && gate.type !== 'ready') {
             if (gate.type === 'accept_tos') {
                 guardWithTos()
             } else {
                 setShowKycModal(true)
             }
             return
         }

Then update the BridgeTosStep callback:

 <BridgeTosStep
     visible={showBridgeTos}
     onComplete={() => {
         hideTos()
-        handleCreateAndInitiateOfframp()
+        handleCreateAndInitiateOfframp(true)
     }}
     onSkip={hideTos}
 />
🤖 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/app/`(mobile-ui)/withdraw/[country]/bank/page.tsx around lines 448 - 454,
The gate closure still reads 'accept_tos' causing guardWithTos to re-run right
after BridgeTosStep.onComplete; fix by adding a bypass flag (e.g., bypassTos:
boolean) to handleCreateAndInitiateOfframp and make the early gate check skip
guardWithTos when bypassTos is true, then update the BridgeTosStep onComplete
callback to call hideTos(), await confirmBridgeTosAndAwaitRails() if needed, and
invoke handleCreateAndInitiateOfframp(true); ensure guardWithTos remains
unchanged except for honoring the bypassTos parameter.
🧹 Nitpick comments (1)
src/components/AddWithdraw/AddWithdrawCountriesList.tsx (1)

99-112: ⚡ Quick win

fetchUser() on every submission adds unnecessary latency when gate blocks.

The call to fetchUser() at line 101 runs unconditionally before checking the gate. When the gate is not ready (TOS/KYC/rejection), this network call adds latency without benefit since the action will be blocked anyway. Consider moving the fetch after the gate check or making it conditional.

♻️ Suggested optimization
     const handleFormSubmit = async (
         payload: AddBankAccountPayload,
         rawData: IBankAccountDetails
     ): Promise<{ error?: string; silent?: boolean }> => {
-        // re-fetch user to ensure we have the latest KYC status
-        // (the multi-phase flow may have completed but websocket/state not yet propagated)
-        await fetchUser()
-
         // unified bridge gate: tos → fixable rejection → blocked → enrollment
         // return a non-visible error to prevent the form from treating this as success
         if (gate.type !== 'ready') {
             if (gate.type === 'accept_tos') {
                 guardWithTos()
             } else {
                 setIsKycModalOpen(true)
             }
             return { error: 'gate_blocked', silent: true }
         }

+        // re-fetch user to ensure we have the latest KYC status
+        // (the multi-phase flow may have completed but websocket/state not yet propagated)
+        await fetchUser()
+
         // scenario (1): happy path...
🤖 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/AddWithdraw/AddWithdrawCountriesList.tsx` around lines 99 -
112, The fetchUser() call is invoked unconditionally before the gate check,
causing unnecessary latency when the bridge gate blocks; update the flow in the
submission handler so you first inspect gate.type and handle non-'ready' cases
(calling guardWithTos() or setIsKycModalOpen(true) and returning the { error:
'gate_blocked', silent: true } early), and only call fetchUser() afterwards (or
call it conditionally when gate.type === 'ready') to avoid the redundant network
request.
🤖 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/Claim/Link/views/BankFlowManager.view.tsx`:
- Around line 509-516: The BridgeTosStep.onComplete closure captures a stale
gate value ('accept_tos') causing guardWithTos() to re-trigger when
handleCreateOfframpAndClaim(localBankDetails) is called; update
handleCreateOfframpAndClaim (or add an overload) to accept a bypassTos boolean
and, when true, skip the gate/guardWithTos() check, then change the onComplete
callback to call hideTos() followed by
handleCreateOfframpAndClaim(localBankDetails, true) (or the new bypass param) so
the Tos acceptance gate is not re-evaluated from the stale closure; ensure
BridgeTosStep, onComplete, hideTos, handleCreateOfframpAndClaim and the
gate/guardWithTos logic are the referenced symbols updated.

In `@src/hooks/useBridgeTransferReadiness.ts`:
- Around line 44-50: The ready branch currently returns { type: 'ready' }
without ensuring any KYC is approved, allowing unapproved users through; in
useBridgeTransferReadiness.ts update the final readiness check so that the
'ready' state is only returned when KYC is satisfied (e.g., require
isUserSumsubKycApproved OR isUserBridgeKycApproved, or use a consolidated
isKycApproved boolean) and otherwise return a proper blocking state (e.g.,
'needs_enrollment' or 'kyc_required'); adjust the logic around
isUserSumsubKycApproved, isUserBridgeKycApproved and isUserBridgeKycUnderReview
to enforce the documented priority order and ensure the function
(useBridgeTransferReadiness / its return paths) never falls through to 'ready'
for users without KYC approval.

---

Duplicate comments:
In `@src/app/`(mobile-ui)/withdraw/[country]/bank/page.tsx:
- Around line 448-454: The gate closure still reads 'accept_tos' causing
guardWithTos to re-run right after BridgeTosStep.onComplete; fix by adding a
bypass flag (e.g., bypassTos: boolean) to handleCreateAndInitiateOfframp and
make the early gate check skip guardWithTos when bypassTos is true, then update
the BridgeTosStep onComplete callback to call hideTos(), await
confirmBridgeTosAndAwaitRails() if needed, and invoke
handleCreateAndInitiateOfframp(true); ensure guardWithTos remains unchanged
except for honoring the bypassTos parameter.

---

Nitpick comments:
In `@src/components/AddWithdraw/AddWithdrawCountriesList.tsx`:
- Around line 99-112: The fetchUser() call is invoked unconditionally before the
gate check, causing unnecessary latency when the bridge gate blocks; update the
flow in the submission handler so you first inspect gate.type and handle
non-'ready' cases (calling guardWithTos() or setIsKycModalOpen(true) and
returning the { error: 'gate_blocked', silent: true } early), and only call
fetchUser() afterwards (or call it conditionally when gate.type === 'ready') to
avoid the redundant network request.
🪄 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: 9c15ab31-8a2e-4886-8b80-f9aefcaa0b3a

📥 Commits

Reviewing files that changed from the base of the PR and between 5868e00 and fa45dff.

📒 Files selected for processing (7)
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx
  • src/components/AddWithdraw/DynamicBankAccountForm.tsx
  • src/components/Claim/Link/views/BankFlowManager.view.tsx
  • src/hooks/useBridgeTransferReadiness.ts
  • src/hooks/useSumsubKycFlow.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useSumsubKycFlow.ts

Comment thread src/components/Claim/Link/views/BankFlowManager.view.tsx
Comment thread src/hooks/useBridgeTransferReadiness.ts
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Follow-up items (not blocking merge)

1. Unit tests for useBridgeTransferReadiness

The hook is load-bearing — it gates all Bridge bank transfer flows. CLAUDE.md says custom hooks that gate flows need tests. Pattern exists in useSendMoney.test.ts. Should cover:

  • blocked_rejection takes priority over accept_tos
  • accept_tos fires when additionalRequirements includes tos_acceptance
  • fixable_rejection when selfHealable: true + rejectType !== 'PROVIDER_FINAL'
  • needs_enrollment when sumsub approved but no bridge customer
  • ready when all rails are ENABLED

2. RegionIntent concurrent write — confirmed acceptable

The shouldUpdateIntent guard in initiate-kyc.ts is not atomic — two concurrent requests with different regionIntent values can both pass the guard. This is last-write-wins by design and is acceptable because:

  • The window is tiny: the guard reads verification.status and existingMetadata.regionIntent, then writes. Both happen in the same request handler — the race requires two requests arriving within milliseconds of each other for the same user.
  • The consequence is benign: if two intents race, the last one persists. The user's next action (clicking "Start Verification" or scanning a QR) will use the correct intent because handleInitiateKyc always passes the intent at call time. The stored intent only matters at KYC approval time (handleKycApproval reads it to decide which provider to submit to).
  • Once approved, intent is locked: the guard explicitly checks verification.status !== 'APPROVED' — after approval, no overwrite is possible regardless of concurrency.
  • Real-world scenario: a user would need to click "Start Verification" on Europe AND scan a QR code (LATAM) at the exact same millisecond. Even then, the Sumsub SDK opens with whichever token was returned last, and the approval webhook will read the final stored intent.
  • Atomic alternative would be worse: using UPDATE ... WHERE regionIntent = oldValue (optimistic lock) would silently fail the second request, leaving the user confused about which region they're verifying for.

…minate SDK auto-close race

the fetchCurrentStatus effect could still race with handleInitiateKyc in rare
cases — initiatingRef resets in the finally block before React batches the
showWrapper state update. adding userInitiatedRef as a guard permanently
disables the background fetch after any user-initiated flow, since the SDK
and websocket handle status updates from that point.

@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: 2

🤖 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/app/`(mobile-ui)/withdraw/manteca/page.tsx:
- Around line 411-414: The current useEffect in the component unconditionally
clears balanceErrorMessage on usdAmount or balance changes (useEffect(() =>
setBalanceErrorMessage(null), [usdAmount, balance])), which bypasses
insufficient-balance validation; replace that effect with logic that computes
whether usdAmount > balance and sets balanceErrorMessage to a descriptive error
when true and to null when false (i.e., validate inside the useEffect and call
setBalanceErrorMessage accordingly), ensuring the amount step blocks when the
requested amount exceeds the wallet balance.

In `@src/app/`(mobile-ui)/withdraw/page.tsx:
- Around line 168-175: The validateAmount function and the isContinueDisabled
gating were temporarily stubbed and must be restored: implement validateAmount
to parse the input string to a decimal/number, enforce minUsdAmount and max USD
thresholds (use the existing minUsdAmount calculation), enforce wallet balance
constraint using maxDecimalAmount, setError appropriately with descriptive
messages when invalid, and return true only when the amount passes all checks;
update isContinueDisabled to also check that parsed amount is finite and
positive AND that it respects minUsdAmount, maxDecimalAmount (wallet balance),
the component's limits validation blocking state, and the current error state so
users cannot continue when any of those validations fail. Ensure you reference
and use minUsdAmount, maxDecimalAmount, setError, and the limits validation
gating variables present in this file.
🪄 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: 2cfb6f5e-31fa-4a56-ab54-1b6614db126a

📥 Commits

Reviewing files that changed from the base of the PR and between bb91a2b and 4bd359e.

📒 Files selected for processing (3)
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/(mobile-ui)/withdraw/page.tsx
  • src/hooks/useSumsubKycFlow.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useSumsubKycFlow.ts

Comment thread src/app/(mobile-ui)/withdraw/manteca/page.tsx Outdated
Comment thread src/app/(mobile-ui)/withdraw/page.tsx Outdated
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Manual Testing Results — 20/20 Passed

All tests performed locally with FE worktree pointed to local BE + local DB (staging Bridge/Sumsub API keys). DB state was modified between tests to simulate different user states.


Bridge Bank Flows (Withdraw / Deposit)

# Test User State Flow Expected Result
1 Happy path — withdraw Bridge approved, ENABLED rails, ToS accepted Withdraw → Belgium → bank details → Review No gate, proceeds to confirmation
2 Happy path — deposit Same Add money → Belgium → enter amount → Continue No gate, proceeds
3 ToS needed — withdraw REQUIRES_INFORMATION rails, tos_acceptance in additionalRequirements, tosStatus: pending Withdraw → Belgium → bank details → Review "Accept Terms of Service" modal appears
4 ToS needed — deposit Same Add money → Belgium → enter amount → Continue Same ToS modal appears
5 ToS dismiss Same ToS modal → click "Not now" Modal closes, back to form, can retry
6 ToS accept Same ToS modal → click "Accept Terms" → iframe opens Bridge ToS iframe loads correctly
7 Hard rejection — withdraw Rails REJECTED, selfHealable: false, PROVIDER_FINAL, duplicate_customer_detected Withdraw → Belgium → bank details → Review "Verification issue" + "Contact support" modal
8 Hard rejection — deposit Same Add money → Belgium → bank details → Review Same "Verification issue" modal
9 Hard rejection — contact support Same Click "Contact support" CTA Support drawer opens (setIsSupportModalOpen)
10 Fixable rejection — withdraw Rails REJECTED, selfHealable: true, PROVIDER_FIXABLE, government_id_verification_failed Withdraw → Belgium → bank details → Review "We need extra documents" + "Upload document" modal
11 Fixable rejection — deposit Same Add money → Belgium → bank details → Review Same self-heal modal
12 Self-heal SDK Same Click "Upload document" Sumsub SDK opens inline with applicant action
13 Self-heal error reject_labels set to unrecognized reason Click "Upload document" "Something went wrong" + "Contact support" error modal

QR Pay Flow

# Test User State Flow Expected Result
14 Unverified user — inline KYC Fresh user testthu141, no KYC Scan Mercado Pago QR "Verify your identity" modal (NOT redirect to /profile/identity-verification)
15 Unverified user — SDK opens Same Click "Verify now" Sumsub SDK opens inline with handleInitiateKyc('LATAM')
16 Close modal → /home Same Close KYC modal Navigates to /home
17 Verified user — proceeds Manteca-approved user Scan QR No KYC gate, proceeds to payment screen

Identity Verification Page

# Test User State Flow Expected Result
18 SDK first click — no auto-close Sumsub APPROVED user /profile/identity-verification → click region → "Start Verification" SDK opens on first click, stays open (no race condition auto-close)

Cross-Region Enrollment

# Test User State Flow Expected Result
19 Manteca-approved → Bridge withdraw Sumsub APPROVED, Manteca ACTIVE (AR), no Bridge customer/rails Withdraw → Belgium → bank details → Review "Your identity is already verified. To enable payments in this region, we need a valid ID" modal with cross_region variant
20 Manteca-approved → Bridge deposit Same Add money → Belgium → enter amount → Continue Same cross-region enrollment modal
21 STANDARD-approved → LATAM enrollment Sumsub APPROVED (STANDARD), Bridge approved, no Manteca Withdraw → Argentina Cross-region LATAM action created, Sumsub SDK opens for Manteca enrollment
22 RegionIntent overwrite Fresh user, NOT_STARTED QR flow sets LATAM → settings page clicks Europe regionIntent correctly updated from LATAM to STANDARD (verified in DB)

Claim Flow

# Test User State Flow Expected Result
23 Claim — ToS gate REQUIRES_INFORMATION rails, tos_acceptance in requirements Claim send link → bank → confirm "Accept Terms of Service" modal on confirm screen
24 Claim — hard rejection Rails REJECTED, selfHealable: false, PROVIDER_FINAL Claim send link → bank → confirm "Verification issue" + "Contact support" modal

Priority Order Verification

# Test User State Expected Gate Result
25 Hard rejected + ToS needed REJECTED + selfHealable: false + tos_acceptance in requirements blocked_rejection (not accept_tos)
26 ToS needed + fixable rejection REJECTED + selfHealable: true + tos_acceptance in requirements accept_tos (before fixable_rejection) ✅ (unit test)
27 ToS needed + needs enrollment No Bridge rails + tos_acceptance conditions accept_tos (before needs_enrollment) ✅ (unit test)

Summary

26/26 tests passed (23 manual + 3 covered by unit tests in useBridgeTransferReadiness.test.ts)

Accounts used:

  • 63e8184f (kjkjkjs) — primary test account, DB state modified between tests to simulate Bridge states
  • caf6e963 (testthu141) — fresh user for QR pay unverified flow + cross-region testing (Manteca-approved, no Bridge)
  • Prod Bridge customer c142e797 linked to local account for happy path testing
  • Prod Manteca customer 68bc60c7 (AR) linked for cross-region testing

@Hugo0 Hugo0 merged commit 3373423 into main May 7, 2026
9 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.

2 participants