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
23 changes: 3 additions & 20 deletions src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import { useOnrampFlow } from '@/context/OnrampFlowContext'
import { useWallet } from '@/hooks/wallet/useWallet'
import { formatAmount } from '@/utils/general.utils'
import { countryData } from '@/components/AddMoney/consts'
import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils'
import { useWebSocket } from '@/hooks/useWebSocket'
import { useAuth } from '@/context/authContext'
import useKycStatus from '@/hooks/useKycStatus'
import { useCreateOnramp } from '@/hooks/useCreateOnramp'
import { useRouter, useParams } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from 'react'
Expand Down Expand Up @@ -56,7 +55,6 @@ export default function OnrampBankPage() {
const [showWarningModal, setShowWarningModal] = useState<boolean>(false)
const [showKycModal, setShowKycModal] = useState<boolean>(false)
const [isRiskAccepted, setIsRiskAccepted] = useState<boolean>(false)
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(undefined)
const { setError, error, setOnrampData, onrampData } = useOnrampFlow()

const { balance } = useWallet()
Expand Down Expand Up @@ -91,19 +89,7 @@ export default function OnrampBankPage() {
// uk-specific check
const isUK = isUKCountry(selectedCountryPath)

useWebSocket({
username: user?.user.username ?? undefined,
autoConnect: !!user?.user.username,
onKycStatusUpdate: (newStatus) => {
setLiveKycStatus(newStatus as BridgeKycStatus)
},
})

useEffect(() => {
if (user?.user.bridgeKycStatus) {
setLiveKycStatus(user.user.bridgeKycStatus as BridgeKycStatus)
}
}, [user?.user.bridgeKycStatus])
const { isUserKycApproved } = useKycStatus()

useEffect(() => {
fetchUser()
Expand Down Expand Up @@ -203,10 +189,7 @@ export default function OnrampBankPage() {
const handleAmountContinue = () => {
if (!validateAmount(rawTokenAmount)) return

const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus
const isUserKycVerified = currentKycStatus === 'approved'

if (!isUserKycVerified) {
if (!isUserKycApproved) {
setShowKycModal(true)
return
}
Expand Down
4 changes: 4 additions & 0 deletions src/app/(mobile-ui)/history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ const HistoryPage = () => {
console.log('KYC status updated via WebSocket:', newStatus)
await fetchUser()
},
onSumsubKycStatusUpdate: async (newStatus: string) => {
console.log('Sumsub KYC status updated via WebSocket:', newStatus)
await fetchUser()
},
})

const allEntries = useMemo(() => historyData?.pages.flatMap((page) => page.entries) ?? [], [historyData])
Expand Down
33 changes: 6 additions & 27 deletions src/components/AddWithdraw/AddWithdrawCountriesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ import Image, { type StaticImageData } from 'next/image'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
import EmptyState from '../Global/EmptyStates/EmptyState'
import { useAuth } from '@/context/authContext'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { DynamicBankAccountForm, type IBankAccountDetails } from './DynamicBankAccountForm'
import { addBankAccount } from '@/app/actions/users'
import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils'
import { type AddBankAccountPayload } from '@/app/actions/types/users.types'
import { useWebSocket } from '@/hooks/useWebSocket'
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
import { type Account } from '@/interfaces'
import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
Expand Down Expand Up @@ -64,28 +62,11 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
const [view, setView] = useState<'list' | 'form'>(flow === 'withdraw' && amountToWithdraw ? 'form' : 'list')
const [isKycModalOpen, setIsKycModalOpen] = useState(false)
const formRef = useRef<{ handleSubmit: () => void }>(null)
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(
user?.user?.bridgeKycStatus as BridgeKycStatus
)
const [isSupportedTokensModalOpen, setIsSupportedTokensModalOpen] = useState(false)

const { isUserBridgeKycUnderReview } = useKycStatus()
const { isUserKycApproved, isUserBridgeKycUnderReview } = useKycStatus()
const [showKycStatusModal, setShowKycStatusModal] = useState(false)

useWebSocket({
username: user?.user.username ?? undefined,
autoConnect: !!user?.user.username,
onKycStatusUpdate: (newStatus) => {
setLiveKycStatus(newStatus as BridgeKycStatus)
},
})

useEffect(() => {
if (user?.user.bridgeKycStatus) {
setLiveKycStatus(user.user.bridgeKycStatus as BridgeKycStatus)
}
}, [user?.user.bridgeKycStatus])

const countryPathParts = Array.isArray(params.country) ? params.country : [params.country]
const isBankPage = countryPathParts[countryPathParts.length - 1] === 'bank'
const countrySlugFromUrl = isBankPage ? countryPathParts.slice(0, -1).join('-') : countryPathParts.join('-')
Expand All @@ -100,14 +81,12 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
): Promise<{ error?: string }> => {
// re-fetch user to ensure we have the latest KYC status
// (the multi-phase flow may have completed but websocket/state not yet propagated)
const freshUser = await fetchUser()
const currentKycStatus = freshUser?.user?.bridgeKycStatus || liveKycStatus || user?.user.bridgeKycStatus
const isUserKycVerified = currentKycStatus === 'approved'
await fetchUser()

// scenario (1): happy path: if the user has already completed kyc, we can add the bank account directly
// email and name are now collected by sumsub — no need to check them here
if (isUserKycVerified) {
const currentAccountIds = new Set((freshUser?.accounts ?? user?.accounts ?? []).map((acc) => acc.id))
if (isUserKycApproved) {
const currentAccountIds = new Set((user?.accounts ?? []).map((acc) => acc.id))

const result = await addBankAccount(payload)
if (result.error) {
Expand Down Expand Up @@ -149,7 +128,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {

// scenario (2): if the user hasn't completed kyc yet
// name and email are now collected by sumsub sdk — no need to save them beforehand
if (!isUserKycVerified) {
if (!isUserKycApproved) {
await sumsubFlow.handleInitiateKyc('STANDARD')
}

Expand Down
23 changes: 1 addition & 22 deletions src/components/Claim/Link/views/BankFlowManager.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { type IClaimScreenProps } from '../../Claim.consts'
import { DynamicBankAccountForm, type IBankAccountDetails } from '@/components/AddWithdraw/DynamicBankAccountForm'
import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { useCallback, useContext, useState, useRef, useEffect } from 'react'
import { useCallback, useContext, useState, useRef } from 'react'
import { loadingStateContext } from '@/context'
import { createBridgeExternalAccountForGuest } from '@/app/actions/external-accounts'
import { confirmOfframp, createOfframp, createOfframpForGuest } from '@/app/actions/offramp'
Expand All @@ -25,8 +25,6 @@ import useSavedAccounts from '@/hooks/useSavedAccounts'
import { ConfirmBankClaimView } from './Confirm.bank-claim.view'
import { CountryListRouter } from '@/components/Common/CountryListRouter'
import NavHeader from '@/components/Global/NavHeader'
import { useWebSocket } from '@/hooks/useWebSocket'
import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils'
import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
import { useAppDispatch } from '@/redux/hooks'
import { bankFormActions } from '@/redux/slices/bank-form-slice'
Expand Down Expand Up @@ -96,28 +94,9 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
const [receiverFullName, setReceiverFullName] = useState<string>('')
const [error, setError] = useState<string | null>(null)
const formRef = useRef<{ handleSubmit: () => void }>(null)
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(
user?.user?.bridgeKycStatus as BridgeKycStatus
)
const [isProcessingKycSuccess, setIsProcessingKycSuccess] = useState(false)
const [offrampData, setOfframpData] = useState<TCreateOfframpResponse | null>(null)

// websocket for real-time KYC status updates
useWebSocket({
username: user?.user.username ?? undefined,
autoConnect: !!user?.user.username,
onKycStatusUpdate: (newStatus) => {
setLiveKycStatus(newStatus as BridgeKycStatus)
},
})

// effect to update live KYC status from user object
useEffect(() => {
if (user?.user.bridgeKycStatus) {
setLiveKycStatus(user.user.bridgeKycStatus as BridgeKycStatus)
}
}, [user?.user.bridgeKycStatus])

/**
* @name handleConfirmClaim
* @description claims the link to the deposit address provided by the off-ramp api and confirms the transfer.
Expand Down
7 changes: 7 additions & 0 deletions src/components/Home/HomeHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h
},
[fetchUser]
),
onSumsubKycStatusUpdate: useCallback(
async (newStatus: string) => {
console.log('Sumsub KYC status updated via WebSocket:', newStatus)
await fetchUser()
},
[fetchUser]
),
})

// Combine fetched history with real-time updates
Expand Down
56 changes: 29 additions & 27 deletions src/components/IdentityVerification/StartVerificationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,42 @@ import { Icon } from '../Global/Icons/Icon'
import { type Region } from '@/hooks/useIdentityVerification'
import React from 'react'

const QR_PAYMENTS = (
<p key="qr">
QR Payments in <b>Argentina and Brazil</b>
</p>
)

const BRIDGE_UNLOCK_ITEMS: Array<string | React.ReactNode> = [
<p key="sepa">
<b>Europe</b> SEPA transfers (+30 countries)
</p>,
<p key="uk">
<b>UK</b> Faster payment transfers
</p>,
<p key="ach">
<b>United States</b> ACH and Wire transfers
</p>,
<p key="mx">
<b>Mexico</b> SPEI transfers
</p>,
QR_PAYMENTS,
]

// unlock benefits shown per region
const REGION_UNLOCK_ITEMS: Record<string, Array<string | React.ReactNode>> = {
latam: [
<p key="bank">
Bank transfers to your own accounts in <b>LATAM</b>
</p>,
<p key="qr">
QR Payments in <b>Argentina and Brazil</b>
</p>,
],
europe: [
<p key="sepa">
<b>Europe</b> SEPA transfers (+30 countries)
</p>,
<p key="qr">
QR Payments in <b>Argentina and Brazil</b>
</p>,
],
'north-america': [
<p key="ach">
<b>United States</b> ACH and Wire transfers
</p>,
<p key="mx">
<b>Mexico</b> SPEI transfers
</p>,
<p key="qr">
QR Payments in <b>Argentina and Brazil</b>
</p>,
],
'rest-of-the-world': [
<p key="qr">
QR Payments in <b>Argentina and Brazil</b>
</p>,
QR_PAYMENTS,
],

europe: BRIDGE_UNLOCK_ITEMS,

'north-america': BRIDGE_UNLOCK_ITEMS,

'rest-of-the-world': [QR_PAYMENTS],
}

const DEFAULT_UNLOCK_ITEMS = [<p key="bank">Bank transfers and local payment methods</p>]
Expand Down
4 changes: 2 additions & 2 deletions src/components/Kyc/CountryFlagAndName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const CountryFlagAndName = ({ countryCode, isBridgeRegion }: CountryFlagA
icons={[
'https://flagcdn.com/w160/us.png',
'https://flagcdn.com/w160/eu.png',
'https://flagcdn.com/w160/mx.png',
'https://flagcdn.com/w160/gb.png',
'https://flagcdn.com/w160/mx.png',
]}
iconSize={80}
imageClassName="h-5 w-5 min-h-5 min-w-5 rounded-full object-cover object-center shadow-sm"
Expand All @@ -32,7 +32,7 @@ export const CountryFlagAndName = ({ countryCode, isBridgeRegion }: CountryFlagA
loading="lazy"
/>
)}
{isBridgeRegion ? 'US/EU/MX/UK' : countryName}
{isBridgeRegion ? 'US/EU/UK/MX' : countryName}
</div>
)
}
18 changes: 14 additions & 4 deletions src/components/Kyc/KycStatusItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ export const KycStatusItem = ({
const finalBridgeKycStatus = wsBridgeKycStatus || bridgeKycStatus || user?.user?.bridgeKycStatus
const kycStatus = verification ? verification.status : finalBridgeKycStatus

// check if any bridge rail needs additional documents
const hasBridgeDocsNeeded = useMemo(
() =>
(user?.rails ?? []).some(
(r) => r.status === 'REQUIRES_EXTRA_INFORMATION' && r.rail.provider.code === 'BRIDGE'
),
[user?.rails]
)

const isApproved = isKycStatusApproved(kycStatus)
const isPending = isKycStatusPending(kycStatus)
const isRejected = isKycStatusFailed(kycStatus)
Expand All @@ -80,13 +89,14 @@ export const KycStatusItem = ({
const isInitiatedButNotStarted = !!verification && isKycStatusNotStarted(kycStatus)

const subtitle = useMemo(() => {
if (hasBridgeDocsNeeded) return 'Action needed'
if (isInitiatedButNotStarted) return 'Not completed'
if (isActionRequired) return 'Action needed'
if (isPending) return 'Processing'
if (isApproved) return 'Completed'
if (isApproved) return 'Verified'
if (isRejected) return 'Failed'
return 'Unknown'
}, [isInitiatedButNotStarted, isActionRequired, isPending, isApproved, isRejected])
}, [hasBridgeDocsNeeded, isInitiatedButNotStarted, isActionRequired, isPending, isApproved, isRejected])

const title = useMemo(() => {
if (region === 'LATAM') return 'LATAM verification'
Expand All @@ -95,7 +105,7 @@ export const KycStatusItem = ({

// only hide for bridge's default "not_started" state.
// if a verification record exists, the user has initiated KYC — show it.
if (!verification && isKycStatusNotStarted(kycStatus)) {
if (!verification && !hasBridgeDocsNeeded && isKycStatusNotStarted(kycStatus)) {
return null
}

Expand All @@ -117,7 +127,7 @@ export const KycStatusItem = ({
<p className="text-sm text-grey-1">{subtitle}</p>
<StatusPill
status={
isInitiatedButNotStarted || isActionRequired || isPending
hasBridgeDocsNeeded || isInitiatedButNotStarted || isActionRequired || isPending
? 'pending'
: isRejected
? 'cancelled'
Expand Down
4 changes: 2 additions & 2 deletions src/components/Kyc/KycVerificationInProgressModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ export const KycVerificationInProgressModal = ({
onClose={onClose}
isLoadingIcon
iconContainerClassName="bg-yellow-1 text-black"
title="Identity verified!"
title="Verification in progress"
description={
preparingTimedOut
? "This is taking longer than expected. You can continue and we'll notify you when it's ready."
: 'Preparing your account...'
: 'Submitting your information and preparing your account. This usually takes less than a minute.'
}
ctas={
preparingTimedOut
Expand Down
10 changes: 10 additions & 0 deletions src/components/Kyc/SumsubKycWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export const SumsubKycWrapper = ({
}

try {
// track sdk init time so we can ignore stale onApplicantStatusChanged events
// that fire immediately when the applicant is already approved (e.g. additional-docs flow)
const sdkInitTime = Date.now()

const handleSubmitted = () => {
console.log('[sumsub] onApplicantSubmitted fired')
stableOnComplete()
Expand All @@ -103,6 +107,12 @@ export const SumsubKycWrapper = ({
reviewResult?: { reviewAnswer?: string }
}) => {
console.log('[sumsub] onApplicantStatusChanged fired', payload)
// ignore status events that fire within 3s of sdk init — these reflect
// pre-existing state (e.g. user already approved), not a new submission
if (Date.now() - sdkInitTime < 3000) {
console.log('[sumsub] ignoring early onApplicantStatusChanged (pre-existing state)')
return
}
// auto-close when sumsub shows success screen
if (payload?.reviewStatus === 'completed' && payload?.reviewResult?.reviewAnswer === 'GREEN') {
stableOnComplete()
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/modals/KycProcessingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const KycProcessingModal = ({ visible, onClose }: KycProcessingModalProps
visible={visible}
onClose={onClose}
icon="clock"
iconContainerClassName="bg-purple-3"
iconContainerClassName="bg-yellow-1"
title="Verification in progress"
description="We're reviewing your identity. This usually takes less than a minute."
ctas={[
Expand Down
2 changes: 1 addition & 1 deletion src/components/Kyc/states/KycCompleted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const KycCompleted = ({

return (
<div className="space-y-4">
<KYCStatusDrawerItem status="completed" />
<KYCStatusDrawerItem status="completed" customText="Verified" />
<Card position="single">
<PaymentInfoRow label="Verified on" value={verifiedOn} />
<CountryRegionRow countryCode={countryCode} isBridge={isBridge} />
Expand Down
Loading
Loading