From 9bc513b19fe1b17dfd6980731eab65f4b6014330 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:04:53 +0530 Subject: [PATCH 01/14] fix: thread crossRegion flag for explicit cross-region KYC requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pairs with backend fix that moves rail enrollment to webhook time. cross-region moveToLevel now requires explicit crossRegion=true flag. only set when user clicks "Verify now" on a different region — automatic calls (fetchCurrentStatus, polling) never set it. --- src/app/actions/sumsub.ts | 4 +++- src/components/Profile/views/RegionsVerification.view.tsx | 6 ++++-- src/hooks/useMultiPhaseKycFlow.ts | 4 ++-- src/hooks/useSumsubKycFlow.ts | 3 ++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app/actions/sumsub.ts b/src/app/actions/sumsub.ts index 7222004c6..ebc0769ba 100644 --- a/src/app/actions/sumsub.ts +++ b/src/app/actions/sumsub.ts @@ -11,6 +11,7 @@ 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 @@ -18,9 +19,10 @@ export const initiateSumsubKyc = async (params?: { return { error: 'Authentication required' } } - const body: Record = { + const body: Record = { regionIntent: params?.regionIntent, levelName: params?.levelName, + crossRegion: params?.crossRegion, } try { diff --git a/src/components/Profile/views/RegionsVerification.view.tsx b/src/components/Profile/views/RegionsVerification.view.tsx index fdd051eb5..332c710fa 100644 --- a/src/components/Profile/views/RegionsVerification.view.tsx +++ b/src/components/Profile/views/RegionsVerification.view.tsx @@ -100,9 +100,11 @@ const RegionsVerification = () => { const handleStartKyc = useCallback(async () => { const intent = selectedRegion ? getRegionIntent(selectedRegion.path) : undefined if (intent) setActiveRegionIntent(intent) + // detect cross-region: user is approved for a different region and wants to unlock a new one + const isCrossRegion = !!sumsubVerificationRegionIntent && !!intent && intent !== sumsubVerificationRegionIntent setSelectedRegion(null) - await flow.handleInitiateKyc(intent) - }, [flow.handleInitiateKyc, selectedRegion]) + await flow.handleInitiateKyc(intent, undefined, isCrossRegion) + }, [flow.handleInitiateKyc, selectedRegion, sumsubVerificationRegionIntent]) // re-submission: skip StartVerificationView since user already consented const handleResubmitKyc = useCallback(async () => { diff --git a/src/hooks/useMultiPhaseKycFlow.ts b/src/hooks/useMultiPhaseKycFlow.ts index 6ec2f520a..c6cc004f2 100644 --- a/src/hooks/useMultiPhaseKycFlow.ts +++ b/src/hooks/useMultiPhaseKycFlow.ts @@ -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, @@ -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] ) diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts index 47566ba0b..e6c995f98 100644 --- a/src/hooks/useSumsubKycFlow.ts +++ b/src/hooks/useSumsubKycFlow.ts @@ -119,7 +119,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) @@ -128,6 +128,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: const response = await initiateSumsubKyc({ regionIntent: overrideIntent ?? regionIntent, levelName, + crossRegion, }) if (response.error) { From f9f056bae2ba6dfc1fcbc98bb7f4f60d63e9334e Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:47:06 +0530 Subject: [PATCH 02/14] fix: only send crossRegion flag when true, omit for same-region --- src/components/Profile/views/RegionsVerification.view.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Profile/views/RegionsVerification.view.tsx b/src/components/Profile/views/RegionsVerification.view.tsx index 332c710fa..9711d0483 100644 --- a/src/components/Profile/views/RegionsVerification.view.tsx +++ b/src/components/Profile/views/RegionsVerification.view.tsx @@ -100,10 +100,11 @@ const RegionsVerification = () => { const handleStartKyc = useCallback(async () => { const intent = selectedRegion ? getRegionIntent(selectedRegion.path) : undefined if (intent) setActiveRegionIntent(intent) - // detect cross-region: user is approved for a different region and wants to unlock a new one - const isCrossRegion = !!sumsubVerificationRegionIntent && !!intent && intent !== sumsubVerificationRegionIntent + // 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, undefined, isCrossRegion) + await flow.handleInitiateKyc(intent, undefined, crossRegion) }, [flow.handleInitiateKyc, selectedRegion, sumsubVerificationRegionIntent]) // re-submission: skip StartVerificationView since user already consented From 3a38e9f056424fd157392501d804365ff43d00da Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:06:35 +0530 Subject: [PATCH 03/14] feat: add REVERIFYING to approved statuses and SumsubKycStatus type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REVERIFYING is treated as approved for all access checks — user retains existing provider access while re-verifying for a new region. added to APPROVED_STATUSES set (single source of truth) so all downstream readers (useKycStatus, useQrKycGate, useIdentityVerification, KycStatusItem, etc.) automatically handle it correctly. --- src/app/actions/types/sumsub.types.ts | 9 ++++++++- src/constants/kyc.consts.ts | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/actions/types/sumsub.types.ts b/src/app/actions/types/sumsub.types.ts index 8565d2961..f7c2cd192 100644 --- a/src/app/actions/types/sumsub.types.ts +++ b/src/app/actions/types/sumsub.types.ts @@ -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' diff --git a/src/constants/kyc.consts.ts b/src/constants/kyc.consts.ts index d35557a54..308548ae3 100644 --- a/src/constants/kyc.consts.ts +++ b/src/constants/kyc.consts.ts @@ -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 = 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 = new Set(['approved', 'ACTIVE', 'APPROVED', 'REVERIFYING']) const FAILED_STATUSES: ReadonlySet = new Set(['rejected', 'INACTIVE', 'REJECTED']) const PENDING_STATUSES: ReadonlySet = new Set([ 'under_review', From 2834cc60494c9d87018d84a95ac99e8a4621de3c Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:43:05 +0530 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20REV?= =?UTF-8?q?ERIFYING=20exclusion=20in=20status=20effect=20+=20token-less=20?= =?UTF-8?q?handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. add REVERIFYING to exclusion list in status-transition effect so it doesn't close the progress modal if received via websocket during SDK completion 2. handle REVERIFYING + no token same as APPROVED — call onKycSuccess instead of showing misleading "Could not initiate verification" error --- src/hooks/useSumsubKycFlow.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts index e6c995f98..e02954b74 100644 --- a/src/hooks/useSumsubKycFlow.ts +++ b/src/hooks/useSumsubKycFlow.ts @@ -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) @@ -149,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 } From 1746c1430a0d999c8e9ddbcf5f3247dccc79c4d0 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Thu, 9 Apr 2026 20:23:30 +0100 Subject: [PATCH 05/14] fix: instrument reward analytics on QR payment page The 4 reward/surprise PostHog events (surprise_moment_shown, reward_claim_shown, reward_claimed, reward_claim_dismissed) were instrumented on PerkClaimModal but that component only renders for "Card Pioneer" perks. The actual surprise moment perks (Rewards v2 - Surprise 35c/65c) are claimed inline on the QR payment success screen, which had zero PostHog instrumentation. Added: - REWARD_CLAIM_SHOWN + SURPRISE_MOMENT_SHOWN when perk UI appears after successful QR payment (useEffect with hasTrackedShow ref) - REWARD_CLAIMED after successful mantecaApi.claimPerk() - REWARD_CLAIM_DISMISSED on unmount if shown but not claimed These events unblock KR2 measurement for Rewards v2. --- src/app/(mobile-ui)/qr-pay/page.tsx | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index e4b7661c4..a8086f421 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -34,6 +34,8 @@ import { loadingStateContext } from '@/context' import { getCurrencyPrice } from '@/app/actions/currency' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { captureException } from '@sentry/nextjs' +import posthog from 'posthog-js' +import { ANALYTICS_EVENTS } from '@/constants/analytics.consts' import { isPaymentProcessorQR, parseSimpleFiQr, @@ -177,6 +179,35 @@ export default function QRPayPage() { } }, []) + // Track reward claim shown + surprise moment when perk UI appears after payment + const hasTrackedPerkShown = useRef(false) + const perkClaimedRef = useRef(false) + useEffect(() => { + perkClaimedRef.current = perkClaimed + }, [perkClaimed]) + + useEffect(() => { + if (isSuccess && qrPayment?.perk?.eligible && !perkClaimed && !hasTrackedPerkShown.current) { + hasTrackedPerkShown.current = true + const eventProps = { + amount_usd: qrPayment.perk.amountSponsored, + discount_pct: qrPayment.perk.discountPercentage, + merchant: qrPayment.details?.merchant?.name, + } + posthog.capture(ANALYTICS_EVENTS.REWARD_CLAIM_SHOWN, eventProps) + posthog.capture(ANALYTICS_EVENTS.SURPRISE_MOMENT_SHOWN, eventProps) + } + }, [isSuccess, qrPayment?.perk?.eligible, perkClaimed, qrPayment]) + + // Track dismiss: user navigated away after seeing perk but without claiming + useEffect(() => { + return () => { + if (hasTrackedPerkShown.current && !perkClaimedRef.current) { + posthog.capture(ANALYTICS_EVENTS.REWARD_CLAIM_DISMISSED) + } + } + }, []) + const handleSimpleFiStatusUpdate = useCallback( async (entry: HistoryEntry) => { if (!pendingSimpleFiPaymentId || entry.uuid !== pendingSimpleFiPaymentId) { @@ -784,6 +815,10 @@ export default function QRPayPage() { try { const result = await mantecaApi.claimPerk(qrPayment.externalId) if (result.success) { + posthog.capture(ANALYTICS_EVENTS.REWARD_CLAIMED, { + amount_usd: result.perk.amountSponsored, + discount_pct: result.perk.discountPercentage, + }) // Update qrPayment with actual claimed perk info from backend setQrPayment({ ...qrPayment, From de7136f38a4b62cd8f01bfcdf4fea202b2156458 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Thu, 9 Apr 2026 20:32:42 +0100 Subject: [PATCH 06/14] fix: reset analytics refs between QR payment flows CodeRabbit caught that hasTrackedPerkShown and perkClaimedRef survive resetState(), so a second QR payment in the same session would skip analytics tracking. Move refs before resetState and clear them there. --- src/app/(mobile-ui)/qr-pay/page.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index a8086f421..03b68896b 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -136,6 +136,10 @@ export default function QRPayPage() { const [waitingForMerchantAmount, setWaitingForMerchantAmount] = useState(false) const retryCount = useRef(0) + // Analytics tracking refs (declared before resetState so it can clear them) + const hasTrackedPerkShown = useRef(false) + const perkClaimedRef = useRef(false) + const resetState = () => { setIsSuccess(false) setErrorMessage(null) @@ -167,6 +171,9 @@ export default function QRPayPage() { // reset perk states setIsClaimingPerk(false) setPerkClaimed(false) + // reset analytics tracking refs so a new QR flow gets fresh tracking + hasTrackedPerkShown.current = false + perkClaimedRef.current = false } // Cleanup timers on unmount @@ -180,8 +187,6 @@ export default function QRPayPage() { }, []) // Track reward claim shown + surprise moment when perk UI appears after payment - const hasTrackedPerkShown = useRef(false) - const perkClaimedRef = useRef(false) useEffect(() => { perkClaimedRef.current = perkClaimed }, [perkClaimed]) From 1c3f8e072952fea56cb7c8296c309f78b0a72ac8 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:16:03 +0530 Subject: [PATCH 07/14] feat: handle applicant action responses for cross-region KYC - add actionType to InitiateSumsubKycResponse type - handle bridge-direct: skip SDK, show preparing modal, backend handles submission - handle manteca action: open SDK with action token (questionnaire only) - add isActionFlow flag to disable multi-level for action flows (cross-region LATAM uses action, not multi-level workflow) pairs with backend feat/applicant-actions branch. --- src/app/actions/sumsub.ts | 1 + src/app/actions/types/sumsub.types.ts | 5 ++++- src/assets/animations | 2 +- src/hooks/useMultiPhaseKycFlow.ts | 5 ++++- src/hooks/useSumsubKycFlow.ts | 16 +++++++++++++++- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/app/actions/sumsub.ts b/src/app/actions/sumsub.ts index ebc0769ba..6e1a2eea0 100644 --- a/src/app/actions/sumsub.ts +++ b/src/app/actions/sumsub.ts @@ -47,6 +47,7 @@ export const initiateSumsubKyc = async (params?: { token: responseJson.token, applicantId: responseJson.applicantId, status: responseJson.status, + actionType: responseJson.actionType, }, } } catch (e: unknown) { diff --git a/src/app/actions/types/sumsub.types.ts b/src/app/actions/types/sumsub.types.ts index f7c2cd192..fa8b91d5f 100644 --- a/src/app/actions/types/sumsub.types.ts +++ b/src/app/actions/types/sumsub.types.ts @@ -1,7 +1,10 @@ +export type KycActionType = 'manteca' | 'bridge-direct' + export interface InitiateSumsubKycResponse { - token: string | null // null when user is already APPROVED + token: string | null // null when user is already APPROVED or bridge-direct applicantId: string | null status: SumsubKycStatus + actionType?: KycActionType // present for cross-region responses } export type SumsubKycStatus = diff --git a/src/assets/animations b/src/assets/animations index d475d960d..03ded53de 160000 --- a/src/assets/animations +++ b/src/assets/animations @@ -1 +1 @@ -Subproject commit d475d960d16a6009c2db3c5e35ef9aa31179ebfd +Subproject commit 03ded53de32d81ff1b9982a84557119cdbab724f diff --git a/src/hooks/useMultiPhaseKycFlow.ts b/src/hooks/useMultiPhaseKycFlow.ts index c6cc004f2..953ae58ae 100644 --- a/src/hooks/useMultiPhaseKycFlow.ts +++ b/src/hooks/useMultiPhaseKycFlow.ts @@ -148,6 +148,7 @@ export const useMultiPhaseKycFlow = ({ onKycSuccess, onManualClose, regionIntent refreshToken, isVerificationProgressModalOpen, closeVerificationProgressModal, + isActionFlow, } = useSumsubKycFlow({ onKycSuccess: handleSumsubApproved, onManualClose, regionIntent }) // keep ref in sync @@ -306,7 +307,9 @@ export const useMultiPhaseKycFlow = ({ onKycSuccess, onManualClose, regionIntent const isModalOpen = isVerificationProgressModalOpen || forceShowModal - const isMultiLevel = regionIntent === 'LATAM' + // multi-level only for first-time LATAM (workflow with conditional questionnaire). + // cross-region LATAM uses an applicant action (single level, not multi-level). + const isMultiLevel = regionIntent === 'LATAM' && !isActionFlow // Derive preparing stage from elapsed time for progressive copy const preparingStage = useMemo<'initial' | 'configuring' | 'slow'>(() => { diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts index e02954b74..d518a7851 100644 --- a/src/hooks/useSumsubKycFlow.ts +++ b/src/hooks/useSumsubKycFlow.ts @@ -22,6 +22,8 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: const [isVerificationProgressModalOpen, setIsVerificationProgressModalOpen] = useState(false) const [liveKycStatus, setLiveKycStatus] = useState(undefined) const [rejectLabels, setRejectLabels] = useState(undefined) + // true when the SDK is showing an applicant action (not a standard level) + const [isActionFlow, setIsActionFlow] = useState(false) const prevStatusRef = useRef(liveKycStatus) const showWrapperRef = useRef(showWrapper) showWrapperRef.current = showWrapper @@ -150,9 +152,19 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: if (effectiveIntent) regionIntentRef.current = effectiveIntent levelNameRef.current = levelName + // cross-region: bridge-direct means no SDK needed — backend is handling + // rail enrollment + submission. go straight to the post-approval flow. + if (response.data?.actionType === 'bridge-direct') { + prevStatusRef.current = 'APPROVED' + userInitiatedRef.current = true + setIsVerificationProgressModalOpen(true) + onKycSuccess?.() + return + } + // 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. cross-region or additional-docs flow), we still need to show the SDK. + // when a token IS returned (e.g. cross-region action or additional-docs), we still need to show the SDK. const status = response.data?.status if ((status === 'APPROVED' || status === 'REVERIFYING') && !response.data?.token) { prevStatusRef.current = status @@ -162,6 +174,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: if (response.data?.token) { setAccessToken(response.data.token) + setIsActionFlow(!!response.data.actionType) setShowWrapper(true) } else { setError('Could not initiate verification. Please try again.') @@ -233,5 +246,6 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: closeVerificationProgressModal, closeVerificationModalAndGoHome, resetError, + isActionFlow, } } From fa8e13d49048ba6c14402fba78f568ad283f9b9d Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:01:18 +0530 Subject: [PATCH 08/14] fix: reset isActionFlow in handleSdkComplete and handleClose --- src/hooks/useSumsubKycFlow.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts index d518a7851..62eb661de 100644 --- a/src/hooks/useSumsubKycFlow.ts +++ b/src/hooks/useSumsubKycFlow.ts @@ -193,12 +193,14 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: const handleSdkComplete = useCallback(() => { userInitiatedRef.current = true setShowWrapper(false) + setIsActionFlow(false) setIsVerificationProgressModalOpen(true) }, []) // called when user manually closes the sdk modal const handleClose = useCallback(() => { setShowWrapper(false) + setIsActionFlow(false) onManualClose?.() }, [onManualClose]) From d9898aa9d8a54d1429396e5662667ce1a9a8e9c6 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Sun, 12 Apr 2026 23:51:10 +0100 Subject: [PATCH 09/14] feat(analytics): detect Brave browser and set PostHog person property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brave identifies as Chrome in its User-Agent string, causing PostHog to misclassify Brave users. Given our crypto-native audience, Brave usage is likely 5-15% of traffic — currently invisible in analytics. Uses navigator.brave.isBrave() API to detect Brave and sets a browser_override person property in PostHog for accurate breakdowns. --- instrumentation-client.ts | 10 ++++++++++ src/types/global.d.ts | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 4f3c5e367..5082309fb 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -9,4 +9,14 @@ if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'development') { capture_pageleave: true, autocapture: true, }) + + // Brave identifies as Chrome in User-Agent — detect it and set a person property + // so we can accurately measure our crypto-native Brave audience in PostHog + if (navigator.brave) { + navigator.brave.isBrave().then((isBrave) => { + if (isBrave) { + posthog.setPersonProperties({ browser_override: 'Brave' }) + } + }) + } } diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 9b6bf210b..0462983a0 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -4,3 +4,9 @@ interface Window { CRISP_TOKEN_ID?: string | null CRISP_WEBSITE_ID?: string } + +interface Navigator { + brave?: { + isBrave: () => Promise + } +} From edd7a8ec45e13d24330650e820e522051c1cb589 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:58:42 +0530 Subject: [PATCH 10/14] fix: reset submodule pointer, reset isActionFlow in bridge-direct path - revert animations submodule to main (accidental change from worktree init) - reset isActionFlow in bridge-direct early-return to prevent stale state --- src/assets/animations | 2 +- src/hooks/useSumsubKycFlow.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/assets/animations b/src/assets/animations index 03ded53de..d475d960d 160000 --- a/src/assets/animations +++ b/src/assets/animations @@ -1 +1 @@ -Subproject commit 03ded53de32d81ff1b9982a84557119cdbab724f +Subproject commit d475d960d16a6009c2db3c5e35ef9aa31179ebfd diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts index 62eb661de..7eaf83c8a 100644 --- a/src/hooks/useSumsubKycFlow.ts +++ b/src/hooks/useSumsubKycFlow.ts @@ -157,6 +157,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: if (response.data?.actionType === 'bridge-direct') { prevStatusRef.current = 'APPROVED' userInitiatedRef.current = true + setIsActionFlow(false) setIsVerificationProgressModalOpen(true) onKycSuccess?.() return From 3bdcd642431bad2a92a7b0742e1ecf6ae8cfd500 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:53:25 +0530 Subject: [PATCH 11/14] fix: prevent fetchCurrentStatus race closing SDK on cross-region initiation --- src/hooks/useSumsubKycFlow.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts index 7eaf83c8a..71e072f46 100644 --- a/src/hooks/useSumsubKycFlow.ts +++ b/src/hooks/useSumsubKycFlow.ts @@ -127,6 +127,13 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: setIsLoading(true) setError(null) + // for cross-region: pre-set prevStatusRef to APPROVED so the fetchCurrentStatus + // effect (which also fires when regionIntent changes) doesn't trigger onKycSuccess + // when it sees the existing APPROVED status. + if (crossRegion) { + prevStatusRef.current = 'APPROVED' + } + try { const response = await initiateSumsubKyc({ regionIntent: overrideIntent ?? regionIntent, From cfa77ce2581bd771a4c261bbedfcc5c0f825acfe Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:57:26 +0530 Subject: [PATCH 12/14] fix: guard fetchCurrentStatus with initiatingRef to prevent race on cross-region --- src/hooks/useSumsubKycFlow.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSumsubKycFlow.ts b/src/hooks/useSumsubKycFlow.ts index 71e072f46..d92709a2e 100644 --- a/src/hooks/useSumsubKycFlow.ts +++ b/src/hooks/useSumsubKycFlow.ts @@ -31,6 +31,8 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: const regionIntentRef = useRef(regionIntent) // tracks the level name across initiate + refresh (e.g. 'peanut-additional-docs') const levelNameRef = useRef(undefined) + // guards fetchCurrentStatus from running while handleInitiateKyc is in progress + const initiatingRef = useRef(false) // guard: only fire onKycSuccess when the user initiated a kyc flow in this session. // prevents stale websocket events or mount-time fetches from auto-closing the drawer. const userInitiatedRef = useRef(false) @@ -82,11 +84,13 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: // (e.g. RegionsVerification mounts with no region selected yet). useEffect(() => { if (!regionIntent) return + // skip if handleInitiateKyc is already in progress — it handles status sync itself + if (initiatingRef.current) return const fetchCurrentStatus = async () => { try { const response = await initiateSumsubKyc({ regionIntent }) - if (response.data?.status) { + if (response.data?.status && !initiatingRef.current) { setLiveKycStatus(response.data.status) } } catch { @@ -124,6 +128,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: const handleInitiateKyc = useCallback( async (overrideIntent?: KYCRegionIntent, levelName?: string, crossRegion?: boolean) => { userInitiatedRef.current = true + initiatingRef.current = true setIsLoading(true) setError(null) @@ -192,6 +197,7 @@ export const useSumsubKycFlow = ({ onKycSuccess, onManualClose, regionIntent }: setError(message) } finally { setIsLoading(false) + initiatingRef.current = false } }, [regionIntent, onKycSuccess] From 9c897a170eec5ab6ab96a4d997f8f424c22ce8ba Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:59:57 +0530 Subject: [PATCH 13/14] fix: add onApplicantActionStatusChanged handler for action SDK close --- src/components/Kyc/SumsubKycWrapper.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/Kyc/SumsubKycWrapper.tsx b/src/components/Kyc/SumsubKycWrapper.tsx index 01a553685..95460e566 100644 --- a/src/components/Kyc/SumsubKycWrapper.tsx +++ b/src/components/Kyc/SumsubKycWrapper.tsx @@ -135,6 +135,12 @@ export const SumsubKycWrapper = ({ } } + // for applicant actions, the SDK may fire action-specific events + const handleActionCompleted = (payload: unknown) => { + console.log('[sumsub] onApplicantActionStatusChanged fired', payload) + stableOnComplete() + } + const sdk = window.snsWebSdk .init(accessToken, stableOnRefreshToken) .withConf({ lang: 'en', theme: 'light' }) @@ -142,10 +148,12 @@ export const SumsubKycWrapper = ({ .on('onApplicantSubmitted', handleSubmitted) .on('onApplicantResubmitted', handleResubmitted) .on('onApplicantStatusChanged', handleStatusChanged) + .on('onApplicantActionStatusChanged', handleActionCompleted) // also listen for idCheck-prefixed events (some sdk versions use these) .on('idCheck.onApplicantSubmitted', handleSubmitted) .on('idCheck.onApplicantResubmitted', handleResubmitted) .on('idCheck.onApplicantStatusChanged', handleStatusChanged) + .on('idCheck.onApplicantActionStatusChanged', handleActionCompleted) .on('onError', (error: unknown) => { console.error('[sumsub] sdk error', error) stableOnError(error) From f2f94f726b84e223799f2847eae9126041de78d8 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:10:34 +0530 Subject: [PATCH 14/14] fix: guard action status handler on terminal reviewStatus only close SDK when onApplicantActionStatusChanged fires with reviewStatus === 'completed', preventing premature closure on intermediate action events --- src/components/Kyc/SumsubKycWrapper.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Kyc/SumsubKycWrapper.tsx b/src/components/Kyc/SumsubKycWrapper.tsx index 95460e566..061b5705d 100644 --- a/src/components/Kyc/SumsubKycWrapper.tsx +++ b/src/components/Kyc/SumsubKycWrapper.tsx @@ -135,10 +135,16 @@ export const SumsubKycWrapper = ({ } } - // for applicant actions, the SDK may fire action-specific events - const handleActionCompleted = (payload: unknown) => { + // for applicant actions, the SDK fires action-specific events. + // only close on terminal status to avoid premature SDK closure. + const handleActionCompleted = (payload: { + reviewStatus?: string + reviewResult?: { reviewAnswer?: string } + }) => { console.log('[sumsub] onApplicantActionStatusChanged fired', payload) - stableOnComplete() + if (payload?.reviewStatus === 'completed') { + stableOnComplete() + } } const sdk = window.snsWebSdk