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
4 changes: 3 additions & 1 deletion src/app/actions/sumsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ const API_KEY = process.env.PEANUT_API_KEY!
export const initiateSumsubKyc = async (params?: {
regionIntent?: KYCRegionIntent
levelName?: string
crossRegion?: boolean
}): Promise<{ data?: InitiateSumsubKycResponse; error?: string }> => {
const jwtToken = (await getJWTCookie())?.value

if (!jwtToken) {
return { error: 'Authentication required' }
}

const body: Record<string, string | undefined> = {
const body: Record<string, string | boolean | undefined> = {
regionIntent: params?.regionIntent,
levelName: params?.levelName,
crossRegion: params?.crossRegion,
}

try {
Expand Down
9 changes: 8 additions & 1 deletion src/app/actions/types/sumsub.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ export interface InitiateSumsubKycResponse {
status: SumsubKycStatus
}

export type SumsubKycStatus = 'NOT_STARTED' | 'PENDING' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'ACTION_REQUIRED'
export type SumsubKycStatus =
| 'NOT_STARTED'
| 'PENDING'
| 'IN_REVIEW'
| 'APPROVED'
| 'REJECTED'
| 'ACTION_REQUIRED'
| 'REVERIFYING'

export type KYCRegionIntent = 'STANDARD' | 'LATAM'
7 changes: 5 additions & 2 deletions src/components/Profile/views/RegionsVerification.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,12 @@ const RegionsVerification = () => {
const handleStartKyc = useCallback(async () => {
const intent = selectedRegion ? getRegionIntent(selectedRegion.path) : undefined
if (intent) setActiveRegionIntent(intent)
// only signal cross-region when user is switching to a different region
const crossRegion =
sumsubVerificationRegionIntent && intent && intent !== sumsubVerificationRegionIntent ? true : undefined
setSelectedRegion(null)
await flow.handleInitiateKyc(intent)
}, [flow.handleInitiateKyc, selectedRegion])
await flow.handleInitiateKyc(intent, undefined, crossRegion)
}, [flow.handleInitiateKyc, selectedRegion, sumsubVerificationRegionIntent])

// re-submission: skip StartVerificationView since user already consented
const handleResubmitKyc = useCallback(async () => {
Expand Down
4 changes: 3 additions & 1 deletion src/constants/kyc.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export type KycVerificationStatus = MantecaKycStatus | SumsubKycStatus | string
export type KycStatusCategory = 'completed' | 'processing' | 'failed' | 'action_required'

// sets of status values by category — single source of truth
const APPROVED_STATUSES: ReadonlySet<string> = new Set(['approved', 'ACTIVE', 'APPROVED'])
// REVERIFYING = user is approved but re-verifying for a new region (cross-region KYC).
// treated as approved for access checks — user retains existing provider access.
const APPROVED_STATUSES: ReadonlySet<string> = new Set(['approved', 'ACTIVE', 'APPROVED', 'REVERIFYING'])
Comment on lines +13 to +15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check all usages of getKycStatusCategory to assess impact of REVERIFYING returning 'completed'
echo "=== getKycStatusCategory usages ==="
rg -n -C3 'getKycStatusCategory'

echo ""
echo "=== isKycStatusApproved usages ==="
rg -n -C3 'isKycStatusApproved'

echo ""
echo "=== isUserKycVerified usages ==="
rg -n -C3 'isUserKycVerified'

Repository: peanutprotocol/peanut-ui

Length of output: 17219


🏁 Script executed:

# Check the current implementation of getKycStatusCategory and surrounding constants
cat -n src/constants/kyc.consts.ts | head -90

Repository: peanutprotocol/peanut-ui

Length of output: 4364


🏁 Script executed:

# Check KycStatusDrawer.tsx around the lines mentioned in the review (39-60, 131-137)
cat -n src/components/Kyc/KycStatusDrawer.tsx | head -150

Repository: peanutprotocol/peanut-ui

Length of output: 7521


🏁 Script executed:

# Check if REVERIFYING is already handled specially anywhere
rg -n 'REVERIFYING' --type ts --type tsx

Repository: peanutprotocol/peanut-ui

Length of output: 95


🏁 Script executed:

# Search for REVERIFYING references without type filter
rg -n 'REVERIFYING'

Repository: peanutprotocol/peanut-ui

Length of output: 384


🏁 Script executed:

# Check if there's any conditional handling for REVERIFYING in the codebase
rg -n -B2 -A2 "statusCategory.*completed"

Repository: peanutprotocol/peanut-ui

Length of output: 526


REVERIFYING in APPROVED_STATUSES breaks cross-region KYC flows.

Adding REVERIFYING to APPROVED_STATUSES causes getKycStatusCategory() to return 'completed' for users actively re-verifying for a new region. This creates two problems:

  1. KycStatusDrawer prevents KYC initialization: Line 60 sets regionIntent = undefined when statusCategory === 'completed', blocking initiateSumsubKyc() from being called for REVERIFYING users who need to complete cross-region verification.

  2. Wrong UI shown to users: REVERIFYING users see the KycCompleted component (lines 131–139), displaying "verification complete" while they're still actively verifying.

Consider separating access-check logic from UI categorization:

Suggested fix: Handle REVERIFYING as 'processing' in getKycStatusCategory
 export const getKycStatusCategory = (status: string): KycStatusCategory => {
+    // REVERIFYING users are approved but actively re-verifying for a new region
+    if (status === 'REVERIFYING') return 'processing'
     if (APPROVED_STATUSES.has(status)) return 'completed'
     if (FAILED_STATUSES.has(status)) return 'failed'
     if (ACTION_REQUIRED_STATUSES.has(status)) return 'action_required'
     return 'processing'
 }

This allows access checks (isKycStatusApproved, isUserKycVerified) to return true for REVERIFYING users while showing appropriate processing UI for the cross-region flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/constants/kyc.consts.ts` around lines 13 - 15, APPROVED_STATUSES
currently includes 'REVERIFYING', which causes getKycStatusCategory() to
classify re-verifying users as 'completed' and breaks the cross-region flow;
remove 'REVERIFYING' from the APPROVED_STATUSES set in
src/constants/kyc.consts.ts and instead update getKycStatusCategory() to treat
'REVERIFYING' as 'processing' so UI components (KycStatusDrawer, KycCompleted)
render the processing state while keeping access-check helpers
(isKycStatusApproved, isUserKycVerified) unchanged if they must still treat
REVERIFYING as approved for access; ensure regionIntent is not cleared for
REVERIFYING users so initiateSumsubKyc() can run.

const FAILED_STATUSES: ReadonlySet<string> = new Set(['rejected', 'INACTIVE', 'REJECTED'])
const PENDING_STATUSES: ReadonlySet<string> = new Set([
'under_review',
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useMultiPhaseKycFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export const useMultiPhaseKycFlow = ({ onKycSuccess, onManualClose, regionIntent

// wrap handleInitiateKyc to reset state for new attempts
const handleInitiateKyc = useCallback(
async (overrideIntent?: KYCRegionIntent, levelName?: string) => {
async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean) => {
const intent = overrideIntent ?? regionIntent
posthog.capture(
intent === 'LATAM' ? ANALYTICS_EVENTS.MANTECA_KYC_INITIATED : ANALYTICS_EVENTS.KYC_INITIATED,
Expand All @@ -192,7 +192,7 @@ export const useMultiPhaseKycFlow = ({ onKycSuccess, onManualClose, regionIntent
isRealtimeFlowRef.current = false
clearPreparingTimer()

await originalHandleInitiateKyc(overrideIntent, levelName)
await originalHandleInitiateKyc(overrideIntent, levelName, crossRegion)
},
[originalHandleInitiateKyc, clearPreparingTimer, regionIntent, acquisitionSource]
)
Expand Down
15 changes: 9 additions & 6 deletions src/hooks/useSumsubKycFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
liveKycStatus &&
liveKycStatus !== prevStatus &&
liveKycStatus !== 'APPROVED' &&
liveKycStatus !== 'PENDING'
liveKycStatus !== 'PENDING' &&
liveKycStatus !== 'REVERIFYING'
) {
// close modal for any non-success terminal state (REJECTED, ACTION_REQUIRED, FAILED, etc.)
setIsVerificationProgressModalOpen(false)
Expand Down Expand Up @@ -119,7 +120,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
}, [isVerificationProgressModalOpen])

const handleInitiateKyc = useCallback(
async (overrideIntent?: KYCRegionIntent, levelName?: string) => {
async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean) => {
userInitiatedRef.current = true
setIsLoading(true)
setError(null)
Expand All @@ -128,6 +129,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
const response = await initiateSumsubKyc({
regionIntent: overrideIntent ?? regionIntent,
levelName,
crossRegion,
})

if (response.error) {
Expand All @@ -148,11 +150,12 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }:
if (effectiveIntent) regionIntentRef.current = effectiveIntent
levelNameRef.current = levelName

// if already approved and no token returned, kyc is done.
// if already approved (or reverifying) and no token returned, kyc is done.
// set prevStatusRef so the transition effect doesn't fire onKycSuccess a second time.
// when a token IS returned (e.g. additional-docs flow), we still need to show the SDK.
if (response.data?.status === 'APPROVED' && !response.data?.token) {
prevStatusRef.current = 'APPROVED'
// when a token IS returned (e.g. cross-region or additional-docs flow), we still need to show the SDK.
const status = response.data?.status
if ((status === 'APPROVED' || status === 'REVERIFYING') && !response.data?.token) {
prevStatusRef.current = status
onKycSuccess?.()
return
}
Expand Down
Loading