Skip to content

fix: check transfer readiness gate before 'under review' modal#2063

Merged
Hugo0 merged 4 commits into
mainfrom
fix/bridge-tos-gating
May 19, 2026
Merged

fix: check transfer readiness gate before 'under review' modal#2063
Hugo0 merged 4 commits into
mainfrom
fix/bridge-tos-gating

Conversation

@kushagrasarathe

Copy link
Copy Markdown
Contributor

Summary

  • Bug: Users with Bridge KYC incomplete status (needs TOS) saw "Your verification is under review" dead-end modal instead of the TOS acceptance flow
  • Root cause: AddWithdrawCountriesList checked isUserBridgeKycUnderReview before the transfer readiness gate, and isBridgeUnderReview included both under_review AND incomplete statuses
  • Fix: Gate check runs first (TOS → rejection → enrollment → ready), then falls back to "under review" modal only for genuinely reviewing users

Changes

  • useUnifiedKycStatus — split isBridgeUnderReview (only under_review) from new isBridgeIncomplete (only incomplete)
  • useKycStatus — expose isUserBridgeKycIncomplete
  • AddWithdrawCountriesList — check gate before status modal in both handleAddMethodClick and handleWithdrawMethodClick; added BridgeTosStep and InitiateKycModal to list view
  • useBridgeTransferReadiness — skip needs_enrollment for incomplete users (already have Bridge rails)
  • useHomeCarouselCTAs — exclude incomplete users from "start KYC" CTA
  • LimitsPageView — use combined flag for locked regions badge

Companion PR: peanutprotocol/peanut-api-ts#843 (backend status mapping fix)

Test plan

  • npm test — all 53 suites pass (1074 tests)
  • npx tsc --noEmit — clean
  • User with incomplete status + TOS needed → sees TOS modal on country method click
  • User with under_review status (genuinely reviewing) → sees "under review" modal
  • User with rejected status → gate shows rejection modal

Users with Bridge KYC status 'incomplete' (needs TOS acceptance) were
shown a dead-end 'Your verification is under review' modal because
AddWithdrawCountriesList checked isUserBridgeKycUnderReview before
consulting the transfer readiness gate. The gate correctly identifies
TOS as the blocker via rails metadata, but was never reached.

Changes:
- Split isBridgeUnderReview into isBridgeUnderReview (genuinely under
  review) and isBridgeIncomplete (user needs to act)
- AddWithdrawCountriesList now checks the gate first — TOS modal opens
  for incomplete users, 'under review' only for genuinely reviewing
- Added BridgeTosStep and InitiateKycModal to the country list view
- Updated useBridgeTransferReadiness to skip enrollment for incomplete
- Updated LimitsPageView and useHomeCarouselCTAs for the new flag
@vercel

vercel Bot commented May 19, 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 19, 2026 10:20am

Request Review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR separates Bridge KYC "incomplete" from "under review" states and integrates this distinction into unified KYC hooks, transfer readiness gating, carousel CTA visibility, and Add/Withdraw UI modal gating and replay behavior.

Changes

Bridge KYC Incomplete State Handling

Layer / File(s) Summary
KYC State Foundation: Separate Bridge Incomplete Status
src/hooks/useUnifiedKycStatus.ts, src/hooks/useKycStatus.tsx
Core KYC hooks now distinguish Bridge KYC "incomplete" from "under review"; isBridgeIncomplete is added, isBridgeUnderReview narrowed, isKycInProgress updated, and useKycStatus exposes isUserBridgeKycIncomplete.
Transfer Readiness Gate Refinement
src/hooks/useBridgeTransferReadiness.ts, src/hooks/__tests__/useBridgeTransferReadiness.test.ts
useBridgeTransferReadiness consumes isUserBridgeKycIncomplete, refines the needs_enrollment gate and memo deps; tests updated to cover incomplete scenarios.
Carousel CTA Visibility Update
src/hooks/useHomeCarouselCTAs.tsx
useHomeCarouselCTAs now includes isUserBridgeKycIncomplete and tightens kyc-prompt gating and its dependency array.
Component Integration: Withdraw and Limits UI
src/components/AddWithdraw/AddWithdrawCountriesList.tsx, src/features/limits/views/LimitsPageView.tsx
AddWithdrawCountriesList centralizes checkBridgeGate, queues/replays post-TOS callbacks, and shares InitiateKycModal/BridgeTosStep/SumsubKycModals across views; LimitsPageView treats incomplete as pending for LockedRegionsList.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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 accurately summarizes the main fix: checking the transfer readiness gate before showing the 'under review' modal, which is the central change addressing the bug where incomplete KYC users saw the wrong modal.
Description check ✅ Passed The description comprehensively explains the bug, root cause, and fix with specific file changes, gate logic updates, and test scenarios—all directly related to the changeset.
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.

@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5709.95 → 5712.39 (+2.44)
Findings: 0 net (+27 new, -27 resolved)

🆕 New findings (27)

  • critical complexity — src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 109, MI 56.8, SLOC 323
  • critical complexity — src/hooks/useHomeCarouselCTAs.tsx — CC 51, MI 58.83, SLOC 227
  • high hotspot — src/hooks/useHomeCarouselCTAs.tsx — 37 commits, +462/-323 lines since 6 months ago
  • high hotspot — src/components/AddWithdraw/AddWithdrawCountriesList.tsx — 31 commits, +493/-361 lines since 6 months ago
  • medium high-mdd — src/hooks/useHomeCarouselCTAs.tsx:72 — useHomeCarouselCTAs: MDD 135.9 (uses across many lines from declarations)
  • medium high-mdd — src/components/AddWithdraw/AddWithdrawCountriesList.tsx:46 — AddWithdrawCountriesList: MDD 127.0 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useHomeCarouselCTAs.tsx:128 — : MDD 89.4 (uses across many lines from declarations)
  • medium high-dlt — src/components/AddWithdraw/AddWithdrawCountriesList.tsx:46 — AddWithdrawCountriesList: DLT 59 (calls 59 distinct functions — high context load)
  • medium high-mdd — src/hooks/useBridgeTransferReadiness.ts:25 — useBridgeTransferReadiness: MDD 35.0 (uses across many lines from declarations)
  • medium complexity — src/hooks/useUnifiedKycStatus.ts — CC 26, MI 70.11, SLOC 85
  • medium high-mdd — src/features/limits/views/LimitsPageView.tsx:20 — LimitsPageView: MDD 24.0 (uses across many lines from declarations)
  • medium method-complexity — src/hooks/useHomeCarouselCTAs.tsx:128 — CC 24 SLOC 84
  • medium high-mdd — src/hooks/useUnifiedKycStatus.ts:12 — useUnifiedKycStatus: MDD 24.3 (uses across many lines from declarations)
  • medium complexity — src/features/limits/views/LimitsPageView.tsx — CC 23, MI 64.94, SLOC 72
  • medium method-complexity — src/components/AddWithdraw/AddWithdrawCountriesList.tsx:46 — CC 20 SLOC 113
  • medium complexity — src/hooks/useBridgeTransferReadiness.ts — CC 17, MI 61.05, SLOC 47
  • medium react-effect-derives-state — src/components/AddWithdraw/AddWithdrawCountriesList.tsx:90 — small useEffect that only sets state from deps
  • low high-mdd — src/components/AddWithdraw/AddWithdrawCountriesList.tsx:380 — renderPaymentMethods: MDD 19.8 (uses across many lines from declarations)
  • low high-mdd — src/components/AddWithdraw/AddWithdrawCountriesList.tsx:389 — : MDD 19.8 (uses across many lines from declarations)
  • low high-mdd — src/features/limits/views/LimitsPageView.tsx:149 — LockedRegionsList: MDD 19.6 (uses across many lines from declarations)

…and 7 more.

✅ Resolved (27)

  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx — CC 101, MI 56.73, SLOC 290
  • src/hooks/useHomeCarouselCTAs.tsx — CC 50, MI 58.9, SLOC 226
  • src/hooks/useHomeCarouselCTAs.tsx — 36 commits, +458/-321 lines since 6 months ago
  • src/hooks/useHomeCarouselCTAs.tsx:72 — useHomeCarouselCTAs: MDD 135.4 (uses across many lines from declarations)
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx:46 — AddWithdrawCountriesList: MDD 118.2 (uses across many lines from declarations)
  • src/hooks/useHomeCarouselCTAs.tsx:127 — : MDD 89.4 (uses across many lines from declarations)
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx:46 — AddWithdrawCountriesList: DLT 56 (calls 56 distinct functions — high context load)
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx — 28 commits, +366/-267 lines since 6 months ago
  • src/hooks/useUnifiedKycStatus.ts — CC 25, MI 70.21, SLOC 80
  • src/hooks/useUnifiedKycStatus.ts:12 — useUnifiedKycStatus: MDD 24.9 (uses across many lines from declarations)
  • src/features/limits/views/LimitsPageView.tsx:20 — LimitsPageView: MDD 23.2 (uses across many lines from declarations)
  • src/hooks/useBridgeTransferReadiness.ts:25 — useBridgeTransferReadiness: MDD 23.0 (uses across many lines from declarations)
  • src/hooks/useHomeCarouselCTAs.tsx:127 — CC 23 SLOC 84
  • src/features/limits/views/LimitsPageView.tsx — CC 22, MI 65.14, SLOC 71
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx:46 — CC 20 SLOC 107
  • src/hooks/useBridgeTransferReadiness.ts — CC 16, MI 61.4, SLOC 46
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx:87 — small useEffect that only sets state from deps
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx:347 — renderPaymentMethods: MDD 19.8 (uses across many lines from declarations)
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx:356 — : MDD 19.8 (uses across many lines from declarations)
  • src/features/limits/views/LimitsPageView.tsx:146 — LockedRegionsList: MDD 19.6 (uses across many lines from declarations)

…and 7 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/AddWithdraw/AddWithdrawCountriesList.tsx 16.5 17.2 +0.7
src/hooks/useBridgeTransferReadiness.ts 6.2 6.7 +0.6

@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1079 ran, 0 failed, 0 skipped, 19.2s

📊 Coverage (unit)

metric %
statements 46.9%
branches 28.0%
functions 28.5%
lines 46.6%
⏱ 10 slowest test cases
time test
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in createOnrampForGuest
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid Italian IBAN
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (invalid characters)
0.1s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in createOnrampForGuest body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle maximum length (17 digits) US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@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

🧹 Nitpick comments (1)
src/components/AddWithdraw/AddWithdrawCountriesList.tsx (1)

327-350: ⚡ Quick win

Deduplicate InitiateKycModal configuration across both branches.

The same modal props/handlers are repeated in form and list renders. Extracting shared modal rendering will reduce drift risk.

♻️ Suggested refactor
+const renderInitiateKycModal = () => (
+  <InitiateKycModal
+    visible={isKycModalOpen}
+    onClose={() => setIsKycModalOpen(false)}
+    onVerify={async () => {
+      if (gate.type === 'fixable_rejection') {
+        await sumsubFlow.handleSelfHealResubmit('BRIDGE')
+      } else {
+        await sumsubFlow.handleInitiateKyc('STANDARD', undefined, gate.type === 'needs_enrollment' || undefined)
+      }
+    }}
+    onContactSupport={() => {
+      setIsKycModalOpen(false)
+      setIsSupportModalOpen(true)
+    }}
+    isLoading={sumsubFlow.isLoading}
+    error={sumsubFlow.error}
+    variant={getKycModalVariant(gate.type)}
+    providerMessage={getGateProviderMessage(gate)}
+    regionName={currentCountry?.title}
+  />
+)

Also applies to: 474-497

🤖 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 327 -
350, The InitiateKycModal JSX is duplicated across two render branches; extract
it once and reuse to avoid drift by creating a single shared render (e.g., a
local variable or small helper component) that returns <InitiateKycModal ...>
wired to the existing props and handlers (isKycModalOpen, setIsKycModalOpen,
setIsSupportModalOpen,
sumsubFlow.{isLoading,error,handleInitiateKyc,handleSelfHealResubmit},
getKycModalVariant(gate.type), getGateProviderMessage(gate),
currentCountry?.title). Replace both duplicated blocks with a reference to that
shared renderer and ensure gate/type-dependent logic for onVerify still uses
gate.type to call sumsubFlow.handleSelfHealResubmit('BRIDGE') for
'fixable_rejection' or sumsubFlow.handleInitiateKyc('STANDARD', undefined,
gate.type === 'needs_enrollment' || undefined) otherwise.
🤖 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/features/limits/views/LimitsPageView.tsx`:
- Line 70: Run Prettier to fix formatting in LimitsPageView.tsx so CI passes;
specifically format the line rendering the LockedRegionsList component (the JSX
with LockedRegionsList, props filteredLockedRegions, isUserBridgeKycUnderReview,
isUserBridgeKycIncomplete) and commit the resulting changes (or run the repo's
formatter script) so the file adheres to the project's Prettier rules.

In `@src/hooks/useKycStatus.tsx`:
- Around line 10-11: The file fails Prettier checks; run Prettier to reformat
src/hooks/useKycStatus.tsx (or your editor's format command) so the
destructuring line that calls useUnifiedKycStatus() and any surrounding code
match the repo's Prettier rules; e.g., run prettier --write
src/hooks/useKycStatus.tsx (or run project-wide prettier --write) and commit the
reformatted file so CI formatting checks pass.

---

Nitpick comments:
In `@src/components/AddWithdraw/AddWithdrawCountriesList.tsx`:
- Around line 327-350: The InitiateKycModal JSX is duplicated across two render
branches; extract it once and reuse to avoid drift by creating a single shared
render (e.g., a local variable or small helper component) that returns
<InitiateKycModal ...> wired to the existing props and handlers (isKycModalOpen,
setIsKycModalOpen, setIsSupportModalOpen,
sumsubFlow.{isLoading,error,handleInitiateKyc,handleSelfHealResubmit},
getKycModalVariant(gate.type), getGateProviderMessage(gate),
currentCountry?.title). Replace both duplicated blocks with a reference to that
shared renderer and ensure gate/type-dependent logic for onVerify still uses
gate.type to call sumsubFlow.handleSelfHealResubmit('BRIDGE') for
'fixable_rejection' or sumsubFlow.handleInitiateKyc('STANDARD', undefined,
gate.type === 'needs_enrollment' || undefined) otherwise.
🪄 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: d393be0b-1f1f-4456-b09c-a79472414cff

📥 Commits

Reviewing files that changed from the base of the PR and between d96a22b and 373afb7.

📒 Files selected for processing (7)
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx
  • src/features/limits/views/LimitsPageView.tsx
  • src/hooks/__tests__/useBridgeTransferReadiness.test.ts
  • src/hooks/useBridgeTransferReadiness.ts
  • src/hooks/useHomeCarouselCTAs.tsx
  • src/hooks/useKycStatus.tsx
  • src/hooks/useUnifiedKycStatus.ts

Comment thread src/features/limits/views/LimitsPageView.tsx Outdated
Comment thread src/hooks/useKycStatus.tsx Outdated
- Extract checkBridgeGate() helper — gate-check + under-review block
  was duplicated in handleAddMethodClick and handleWithdrawMethodClick
- Hoist InitiateKycModal, BridgeTosStep, SumsubKycModals into
  sharedModals fragment — rendered once instead of duplicated in
  form and list views
- Fix BridgeTosStep onComplete in list view: was a no-op (just
  hideTos). Now stores the interrupted method click in a ref and
  replays it after TOS acceptance. Form view falls back to
  formRef.current?.handleSubmit() as before.
@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 19, 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 "=== useUnifiedKycStatus ==="
cat src/hooks/useUnifiedKycStatus.ts

echo ""
echo "=== useKycStatus ==="
cat src/hooks/useKycStatus.tsx

echo ""
echo "=== useBridgeTransferReadiness ==="
cat src/hooks/useBridgeTransferReadiness.ts

echo ""
echo "=== useHomeCarouselCTAs ==="
cat src/hooks/useHomeCarouselCTAs.tsx

echo ""
echo "=== LimitsPageView ==="
cat src/features/limits/views/LimitsPageView.tsx

echo ""
echo "=== AddWithdrawCountriesList ==="
cat src/components/AddWithdraw/AddWithdrawCountriesList.tsx

echo ""
echo "=== Test file ==="
cat src/hooks/__tests__/useBridgeTransferReadiness.test.ts

echo ""
echo "=== useBridgeTransferReadiness gate logic - full ==="
rg -n "isBridgeIncomplete|isBridgeUnderReview|needs_enrollment|accept_tos|gate" src/hooks/useBridgeTransferReadiness.ts

echo ""
echo "=== All usages of isUserBridgeKycIncomplete ==="
rg -n "isUserBridgeKycIncomplete" --type ts --type tsx -A2 -B2

echo ""
echo "=== All usages of isBridgeIncomplete ==="
rg -n "isBridgeIncomplete" --type ts --type tsx -A2 -B2

echo ""
echo "=== checkBridgeGate usage in AddWithdrawCountriesList ==="
rg -n "checkBridgeGate\|postTosCallback\|pendingCallback" src/components/AddWithdraw/AddWithdrawCountriesList.tsx

echo ""
echo "=== BridgeTosStep usage ==="
rg -n "BridgeTosStep\|InitiateKycModal\|sharedModals" src/components/AddWithdraw/AddWithdrawCountriesList.tsx

echo ""
echo "=== Other files referencing isBridgeUnderReview ==="
rg -rn "isBridgeUnderReview\|isUserBridgeKycUnderReview" --type ts --type tsx -A2 -B2

Length of output: 59331


@kushagrasarathe Here's the in-depth review:


Deep Review: PR #2063

1. Risk of New Bugs / Breaking Changes

🔴 Medium Risk — handleFormSubmit duplicates gate logic without the isUserBridgeKycUnderReview guard

checkBridgeGate (used in the list-view click handlers) has two layers of protection:

  1. gate.type !== 'ready' → open TOS or KYC modal
  2. isUserBridgeKycUnderReview → open the status modal

But handleFormSubmit only reimplements layer 1 inline, without the isUserBridgeKycUnderReview check:

// handleFormSubmit (form view) — only layer 1:
if (gate.type !== 'ready') { ... }
// then falls through to:
if (!isUserKycApproved) {
    await sumsubFlow.handleInitiateKyc('STANDARD')  // ← fires for under_review users too
}

If a user's status flips to under_review between navigating to the form view and submitting, the gate returns ready (under_review is not a gate condition), isUserKycApproved is false, and Sumsub enrollment is erroneously initiated. This is a low-probability race, but a straightforward fix — either call checkBridgeGate from handleFormSubmit or add the missing guard:

if (isUserBridgeKycUnderReview) {
    setShowKycStatusModal(true)
    return { error: 'gate_blocked', silent: true }
}

🟡 Medium Risk — isBridgeIncomplete + TOS already accepted → gate returns ready silently

If a user has incomplete bridge status but has already accepted TOS (so needsBridgeTos = false), the gate skips needs_enrollment (correct) and returns ready. The user is then allowed to proceed to bank add/withdraw — but whether the backend actually accepts this depends entirely on the companion PR peanutprotocol/peanut-api-ts#843. If that PR is not co-deployed, the backend may reject the call silently. There's no defensive client-side messaging for this state.


🟡 Low-Medium Risk — TOS replay could loop if gate remains non-ready

In handleAddMethodClick:

if (checkBridgeGate(() => handleAddMethodClick(method))) return

After TOS acceptance, pendingAfterTosRef replays handleAddMethodClick(method), which re-enters checkBridgeGate. If (in an edge case) the gate is still accept_tos — e.g., a very slow state update — guardWithTos() is called again and pendingAfterTosRef is overwritten with a new () => handleAddMethodClick(method). This creates a repeating TOS loop. Adding a guard (e.g., max retry count or a isReplaying ref) would prevent this.


🟢 Low Risk — getKycModalVariant('accept_tos') is a dead code path

export function getKycModalVariant(gateType: BridgeGateAction['type']) {
    if (gateType === 'blocked_rejection') return 'blocked' as const
    if (gateType === 'fixable_rejection') return 'provider_rejection' as const
    if (gateType === 'needs_enrollment') return 'cross_region' as const
    return 'default' as const  // covers 'accept_tos' and 'ready'
}

When gate.type === 'accept_tos', checkBridgeGate calls guardWithTos() — it never calls setIsKycModalOpen(true). So getKycModalVariant('accept_tos') is never actually reached in practice. Not a bug, but may confuse future maintainers. Consider adding a comment or exhaustive switch.


🟢 Low Risk — Missing test for isBridgeIncomplete + needsBridgeTos → accept_tos

The new isBridgeIncomplete flag interacts with the TOS gate (incomplete user who still needs TOS). This is the exact bug scenario described in the PR and would be a valuable regression test:

it('accept_tos when bridge incomplete and tos needed', () => {
    setup({ needsBridgeTos: true, isBridgeIncomplete: true })
    const { result } = renderHook(() => useBridgeTransferReadiness())
    expect(result.current.gate.type).toBe('accept_tos')
})

2. Performance Impact

No meaningful performance regressions. The changes are:

  • New useMemo entries in useUnifiedKycStatus are atomic primitive comparisons — negligible cost.
  • checkBridgeGate is properly wrapped in useCallback with a complete dependency array.
  • pendingAfterTosRef is a ref (no re-render on assignment) — this is the correct pattern for storing a callback without triggering re-renders.
  • sharedModals is constructed inline in the render function body. Since it references live state (isKycModalOpen, showBridgeTos, gate), memoizing it wouldn't help. This is acceptable.

One minor note: isCardPioneerEligible and isCardPioneerLoading remain in generateCarouselCTAs's dependency array in useHomeCarouselCTAs but are not referenced in the function body. This is a pre-existing issue unrelated to this PR, but worth a cleanup ticket.


3. Code Quality & DRY

Improvements are solid:

Pattern Assessment
checkBridgeGate() callback ✅ Correct DRY — eliminates duplicate guard logic across both click handlers
sharedModals JSX fragment ✅ Correct DRY — single source of truth for modal tree in both list and form views
pendingAfterTosRef for TOS replay ✅ Good pattern — ref avoids closure staleness and extra re-renders
isBridgeIncomplete split from isBridgeUnderReview ✅ Clear semantic separation at the source (single source of truth hook)
handleFormSubmit inline gate logic ⚠️ Acceptable divergence (needs { error, silent } return shape), but missing isUserBridgeKycUnderReview check (see above)

🧪 QA Testing Guide

Setup requirements: Companion backend PR #843 must be deployed; use staging with controllable Bridge KYC status per user.

Scenario 1 — Main bug fix: incomplete user sees TOS, not "under review"

  1. Set user's Bridge KYC status to incomplete, TOS not yet accepted.
  2. Navigate to Add Money or Withdraw → select a Bridge bank method (e.g., Europe/North America).
  3. ✅ Expected: TOS acceptance modal appears.
  4. Accept TOS → ✅ Expected: the original method action replays automatically (navigates to next step).

Scenario 2 — Genuine under_review user sees the correct modal

  1. Set user's Bridge KYC status to under_review.
  2. Navigate to Add Money or Withdraw → select a Bridge bank method.
  3. ✅ Expected: "Your verification is under review" modal appears (not TOS).

Scenario 3 — rejected users are gated correctly

  1. Set user to fixable rejection state → ✅ Expected: KYC initiation modal with resubmit option.
  2. Set user to blocked rejection state → ✅ Expected: KYC modal with "Contact Support" CTA.

Scenario 4 — incomplete user on home carousel

  1. Set user's Bridge KYC status to incomplete.
  2. Open home screen → scroll carousel.
  3. ✅ Expected: "Unlock QR code payments / Start KYC" CTA is not shown.

Scenario 5 — Limits page pending badge

  1. Set Bridge KYC status to incomplete → ✅ Expected: Europe and North America show "Pending" badge.
  2. Set Bridge KYC status to under_review → ✅ Expected: Same "Pending" badge.
  3. Set Bridge KYC status to approved → ✅ Expected: No pending badge.

Scenario 6 — Form view TOS replay

  1. Navigate directly to the bank form view (e.g., from an existing account).
  2. Submit form while TOS is pending → ✅ Expected: TOS modal appears.
  3. Accept TOS → ✅ Expected: Form submits automatically without user resubmitting.

Scenario 7 — incomplete + TOS already accepted → proceed

  1. Set Bridge KYC status to incomplete, TOS already accepted.
  2. Click bank add/withdraw method.
  3. ✅ Expected: No gate modal; flow proceeds to next step (backend must handle this gracefully per PR #843).

@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 19, 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 "=== Recent commits on this PR branch ==="
git log --oneline -10

echo ""
echo "=== useUnifiedKycStatus ==="
cat src/hooks/useUnifiedKycStatus.ts

echo ""
echo "=== useKycStatus ==="
cat src/hooks/useKycStatus.tsx

echo ""
echo "=== useBridgeTransferReadiness ==="
cat src/hooks/useBridgeTransferReadiness.ts

echo ""
echo "=== useHomeCarouselCTAs ==="
cat src/hooks/useHomeCarouselCTAs.tsx

echo ""
echo "=== LimitsPageView ==="
cat src/features/limits/views/LimitsPageView.tsx

echo ""
echo "=== AddWithdrawCountriesList (full) ==="
cat src/components/AddWithdraw/AddWithdrawCountriesList.tsx

echo ""
echo "=== Test file ==="
cat src/hooks/__tests__/useBridgeTransferReadiness.test.ts

echo ""
echo "=== handleFormSubmit - full context ==="
rg -n "handleFormSubmit|checkBridgeGate|isUserBridgeKycUnderReview|pendingAfterTos|postTosCallback" src/components/AddWithdraw/AddWithdrawCountriesList.tsx -A3 -B3

echo ""
echo "=== All references to isBridgeUnderReview / isBridgeIncomplete ==="
rg -rn "isBridgeUnderReview|isBridgeIncomplete|isUserBridgeKycIncomplete|isUserBridgeKycUnderReview" --type ts --type tsx -A1 -B1

echo ""
echo "=== getKycModalVariant ==="
rg -rn "getKycModalVariant" --type ts --type tsx -A5 -B2

Length of output: 62331


@kushagrasarathe Here is the updated in-depth re-review based on the current code (post-refactor commit d95cffcb):


Re-Review: PR #2063

What's Improved Since the Original Review ✅

Area Improvement
checkBridgeGate Correctly extracted with both gate layers; used consistently across both click handlers
sharedModals Hoisted to single JSX fragment — renders once in both list and form views
pendingAfterTosRef Correct pattern for TOS replay; avoids closure staleness and unnecessary re-renders
isBridgeIncomplete split Clean semantic separation at the source hook; isKycInProgress correctly includes both
Regression test ready when sumsub approved and bridge incomplete (enrollment not needed) covers the core bug scenario

1. Risk of New Bugs / Breaking Existing Functionality

🔴 Medium Risk — handleFormSubmit still missing the isUserBridgeKycUnderReview guard (unchanged)

checkBridgeGate (list view) has two protection layers:

// Layer 1: gate not ready
if (gate.type !== 'ready') { ... return true }
// Layer 2: under_review race
if (isUserBridgeKycUnderReview) { setShowKycStatusModal(true); return true }

handleFormSubmit (form view) only has layer 1:

if (gate.type !== 'ready') { ... return { error: 'gate_blocked', silent: true } }
// ← no isUserBridgeKycUnderReview check here

if (!isUserKycApproved) {
    await sumsubFlow.handleInitiateKyc('STANDARD') // ← fires for under_review users
}

For an under_review user: gate returns ready (correct — under_review is not a gate condition), isUserKycApproved is false, and Sumsub enrollment is initiated erroneously. The fix is one line:

// After the gate.type check in handleFormSubmit:
if (isUserBridgeKycUnderReview) {
    setShowKycStatusModal(true)
    return { error: 'gate_blocked', silent: true }
}

🟡 Medium Risk — incomplete + TOS already accepted → client proceeds, backend may not

If isUserBridgeKycIncomplete = true and needsBridgeTos = false (TOS already accepted), the gate returns ready and the client proceeds to bank add/withdraw. Whether the backend accepts this depends entirely on companion PR peanutprotocol/peanut-api-ts#843. If it's not co-deployed, the backend silently rejects the call with no client-side defensive messaging. Make sure both land in the same deploy window.


🟡 Low-Medium Risk — TOS replay loop (unchanged)

if (checkBridgeGate(() => handleAddMethodClick(method))) return

After TOS acceptance, onComplete replays handleAddMethodClick(method), which re-enters checkBridgeGate. If the TOS state hasn't propagated yet (race/slow update), gate.type is still accept_tos, pendingAfterTosRef is overwritten again with a fresh callback, and guardWithTos() fires again — repeating the TOS modal indefinitely.

Suggested guard:

const isReplayingTosRef = useRef(false)

// In BridgeTosStep onComplete:
onComplete={() => {
    hideTos()
    const replay = pendingAfterTosRef.current
    pendingAfterTosRef.current = null
    if (replay) {
        isReplayingTosRef.current = true   // set flag
        replay()
        isReplayingTosRef.current = false  // clear after sync
    } else {
        formRef.current?.handleSubmit()
    }
}}

// In checkBridgeGate:
if (gate.type === 'accept_tos' && !isReplayingTosRef.current) {
    pendingAfterTosRef.current = onAfterTos ?? null
    guardWithTos()
}

🟢 Low Risk — getKycModalVariant('accept_tos') is unreachable (unchanged)

When gate.type === 'accept_tos', checkBridgeGate calls guardWithTos() and never calls setIsKycModalOpen(true). So InitiateKycModal is never rendered for accept_tos, and getKycModalVariant is never invoked with that argument. The function falls through to return 'default' as const — not a runtime bug, but worth a comment for clarity.


🟢 Low Risk — Stale entries in generateCarouselCTAs dependency array (pre-existing)

isCardPioneerEligible and isCardPioneerLoading remain in the useCallback dependency array but are not referenced in the function body. This causes unnecessary re-computation but no functional regression. Flagging for a cleanup ticket.


🟢 Low Risk — Missing regression test for isBridgeIncomplete + needsBridgeTos → accept_tos

The test suite has the incomplete + sumsub → ready case (new ✅) but still doesn't explicitly test the main PR bug scenario:

it('accept_tos when bridge incomplete and tos needed', () => {
    setup({ needsBridgeTos: true, isBridgeIncomplete: true })
    const { result } = renderHook(() => useBridgeTransferReadiness())
    expect(result.current.gate.type).toBe('accept_tos')
})

The behavior is correct (the generic accept_tos test covers the path), but this would be a precise regression guard for the exact bug described in the PR.


2. Performance Impact

No regressions. Specific notes:

  • All new booleans in useUnifiedKycStatus are single-equality useMemo calls — effectively free.
  • checkBridgeGate is correctly useCallback-wrapped with a minimal, accurate dependency array ([gate, isUserBridgeKycUnderReview, guardWithTos]).
  • pendingAfterTosRef is a ref — no re-renders on write. Correct pattern.
  • sharedModals is constructed inline (not memoized) but references live state (isKycModalOpen, showBridgeTos, gate), so memoizing it would gain nothing.
  • useBridgeTransferReadiness's useMemo dependency array is now complete and accurate.

3. Code Quality & DRY

Pattern Assessment
checkBridgeGate extraction ✅ Solid DRY — single callback for both click handlers
sharedModals JSX fragment ✅ Correct — eliminates duplicate modal trees across views
pendingAfterTosRef ✅ Good pattern — avoids stale closure and extra re-renders
isBridgeIncomplete semantic split ✅ Single source of truth in useUnifiedKycStatus
handleFormSubmit inline gate logic ⚠️ Acceptable divergence (needs different return shape) but missing isUserBridgeKycUnderReview guard — see above
getKycModalVariant for accept_tos ⚠️ Dead code branch; harmless but may confuse future maintainers

🧪 QA Testing Guide (concise)

Prerequisite: companion backend PR peanutprotocol/peanut-api-ts#843 must be deployed. Use staging with controllable Bridge KYC status.

# Scenario Steps Expected
1 Main bug fix Bridge KYC = incomplete, TOS not accepted → click Bridge bank method TOS modal appears; accept → original action replays automatically
2 Genuine under-review Bridge KYC = under_review → click Bridge bank method "Under review" modal appears (not TOS)
3 Rejected - fixable Bridge KYC = fixable rejection → click Bridge bank method KYC modal with resubmit option
4 Rejected - blocked Bridge KYC = blocked rejection → click Bridge bank method KYC modal with "Contact Support" CTA
5 Home carousel Bridge KYC = incomplete → open home carousel "Start KYC" CTA is hidden
6 Limits page badge Bridge KYC = incomplete or under_review → Limits page Europe & North America show "Pending" badge
7 Form view TOS replay Navigate to bank form → submit while TOS pending TOS modal appears; accept → form auto-submits
8 Incomplete + TOS accepted Bridge KYC = incomplete, TOS already accepted → click method No gate modal; flow proceeds (requires #843 on backend)

Summary of Open Action Items

Priority Item
🔴 Should fix Add isUserBridgeKycUnderReview guard to handleFormSubmit
🟡 Coordinate Ensure #843 is co-deployed to avoid silent backend rejections for incomplete + TOS accepted users
🟡 Nice to have Add a replay guard (isReplayingTosRef) to prevent TOS loop on slow state updates
🟢 Follow-up Add explicit regression test for isBridgeIncomplete + needsBridgeTos → accept_tos
🟢 Cleanup Remove isCardPioneerEligible/isCardPioneerLoading from generateCarouselCTAs dep array

- handleFormSubmit now checks isUserBridgeKycUnderReview after the gate
  check — prevents erroneously initiating sumsub for users whose bridge
  KYC is genuinely under review (reachable via deep-link or back nav)
- add explicit regression test for the main bug scenario: bridge
  incomplete + tos needed → gate returns accept_tos

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

🧹 Nitpick comments (1)
src/components/AddWithdraw/AddWithdrawCountriesList.tsx (1)

107-126: ⚡ Quick win

Consider adding a replay guard to prevent TOS modal loop.

If the gate state hasn't updated by the time the TOS completion callback replays the action, checkBridgeGate will re-enter with gate.type === 'accept_tos' and re-show the TOS modal. Adding a guard (e.g., a ref flag or single-shot replay limit) would prevent this race.

🔧 Proposed fix: add replay guard
 // stores the callback to replay after tos acceptance in the list view
 const pendingAfterTosRef = useRef<(() => void) | null>(null)
+const isReplayingRef = useRef(false)

 /** returns true if the user is gated (caller should return early) */
 const checkBridgeGate = useCallback(
     (onAfterTos?: () => void): boolean => {
+        // skip re-gating during a TOS replay
+        if (isReplayingRef.current) {
+            isReplayingRef.current = false
+            return false
+        }
         if (gate.type !== 'ready') {
             if (gate.type === 'accept_tos') {
                 pendingAfterTosRef.current = onAfterTos ?? null
                 guardWithTos()
             } else {
                 setIsKycModalOpen(true)
             }
             return true
         }
         if (isUserBridgeKycUnderReview) {
             setShowKycStatusModal(true)
             return true
         }
         return false
     },
     [gate, isUserBridgeKycUnderReview, guardWithTos]
 )

Then in BridgeTosStep.onComplete:

 onComplete={() => {
     hideTos()
     const replay = pendingAfterTosRef.current
     pendingAfterTosRef.current = null
-    if (replay) replay()
+    if (replay) {
+        isReplayingRef.current = true
+        replay()
+    }
     else formRef.current?.handleSubmit()
 }}
🤖 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 107 -
126, checkBridgeGate can re-enter and re-show the TOS modal when a replayed
pendingAfterTosRef callback fires because gate may still be 'accept_tos'; add a
single-shot replay guard (e.g., a ref like tosReplayGuardRef or a replayCount
ref) checked inside checkBridgeGate before handling 'accept_tos' and set the
guard when you trigger the pendingAfterTosRef replay in BridgeTosStep.onComplete
(reset/clear it after the replay completes or after a short timeout) so
guardWithTos/pendingAfterTosRef logic only runs once and avoids a modal loop.
🤖 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.

Nitpick comments:
In `@src/components/AddWithdraw/AddWithdrawCountriesList.tsx`:
- Around line 107-126: checkBridgeGate can re-enter and re-show the TOS modal
when a replayed pendingAfterTosRef callback fires because gate may still be
'accept_tos'; add a single-shot replay guard (e.g., a ref like tosReplayGuardRef
or a replayCount ref) checked inside checkBridgeGate before handling
'accept_tos' and set the guard when you trigger the pendingAfterTosRef replay in
BridgeTosStep.onComplete (reset/clear it after the replay completes or after a
short timeout) so guardWithTos/pendingAfterTosRef logic only runs once and
avoids a modal loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7470265-9359-48b7-bb44-6e1ed4414563

📥 Commits

Reviewing files that changed from the base of the PR and between 5ca5342 and f2fccae.

📒 Files selected for processing (2)
  • src/components/AddWithdraw/AddWithdrawCountriesList.tsx
  • src/hooks/__tests__/useBridgeTransferReadiness.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/tests/useBridgeTransferReadiness.test.ts

@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 19, 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 "=== Recent commits ==="
git log --oneline -10

echo ""
echo "=== handleFormSubmit + checkBridgeGate + isUserBridgeKycUnderReview in AddWithdrawCountriesList ==="
rg -n "handleFormSubmit|checkBridgeGate|isUserBridgeKycUnderReview|pendingAfterTos|isReplayingTos|guardWithTos|guardWithKyc" src/components/AddWithdraw/AddWithdrawCountriesList.tsx -A5 -B2

echo ""
echo "=== Full AddWithdrawCountriesList ==="
cat src/components/AddWithdraw/AddWithdrawCountriesList.tsx

echo ""
echo "=== Full test file ==="
cat src/hooks/__tests__/useBridgeTransferReadiness.test.ts

echo ""
echo "=== useBridgeTransferReadiness full ==="
cat src/hooks/useBridgeTransferReadiness.ts

echo ""
echo "=== useUnifiedKycStatus full ==="
cat src/hooks/useUnifiedKycStatus.ts

echo ""
echo "=== useHomeCarouselCTAs full ==="
cat src/hooks/useHomeCarouselCTAs.tsx

echo ""
echo "=== LimitsPageView ==="
cat src/features/limits/views/LimitsPageView.tsx

echo ""
echo "=== useKycStatus ==="
cat src/hooks/useKycStatus.tsx

echo ""
echo "=== All isBridgeIncomplete / isUserBridgeKycIncomplete usages ==="
rg -rn "isBridgeIncomplete|isUserBridgeKycIncomplete" --type ts --type tsx -A2 -B2

echo ""
echo "=== getKycModalVariant usages ==="
rg -rn "getKycModalVariant" --type ts --type tsx -A5 -B2

echo ""
echo "=== isCardPioneerEligible in useHomeCarouselCTAs ==="
rg -n "isCardPioneer" src/hooks/useHomeCarouselCTAs.tsx -A2 -B2

Length of output: 64524


@kushagrasarathe Here is the current state after reading the latest code (commit f2fccaef):


Re-Review: PR #2063 (Current State)

What's Fixed Since Last Review ✅

Item Status
handleFormSubmit missing isUserBridgeKycUnderReview guard Fixed — guard added at lines 148–151
Missing regression test for incomplete + needsBridgeTos → accept_tos Fixed — test 'accept_tos when bridge incomplete and tos needed (main bug scenario)' now present

1. Remaining Risks

🟡 Low-Medium — TOS replay loop on slow state propagation

pendingAfterTosRef is correctly nulled before replay() runs, which prevents double-storing the callback. However, if state hasn't propagated by the time replay() re-enters checkBridgeGate, gate.type can still be accept_tos, writing a new callback to pendingAfterTosRef and calling guardWithTos() again — causing a repeated TOS modal. A one-liner guard prevents this:

const isReplayingRef = useRef(false)

// In BridgeTosStep onComplete:
onComplete={() => {
    hideTos()
    const replay = pendingAfterTosRef.current
    pendingAfterTosRef.current = null
    if (replay) {
        isReplayingRef.current = true
        replay()
        isReplayingRef.current = false
    } else {
        formRef.current?.handleSubmit()
    }
}}

// In checkBridgeGate, guard the accept_tos branch:
if (gate.type === 'accept_tos' && !isReplayingRef.current) {
    pendingAfterTosRef.current = onAfterTos ?? null
    guardWithTos()
}

🟡 Medium — incomplete + TOS already accepted → backend dependency

If isUserBridgeKycIncomplete = true and needsBridgeTos = false, the gate returns ready and the client proceeds. This is intentional but wholly dependent on companion PR peanutprotocol/peanut-api-ts#843 being co-deployed. A missing deploy would cause silent backend rejection with no client-side feedback. Ensure both land in the same deploy window.


🟢 Low — getKycModalVariant('accept_tos') is a dead code path

In checkBridgeGate, the accept_tos branch calls guardWithTos() and returns truesetIsKycModalOpen is never called, so InitiateKycModal is never rendered with gate.type === 'accept_tos'. getKycModalVariant falls through to 'default' for this input but is never actually invoked with it. Not a bug, but a comment would help future maintainers:

// Note: 'accept_tos' is handled by guardWithTos() in checkBridgeGate;
// InitiateKycModal is never rendered for this gate type.
export function getKycModalVariant(gateType: BridgeGateAction['type']) { ... }

🟢 Low — Stale entries in generateCarouselCTAs dependency array

isCardPioneerEligible and isCardPioneerLoading are destructured and present in the useCallback dep array, but generateCarouselCTAs only references hasCardPioneerPurchased. These stale deps cause unnecessary recomputation. Cleanup:

// Remove from dep array:
- isCardPioneerEligible,
- isCardPioneerLoading,

This is pre-existing and unrelated to this PR, but worth a cleanup ticket.


2. Performance

No regressions. All new boolean flags are single useMemo equality checks — negligible. checkBridgeGate is correctly useCallback-wrapped with a minimal, accurate dep array ([gate, isUserBridgeKycUnderReview, guardWithTos]). pendingAfterTosRef is a ref — no re-renders on write. sharedModals constructed inline is fine since it references live state.


3. Code Quality & DRY

Pattern Assessment
checkBridgeGate extraction ✅ Solid — single callback for both click handlers
sharedModals JSX fragment ✅ Single source of truth for modal tree across both views
pendingAfterTosRef ✅ Good pattern — avoids stale closure and extra re-renders
isBridgeIncomplete semantic split ✅ Clean — single source of truth in useUnifiedKycStatus
handleFormSubmit gate logic ✅ Now aligned — both gate check + isUserBridgeKycUnderReview guard present
getKycModalVariant for accept_tos ⚠️ Unreachable branch — add a comment
Stale dep array entries ⚠️ Pre-existing — schedule cleanup

🧪 QA Testing Guide (concise)

Prerequisite: backend PR peanutprotocol/peanut-api-ts#843 must be deployed. Use staging with controllable Bridge KYC status.

# Scenario Steps Expected
1 Main bug fix Bridge KYC = incomplete, TOS not accepted → click Bridge bank method (list view) TOS modal appears; accept → original action replays automatically
2 Form view TOS replay Navigate to bank form → submit while TOS pending TOS modal appears; accept → form auto-submits
3 Genuine under-review (list) Bridge KYC = under_review → click Bridge bank method "Under review" modal — no TOS
4 Genuine under-review (form) Bridge KYC = under_review → submit form directly "Under review" modal shown; Sumsub not initiated
5 Rejected — fixable Bridge KYC = fixable rejection → click Bridge bank method KYC modal with resubmit option
6 Rejected — blocked Bridge KYC = blocked rejection → click Bridge bank method KYC modal with "Contact Support" CTA
7 Home carousel Bridge KYC = incomplete → home screen "Start KYC" CTA is not shown
8 Limits page badge Bridge KYC = incomplete or under_review → Limits page Europe & North America show "Pending" badge
9 Incomplete + TOS accepted Bridge KYC = incomplete, TOS already accepted → click method No gate modal; flow proceeds (requires #843 on backend)

Open Action Items Summary

Priority Item
🟡 Coordinate Co-deploy with peanutprotocol/peanut-api-ts#843 to handle incomplete + TOS accepted on the backend
🟡 Nice to have Add isReplayingRef guard to prevent TOS loop on slow state propagation
🟢 Follow-up Comment getKycModalVariant to note accept_tos is never passed in practice
🟢 Cleanup Remove isCardPioneerEligible/isCardPioneerLoading from generateCarouselCTAs dep array

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