Skip to content
Open
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'])
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