From 58df746160fb9c3e1fc57c150133458303bbb756 Mon Sep 17 00:00:00 2001
From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com>
Date: Wed, 25 Feb 2026 12:26:19 +0530
Subject: [PATCH 01/11] refactor: unify all KYC flows through Sumsub via
useMultiPhaseKycFlow
- Delete Bridge iframe integration: useBridgeKycFlow, InitiateBridgeKYCModal
- Delete Manteca widget integration: useMantecaKycFlow, InitiateMantecaKYCModal
- Extract shared useMultiPhaseKycFlow hook + SumsubKycModals component
- Replace all KYC entry points with inline Sumsub flow:
- Bridge bank flows use regionIntent: 'STANDARD'
- Manteca flows use regionIntent: 'LATAM'
- Add polling fallback in useSumsubKycFlow for missed WebSocket events
- Relocate KycHistoryEntry type + isKycStatusItem guard to KycStatusItem
- Net reduction: ~970 lines
Co-Authored-By: Claude Opus 4.6
---
.../add-money/[country]/bank/page.tsx | 26 +-
src/app/(mobile-ui)/history/page.tsx | 2 +-
src/app/(mobile-ui)/withdraw/manteca/page.tsx | 62 +---
.../AddMoney/components/MantecaAddMoney.tsx | 74 ++---
.../AddWithdraw/AddWithdrawCountriesList.tsx | 35 +--
.../Claim/Link/MantecaFlowManager.tsx | 49 +--
.../Claim/Link/views/BankFlowManager.view.tsx | 37 +--
src/components/Home/HomeHistory.tsx | 3 +-
src/components/Kyc/InitiateBridgeKYCModal.tsx | 96 ------
.../Kyc/InitiateMantecaKYCModal.tsx | 164 ----------
src/components/Kyc/KycStatusDrawer.tsx | 81 +----
src/components/Kyc/KycStatusItem.tsx | 13 +
src/components/Kyc/SumsubKycFlow.tsx | 276 +---------------
src/components/Kyc/SumsubKycModals.tsx | 47 +++
.../views/RegionsVerification.view.tsx | 68 +---
src/hooks/useBridgeKycFlow.ts | 188 -----------
src/hooks/useMantecaKycFlow.ts | 110 -------
src/hooks/useMultiPhaseKycFlow.ts | 295 ++++++++++++++++++
src/hooks/useSumsubKycFlow.ts | 24 ++
19 files changed, 506 insertions(+), 1144 deletions(-)
delete mode 100644 src/components/Kyc/InitiateBridgeKYCModal.tsx
delete mode 100644 src/components/Kyc/InitiateMantecaKYCModal.tsx
create mode 100644 src/components/Kyc/SumsubKycModals.tsx
delete mode 100644 src/hooks/useBridgeKycFlow.ts
delete mode 100644 src/hooks/useMantecaKycFlow.ts
create mode 100644 src/hooks/useMultiPhaseKycFlow.ts
diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
index fb8fe265d..fb2fb5764 100644
--- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
+++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
@@ -24,13 +24,14 @@ import { updateUserById } from '@/app/actions/users'
import AddMoneyBankDetails from '@/components/AddMoney/components/AddMoneyBankDetails'
import { getCurrencyConfig, getCurrencySymbol, getMinimumAmount } from '@/utils/bridge.utils'
import { OnrampConfirmationModal } from '@/components/AddMoney/components/OnrampConfirmationModal'
-import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal'
import InfoCard from '@/components/Global/InfoCard'
import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs'
import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation'
import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard'
import { getLimitsWarningCardProps } from '@/features/limits/utils'
import { useExchangeRate } from '@/hooks/useExchangeRate'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
// Step type for URL state
type BridgeBankStep = 'inputAmount' | 'kyc' | 'collectUserDetails' | 'showDetails'
@@ -68,6 +69,18 @@ export default function OnrampBankPage() {
const { user, fetchUser } = useAuth()
const { createOnramp, isLoading: isCreatingOnramp, error: onrampError } = useCreateOnramp()
+ // inline sumsub kyc flow for bridge bank onramp
+ const sumsubFlow = useMultiPhaseKycFlow({
+ regionIntent: 'STANDARD',
+ onKycSuccess: () => {
+ setIsKycModalOpen(false)
+ setUrlState({ step: 'inputAmount' })
+ },
+ onManualClose: () => {
+ setIsKycModalOpen(false)
+ },
+ })
+
const selectedCountryPath = params.country as string
const selectedCountry = useMemo(() => {
@@ -312,7 +325,7 @@ export default function OnrampBankPage() {
useEffect(() => {
if (urlState.step === 'kyc') {
- setIsKycModalOpen(true)
+ sumsubFlow.handleInitiateKyc()
}
}, [urlState.step])
@@ -374,13 +387,8 @@ export default function OnrampBankPage() {
if (urlState.step === 'kyc') {
return (
- router.push(`/add-money/${selectedCountry.path}`)}
- flow="add"
- />
+ setUrlState({ step: 'collectUserDetails' })} />
+
)
}
diff --git a/src/app/(mobile-ui)/history/page.tsx b/src/app/(mobile-ui)/history/page.tsx
index 2ace526be..e9d9bae9a 100644
--- a/src/app/(mobile-ui)/history/page.tsx
+++ b/src/app/(mobile-ui)/history/page.tsx
@@ -12,7 +12,7 @@ import { useTransactionHistory } from '@/hooks/useTransactionHistory'
import { useUserStore } from '@/redux/hooks'
import { formatGroupHeaderDate, getDateGroup, getDateGroupKey } from '@/utils/dateGrouping.utils'
import * as Sentry from '@sentry/nextjs'
-import { isKycStatusItem } from '@/hooks/useBridgeKycFlow'
+import { isKycStatusItem } from '@/components/Kyc/KycStatusItem'
import { useAuth } from '@/context/authContext'
import { BadgeStatusItem } from '@/components/Badges/BadgeStatusItem'
import { isBadgeHistoryItem } from '@/components/Badges/badge.types'
diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx
index 8371f74f8..af29ed048 100644
--- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx
+++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx
@@ -23,16 +23,15 @@ import AmountInput from '@/components/Global/AmountInput'
import { formatUnits, parseUnits } from 'viem'
import type { TransactionReceipt, Hash } from 'viem'
import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
-import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow'
-import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal'
import { useAuth } from '@/context/authContext'
-import { useWebSocket } from '@/hooks/useWebSocket'
import { useModalsContext } from '@/context/ModalsContext'
import Select from '@/components/Global/Select'
import { SoundPlayer } from '@/components/Global/SoundPlayer'
import { useQueryClient } from '@tanstack/react-query'
import { captureException } from '@sentry/nextjs'
import useKycStatus from '@/hooks/useKycStatus'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions'
import { PointsAction } from '@/services/services.types'
import { usePointsConfetti } from '@/hooks/usePointsConfetti'
@@ -68,7 +67,6 @@ export default function MantecaWithdrawFlow() {
const [selectedBank, setSelectedBank] = useState(null)
const [accountType, setAccountType] = useState(null)
const [errorMessage, setErrorMessage] = useState(null)
- const [isKycModalOpen, setIsKycModalOpen] = useState(false)
const [isDestinationAddressValid, setIsDestinationAddressValid] = useState(false)
const [isDestinationAddressChanging, setIsDestinationAddressChanging] = useState(false)
// price lock state - holds the locked price from /withdraw/init
@@ -78,11 +76,16 @@ export default function MantecaWithdrawFlow() {
const { sendMoney, balance } = useWallet()
const { signTransferUserOp } = useSignUserOp()
const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext)
- const { user, fetchUser } = useAuth()
+ const { user } = useAuth()
const { setIsSupportModalOpen } = useModalsContext()
const queryClient = useQueryClient()
- const { isUserBridgeKycApproved } = useKycStatus()
+ const { isUserMantecaKycApproved } = useKycStatus()
const { hasPendingTransactions } = usePendingTransactions()
+
+ // inline sumsub kyc flow for manteca users who need LATAM verification
+ const sumsubFlow = useMultiPhaseKycFlow({
+ regionIntent: 'LATAM',
+ })
// Get method and country from URL parameters
const selectedMethodType = searchParams.get('method') // mercadopago, pix, bank-transfer, etc.
const countryFromUrl = searchParams.get('country') // argentina, brazil, etc.
@@ -106,9 +109,6 @@ export default function MantecaWithdrawFlow() {
isLoading: isCurrencyLoading,
} = useCurrency(selectedCountry?.currency!)
- // Initialize KYC flow hook
- const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry })
-
// validates withdrawal against user's limits
// currency comes from country config - hook normalizes it internally
const limitsValidation = useLimitsValidation({
@@ -117,19 +117,6 @@ export default function MantecaWithdrawFlow() {
currency: selectedCountry?.currency,
})
- // WebSocket listener for KYC status updates
- useWebSocket({
- username: user?.user.username ?? undefined,
- autoConnect: !!user?.user.username,
- onMantecaKycStatusUpdate: (newStatus) => {
- if (newStatus === 'ACTIVE' || newStatus === 'WIDGET_FINISHED') {
- fetchUser()
- setIsKycModalOpen(false)
- setStep('review') // Proceed to review after successful KYC
- }
- },
- })
-
// Get country flag code
const countryFlagCode = useMemo(() => {
return selectedCountry?.iso2?.toLowerCase()
@@ -200,14 +187,8 @@ export default function MantecaWithdrawFlow() {
}
setErrorMessage(null)
- // check if we still need to determine KYC status
- if (isMantecaKycRequired === null) {
- return
- }
-
- // check KYC status before proceeding to review
- if (isMantecaKycRequired === true) {
- setIsKycModalOpen(true)
+ if (!isUserMantecaKycApproved) {
+ await sumsubFlow.handleInitiateKyc()
return
}
@@ -250,8 +231,9 @@ export default function MantecaWithdrawFlow() {
usdAmount,
currencyCode,
currencyAmount,
- isMantecaKycRequired,
+ isUserMantecaKycApproved,
isLockingPrice,
+ sumsubFlow.handleInitiateKyc,
])
const handleWithdraw = async () => {
@@ -342,7 +324,6 @@ export default function MantecaWithdrawFlow() {
setSelectedBank(null)
setAccountType(null)
setErrorMessage(null)
- setIsKycModalOpen(false)
setIsDestinationAddressValid(false)
setIsDestinationAddressChanging(false)
setBalanceErrorMessage(null)
@@ -468,6 +449,7 @@ export default function MantecaWithdrawFlow() {
}
return (
+
{
@@ -650,22 +632,6 @@ export default function MantecaWithdrawFlow() {
{errorMessage && }
- {/* KYC Modal */}
- {isKycModalOpen && selectedCountry && (
- setIsKycModalOpen(false)}
- onManualClose={() => setIsKycModalOpen(false)}
- onKycSuccess={() => {
- setIsKycModalOpen(false)
- fetchUser()
- setStep('review')
- }}
- selectedCountry={selectedCountry}
- />
- )}
)}
diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx
index f7e902f18..62dbc1598 100644
--- a/src/components/AddMoney/components/MantecaAddMoney.tsx
+++ b/src/components/AddMoney/components/MantecaAddMoney.tsx
@@ -2,18 +2,17 @@
import { type FC, useEffect, useMemo, useState, useCallback } from 'react'
import MantecaDepositShareDetails from '@/components/AddMoney/components/MantecaDepositShareDetails'
import InputAmountStep from '@/components/AddMoney/components/InputAmountStep'
-import { useParams, useRouter } from 'next/navigation'
+import { useParams } from 'next/navigation'
import { type CountryData, countryData } from '@/components/AddMoney/consts'
import { type MantecaDepositResponseData } from '@/types/manteca.types'
-import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal'
-import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow'
import { useCurrency } from '@/hooks/useCurrency'
import { useAuth } from '@/context/authContext'
-import { useWebSocket } from '@/hooks/useWebSocket'
import { mantecaApi } from '@/services/manteca'
import { parseUnits } from 'viem'
import { useQueryClient } from '@tanstack/react-query'
import useKycStatus from '@/hooks/useKycStatus'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
import { MIN_MANTECA_DEPOSIT_AMOUNT } from '@/constants/payment.consts'
import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts'
import { TRANSACTIONS } from '@/constants/query.consts'
@@ -28,7 +27,6 @@ type CurrencyDenomination = 'USD' | 'ARS' | 'BRL' | 'MXN' | 'EUR'
const MantecaAddMoney: FC = () => {
const params = useParams()
- const router = useRouter()
const queryClient = useQueryClient()
// URL state - persisted in query params
@@ -57,16 +55,19 @@ const MantecaAddMoney: FC = () => {
const [isCreatingDeposit, setIsCreatingDeposit] = useState(false)
const [error, setError] = useState(null)
const [depositDetails, setDepositDetails] = useState()
- const [isKycModalOpen, setIsKycModalOpen] = useState(false)
const selectedCountryPath = params.country as string
const selectedCountry = useMemo(() => {
return countryData.find((country) => country.type === 'country' && country.path === selectedCountryPath)
}, [selectedCountryPath])
- const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry as CountryData })
- const { isUserBridgeKycApproved } = useKycStatus()
+ const { isUserMantecaKycApproved } = useKycStatus()
const currencyData = useCurrency(selectedCountry?.currency ?? 'ARS')
- const { user, fetchUser } = useAuth()
+ const { user } = useAuth()
+
+ // inline sumsub kyc flow for manteca users who need LATAM verification
+ const sumsubFlow = useMultiPhaseKycFlow({
+ regionIntent: 'LATAM',
+ })
// validates deposit amount against user's limits
// currency comes from country config - hook normalizes it internally
@@ -76,18 +77,6 @@ const MantecaAddMoney: FC = () => {
currency: selectedCountry?.currency,
})
- useWebSocket({
- username: user?.user.username ?? undefined,
- autoConnect: !!user?.user.username,
- onMantecaKycStatusUpdate: (newStatus) => {
- // listen for manteca kyc status updates, either when the user is approved or when the widget is finished to continue with the flow
- if (newStatus === 'ACTIVE' || newStatus === 'WIDGET_FINISHED') {
- fetchUser()
- setIsKycModalOpen(false)
- }
- },
- })
-
// Validate USD amount (min check only - max is handled by limits validation)
useEffect(() => {
// if user hasn't entered any amount yet, don't show error
@@ -118,13 +107,6 @@ const MantecaAddMoney: FC = () => {
}
}, [step, queryClient])
- const handleKycCancel = () => {
- setIsKycModalOpen(false)
- if (selectedCountry?.path) {
- router.push(`/add-money/${selectedCountry.path}`)
- }
- }
-
// Handle displayed amount change - save to URL
// This is called by AmountInput with the currently DISPLAYED value
const handleDisplayedAmountChange = useCallback(
@@ -156,14 +138,8 @@ const MantecaAddMoney: FC = () => {
if (!selectedCountry?.currency) return
if (isCreatingDeposit) return
- // check if we still need to determine KYC status
- if (isMantecaKycRequired === null) {
- // still loading/determining KYC status, don't proceed yet
- return
- }
-
- if (isMantecaKycRequired === true) {
- setIsKycModalOpen(true)
+ if (!isUserMantecaKycApproved) {
+ await sumsubFlow.handleInitiateKyc()
return
}
@@ -191,14 +167,14 @@ const MantecaAddMoney: FC = () => {
} finally {
setIsCreatingDeposit(false)
}
- }, [currentDenomination, selectedCountry, displayedAmount, isMantecaKycRequired, isCreatingDeposit, setUrlState])
+ }, [currentDenomination, selectedCountry, displayedAmount, isUserMantecaKycApproved, isCreatingDeposit, setUrlState, sumsubFlow.handleInitiateKyc])
- // handle verification modal opening
+ // auto-start KYC if user hasn't completed manteca verification
useEffect(() => {
- if (isMantecaKycRequired) {
- setIsKycModalOpen(true)
+ if (!isUserMantecaKycApproved) {
+ sumsubFlow.handleInitiateKyc()
}
- }, [isMantecaKycRequired])
+ }, [isUserMantecaKycApproved]) // eslint-disable-line react-hooks/exhaustive-deps
// Redirect to inputAmount if depositDetails is accessed without required data (deep link / back navigation)
useEffect(() => {
@@ -212,6 +188,7 @@ const MantecaAddMoney: FC = () => {
if (step === 'inputAmount') {
return (
<>
+
{
limitsValidation={limitsValidation}
limitsCurrency={limitsValidation.currency}
/>
- {isKycModalOpen && (
- {
- // close the modal and let the user continue with amount input
- setIsKycModalOpen(false)
- fetchUser()
- }}
- selectedCountry={selectedCountry}
- />
- )}
>
)
}
diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
index de668ef0d..3707f0b9e 100644
--- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
+++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx
@@ -22,11 +22,12 @@ import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType'
import { useAppDispatch } from '@/redux/hooks'
import { bankFormActions } from '@/redux/slices/bank-form-slice'
-import { InitiateBridgeKYCModal } from '../Kyc/InitiateBridgeKYCModal'
import useKycStatus from '@/hooks/useKycStatus'
import KycVerifiedOrReviewModal from '../Global/KycVerifiedOrReviewModal'
import { ActionListCard } from '@/components/ActionListCard'
import TokenAndNetworkConfirmationModal from '../Global/TokenAndNetworkConfirmationModal'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
interface AddWithdrawCountriesListProps {
flow: 'add' | 'withdraw'
@@ -48,6 +49,16 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
const { setSelectedBankAccount, amountToWithdraw, setSelectedMethod, setAmountToWithdraw } = useWithdrawFlow()
const dispatch = useAppDispatch()
+ // inline sumsub kyc flow for bridge bank users who need verification
+ const sumsubFlow = useMultiPhaseKycFlow({
+ regionIntent: 'STANDARD',
+ onKycSuccess: () => {
+ setIsKycModalOpen(false)
+ setView('form')
+ },
+ onManualClose: () => setIsKycModalOpen(false),
+ })
+
// component level states
const [view, setView] = useState<'list' | 'form'>(flow === 'withdraw' && amountToWithdraw ? 'form' : 'list')
const [isKycModalOpen, setIsKycModalOpen] = useState(false)
@@ -168,20 +179,12 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
}
}
- setIsKycModalOpen(true)
+ await sumsubFlow.handleInitiateKyc()
}
return {}
}
- const handleKycSuccess = () => {
- // only transition to form if this component initiated the KYC modal
- if (isKycModalOpen) {
- setIsKycModalOpen(false)
- setView('form')
- }
- }
-
const handleWithdrawMethodClick = (method: SpecificPaymentMethod) => {
// preserve method param only if coming from bank send flow (not crypto)
const methodQueryParam = isBankFromSend ? `?method=${methodParam}` : ''
@@ -312,11 +315,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
initialData={{}}
error={null}
/>
- setIsKycModalOpen(false)}
- onKycSuccess={handleKycSuccess}
- />
+
)
}
@@ -431,11 +430,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
isKycApprovedModalOpen={showKycStatusModal}
onClose={() => setShowKycStatusModal(false)}
/>
- setIsKycModalOpen(false)}
- onKycSuccess={handleKycSuccess}
- />
+
)
}
diff --git a/src/components/Claim/Link/MantecaFlowManager.tsx b/src/components/Claim/Link/MantecaFlowManager.tsx
index b27e54a4d..cb3e072a3 100644
--- a/src/components/Claim/Link/MantecaFlowManager.tsx
+++ b/src/components/Claim/Link/MantecaFlowManager.tsx
@@ -12,9 +12,8 @@ import MantecaReviewStep from './views/MantecaReviewStep'
import { Button } from '@/components/0_Bruddle/Button'
import { useRouter } from 'next/navigation'
import useKycStatus from '@/hooks/useKycStatus'
-import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal'
-import { useAuth } from '@/context/authContext'
-import { type CountryData } from '@/components/AddMoney/consts'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
interface MantecaFlowManagerProps {
claimLinkData: ClaimLinkData
@@ -27,35 +26,24 @@ const MantecaFlowManager: FC = ({ claimLinkData, amount
const [currentStep, setCurrentStep] = useState(MercadoPagoStep.DETAILS)
const router = useRouter()
const [destinationAddress, setDestinationAddress] = useState('')
- const [isKYCModalOpen, setIsKYCModalOpen] = useState(false)
- const argentinaCountryData = {
- id: 'AR',
- type: 'country',
- title: 'Argentina',
- currency: 'ARS',
- path: 'argentina',
- iso2: 'AR',
- iso3: 'ARG',
- } as CountryData
+ const { isUserMantecaKycApproved } = useKycStatus()
- const { isUserMantecaKycApproved, isUserBridgeKycApproved } = useKycStatus()
- const { fetchUser } = useAuth()
+ // inline sumsub kyc flow for manteca users who need LATAM verification
+ const sumsubFlow = useMultiPhaseKycFlow({
+ regionIntent: 'LATAM',
+ })
const isSuccess = currentStep === MercadoPagoStep.SUCCESS
const selectedCurrency = selectedCountry?.currency || 'ARS'
const regionalMethodLogo = regionalMethodType === 'mercadopago' ? MERCADO_PAGO : PIX
const logo = selectedCountry?.id ? undefined : regionalMethodLogo
- const handleKycCancel = () => {
- setIsKYCModalOpen(false)
- onPrev()
- }
-
+ // auto-start KYC if user hasn't completed manteca verification
useEffect(() => {
if (!isUserMantecaKycApproved) {
- setIsKYCModalOpen(true)
+ sumsubFlow.handleInitiateKyc()
}
- }, [isUserMantecaKycApproved])
+ }, [isUserMantecaKycApproved]) // eslint-disable-line react-hooks/exhaustive-deps
const renderStepDetails = () => {
if (currentStep === MercadoPagoStep.DETAILS) {
@@ -125,23 +113,8 @@ const MantecaFlowManager: FC = ({ claimLinkData, amount
/>
{renderStepDetails()}
-
- {isKYCModalOpen && (
- {
- // close the modal and let the user continue with amount input
- setIsKYCModalOpen(false)
- fetchUser()
- }}
- selectedCountry={selectedCountry || argentinaCountryData}
- />
- )}
+
)
}
diff --git a/src/components/Claim/Link/views/BankFlowManager.view.tsx b/src/components/Claim/Link/views/BankFlowManager.view.tsx
index 2e6519249..50de618d1 100644
--- a/src/components/Claim/Link/views/BankFlowManager.view.tsx
+++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx
@@ -31,8 +31,9 @@ import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
import { useAppDispatch } from '@/redux/hooks'
import { bankFormActions } from '@/redux/slices/bank-form-slice'
import { sendLinksApi } from '@/services/sendLinks'
-import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal'
import { useSearchParams } from 'next/navigation'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
type BankAccountWithId = IBankAccountDetails &
(
@@ -76,6 +77,19 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
const { claimLink } = useClaimLink()
const dispatch = useAppDispatch()
+ // inline sumsub kyc flow for users who need verification
+ const sumsubFlow = useMultiPhaseKycFlow({
+ regionIntent: 'STANDARD',
+ onKycSuccess: async () => {
+ if (justCompletedKyc) return
+ setIsKycModalOpen(false)
+ await fetchUser()
+ setJustCompletedKyc(true)
+ setClaimBankFlowStep(ClaimBankFlowStep.BankDetailsForm)
+ },
+ onManualClose: () => setIsKycModalOpen(false),
+ })
+
// local states for this component
const [localBankDetails, setLocalBankDetails] = useState(null)
const [receiverFullName, setReceiverFullName] = useState('')
@@ -257,7 +271,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
}
}
- setIsKycModalOpen(true)
+ await sumsubFlow.handleInitiateKyc()
return {}
}
@@ -391,19 +405,6 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
return {}
}
- /**
- * @name handleKycSuccess
- * @description callback for when the KYC process is successfully completed.
- */
- const handleKycSuccess = useCallback(async () => {
- if (justCompletedKyc) return
-
- setIsKycModalOpen(false)
- await fetchUser()
- setJustCompletedKyc(true)
- setClaimBankFlowStep(ClaimBankFlowStep.BankDetailsForm)
- }, [fetchUser, setClaimBankFlowStep, setIsKycModalOpen, setJustCompletedKyc, justCompletedKyc])
-
// main render logic based on the current flow step
switch (claimBankFlowStep) {
case ClaimBankFlowStep.SavedAccountsList:
@@ -492,11 +493,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
initialData={{}}
error={error}
/>
- setIsKycModalOpen(false)}
- onKycSuccess={handleKycSuccess}
- />
+
)
case ClaimBankFlowStep.BankConfirmClaim:
diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx
index c89f606b1..f0e5893d9 100644
--- a/src/components/Home/HomeHistory.tsx
+++ b/src/components/Home/HomeHistory.tsx
@@ -13,9 +13,8 @@ import { twMerge } from 'tailwind-merge'
import Card from '../Global/Card'
import { type CardPosition, getCardPosition } from '../Global/Card/card.utils'
import EmptyState from '../Global/EmptyStates/EmptyState'
-import { KycStatusItem } from '../Kyc/KycStatusItem'
+import { KycStatusItem, isKycStatusItem, type KycHistoryEntry } from '../Kyc/KycStatusItem'
import { BridgeTosReminder } from '../Kyc/BridgeTosReminder'
-import { isKycStatusItem, type KycHistoryEntry } from '@/hooks/useBridgeKycFlow'
import { useBridgeTosStatus } from '@/hooks/useBridgeTosStatus'
import { useWallet } from '@/hooks/wallet/useWallet'
import { BadgeStatusItem } from '@/components/Badges/BadgeStatusItem'
diff --git a/src/components/Kyc/InitiateBridgeKYCModal.tsx b/src/components/Kyc/InitiateBridgeKYCModal.tsx
deleted file mode 100644
index dfd8b1887..000000000
--- a/src/components/Kyc/InitiateBridgeKYCModal.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import ActionModal from '@/components/Global/ActionModal'
-import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow'
-import IframeWrapper from '@/components/Global/IframeWrapper'
-import { KycVerificationInProgressModal } from './KycVerificationInProgressModal'
-import { type IconName } from '@/components/Global/Icons/Icon'
-import { saveRedirectUrl } from '@/utils/general.utils'
-import useClaimLink from '../Claim/useClaimLink'
-import { useEffect } from 'react'
-
-interface BridgeKycModalFlowProps {
- isOpen: boolean
- onClose: () => void
- onKycSuccess?: () => void
- onManualClose?: () => void
- flow?: 'add' | 'withdraw' | 'request_fulfillment'
-}
-
-export const InitiateBridgeKYCModal = ({
- isOpen,
- onClose,
- onKycSuccess,
- onManualClose,
- flow,
-}: BridgeKycModalFlowProps) => {
- const {
- isLoading,
- error,
- iframeOptions,
- isVerificationProgressModalOpen,
- handleInitiateKyc,
- handleIframeClose,
- closeVerificationProgressModal,
- resetError,
- } = useBridgeKycFlow({ onKycSuccess, flow, onManualClose })
- const { addParamStep } = useClaimLink()
-
- // Reset error when modal opens to ensure clean state
- useEffect(() => {
- if (isOpen) {
- resetError()
- }
- }, [isOpen, resetError])
-
- const handleVerifyClick = async () => {
- // Only add step param for claim flows (not add-money flow which has its own URL state)
- if (flow !== 'add') {
- addParamStep('bank')
- }
- const result = await handleInitiateKyc()
- if (result?.success) {
- saveRedirectUrl()
- onClose()
- }
- }
-
- return (
- <>
-
-
-
- >
- )
-}
diff --git a/src/components/Kyc/InitiateMantecaKYCModal.tsx b/src/components/Kyc/InitiateMantecaKYCModal.tsx
deleted file mode 100644
index 6f547e6f3..000000000
--- a/src/components/Kyc/InitiateMantecaKYCModal.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-'use client'
-
-import ActionModal from '@/components/Global/ActionModal'
-import IframeWrapper from '@/components/Global/IframeWrapper'
-import { type IconName } from '@/components/Global/Icons/Icon'
-import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow'
-import { type CountryData } from '@/components/AddMoney/consts'
-import { Button } from '@/components/0_Bruddle/Button'
-import { PeanutDoesntStoreAnyPersonalInformation } from '@/components/Kyc/PeanutDoesntStoreAnyPersonalInformation'
-import { useEffect } from 'react'
-
-interface Props {
- isOpen: boolean
- onClose: () => void
- onKycSuccess?: () => void
- onManualClose?: () => void
- country: CountryData
- title?: string | React.ReactNode
- description?: string | React.ReactNode
- ctaText?: string
- footer?: React.ReactNode
- autoStartKyc?: boolean
-}
-
-const InitiateMantecaKYCModal = ({
- isOpen,
- onClose,
- onKycSuccess,
- onManualClose,
- country,
- title,
- description,
- ctaText,
- footer,
- autoStartKyc,
-}: Props) => {
- const { isLoading, iframeOptions, openMantecaKyc, handleIframeClose } = useMantecaKycFlow({
- onClose: onManualClose, // any non-success close from iframe is a manual close in case of Manteca KYC
- onSuccess: onKycSuccess,
- onManualClose,
- country,
- })
-
- useEffect(() => {
- const handleMessage = (event: MessageEvent) => {
- if (event.data.source === 'peanut-kyc-success') {
- onKycSuccess?.()
- }
- }
-
- window.addEventListener('message', handleMessage)
-
- return () => {
- window.removeEventListener('message', handleMessage)
- }
- }, [])
-
- useEffect(() => {
- if (autoStartKyc) {
- openMantecaKyc(country)
- }
- }, [autoStartKyc])
-
- const isAutoStarting = autoStartKyc && isLoading
- const displayTitle = isAutoStarting ? 'Starting verification...' : (title ?? 'Verify your identity first')
- const displayDescription = isAutoStarting
- ? 'Please wait while we start your verification...'
- : (description ??
- 'To continue, you need to complete identity verification. This usually takes just a few minutes.')
-
- return (
- <>
- openMantecaKyc(country),
- variant: 'purple',
- disabled: isLoading,
- shadowSize: '4',
- icon: 'check-circle',
- className: 'h-11',
- },
- ]}
- footer={footer}
- />
-
- >
- )
-}
-
-export const MantecaGeoSpecificKycModal = ({
- isUserBridgeKycApproved,
- selectedCountry,
- setIsMantecaModalOpen,
- isMantecaModalOpen,
- onKycSuccess,
- onClose,
- onManualClose,
-}: {
- isUserBridgeKycApproved: boolean
- selectedCountry: { id: string; title: string }
- setIsMantecaModalOpen: (isOpen: boolean) => void
- isMantecaModalOpen: boolean
- onKycSuccess: () => void
- onClose?: () => void
- onManualClose?: () => void
-}) => {
- return (
-
- You're already verified in Europe, USA, and Mexico, but to use features in{' '}
- {selectedCountry.title} you need to complete a separate verification.
Since{' '}
- we don't keep personal data, your previous KYC can't be reused.
-
- ) : (
-
- Verify your identity to start using features like Mercado Pago payments in{' '}
- {selectedCountry.title}.{' '}
-
- )
- }
- footer={
- isUserBridgeKycApproved ? (
-
- ) : (
-
- )
- }
- ctaText="Start Verification"
- isOpen={isMantecaModalOpen}
- onClose={() => {
- setIsMantecaModalOpen(false)
- onClose?.()
- }}
- onKycSuccess={() => {
- setIsMantecaModalOpen(false)
- onKycSuccess?.()
- }}
- onManualClose={() => {
- setIsMantecaModalOpen(false)
- onManualClose?.()
- }}
- country={{ id: selectedCountry.id, title: selectedCountry.title, type: 'country', path: '' }}
- />
- )
-}
diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx
index 07b6b292d..74c21757c 100644
--- a/src/components/Kyc/KycStatusDrawer.tsx
+++ b/src/components/Kyc/KycStatusDrawer.tsx
@@ -3,17 +3,12 @@ import { KycCompleted } from './states/KycCompleted'
import { KycFailed } from './states/KycFailed'
import { KycProcessing } from './states/KycProcessing'
import { KycRequiresDocuments } from './states/KycRequiresDocuments'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
import { Drawer, DrawerContent, DrawerTitle } from '../Global/Drawer'
import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils'
import { type IUserKycVerification } from '@/interfaces'
import { useUserStore } from '@/redux/hooks'
-import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow'
-import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow'
-import { useSumsubKycFlow } from '@/hooks/useSumsubKycFlow'
-import { type CountryData, countryData } from '@/components/AddMoney/consts'
-import IFrameWrapper from '@/components/Global/IframeWrapper'
-import { SumsubKycWrapper } from '@/components/Kyc/SumsubKycWrapper'
-import { KycVerificationInProgressModal } from '@/components/Kyc/KycVerificationInProgressModal'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
import { getKycStatusCategory, isKycStatusNotStarted } from '@/constants/kyc.consts'
import { type KYCRegionIntent } from '@/app/actions/types/sumsub.types'
@@ -38,52 +33,17 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus
verification?.provider === 'SUMSUB' ? verification?.metadata?.regionIntent : undefined
) as KYCRegionIntent | undefined
- const {
- handleInitiateKyc: initiateBridgeKyc,
- iframeOptions: bridgeIframeOptions,
- handleIframeClose: handleBridgeIframeClose,
- isLoading: isBridgeLoading,
- } = useBridgeKycFlow({ onKycSuccess: onClose, onManualClose: onClose })
-
- const country = countryCode ? countryData.find((c) => c.id.toUpperCase() === countryCode.toUpperCase()) : undefined
-
- const {
- openMantecaKyc,
- iframeOptions: mantecaIframeOptions,
- handleIframeClose: handleMantecaIframeClose,
- isLoading: isMantecaLoading,
- } = useMantecaKycFlow({
- onSuccess: onClose,
- onClose: onClose,
+ const sumsubFlow = useMultiPhaseKycFlow({
+ onKycSuccess: onClose,
onManualClose: onClose,
- country: country as CountryData,
+ regionIntent: sumsubRegionIntent,
})
- const {
- handleInitiateKyc: initiateSumsub,
- showWrapper: showSumsubWrapper,
- accessToken: sumsubAccessToken,
- handleSdkComplete: handleSumsubComplete,
- handleClose: handleSumsubClose,
- refreshToken: sumsubRefreshToken,
- isLoading: isSumsubLoading,
- isVerificationProgressModalOpen: isSumsubProgressModalOpen,
- closeVerificationProgressModal: closeSumsubProgressModal,
- error: sumsubError,
- } = useSumsubKycFlow({ onKycSuccess: onClose, onManualClose: onClose, regionIntent: sumsubRegionIntent })
-
+ // all kyc retries now go through sumsub
const onRetry = async () => {
- if (provider === 'SUMSUB') {
- await initiateSumsub()
- } else if (provider === 'MANTECA') {
- await openMantecaKyc(country as CountryData)
- } else {
- await initiateBridgeKyc()
- }
+ await sumsubFlow.handleInitiateKyc()
}
- const isLoadingKyc = isBridgeLoading || isMantecaLoading || isSumsubLoading
-
// check if any bridge rail needs additional documents
const bridgeRailsNeedingDocs = (user?.rails ?? []).filter(
(r) => r.status === 'REQUIRES_EXTRA_INFORMATION' && r.rail.provider.code === 'BRIDGE'
@@ -102,14 +62,11 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus
: []
// count sumsub rejections for failure lockout.
- // counts total REJECTED entries — accurate if backend creates a new row per attempt.
- // if backend updates in-place (single row), this will be 0 or 1 and the lockout
- // won't trigger from count alone (terminal labels and rejectType still work).
const sumsubFailureCount =
user?.user?.kycVerifications?.filter((v) => v.provider === 'SUMSUB' && v.status === 'REJECTED').length ?? 0
const handleSubmitAdditionalDocs = async () => {
- await initiateSumsub(undefined, 'peanut-additional-docs')
+ await sumsubFlow.handleInitiateKyc(undefined, 'peanut-additional-docs')
}
const renderContent = () => {
@@ -119,7 +76,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus
)
}
@@ -145,7 +102,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus
return (
)
@@ -161,7 +118,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus
countryCode={countryCode ?? undefined}
isBridge={isBridgeKyc}
onRetry={onRetry}
- isLoading={isLoadingKyc}
+ isLoading={sumsubFlow.isLoading}
/>
)
default:
@@ -181,22 +138,12 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus
KYC Status
{renderContent()}
- {sumsubError && provider === 'SUMSUB' && (
- {sumsubError}
+ {sumsubFlow.error && provider === 'SUMSUB' && (
+ {sumsubFlow.error}
)}
-
-
-
-
+
>
)
}
diff --git a/src/components/Kyc/KycStatusItem.tsx b/src/components/Kyc/KycStatusItem.tsx
index 7eae92316..bfb6f54a9 100644
--- a/src/components/Kyc/KycStatusItem.tsx
+++ b/src/components/Kyc/KycStatusItem.tsx
@@ -18,6 +18,19 @@ import {
isKycStatusActionRequired,
} from '@/constants/kyc.consts'
+// kyc history entry type + type guard — used by HomeHistory and history page
+export interface KycHistoryEntry {
+ isKyc: true
+ uuid: string
+ timestamp: string
+ verification?: IUserKycVerification
+ bridgeKycStatus?: BridgeKycStatus
+}
+
+export const isKycStatusItem = (entry: object): entry is KycHistoryEntry => {
+ return 'isKyc' in entry && entry.isKyc === true
+}
+
// this component shows the current kyc status and opens a drawer with more details on click
export const KycStatusItem = ({
position = 'first',
diff --git a/src/components/Kyc/SumsubKycFlow.tsx b/src/components/Kyc/SumsubKycFlow.tsx
index e5d01ea85..2c1ab001c 100644
--- a/src/components/Kyc/SumsubKycFlow.tsx
+++ b/src/components/Kyc/SumsubKycFlow.tsx
@@ -1,16 +1,7 @@
-import { useState, useCallback, useEffect, useRef } from 'react'
import { Button, type ButtonProps } from '@/components/0_Bruddle/Button'
-import { SumsubKycWrapper } from '@/components/Kyc/SumsubKycWrapper'
-import { KycVerificationInProgressModal } from '@/components/Kyc/KycVerificationInProgressModal'
-import IframeWrapper from '@/components/Global/IframeWrapper'
-import { useSumsubKycFlow } from '@/hooks/useSumsubKycFlow'
-import { useRailStatusTracking } from '@/hooks/useRailStatusTracking'
-import { useAuth } from '@/context/authContext'
-import { getBridgeTosLink, confirmBridgeTos } from '@/app/actions/users'
+import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals'
+import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow'
import { type KYCRegionIntent } from '@/app/actions/types/sumsub.types'
-import { type KycModalPhase } from '@/interfaces'
-
-const PREPARING_TIMEOUT_MS = 30000
interface SumsubKycFlowProps extends ButtonProps {
onKycSuccess?: () => void
@@ -25,270 +16,17 @@ interface SumsubKycFlowProps extends ButtonProps {
* verifying → preparing → bridge_tos (if applicable) → complete
*/
export const SumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent, ...buttonProps }: SumsubKycFlowProps) => {
- const { fetchUser } = useAuth()
-
- // multi-phase modal state
- const [modalPhase, setModalPhase] = useState('verifying')
- const [forceShowModal, setForceShowModal] = useState(false)
- const [preparingTimedOut, setPreparingTimedOut] = useState(false)
- const preparingTimerRef = useRef(null)
- const isRealtimeFlowRef = useRef(false)
-
- // bridge ToS state
- const [tosLink, setTosLink] = useState(null)
- const [showTosIframe, setShowTosIframe] = useState(false)
- const [tosError, setTosError] = useState(null)
- const [isLoadingTos, setIsLoadingTos] = useState(false)
-
- // ref for closeVerificationProgressModal (avoids circular dep with completeFlow)
- const closeVerificationModalRef = useRef<() => void>(() => {})
-
- // rail tracking
- const { allSettled, needsBridgeTos, startTracking, stopTracking } = useRailStatusTracking()
-
- const clearPreparingTimer = useCallback(() => {
- if (preparingTimerRef.current) {
- clearTimeout(preparingTimerRef.current)
- preparingTimerRef.current = null
- }
- }, [])
-
- // complete the flow — close everything, call original onKycSuccess
- const completeFlow = useCallback(() => {
- isRealtimeFlowRef.current = false
- setForceShowModal(false)
- setModalPhase('verifying')
- setPreparingTimedOut(false)
- setTosLink(null)
- setShowTosIframe(false)
- setTosError(null)
- clearPreparingTimer()
- stopTracking()
- closeVerificationModalRef.current()
- onKycSuccess?.()
- }, [onKycSuccess, clearPreparingTimer, stopTracking])
-
- // called by useSumsubKycFlow when sumsub status transitions to APPROVED
- const handleSumsubApproved = useCallback(async () => {
- // for real-time flow, optimistically show "Identity verified!" while we check rails
- if (isRealtimeFlowRef.current) {
- setModalPhase('preparing')
- setForceShowModal(true)
- }
-
- const updatedUser = await fetchUser()
- const rails = updatedUser?.rails ?? []
-
- const bridgeNeedsTos = rails.some(
- (r) => r.rail.provider.code === 'BRIDGE' && r.status === 'REQUIRES_INFORMATION'
- )
-
- if (bridgeNeedsTos) {
- setModalPhase('bridge_tos')
- setForceShowModal(true)
- clearPreparingTimer()
- return
- }
-
- const anyPending = rails.some((r) => r.status === 'PENDING')
-
- if (anyPending || (rails.length === 0 && isRealtimeFlowRef.current)) {
- // rails still being set up — show preparing and start tracking
- setModalPhase('preparing')
- setForceShowModal(true)
- startTracking()
- return
- }
-
- // all settled — done
- completeFlow()
- }, [fetchUser, startTracking, clearPreparingTimer, completeFlow])
-
- const {
- isLoading,
- error,
- showWrapper,
- accessToken,
- liveKycStatus,
- handleInitiateKyc: originalHandleInitiateKyc,
- handleSdkComplete: originalHandleSdkComplete,
- handleClose,
- refreshToken,
- isVerificationProgressModalOpen,
- closeVerificationProgressModal,
- } = useSumsubKycFlow({ onKycSuccess: handleSumsubApproved, onManualClose, regionIntent })
-
- // keep ref in sync
- useEffect(() => {
- closeVerificationModalRef.current = closeVerificationProgressModal
- }, [closeVerificationProgressModal])
-
- // refresh user store when kyc status transitions to a non-success state
- // so the drawer/status item reads the updated verification record
- useEffect(() => {
- if (liveKycStatus === 'ACTION_REQUIRED' || liveKycStatus === 'REJECTED') {
- fetchUser()
- }
- }, [liveKycStatus, fetchUser])
-
- // wrap handleSdkComplete to track real-time flow
- const handleSdkComplete = useCallback(() => {
- isRealtimeFlowRef.current = true
- originalHandleSdkComplete()
- }, [originalHandleSdkComplete])
-
- // wrap handleInitiateKyc to reset state for new attempts
- const handleInitiateKyc = useCallback(
- async (overrideIntent?: KYCRegionIntent) => {
- setModalPhase('verifying')
- setForceShowModal(false)
- setPreparingTimedOut(false)
- setTosLink(null)
- setShowTosIframe(false)
- setTosError(null)
- isRealtimeFlowRef.current = false
- clearPreparingTimer()
-
- await originalHandleInitiateKyc(overrideIntent)
- },
- [originalHandleInitiateKyc, clearPreparingTimer]
- )
-
- // 30s timeout for preparing phase
- useEffect(() => {
- if (modalPhase === 'preparing' && !preparingTimedOut) {
- clearPreparingTimer()
- preparingTimerRef.current = setTimeout(() => {
- setPreparingTimedOut(true)
- }, PREPARING_TIMEOUT_MS)
- } else {
- clearPreparingTimer()
- }
- }, [modalPhase, preparingTimedOut, clearPreparingTimer])
-
- // phase transitions driven by rail tracking
- useEffect(() => {
- if (modalPhase === 'preparing') {
- if (needsBridgeTos) {
- setModalPhase('bridge_tos')
- clearPreparingTimer()
- } else if (allSettled) {
- setModalPhase('complete')
- clearPreparingTimer()
- stopTracking()
- }
- } else if (modalPhase === 'bridge_tos') {
- // after ToS accepted, rails transition to ENABLED
- if (allSettled && !needsBridgeTos) {
- setModalPhase('complete')
- stopTracking()
- }
- }
- }, [modalPhase, needsBridgeTos, allSettled, clearPreparingTimer, stopTracking])
-
- // handle "Accept Terms" click in bridge_tos phase
- const handleAcceptTerms = useCallback(async () => {
- setIsLoadingTos(true)
- setTosError(null)
-
- try {
- const response = await getBridgeTosLink()
-
- if (response.error || !response.data?.tosLink) {
- setTosError(
- response.error || 'Could not load terms. You can accept them later from your activity feed.'
- )
- return
- }
-
- setTosLink(response.data.tosLink)
- setShowTosIframe(true)
- } catch {
- setTosError('Something went wrong. You can accept terms later from your activity feed.')
- } finally {
- setIsLoadingTos(false)
- }
- }, [])
-
- // handle ToS iframe close
- const handleTosIframeClose = useCallback(
- async (source?: 'manual' | 'completed' | 'tos_accepted') => {
- setShowTosIframe(false)
-
- if (source === 'tos_accepted') {
- // confirm with backend
- const result = await confirmBridgeTos()
-
- if (!result.data?.accepted) {
- // bridge may not have registered acceptance yet — retry after short delay
- await new Promise((resolve) => setTimeout(resolve, 2000))
- const retryResult = await confirmBridgeTos()
- if (!retryResult.data?.accepted) {
- console.warn('[SumsubKycFlow] bridge ToS confirmation failed after retry')
- }
- }
-
- // refetch user — the phase-transition effect will handle moving to 'complete'
- await fetchUser()
- }
- // if manual close, stay on bridge_tos phase (user can try again or skip)
- },
- [fetchUser]
- )
-
- // handle "Skip for now" in bridge_tos phase
- const handleSkipTerms = useCallback(() => {
- completeFlow()
- }, [completeFlow])
-
- // handle modal close (Go to Home, etc.)
- const handleModalClose = useCallback(() => {
- isRealtimeFlowRef.current = false
- setForceShowModal(false)
- clearPreparingTimer()
- stopTracking()
- closeVerificationProgressModal()
- }, [clearPreparingTimer, stopTracking, closeVerificationProgressModal])
-
- // cleanup on unmount
- useEffect(() => {
- return () => {
- clearPreparingTimer()
- stopTracking()
- }
- }, [clearPreparingTimer, stopTracking])
-
- const isModalOpen = isVerificationProgressModalOpen || forceShowModal
+ const flow = useMultiPhaseKycFlow({ onKycSuccess, onManualClose, regionIntent })
return (
<>
-