diff --git a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx index 91a24a606..4a2b2b0fc 100644 --- a/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx +++ b/src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx @@ -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() diff --git a/src/components/Kyc/states/KycActionRequired.tsx b/src/components/Kyc/states/KycActionRequired.tsx index 418f28842..a2ba581f3 100644 --- a/src/components/Kyc/states/KycActionRequired.tsx +++ b/src/components/Kyc/states/KycActionRequired.tsx @@ -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'} diff --git a/src/components/Kyc/states/KycFailed.tsx b/src/components/Kyc/states/KycFailed.tsx index 01166f00c..3c224c18b 100644 --- a/src/components/Kyc/states/KycFailed.tsx +++ b/src/components/Kyc/states/KycFailed.tsx @@ -97,7 +97,7 @@ export const KycFailed = ({ variant="purple" className="w-full" shadowSize="4" - onClick={onRetry} + onClick={() => onRetry()} disabled={isLoading} > {isLoading ? 'Loading...' : 'Retry verification'} diff --git a/src/components/Kyc/states/KycNotStarted.tsx b/src/components/Kyc/states/KycNotStarted.tsx index cb649ade9..e55149ff0 100644 --- a/src/components/Kyc/states/KycNotStarted.tsx +++ b/src/components/Kyc/states/KycNotStarted.tsx @@ -15,7 +15,7 @@ export const KycNotStarted = ({ onResume, isLoading }: { onResume: () => void; i payments." /> - diff --git a/src/components/Kyc/states/KycRequiresDocuments.tsx b/src/components/Kyc/states/KycRequiresDocuments.tsx index c9a751615..5355434f6 100644 --- a/src/components/Kyc/states/KycRequiresDocuments.tsx +++ b/src/components/Kyc/states/KycRequiresDocuments.tsx @@ -41,7 +41,13 @@ export const KycRequiresDocuments = ({ )} - diff --git a/src/components/Kyc/states/__tests__/KycNotStarted.test.tsx b/src/components/Kyc/states/__tests__/KycNotStarted.test.tsx new file mode 100644 index 000000000..ea8eba9b5 --- /dev/null +++ b/src/components/Kyc/states/__tests__/KycNotStarted.test.tsx @@ -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: () =>
, +})) + +jest.mock('../../RejectLabelsList', () => ({ + RejectLabelsList: () =>
, +})) + +jest.mock('../../KycFailedContent', () => ({ + KycFailedContent: () =>
, +})) + +jest.mock('../../CountryRegionRow', () => ({ + CountryRegionRow: () =>
, +})) + +jest.mock('@/components/Payment/PaymentInfoRow', () => ({ + PaymentInfoRow: () =>
, +})) + +jest.mock('@/components/Global/Card', () => ({ + __esModule: true, + default: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +jest.mock('@/components/Global/InfoCard', () => ({ + __esModule: true, + default: ({ description }: { description: string }) =>
{description}
, +})) + +jest.mock('@/context/ModalsContext', () => ({ + useModalsContext: () => ({ setIsSupportModalOpen: jest.fn() }), +})) + +describe('KycNotStarted', () => { + it('does not pass the click event to onResume', () => { + const onResume = jest.fn() + render() + + 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() + + 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() + + 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() + + fireEvent.click(screen.getByText('Submit documents')) + + expect(onSubmitDocuments).toHaveBeenCalledTimes(1) + expect(onSubmitDocuments).toHaveBeenCalledWith() + }) +}) diff --git a/src/hooks/__tests__/useBridgeTransferReadiness.test.ts b/src/hooks/__tests__/useBridgeTransferReadiness.test.ts index fad5663e6..36e53637f 100644 --- a/src/hooks/__tests__/useBridgeTransferReadiness.test.ts +++ b/src/hooks/__tests__/useBridgeTransferReadiness.test.ts @@ -136,6 +136,24 @@ 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', () => { @@ -143,6 +161,7 @@ describe('getKycModalVariant', () => { 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') }) @@ -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() }) diff --git a/src/hooks/useBridgeTransferReadiness.ts b/src/hooks/useBridgeTransferReadiness.ts index 8bed58d17..a388da9af 100644 --- a/src/hooks/useBridgeTransferReadiness.ts +++ b/src/hooks/useBridgeTransferReadiness.ts @@ -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' } @@ -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() @@ -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 && @@ -52,7 +60,7 @@ export function useBridgeTransferReadiness() { return { type: 'needs_enrollment' } } - // 5. ready + // 6. ready return { type: 'ready' } }, [ needsBridgeTos, @@ -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 }