diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx
index f02007d2a..ff91f1a7b 100644
--- a/src/app/(mobile-ui)/qr-pay/page.tsx
+++ b/src/app/(mobile-ui)/qr-pay/page.tsx
@@ -116,6 +116,17 @@ export default function QRPayPage() {
return null
}
}, [qrType])
+ const targetMantecaCountry = useMemo(() => {
+ switch (qrType) {
+ case EQrType.PIX:
+ return 'BR'
+ case EQrType.MERCADO_PAGO:
+ case EQrType.ARGENTINA_QR3:
+ return 'AR'
+ default:
+ return undefined
+ }
+ }, [qrType])
// Check if this payment provider is under maintenance
const isProviderDisabled = useMemo(() => {
@@ -123,7 +134,7 @@ export default function QRPayPage() {
}, [paymentProcessor])
const { shouldBlockPay, kycGateState } = useQrKycGate(paymentProcessor)
- const { isUserMantecaKycApproved, isUserSumsubKycApproved } = useKycStatus()
+ const { isUserSumsubKycApproved } = useKycStatus()
const sumsubFlow = useMultiPhaseKycFlow({})
const queryClient = useQueryClient()
const [isShaking, setIsShaking] = useState(false)
@@ -1158,7 +1169,12 @@ export default function QRPayPage() {
{
text: 'Verify now',
onClick: () =>
- sumsubFlow.handleInitiateKyc('LATAM', undefined, isUserSumsubKycApproved || undefined),
+ sumsubFlow.handleInitiateKyc(
+ 'LATAM',
+ undefined,
+ isUserSumsubKycApproved || undefined,
+ targetMantecaCountry
+ ),
variant: 'purple',
shadowSize: '4',
icon: 'check-circle',
@@ -1176,7 +1192,12 @@ export default function QRPayPage() {
{
text: 'Continue verification',
onClick: () =>
- sumsubFlow.handleInitiateKyc('LATAM', undefined, isUserSumsubKycApproved || undefined),
+ sumsubFlow.handleInitiateKyc(
+ 'LATAM',
+ undefined,
+ isUserSumsubKycApproved || undefined,
+ targetMantecaCountry
+ ),
variant: 'purple',
shadowSize: '4',
icon: 'check-circle',
diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx
index c8448eaaa..e94d0a0f9 100644
--- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx
+++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx
@@ -27,7 +27,6 @@ import ValidatedInput from '@/components/Global/ValidatedInput'
import AmountInput from '@/components/Global/AmountInput'
import { formatUnits, parseUnits } from 'viem'
import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
-import { useAuth } from '@/context/authContext'
import { useModalsContext } from '@/context/ModalsContext'
import Select from '@/components/Global/Select'
import { SoundPlayer } from '@/components/Global/SoundPlayer'
@@ -61,6 +60,7 @@ import { useSumsubActionFlow } from '@/hooks/useSumsubActionFlow'
import { initiateIncreaseLimits } from '@/app/actions/increase-limits'
import { SumsubKycWrapper } from '@/components/Kyc/SumsubKycWrapper'
import { useLimits } from '@/hooks/useLimits'
+import { useIdentityVerification } from '@/hooks/useIdentityVerification'
type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' | 'failure'
@@ -88,10 +88,10 @@ export default function MantecaWithdrawFlow() {
const { sendMoney, balance } = useWallet()
const { signTransferUserOp } = useSignUserOp()
const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext)
- const { user } = useAuth()
const { setIsSupportModalOpen, openSupportWithMessage } = useModalsContext()
const queryClient = useQueryClient()
- const { isUserMantecaKycApproved, isUserSumsubKycApproved } = useKycStatus()
+ const { isUserSumsubKycApproved } = useKycStatus()
+ const { isVerifiedForCountry } = useIdentityVerification()
const { manteca: mantecaRejection } = useProviderRejectionStatus()
const { hasPendingTransactions } = usePendingTransactions()
@@ -105,12 +105,11 @@ export default function MantecaWithdrawFlow() {
// 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.
-
- // Determine country and currency from URL params or context
- const countryPath = countryFromUrl || 'argentina'
+ const countryPath = countryFromUrl
// Map country path to CountryData for KYC
const selectedCountry = useMemo(() => {
+ if (!countryPath) return undefined
return countryData.find((country) => country.type === 'country' && country.path === countryPath)
}, [countryPath])
@@ -118,6 +117,7 @@ export default function MantecaWithdrawFlow() {
if (!selectedCountry) return undefined
return MANTECA_COUNTRIES_CONFIG[selectedCountry.id]
}, [selectedCountry])
+ const isUserMantecaKycApprovedForCountry = selectedCountry ? isVerifiedForCountry(selectedCountry.id) : false
const {
code: currencyCode,
@@ -236,7 +236,7 @@ export default function MantecaWithdrawFlow() {
}
setErrorMessage(null)
- if (!isUserMantecaKycApproved) {
+ if (!isUserMantecaKycApprovedForCountry) {
setShowKycModal(true)
return
}
@@ -281,7 +281,7 @@ export default function MantecaWithdrawFlow() {
usdAmount,
currencyCode,
currencyAmount,
- isUserMantecaKycApproved,
+ isUserMantecaKycApprovedForCountry,
isLockingPrice,
handleOnboardingError,
])
@@ -444,12 +444,12 @@ export default function MantecaWithdrawFlow() {
}
}, [step, queryClient])
- // redirect to withdraw page if country is not supported by manteca
+ // redirect to withdraw page if country is missing or not supported by manteca
useEffect(() => {
- if (!selectedCountry || !MANTECA_COUNTRIES_CONFIG[selectedCountry.id]) {
+ if (!countryFromUrl || !selectedCountry || !MANTECA_COUNTRIES_CONFIG[selectedCountry.id]) {
router.replace('/withdraw')
}
- }, [selectedCountry, router])
+ }, [countryFromUrl, selectedCountry, router])
if (isCurrencyLoading || !currencyPrice || !selectedCountry || !countryConfig) {
return
@@ -537,7 +537,7 @@ export default function MantecaWithdrawFlow() {
if (hasRejection) {
await sumsubFlow.handleSelfHealResubmit('MANTECA')
} else {
- await sumsubFlow.handleInitiateKyc('LATAM', undefined, true)
+ await sumsubFlow.handleInitiateKyc('LATAM', undefined, true, selectedCountry?.id)
}
setShowKycModal(false)
}}
diff --git a/src/app/actions/sumsub.ts b/src/app/actions/sumsub.ts
index 8cb387990..dfb857d99 100644
--- a/src/app/actions/sumsub.ts
+++ b/src/app/actions/sumsub.ts
@@ -12,6 +12,7 @@ export const initiateSumsubKyc = async (params?: {
regionIntent?: KYCRegionIntent
levelName?: string
crossRegion?: boolean
+ targetCountry?: string
}): Promise<{ data?: InitiateSumsubKycResponse; error?: string }> => {
const jwtToken = (await getJWTCookie())?.value
@@ -23,6 +24,7 @@ export const initiateSumsubKyc = async (params?: {
regionIntent: params?.regionIntent,
levelName: params?.levelName,
crossRegion: params?.crossRegion,
+ targetCountry: params?.targetCountry,
}
try {
diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx
index ac03ac285..171537246 100644
--- a/src/components/AddMoney/components/MantecaAddMoney.tsx
+++ b/src/components/AddMoney/components/MantecaAddMoney.tsx
@@ -6,7 +6,6 @@ import { useParams } from 'next/navigation'
import { type CountryData, countryData } from '@/components/AddMoney/consts'
import { type MantecaDepositResponseData } from '@/types/manteca.types'
import { useCurrency } from '@/hooks/useCurrency'
-import { useAuth } from '@/context/authContext'
import { mantecaApi } from '@/services/manteca'
import { parseUnits } from 'viem'
import { useQueryClient } from '@tanstack/react-query'
@@ -22,6 +21,7 @@ import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs'
import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation'
import posthog from 'posthog-js'
import { ANALYTICS_EVENTS } from '@/constants/analytics.consts'
+import { useIdentityVerification } from '@/hooks/useIdentityVerification'
// Step type for URL state
type MantecaStep = 'inputAmount' | 'depositDetails'
@@ -64,16 +64,16 @@ const MantecaAddMoney: FC = () => {
const selectedCountry = useMemo(() => {
return countryData.find((country) => country.type === 'country' && country.path === selectedCountryPath)
}, [selectedCountryPath])
- const { isUserMantecaKycApproved, isUserSumsubKycApproved } = useKycStatus()
+ const { isUserSumsubKycApproved } = useKycStatus()
+ const { isVerifiedForCountry } = useIdentityVerification()
const { manteca: mantecaRejection } = useProviderRejectionStatus()
const currencyData = useCurrency(selectedCountry?.currency ?? 'ARS')
- const { user } = useAuth()
-
// inline sumsub kyc flow for manteca users who need LATAM verification
// regionIntent is NOT passed here to avoid creating a backend record on mount.
// intent is passed at call time: handleInitiateKyc('LATAM')
const sumsubFlow = useMultiPhaseKycFlow({})
const [showKycModal, setShowKycModal] = useState(false)
+ const isUserMantecaKycApprovedForCountry = selectedCountry ? isVerifiedForCountry(selectedCountry.id) : false
// validates deposit amount against user's limits
// currency comes from country config - hook normalizes it internally
@@ -144,7 +144,7 @@ const MantecaAddMoney: FC = () => {
if (!selectedCountry?.currency) return
if (isCreatingDeposit) return
- if (!isUserMantecaKycApproved) {
+ if (!isUserMantecaKycApprovedForCountry) {
setShowKycModal(true)
return
}
@@ -200,7 +200,7 @@ const MantecaAddMoney: FC = () => {
currentDenomination,
selectedCountry,
displayedAmount,
- isUserMantecaKycApproved,
+ isUserMantecaKycApprovedForCountry,
isCreatingDeposit,
setUrlState,
usdAmount,
@@ -227,7 +227,7 @@ const MantecaAddMoney: FC = () => {
if (hasRejection) {
await sumsubFlow.handleSelfHealResubmit('MANTECA')
} else {
- await sumsubFlow.handleInitiateKyc('LATAM', undefined, true)
+ await sumsubFlow.handleInitiateKyc('LATAM', undefined, true, selectedCountry?.id)
}
setShowKycModal(false)
}}
diff --git a/src/components/Global/TranslationSafeWrapper.tsx b/src/components/Global/TranslationSafeWrapper.tsx
index 4e77af618..d05006cdd 100644
--- a/src/components/Global/TranslationSafeWrapper.tsx
+++ b/src/components/Global/TranslationSafeWrapper.tsx
@@ -1,18 +1,8 @@
'use client'
import { useTranslationMutationHandler } from '@/hooks/useTranslationMutationHandler'
-import { useRef } from 'react'
-// wraps the app to handle google translate dom mutations globally
-// prevents "Failed to execute 'insertBefore' on 'Node'" errors
-// while still allowing translations to work properly
+// patches dom methods globally to prevent translation extension crashes
export const TranslationSafeWrapper = ({ children }: { children: React.ReactNode }) => {
- const wrapperRef = useRef(null)
- // attach mutation observer to handle translation service dom changes
- useTranslationMutationHandler(wrapperRef)
-
- return (
-
- {children}
-
- )
+ useTranslationMutationHandler()
+ return <>{children}>
}
diff --git a/src/components/Kyc/InitiateKycModal.tsx b/src/components/Kyc/InitiateKycModal.tsx
index 48926dec0..9c5814432 100644
--- a/src/components/Kyc/InitiateKycModal.tsx
+++ b/src/components/Kyc/InitiateKycModal.tsx
@@ -21,7 +21,7 @@ interface InitiateKycModalProps {
// for fresh KYC: "Verify your identity"
// for provider rejections: "We need extra documents"
// for blocked: "Verification issue — contact support"
-// for cross-region: "Your identity is verified, we need a local ID"
+// for cross-region: "Your identity is verified, submit a local ID"
export const InitiateKycModal = ({
visible,
onClose,
@@ -41,6 +41,7 @@ export const InitiateKycModal = ({
if (error) return 'Something went wrong'
if (isBlocked) return 'Verification issue'
if (isProviderRejection) return 'We need extra documents'
+ if (isCrossRegion) return 'Submit local ID'
return 'Verify your identity'
}
@@ -70,6 +71,13 @@ export const InitiateKycModal = ({
icon: 'upload' as IconName,
}
}
+ if (isCrossRegion) {
+ return {
+ text: isLoading ? 'Loading...' : 'Submit document',
+ onClick: onVerify,
+ icon: 'upload' as IconName,
+ }
+ }
return {
text: isLoading ? 'Loading...' : 'Start Verification',
onClick: onVerify,
diff --git a/src/hooks/useMultiPhaseKycFlow.ts b/src/hooks/useMultiPhaseKycFlow.ts
index 64c6e0664..f044166ea 100644
--- a/src/hooks/useMultiPhaseKycFlow.ts
+++ b/src/hooks/useMultiPhaseKycFlow.ts
@@ -183,7 +183,7 @@ export const useMultiPhaseKycFlow = ({ onKycSuccess, onManualClose, regionIntent
// wrap handleInitiateKyc to reset state for new attempts
const handleInitiateKyc = useCallback(
- async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean) => {
+ async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean, targetCountry?: string) => {
const intent = overrideIntent ?? regionIntent
posthog.capture(
intent === 'LATAM' ? ANALYTICS_EVENTS.MANTECA_KYC_INITIATED : ANALYTICS_EVENTS.KYC_INITIATED,
@@ -199,7 +199,7 @@ export const useMultiPhaseKycFlow = ({ onKycSuccess, onManualClose, regionIntent
isRealtimeFlowRef.current = false
clearPreparingTimer()
- await originalHandleInitiateKyc(overrideIntent, levelName, crossRegion)
+ await originalHandleInitiateKyc(overrideIntent, levelName, crossRegion, targetCountry)
},
[originalHandleInitiateKyc, clearPreparingTimer, regionIntent, acquisitionSource]
)
diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts
index 03c119288..3fe38a3e1 100644
--- a/src/hooks/useSumsubKycFlow.ts
+++ b/src/hooks/useSumsubKycFlow.ts
@@ -31,6 +31,8 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
const regionIntentRef = useRef(regionIntent)
// tracks the level name across initiate + refresh (e.g. 'peanut-additional-docs')
const levelNameRef = useRef(undefined)
+ // tracks the selected target country across initiate + refresh for country-scoped Manteca actions
+ const targetCountryRef = useRef(undefined)
// guards fetchCurrentStatus from running while handleInitiateKyc is in progress
const initiatingRef = useRef(false)
// guard: only fire onKycSuccess when the user initiated a kyc flow in this session.
@@ -120,6 +122,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
const response = await initiateSumsubKyc({
regionIntent: regionIntentRef.current,
levelName: levelNameRef.current,
+ targetCountry: targetCountryRef.current,
})
if (response.data?.status) {
setLiveKycStatus(response.data.status)
@@ -134,7 +137,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
}, [isVerificationProgressModalOpen])
const handleInitiateKyc = useCallback(
- async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean) => {
+ async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean, targetCountry?: string) => {
userInitiatedRef.current = true
initiatingRef.current = true
selfHealProviderRef.current = null
@@ -154,6 +157,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
regionIntent: overrideIntent ?? regionIntent,
levelName,
crossRegion,
+ targetCountry,
})
if (response.error) {
@@ -174,6 +178,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
const effectiveIntent = overrideIntent ?? regionIntent
if (effectiveIntent) regionIntentRef.current = effectiveIntent
levelNameRef.current = levelName
+ targetCountryRef.current = targetCountry
// cross-region: bridge-direct means no SDK needed — backend is handling
// rail enrollment + submission. go straight to the post-approval flow.
@@ -246,6 +251,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
const response = await initiateSumsubKyc({
regionIntent: regionIntentRef.current,
levelName: levelNameRef.current,
+ targetCountry: targetCountryRef.current,
})
if (response.error || !response.data?.token) {
diff --git a/src/hooks/useTranslationMutationHandler.ts b/src/hooks/useTranslationMutationHandler.ts
index e079660b0..2d7bc525e 100644
--- a/src/hooks/useTranslationMutationHandler.ts
+++ b/src/hooks/useTranslationMutationHandler.ts
@@ -1,67 +1,41 @@
-import { useEffect, useRef } from 'react'
+import { useEffect } from 'react'
-// handles translation service dom mutations to prevent errors while allowing translations
-export const useTranslationMutationHandler = (targetRef: React.RefObject) => {
- // keep reference to observer instance for cleanup
- const observerRef = useRef(null)
+const PATCH_KEY = '__peanut_translation_patched__'
+// patches removeChild and insertBefore to prevent crashes when browser
+// translation extensions (google translate, brave translate, etc) move
+// dom nodes out of their react-managed parents. without this, react's
+// reconciliation throws "not a child of this node" during unmount/update.
+//
+// intentionally no useEffect cleanup — patches are permanent for the page
+// lifetime. removing them on unmount would re-expose the crash.
+export const useTranslationMutationHandler = () => {
useEffect(() => {
- const setupObserver = () => {
- if (!targetRef.current) return
+ // window global survives HMR — module-scoped flag resets on hot reload,
+ // which would nest patched wrappers around each other
+ if ((window as any)[PATCH_KEY]) return
+ ;(window as any)[PATCH_KEY] = true
- if (observerRef.current) {
- observerRef.current.disconnect()
+ const originalRemoveChild = Node.prototype.removeChild
+ Node.prototype.removeChild = function (child: T): T {
+ if (child.parentNode !== this) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn('[translation-patch] removeChild: node is not a child, skipping', child)
+ }
+ return child
}
-
- observerRef.current = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.type === 'childList') {
- mutation.addedNodes.forEach((node) => {
- if (node.nodeType === 1) {
- try {
- const element = node as Element
- // handle translated content nodes that google translate adds
- if (element.hasAttribute('data-translated')) {
- const parent = element.parentElement
- if (parent) {
- // remove any duplicate translations to prevent conflicts
- const existingTranslations = parent.querySelectorAll('[data-translated]')
- existingTranslations.forEach((el) => {
- if (el !== element && el.textContent === element.textContent) {
- el.remove()
- }
- })
-
- // append new translation if not already present
- if (!parent.contains(element)) {
- requestAnimationFrame(() => {
- try {
- parent.appendChild(element)
- } catch (e) {
- console.error(e)
- }
- })
- }
- }
- }
- } catch (e) {
- console.error(e)
- }
- }
- })
- }
- })
- })
-
- // observe changes to dom structure and attributes
- observerRef.current.observe(targetRef.current, {
- childList: true,
- subtree: true,
- attributes: true,
- })
+ return originalRemoveChild.call(this, child) as T
}
- setupObserver()
- return () => observerRef.current?.disconnect()
- }, [targetRef])
+ const originalInsertBefore = Node.prototype.insertBefore
+ Node.prototype.insertBefore = function (newNode: T, refNode: Node | null): T {
+ if (refNode && refNode.parentNode !== this) {
+ if (process.env.NODE_ENV !== 'production') {
+ console.warn('[translation-patch] insertBefore: ref node is not a child, skipping', refNode)
+ }
+ return newNode
+ }
+ return originalInsertBefore.call(this, newNode, refNode) as T
+ }
+ }, [])
}