Skip to content

Commit f5756da

Browse files
Merge pull request #1713 from peanutprotocol/fix/kyc2.0-bugs-v3
fix: kyc 2.0 bugs part 3
2 parents e1fabd4 + c6fe2b9 commit f5756da

16 files changed

Lines changed: 118 additions & 125 deletions

src/app/(mobile-ui)/add-money/[country]/bank/page.tsx

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import { useOnrampFlow } from '@/context/OnrampFlowContext'
99
import { useWallet } from '@/hooks/wallet/useWallet'
1010
import { formatAmount } from '@/utils/general.utils'
1111
import { countryData } from '@/components/AddMoney/consts'
12-
import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils'
13-
import { useWebSocket } from '@/hooks/useWebSocket'
1412
import { useAuth } from '@/context/authContext'
13+
import useKycStatus from '@/hooks/useKycStatus'
1514
import { useCreateOnramp } from '@/hooks/useCreateOnramp'
1615
import { useRouter, useParams } from 'next/navigation'
1716
import { useCallback, useEffect, useMemo, useState } from 'react'
@@ -56,7 +55,6 @@ export default function OnrampBankPage() {
5655
const [showWarningModal, setShowWarningModal] = useState<boolean>(false)
5756
const [showKycModal, setShowKycModal] = useState<boolean>(false)
5857
const [isRiskAccepted, setIsRiskAccepted] = useState<boolean>(false)
59-
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(undefined)
6058
const { setError, error, setOnrampData, onrampData } = useOnrampFlow()
6159

6260
const { balance } = useWallet()
@@ -91,19 +89,7 @@ export default function OnrampBankPage() {
9189
// uk-specific check
9290
const isUK = isUKCountry(selectedCountryPath)
9391

94-
useWebSocket({
95-
username: user?.user.username ?? undefined,
96-
autoConnect: !!user?.user.username,
97-
onKycStatusUpdate: (newStatus) => {
98-
setLiveKycStatus(newStatus as BridgeKycStatus)
99-
},
100-
})
101-
102-
useEffect(() => {
103-
if (user?.user.bridgeKycStatus) {
104-
setLiveKycStatus(user.user.bridgeKycStatus as BridgeKycStatus)
105-
}
106-
}, [user?.user.bridgeKycStatus])
92+
const { isUserKycApproved } = useKycStatus()
10793

10894
useEffect(() => {
10995
fetchUser()
@@ -203,10 +189,7 @@ export default function OnrampBankPage() {
203189
const handleAmountContinue = () => {
204190
if (!validateAmount(rawTokenAmount)) return
205191

206-
const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus
207-
const isUserKycVerified = currentKycStatus === 'approved'
208-
209-
if (!isUserKycVerified) {
192+
if (!isUserKycApproved) {
210193
setShowKycModal(true)
211194
return
212195
}

src/app/(mobile-ui)/history/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ const HistoryPage = () => {
141141
console.log('KYC status updated via WebSocket:', newStatus)
142142
await fetchUser()
143143
},
144+
onSumsubKycStatusUpdate: async (newStatus: string) => {
145+
console.log('Sumsub KYC status updated via WebSocket:', newStatus)
146+
await fetchUser()
147+
},
144148
})
145149

146150
const allEntries = useMemo(() => historyData?.pages.flatMap((page) => page.entries) ?? [], [historyData])

src/components/AddWithdraw/AddWithdrawCountriesList.tsx

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ import Image, { type StaticImageData } from 'next/image'
1010
import { useParams, useRouter, useSearchParams } from 'next/navigation'
1111
import EmptyState from '../Global/EmptyStates/EmptyState'
1212
import { useAuth } from '@/context/authContext'
13-
import { useEffect, useMemo, useRef, useState } from 'react'
13+
import { useMemo, useRef, useState } from 'react'
1414
import { DynamicBankAccountForm, type IBankAccountDetails } from './DynamicBankAccountForm'
1515
import { addBankAccount } from '@/app/actions/users'
16-
import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils'
1716
import { type AddBankAccountPayload } from '@/app/actions/types/users.types'
18-
import { useWebSocket } from '@/hooks/useWebSocket'
1917
import { useWithdrawFlow } from '@/context/WithdrawFlowContext'
2018
import { type Account } from '@/interfaces'
2119
import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
@@ -64,28 +62,11 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
6462
const [view, setView] = useState<'list' | 'form'>(flow === 'withdraw' && amountToWithdraw ? 'form' : 'list')
6563
const [isKycModalOpen, setIsKycModalOpen] = useState(false)
6664
const formRef = useRef<{ handleSubmit: () => void }>(null)
67-
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(
68-
user?.user?.bridgeKycStatus as BridgeKycStatus
69-
)
7065
const [isSupportedTokensModalOpen, setIsSupportedTokensModalOpen] = useState(false)
7166

72-
const { isUserBridgeKycUnderReview } = useKycStatus()
67+
const { isUserKycApproved, isUserBridgeKycUnderReview } = useKycStatus()
7368
const [showKycStatusModal, setShowKycStatusModal] = useState(false)
7469

75-
useWebSocket({
76-
username: user?.user.username ?? undefined,
77-
autoConnect: !!user?.user.username,
78-
onKycStatusUpdate: (newStatus) => {
79-
setLiveKycStatus(newStatus as BridgeKycStatus)
80-
},
81-
})
82-
83-
useEffect(() => {
84-
if (user?.user.bridgeKycStatus) {
85-
setLiveKycStatus(user.user.bridgeKycStatus as BridgeKycStatus)
86-
}
87-
}, [user?.user.bridgeKycStatus])
88-
8970
const countryPathParts = Array.isArray(params.country) ? params.country : [params.country]
9071
const isBankPage = countryPathParts[countryPathParts.length - 1] === 'bank'
9172
const countrySlugFromUrl = isBankPage ? countryPathParts.slice(0, -1).join('-') : countryPathParts.join('-')
@@ -100,14 +81,12 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => {
10081
): Promise<{ error?: string }> => {
10182
// re-fetch user to ensure we have the latest KYC status
10283
// (the multi-phase flow may have completed but websocket/state not yet propagated)
103-
const freshUser = await fetchUser()
104-
const currentKycStatus = freshUser?.user?.bridgeKycStatus || liveKycStatus || user?.user.bridgeKycStatus
105-
const isUserKycVerified = currentKycStatus === 'approved'
84+
await fetchUser()
10685

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

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

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

src/components/Claim/Link/views/BankFlowManager.view.tsx

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { type IClaimScreenProps } from '../../Claim.consts'
44
import { DynamicBankAccountForm, type IBankAccountDetails } from '@/components/AddWithdraw/DynamicBankAccountForm'
55
import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext'
6-
import { useCallback, useContext, useState, useRef, useEffect } from 'react'
6+
import { useCallback, useContext, useState, useRef } from 'react'
77
import { loadingStateContext } from '@/context'
88
import { createBridgeExternalAccountForGuest } from '@/app/actions/external-accounts'
99
import { confirmOfframp, createOfframp, createOfframpForGuest } from '@/app/actions/offramp'
@@ -25,8 +25,6 @@ import useSavedAccounts from '@/hooks/useSavedAccounts'
2525
import { ConfirmBankClaimView } from './Confirm.bank-claim.view'
2626
import { CountryListRouter } from '@/components/Common/CountryListRouter'
2727
import NavHeader from '@/components/Global/NavHeader'
28-
import { useWebSocket } from '@/hooks/useWebSocket'
29-
import { type BridgeKycStatus } from '@/utils/bridge-accounts.utils'
3028
import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils'
3129
import { useAppDispatch } from '@/redux/hooks'
3230
import { bankFormActions } from '@/redux/slices/bank-form-slice'
@@ -96,28 +94,9 @@ export const BankFlowManager = (props: IClaimScreenProps) => {
9694
const [receiverFullName, setReceiverFullName] = useState<string>('')
9795
const [error, setError] = useState<string | null>(null)
9896
const formRef = useRef<{ handleSubmit: () => void }>(null)
99-
const [liveKycStatus, setLiveKycStatus] = useState<BridgeKycStatus | undefined>(
100-
user?.user?.bridgeKycStatus as BridgeKycStatus
101-
)
10297
const [isProcessingKycSuccess, setIsProcessingKycSuccess] = useState(false)
10398
const [offrampData, setOfframpData] = useState<TCreateOfframpResponse | null>(null)
10499

105-
// websocket for real-time KYC status updates
106-
useWebSocket({
107-
username: user?.user.username ?? undefined,
108-
autoConnect: !!user?.user.username,
109-
onKycStatusUpdate: (newStatus) => {
110-
setLiveKycStatus(newStatus as BridgeKycStatus)
111-
},
112-
})
113-
114-
// effect to update live KYC status from user object
115-
useEffect(() => {
116-
if (user?.user.bridgeKycStatus) {
117-
setLiveKycStatus(user.user.bridgeKycStatus as BridgeKycStatus)
118-
}
119-
}, [user?.user.bridgeKycStatus])
120-
121100
/**
122101
* @name handleConfirmClaim
123102
* @description claims the link to the deposit address provided by the off-ramp api and confirms the transfer.

src/components/Home/HomeHistory.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h
7171
},
7272
[fetchUser]
7373
),
74+
onSumsubKycStatusUpdate: useCallback(
75+
async (newStatus: string) => {
76+
console.log('Sumsub KYC status updated via WebSocket:', newStatus)
77+
await fetchUser()
78+
},
79+
[fetchUser]
80+
),
7481
})
7582

7683
// Combine fetched history with real-time updates

src/components/IdentityVerification/StartVerificationModal.tsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,42 @@ import { Icon } from '../Global/Icons/Icon'
66
import { type Region } from '@/hooks/useIdentityVerification'
77
import React from 'react'
88

9+
const QR_PAYMENTS = (
10+
<p key="qr">
11+
QR Payments in <b>Argentina and Brazil</b>
12+
</p>
13+
)
14+
15+
const BRIDGE_UNLOCK_ITEMS: Array<string | React.ReactNode> = [
16+
<p key="sepa">
17+
<b>Europe</b> SEPA transfers (+30 countries)
18+
</p>,
19+
<p key="uk">
20+
<b>UK</b> Faster payment transfers
21+
</p>,
22+
<p key="ach">
23+
<b>United States</b> ACH and Wire transfers
24+
</p>,
25+
<p key="mx">
26+
<b>Mexico</b> SPEI transfers
27+
</p>,
28+
QR_PAYMENTS,
29+
]
30+
931
// unlock benefits shown per region
1032
const REGION_UNLOCK_ITEMS: Record<string, Array<string | React.ReactNode>> = {
1133
latam: [
1234
<p key="bank">
1335
Bank transfers to your own accounts in <b>LATAM</b>
1436
</p>,
15-
<p key="qr">
16-
QR Payments in <b>Argentina and Brazil</b>
17-
</p>,
18-
],
19-
europe: [
20-
<p key="sepa">
21-
<b>Europe</b> SEPA transfers (+30 countries)
22-
</p>,
23-
<p key="qr">
24-
QR Payments in <b>Argentina and Brazil</b>
25-
</p>,
26-
],
27-
'north-america': [
28-
<p key="ach">
29-
<b>United States</b> ACH and Wire transfers
30-
</p>,
31-
<p key="mx">
32-
<b>Mexico</b> SPEI transfers
33-
</p>,
34-
<p key="qr">
35-
QR Payments in <b>Argentina and Brazil</b>
36-
</p>,
37-
],
38-
'rest-of-the-world': [
39-
<p key="qr">
40-
QR Payments in <b>Argentina and Brazil</b>
41-
</p>,
37+
QR_PAYMENTS,
4238
],
39+
40+
europe: BRIDGE_UNLOCK_ITEMS,
41+
42+
'north-america': BRIDGE_UNLOCK_ITEMS,
43+
44+
'rest-of-the-world': [QR_PAYMENTS],
4345
}
4446

4547
const DEFAULT_UNLOCK_ITEMS = [<p key="bank">Bank transfers and local payment methods</p>]

src/components/Kyc/CountryFlagAndName.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export const CountryFlagAndName = ({ countryCode, isBridgeRegion }: CountryFlagA
1616
icons={[
1717
'https://flagcdn.com/w160/us.png',
1818
'https://flagcdn.com/w160/eu.png',
19-
'https://flagcdn.com/w160/mx.png',
2019
'https://flagcdn.com/w160/gb.png',
20+
'https://flagcdn.com/w160/mx.png',
2121
]}
2222
iconSize={80}
2323
imageClassName="h-5 w-5 min-h-5 min-w-5 rounded-full object-cover object-center shadow-sm"
@@ -32,7 +32,7 @@ export const CountryFlagAndName = ({ countryCode, isBridgeRegion }: CountryFlagA
3232
loading="lazy"
3333
/>
3434
)}
35-
{isBridgeRegion ? 'US/EU/MX/UK' : countryName}
35+
{isBridgeRegion ? 'US/EU/UK/MX' : countryName}
3636
</div>
3737
)
3838
}

src/components/Kyc/KycStatusItem.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ export const KycStatusItem = ({
7171
const finalBridgeKycStatus = wsBridgeKycStatus || bridgeKycStatus || user?.user?.bridgeKycStatus
7272
const kycStatus = verification ? verification.status : finalBridgeKycStatus
7373

74+
// check if any bridge rail needs additional documents
75+
const hasBridgeDocsNeeded = useMemo(
76+
() =>
77+
(user?.rails ?? []).some(
78+
(r) => r.status === 'REQUIRES_EXTRA_INFORMATION' && r.rail.provider.code === 'BRIDGE'
79+
),
80+
[user?.rails]
81+
)
82+
7483
const isApproved = isKycStatusApproved(kycStatus)
7584
const isPending = isKycStatusPending(kycStatus)
7685
const isRejected = isKycStatusFailed(kycStatus)
@@ -80,13 +89,14 @@ export const KycStatusItem = ({
8089
const isInitiatedButNotStarted = !!verification && isKycStatusNotStarted(kycStatus)
8190

8291
const subtitle = useMemo(() => {
92+
if (hasBridgeDocsNeeded) return 'Action needed'
8393
if (isInitiatedButNotStarted) return 'Not completed'
8494
if (isActionRequired) return 'Action needed'
8595
if (isPending) return 'Processing'
86-
if (isApproved) return 'Completed'
96+
if (isApproved) return 'Verified'
8797
if (isRejected) return 'Failed'
8898
return 'Unknown'
89-
}, [isInitiatedButNotStarted, isActionRequired, isPending, isApproved, isRejected])
99+
}, [hasBridgeDocsNeeded, isInitiatedButNotStarted, isActionRequired, isPending, isApproved, isRejected])
90100

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

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

@@ -117,7 +127,7 @@ export const KycStatusItem = ({
117127
<p className="text-sm text-grey-1">{subtitle}</p>
118128
<StatusPill
119129
status={
120-
isInitiatedButNotStarted || isActionRequired || isPending
130+
hasBridgeDocsNeeded || isInitiatedButNotStarted || isActionRequired || isPending
121131
? 'pending'
122132
: isRejected
123133
? 'cancelled'

src/components/Kyc/KycVerificationInProgressModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ export const KycVerificationInProgressModal = ({
7373
onClose={onClose}
7474
isLoadingIcon
7575
iconContainerClassName="bg-yellow-1 text-black"
76-
title="Identity verified!"
76+
title="Verification in progress"
7777
description={
7878
preparingTimedOut
7979
? "This is taking longer than expected. You can continue and we'll notify you when it's ready."
80-
: 'Preparing your account...'
80+
: 'Submitting your information and preparing your account. This usually takes less than a minute.'
8181
}
8282
ctas={
8383
preparingTimedOut

src/components/Kyc/SumsubKycWrapper.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export const SumsubKycWrapper = ({
9090
}
9191

9292
try {
93+
// track sdk init time so we can ignore stale onApplicantStatusChanged events
94+
// that fire immediately when the applicant is already approved (e.g. additional-docs flow)
95+
const sdkInitTime = Date.now()
96+
9397
const handleSubmitted = () => {
9498
console.log('[sumsub] onApplicantSubmitted fired')
9599
stableOnComplete()
@@ -103,6 +107,12 @@ export const SumsubKycWrapper = ({
103107
reviewResult?: { reviewAnswer?: string }
104108
}) => {
105109
console.log('[sumsub] onApplicantStatusChanged fired', payload)
110+
// ignore status events that fire within 3s of sdk init — these reflect
111+
// pre-existing state (e.g. user already approved), not a new submission
112+
if (Date.now() - sdkInitTime < 3000) {
113+
console.log('[sumsub] ignoring early onApplicantStatusChanged (pre-existing state)')
114+
return
115+
}
106116
// auto-close when sumsub shows success screen
107117
if (payload?.reviewStatus === 'completed' && payload?.reviewResult?.reviewAnswer === 'GREEN') {
108118
stableOnComplete()

0 commit comments

Comments
 (0)