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
Original file line number Diff line number Diff line change
Expand Up @@ -1109,12 +1109,12 @@ describe('GROUP 5: Bridge Bank Onramp', () => {
expect(screen.getByText('Country not found')).toBeInTheDocument()
})

test('user not KYC approved shows InitiateKycModal on Continue', async () => {
test('fresh user needs KYC before Bridge deposit confirmation', async () => {
mockUseKycStatus.mockReturnValue({
isUserKycApproved: false,
isUserMantecaKycApproved: false,
})
mockGate.mockReturnValue({ type: 'needs_enrollment' })
mockGate.mockReturnValue({ type: 'needs_kyc' })
resetQueryState({ step: 'inputAmount', amount: '100' })

renderWithProviders(<OnrampBankPage />)
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/states/KycActionRequired.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const KycActionRequired = ({
icon={'retry' as IconName}
className="w-full"
shadowSize="4"
onClick={onResume}
onClick={() => onResume()}
disabled={isLoading}
>
{isLoading ? 'Loading...' : 'Re-submit verification'}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/states/KycFailed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const KycFailed = ({
variant="purple"
className="w-full"
shadowSize="4"
onClick={onRetry}
onClick={() => onRetry()}
disabled={isLoading}
>
{isLoading ? 'Loading...' : 'Retry verification'}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/states/KycNotStarted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const KycNotStarted = ({ onResume, isLoading }: { onResume: () => void; i
payments."
/>

<Button className="w-full" shadowSize="4" onClick={onResume} disabled={isLoading}>
<Button className="w-full" shadowSize="4" onClick={() => onResume()} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Continue verification'}
</Button>
</div>
Expand Down
8 changes: 7 additions & 1 deletion src/components/Kyc/states/KycRequiresDocuments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export const KycRequiresDocuments = ({
)}
</div>

<Button icon="docs" className="w-full" shadowSize="4" onClick={onSubmitDocuments} disabled={isLoading}>
<Button
icon="docs"
className="w-full"
shadowSize="4"
onClick={() => onSubmitDocuments()}
disabled={isLoading}
>
{isLoading ? 'Loading...' : 'Submit documents'}
</Button>
</div>
Expand Down
94 changes: 94 additions & 0 deletions src/components/Kyc/states/__tests__/KycNotStarted.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { type ReactNode } from 'react'
import { KycActionRequired } from '../KycActionRequired'
import { KycFailed } from '../KycFailed'
import { KycNotStarted } from '../KycNotStarted'
import { KycRequiresDocuments } from '../KycRequiresDocuments'

jest.mock('use-haptic', () => ({
useHaptic: () => ({ triggerHaptic: jest.fn() }),
}))

jest.mock('@/hooks/useLongPress', () => ({
useLongPress: () => ({
isLongPressed: false,
pressProgress: 0,
handlers: {},
}),
}))

jest.mock('../../KYCStatusDrawerItem', () => ({
KYCStatusDrawerItem: () => <div data-testid="kyc-status-drawer-item" />,
}))

jest.mock('../../RejectLabelsList', () => ({
RejectLabelsList: () => <div data-testid="reject-labels-list" />,
}))

jest.mock('../../KycFailedContent', () => ({
KycFailedContent: () => <div data-testid="kyc-failed-content" />,
}))

jest.mock('../../CountryRegionRow', () => ({
CountryRegionRow: () => <div data-testid="country-region-row" />,
}))

jest.mock('@/components/Payment/PaymentInfoRow', () => ({
PaymentInfoRow: () => <div data-testid="payment-info-row" />,
}))

jest.mock('@/components/Global/Card', () => ({
__esModule: true,
default: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))

jest.mock('@/components/Global/InfoCard', () => ({
__esModule: true,
default: ({ description }: { description: string }) => <div>{description}</div>,
}))

jest.mock('@/context/ModalsContext', () => ({
useModalsContext: () => ({ setIsSupportModalOpen: jest.fn() }),
}))

describe('KycNotStarted', () => {
it('does not pass the click event to onResume', () => {
const onResume = jest.fn()
render(<KycNotStarted onResume={onResume} />)

fireEvent.click(screen.getByText('Continue verification'))

expect(onResume).toHaveBeenCalledTimes(1)
expect(onResume).toHaveBeenCalledWith()
})

it('does not pass the click event to action-required resume', () => {
const onResume = jest.fn()
render(<KycActionRequired onResume={onResume} />)

fireEvent.click(screen.getByText('Re-submit verification'))

expect(onResume).toHaveBeenCalledTimes(1)
expect(onResume).toHaveBeenCalledWith()
})

it('does not pass the click event to failed retry', () => {
const onRetry = jest.fn()
render(<KycFailed onRetry={onRetry} isSumsub={false} />)

fireEvent.click(screen.getByText('Retry verification'))

expect(onRetry).toHaveBeenCalledTimes(1)
expect(onRetry).toHaveBeenCalledWith()
})

it('does not pass the click event to document submission', () => {
const onSubmitDocuments = jest.fn()
render(<KycRequiresDocuments requirements={[]} onSubmitDocuments={onSubmitDocuments} />)

fireEvent.click(screen.getByText('Submit documents'))

expect(onSubmitDocuments).toHaveBeenCalledTimes(1)
expect(onSubmitDocuments).toHaveBeenCalledWith()
})
})
20 changes: 20 additions & 0 deletions src/hooks/__tests__/useBridgeTransferReadiness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,32 @@ describe('useBridgeTransferReadiness', () => {
const { result } = renderHook(() => useBridgeTransferReadiness())
expect(result.current.gate.type).toBe('accept_tos')
})

it('needs_kyc when user has not started standard verification', () => {
setup({ isSumsubApproved: false, isBridgeApproved: false })
const { result } = renderHook(() => useBridgeTransferReadiness())
expect(result.current.gate.type).toBe('needs_kyc')
})

it('needs_kyc when standard verification is not approved and bridge is under review', () => {
setup({ isSumsubApproved: false, isBridgeUnderReview: true })
const { result } = renderHook(() => useBridgeTransferReadiness())
expect(result.current.gate.type).toBe('needs_kyc')
})

it('ready when standard verification is not approved but bridge is approved', () => {
setup({ isSumsubApproved: false, isBridgeApproved: true })
const { result } = renderHook(() => useBridgeTransferReadiness())
expect(result.current.gate.type).toBe('ready')
})
})

describe('getKycModalVariant', () => {
it('maps gate types to modal variants', () => {
expect(getKycModalVariant('blocked_rejection')).toBe('blocked')
expect(getKycModalVariant('fixable_rejection')).toBe('provider_rejection')
expect(getKycModalVariant('needs_enrollment')).toBe('cross_region')
expect(getKycModalVariant('needs_kyc')).toBe('default')
expect(getKycModalVariant('accept_tos')).toBe('default')
expect(getKycModalVariant('ready')).toBe('default')
})
Expand All @@ -160,6 +179,7 @@ describe('getGateProviderMessage', () => {

it('returns undefined for non-rejection gates', () => {
expect(getGateProviderMessage({ type: 'accept_tos' })).toBeUndefined()
expect(getGateProviderMessage({ type: 'needs_kyc' })).toBeUndefined()
expect(getGateProviderMessage({ type: 'needs_enrollment' })).toBeUndefined()
expect(getGateProviderMessage({ type: 'ready' })).toBeUndefined()
})
Expand Down
17 changes: 13 additions & 4 deletions src/hooks/useBridgeTransferReadiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type BridgeGateAction =
| { type: 'accept_tos' }
| { type: 'fixable_rejection'; userMessage: string | null }
| { type: 'blocked_rejection'; userMessage: string | null }
| { type: 'needs_kyc' }
| { type: 'needs_enrollment' }
| { type: 'ready' }

Expand All @@ -19,8 +20,9 @@ export type BridgeGateAction =
* 1. hard rejection (contact support — tos is moot)
* 2. tos acceptance
* 3. fixable rejection (user can submit additional details)
* 4. needs enrollment (sumsub approved, bridge not started)
* 5. ready
* 4. needs standard kyc (fresh user)
* 5. needs enrollment (sumsub approved, bridge not started)
* 6. ready
*/
export function useBridgeTransferReadiness() {
const { needsBridgeTos } = useBridgeTosStatus()
Expand All @@ -42,7 +44,13 @@ export function useBridgeTransferReadiness() {
return { type: 'fixable_rejection', userMessage: bridgeRejection.userMessage }
}

// 4. needs enrollment (sumsub approved but bridge not started/approved/in-progress)
// 4. fresh user needs standard kyc before creating a transfer.
// an approved bridge rail still passes for legacy/out-of-band approvals.
if (!isUserSumsubKycApproved && !isUserBridgeKycApproved) {
return { type: 'needs_kyc' }
}

// 5. needs enrollment (sumsub approved but bridge not started/approved/in-progress)
if (
isUserSumsubKycApproved &&
!isUserBridgeKycApproved &&
Expand All @@ -52,7 +60,7 @@ export function useBridgeTransferReadiness() {
return { type: 'needs_enrollment' }
}

// 5. ready
// 6. ready
return { type: 'ready' }
}, [
needsBridgeTos,
Expand All @@ -70,6 +78,7 @@ export function useBridgeTransferReadiness() {
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_kyc') return 'default' as const
if (gateType === 'needs_enrollment') return 'cross_region' as const
return 'default' as const
}
Expand Down