Skip to content
Closed
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
10 changes: 5 additions & 5 deletions src/components/Home/KycCompletedModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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])

Expand All @@ -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
}
Expand Down
127 changes: 127 additions & 0 deletions src/hooks/__tests__/kyc-withdrawal-gate.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof useAuth>

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<string, unknown>) {
mockUseAuth.mockReturnValue({
user: authUser,
} as unknown as ReturnType<typeof useAuth>)
}

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)
})
})
7 changes: 3 additions & 4 deletions src/hooks/useIdentityVerification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/hooks/useUnifiedKycStatus.ts
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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]
)
Expand All @@ -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])

Expand Down
Loading