Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/components/AddWithdraw/AddWithdrawCountriesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
}
if (gate.kind === 'accept-tos') {
guardWithTos()
} else if (gate.kind === 'provide-email') {
// A rail that flipped to email-blocked between form-open and submit
// is self-serve — open the email sheet, NOT the contact-support KYC
// modal (mirrors checkBridgeGate; the whole point of provide-email).
setShowProvideEmail(true)
} else {
setIsKycModalOpen(true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ jest.mock('@/components/AddWithdraw/DynamicBankAccountForm', () => ({ DynamicBan
jest.mock('@/components/Global/TokenAndNetworkConfirmationModal', () => ({ __esModule: true, default: () => null }))
jest.mock('@/components/Kyc/SumsubKycModals', () => ({ SumsubKycModals: () => null }))
jest.mock('@/components/Kyc/BridgeTosStep', () => ({ BridgeTosStep: () => null }))
jest.mock('@/components/Kyc/ProvideEmailStep', () => ({
__esModule: true,
default: (props: any) => (props.visible ? <div data-testid="provide-email-sheet" /> : null),
}))
jest.mock('@/components/Kyc/InitiateKycModal', () => ({
InitiateKycModal: (props: any) => (props.visible ? <div data-testid="initiate-kyc-modal" /> : null),
}))
Expand Down Expand Up @@ -235,6 +239,22 @@ describe('AddWithdrawCountriesList — bank gate', () => {
expect(mockPush).not.toHaveBeenCalled()
expect(screen.getByTestId('initiate-kyc-modal')).toBeInTheDocument()
})

// provide-email is a self-serve gate (one email unblocks the rail) — it must
// open the email sheet, NEVER the contact-support KYC modal. Both the click
// gate (checkBridgeGate) and the form-submit gate (handleFormSubmit) must
// route it there; a missing branch on the submit path turned self-serve
// recovery into a support ticket (2026-07 review finding).
it('an email-blocked gate opens the provide-email sheet, not the contact-support KYC modal', () => {
setCapabilities('provide-email', [{ status: 'blocked', channel: 'bank', country: 'US' }])

render(<AddWithdrawCountriesList flow="add" />)
fireEvent.click(screen.getByTestId('method-bank'))

expect(screen.getByTestId('provide-email-sheet')).toBeInTheDocument()
expect(screen.queryByTestId('initiate-kyc-modal')).toBeNull()
expect(mockPush).not.toHaveBeenCalled()
})
})

/**
Expand Down
29 changes: 17 additions & 12 deletions src/components/Home/ActivationCTAs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export default function ActivationCTAs({ activationStep, onDismissCard }: Activa
return {
hasFixableRejection: !!fixableRail,
hasBlockedRejection: !!blocked,
primaryRejectionMessage: (fixableRail ?? blocked)?.reason?.userMessage ?? null,
// Same precedence the copy/onClick use: email-blocked → fixable → terminal.
primaryRejectionMessage: (emailBlocked ?? fixableRail ?? blocked)?.reason?.userMessage ?? null,
blockedRail: blocked,
isEmailBlocked: !!emailBlocked,
}
Expand Down Expand Up @@ -139,17 +140,11 @@ export default function ActivationCTAs({ activationStep, onDismissCard }: Activa
if (activationStep === 'verify' && isIdentityProcessing && !isIdentityActionRequired) return null

if (hasProviderRejection) {
if (hasFixableRejection) {
return {
icon: 'globe-lock',
iconBg: 'bg-primary-1',
title: 'Complete your setup',
description: primaryRejectionMessage || 'We need an updated document before you can add money.',
ctaLabel: 'Upload document',
href: '/profile/identity-verification',
}
}
// blocked on a missing email — self-serve, not a support ticket
// Email-blocked (status=blocked) outranks a fixable RFI (status=requires-info)
// — the canonical `deriveGate` order, and the order this card's onClick
// already follows (isEmailBlocked first). Ranking fixable above email here
// made the copy say "Upload document" while the button opened the email
// sheet, and hid the document-upload path entirely when both coexisted.
if (isEmailBlocked) {
return {
icon: 'globe-lock',
Expand All @@ -161,6 +156,16 @@ export default function ActivationCTAs({ activationStep, onDismissCard }: Activa
href: '', // handled in onClick
}
}
if (hasFixableRejection) {
return {
icon: 'globe-lock',
iconBg: 'bg-primary-1',
title: 'Complete your setup',
description: primaryRejectionMessage || 'We need an updated document before you can add money.',
ctaLabel: 'Upload document',
href: '/profile/identity-verification',
}
}
// blocked
return {
icon: 'globe-lock',
Expand Down
Loading