diff --git a/src/components/Home/KycCompletedModal/index.tsx b/src/components/Home/KycCompletedModal/index.tsx index 58181b0e9..4f28c017b 100644 --- a/src/components/Home/KycCompletedModal/index.tsx +++ b/src/components/Home/KycCompletedModal/index.tsx @@ -4,12 +4,12 @@ import ActionModal from '@/components/Global/ActionModal' import type { IconName } from '@/components/Global/Icons/Icon' import InfoCard from '@/components/Global/InfoCard' import { useAuth } from '@/context/authContext' -import { MantecaKycStatus } from '@/interfaces' import { countryData, MantecaSupportedExchanges, type CountryData } from '@/components/AddMoney/consts' import useUnifiedKycStatus from '@/hooks/useUnifiedKycStatus' import { useIdentityVerification } from '@/hooks/useIdentityVerification' import posthog from 'posthog-js' import { ANALYTICS_EVENTS, MODAL_TYPES } from '@/constants/analytics.consts' +import { isKycStatusApproved } from '@/constants/kyc.consts' const KycCompletedModal = ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => { const { user } = useAuth() @@ -28,13 +28,13 @@ const KycCompletedModal = ({ isOpen, onClose }: { isOpen: boolean; onClose: () = const { getVerificationUnlockItems } = useIdentityVerification() const kycApprovalType = useMemo(() => { + if (isBridgeApproved && isMantecaApproved) return 'all' + if (isMantecaApproved) return 'manteca' if (isSumsubApproved) { if (sumsubVerificationRegionIntent === 'LATAM') return 'manteca' return 'bridge' } - if (isBridgeApproved && isMantecaApproved) return 'all' if (isBridgeApproved) return 'bridge' - if (isMantecaApproved) return 'manteca' return 'none' }, [isBridgeApproved, isMantecaApproved, isSumsubApproved, sumsubVerificationRegionIntent]) @@ -51,9 +51,9 @@ const KycCompletedModal = ({ isOpen, onClose }: { isOpen: boolean; onClose: () = // get the manteca approved country user?.user.kycVerifications?.forEach((v) => { if ( - v.provider === 'MANTECA' && + (v.provider === 'MANTECA' || v.provider === 'SUMSUB') && supportedCountries.includes((v.mantecaGeo || '').toUpperCase()) && - v.status === MantecaKycStatus.ACTIVE + isKycStatusApproved(v.status) ) { approvedCountry = v.mantecaGeo } diff --git a/src/hooks/__tests__/kyc-withdrawal-gate.test.tsx b/src/hooks/__tests__/kyc-withdrawal-gate.test.tsx new file mode 100644 index 000000000..5e20839fa --- /dev/null +++ b/src/hooks/__tests__/kyc-withdrawal-gate.test.tsx @@ -0,0 +1,127 @@ +import { renderHook } from '@testing-library/react' +import { useIdentityVerification } from '../useIdentityVerification' +import useUnifiedKycStatus from '../useUnifiedKycStatus' + +jest.mock('@/assets', () => ({ + EUROPE_GLOBE_ICON: 'europe', + LATAM_GLOBE_ICON: 'latam', + NORTH_AMERICA_GLOBE_ICON: 'north-america', + REST_OF_WORLD_GLOBE_ICON: 'rest-of-world', +})) + +jest.mock('@/components/AddMoney/consts', () => ({ + BRIDGE_ALPHA3_TO_ALPHA2: { USA: 'US' }, + MantecaSupportedExchanges: { AR: 'ARGENTINA', BR: 'BRAZIL' }, + countryData: [ + { id: 'AR', type: 'country', title: 'Argentina', path: 'argentina', iso2: 'AR' }, + { id: 'US', type: 'country', title: 'United States', path: 'united-states', iso2: 'US' }, + ], +})) + +jest.mock('@/context/authContext', () => ({ + useAuth: jest.fn(), +})) + +import { useAuth } from '@/context/authContext' + +const mockUseAuth = useAuth as jest.MockedFunction + +const enabledMantecaArRail = { + id: 'rail-1', + railId: 'rail-def-1', + status: 'ENABLED', + rail: { + id: 'rail-def-1', + provider: { code: 'MANTECA', name: 'Manteca' }, + method: { code: 'BANK_TRANSFER_AR', name: 'Bank Transfer AR', country: 'AR', currency: 'ARS' }, + }, +} + +function setUser(authUser: Record) { + mockUseAuth.mockReturnValue({ + user: authUser, + } as unknown as ReturnType) +} + +describe('kyc withdrawal gating', () => { + afterEach(() => jest.resetAllMocks()) + + it('treats migrated SUMSUB ACTIVE Manteca rows as approved', () => { + setUser({ + user: { + bridgeKycStatus: null, + kycVerifications: [ + { + provider: 'SUMSUB', + mantecaGeo: 'AR', + status: 'ACTIVE', + updatedAt: '2026-03-26T23:02:30.330Z', + }, + ], + }, + rails: [], + }) + + const { result } = renderHook(() => useUnifiedKycStatus()) + + expect(result.current.isSumsubApproved).toBe(true) + expect(result.current.isMantecaApproved).toBe(true) + expect(result.current.isKycApproved).toBe(true) + }) + + it('allows Argentina Manteca withdrawal when migrated KYC is country-scoped', () => { + setUser({ + user: { + bridgeKycStatus: null, + kycVerifications: [ + { + provider: 'SUMSUB', + mantecaGeo: 'AR', + status: 'ACTIVE', + updatedAt: '2026-03-17T14:47:34.702Z', + }, + ], + }, + rails: [], + }) + + const { result } = renderHook(() => useIdentityVerification()) + + expect(result.current.isVerifiedForCountry('AR')).toBe(true) + }) + + it('allows Argentina Manteca withdrawal for a normal active Manteca row', () => { + setUser({ + user: { + bridgeKycStatus: null, + kycVerifications: [ + { + provider: 'MANTECA', + mantecaGeo: 'AR', + status: 'ACTIVE', + updatedAt: '2025-10-30T01:12:06.099Z', + }, + ], + }, + rails: [], + }) + + const { result } = renderHook(() => useIdentityVerification()) + + expect(result.current.isVerifiedForCountry('AR')).toBe(true) + }) + + it('does not allow Argentina Manteca withdrawal from an enabled rail alone', () => { + setUser({ + user: { + bridgeKycStatus: null, + kycVerifications: [], + }, + rails: [enabledMantecaArRail], + }) + + const { result } = renderHook(() => useIdentityVerification()) + + expect(result.current.isVerifiedForCountry('AR')).toBe(false) + }) +}) diff --git a/src/hooks/useIdentityVerification.tsx b/src/hooks/useIdentityVerification.tsx index bfb19bb8f..5dd16e369 100644 --- a/src/hooks/useIdentityVerification.tsx +++ b/src/hooks/useIdentityVerification.tsx @@ -4,11 +4,11 @@ import useKycStatus from './useKycStatus' import useUnifiedKycStatus from './useUnifiedKycStatus' import { useMemo, useCallback } from 'react' import { useAuth } from '@/context/authContext' -import { MantecaKycStatus } from '@/interfaces' import { BRIDGE_ALPHA3_TO_ALPHA2, MantecaSupportedExchanges, countryData } from '@/components/AddMoney/consts' import { getFlagUrl } from '@/constants/countryCurrencyMapping' import { type KYCRegionIntent } from '@/app/actions/types/sumsub.types' import React from 'react' +import { isKycStatusApproved } from '@/constants/kyc.consts' /** Represents a geographic region with its display information */ export type Region = { @@ -134,13 +134,12 @@ export const useIdentityVerification = () => { (code: string) => { const upper = code.toUpperCase() - // Check if user has active Manteca verification for this specific country const mantecaActive = user?.user.kycVerifications?.some( (v) => - v.provider === 'MANTECA' && + (v.provider === 'MANTECA' || v.provider === 'SUMSUB') && (v.mantecaGeo || '').toUpperCase() === upper && - v.status === MantecaKycStatus.ACTIVE + isKycStatusApproved(v.status) ) ?? false // Manteca countries need country-specific verification, others just need Bridge KYC diff --git a/src/hooks/useUnifiedKycStatus.ts b/src/hooks/useUnifiedKycStatus.ts index bc5b410ae..5b099649c 100644 --- a/src/hooks/useUnifiedKycStatus.ts +++ b/src/hooks/useUnifiedKycStatus.ts @@ -1,10 +1,9 @@ 'use client' import { useAuth } from '@/context/authContext' -import { MantecaKycStatus } from '@/interfaces' import { useMemo } from 'react' import { type SumsubKycStatus } from '@/app/actions/types/sumsub.types' -import { isSumsubStatusInProgress } from '@/constants/kyc.consts' +import { isKycStatusApproved, isSumsubStatusInProgress } from '@/constants/kyc.consts' /** * single source of truth for kyc status across all providers (bridge, manteca, sumsub). @@ -18,7 +17,9 @@ export default function useUnifiedKycStatus() { const isMantecaApproved = useMemo( () => user?.user.kycVerifications?.some( - (v) => v.provider === 'MANTECA' && v.status === MantecaKycStatus.ACTIVE + (v) => + (v.provider === 'MANTECA' || (v.provider === 'SUMSUB' && !!v.mantecaGeo)) && + isKycStatusApproved(v.status) ) ?? false, [user] ) @@ -32,7 +33,7 @@ export default function useUnifiedKycStatus() { [user] ) - const isSumsubApproved = useMemo(() => sumsubVerification?.status === 'APPROVED', [sumsubVerification]) + const isSumsubApproved = useMemo(() => isKycStatusApproved(sumsubVerification?.status), [sumsubVerification]) const sumsubStatus = useMemo(() => (sumsubVerification?.status as SumsubKycStatus) ?? null, [sumsubVerification])