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 d34df9090..a2cc64934 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 @@ -1113,12 +1113,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 dde34fdfb..5b907f0e4 100644 --- a/src/hooks/__tests__/useBridgeTransferReadiness.test.ts +++ b/src/hooks/__tests__/useBridgeTransferReadiness.test.ts @@ -127,9 +127,21 @@ describe('useBridgeTransferReadiness', () => { expect(result.current.gate.type).toBe('ready') }) - it('does not flag needs_enrollment when sumsub is not approved', () => { + it('needs_kyc when user has not started standard verification', () => { setup({ isSumsubApproved: false, bridgeRailStatus: null }) const { result } = renderHook(() => useBridgeTransferReadiness()) + expect(result.current.gate.type).toBe('needs_kyc') + }) + + it('needs_kyc when standard verification is not approved and bridge rail is pending', () => { + setup({ isSumsubApproved: false, bridgeRailStatus: 'PENDING' }) + const { result } = renderHook(() => useBridgeTransferReadiness()) + expect(result.current.gate.type).toBe('needs_kyc') + }) + + it('ready when standard verification is not approved but bridge rail is enabled', () => { + setup({ isSumsubApproved: false, bridgeRailStatus: 'ENABLED' }) + const { result } = renderHook(() => useBridgeTransferReadiness()) expect(result.current.gate.type).toBe('ready') }) @@ -151,6 +163,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') }) @@ -168,6 +181,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 9fb43822e..e16dfe64b 100644 --- a/src/hooks/useBridgeTransferReadiness.ts +++ b/src/hooks/useBridgeTransferReadiness.ts @@ -4,12 +4,13 @@ import { useMemo } from 'react' import { useBridgeTosStatus } from './useBridgeTosStatus' import useProviderRejectionStatus from './useProviderRejectionStatus' import useKycStatus from './useKycStatus' -import { hasFunctionalRail } from '@/utils/railGate.utils' +import { hasEnabledRail, hasFunctionalRail } from '@/utils/railGate.utils' 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' } @@ -20,8 +21,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, no functional bridge rail yet) - * 5. ready + * 4. needs standard kyc (fresh user) + * 5. needs enrollment (sumsub approved, no functional bridge rail yet) + * 6. ready * * Phase 6 of rail-gating: the bridge-state checks are derived from the * user's Bridge rails (via useBridgeTosStatus / useProviderRejectionStatus). @@ -47,13 +49,19 @@ export function useBridgeTransferReadiness() { return { type: 'fixable_rejection', userMessage: bridgeRejection.userMessage } } - // 4. needs enrollment — sumsub approved but no Bridge rail in a + // 4. fresh user needs standard kyc before creating a transfer. + // an enabled bridge rail still passes for legacy/out-of-band approvals. + if (!isUserSumsubKycApproved && !hasEnabledRail(bridgeRails, 'BRIDGE')) { + return { type: 'needs_kyc' } + } + + // 5. needs enrollment — sumsub approved but no Bridge rail in a // functional or in-progress state (the user has not started Bridge) if (isUserSumsubKycApproved && !hasFunctionalRail(bridgeRails, 'BRIDGE')) { return { type: 'needs_enrollment' } } - // 5. ready + // 6. ready return { type: 'ready' } }, [needsBridgeTos, bridgeRejection, isUserSumsubKycApproved, bridgeRails])