fix: check transfer readiness gate before 'under review' modal#2063
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe 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. ChangesBridge KYC Incomplete State Handling
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
Code-analysis diffPainscore total: 5709.95 → 5712.39 (+2.44) 🆕 New findings (27)
…and 7 more. ✅ Resolved (27)
…and 7 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/AddWithdraw/AddWithdrawCountriesList.tsx (1)
327-350: ⚡ Quick winDeduplicate
InitiateKycModalconfiguration 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
📒 Files selected for processing (7)
src/components/AddWithdraw/AddWithdrawCountriesList.tsxsrc/features/limits/views/LimitsPageView.tsxsrc/hooks/__tests__/useBridgeTransferReadiness.test.tssrc/hooks/useBridgeTransferReadiness.tssrc/hooks/useHomeCarouselCTAs.tsxsrc/hooks/useKycStatus.tsxsrc/hooks/useUnifiedKycStatus.ts
- 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.
|
@coderabbitai review in depth. Evaluate:
Also, write a very concise testing guide for QA |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 59331
Deep Review: PR
|
| 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 |
{ 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"
- Set user's Bridge KYC status to
incomplete, TOS not yet accepted. - Navigate to Add Money or Withdraw → select a Bridge bank method (e.g., Europe/North America).
- ✅ Expected: TOS acceptance modal appears.
- Accept TOS → ✅ Expected: the original method action replays automatically (navigates to next step).
Scenario 2 — Genuine under_review user sees the correct modal
- Set user's Bridge KYC status to
under_review. - Navigate to Add Money or Withdraw → select a Bridge bank method.
- ✅ Expected: "Your verification is under review" modal appears (not TOS).
Scenario 3 — rejected users are gated correctly
- Set user to
fixablerejection state → ✅ Expected: KYC initiation modal with resubmit option. - Set user to
blockedrejection state → ✅ Expected: KYC modal with "Contact Support" CTA.
Scenario 4 — incomplete user on home carousel
- Set user's Bridge KYC status to
incomplete. - Open home screen → scroll carousel.
- ✅ Expected: "Unlock QR code payments / Start KYC" CTA is not shown.
Scenario 5 — Limits page pending badge
- Set Bridge KYC status to
incomplete→ ✅ Expected: Europe and North America show "Pending" badge. - Set Bridge KYC status to
under_review→ ✅ Expected: Same "Pending" badge. - Set Bridge KYC status to
approved→ ✅ Expected: No pending badge.
Scenario 6 — Form view TOS replay
- Navigate directly to the bank form view (e.g., from an existing account).
- Submit form while TOS is pending → ✅ Expected: TOS modal appears.
- Accept TOS → ✅ Expected: Form submits automatically without user resubmitting.
Scenario 7 — incomplete + TOS already accepted → proceed
- Set Bridge KYC status to
incomplete, TOS already accepted. - Click bank add/withdraw method.
- ✅ Expected: No gate modal; flow proceeds to next step (backend must handle this gracefully per PR
#843).
|
@coderabbitai re-review in depth. Evaluate:
Also, write a very concise testing guide for QA |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 62331
Re-Review: PR
|
| 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))) returnAfter 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
useUnifiedKycStatusare single-equalityuseMemocalls — effectively free. checkBridgeGateis correctlyuseCallback-wrapped with a minimal, accurate dependency array ([gate, isUserBridgeKycUnderReview, guardWithTos]).pendingAfterTosRefis a ref — no re-renders on write. Correct pattern.sharedModalsis constructed inline (not memoized) but references live state (isKycModalOpen,showBridgeTos,gate), so memoizing it would gain nothing.useBridgeTransferReadiness'suseMemodependency 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 |
isUserBridgeKycUnderReview guard — see above |
getKycModalVariant for accept_tos |
🧪 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
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/components/AddWithdraw/AddWithdrawCountriesList.tsx (1)
107-126: ⚡ Quick winConsider adding a replay guard to prevent TOS modal loop.
If the
gatestate hasn't updated by the time the TOS completion callback replays the action,checkBridgeGatewill re-enter withgate.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
📒 Files selected for processing (2)
src/components/AddWithdraw/AddWithdrawCountriesList.tsxsrc/hooks/__tests__/useBridgeTransferReadiness.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/hooks/tests/useBridgeTransferReadiness.test.ts
|
@coderabbitai re-review in depth. Evaluate:
Also, write a very concise testing guide for QA |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 64524
Re-Review: PR
|
| 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 true — setIsKycModalOpen 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 |
|
| Stale dep array entries |
🧪 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 |
Summary
incompletestatus (needs TOS) saw "Your verification is under review" dead-end modal instead of the TOS acceptance flowAddWithdrawCountriesListcheckedisUserBridgeKycUnderReviewbefore the transfer readiness gate, andisBridgeUnderReviewincluded bothunder_reviewANDincompletestatusesChanges
useUnifiedKycStatus— splitisBridgeUnderReview(onlyunder_review) from newisBridgeIncomplete(onlyincomplete)useKycStatus— exposeisUserBridgeKycIncompleteAddWithdrawCountriesList— check gate before status modal in bothhandleAddMethodClickandhandleWithdrawMethodClick; addedBridgeTosStepandInitiateKycModalto list viewuseBridgeTransferReadiness— skipneeds_enrollmentforincompleteusers (already have Bridge rails)useHomeCarouselCTAs— exclude incomplete users from "start KYC" CTALimitsPageView— use combined flag for locked regions badgeCompanion PR: peanutprotocol/peanut-api-ts#843 (backend status mapping fix)
Test plan
npm test— all 53 suites pass (1074 tests)npx tsc --noEmit— cleanincompletestatus + TOS needed → sees TOS modal on country method clickunder_reviewstatus (genuinely reviewing) → sees "under review" modalrejectedstatus → gate shows rejection modal