From e0f4d206833057ffa78362993a9664f4ef7c773f Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:39:19 +0530 Subject: [PATCH 01/16] fix: bride tos stuck issue --- src/components/Kyc/BridgeTosStep.tsx | 27 +++--------------- src/components/Kyc/KycStatusDrawer.tsx | 2 +- src/hooks/useMultiPhaseKycFlow.ts | 38 ++++++++++++++++---------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/components/Kyc/BridgeTosStep.tsx b/src/components/Kyc/BridgeTosStep.tsx index 12152dbb3..bba3ca60c 100644 --- a/src/components/Kyc/BridgeTosStep.tsx +++ b/src/components/Kyc/BridgeTosStep.tsx @@ -4,8 +4,9 @@ import { useState, useCallback, useEffect } from 'react' import ActionModal from '@/components/Global/ActionModal' import IframeWrapper from '@/components/Global/IframeWrapper' import { type IconName } from '@/components/Global/Icons/Icon' -import { getBridgeTosLink, confirmBridgeTos } from '@/app/actions/users' +import { getBridgeTosLink } from '@/app/actions/users' import { useAuth } from '@/context/authContext' +import { confirmBridgeTosAndAwaitRails } from '@/hooks/useMultiPhaseKycFlow' interface BridgeTosStepProps { visible: boolean @@ -62,29 +63,9 @@ export const BridgeTosStep = ({ visible, onComplete, onSkip }: BridgeTosStepProp setShowIframe(false) if (source === 'tos_accepted') { - // confirm with backend that bridge actually accepted the ToS - const result = await confirmBridgeTos() - - if (result.data?.accepted) { - await fetchUser() - onComplete() - return - } - - // bridge hasn't registered acceptance yet — poll once after a short delay - await new Promise((resolve) => setTimeout(resolve, 2000)) - const retry = await confirmBridgeTos() - - if (retry.data?.accepted) { - await fetchUser() - onComplete() - } else { - // will be caught by poller/webhook eventually - await fetchUser() - onComplete() - } + await confirmBridgeTosAndAwaitRails(fetchUser) + onComplete() } else { - // user closed without accepting — skip, activity feed will remind them onSkip() } }, diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index 25f2249a9..cd05f577e 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -99,7 +99,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus { if (region === 'STANDARD') return r.rail.provider.code === 'BRIDGE' if (region === 'LATAM') return r.rail.provider.code === 'MANTECA' diff --git a/src/hooks/useMultiPhaseKycFlow.ts b/src/hooks/useMultiPhaseKycFlow.ts index 0e722345a..f6064be1c 100644 --- a/src/hooks/useMultiPhaseKycFlow.ts +++ b/src/hooks/useMultiPhaseKycFlow.ts @@ -8,6 +8,27 @@ import { type KYCRegionIntent } from '@/app/actions/types/sumsub.types' const PREPARING_TIMEOUT_MS = 30000 +/** + * confirms bridge ToS acceptance (with one retry) then polls fetchUser + * until bridge rails leave REQUIRES_INFORMATION. max 3 attempts × 2s. + */ +export async function confirmBridgeTosAndAwaitRails(fetchUser: () => Promise) { + const result = await confirmBridgeTos() + if (!result.data?.accepted) { + await new Promise((resolve) => setTimeout(resolve, 2000)) + await confirmBridgeTos() + } + + for (let i = 0; i < 3; i++) { + const updatedUser = await fetchUser() + const stillNeedsTos = (updatedUser?.rails ?? []).some( + (r: any) => r.rail.provider.code === 'BRIDGE' && r.status === 'REQUIRES_INFORMATION' + ) + if (!stillNeedsTos) break + if (i < 2) await new Promise((resolve) => setTimeout(resolve, 2000)) + } +} + interface UseMultiPhaseKycFlowOptions { onKycSuccess?: () => void onManualClose?: () => void @@ -214,20 +235,9 @@ export const useMultiPhaseKycFlow = ({ onKycSuccess, onManualClose, regionIntent setShowTosIframe(false) if (source === 'tos_accepted') { - // confirm with backend - const result = await confirmBridgeTos() - - if (!result.data?.accepted) { - // bridge may not have registered acceptance yet — retry after short delay - await new Promise((resolve) => setTimeout(resolve, 2000)) - const retryResult = await confirmBridgeTos() - if (!retryResult.data?.accepted) { - console.warn('[useMultiPhaseKycFlow] bridge ToS confirmation failed after retry') - } - } - - // optimistically complete — don't wait for rail status WebSocket - await fetchUser() + // show loading state while confirming + polling + setModalPhase('preparing') + await confirmBridgeTosAndAwaitRails(fetchUser) completeFlow() } // if manual close, stay on bridge_tos phase (user can try again) From 54d0aa42d26da80e6b1f76e71b175a76ad9cc24f Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:52:22 +0530 Subject: [PATCH 02/16] fix: remove email from add bank account flow --- src/app/actions/users.ts | 1 + .../AddWithdraw/AddWithdrawCountriesList.tsx | 11 ++++++----- src/components/AddWithdraw/DynamicBankAccountForm.tsx | 6 ------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/app/actions/users.ts b/src/app/actions/users.ts index 5929fed25..07e1498ef 100644 --- a/src/app/actions/users.ts +++ b/src/app/actions/users.ts @@ -194,6 +194,7 @@ export const confirmBridgeTos = async (): Promise<{ data?: { accepted: boolean } Authorization: `Bearer ${jwtToken}`, 'api-key': API_KEY, }, + body: JSON.stringify({}), }) const responseJson = await response.json() if (!response.ok) { diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx index 639b02fd2..8d0dc6df8 100644 --- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx +++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx @@ -98,14 +98,15 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { payload: AddBankAccountPayload, rawData: IBankAccountDetails ): Promise<{ error?: string }> => { - const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus + // 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' - const hasEmailOnLoad = !!user?.user.email - // scenario (1): happy path: if the user has already completed kyc, we can add the bank account directly - // note: we no longer check for fullName as account owner name is now always collected from the form - if (isUserKycVerified && (hasEmailOnLoad || rawData.email)) { + // email and name are now collected by sumsub — no need to check them here + if (isUserKycVerified) { const currentAccountIds = new Set(user?.accounts.map((acc) => acc.id) ?? []) const result = await addBankAccount(payload) diff --git a/src/components/AddWithdraw/DynamicBankAccountForm.tsx b/src/components/AddWithdraw/DynamicBankAccountForm.tsx index c94b48157..b0996cdb3 100644 --- a/src/components/AddWithdraw/DynamicBankAccountForm.tsx +++ b/src/components/AddWithdraw/DynamicBankAccountForm.tsx @@ -430,12 +430,6 @@ export const DynamicBankAccountForm = forwardRef<{ handleSubmit: () => void }, D })} )} - {flow !== 'claim' && - !hideEmailInput && - !user?.user?.email && - renderInput('email', 'E-mail', { - required: 'Email is required', - })} {isMx ? renderInput('clabe', 'CLABE', { From 35e37b1803b04d21b7937cb837333aebcfde69f6 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:15:58 +0530 Subject: [PATCH 03/16] feat: handle pending bridge tos using home carousel card --- src/components/Home/HomeCarouselCTA/index.tsx | 19 ++++++- src/components/Home/HomeHistory.tsx | 8 --- src/components/Kyc/BridgeTosReminder.tsx | 55 ------------------- src/hooks/useHomeCarouselCTAs.tsx | 18 +++++- 4 files changed, 34 insertions(+), 66 deletions(-) delete mode 100644 src/components/Kyc/BridgeTosReminder.tsx diff --git a/src/components/Home/HomeCarouselCTA/index.tsx b/src/components/Home/HomeCarouselCTA/index.tsx index b7867765d..d8a043bda 100644 --- a/src/components/Home/HomeCarouselCTA/index.tsx +++ b/src/components/Home/HomeCarouselCTA/index.tsx @@ -8,14 +8,15 @@ import { type IconName } from '@/components/Global/Icons/Icon' import { useHomeCarouselCTAs, type CarouselCTA as CarouselCTAType } from '@/hooks/useHomeCarouselCTAs' import { perksApi, type PendingPerk } from '@/services/perks' import { useAuth } from '@/context/authContext' +import { BridgeTosStep } from '@/components/Kyc/BridgeTosStep' import { useWebSocket } from '@/hooks/useWebSocket' import { extractInviteeName } from '@/utils/general.utils' import PerkClaimModal from '../PerkClaimModal' import underMaintenanceConfig from '@/config/underMaintenance.config' const HomeCarouselCTA = () => { - const { carouselCTAs, setCarouselCTAs } = useHomeCarouselCTAs() - const { user } = useAuth() + const { carouselCTAs, setCarouselCTAs, showBridgeTos, setShowBridgeTos } = useHomeCarouselCTAs() + const { user, fetchUser } = useAuth() const queryClient = useQueryClient() // Perk claim modal state @@ -89,6 +90,17 @@ const HomeCarouselCTA = () => { setSelectedPerk(null) }, []) + // bridge ToS handlers + const handleTosComplete = useCallback(async () => { + setShowBridgeTos(false) + setCarouselCTAs((prev) => prev.filter((c) => c.id !== 'bridge-tos')) + await fetchUser() + }, [setShowBridgeTos, setCarouselCTAs, fetchUser]) + + const handleTosSkip = useCallback(() => { + setShowBridgeTos(false) + }, [setShowBridgeTos]) + // don't render carousel if there are no CTAs if (!allCTAs.length) return null @@ -130,6 +142,9 @@ const HomeCarouselCTA = () => { onClaimed={handlePerkClaimed} /> )} + + {/* Bridge ToS iframe */} + ) } diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index 9a5a67c57..0cd6d7027 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -15,8 +15,6 @@ import { type CardPosition, getCardPosition } from '../Global/Card/card.utils' import EmptyState from '../Global/EmptyStates/EmptyState' import { KycStatusItem, isKycStatusItem, type KycHistoryEntry } from '../Kyc/KycStatusItem' import { groupKycByRegion } from '@/utils/kyc-grouping.utils' -import { BridgeTosReminder } from '../Kyc/BridgeTosReminder' -import { useBridgeTosStatus } from '@/hooks/useBridgeTosStatus' import { useWallet } from '@/hooks/wallet/useWallet' import { BadgeStatusItem } from '@/components/Badges/BadgeStatusItem' import { isBadgeHistoryItem } from '@/components/Badges/badge.types' @@ -45,8 +43,6 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h const { fetchBalance } = useWallet() const { triggerHaptic } = useHaptic() const { fetchUser } = useAuth() - const { needsBridgeTos } = useBridgeTosStatus() - const isViewingOwnHistory = useMemo( () => (isLoggedIn && !username) || (isLoggedIn && username === user?.user.username), [isLoggedIn, username, user?.user.username] @@ -251,7 +247,6 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h return (

Activity

- {isViewingOwnHistory && needsBridgeTos && } {isViewingOwnHistory && user?.user && (() => { @@ -290,9 +285,6 @@ const HomeHistory = ({ username, hideTxnAmount = false }: { username?: string; h return (
- {/* bridge ToS reminder for users who haven't accepted yet */} - {isViewingOwnHistory && needsBridgeTos && } - {/* link to the full history page */} {pendingRequests.length > 0 && ( <> diff --git a/src/components/Kyc/BridgeTosReminder.tsx b/src/components/Kyc/BridgeTosReminder.tsx deleted file mode 100644 index 7db489a4b..000000000 --- a/src/components/Kyc/BridgeTosReminder.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client' - -import { useState, useCallback } from 'react' -import Card from '@/components/Global/Card' -import { Icon } from '@/components/Global/Icons/Icon' -import { BridgeTosStep } from '@/components/Kyc/BridgeTosStep' -import { useAuth } from '@/context/authContext' -import { type CardPosition } from '@/components/Global/Card/card.utils' - -interface BridgeTosReminderProps { - position?: CardPosition -} - -// shown in the activity feed when user has bridge rails needing ToS acceptance. -// clicking opens the bridge ToS flow. -export const BridgeTosReminder = ({ position = 'single' }: BridgeTosReminderProps) => { - const { fetchUser } = useAuth() - const [showTosStep, setShowTosStep] = useState(false) - const [tosJustAccepted, setTosJustAccepted] = useState(false) - - const handleClick = useCallback(() => { - setShowTosStep(true) - }, []) - - const handleComplete = useCallback(async () => { - setShowTosStep(false) - setTosJustAccepted(true) // optimistically hide — backend rail transition is async - await fetchUser() - }, [fetchUser]) - - const handleSkip = useCallback(() => { - setShowTosStep(false) - }, []) - - if (tosJustAccepted) return null - - return ( - <> - -
-
- -
-
-

Accept terms of service

-

Required to enable bank transfers

-
- -
-
- - - - ) -} diff --git a/src/hooks/useHomeCarouselCTAs.tsx b/src/hooks/useHomeCarouselCTAs.tsx index 8613eccf9..6b0fc48bf 100644 --- a/src/hooks/useHomeCarouselCTAs.tsx +++ b/src/hooks/useHomeCarouselCTAs.tsx @@ -12,6 +12,7 @@ import { DeviceType, useDeviceType } from './useGetDeviceType' import { usePWAStatus } from './usePWAStatus' import { useGeoLocation } from './useGeoLocation' import { useCardPioneerInfo } from './useCardPioneerInfo' +import { useBridgeTosStatus } from './useBridgeTosStatus' import { STAR_STRAIGHT_ICON } from '@/assets' import underMaintenanceConfig from '@/config/underMaintenance.config' @@ -50,6 +51,8 @@ export const useHomeCarouselCTAs = () => { hasPurchased: hasCardPioneerPurchased, isLoading: isCardPioneerLoading, } = useCardPioneerInfo() + const { needsBridgeTos } = useBridgeTosStatus() + const [showBridgeTos, setShowBridgeTos] = useState(false) const generateCarouselCTAs = useCallback(() => { const _carouselCTAs: CarouselCTA[] = [] @@ -58,6 +61,18 @@ export const useHomeCarouselCTAs = () => { const hasKycApproval = isUserKycApproved || isUserMantecaKycApproved const isLatamUser = userCountryCode === 'AR' || userCountryCode === 'BR' + // Bridge ToS acceptance — must be first CTA when user has pending ToS + if (needsBridgeTos) { + _carouselCTAs.push({ + id: 'bridge-tos', + title: 'Accept terms of service', + description: 'Required to enable bank transfers', + icon: 'alert', + iconContainerClassName: 'bg-yellow-1', + onClick: () => setShowBridgeTos(true), + }) + } + // Card Pioneer CTA - show to all users who haven't purchased yet // Eligibility check happens during the flow (geo screen) // Only show when we know for sure they haven't purchased (not while loading) @@ -215,6 +230,7 @@ export const useHomeCarouselCTAs = () => { isCardPioneerEligible, hasCardPioneerPurchased, isCardPioneerLoading, + needsBridgeTos, ]) useEffect(() => { @@ -226,5 +242,5 @@ export const useHomeCarouselCTAs = () => { generateCarouselCTAs() }, [user, generateCarouselCTAs, isPermissionGranted]) - return { carouselCTAs, setCarouselCTAs } + return { carouselCTAs, setCarouselCTAs, showBridgeTos, setShowBridgeTos } } From 1eb165849dfa55ac2baffa855750005ec82ea932 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:48:28 +0530 Subject: [PATCH 04/16] fix: handle retry if user abandons kyc --- src/components/Kyc/KycStatusDrawer.tsx | 6 ++++++ src/components/Kyc/KycStatusItem.tsx | 2 +- src/components/Kyc/states/KycNotStarted.tsx | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/components/Kyc/states/KycNotStarted.tsx diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index cd05f577e..30effed2d 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -1,6 +1,7 @@ import { KycActionRequired } from './states/KycActionRequired' import { KycCompleted } from './states/KycCompleted' import { KycFailed } from './states/KycFailed' +import { KycNotStarted } from './states/KycNotStarted' import { KycProcessing } from './states/KycProcessing' import { KycRequiresDocuments } from './states/KycRequiresDocuments' import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals' @@ -74,6 +75,11 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus } const renderContent = () => { + // user initiated kyc but abandoned before submitting — show resume cta + if (verification && isKycStatusNotStarted(status)) { + return + } + // bridge additional document requirement — but don't mask terminal kyc states if (needsAdditionalDocs && statusCategory !== 'failed' && statusCategory !== 'action_required') { return ( diff --git a/src/components/Kyc/KycStatusItem.tsx b/src/components/Kyc/KycStatusItem.tsx index 359e53a1f..1f553a0e5 100644 --- a/src/components/Kyc/KycStatusItem.tsx +++ b/src/components/Kyc/KycStatusItem.tsx @@ -77,7 +77,7 @@ export const KycStatusItem = ({ const isInitiatedButNotStarted = !!verification && isKycStatusNotStarted(kycStatus) const subtitle = useMemo(() => { - if (isInitiatedButNotStarted) return 'In progress' + if (isInitiatedButNotStarted) return 'Not completed' if (isActionRequired) return 'Action needed' if (isPending) return 'Under review' if (isApproved) return 'Approved' diff --git a/src/components/Kyc/states/KycNotStarted.tsx b/src/components/Kyc/states/KycNotStarted.tsx new file mode 100644 index 000000000..a4dd9a38b --- /dev/null +++ b/src/components/Kyc/states/KycNotStarted.tsx @@ -0,0 +1,21 @@ +import { KYCStatusDrawerItem } from '../KYCStatusDrawerItem' +import { Button } from '@/components/0_Bruddle/Button' + +// shown when user initiated kyc but abandoned before submitting documents. +// provides a cta to resume the verification flow. +export const KycNotStarted = ({ onResume, isLoading }: { onResume: () => void; isLoading?: boolean }) => { + return ( +
+ + +

+ Your verification isn't complete yet. Continue where you left off to enable bank transfers and QR + payments. +

+ + +
+ ) +} From b26a33c538d0cb85f31c3228f43380e29c5e988f Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:57:05 +0530 Subject: [PATCH 05/16] fix: remove duplicate rails entry in kyc drawer --- src/components/Kyc/KycStatusDrawer.tsx | 5 ---- src/components/Kyc/states/KycCompleted.tsx | 32 ---------------------- 2 files changed, 37 deletions(-) diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index 30effed2d..bb1ad12b2 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -106,11 +106,6 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus bridgeKycApprovedAt={verification?.approvedAt ?? user?.user?.bridgeKycApprovedAt} countryCode={countryCode ?? undefined} isBridge={isBridgeKyc || region === 'STANDARD'} - rails={user?.rails?.filter((r) => { - if (region === 'STANDARD') return r.rail.provider.code === 'BRIDGE' - if (region === 'LATAM') return r.rail.provider.code === 'MANTECA' - return true - })} /> ) case 'action_required': diff --git a/src/components/Kyc/states/KycCompleted.tsx b/src/components/Kyc/states/KycCompleted.tsx index f1fc3a79d..0c59259b9 100644 --- a/src/components/Kyc/states/KycCompleted.tsx +++ b/src/components/Kyc/states/KycCompleted.tsx @@ -6,8 +6,6 @@ import { formatDate } from '@/utils/general.utils' import { CountryRegionRow } from '../CountryRegionRow' import Image from 'next/image' import { STAR_STRAIGHT_ICON } from '@/assets' -import { type IUserRail } from '@/interfaces' -import { getCurrencyFlagUrl } from '@/constants/countryCurrencyMapping' // @dev TODO: Remove hardcoded KYC points - this should come from backend // See comment in KycStatusItem.tsx for proper implementation plan @@ -18,12 +16,10 @@ export const KycCompleted = ({ bridgeKycApprovedAt, countryCode, isBridge, - rails, }: { bridgeKycApprovedAt?: string countryCode?: string | null isBridge?: boolean - rails?: IUserRail[] }) => { const verifiedOn = useMemo(() => { if (!bridgeKycApprovedAt) return 'N/A' @@ -35,8 +31,6 @@ export const KycCompleted = ({ } }, [bridgeKycApprovedAt]) - const enabledRails = useMemo(() => (rails ?? []).filter((r) => r.status === 'ENABLED'), [rails]) - return (
@@ -54,32 +48,6 @@ export const KycCompleted = ({ /> - {enabledRails.length > 0 && ( - - {enabledRails.map((r, index) => ( - - {getCurrencyFlagUrl(r.rail.method.currency) && ( - {`${r.rail.method.currency} - )} - {r.rail.method.name} -
- } - value={r.rail.method.currency} - hideBottomBorder={index === enabledRails.length - 1} - /> - ))} - - )}
) } From aca3fb0cd8a7bb7f385daea4482cd6af96226850 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:44:27 +0530 Subject: [PATCH 06/16] fix: stop auto initiate kyc in deposit flow --- .../add-money/[country]/bank/page.tsx | 69 ++++++++----------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index eda417834..6c043e783 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -30,9 +30,10 @@ import { getLimitsWarningCardProps } from '@/features/limits/utils' import { useExchangeRate } from '@/hooks/useExchangeRate' import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow' import { SumsubKycModals } from '@/components/Kyc/SumsubKycModals' +import { InitiateKycModal } from '@/components/Kyc/InitiateKycModal' // Step type for URL state -type BridgeBankStep = 'inputAmount' | 'kyc' | 'showDetails' +type BridgeBankStep = 'inputAmount' | 'showDetails' export default function OnrampBankPage() { const router = useRouter() @@ -42,7 +43,7 @@ export default function OnrampBankPage() { // Example: /add-money/mexico/bank?step=inputAmount&amount=500 const [urlState, setUrlState] = useQueryStates( { - step: parseAsStringEnum(['inputAmount', 'kyc', 'showDetails']), + step: parseAsStringEnum(['inputAmount', 'showDetails']), amount: parseAsString, }, { history: 'push' } @@ -53,6 +54,7 @@ export default function OnrampBankPage() { // Local UI state (not URL-appropriate - transient) const [showWarningModal, setShowWarningModal] = useState(false) + const [showKycModal, setShowKycModal] = useState(false) const [isRiskAccepted, setIsRiskAccepted] = useState(false) const [liveKycStatus, setLiveKycStatus] = useState(undefined) const { setError, error, setOnrampData, onrampData } = useOnrampFlow() @@ -152,30 +154,12 @@ export default function OnrampBankPage() { currency: 'USD', }) - // Determine initial step based on KYC status (only when URL has no step) + // Default to inputAmount step when no step in URL useEffect(() => { - // If URL already has a step, respect it (allows deep linking) if (urlState.step) return - - // Wait for user to be fetched before determining initial step if (user === null) return - - const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus - const isUserKycVerified = currentKycStatus === 'approved' - - if (!isUserKycVerified) { - setUrlState({ step: 'kyc' }) - } else { - setUrlState({ step: 'inputAmount' }) - } - }, [liveKycStatus, user, urlState.step, setUrlState]) - - // Handle KYC completion - useEffect(() => { - if (urlState.step === 'kyc' && liveKycStatus === 'approved') { - setUrlState({ step: 'inputAmount' }) - } - }, [liveKycStatus, urlState.step, setUrlState]) + setUrlState({ step: 'inputAmount' }) + }, [user, urlState.step, setUrlState]) const validateAmount = useCallback( (amountStr: string): boolean => { @@ -217,9 +201,17 @@ export default function OnrampBankPage() { }, [rawTokenAmount, validateAmount, setError]) const handleAmountContinue = () => { - if (validateAmount(rawTokenAmount)) { - setShowWarningModal(true) + if (!validateAmount(rawTokenAmount)) return + + const currentKycStatus = liveKycStatus || user?.user.bridgeKycStatus + const isUserKycVerified = currentKycStatus === 'approved' + + if (!isUserKycVerified) { + setShowKycModal(true) + return } + + setShowWarningModal(true) } const handleWarningConfirm = async () => { @@ -271,12 +263,6 @@ export default function OnrampBankPage() { } } - useEffect(() => { - if (urlState.step === 'kyc') { - sumsubFlow.handleInitiateKyc('STANDARD') - } - }, [urlState.step]) // eslint-disable-line react-hooks/exhaustive-deps - // Redirect to inputAmount if showDetails is accessed without required data (deep link / back navigation) useEffect(() => { if (urlState.step === 'showDetails' && !onrampData?.transferId) { @@ -303,15 +289,6 @@ export default function OnrampBankPage() { return } - if (urlState.step === 'kyc') { - return ( -
- - -
- ) - } - if (urlState.step === 'showDetails') { // Show loading while useEffect redirects if data is missing if (!onrampData?.transferId) { @@ -408,6 +385,18 @@ export default function OnrampBankPage() { amount={rawTokenAmount} currency={getCurrencySymbol(getCurrencyConfig(selectedCountry.id, 'onramp').currency)} /> + + setShowKycModal(false)} + onVerify={async () => { + setShowKycModal(false) + await sumsubFlow.handleInitiateKyc('STANDARD') + }} + isLoading={sumsubFlow.isLoading} + /> + +
) } From 72d75f0d4bb01f828c8a21b21bfb189a50659009 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:44:52 +0530 Subject: [PATCH 07/16] fix: sumsub sdk unmounting issue --- src/components/Kyc/KycStatusDrawer.tsx | 35 ++++++++++++++++++++++---- src/components/Kyc/KycStatusItem.tsx | 6 ++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index bb1ad12b2..5bb0144d9 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -12,6 +12,7 @@ import { useUserStore } from '@/redux/hooks' import { useMultiPhaseKycFlow } from '@/hooks/useMultiPhaseKycFlow' import { getKycStatusCategory, isKycStatusNotStarted } from '@/constants/kyc.consts' import { type KYCRegionIntent } from '@/app/actions/types/sumsub.types' +import { useCallback } from 'react' interface KycStatusDrawerProps { isOpen: boolean @@ -19,10 +20,19 @@ interface KycStatusDrawerProps { verification?: IUserKycVerification bridgeKycStatus?: BridgeKycStatus region?: 'STANDARD' | 'LATAM' + /** keep this component mounted even after drawer closes (so SumsubKycModals persists) */ + onKeepMounted?: (keep: boolean) => void } // this component determines which kyc state to show inside the drawer and fetches rejection reasons if the kyc has failed. -export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus, region }: KycStatusDrawerProps) => { +export const KycStatusDrawer = ({ + isOpen, + onClose, + verification, + bridgeKycStatus, + region, + onKeepMounted, +}: KycStatusDrawerProps) => { const { user } = useUserStore() const status = verification ? verification.status : bridgeKycStatus @@ -35,9 +45,15 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus verification?.provider === 'SUMSUB' ? verification?.metadata?.regionIntent : undefined ) as KYCRegionIntent | undefined + // close drawer and release the keep-mounted hold + const handleFlowDone = useCallback(() => { + onClose() + onKeepMounted?.(false) + }, [onClose, onKeepMounted]) + const sumsubFlow = useMultiPhaseKycFlow({ - onKycSuccess: onClose, - onManualClose: onClose, + onKycSuccess: handleFlowDone, + onManualClose: handleFlowDone, // don't pass regionIntent for completed kyc — prevents the mount effect // in useSumsubKycFlow from calling initiateSumsubKyc(), which triggers // the undefined->APPROVED transition that auto-closes the drawer @@ -75,9 +91,18 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus } const renderContent = () => { - // user initiated kyc but abandoned before submitting — show resume cta + // user initiated kyc but abandoned before submitting — close drawer visually + // but keep component mounted so SumsubKycModals persists for the SDK flow if (verification && isKycStatusNotStarted(status)) { - return + return ( + { + onKeepMounted?.(true) + onClose() + sumsubFlow.handleInitiateKyc() + }} + /> + ) } // bridge additional document requirement — but don't mask terminal kyc states diff --git a/src/components/Kyc/KycStatusItem.tsx b/src/components/Kyc/KycStatusItem.tsx index 1f553a0e5..fb8086e97 100644 --- a/src/components/Kyc/KycStatusItem.tsx +++ b/src/components/Kyc/KycStatusItem.tsx @@ -51,6 +51,9 @@ export const KycStatusItem = ({ const { user } = useUserStore() const [isDrawerOpen, setIsDrawerOpen] = useState(false) const [wsBridgeKycStatus, setWsBridgeKycStatus] = useState(undefined) + // keep drawer component mounted when SDK flow is active (so SumsubKycModals persists + // even after the drawer visually closes) + const [keepDrawerMounted, setKeepDrawerMounted] = useState(false) const handleCloseDrawer = useCallback(() => { setIsDrawerOpen(false) @@ -127,13 +130,14 @@ export const KycStatusItem = ({ - {isDrawerOpen && ( + {(isDrawerOpen || keepDrawerMounted) && ( )} From 17db99e2815432cd708f7d1bfd3857e724fda56c Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:44:42 +0530 Subject: [PATCH 08/16] fix: resolve cr comment --- src/app/(mobile-ui)/add-money/[country]/bank/page.tsx | 2 +- src/app/(mobile-ui)/withdraw/manteca/page.tsx | 2 +- src/components/AddMoney/components/MantecaAddMoney.tsx | 2 +- src/components/AddWithdraw/AddWithdrawCountriesList.tsx | 2 +- src/components/Claim/Link/MantecaFlowManager.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index 6c043e783..3e773c247 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -390,8 +390,8 @@ export default function OnrampBankPage() { visible={showKycModal} onClose={() => setShowKycModal(false)} onVerify={async () => { - setShowKycModal(false) await sumsubFlow.handleInitiateKyc('STANDARD') + setShowKycModal(false) }} isLoading={sumsubFlow.isLoading} /> diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index 712d8b0bc..e2c564e4a 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -454,8 +454,8 @@ export default function MantecaWithdrawFlow() { visible={showKycModal} onClose={() => setShowKycModal(false)} onVerify={async () => { - setShowKycModal(false) await sumsubFlow.handleInitiateKyc('LATAM') + setShowKycModal(false) }} isLoading={sumsubFlow.isLoading} /> diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx index c5f88bf6b..eab40ae05 100644 --- a/src/components/AddMoney/components/MantecaAddMoney.tsx +++ b/src/components/AddMoney/components/MantecaAddMoney.tsx @@ -194,8 +194,8 @@ const MantecaAddMoney: FC = () => { visible={showKycModal} onClose={() => setShowKycModal(false)} onVerify={async () => { - setShowKycModal(false) await sumsubFlow.handleInitiateKyc('LATAM') + setShowKycModal(false) }} isLoading={sumsubFlow.isLoading} /> diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx index 8d0dc6df8..0472cdffa 100644 --- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx +++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx @@ -107,7 +107,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { // 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(user?.accounts.map((acc) => acc.id) ?? []) + const currentAccountIds = new Set((freshUser?.accounts ?? user?.accounts ?? []).map((acc) => acc.id)) const result = await addBankAccount(payload) if (result.error) { diff --git a/src/components/Claim/Link/MantecaFlowManager.tsx b/src/components/Claim/Link/MantecaFlowManager.tsx index e014c639b..0598ac4b1 100644 --- a/src/components/Claim/Link/MantecaFlowManager.tsx +++ b/src/components/Claim/Link/MantecaFlowManager.tsx @@ -120,8 +120,8 @@ const MantecaFlowManager: FC = ({ claimLinkData, amount visible={showKycModal} onClose={() => setShowKycModal(false)} onVerify={async () => { - setShowKycModal(false) await sumsubFlow.handleInitiateKyc('LATAM') + setShowKycModal(false) }} isLoading={sumsubFlow.isLoading} /> From 87b06d9dd1ed6ba20ecf8b7d21b3282e0417f0fb Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:55:49 +0530 Subject: [PATCH 09/16] fix: replace legacy bridgeKycStatus checks with unified isUserKycVerified Added isUserKycVerified utility in kyc.consts.ts that checks all KYC providers (Bridge, Manteca, Sumsub) instead of only bridge status. Replaced direct bridgeKycStatus === 'approved' checks across 12 files to support KYC 2.0 where users may be verified through any provider. --- src/app/(mobile-ui)/points/invites/page.tsx | 2 +- src/app/(mobile-ui)/points/page.tsx | 2 +- src/components/Claim/Claim.tsx | 5 +++-- .../Global/PostSignupActionManager/index.tsx | 3 ++- .../Profile/components/PublicProfile.tsx | 11 ++--------- .../Profile/views/ProfileEdit.view.tsx | 8 ++++---- .../views/Initial.direct.request.view.tsx | 3 ++- src/components/Send/views/Contacts.view.tsx | 3 ++- src/constants/kyc.consts.ts | 19 +++++++++++++++++++ src/hooks/useDetermineBankClaimType.ts | 7 ++++--- src/hooks/useDetermineBankRequestType.ts | 7 ++++--- src/utils/general.utils.ts | 3 ++- 12 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/app/(mobile-ui)/points/invites/page.tsx b/src/app/(mobile-ui)/points/invites/page.tsx index ee1a36597..f7665b0c3 100644 --- a/src/app/(mobile-ui)/points/invites/page.tsx +++ b/src/app/(mobile-ui)/points/invites/page.tsx @@ -75,7 +75,7 @@ const InvitesPage = () => { {invites?.invitees?.map((invite: PointsInvite, i: number) => { const username = invite.username const fullName = invite.fullName - const isVerified = invite.kycStatus === 'approved' + const isVerified = invite.kycVerified const pointsEarned = Math.floor(invite.totalPoints * TRANSITIVITY_MULTIPLIER) // respect user's showFullName preference for avatar and display name const displayName = invite.showFullName && fullName ? fullName : username diff --git a/src/app/(mobile-ui)/points/page.tsx b/src/app/(mobile-ui)/points/page.tsx index dd1c38605..d9cc569dc 100644 --- a/src/app/(mobile-ui)/points/page.tsx +++ b/src/app/(mobile-ui)/points/page.tsx @@ -217,7 +217,7 @@ const PointsPage = () => { {invites.invitees?.slice(0, 5).map((invite: PointsInvite, i: number) => { const username = invite.username const fullName = invite.fullName - const isVerified = invite.kycStatus === 'approved' + const isVerified = invite.kycVerified const pointsEarned = Math.floor(invite.totalPoints * TRANSITIVITY_MULTIPLIER) // respect user's showFullName preference for avatar and display name const displayName = invite.showFullName && fullName ? fullName : username diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index 318eba629..cb61325d7 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -22,6 +22,7 @@ import { getTokenLogo, getChainLogo, } from '@/utils/general.utils' +import { isUserKycVerified } from '@/constants/kyc.consts' import * as Sentry from '@sentry/nextjs' import { useQuery } from '@tanstack/react-query' import type { Hash } from 'viem' @@ -189,7 +190,7 @@ export const Claim = ({}) => { peanutFeeDetails: { amountDisplay: '$ 0.00', }, - isVerified: claimLinkData.sender?.bridgeKycStatus === 'approved', + isVerified: isUserKycVerified(claimLinkData.sender), haveSentMoneyToUser: claimLinkData.sender?.userId ? interactions[claimLinkData.sender?.userId] || false : false, @@ -396,7 +397,7 @@ export const Claim = ({}) => { // redirect to bank flow if user is KYC approved and step is bank useEffect(() => { const stepFromURL = searchParams.get('step') - if (user?.user.bridgeKycStatus === 'approved' && stepFromURL === 'bank') { + if (isUserKycVerified(user?.user) && stepFromURL === 'bank') { setClaimBankFlowStep(ClaimBankFlowStep.BankCountryList) } }, [user]) diff --git a/src/components/Global/PostSignupActionManager/index.tsx b/src/components/Global/PostSignupActionManager/index.tsx index 4c0821810..b58d6143f 100644 --- a/src/components/Global/PostSignupActionManager/index.tsx +++ b/src/components/Global/PostSignupActionManager/index.tsx @@ -7,6 +7,7 @@ import ActionModal from '../ActionModal' import { POST_SIGNUP_ACTIONS } from './post-signup-action.consts' import { type IconName } from '../Icons/Icon' import { useAuth } from '@/context/authContext' +import { isUserKycVerified } from '@/constants/kyc.consts' export const PostSignupActionManager = ({ onActionModalVisibilityChange, @@ -26,7 +27,7 @@ export const PostSignupActionManager = ({ const checkClaimModalAfterKYC = () => { const redirectUrl = getRedirectUrl() - if (user?.user.bridgeKycStatus === 'approved' && redirectUrl) { + if (isUserKycVerified(user?.user) && redirectUrl) { const matchedAction = POST_SIGNUP_ACTIONS.find((action) => action.pathPattern.test(redirectUrl)) if (matchedAction) { setActionConfig({ diff --git a/src/components/Profile/components/PublicProfile.tsx b/src/components/Profile/components/PublicProfile.tsx index ef6137a33..8fbeba0da 100644 --- a/src/components/Profile/components/PublicProfile.tsx +++ b/src/components/Profile/components/PublicProfile.tsx @@ -15,7 +15,7 @@ import { checkIfInternalNavigation } from '@/utils/general.utils' import { useAuth } from '@/context/authContext' import ShareButton from '@/components/Global/ShareButton' import ActionModal from '@/components/Global/ActionModal' -import { MantecaKycStatus } from '@/interfaces' +import { isUserKycVerified } from '@/constants/kyc.consts' import BadgesRow from '@/components/Badges/BadgesRow' interface PublicProfileProps { @@ -54,14 +54,7 @@ const PublicProfile: React.FC = ({ username, isLoggedIn = fa if (apiUser?.fullName) setFullName(apiUser.fullName) // get the profile owner's showFullName preference setShowFullName(apiUser?.showFullName ?? false) - if ( - apiUser?.bridgeKycStatus === 'approved' || - apiUser?.kycVerifications?.some((v) => v.status === MantecaKycStatus.ACTIVE) - ) { - setIsKycVerified(true) - } else { - setIsKycVerified(false) - } + setIsKycVerified(isUserKycVerified(apiUser)) // to check if the logged in user has sent money to the profile user, // we check the amount that the profile user has received from the logged in user. if (apiUser?.totalUsdReceivedFromCurrentUser) { diff --git a/src/components/Profile/views/ProfileEdit.view.tsx b/src/components/Profile/views/ProfileEdit.view.tsx index 442d47dc9..b7fac0170 100644 --- a/src/components/Profile/views/ProfileEdit.view.tsx +++ b/src/components/Profile/views/ProfileEdit.view.tsx @@ -14,7 +14,7 @@ import useKycStatus from '@/hooks/useKycStatus' export const ProfileEditView = () => { const router = useRouter() const { user, fetchUser } = useAuth() - const { isUserBridgeKycApproved } = useKycStatus() + const { isUserKycApproved } = useKycStatus() const [isLoading, setIsLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') @@ -115,7 +115,7 @@ export const ProfileEditView = () => {
router.push('/profile')} /> - +
{ value={formData.name} onChange={(value) => handleChange('name', value)} placeholder="Add your name" - disabled={user?.user.bridgeKycStatus === 'approved'} + disabled={isUserKycApproved} /> { value={formData.surname} onChange={(value) => handleChange('surname', value)} placeholder="Add your surname" - disabled={user?.user.bridgeKycStatus === 'approved'} + disabled={isUserKycApproved} /> diff --git a/src/components/Send/views/Contacts.view.tsx b/src/components/Send/views/Contacts.view.tsx index 148b4e59b..73efbc09f 100644 --- a/src/components/Send/views/Contacts.view.tsx +++ b/src/components/Send/views/Contacts.view.tsx @@ -13,6 +13,7 @@ import PeanutLoading from '@/components/Global/PeanutLoading' import EmptyState from '@/components/Global/EmptyStates/EmptyState' import { Button } from '@/components/0_Bruddle/Button' import { useDebounce } from '@/hooks/useDebounce' +import { isUserKycVerified } from '@/constants/kyc.consts' import { ContactsListSkeleton } from '@/components/Common/ContactsListSkeleton' export default function ContactsView() { @@ -138,7 +139,7 @@ export default function ContactsView() {

Your contacts

{contacts.map((contact, index) => { - const isVerified = contact.bridgeKycStatus === 'approved' + const isVerified = isUserKycVerified(contact) const displayName = contact.showFullName ? contact.fullName || contact.username : contact.username diff --git a/src/constants/kyc.consts.ts b/src/constants/kyc.consts.ts index f8fc5d507..d35557a54 100644 --- a/src/constants/kyc.consts.ts +++ b/src/constants/kyc.consts.ts @@ -31,6 +31,25 @@ const SUMSUB_IN_PROGRESS_STATUSES: ReadonlySet = new Set(['PENDING', 'IN export const isKycStatusApproved = (status: string | undefined | null): boolean => !!status && APPROVED_STATUSES.has(status) +/** + * check if a user (from API data) has completed kyc with any provider. + * works with user objects from getUserById, contacts, senders, recipients, etc. + * for current user, prefer useUnifiedKycStatus hook instead. + */ +export function isUserKycVerified( + user: + | { + bridgeKycStatus?: string | null + kycVerifications?: Array<{ status: string }> | null + } + | null + | undefined +): boolean { + if (!user) return false + if (user.bridgeKycStatus === 'approved') return true + return user.kycVerifications?.some((v) => isKycStatusApproved(v.status)) ?? false +} + /** check if a kyc status represents a failed/rejected state */ export const isKycStatusFailed = (status: string | undefined | null): boolean => !!status && FAILED_STATUSES.has(status) diff --git a/src/hooks/useDetermineBankClaimType.ts b/src/hooks/useDetermineBankClaimType.ts index 0d73234cb..69bef4914 100644 --- a/src/hooks/useDetermineBankClaimType.ts +++ b/src/hooks/useDetermineBankClaimType.ts @@ -3,6 +3,7 @@ import { useAuth } from '@/context/authContext' import { useClaimBankFlow } from '@/context/ClaimBankFlowContext' import { useEffect, useState } from 'react' import useKycStatus from './useKycStatus' +import { isUserKycVerified } from '@/constants/kyc.consts' export enum BankClaimType { GuestBankClaim = 'guest-bank-claim', @@ -23,12 +24,12 @@ export function useDetermineBankClaimType(senderUserId: string): { const { user } = useAuth() const [claimType, setClaimType] = useState(BankClaimType.ReceiverKycNeeded) const { setSenderDetails } = useClaimBankFlow() - const { isUserBridgeKycApproved } = useKycStatus() + const { isUserKycApproved } = useKycStatus() useEffect(() => { const determineBankClaimType = async () => { // check if receiver (logged in user) exists and is KYC approved - const receiverKycApproved = isUserBridgeKycApproved + const receiverKycApproved = isUserKycApproved if (receiverKycApproved) { // condition 1: Receiver is KYC approved → UserBankClaim @@ -48,7 +49,7 @@ export function useDetermineBankClaimType(senderUserId: string): { try { const senderDetails = await getUserById(senderUserId) - const senderKycApproved = senderDetails?.bridgeKycStatus === 'approved' + const senderKycApproved = isUserKycVerified(senderDetails) if (senderKycApproved) { // condition 3: Receiver not KYC approved BUT sender is → GuestBankClaim diff --git a/src/hooks/useDetermineBankRequestType.ts b/src/hooks/useDetermineBankRequestType.ts index 5f6583fee..4e84b763c 100644 --- a/src/hooks/useDetermineBankRequestType.ts +++ b/src/hooks/useDetermineBankRequestType.ts @@ -3,6 +3,7 @@ import { useAuth } from '@/context/authContext' import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' import { useEffect, useState } from 'react' import useKycStatus from './useKycStatus' +import { isUserKycVerified } from '@/constants/kyc.consts' export enum BankRequestType { GuestBankRequest = 'guest-bank-request', @@ -23,11 +24,11 @@ export function useDetermineBankRequestType(requesterUserId: string): { const { user } = useAuth() const [requestType, setRequestType] = useState(BankRequestType.PayerKycNeeded) const { setRequesterDetails } = useRequestFulfillmentFlow() - const { isUserBridgeKycApproved } = useKycStatus() + const { isUserKycApproved } = useKycStatus() useEffect(() => { const determineBankRequestType = async () => { - const payerKycApproved = isUserBridgeKycApproved + const payerKycApproved = isUserKycApproved if (payerKycApproved) { setRequestType(BankRequestType.UserBankRequest) @@ -45,7 +46,7 @@ export function useDetermineBankRequestType(requesterUserId: string): { try { const requesterDetails = await getUserById(requesterUserId) - const requesterKycApproved = requesterDetails?.bridgeKycStatus === 'approved' + const requesterKycApproved = isUserKycVerified(requesterDetails) if (requesterKycApproved) { setRequesterDetails(requesterDetails) diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 87ea6ce20..ebe0241ab 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -19,6 +19,7 @@ import { type ChargeEntry } from '@/services/services.types' import { toWebAuthnKey } from '@zerodev/passkey-validator' import { USER_OPERATION_REVERT_REASON_TOPIC } from '@/constants/zerodev.consts' import { CHAIN_LOGOS, type ChainName } from '@/constants/rhino.consts' +import { isUserKycVerified } from '@/constants/kyc.consts' export function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4) @@ -984,7 +985,7 @@ export const getContributorsFromCharge = (charges: ChargeEntry[]) => { amount: charge.tokenAmount, username, fulfillmentPayment: charge.fulfillmentPayment, - isUserVerified: successfulPayment?.payerAccount?.user?.bridgeKycStatus === 'approved', + isUserVerified: isUserKycVerified(successfulPayment?.payerAccount?.user), isPeanutUser, } }) From 6055b42945cfa861b0f8187515c1311671e5002f Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:55:59 +0530 Subject: [PATCH 10/16] fix: use rails-based flags for limits display Replaced legacy isUserMantecaKycApproved with hasMantecaLimits from useLimits hook in BridgeLimitsView and LimitsPageView. Limits now derive from the rails-based /users/limits endpoint instead of direct KYC status checks. --- .../limits/hooks/useLimitsValidation.ts | 36 ++++++++----------- .../limits/views/BridgeLimitsView.tsx | 6 ++-- src/features/limits/views/LimitsPageView.tsx | 6 ++-- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/features/limits/hooks/useLimitsValidation.ts b/src/features/limits/hooks/useLimitsValidation.ts index b2743c350..406dc8e2d 100644 --- a/src/features/limits/hooks/useLimitsValidation.ts +++ b/src/features/limits/hooks/useLimitsValidation.ts @@ -2,7 +2,6 @@ import { useMemo } from 'react' import { useLimits } from '@/hooks/useLimits' -import useKycStatus from '@/hooks/useKycStatus' import type { MantecaLimit } from '@/interfaces' import { MAX_QR_PAYMENT_AMOUNT_FOREIGN, @@ -42,20 +41,17 @@ interface UseLimitsValidationOptions { } /** - * hook to validate amounts against user's transaction limits - * automatically determines if user is local (manteca) or foreign (bridge) based on their kyc status - * returns warning/blocking state based on remaining limits + * hook to validate amounts against user's transaction limits. + * uses the presence of API-returned limits (which are gated behind ENABLED rails on the backend) */ export function useLimitsValidation({ flowType, amount, currency: currencyInput }: UseLimitsValidationOptions) { const { mantecaLimits, bridgeLimits, isLoading, hasMantecaLimits, hasBridgeLimits } = useLimits() - const { isUserMantecaKycApproved, isUserBridgeKycApproved } = useKycStatus() // normalize currency to valid LimitCurrency type const currency = mapToLimitCurrency(currencyInput) - // determine if user is "local" (has manteca kyc for latam operations) - // this replaces the external isLocalUser parameter - const isLocalUser = isUserMantecaKycApproved + // determine if user is "local" (has manteca limits = enabled manteca rails) + const isLocalUser = hasMantecaLimits // parse amount to number - strip commas to handle "1,200" format const numericAmount = useMemo(() => { @@ -81,7 +77,7 @@ export function useLimitsValidation({ flowType, amount, currency: currencyInput // validate for manteca users (argentina/brazil) const mantecaValidation = useMemo(() => { - if (!isUserMantecaKycApproved || !relevantMantecaLimit) { + if (!hasMantecaLimits || !relevantMantecaLimit) { return { isBlocking: false, isWarning: false, @@ -155,12 +151,12 @@ export function useLimitsValidation({ flowType, amount, currency: currencyInput daysUntilReset: daysUntilMonthlyReset, limitCurrency: currency, } - }, [isUserMantecaKycApproved, relevantMantecaLimit, numericAmount, currency, daysUntilMonthlyReset, flowType]) + }, [hasMantecaLimits, relevantMantecaLimit, numericAmount, currency, daysUntilMonthlyReset, flowType]) // validate for bridge users (us/europe/mexico) - per transaction limits // bridge limits are always in USD const bridgeValidation = useMemo(() => { - if (!isUserBridgeKycApproved || !bridgeLimits) { + if (!hasBridgeLimits || !bridgeLimits) { return { isBlocking: false, isWarning: false, @@ -213,7 +209,7 @@ export function useLimitsValidation({ flowType, amount, currency: currencyInput daysUntilReset: null, limitCurrency: 'USD', } - }, [isUserBridgeKycApproved, bridgeLimits, flowType, numericAmount]) + }, [hasBridgeLimits, bridgeLimits, flowType, numericAmount]) // qr payment validation for foreign users (non-manteca kyc) // foreign qr limits are always in USD @@ -258,8 +254,8 @@ export function useLimitsValidation({ flowType, amount, currency: currencyInput const validation = useMemo(() => { // for qr payments if (flowType === 'qr-payment') { - // local users (manteca kyc) use manteca limits - if (isLocalUser && isUserMantecaKycApproved) { + // local users (manteca limits) use manteca limits + if (isLocalUser && hasMantecaLimits) { return mantecaValidation } // foreign users have fixed per-tx limit @@ -268,14 +264,14 @@ export function useLimitsValidation({ flowType, amount, currency: currencyInput // for onramp/offramp - check which provider applies // only use manteca if there's a relevant limit for the currency (prevents skipping bridge validation) - if (isUserMantecaKycApproved && hasMantecaLimits && relevantMantecaLimit) { + if (hasMantecaLimits && relevantMantecaLimit) { return mantecaValidation } - if (isUserBridgeKycApproved && hasBridgeLimits) { + if (hasBridgeLimits) { return bridgeValidation } - // no kyc - no limits to validate + // no enabled rails - no limits to validate return { isBlocking: false, isWarning: false, @@ -288,8 +284,6 @@ export function useLimitsValidation({ flowType, amount, currency: currencyInput }, [ flowType, isLocalUser, - isUserMantecaKycApproved, - isUserBridgeKycApproved, hasMantecaLimits, hasBridgeLimits, relevantMantecaLimit, @@ -305,7 +299,7 @@ export function useLimitsValidation({ flowType, amount, currency: currencyInput currency, // convenience getters hasLimits: hasMantecaLimits || hasBridgeLimits, - isMantecaUser: isUserMantecaKycApproved, - isBridgeUser: isUserBridgeKycApproved, + isMantecaUser: hasMantecaLimits, + isBridgeUser: hasBridgeLimits, } } diff --git a/src/features/limits/views/BridgeLimitsView.tsx b/src/features/limits/views/BridgeLimitsView.tsx index 8bbb6fc5e..8fa791ab0 100644 --- a/src/features/limits/views/BridgeLimitsView.tsx +++ b/src/features/limits/views/BridgeLimitsView.tsx @@ -4,7 +4,6 @@ import NavHeader from '@/components/Global/NavHeader' import Card from '@/components/Global/Card' import { Icon } from '@/components/Global/Icons/Icon' import { useLimits } from '@/hooks/useLimits' -import useKycStatus from '@/hooks/useKycStatus' import { useRouter } from 'next/navigation' import { MAX_QR_PAYMENT_AMOUNT_FOREIGN } from '@/constants/payment.consts' import Image from 'next/image' @@ -25,8 +24,7 @@ import EmptyState from '@/components/Global/EmptyStates/EmptyState' */ const BridgeLimitsView = () => { const router = useRouter() - const { bridgeLimits, isLoading, error } = useLimits() - const { isUserMantecaKycApproved } = useKycStatus() + const { bridgeLimits, isLoading, error, hasMantecaLimits } = useLimits() // url state for source region (where user came from) const [region] = useQueryState( @@ -90,7 +88,7 @@ const BridgeLimitsView = () => { )} {/* qr payment limits accordion - for bridge users without manteca kyc */} - {!isUserMantecaKycApproved && ( + {!hasMantecaLimits && (

QR payment limits:

diff --git a/src/features/limits/views/LimitsPageView.tsx b/src/features/limits/views/LimitsPageView.tsx index 4ea063318..8eb45ec8b 100644 --- a/src/features/limits/views/LimitsPageView.tsx +++ b/src/features/limits/views/LimitsPageView.tsx @@ -6,6 +6,7 @@ import NavHeader from '@/components/Global/NavHeader' import StatusBadge from '@/components/Global/Badges/StatusBadge' import { useIdentityVerification, type Region } from '@/hooks/useIdentityVerification' import useKycStatus from '@/hooks/useKycStatus' +import { useLimits } from '@/hooks/useLimits' import Image from 'next/image' import { useRouter } from 'next/navigation' import { useMemo } from 'react' @@ -18,7 +19,8 @@ import { getProviderRoute } from '../utils' const LimitsPageView = () => { const router = useRouter() const { unlockedRegions, lockedRegions } = useIdentityVerification() - const { isUserKycApproved, isUserBridgeKycUnderReview, isUserMantecaKycApproved } = useKycStatus() + const { isUserKycApproved, isUserBridgeKycUnderReview } = useKycStatus() + const { hasMantecaLimits } = useLimits() // check if user has any kyc at all const hasAnyKyc = isUserKycApproved @@ -63,7 +65,7 @@ const LimitsPageView = () => { {/* unlocked regions */} {unlockedRegions.length > 0 && ( - + )} {/* locked regions - only render if there are actual locked regions */} From 48cbe3193fbd8d8f63f944a365657a158fb49c68 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:56:11 +0530 Subject: [PATCH 11/16] fix: check provider rail status before showing regions as unlocked Regions now require provider rails to be in a functional state (ENABLED/REQUIRES_INFORMATION/REQUIRES_EXTRA_INFORMATION) before showing as unlocked. Prevents showing EU/US/MX as unlocked when Bridge submission failed. Also optimized isBridgeSupportedCountry to use O(1) lookups instead of allocating arrays per call. --- src/hooks/useIdentityVerification.tsx | 42 +++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/hooks/useIdentityVerification.tsx b/src/hooks/useIdentityVerification.tsx index e3f396532..548555b0c 100644 --- a/src/hooks/useIdentityVerification.tsx +++ b/src/hooks/useIdentityVerification.tsx @@ -77,6 +77,9 @@ const BRIDGE_SUPPORTED_LATAM_COUNTRIES: Region[] = [ }, ] +// precompute bridge alpha2 values for O(1) lookup +const BRIDGE_ALPHA2_SET = new Set(Object.values(BRIDGE_ALPHA3_TO_ALPHA2)) + /** maps a region path to the sumsub kyc template intent */ export const getRegionIntent = (regionPath: string): KYCRegionIntent => { return regionPath === 'latam' ? 'LATAM' : 'STANDARD' @@ -159,17 +162,31 @@ export const useIdentityVerification = () => { const isMantecaApproved = isUserMantecaKycApproved const isSumsubApproved = isUserSumsubKycApproved + // check if a provider's rails are in a functional state (not pending/failed) + const hasProviderAccess = (providerCode: string) => { + const providerRails = user?.rails?.filter((r) => r.rail.provider.code === providerCode) ?? [] + if (providerRails.length === 0) return false + return providerRails.some( + (r) => + r.status === 'ENABLED' || + r.status === 'REQUIRES_INFORMATION' || + r.status === 'REQUIRES_EXTRA_INFORMATION' + ) + } + // helper to check if a region should be unlocked const isRegionUnlocked = (regionName: string) => { // sumsub approval scoped by the regionIntent used during verification. - // 'LATAM' intent → unlocks LATAM. 'STANDARD' intent → unlocks Bridge regions + rest of world. - // no intent (or rest-of-world) → unlocks rest of world only. + // 'LATAM' intent → unlocks LATAM. 'STANDARD' intent → unlocks Bridge regions. + // rest of world is always unlocked with any sumsub approval (crypto features). + // provider-specific regions require the provider rails to be functional + // (not still PENDING from submission or FAILED). if (isSumsubApproved) { + if (regionName === 'Rest of the world') return true if (sumsubVerificationRegionIntent === 'LATAM') { - return MANTECA_SUPPORTED_REGIONS.includes(regionName) || regionName === 'Rest of the world' + return hasProviderAccess('MANTECA') && MANTECA_SUPPORTED_REGIONS.includes(regionName) } - // STANDARD intent covers bridge regions + rest of world - return BRIDGE_SUPPORTED_REGIONS.includes(regionName) || regionName === 'Rest of the world' + return hasProviderAccess('BRIDGE') && BRIDGE_SUPPORTED_REGIONS.includes(regionName) } return ( (isBridgeApproved && BRIDGE_SUPPORTED_REGIONS.includes(regionName)) || @@ -190,7 +207,13 @@ export const useIdentityVerification = () => { lockedRegions: locked, unlockedRegions: unlocked, } - }, [isUserBridgeKycApproved, isUserMantecaKycApproved, isUserSumsubKycApproved, sumsubVerificationRegionIntent]) + }, [ + isUserBridgeKycApproved, + isUserMantecaKycApproved, + isUserSumsubKycApproved, + sumsubVerificationRegionIntent, + user?.rails, + ]) /** * Check if a region is already unlocked by comparing region paths. @@ -270,12 +293,7 @@ export const useIdentityVerification = () => { const isBridgeSupportedCountry = useCallback((code: string) => { const upper = code.toUpperCase() - return ( - upper === 'US' || - upper === 'MX' || - Object.keys(BRIDGE_ALPHA3_TO_ALPHA2).includes(upper) || - Object.values(BRIDGE_ALPHA3_TO_ALPHA2).includes(upper) - ) + return upper === 'US' || upper === 'MX' || upper in BRIDGE_ALPHA3_TO_ALPHA2 || BRIDGE_ALPHA2_SET.has(upper) }, []) return { From 215d37eb24364c9426647d13c06d16444ff443b1 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:56:26 +0530 Subject: [PATCH 12/16] fix: kyc drawer SDK unresponsiveness, status labels, and DRY refactor - Fixed SDK unresponsiveness by closing drawer before opening Sumsub SDK (drawer z-50 was blocking SDK Dialog.Panel at z-10) - Extracted closeAndStartKyc helper to deduplicate 3 close+start patterns - Aligned status labels between activity list and drawer (Processing, Completed, Failed) - Fixed rejected state: hide RejectLabelsList in terminal rejection, fixed bottom border when Rejected On is the only card row - Extracted KycFailedContent shared component for drawer and modal - Renamed KycRejectedModal to KycFailedModal for naming consistency --- src/components/Kyc/CountryRegionRow.tsx | 4 +- src/components/Kyc/KycFailedContent.tsx | 24 ++++++++++ src/components/Kyc/KycStatusDrawer.tsx | 32 ++++++------- src/components/Kyc/KycStatusItem.tsx | 6 +-- ...ycRejectedModal.tsx => KycFailedModal.tsx} | 30 ++++-------- src/components/Kyc/states/KycFailed.tsx | 46 ++++++++----------- .../views/RegionsVerification.view.tsx | 4 +- 7 files changed, 77 insertions(+), 69 deletions(-) create mode 100644 src/components/Kyc/KycFailedContent.tsx rename src/components/Kyc/modals/{KycRejectedModal.tsx => KycFailedModal.tsx} (64%) diff --git a/src/components/Kyc/CountryRegionRow.tsx b/src/components/Kyc/CountryRegionRow.tsx index 00519a908..aa247b36d 100644 --- a/src/components/Kyc/CountryRegionRow.tsx +++ b/src/components/Kyc/CountryRegionRow.tsx @@ -4,9 +4,10 @@ import { CountryFlagAndName } from './CountryFlagAndName' interface CountryRegionRowProps { countryCode?: string | null isBridge?: boolean + hideBottomBorder?: boolean } -export const CountryRegionRow = ({ countryCode, isBridge }: CountryRegionRowProps) => { +export const CountryRegionRow = ({ countryCode, isBridge, hideBottomBorder }: CountryRegionRowProps) => { if (!isBridge && !countryCode) { return null } @@ -15,6 +16,7 @@ export const CountryRegionRow = ({ countryCode, isBridge }: CountryRegionRowProp } + hideBottomBorder={hideBottomBorder} /> ) } diff --git a/src/components/Kyc/KycFailedContent.tsx b/src/components/Kyc/KycFailedContent.tsx new file mode 100644 index 000000000..18151263f --- /dev/null +++ b/src/components/Kyc/KycFailedContent.tsx @@ -0,0 +1,24 @@ +import { RejectLabelsList } from './RejectLabelsList' +import InfoCard from '@/components/Global/InfoCard' + +interface KycFailedContentProps { + rejectLabels?: string[] | null + isTerminal: boolean +} + +// shared rejection details — used by both KycFailed (drawer) and KycFailedModal. +// renders reject labels (non-terminal) or terminal error info card. +export const KycFailedContent = ({ rejectLabels, isTerminal }: KycFailedContentProps) => { + if (isTerminal) { + return ( + + ) + } + + return +} diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index 5bb0144d9..d8ca56646 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -60,10 +60,15 @@ export const KycStatusDrawer = ({ regionIntent: statusCategory === 'completed' ? undefined : sumsubRegionIntent, }) - // all kyc retries now go through sumsub - const onRetry = async () => { - await sumsubFlow.handleInitiateKyc() - } + // close drawer but keep mounted so SumsubKycModals persists, then start kyc + const closeAndStartKyc = useCallback( + async (regionIntent?: KYCRegionIntent, levelName?: string) => { + onKeepMounted?.(true) + onClose() + await sumsubFlow.handleInitiateKyc(regionIntent, levelName) + }, + [onKeepMounted, onClose, sumsubFlow] + ) // check if any bridge rail needs additional documents const bridgeRailsNeedingDocs = (user?.rails ?? []).filter( @@ -86,22 +91,17 @@ export const KycStatusDrawer = ({ const sumsubFailureCount = user?.user?.kycVerifications?.filter((v) => v.provider === 'SUMSUB' && v.status === 'REJECTED').length ?? 0 - const handleSubmitAdditionalDocs = async () => { - await sumsubFlow.handleInitiateKyc(undefined, 'peanut-additional-docs') - } + const handleSubmitAdditionalDocs = useCallback( + () => closeAndStartKyc(undefined, 'peanut-additional-docs'), + [closeAndStartKyc] + ) const renderContent = () => { // user initiated kyc but abandoned before submitting — close drawer visually // but keep component mounted so SumsubKycModals persists for the SDK flow if (verification && isKycStatusNotStarted(status)) { return ( - { - onKeepMounted?.(true) - onClose() - sumsubFlow.handleInitiateKyc() - }} - /> + ) } @@ -136,7 +136,7 @@ export const KycStatusDrawer = ({ case 'action_required': return ( @@ -152,7 +152,7 @@ export const KycStatusDrawer = ({ bridgeKycRejectedAt={verification?.updatedAt ?? user?.user?.bridgeKycRejectedAt} countryCode={countryCode ?? undefined} isBridge={isBridgeKyc} - onRetry={onRetry} + onRetry={closeAndStartKyc} isLoading={sumsubFlow.isLoading} /> ) diff --git a/src/components/Kyc/KycStatusItem.tsx b/src/components/Kyc/KycStatusItem.tsx index fb8086e97..45e04b036 100644 --- a/src/components/Kyc/KycStatusItem.tsx +++ b/src/components/Kyc/KycStatusItem.tsx @@ -82,9 +82,9 @@ export const KycStatusItem = ({ const subtitle = useMemo(() => { if (isInitiatedButNotStarted) return 'Not completed' if (isActionRequired) return 'Action needed' - if (isPending) return 'Under review' - if (isApproved) return 'Approved' - if (isRejected) return 'Rejected' + if (isPending) return 'Processing' + if (isApproved) return 'Completed' + if (isRejected) return 'Failed' return 'Unknown' }, [isInitiatedButNotStarted, isActionRequired, isPending, isApproved, isRejected]) diff --git a/src/components/Kyc/modals/KycRejectedModal.tsx b/src/components/Kyc/modals/KycFailedModal.tsx similarity index 64% rename from src/components/Kyc/modals/KycRejectedModal.tsx rename to src/components/Kyc/modals/KycFailedModal.tsx index 9d2aece39..ab3a1f12d 100644 --- a/src/components/Kyc/modals/KycRejectedModal.tsx +++ b/src/components/Kyc/modals/KycFailedModal.tsx @@ -1,11 +1,10 @@ import { useMemo } from 'react' import ActionModal from '@/components/Global/ActionModal' -import InfoCard from '@/components/Global/InfoCard' -import { RejectLabelsList } from '../RejectLabelsList' +import { KycFailedContent } from '../KycFailedContent' import { isTerminalRejection } from '@/constants/sumsub-reject-labels.consts' import { useModalsContext } from '@/context/ModalsContext' -interface KycRejectedModalProps { +interface KycFailedModalProps { visible: boolean onClose: () => void onRetry: () => void @@ -16,7 +15,7 @@ interface KycRejectedModalProps { } // shown when user clicks a locked region while their kyc is rejected -export const KycRejectedModal = ({ +export const KycFailedModal = ({ visible, onClose, onRetry, @@ -24,7 +23,7 @@ export const KycRejectedModal = ({ rejectLabels, rejectType, failureCount, -}: KycRejectedModalProps) => { +}: KycFailedModalProps) => { const { setIsSupportModalOpen } = useModalsContext() const isTerminal = useMemo( @@ -36,24 +35,13 @@ export const KycRejectedModal = ({ - - {isTerminal && ( - - )} +
+
} ctas={[ diff --git a/src/components/Kyc/states/KycFailed.tsx b/src/components/Kyc/states/KycFailed.tsx index 991ff9a3f..493a4ef8b 100644 --- a/src/components/Kyc/states/KycFailed.tsx +++ b/src/components/Kyc/states/KycFailed.tsx @@ -1,9 +1,8 @@ import { Button } from '@/components/0_Bruddle/Button' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { KYCStatusDrawerItem } from '../KYCStatusDrawerItem' -import { RejectLabelsList } from '../RejectLabelsList' +import { KycFailedContent } from '../KycFailedContent' import Card from '@/components/Global/Card' -import InfoCard from '@/components/Global/InfoCard' import { useMemo } from 'react' import { formatDate } from '@/utils/general.utils' import { CountryRegionRow } from '../CountryRegionRow' @@ -54,39 +53,34 @@ export const KycFailed = ({ [isSumsub, rejectType, failureCount, rejectLabels] ) - // formatted bridge reason (legacy display) - const formattedBridgeReason = useMemo(() => { - const reasonText = bridgeReason || 'There was an issue. Contact Support.' - const lines = reasonText.split(/\\n|\n/).filter((line) => line.trim() !== '') - if (lines.length === 1) return reasonText - return ( -
    - {lines.map((line, index) => ( -
  • {line}
  • - ))} -
- ) - }, [bridgeReason]) + // determine which row is last in the card for border handling + const hasCountryRow = isBridge || !!countryCode + const hasReasonRow = !isSumsub return (
- - - - {!isSumsub && } + + + + {hasReasonRow && ( + + )} - {isSumsub && } + {isSumsub && } {isTerminal ? ( -
- +
{/* TODO: auto-create crisp support ticket on terminal rejection */}