Skip to content
Merged
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
111 changes: 19 additions & 92 deletions src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,25 @@ import { useWebSocket } from '@/hooks/useWebSocket'
import { useAuth } from '@/context/authContext'
import { useCreateOnramp } from '@/hooks/useCreateOnramp'
import { useRouter, useParams } from 'next/navigation'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import countryCurrencyMappings, { isNonEuroSepaCountry, isUKCountry } from '@/constants/countryCurrencyMapping'
import { formatUnits } from 'viem'
import PeanutLoading from '@/components/Global/PeanutLoading'
import EmptyState from '@/components/Global/EmptyStates/EmptyState'
import { UserDetailsForm, type UserDetailsFormData } from '@/components/AddMoney/UserDetailsForm'
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'
type BridgeBankStep = 'inputAmount' | 'kyc' | 'showDetails'

export default function OnrampBankPage() {
const router = useRouter()
Expand All @@ -43,7 +42,7 @@ export default function OnrampBankPage() {
// Example: /add-money/mexico/bank?step=inputAmount&amount=500
const [urlState, setUrlState] = useQueryStates(
{
step: parseAsStringEnum<BridgeBankStep>(['inputAmount', 'kyc', 'collectUserDetails', 'showDetails']),
step: parseAsStringEnum<BridgeBankStep>(['inputAmount', 'kyc', 'showDetails']),
amount: parseAsString,
},
{ history: 'push' }
Expand All @@ -55,19 +54,22 @@ export default function OnrampBankPage() {
// Local UI state (not URL-appropriate - transient)
const [showWarningModal, setShowWarningModal] = useState<boolean>(false)
const [isRiskAccepted, setIsRiskAccepted] = useState<boolean>(false)
const [isKycModalOpen, setIsKycModalOpen] = useState(false)
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(undefined)
const [isUpdatingUser, setIsUpdatingUser] = useState(false)
const [userUpdateError, setUserUpdateError] = useState<string | null>(null)
const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false)

const { setError, error, setOnrampData, onrampData } = useOnrampFlow()
const formRef = useRef<{ handleSubmit: () => void }>(null)

const { balance } = useWallet()
const { user, fetchUser } = useAuth()
const { createOnramp, isLoading: isCreatingOnramp, error: onrampError } = useCreateOnramp()

// inline sumsub kyc flow for bridge bank onramp
// regionIntent is NOT passed here to avoid creating a backend record on mount.
// intent is passed at call time: handleInitiateKyc('STANDARD')
const sumsubFlow = useMultiPhaseKycFlow({
onKycSuccess: () => {
setUrlState({ step: 'inputAmount' })
},
})

const selectedCountryPath = params.country as string

const selectedCountry = useMemo(() => {
Expand Down Expand Up @@ -162,7 +164,7 @@ export default function OnrampBankPage() {
const isUserKycVerified = currentKycStatus === 'approved'

if (!isUserKycVerified) {
setUrlState({ step: 'collectUserDetails' })
setUrlState({ step: 'kyc' })
} else {
setUrlState({ step: 'inputAmount' })
}
Expand Down Expand Up @@ -261,39 +263,6 @@ export default function OnrampBankPage() {
setIsRiskAccepted(false)
}

const handleKycSuccess = () => {
setIsKycModalOpen(false)
setUrlState({ step: 'inputAmount' })
}

const handleKycModalClose = () => {
setIsKycModalOpen(false)
}

const handleUserDetailsSubmit = async (data: UserDetailsFormData) => {
setIsUpdatingUser(true)
setUserUpdateError(null)
try {
if (!user?.user.userId) throw new Error('User not found')
const result = await updateUserById({
userId: user.user.userId,
fullName: data.fullName,
email: data.email,
})
if (result.error) {
throw new Error(result.error)
}
await fetchUser()
setUrlState({ step: 'kyc' })
} catch (error: any) {
setUserUpdateError(error.message)
return { error: error.message }
} finally {
setIsUpdatingUser(false)
}
return {}
}

const handleBack = () => {
if (selectedCountry) {
router.push(`/add-money/${selectedCountry.path}`)
Expand All @@ -302,19 +271,11 @@ export default function OnrampBankPage() {
}
}

const initialUserDetails: Partial<UserDetailsFormData> = useMemo(
() => ({
fullName: user?.user.fullName ?? '',
email: user?.user.email ?? '',
}),
[user?.user.fullName, user?.user.email]
)

useEffect(() => {
if (urlState.step === 'kyc') {
setIsKycModalOpen(true)
sumsubFlow.handleInitiateKyc('STANDARD')
}
}, [urlState.step])
}, [urlState.step]) // eslint-disable-line react-hooks/exhaustive-deps

// Redirect to inputAmount if showDetails is accessed without required data (deep link / back navigation)
useEffect(() => {
Expand Down Expand Up @@ -342,45 +303,11 @@ export default function OnrampBankPage() {
return <PeanutLoading />
}

if (urlState.step === 'collectUserDetails') {
return (
<div className="flex flex-col justify-start space-y-8">
<NavHeader onPrev={handleBack} title="Identity Verification" />
<div className="flex flex-grow flex-col justify-center space-y-4">
<h3 className="text-sm font-bold">Verify your details</h3>
<UserDetailsForm
ref={formRef}
onSubmit={handleUserDetailsSubmit}
isSubmitting={isUpdatingUser}
onValidChange={setIsUserDetailsFormValid}
initialData={initialUserDetails}
/>
<Button
onClick={() => formRef.current?.handleSubmit()}
loading={isUpdatingUser}
variant="purple"
shadowSize="4"
className="w-full"
disabled={!isUserDetailsFormValid || isUpdatingUser}
>
Continue
</Button>
{userUpdateError && <ErrorAlert description={userUpdateError} />}
</div>
</div>
)
}

if (urlState.step === 'kyc') {
return (
<div className="flex flex-col justify-start space-y-8">
<InitiateBridgeKYCModal
isOpen={isKycModalOpen}
onClose={handleKycModalClose}
onKycSuccess={handleKycSuccess}
onManualClose={() => router.push(`/add-money/${selectedCountry.path}`)}
flow="add"
/>
<NavHeader title="Identity Verification" onPrev={handleBack} />
<SumsubKycModals flow={sumsubFlow} autoStartSdk />
</div>
)
}
Expand Down
32 changes: 7 additions & 25 deletions src/app/(mobile-ui)/history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ 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 { groupKycByRegion } from '@/utils/kyc-grouping.utils'
import { useAuth } from '@/context/authContext'
import { BadgeStatusItem } from '@/components/Badges/BadgeStatusItem'
import { isBadgeHistoryItem } from '@/components/Badges/badge.types'
Expand Down Expand Up @@ -165,30 +166,10 @@ const HistoryPage = () => {
})
})

if (user) {
if (user.user?.bridgeKycStatus && user.user.bridgeKycStatus !== 'not_started') {
// Use appropriate timestamp based on KYC status
const bridgeKycTimestamp = (() => {
const status = user.user.bridgeKycStatus
if (status === 'approved') return user.user.bridgeKycApprovedAt
if (status === 'rejected') return user.user.bridgeKycRejectedAt
return user.user.bridgeKycStartedAt
})()
entries.push({
isKyc: true,
timestamp: bridgeKycTimestamp ?? user.user.createdAt ?? new Date().toISOString(),
uuid: 'bridge-kyc-status-item',
bridgeKycStatus: user.user.bridgeKycStatus,
})
}
user.user.kycVerifications?.forEach((verification) => {
entries.push({
isKyc: true,
timestamp: verification.approvedAt ?? verification.updatedAt ?? verification.createdAt,
uuid: verification.providerUserId ?? `${verification.provider}-${verification.mantecaGeo}`,
verification,
})
})
// add one kyc entry per region (STANDARD, LATAM)
if (user?.user) {
const regionEntries = groupKycByRegion(user.user)
entries.push(...regionEntries)
}

entries.sort((a, b) => {
Expand Down Expand Up @@ -272,6 +253,7 @@ const HistoryPage = () => {
bridgeKycStartedAt={
item.bridgeKycStatus ? user?.user.bridgeKycStartedAt : undefined
}
region={item.region}
/>
) : isBadgeHistoryItem(item) ? (
<BadgeStatusItem position={position} entry={item} />
Expand Down
73 changes: 24 additions & 49 deletions src/app/(mobile-ui)/withdraw/manteca/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ 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 { InitiateKycModal } from '@/components/Kyc/InitiateKycModal'
import { usePendingTransactions } from '@/hooks/wallet/usePendingTransactions'
import { PointsAction } from '@/services/services.types'
import { usePointsConfetti } from '@/hooks/usePointsConfetti'
Expand Down Expand Up @@ -68,7 +68,6 @@ export default function MantecaWithdrawFlow() {
const [selectedBank, setSelectedBank] = useState<MantecaBankCode | null>(null)
const [accountType, setAccountType] = useState<MantecaAccountType | null>(null)
const [errorMessage, setErrorMessage] = useState<string | null>(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
Expand All @@ -78,11 +77,17 @@ 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
// 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)
// 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.
Expand All @@ -106,9 +111,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({
Expand All @@ -117,19 +119,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()
Expand Down Expand Up @@ -200,14 +189,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) {
setShowKycModal(true)
return
}

Expand Down Expand Up @@ -250,7 +233,7 @@ export default function MantecaWithdrawFlow() {
usdAmount,
currencyCode,
currencyAmount,
isMantecaKycRequired,
isUserMantecaKycApproved,
isLockingPrice,
])

Expand Down Expand Up @@ -342,7 +325,6 @@ export default function MantecaWithdrawFlow() {
setSelectedBank(null)
setAccountType(null)
setErrorMessage(null)
setIsKycModalOpen(false)
setIsDestinationAddressValid(false)
setIsDestinationAddressChanging(false)
setBalanceErrorMessage(null)
Expand Down Expand Up @@ -468,6 +450,16 @@ export default function MantecaWithdrawFlow() {
}
return (
<div className="flex min-h-[inherit] flex-col gap-8">
<InitiateKycModal
visible={showKycModal}
onClose={() => setShowKycModal(false)}
onVerify={async () => {
setShowKycModal(false)
await sumsubFlow.handleInitiateKyc('LATAM')
}}
isLoading={sumsubFlow.isLoading}
/>
<SumsubKycModals flow={sumsubFlow} />
<NavHeader
title="Withdraw"
onPrev={() => {
Expand Down Expand Up @@ -649,23 +641,6 @@ export default function MantecaWithdrawFlow() {

{errorMessage && <ErrorAlert description={errorMessage} />}
</div>

{/* KYC Modal */}
{isKycModalOpen && selectedCountry && (
<MantecaGeoSpecificKycModal
isUserBridgeKycApproved={isUserBridgeKycApproved}
isMantecaModalOpen={isKycModalOpen}
setIsMantecaModalOpen={setIsKycModalOpen}
onClose={() => setIsKycModalOpen(false)}
onManualClose={() => setIsKycModalOpen(false)}
onKycSuccess={() => {
setIsKycModalOpen(false)
fetchUser()
setStep('review')
}}
selectedCountry={selectedCountry}
/>
)}
</div>
)}

Expand Down
Loading
Loading