From fda834576dd468b09decd0aa64da125ea6395722 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:28:06 +0530 Subject: [PATCH 01/15] feat: manteca onboarding widget service --- src/services/manteca.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/services/manteca.ts diff --git a/src/services/manteca.ts b/src/services/manteca.ts new file mode 100644 index 000000000..40816647d --- /dev/null +++ b/src/services/manteca.ts @@ -0,0 +1,23 @@ +import { PEANUT_API_URL } from '@/constants' +import { fetchWithSentry } from '@/utils' +import Cookies from 'js-cookie' + +export const mantecaApi = { + initiateOnboarding: async (params: { returnUrl: string; failureUrl?: string }): Promise<{ url: string }> => { + const response = await fetchWithSentry(`${PEANUT_API_URL}/manteca/initiate-onboarding`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Cookies.get('jwt-token')}`, + }, + body: JSON.stringify(params), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.message || `Failed to get onboarding URL`) + } + + return response.json() + }, +} From efe9139e6180820b96d757aba2f21f8d12c06d4a Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:28:46 +0530 Subject: [PATCH 02/15] feat: useMantecaKyc hook --- src/hooks/useMantecaKycFlow.ts | 71 ++++++++++++++++++++++++++++++++++ src/interfaces/interfaces.ts | 5 +++ 2 files changed, 76 insertions(+) create mode 100644 src/hooks/useMantecaKycFlow.ts diff --git a/src/hooks/useMantecaKycFlow.ts b/src/hooks/useMantecaKycFlow.ts new file mode 100644 index 000000000..157043643 --- /dev/null +++ b/src/hooks/useMantecaKycFlow.ts @@ -0,0 +1,71 @@ +import { useCallback, useEffect, useState } from 'react' +import type { IFrameWrapperProps } from '@/components/Global/IframeWrapper' +import { mantecaApi } from '@/services/manteca' +import { useAuth } from '@/context/authContext' + +type UseMantecaKycFlowOptions = { + onClose?: () => void + onSuccess?: () => void + onManualClose?: () => void +} + +export const useMantecaKycFlow = ({ onClose, onSuccess, onManualClose }: UseMantecaKycFlowOptions = {}) => { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [iframeOptions, setIframeOptions] = useState>({ + src: '', + visible: false, + closeConfirmMessage: undefined, + }) + const { user } = useAuth() + const [isMantecaKycRequired, setNeedsMantecaKyc] = useState( + !user?.user?.mantecaKycStatus || user.user.mantecaKycStatus !== 'ACTIVE' + ) + + useEffect(() => { + setNeedsMantecaKyc(!user?.user?.mantecaKycStatus || user.user.mantecaKycStatus !== 'ACTIVE') + }, [user?.user?.mantecaKycStatus]) + + const openMantecaKyc = useCallback(async () => { + setIsLoading(true) + setError(null) + try { + const { url } = await mantecaApi.initiateOnboarding({ returnUrl: window.location.href }) + setIframeOptions({ + src: url, + visible: true, + }) + return { success: true as const } + } catch (e: any) { + setError(e?.message ?? 'Failed to initiate onboarding') + return { success: false as const, error: e?.message as string } + } finally { + setIsLoading(false) + } + }, []) + + const handleIframeClose = useCallback( + (source?: 'manual' | 'completed' | 'tos_accepted') => { + setIframeOptions((prev) => ({ ...prev, visible: false })) + if (source === 'completed') { + onSuccess?.() + return + } + if (source === 'manual') { + onManualClose?.() + return + } + onClose?.() + }, + [onClose, onSuccess, onManualClose] + ) + + return { + isLoading, + error, + iframeOptions, + openMantecaKyc, + handleIframeClose, + isMantecaKycRequired, + } +} diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 8451b10bc..2b10b5f5e 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -221,6 +221,8 @@ interface ReferralConnection { account_identifier: string } +export type MantecaKycStatus = 'ONBOARDING' | 'ACTIVE' | 'INACTIVE' + export interface User { userId: string email: string @@ -230,6 +232,9 @@ export interface User { bridgeKycStartedAt?: string bridgeKycApprovedAt?: string bridgeKycRejectedAt?: string + mantecaKycStatus: MantecaKycStatus + mantecaKycStartedAt?: string + mantecaKycApprovedAt?: string tosStatus?: string tosAcceptedAt?: string bridgeCustomerId: string | null From 3afb9afb84507cc9cc4ab397fb104004fd72cf4e Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:29:19 +0530 Subject: [PATCH 03/15] feat: update manteca deposit flow to handle kyc --- .../RegionalMethods/MercadoPago/index.tsx | 53 +++++++++++++++---- .../Kyc/InitiateMantecaKYCModal.tsx | 45 ++++++++++++++++ 2 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 src/components/Kyc/InitiateMantecaKYCModal.tsx diff --git a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx index c7dd60745..dc3ab7194 100644 --- a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx +++ b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx @@ -1,24 +1,42 @@ -import React, { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import MercadoPagoDepositDetails from './MercadoPagoDepositDetails' import InputAmountStep from '../../InputAmountStep' import { createMantecaOnramp } from '@/app/actions/onramp' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { countryData } from '@/components/AddMoney/consts' import { MantecaDepositDetails } from '@/types/manteca.types' +import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal' +import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' const MercadoPago = () => { const params = useParams() + const router = useRouter() const [step, setStep] = useState('inputAmount') const [isCreatingDeposit, setIsCreatingDeposit] = useState(false) const [tokenAmount, setTokenAmount] = useState('') const [tokenUSDAmount, setTokenUSDAmount] = useState('') const [error, setError] = useState(null) const [depositDetails, setDepositDetails] = useState() + const [isKycModalOpen, setIsKycModalOpen] = useState(false) const selectedCountryPath = params.country as string const selectedCountry = useMemo(() => { return countryData.find((country) => country.type === 'country' && country.path === selectedCountryPath) }, [selectedCountryPath]) + const { isMantecaKycRequired } = useMantecaKycFlow() + + useEffect(() => { + if (isMantecaKycRequired) { + setIsKycModalOpen(true) + } + }, [isMantecaKycRequired]) + + const handleKycCancel = () => { + setIsKycModalOpen(false) + if (selectedCountry?.path) { + router.push(`/add-money/${selectedCountry.path}`) + } + } const handleAmountSubmit = async () => { if (!selectedCountry?.currency) return @@ -48,15 +66,28 @@ const MercadoPago = () => { if (step === 'inputAmount') { return ( - + <> + + {isKycModalOpen && ( + { + // close the modal and let the user continue with amount input + setIsKycModalOpen(false) + }} + /> + )} + ) } diff --git a/src/components/Kyc/InitiateMantecaKYCModal.tsx b/src/components/Kyc/InitiateMantecaKYCModal.tsx new file mode 100644 index 000000000..d6dd9e65d --- /dev/null +++ b/src/components/Kyc/InitiateMantecaKYCModal.tsx @@ -0,0 +1,45 @@ +import ActionModal from '@/components/Global/ActionModal' +import IframeWrapper from '@/components/Global/IframeWrapper' +import { IconName } from '@/components/Global/Icons/Icon' +import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' + +interface Props { + isOpen: boolean + onClose: () => void + onKycSuccess?: () => void + onManualClose?: () => void +} + +export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManualClose }: Props) => { + const { isLoading, iframeOptions, openMantecaKyc, handleIframeClose } = useMantecaKycFlow({ + onClose: onManualClose, // any non-success close from iframe is a manual close in case of Manteca KYC + onSuccess: onKycSuccess, + onManualClose, + }) + + return ( + <> + openMantecaKyc(), + variant: 'purple', + disabled: isLoading, + shadowSize: '4', + icon: 'check-circle', + className: 'h-11', + }, + ]} + /> + + + ) +} From 781501a02bcb1bd45d9d04018da98a218cbd8b78 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:50:32 +0530 Subject: [PATCH 04/15] fix: rename useKycFlow to useBridgeKycFlow --- src/app/(mobile-ui)/history/page.tsx | 2 +- src/components/Home/HomeHistory.tsx | 2 +- src/components/Kyc/KycFlow.tsx | 4 ++-- src/components/Kyc/KycStatusDrawer.tsx | 4 ++-- src/components/Kyc/index.tsx | 4 ++-- src/hooks/{useKycFlow.ts => useBridgeKycFlow.ts} | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename src/hooks/{useKycFlow.ts => useBridgeKycFlow.ts} (98%) diff --git a/src/app/(mobile-ui)/history/page.tsx b/src/app/(mobile-ui)/history/page.tsx index 7e619591c..f0104a42d 100644 --- a/src/app/(mobile-ui)/history/page.tsx +++ b/src/app/(mobile-ui)/history/page.tsx @@ -13,7 +13,7 @@ import { useUserStore } from '@/redux/hooks' import { formatGroupHeaderDate, getDateGroup, getDateGroupKey } from '@/utils/dateGrouping.utils' import * as Sentry from '@sentry/nextjs' import { usePathname } from 'next/navigation' -import { isKycStatusItem } from '@/hooks/useKycFlow' +import { isKycStatusItem } from '@/hooks/useBridgeKycFlow' import React, { useEffect, useMemo, useRef } from 'react' /** diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index f694a4713..e2570476e 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -14,7 +14,7 @@ import { twMerge } from 'tailwind-merge' import Card, { CardPosition, getCardPosition } from '../Global/Card' import EmptyState from '../Global/EmptyStates/EmptyState' import { KycStatusItem } from '../Kyc/KycStatusItem' -import { isKycStatusItem, KycHistoryEntry } from '@/hooks/useKycFlow' +import { isKycStatusItem, KycHistoryEntry } from '@/hooks/useBridgeKycFlow' import { BridgeKycStatus } from '@/utils' import { useWallet } from '@/hooks/wallet/useWallet' import { useUserInteractions } from '@/hooks/useUserInteractions' diff --git a/src/components/Kyc/KycFlow.tsx b/src/components/Kyc/KycFlow.tsx index 2a6504aa8..383c699ea 100644 --- a/src/components/Kyc/KycFlow.tsx +++ b/src/components/Kyc/KycFlow.tsx @@ -1,12 +1,12 @@ import { Button, ButtonProps } from '@/components/0_Bruddle/Button' import IframeWrapper from '@/components/Global/IframeWrapper' -import { useKycFlow } from '@/hooks/useKycFlow' +import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' // this component is the main entry point for the kyc flow // it renders a button that, when clicked, initiates the process of fetching // tos/kyc links, showing them in an iframe, and then displaying a status modal export const KycFlow = (props: ButtonProps) => { - const { isLoading, error, iframeOptions, handleInitiateKyc, handleIframeClose } = useKycFlow() + const { isLoading, error, iframeOptions, handleInitiateKyc, handleIframeClose } = useBridgeKycFlow() return ( <> diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index 07b2f25f1..f624b97f9 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -6,7 +6,7 @@ import PeanutLoading from '@/components/Global/PeanutLoading' import { Drawer, DrawerContent } from '../Global/Drawer' import { BridgeKycStatus } from '@/utils' import { getKycDetails } from '@/app/actions/users' -import { useKycFlow } from '@/hooks/useKycFlow' +import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' import IFrameWrapper from '../Global/IframeWrapper' // a helper to categorize the kyc status from the user object @@ -54,7 +54,7 @@ export const KycStatusDrawer = ({ iframeOptions, handleIframeClose, isLoading: isKycFlowLoading, - } = useKycFlow({ onKycSuccess: onClose }) + } = useBridgeKycFlow({ onKycSuccess: onClose }) const statusCategory = getKycStatusCategory(bridgeKycStatus) diff --git a/src/components/Kyc/index.tsx b/src/components/Kyc/index.tsx index 937a0390f..9e62a2737 100644 --- a/src/components/Kyc/index.tsx +++ b/src/components/Kyc/index.tsx @@ -1,5 +1,5 @@ import ActionModal from '@/components/Global/ActionModal' -import { useKycFlow } from '@/hooks/useKycFlow' +import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' import IframeWrapper from '@/components/Global/IframeWrapper' import { KycVerificationInProgressModal } from './KycVerificationInProgressModal' import { IconName } from '@/components/Global/Icons/Icon' @@ -21,7 +21,7 @@ export const InitiateKYCModal = ({ isOpen, onClose, onKycSuccess, onManualClose, handleInitiateKyc, handleIframeClose, closeVerificationProgressModal, - } = useKycFlow({ onKycSuccess, flow, onManualClose }) + } = useBridgeKycFlow({ onKycSuccess, flow, onManualClose }) const handleVerifyClick = async () => { const result = await handleInitiateKyc() diff --git a/src/hooks/useKycFlow.ts b/src/hooks/useBridgeKycFlow.ts similarity index 98% rename from src/hooks/useKycFlow.ts rename to src/hooks/useBridgeKycFlow.ts index e652a3145..e78e90037 100644 --- a/src/hooks/useKycFlow.ts +++ b/src/hooks/useBridgeKycFlow.ts @@ -35,7 +35,7 @@ export const isKycStatusItem = (entry: object): entry is KycHistoryEntry => { return 'isKyc' in entry && entry.isKyc === true } -export const useKycFlow = ({ onKycSuccess, flow, onManualClose }: UseKycFlowOptions = {}) => { +export const useBridgeKycFlow = ({ onKycSuccess, flow, onManualClose }: UseKycFlowOptions = {}) => { const { user } = useUserStore() const router = useRouter() const isMounted = useRef(false) From 4228d45b9db2a72f6666c678dcbb4562d8868131 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:38:03 +0530 Subject: [PATCH 05/15] feat: handle manteca kyc completion flow --- .../[country]/[regional-method]/page.tsx | 9 +++- src/app/(mobile-ui)/kyc/success/page.tsx | 26 +++++++++++ .../AddMoney/components/InputAmountStep.tsx | 11 +++-- .../RegionalMethods/MercadoPago/index.tsx | 26 ++++++++++- src/components/AddMoney/consts/index.ts | 23 ++++++++-- .../AddWithdraw/AddWithdrawCountriesList.tsx | 7 +-- src/components/Global/Footer/index.tsx | 44 ------------------- .../Kyc/InitiateMantecaKYCModal.tsx | 6 ++- src/hooks/useMantecaKycFlow.ts | 10 ++++- src/hooks/useWebSocket.ts | 34 ++++++++++++-- src/services/manteca.ts | 6 ++- src/services/websocket.ts | 22 +++++++++- 12 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 src/app/(mobile-ui)/kyc/success/page.tsx delete mode 100644 src/components/Global/Footer/index.tsx diff --git a/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx b/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx index 981976742..3102ff84a 100644 --- a/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx @@ -1,5 +1,7 @@ 'use client' import MercadoPago from '@/components/AddMoney/components/RegionalMethods/MercadoPago' +import { CountryData, countryData } from '@/components/AddMoney/consts' +import { MantecaSupportedExchanges } from '@/components/AddMoney/consts' import { useParams } from 'next/navigation' export default function AddMoneyRegionalMethodPage() { @@ -7,7 +9,12 @@ export default function AddMoneyRegionalMethodPage() { const country = params.country as string const method = params['regional-method'] as string - if (country === 'argentina' && method === 'mercadopago') { + const countryDetails: CountryData | undefined = countryData.find((c) => c.path === country) + + if ( + MantecaSupportedExchanges[countryDetails?.id as keyof typeof MantecaSupportedExchanges] && + method === 'mercadopago' + ) { return } diff --git a/src/app/(mobile-ui)/kyc/success/page.tsx b/src/app/(mobile-ui)/kyc/success/page.tsx new file mode 100644 index 000000000..ad4c241ba --- /dev/null +++ b/src/app/(mobile-ui)/kyc/success/page.tsx @@ -0,0 +1,26 @@ +'use client' + +import { useEffect } from 'react' +import Image from 'next/image' +import { HandThumbsUp } from '@/assets' + +/* +This page is just to let users know that their KYC was successful. Incase there's some issue with webosckets closing the modal, ideally this should not happen but added this as fallback guide +*/ +export default function KycSuccessPage() { + useEffect(() => { + if (window.parent) { + window.parent.postMessage({ source: 'peanut-kyc-success' }, '*') + } + }, []) + + return ( +
+ Peanut HandThumbsUp +
+

Verification successful!

+

You can now close this window.

+
+
+ ) +} diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx index 2c07421bb..9ff99419f 100644 --- a/src/components/AddMoney/components/InputAmountStep.tsx +++ b/src/components/AddMoney/components/InputAmountStep.tsx @@ -5,34 +5,33 @@ import { Icon } from '@/components/Global/Icons/Icon' import NavHeader from '@/components/Global/NavHeader' import TokenAmountInput from '@/components/Global/TokenAmountInput' import { useRouter } from 'next/navigation' -import { CountryData } from '../consts' import ErrorAlert from '@/components/Global/ErrorAlert' import { useCurrency } from '@/hooks/useCurrency' import PeanutLoading from '@/components/Global/PeanutLoading' +type ICurrency = ReturnType interface InputAmountStepProps { onSubmit: () => void - selectedCountry: CountryData isLoading: boolean tokenAmount: string setTokenAmount: React.Dispatch> setTokenUSDAmount: React.Dispatch> error: string | null + currencyData?: ICurrency } const InputAmountStep = ({ tokenAmount, setTokenAmount, onSubmit, - selectedCountry, isLoading, error, setTokenUSDAmount, + currencyData, }: InputAmountStepProps) => { const router = useRouter() - const currencyData = useCurrency(selectedCountry.currency ?? 'ARS') - if (currencyData.isLoading) { + if (currencyData?.isLoading) { return } @@ -49,7 +48,7 @@ const InputAmountStep = ({ hideCurrencyToggle setUsdValue={(e) => setTokenUSDAmount(e)} currency={ - currencyData + currencyData && currencyData.price && currencyData.price > 0 ? { code: currencyData.code!, symbol: currencyData.symbol!, diff --git a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx index dc3ab7194..655c38ba2 100644 --- a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx +++ b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx @@ -7,6 +7,9 @@ import { countryData } from '@/components/AddMoney/consts' import { MantecaDepositDetails } from '@/types/manteca.types' import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal' import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' +import { useCurrency } from '@/hooks/useCurrency' +import { useAuth } from '@/context/authContext' +import { useWebSocket } from '@/hooks/useWebSocket' const MercadoPago = () => { const params = useParams() @@ -24,6 +27,20 @@ const MercadoPago = () => { return countryData.find((country) => country.type === 'country' && country.path === selectedCountryPath) }, [selectedCountryPath]) const { isMantecaKycRequired } = useMantecaKycFlow() + const currencyData = useCurrency(selectedCountry?.currency ?? 'ARS') + const { user, fetchUser } = useAuth() + + useWebSocket({ + username: user?.user.username ?? undefined, + autoConnect: !!user?.user.username, + onMantecaKycStatusUpdate: (newStatus) => { + // listen for manteca kyc status updates, either when the user is approved or when the widget is finished to continue with the flow + if (newStatus === 'ACTIVE' || newStatus === 'WIDGET_FINISHED') { + fetchUser() + setIsKycModalOpen(false) + } + }, + }) useEffect(() => { if (isMantecaKycRequired) { @@ -41,6 +58,11 @@ const MercadoPago = () => { const handleAmountSubmit = async () => { if (!selectedCountry?.currency) return + if (isMantecaKycRequired) { + setIsKycModalOpen(true) + return + } + try { setError(null) setIsCreatingDeposit(true) @@ -71,10 +93,10 @@ const MercadoPago = () => { tokenAmount={tokenAmount} setTokenAmount={setTokenAmount} onSubmit={handleAmountSubmit} - selectedCountry={selectedCountry} isLoading={isCreatingDeposit} error={error} setTokenUSDAmount={setTokenUSDAmount} + currencyData={currencyData} /> {isKycModalOpen && ( { onKycSuccess={() => { // close the modal and let the user continue with amount input setIsKycModalOpen(false) + fetchUser() }} + country={selectedCountry} /> )} diff --git a/src/components/AddMoney/consts/index.ts b/src/components/AddMoney/consts/index.ts index 6f7a8e155..03dcb56b4 100644 --- a/src/components/AddMoney/consts/index.ts +++ b/src/components/AddMoney/consts/index.ts @@ -4,6 +4,20 @@ import { METAMASK_LOGO, RAINBOW_LOGO, TRUST_WALLET_LOGO } from '@/assets/wallets import { IconName } from '@/components/Global/Icons/Icon' import { StaticImageData } from 'next/image' +// ref: https://docs.manteca.dev/cripto/key-concepts/exchanges-multi-country#Available-Exchanges +export const MantecaSupportedExchanges = { + AR: 'ARGENTINA', + CL: 'CHILE', + BR: 'BRAZIL', + CO: 'COLOMBIA', + PA: 'PANAMA', + CR: 'COSTA_RICA', + GT: 'GUATEMALA', + MX: 'MEXICO', + PH: 'PHILIPPINES', + BO: 'BOLIVIA', +} + export interface CryptoSource { id: string name: string @@ -2099,7 +2113,7 @@ countryData.forEach((country) => { // filter add methods: include Mercado Pago only for LATAM countries const currentAddMethods = UPDATED_DEFAULT_ADD_MONEY_METHODS.filter((method) => { if (method.id === 'mercado-pago-add') { - return LATAM_COUNTRY_CODES.includes(countryCode) + return !!MantecaSupportedExchanges[countryCode as keyof typeof MantecaSupportedExchanges] } return true }).map((m) => { @@ -2110,10 +2124,13 @@ countryData.forEach((country) => { } else if (newMethod.id === 'crypto-add') { newMethod.path = `/add-money/crypto` newMethod.isSoon = false - } else if (newMethod.id === 'mercado-pago-add' && countryCode === 'AR') { + } else if ( + newMethod.id === 'mercado-pago-add' && + MantecaSupportedExchanges[countryCode as keyof typeof MantecaSupportedExchanges] + ) { newMethod.isSoon = false newMethod.path = `/add-money/${country.path}/mercadopago` - } else { + } else if (newMethod.id === 'mercado-pago-add') { newMethod.isSoon = true } return newMethod diff --git a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx index 2149d9f33..e7e33e228 100644 --- a/src/components/AddWithdraw/AddWithdrawCountriesList.tsx +++ b/src/components/AddWithdraw/AddWithdrawCountriesList.tsx @@ -1,11 +1,6 @@ 'use client' -import { - COUNTRY_SPECIFIC_METHODS, - countryCodeMap, - countryData, - SpecificPaymentMethod, -} from '@/components/AddMoney/consts' +import { COUNTRY_SPECIFIC_METHODS, countryData, SpecificPaymentMethod } from '@/components/AddMoney/consts' import StatusBadge from '@/components/Global/Badges/StatusBadge' import { IconName } from '@/components/Global/Icons/Icon' import NavHeader from '@/components/Global/NavHeader' diff --git a/src/components/Global/Footer/index.tsx b/src/components/Global/Footer/index.tsx deleted file mode 100644 index c03717120..000000000 --- a/src/components/Global/Footer/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import Link from 'next/link' - -import * as _consts from './consts' - -const Footer = () => { - return ( -
-
-
- {_consts.SOCIALS.map((social) => { - return ( - - {social.name} - - ) - })} -
- {/* note: temporarily removed links from fotter */} - {/*
- {_consts.LINKS.map((link) => { - return ( - - {link.name} - - ) - })} -
*/} -
-
- ) -} - -export default Footer diff --git a/src/components/Kyc/InitiateMantecaKYCModal.tsx b/src/components/Kyc/InitiateMantecaKYCModal.tsx index d6dd9e65d..8681a8aef 100644 --- a/src/components/Kyc/InitiateMantecaKYCModal.tsx +++ b/src/components/Kyc/InitiateMantecaKYCModal.tsx @@ -2,15 +2,17 @@ import ActionModal from '@/components/Global/ActionModal' import IframeWrapper from '@/components/Global/IframeWrapper' import { IconName } from '@/components/Global/Icons/Icon' import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' +import { CountryData } from '@/components/AddMoney/consts' interface Props { isOpen: boolean onClose: () => void onKycSuccess?: () => void onManualClose?: () => void + country?: CountryData } -export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManualClose }: Props) => { +export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManualClose, country }: Props) => { const { isLoading, iframeOptions, openMantecaKyc, handleIframeClose } = useMantecaKycFlow({ onClose: onManualClose, // any non-success close from iframe is a manual close in case of Manteca KYC onSuccess: onKycSuccess, @@ -30,7 +32,7 @@ export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManua ctas={[ { text: isLoading ? 'Loading...' : 'Verify now', - onClick: () => openMantecaKyc(), + onClick: () => openMantecaKyc(country), variant: 'purple', disabled: isLoading, shadowSize: '4', diff --git a/src/hooks/useMantecaKycFlow.ts b/src/hooks/useMantecaKycFlow.ts index 157043643..7f7434cc8 100644 --- a/src/hooks/useMantecaKycFlow.ts +++ b/src/hooks/useMantecaKycFlow.ts @@ -2,6 +2,8 @@ import { useCallback, useEffect, useState } from 'react' import type { IFrameWrapperProps } from '@/components/Global/IframeWrapper' import { mantecaApi } from '@/services/manteca' import { useAuth } from '@/context/authContext' +import { CountryData, MantecaSupportedExchanges } from '@/components/AddMoney/consts' +import { BASE_URL } from '@/constants' type UseMantecaKycFlowOptions = { onClose?: () => void @@ -26,11 +28,15 @@ export const useMantecaKycFlow = ({ onClose, onSuccess, onManualClose }: UseMant setNeedsMantecaKyc(!user?.user?.mantecaKycStatus || user.user.mantecaKycStatus !== 'ACTIVE') }, [user?.user?.mantecaKycStatus]) - const openMantecaKyc = useCallback(async () => { + const openMantecaKyc = useCallback(async (country?: CountryData) => { setIsLoading(true) setError(null) try { - const { url } = await mantecaApi.initiateOnboarding({ returnUrl: window.location.href }) + const exchange = country?.id + ? MantecaSupportedExchanges[country.id as keyof typeof MantecaSupportedExchanges] + : MantecaSupportedExchanges.AR + const returnUrl = BASE_URL + '/kyc/success' + const { url } = await mantecaApi.initiateOnboarding({ returnUrl, exchange }) setIframeOptions({ src: url, visible: true, diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts index dc6a26ddd..331283c84 100644 --- a/src/hooks/useWebSocket.ts +++ b/src/hooks/useWebSocket.ts @@ -9,6 +9,7 @@ interface UseWebSocketOptions { username?: string onHistoryEntry?: (entry: HistoryEntry) => void onKycStatusUpdate?: (status: string) => void + onMantecaKycStatusUpdate?: (status: string) => void onTosUpdate?: (data: { accepted: boolean }) => void onConnect?: () => void onDisconnect?: () => void @@ -21,6 +22,7 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { username, onHistoryEntry, onKycStatusUpdate, + onMantecaKycStatusUpdate, onTosUpdate, onConnect, onDisconnect, @@ -31,12 +33,28 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { const [historyEntries, setHistoryEntries] = useState([]) const wsRef = useRef(null) - const callbacksRef = useRef({ onHistoryEntry, onKycStatusUpdate, onTosUpdate, onConnect, onDisconnect, onError }) + const callbacksRef = useRef({ + onHistoryEntry, + onKycStatusUpdate, + onMantecaKycStatusUpdate, + onTosUpdate, + onConnect, + onDisconnect, + onError, + }) // Update callbacks ref when useEffect(() => { - callbacksRef.current = { onHistoryEntry, onKycStatusUpdate, onTosUpdate, onConnect, onDisconnect, onError } - }, [onHistoryEntry, onKycStatusUpdate, onTosUpdate, onConnect, onDisconnect, onError]) + callbacksRef.current = { + onHistoryEntry, + onKycStatusUpdate, + onMantecaKycStatusUpdate, + onTosUpdate, + onConnect, + onDisconnect, + onError, + } + }, [onHistoryEntry, onKycStatusUpdate, onMantecaKycStatusUpdate, onTosUpdate, onConnect, onDisconnect, onError]) // Connect to WebSocket const connect = useCallback(() => { @@ -115,6 +133,14 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { } } + const handleMantecaKycStatusUpdate = (data: { status: string }) => { + if (callbacksRef.current.onMantecaKycStatusUpdate) { + callbacksRef.current.onMantecaKycStatusUpdate(data.status) + } else { + console.log(`[WebSocket] No onMantecaKycStatusUpdate callback registered for user: ${username}`) + } + } + const handleTosUpdate = (data: { status: string }) => { if (callbacksRef.current.onTosUpdate) { callbacksRef.current.onTosUpdate({ accepted: data.status === 'approved' }) @@ -129,6 +155,7 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { ws.on('error', handleError) ws.on('history_entry', handleHistoryEntry) ws.on('kyc_status_update', handleKycStatusUpdate) + ws.on('manteca_kyc_status_update', handleMantecaKycStatusUpdate) ws.on('persona_tos_status_update', handleTosUpdate) // Auto-connect if enabled @@ -143,6 +170,7 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { ws.off('error', handleError) ws.off('history_entry', handleHistoryEntry) ws.off('kyc_status_update', handleKycStatusUpdate) + ws.off('manteca_kyc_status_update', handleMantecaKycStatusUpdate) ws.off('persona_tos_status_update', handleTosUpdate) } }, [autoConnect, connect, username]) diff --git a/src/services/manteca.ts b/src/services/manteca.ts index 40816647d..28f22f2e3 100644 --- a/src/services/manteca.ts +++ b/src/services/manteca.ts @@ -3,7 +3,11 @@ import { fetchWithSentry } from '@/utils' import Cookies from 'js-cookie' export const mantecaApi = { - initiateOnboarding: async (params: { returnUrl: string; failureUrl?: string }): Promise<{ url: string }> => { + initiateOnboarding: async (params: { + returnUrl: string + failureUrl?: string + exchange?: string + }): Promise<{ url: string }> => { const response = await fetchWithSentry(`${PEANUT_API_URL}/manteca/initiate-onboarding`, { method: 'POST', headers: { diff --git a/src/services/websocket.ts b/src/services/websocket.ts index a796d0a0f..f01671a1a 100644 --- a/src/services/websocket.ts +++ b/src/services/websocket.ts @@ -1,7 +1,13 @@ import { HistoryEntry } from '@/hooks/useTransactionHistory' export type WebSocketMessage = { - type: 'ping' | 'pong' | 'history_entry' | 'kyc_status_update' | 'persona_tos_status_update' + type: + | 'ping' + | 'pong' + | 'history_entry' + | 'kyc_status_update' + | 'manteca_kyc_status_update' + | 'persona_tos_status_update' data?: HistoryEntry } @@ -110,6 +116,12 @@ export class PeanutWebSocket { } break + case 'manteca_kyc_status_update': + if (message.data && 'status' in (message.data as object)) { + this.emit('manteca_kyc_status_update', message.data) + } + break + case 'persona_tos_status_update': if (message.data && 'status' in (message.data as object)) { this.emit('persona_tos_status_update', message.data) @@ -204,8 +216,14 @@ let websocketInstance: PeanutWebSocket | null = null export const getWebSocketInstance = (username?: string): PeanutWebSocket => { if (!websocketInstance && typeof window !== 'undefined') { - const wsUrl = process.env.NEXT_PUBLIC_PEANUT_WS_URL || '' + let wsUrl = process.env.NEXT_PUBLIC_PEANUT_WS_URL || '' const path = username ? `/ws/charges/${username}` : '/ws/charges' + + // use ws:// for local development to avoid SSL issues + if (window.location.hostname === 'localhost' && wsUrl.startsWith('wss://')) { + wsUrl = wsUrl.replace('wss://', 'ws://') + } + websocketInstance = new PeanutWebSocket(wsUrl, path) } From d066eb456f48c306809023bf58343e467b6c1d1a Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 12 Sep 2025 05:12:54 +0530 Subject: [PATCH 06/15] feat: handle geo specific kyc for manteca --- .../{(mobile-ui) => }/kyc/success/page.tsx | 2 +- .../AddMoney/components/InputAmountStep.tsx | 2 +- .../RegionalMethods/MercadoPago/index.tsx | 25 +++++++---- .../Kyc/InitiateMantecaKYCModal.tsx | 3 +- src/hooks/useMantecaKycFlow.ts | 45 ++++++++++++++----- src/interfaces/interfaces.ts | 20 +++++++-- 6 files changed, 69 insertions(+), 28 deletions(-) rename src/app/{(mobile-ui) => }/kyc/success/page.tsx (89%) diff --git a/src/app/(mobile-ui)/kyc/success/page.tsx b/src/app/kyc/success/page.tsx similarity index 89% rename from src/app/(mobile-ui)/kyc/success/page.tsx rename to src/app/kyc/success/page.tsx index ad4c241ba..7e050b722 100644 --- a/src/app/(mobile-ui)/kyc/success/page.tsx +++ b/src/app/kyc/success/page.tsx @@ -15,7 +15,7 @@ export default function KycSuccessPage() { }, []) return ( -
+
Peanut HandThumbsUp

Verification successful!

diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx index 9ff99419f..4f7f89583 100644 --- a/src/components/AddMoney/components/InputAmountStep.tsx +++ b/src/components/AddMoney/components/InputAmountStep.tsx @@ -66,7 +66,7 @@ const InputAmountStep = ({ variant="purple" shadowSize="4" onClick={onSubmit} - disabled={!parseFloat(tokenAmount.replace(/,/g, ''))} + disabled={!parseFloat(tokenAmount.replace(/,/g, '')) || isLoading} className="w-full" loading={isLoading} > diff --git a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx index 655c38ba2..aa45a7d03 100644 --- a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx +++ b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx @@ -3,7 +3,7 @@ import MercadoPagoDepositDetails from './MercadoPagoDepositDetails' import InputAmountStep from '../../InputAmountStep' import { createMantecaOnramp } from '@/app/actions/onramp' import { useParams, useRouter } from 'next/navigation' -import { countryData } from '@/components/AddMoney/consts' +import { CountryData, countryData } from '@/components/AddMoney/consts' import { MantecaDepositDetails } from '@/types/manteca.types' import { InitiateMantecaKYCModal } from '@/components/Kyc/InitiateMantecaKYCModal' import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' @@ -26,7 +26,7 @@ const MercadoPago = () => { const selectedCountry = useMemo(() => { return countryData.find((country) => country.type === 'country' && country.path === selectedCountryPath) }, [selectedCountryPath]) - const { isMantecaKycRequired } = useMantecaKycFlow() + const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry as CountryData }) const currencyData = useCurrency(selectedCountry?.currency ?? 'ARS') const { user, fetchUser } = useAuth() @@ -42,12 +42,6 @@ const MercadoPago = () => { }, }) - useEffect(() => { - if (isMantecaKycRequired) { - setIsKycModalOpen(true) - } - }, [isMantecaKycRequired]) - const handleKycCancel = () => { setIsKycModalOpen(false) if (selectedCountry?.path) { @@ -58,7 +52,13 @@ const MercadoPago = () => { const handleAmountSubmit = async () => { if (!selectedCountry?.currency) return - if (isMantecaKycRequired) { + // check if we still need to determine KYC status + if (isMantecaKycRequired === null) { + // still loading/determining KYC status, don't proceed yet + return + } + + if (isMantecaKycRequired === true) { setIsKycModalOpen(true) return } @@ -84,6 +84,13 @@ const MercadoPago = () => { } } + // handle verification modal opening + useEffect(() => { + if (isMantecaKycRequired) { + setIsKycModalOpen(true) + } + }, [isMantecaKycRequired, countryData]) + if (!selectedCountry) return null if (step === 'inputAmount') { diff --git a/src/components/Kyc/InitiateMantecaKYCModal.tsx b/src/components/Kyc/InitiateMantecaKYCModal.tsx index 8681a8aef..eab25ccc3 100644 --- a/src/components/Kyc/InitiateMantecaKYCModal.tsx +++ b/src/components/Kyc/InitiateMantecaKYCModal.tsx @@ -9,7 +9,7 @@ interface Props { onClose: () => void onKycSuccess?: () => void onManualClose?: () => void - country?: CountryData + country: CountryData } export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManualClose, country }: Props) => { @@ -17,6 +17,7 @@ export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManua onClose: onManualClose, // any non-success close from iframe is a manual close in case of Manteca KYC onSuccess: onKycSuccess, onManualClose, + country, }) return ( diff --git a/src/hooks/useMantecaKycFlow.ts b/src/hooks/useMantecaKycFlow.ts index 7f7434cc8..af95e0f2e 100644 --- a/src/hooks/useMantecaKycFlow.ts +++ b/src/hooks/useMantecaKycFlow.ts @@ -4,14 +4,16 @@ import { mantecaApi } from '@/services/manteca' import { useAuth } from '@/context/authContext' import { CountryData, MantecaSupportedExchanges } from '@/components/AddMoney/consts' import { BASE_URL } from '@/constants' +import { MantecaKycStatus } from '@/interfaces' type UseMantecaKycFlowOptions = { onClose?: () => void onSuccess?: () => void onManualClose?: () => void + country: CountryData } -export const useMantecaKycFlow = ({ onClose, onSuccess, onManualClose }: UseMantecaKycFlowOptions = {}) => { +export const useMantecaKycFlow = ({ onClose, onSuccess, onManualClose, country }: UseMantecaKycFlowOptions) => { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const [iframeOptions, setIframeOptions] = useState>({ @@ -20,20 +22,38 @@ export const useMantecaKycFlow = ({ onClose, onSuccess, onManualClose }: UseMant closeConfirmMessage: undefined, }) const { user } = useAuth() - const [isMantecaKycRequired, setNeedsMantecaKyc] = useState( - !user?.user?.mantecaKycStatus || user.user.mantecaKycStatus !== 'ACTIVE' - ) + const [isMantecaKycRequired, setNeedsMantecaKyc] = useState(false) + + const userKycVerifications = user?.user?.kycVerifications useEffect(() => { - setNeedsMantecaKyc(!user?.user?.mantecaKycStatus || user.user.mantecaKycStatus !== 'ACTIVE') - }, [user?.user?.mantecaKycStatus]) + // determine if manteca kyc is required based on geo data available in kycVerifications + const selectedGeo = country?.id + + if (selectedGeo && Array.isArray(userKycVerifications) && userKycVerifications.length > 0) { + const isuserActiveForSelectedGeo = userKycVerifications.some( + (v) => + v.provider === 'MANTECA' && + (v.mantecaGeo || '').toUpperCase() === selectedGeo.toUpperCase() && + v.status === MantecaKycStatus.ACTIVE + ) + setNeedsMantecaKyc(!isuserActiveForSelectedGeo) + return + } + + // if no verifications data available, keep as null (undetermined) + // only set to true if we have user data but no matching verification + if (user && userKycVerifications !== undefined) { + setNeedsMantecaKyc(true) + } + }, [userKycVerifications, country?.id, user]) - const openMantecaKyc = useCallback(async (country?: CountryData) => { + const openMantecaKyc = useCallback(async (countryParam?: CountryData) => { setIsLoading(true) setError(null) try { - const exchange = country?.id - ? MantecaSupportedExchanges[country.id as keyof typeof MantecaSupportedExchanges] + const exchange = countryParam?.id + ? MantecaSupportedExchanges[countryParam.id as keyof typeof MantecaSupportedExchanges] : MantecaSupportedExchanges.AR const returnUrl = BASE_URL + '/kyc/success' const { url } = await mantecaApi.initiateOnboarding({ returnUrl, exchange }) @@ -42,9 +62,10 @@ export const useMantecaKycFlow = ({ onClose, onSuccess, onManualClose }: UseMant visible: true, }) return { success: true as const } - } catch (e: any) { - setError(e?.message ?? 'Failed to initiate onboarding') - return { success: false as const, error: e?.message as string } + } catch (e: unknown) { + const message = e instanceof Error ? e.message : 'Failed to initiate onboarding' + setError(message) + return { success: false as const, error: message } } finally { setIsLoading(false) } diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 2b10b5f5e..5591c640f 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -221,7 +221,21 @@ interface ReferralConnection { account_identifier: string } -export type MantecaKycStatus = 'ONBOARDING' | 'ACTIVE' | 'INACTIVE' +export enum MantecaKycStatus { + ONBOARDING = 'ONBOARDING', + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', +} + +export interface IUserKycVerification { + provider: 'MANTECA' | 'BRIDGE' + mantecaGeo?: string | null + bridgeGeo?: string | null + status: MantecaKycStatus + approvedAt?: string | null + providerUserId?: string | null + providerRawStatus?: string | null +} export interface User { userId: string @@ -232,9 +246,7 @@ export interface User { bridgeKycStartedAt?: string bridgeKycApprovedAt?: string bridgeKycRejectedAt?: string - mantecaKycStatus: MantecaKycStatus - mantecaKycStartedAt?: string - mantecaKycApprovedAt?: string + kycVerifications?: IUserKycVerification[] // currently only used for Manteca, can be extended to other providers in the future, bridge is not migrated as it might affect existing users tosStatus?: string tosAcceptedAt?: string bridgeCustomerId: string | null From 963b87dd59158ba11000f0746543de01ace102c3 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:42:15 +0530 Subject: [PATCH 07/15] feat: handle geo based user verifciation in user settings --- src/app/(mobile-ui)/home/page.tsx | 8 +- src/components/AddMoney/consts/index.ts | 2 +- src/components/Claim/Link/Initial.view.tsx | 4 +- src/components/Common/CountryList.tsx | 64 ++++-- .../Kyc/InitiateMantecaKYCModal.tsx | 85 +++++++- .../Kyc/KycVerificationInProgressModal.tsx | 17 +- .../Profile/components/ProfileHeader.tsx | 6 +- .../Profile/components/PublicProfile.tsx | 4 +- src/components/Profile/index.tsx | 38 +--- .../views/IdentityVerification.view.tsx | 204 +++++++++++++++--- .../Profile/views/ProfileEdit.view.tsx | 5 +- src/components/UserHeader/index.tsx | 2 +- src/hooks/useDetermineBankClaimType.ts | 4 +- src/hooks/useDetermineBankRequestType.ts | 4 +- src/hooks/useKycStatus.tsx | 26 +++ src/hooks/useMantecaKycFlow.ts | 2 +- src/hooks/useSavedAccounts.tsx | 2 + 17 files changed, 373 insertions(+), 104 deletions(-) create mode 100644 src/hooks/useKycStatus.tsx diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 9586757f1..b10777015 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -41,6 +41,7 @@ import { PostSignupActionManager } from '@/components/Global/PostSignupActionMan import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { useClaimBankFlow } from '@/context/ClaimBankFlowContext' import { useDeviceType, DeviceType } from '@/hooks/useGetDeviceType' +import useKycStatus from '@/hooks/useKycStatus' const BALANCE_WARNING_THRESHOLD = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_THRESHOLD ?? '500') const BALANCE_WARNING_EXPIRY = parseInt(process.env.NEXT_PUBLIC_BALANCE_WARNING_EXPIRY ?? '1814400') // 21 days in seconds @@ -61,6 +62,7 @@ export default function Home() { const { isFetchingUser, addAccount } = useAuth() const { user } = useUserStore() + const { isUserKycApproved } = useKycStatus() const username = user?.user.username const [showIOSPWAInstallModal, setShowIOSPWAInstallModal] = useState(false) @@ -214,11 +216,7 @@ export default function Home() {
- +
diff --git a/src/components/AddMoney/consts/index.ts b/src/components/AddMoney/consts/index.ts index 31cecd9e0..365c99736 100644 --- a/src/components/AddMoney/consts/index.ts +++ b/src/components/AddMoney/consts/index.ts @@ -13,7 +13,7 @@ export const MantecaSupportedExchanges = { PA: 'PANAMA', CR: 'COSTA_RICA', GT: 'GUATEMALA', - MX: 'MEXICO', + // MX: 'MEXICO', // manteca supports MEXICO, but mercado pago doesnt support qr payments for mexico PH: 'PHILIPPINES', BO: 'BOLIVIA', } diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 9b06079f4..0ab9aaadb 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -51,6 +51,7 @@ import { Button } from '@/components/0_Bruddle' import Image from 'next/image' import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets' import { GuestVerificationModal } from '@/components/Global/GuestVerificationModal' +import useKycStatus from '@/hooks/useKycStatus' export const InitialClaimLinkView = (props: IClaimScreenProps) => { const { @@ -111,6 +112,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { const searchParams = useSearchParams() const prevRecipientType = useRef(null) const prevUser = useRef(user) + const { isUserBridgeKycApproved } = useKycStatus() useEffect(() => { if (!prevUser.current && user) { @@ -329,7 +331,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { recipient: recipient.name ?? recipient.address, password: '', }) - if (user?.user.bridgeKycStatus === 'approved') { + if (isUserBridgeKycApproved) { const account = user.accounts.find( (account) => account.identifier.replaceAll(/\s/g, '').toLowerCase() === diff --git a/src/components/Common/CountryList.tsx b/src/components/Common/CountryList.tsx index 4d467357d..22ab13a5e 100644 --- a/src/components/Common/CountryList.tsx +++ b/src/components/Common/CountryList.tsx @@ -1,10 +1,10 @@ 'use client' -import { countryCodeMap, CountryData, countryData } from '@/components/AddMoney/consts' +import { countryCodeMap, CountryData, countryData, MantecaSupportedExchanges } from '@/components/AddMoney/consts' import EmptyState from '@/components/Global/EmptyStates/EmptyState' import { SearchInput } from '@/components/SearchUsers/SearchInput' import { SearchResultCard } from '@/components/SearchUsers/SearchResultCard' import Image from 'next/image' -import { useMemo, useState } from 'react' +import { useMemo, useState, type ReactNode } from 'react' import { getCardPosition } from '../Global/Card' import { useGeoLocaion } from '@/hooks/useGeoLocaion' import { CountryListSkeleton } from './CountryListSkeleton' @@ -13,10 +13,11 @@ import StatusBadge from '../Global/Badges/StatusBadge' interface CountryListViewProps { inputTitle: string - viewMode: 'claim-request' | 'add-withdraw' + viewMode: 'claim-request' | 'add-withdraw' | 'general-verification' onCountryClick: (country: CountryData) => void onCryptoClick?: (flow: 'add' | 'withdraw') => void flow?: 'add' | 'withdraw' + getRightContent?: (country: CountryData, isSupported: boolean) => ReactNode } /** @@ -24,19 +25,26 @@ interface CountryListViewProps { * * @param {object} props * @param {string} props.inputTitle The title for the input - * @param {string} props.viewMode The view mode of the list, either 'claim-request' or 'add-withdraw' + * @param {string} props.viewMode The view mode of the list, either 'claim-request' or 'add-withdraw' or 'general-verification' * @param {function} props.onCountryClick The function to call when a country is clicked * @param {function} props.onCryptoClick The function to call when the crypto button is clicked * @param {string} props.flow The flow of the list, either 'add' or 'withdraw', only required for 'add-withdraw' view mode * @returns {JSX.Element} */ -export const CountryList = ({ inputTitle, viewMode, onCountryClick, onCryptoClick, flow }: CountryListViewProps) => { +export const CountryList = ({ + inputTitle, + viewMode, + onCountryClick, + onCryptoClick, + flow, + getRightContent, +}: CountryListViewProps) => { const [searchTerm, setSearchTerm] = useState('') const { countryCode: userGeoLocationCountryCode, isLoading: isGeoLoading } = useGeoLocaion() const supportedCountries = useMemo(() => { return countryData.filter((country) => country.type === 'country') - }, [viewMode]) + }, []) // sort countries based on user's geo location, fallback to alphabetical order const sortedCountries = useMemo(() => { @@ -105,17 +113,49 @@ export const CountryList = ({ inputTitle, viewMode, onCountryClick, onCryptoClic const twoLetterCountryCode = countryCodeMap[country.id.toUpperCase()] ?? country.id.toLowerCase() const position = getCardPosition(index, filteredCountries.length) + + const isBridgeSupportedCountry = [ + 'US', + 'MX', + ...Object.keys(countryCodeMap), + ...Object.values(countryCodeMap), + ].includes(country.id) + const isMantecaSupportedCountry = Object.keys(MantecaSupportedExchanges).includes( + country.id + ) + + // determine if country is supported based on view mode + let isSupported = false + + if (viewMode === 'add-withdraw') { + // all countries supported for claim-request + isSupported = true + } else if (viewMode === 'general-verification') { + // support all bridge and manteca supported countries + isSupported = isBridgeSupportedCountry || isMantecaSupportedCountry + } else if (viewMode === 'claim-request') { + // support all countries + isSupported = isBridgeSupportedCountry + } else { + // support all countries + isSupported = true + } + // flag used to show soon badge based on the view mode, check country code map keys and values for supported countries - const isSupported = - viewMode === 'add-withdraw' || - ['US', 'MX', ...Object.keys(countryCodeMap), ...Object.values(countryCodeMap)].includes( - country.id - ) + // const isSupported = + // viewMode === 'add-withdraw' || + // viewMode === 'general-verification' || + // ['US', 'MX', ...Object.keys(countryCodeMap), ...Object.values(countryCodeMap)].includes( + // country.id + // ) + + const customRight = getRightContent ? getRightContent(country, isSupported) : undefined + return ( } + rightContent={customRight ?? (!isSupported && )} description={country.currency} onClick={() => onCountryClick(country)} position={position} diff --git a/src/components/Kyc/InitiateMantecaKYCModal.tsx b/src/components/Kyc/InitiateMantecaKYCModal.tsx index eab25ccc3..19e22a426 100644 --- a/src/components/Kyc/InitiateMantecaKYCModal.tsx +++ b/src/components/Kyc/InitiateMantecaKYCModal.tsx @@ -1,8 +1,12 @@ +'use client' + import ActionModal from '@/components/Global/ActionModal' import IframeWrapper from '@/components/Global/IframeWrapper' import { IconName } from '@/components/Global/Icons/Icon' import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' import { CountryData } from '@/components/AddMoney/consts' +import { Button } from '../0_Bruddle' +import { PeanutDoesntStoreAnyPersonalInformation } from './KycVerificationInProgressModal' interface Props { isOpen: boolean @@ -10,9 +14,23 @@ interface Props { onKycSuccess?: () => void onManualClose?: () => void country: CountryData + title?: string | React.ReactNode + description?: string | React.ReactNode + ctaText?: string + footer?: React.ReactNode } -export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManualClose, country }: Props) => { +export const InitiateMantecaKYCModal = ({ + isOpen, + onClose, + onKycSuccess, + onManualClose, + country, + title, + description, + ctaText, + footer, +}: Props) => { const { isLoading, iframeOptions, openMantecaKyc, handleIframeClose } = useMantecaKycFlow({ onClose: onManualClose, // any non-success close from iframe is a manual close in case of Manteca KYC onSuccess: onKycSuccess, @@ -25,14 +43,17 @@ export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManua openMantecaKyc(country), variant: 'purple', disabled: isLoading, @@ -41,8 +62,62 @@ export const InitiateMantecaKYCModal = ({ isOpen, onClose, onKycSuccess, onManua className: 'h-11', }, ]} + footer={footer} /> ) } + +export const MantecaGeoSpecificKycModal = ({ + isUserBridgeKycApproved, + selectedCountry, + setIsMantecaModalOpen, + isMantecaModalOpen, +}: { + isUserBridgeKycApproved: boolean + selectedCountry: { id: string; title: string } + setIsMantecaModalOpen: (isOpen: boolean) => void + isMantecaModalOpen: boolean +}) => { + return ( + + You're already verified in Europe, USA, and Mexico, but to use features in{' '} + {selectedCountry.title} you need to complete a separate verification.
Since{' '} + we don't keep personal data, your previous KYC can't be reused. +

+ ) : ( +

+ Verify your identity to start using features like Mercado Pago payments in{' '} + {selectedCountry.title}.{' '} +

+ ) + } + footer={ + isUserBridgeKycApproved ? ( + + ) : ( + + ) + } + ctaText="Start Verification" + isOpen={isMantecaModalOpen} + onClose={() => setIsMantecaModalOpen(false)} + onKycSuccess={() => { + setIsMantecaModalOpen(false) + }} + onManualClose={() => setIsMantecaModalOpen(false)} + country={{ id: selectedCountry.id, title: selectedCountry.title, type: 'country', path: '' }} + /> + ) +} diff --git a/src/components/Kyc/KycVerificationInProgressModal.tsx b/src/components/Kyc/KycVerificationInProgressModal.tsx index 9c9d57b28..526798964 100644 --- a/src/components/Kyc/KycVerificationInProgressModal.tsx +++ b/src/components/Kyc/KycVerificationInProgressModal.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/navigation' import ActionModal from '@/components/Global/ActionModal' import { Icon, IconName } from '@/components/Global/Icons/Icon' +import { twMerge } from 'tailwind-merge' interface KycVerificationInProgressModalProps { isOpen: boolean @@ -43,12 +44,16 @@ export const KycVerificationInProgressModal = ({ isOpen, onClose }: KycVerificat ]} preventClose hideModalCloseButton - footer={ -
- - Peanut doesn't store any personal information -
- } + footer={} /> ) } + +export const PeanutDoesntStoreAnyPersonalInformation = ({ className }: { className?: string }) => { + return ( +
+ + Peanut doesn't store any personal information +
+ ) +} diff --git a/src/components/Profile/components/ProfileHeader.tsx b/src/components/Profile/components/ProfileHeader.tsx index 4780c231c..50e0cf793 100644 --- a/src/components/Profile/components/ProfileHeader.tsx +++ b/src/components/Profile/components/ProfileHeader.tsx @@ -10,6 +10,7 @@ import AvatarWithBadge from '../AvatarWithBadge' import { Drawer, DrawerContent, DrawerTitle } from '@/components/Global/Drawer' import { VerifiedUserLabel } from '@/components/UserHeader' import { useAuth } from '@/context/authContext' +import useKycStatus from '@/hooks/useKycStatus' interface ProfileHeaderProps { name: string @@ -29,13 +30,12 @@ const ProfileHeader: React.FC = ({ haveSentMoneyToUser = false, }) => { const { user: authenticatedUser } = useAuth() - const isAuthenticatedUserVerified = authenticatedUser?.user.bridgeKycStatus === 'approved' + const { isUserBridgeKycApproved } = useKycStatus() + const isAuthenticatedUserVerified = isUserBridgeKycApproved && authenticatedUser?.user.username === username const [isDrawerOpen, setIsDrawerOpen] = useState(false) const profileUrl = `${BASE_URL}/${username}` - console.log('isVerified', isVerified) - return ( <>
diff --git a/src/components/Profile/components/PublicProfile.tsx b/src/components/Profile/components/PublicProfile.tsx index 81bfa1181..e8c36035c 100644 --- a/src/components/Profile/components/PublicProfile.tsx +++ b/src/components/Profile/components/PublicProfile.tsx @@ -17,6 +17,7 @@ import Card from '@/components/Global/Card' import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' import { checkIfInternalNavigation } from '@/utils' import { useAuth } from '@/context/authContext' +import useKycStatus from '@/hooks/useKycStatus' interface PublicProfileProps { username: string @@ -32,6 +33,7 @@ const PublicProfile: React.FC = ({ username, isLoggedIn = fa const router = useRouter() const { user } = useAuth() const isSelfProfile = user?.user.username?.toLowerCase() === username.toLowerCase() + const { isUserBridgeKycApproved } = useKycStatus() // Handle send button click const handleSend = () => { @@ -45,7 +47,7 @@ const PublicProfile: React.FC = ({ username, isLoggedIn = fa useEffect(() => { usersApi.getByUsername(username).then((user) => { if (user?.fullName) setFullName(user.fullName) - if (user?.bridgeKycStatus === 'approved') setIsKycVerified(true) + if (isUserBridgeKycApproved) setIsKycVerified(true) // 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 (user?.totalUsdReceivedFromCurrentUser) { diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index e817e119e..a93312bf6 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -8,13 +8,12 @@ import ProfileHeader from './components/ProfileHeader' import ProfileMenuItem from './components/ProfileMenuItem' import { useRouter } from 'next/navigation' import { checkIfInternalNavigation } from '@/utils' -import ActionModal from '../Global/ActionModal' -import { useState } from 'react' +import useKycStatus from '@/hooks/useKycStatus' export const Profile = () => { const { logoutUser, isLoggingOut, user } = useAuth() - const [isKycApprovedModalOpen, setIsKycApprovedModalOpen] = useState(false) const router = useRouter() + const { isUserKycApproved } = useKycStatus() const logout = async () => { await logoutUser() @@ -23,8 +22,6 @@ export const Profile = () => { const fullName = user?.user.fullName || user?.user?.username || 'Anonymous User' const username = user?.user.username || 'anonymous' - const isKycApproved = user?.user.bridgeKycStatus === 'approved' - return (
{ }} />
- +
{/* Menu Item - Invite Entry */} { label="Identity Verification" href="/profile/identity-verification" onClick={() => { - if (isKycApproved) { - setIsKycApprovedModalOpen(true) - } else { - router.push('/profile/identity-verification') - } + router.push('/profile/identity-verification') }} position="middle" - endIcon={isKycApproved ? 'check' : undefined} - endIconClassName={isKycApproved ? 'text-success-3 size-4' : undefined} /> - + /> */}
{/* Menu Items - Second Group */} @@ -111,22 +103,6 @@ export const Profile = () => {
- - setIsKycApprovedModalOpen(false)} - title="You’re already verified" - description="Your identity has already been successfully verified. No further action is needed." - icon="shield" - ctas={[ - { - text: 'Go back', - shadowSize: '4', - className: 'md:py-2', - onClick: () => setIsKycApprovedModalOpen(false), - }, - ]} - />
) } diff --git a/src/components/Profile/views/IdentityVerification.view.tsx b/src/components/Profile/views/IdentityVerification.view.tsx index 6fd02660c..f4d6d45f9 100644 --- a/src/components/Profile/views/IdentityVerification.view.tsx +++ b/src/components/Profile/views/IdentityVerification.view.tsx @@ -1,15 +1,25 @@ 'use client' import { updateUserById } from '@/app/actions/users' import { Button } from '@/components/0_Bruddle' +import { countryCodeMap, MantecaSupportedExchanges } from '@/components/AddMoney/consts' import { UserDetailsForm, UserDetailsFormData } from '@/components/AddMoney/UserDetailsForm' +import { CountryList } from '@/components/Common/CountryList' import ErrorAlert from '@/components/Global/ErrorAlert' import IframeWrapper from '@/components/Global/IframeWrapper' import NavHeader from '@/components/Global/NavHeader' -import { KycVerificationInProgressModal } from '@/components/Kyc/KycVerificationInProgressModal' +import ActionModal from '@/components/Global/ActionModal' +import { + KycVerificationInProgressModal, + PeanutDoesntStoreAnyPersonalInformation, +} from '@/components/Kyc/KycVerificationInProgressModal' +import { MantecaGeoSpecificKycModal } from '@/components/Kyc/InitiateMantecaKYCModal' +import { Icon } from '@/components/Global/Icons/Icon' import { useAuth } from '@/context/authContext' import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' +import { MantecaKycStatus } from '@/interfaces' import { useRouter } from 'next/navigation' -import { useMemo, useRef, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' +import useKycStatus from '@/hooks/useKycStatus' const IdentityVerificationView = () => { const { user, fetchUser } = useAuth() @@ -18,6 +28,12 @@ const IdentityVerificationView = () => { const [isUserDetailsFormValid, setIsUserDetailsFormValid] = useState(false) const [isUpdatingUser, setIsUpdatingUser] = useState(false) const [userUpdateError, setUserUpdateError] = useState(null) + const [showUserDetailsForm, setShowUserDetailsForm] = useState(false) + const [isAlreadyVerifiedModalOpen, setIsAlreadyVerifiedModalOpen] = useState(false) + const [isMantecaModalOpen, setIsMantecaModalOpen] = useState(false) + const [selectedCountry, setSelectedCountry] = useState<{ id: string; title: string } | null>(null) + const [userClickedCountry, setUserClickedCountry] = useState<{ id: string; title: string } | null>(null) + const { iframeOptions, handleInitiateKyc, @@ -28,6 +44,8 @@ const IdentityVerificationView = () => { isLoading: isKycLoading, } = useBridgeKycFlow() + const { isUserBridgeKycApproved } = useKycStatus() + const [firstName, ...lastNameParts] = (user?.user.fullName ?? '').split(' ') const lastName = lastNameParts.join(' ') @@ -54,7 +72,6 @@ const IdentityVerificationView = () => { throw new Error(result.error) } await fetchUser() - // setStep('kyc') await handleInitiateKyc() } catch (error: any) { setUserUpdateError(error.message) @@ -65,40 +82,159 @@ const IdentityVerificationView = () => { return {} } + const handleBack = useCallback(() => { + if (showUserDetailsForm) { + setShowUserDetailsForm(false) + } else { + router.replace('/profile') + } + }, [router, showUserDetailsForm]) + + // country validation helpers + const isBridgeSupportedCountry = useCallback((code: string) => { + const upper = code.toUpperCase() + return ( + upper === 'US' || + upper === 'MX' || + Object.keys(countryCodeMap).includes(upper) || + Object.values(countryCodeMap).includes(upper) + ) + }, []) + + const isMantecaSupportedCountry = useCallback((code: string) => { + const upper = code.toUpperCase() + return Object.prototype.hasOwnProperty.call(MantecaSupportedExchanges, upper) + }, []) + + const isVerifiedForCountry = useCallback( + (code: string) => { + const upper = code.toUpperCase() + // bridge approval covers us/mx/sepa generally + if (isBridgeSupportedCountry(upper) && isUserBridgeKycApproved) return true + // manteca per-geo check + const mantecaActive = user?.user.kycVerifications?.some( + (v) => + v.provider === 'MANTECA' && + (v.mantecaGeo || '').toUpperCase() === upper && + v.status === MantecaKycStatus.ACTIVE + ) + return Boolean(mantecaActive) + }, + [isBridgeSupportedCountry, user?.user.bridgeKycStatus, user?.user.kycVerifications] + ) + + // view components + const CountrySelectionView = () => ( +
+ + isVerifiedForCountry(country.id) ? ( + + ) : undefined + } + onCountryClick={(country) => { + const { id, title } = country + setUserClickedCountry({ id, title }) + + if (isVerifiedForCountry(id)) { + setIsAlreadyVerifiedModalOpen(true) + return + } + + if (isBridgeSupportedCountry(id)) { + setShowUserDetailsForm(true) + return + } + + if (isMantecaSupportedCountry(id)) { + setSelectedCountry({ id, title }) + setIsMantecaModalOpen(true) + } + }} + onCryptoClick={() => console.log('crypto')} + /> +
+ ) + + const UserDetailsFormView = () => ( +
+

Provide information to begin verification

+ + + + + + + + {(userUpdateError || kycError) && } + + + + +
+ ) + + // determine which view to show based on current state + const getCurrentView = () => { + return showUserDetailsForm ? : + } + return ( -
- router.replace('/profile')} /> -
-

Provide information to begin verification

- +
+ + + {getCurrentView()} + + setIsAlreadyVerifiedModalOpen(false)} + title="You're already verified" + description={ +

+ Your identity has already been successfully verified for {userClickedCountry?.title}. You can + continue to use features available in this region. No further action is needed. +

+ } + icon="shield" + ctas={[ + { + text: 'Close', + shadowSize: '4', + className: 'md:py-2', + onClick: () => setIsAlreadyVerifiedModalOpen(false), + }, + ]} + /> - - - {(userUpdateError || kycError) && } - - - - -
+ )}
) } diff --git a/src/components/Profile/views/ProfileEdit.view.tsx b/src/components/Profile/views/ProfileEdit.view.tsx index c48ba7808..0f254d040 100644 --- a/src/components/Profile/views/ProfileEdit.view.tsx +++ b/src/components/Profile/views/ProfileEdit.view.tsx @@ -9,10 +9,13 @@ import { useRouter } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import ProfileEditField from '../components/ProfileEditField' import ProfileHeader from '../components/ProfileHeader' +import useKycStatus from '@/hooks/useKycStatus' export const ProfileEditView = () => { const router = useRouter() const { user, fetchUser } = useAuth() + const { isUserBridgeKycApproved } = useKycStatus() + const [isLoading, setIsLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') @@ -112,7 +115,7 @@ export const ProfileEditView = () => {
router.push('/profile')} /> - +
- +
diff --git a/src/hooks/useDetermineBankClaimType.ts b/src/hooks/useDetermineBankClaimType.ts index 5ebce26da..0d73234cb 100644 --- a/src/hooks/useDetermineBankClaimType.ts +++ b/src/hooks/useDetermineBankClaimType.ts @@ -2,6 +2,7 @@ import { getUserById } from '@/app/actions/users' import { useAuth } from '@/context/authContext' import { useClaimBankFlow } from '@/context/ClaimBankFlowContext' import { useEffect, useState } from 'react' +import useKycStatus from './useKycStatus' export enum BankClaimType { GuestBankClaim = 'guest-bank-claim', @@ -22,11 +23,12 @@ export function useDetermineBankClaimType(senderUserId: string): { const { user } = useAuth() const [claimType, setClaimType] = useState(BankClaimType.ReceiverKycNeeded) const { setSenderDetails } = useClaimBankFlow() + const { isUserBridgeKycApproved } = useKycStatus() useEffect(() => { const determineBankClaimType = async () => { // check if receiver (logged in user) exists and is KYC approved - const receiverKycApproved = user?.user?.bridgeKycStatus === 'approved' + const receiverKycApproved = isUserBridgeKycApproved if (receiverKycApproved) { // condition 1: Receiver is KYC approved → UserBankClaim diff --git a/src/hooks/useDetermineBankRequestType.ts b/src/hooks/useDetermineBankRequestType.ts index 4b19e277a..5f6583fee 100644 --- a/src/hooks/useDetermineBankRequestType.ts +++ b/src/hooks/useDetermineBankRequestType.ts @@ -2,6 +2,7 @@ import { getUserById } from '@/app/actions/users' import { useAuth } from '@/context/authContext' import { useRequestFulfillmentFlow } from '@/context/RequestFulfillmentFlowContext' import { useEffect, useState } from 'react' +import useKycStatus from './useKycStatus' export enum BankRequestType { GuestBankRequest = 'guest-bank-request', @@ -22,10 +23,11 @@ export function useDetermineBankRequestType(requesterUserId: string): { const { user } = useAuth() const [requestType, setRequestType] = useState(BankRequestType.PayerKycNeeded) const { setRequesterDetails } = useRequestFulfillmentFlow() + const { isUserBridgeKycApproved } = useKycStatus() useEffect(() => { const determineBankRequestType = async () => { - const payerKycApproved = user?.user?.bridgeKycStatus === 'approved' + const payerKycApproved = isUserBridgeKycApproved if (payerKycApproved) { setRequestType(BankRequestType.UserBankRequest) diff --git a/src/hooks/useKycStatus.tsx b/src/hooks/useKycStatus.tsx new file mode 100644 index 000000000..136e1970b --- /dev/null +++ b/src/hooks/useKycStatus.tsx @@ -0,0 +1,26 @@ +'use client' + +import { useAuth } from '@/context/authContext' +import { MantecaKycStatus } from '@/interfaces' +import { useMemo } from 'react' + +/** + * Used to get the user's KYC status for all providers - currently only bridge and manteca + * NOTE: This hook can be extended to support more providers in the future based on requirements + * @returns {object} An object with the user's KYC status for all providers and a combined status for all providers, if user is verified for any provider, return true + */ +export default function useKycStatus() { + const { user } = useAuth() + + const isUserBridgeKycApproved = user?.user.bridgeKycStatus === 'approved' + + const isUserMantecaKycApproved = useMemo( + () => + user?.user.kycVerifications?.some((verification) => verification.status === MantecaKycStatus.ACTIVE) ?? + false, + [user?.user.kycVerifications] + ) + const isUserKycApproved = isUserBridgeKycApproved || isUserMantecaKycApproved + + return { isUserBridgeKycApproved, isUserMantecaKycApproved, isUserKycApproved } +} diff --git a/src/hooks/useMantecaKycFlow.ts b/src/hooks/useMantecaKycFlow.ts index af95e0f2e..25d6f697d 100644 --- a/src/hooks/useMantecaKycFlow.ts +++ b/src/hooks/useMantecaKycFlow.ts @@ -10,7 +10,7 @@ type UseMantecaKycFlowOptions = { onClose?: () => void onSuccess?: () => void onManualClose?: () => void - country: CountryData + country?: CountryData } export const useMantecaKycFlow = ({ onClose, onSuccess, onManualClose, country }: UseMantecaKycFlowOptions) => { diff --git a/src/hooks/useSavedAccounts.tsx b/src/hooks/useSavedAccounts.tsx index 04907c66a..8f3bba74c 100644 --- a/src/hooks/useSavedAccounts.tsx +++ b/src/hooks/useSavedAccounts.tsx @@ -1,3 +1,5 @@ +'use client' + import { useAuth } from '@/context/authContext' import { AccountType } from '@/interfaces' import { useMemo } from 'react' From 823ec91229bf8d21270c029166ccca67e9cf72d6 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:48:46 +0530 Subject: [PATCH 08/15] fix: use useclient in mepa comp --- .../AddMoney/components/RegionalMethods/MercadoPago/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx index 78e3bbc9b..3e53d4bd4 100644 --- a/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx +++ b/src/components/AddMoney/components/RegionalMethods/MercadoPago/index.tsx @@ -1,3 +1,4 @@ +'use client' import { FC, useEffect, useMemo, useState } from 'react' import MercadoPagoDepositDetails from './MercadoPagoDepositDetails' import InputAmountStep from '../../InputAmountStep' From b6e0026b295c8cb9029e5ade68cc879a43888acd Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:48:21 +0530 Subject: [PATCH 09/15] fix: resolve coderabbit comments --- src/components/Profile/components/ProfileHeader.tsx | 4 ++-- src/components/Profile/components/PublicProfile.tsx | 12 +++++++++--- src/components/UserHeader/index.tsx | 9 ++++++++- src/services/users.ts | 3 ++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/Profile/components/ProfileHeader.tsx b/src/components/Profile/components/ProfileHeader.tsx index 50e0cf793..df33f94ee 100644 --- a/src/components/Profile/components/ProfileHeader.tsx +++ b/src/components/Profile/components/ProfileHeader.tsx @@ -30,8 +30,8 @@ const ProfileHeader: React.FC = ({ haveSentMoneyToUser = false, }) => { const { user: authenticatedUser } = useAuth() - const { isUserBridgeKycApproved } = useKycStatus() - const isAuthenticatedUserVerified = isUserBridgeKycApproved && authenticatedUser?.user.username === username + const { isUserKycApproved } = useKycStatus() + const isAuthenticatedUserVerified = isUserKycApproved && authenticatedUser?.user.username === username const [isDrawerOpen, setIsDrawerOpen] = useState(false) const profileUrl = `${BASE_URL}/${username}` diff --git a/src/components/Profile/components/PublicProfile.tsx b/src/components/Profile/components/PublicProfile.tsx index e8c36035c..7b540dfba 100644 --- a/src/components/Profile/components/PublicProfile.tsx +++ b/src/components/Profile/components/PublicProfile.tsx @@ -17,7 +17,7 @@ import Card from '@/components/Global/Card' import chillPeanutAnim from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_01.gif' import { checkIfInternalNavigation } from '@/utils' import { useAuth } from '@/context/authContext' -import useKycStatus from '@/hooks/useKycStatus' +import { MantecaKycStatus } from '@/interfaces' interface PublicProfileProps { username: string @@ -33,7 +33,6 @@ const PublicProfile: React.FC = ({ username, isLoggedIn = fa const router = useRouter() const { user } = useAuth() const isSelfProfile = user?.user.username?.toLowerCase() === username.toLowerCase() - const { isUserBridgeKycApproved } = useKycStatus() // Handle send button click const handleSend = () => { @@ -47,7 +46,14 @@ const PublicProfile: React.FC = ({ username, isLoggedIn = fa useEffect(() => { usersApi.getByUsername(username).then((user) => { if (user?.fullName) setFullName(user.fullName) - if (isUserBridgeKycApproved) setIsKycVerified(true) + if ( + user?.bridgeKycStatus === 'approved' || + user?.kycVerifications?.some((v) => v.status === MantecaKycStatus.ACTIVE) + ) { + setIsKycVerified(true) + } else { + setIsKycVerified(false) + } // 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 (user?.totalUsdReceivedFromCurrentUser) { diff --git a/src/components/UserHeader/index.tsx b/src/components/UserHeader/index.tsx index dc8b01aed..6116fc7e9 100644 --- a/src/components/UserHeader/index.tsx +++ b/src/components/UserHeader/index.tsx @@ -8,6 +8,7 @@ import { Tooltip } from '../Tooltip' import { useMemo } from 'react' import { isAddress } from 'viem' import { printableAddress } from '@/utils' +import useKycStatus from '@/hooks/useKycStatus' interface UserHeaderProps { username: string @@ -16,6 +17,8 @@ interface UserHeaderProps { } export const UserHeader = ({ username, fullName, isVerified }: UserHeaderProps) => { + const { isUserKycApproved: isViewerVerified } = useKycStatus() + return (
@@ -24,7 +27,11 @@ export const UserHeader = ({ username, fullName, isVerified }: UserHeaderProps) className="h-7 w-7 text-[11px] md:h-8 md:w-8 md:text-[13px]" name={fullName || username} /> - +
diff --git a/src/services/users.ts b/src/services/users.ts index 3fde5794e..6b1a89b99 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -5,7 +5,7 @@ import { PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_TOKEN_SYMBOL, } from '@/constants' -import { AccountType } from '@/interfaces' +import { AccountType, IUserKycVerification } from '@/interfaces' import { IAttachmentOptions } from '@/redux/types/send-flow.types' import { fetchWithSentry } from '@/utils' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' @@ -28,6 +28,7 @@ export type ApiUser = { totalUsdSentToCurrentUser: string totalUsdReceivedFromCurrentUser: string bridgeKycStatus: string + kycVerifications?: IUserKycVerification[] } export type RecentUser = Pick From af53312fa8363d3dd6dfdc9564796c7d632e0e98 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:45:34 +0530 Subject: [PATCH 10/15] feat: country row comp --- src/components/Kyc/Country-Flag-And-Name.tsx | 39 ++++++++++++++++++++ src/components/Kyc/CountryRegionRow.tsx | 20 ++++++++++ src/interfaces/interfaces.ts | 2 + 3 files changed, 61 insertions(+) create mode 100644 src/components/Kyc/Country-Flag-And-Name.tsx create mode 100644 src/components/Kyc/CountryRegionRow.tsx diff --git a/src/components/Kyc/Country-Flag-And-Name.tsx b/src/components/Kyc/Country-Flag-And-Name.tsx new file mode 100644 index 000000000..814ea9a36 --- /dev/null +++ b/src/components/Kyc/Country-Flag-And-Name.tsx @@ -0,0 +1,39 @@ +import Image from 'next/image' +import { countryData } from '../AddMoney/consts' +import IconStack from '../Global/IconStack' + +interface CountryFlagAndNameProps { + countryCode?: string + isBridgeRegion?: boolean +} + +export const CountryFlagAndName = ({ countryCode, isBridgeRegion }: CountryFlagAndNameProps) => { + const countryName = countryData.find((c) => c.id === countryCode?.toUpperCase())?.title + return ( +
+ {isBridgeRegion ? ( + + ) : ( + {`${countryName} + )} + {isBridgeRegion ? 'US/EU/MX' : countryName} +
+ ) +} + +// this component is used for the completed state of the kyc diff --git a/src/components/Kyc/CountryRegionRow.tsx b/src/components/Kyc/CountryRegionRow.tsx new file mode 100644 index 000000000..64cdb13ea --- /dev/null +++ b/src/components/Kyc/CountryRegionRow.tsx @@ -0,0 +1,20 @@ +import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' +import { CountryFlagAndName } from './Country-Flag-And-Name' + +interface CountryRegionRowProps { + countryCode?: string | null + isBridge?: boolean +} + +export const CountryRegionRow = ({ countryCode, isBridge }: CountryRegionRowProps) => { + if (!isBridge && !countryCode) { + return null + } + + return ( + } + /> + ) +} diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 5591c640f..643866a11 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -235,6 +235,8 @@ export interface IUserKycVerification { approvedAt?: string | null providerUserId?: string | null providerRawStatus?: string | null + createdAt: string + updatedAt: string } export interface User { From eab8914f8f946f1afdd4aa3129a5a381e6a7989a Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:47:35 +0530 Subject: [PATCH 11/15] feat: handle non-bridge kyc activity ui and states --- src/components/Global/IconStack.tsx | 5 +- src/components/Kyc/KycStatusDrawer.tsx | 68 ++++++++++++--------- src/components/Kyc/KycStatusItem.tsx | 30 +++++---- src/components/Kyc/states/KycCompleted.tsx | 18 ++++-- src/components/Kyc/states/KycFailed.tsx | 14 ++++- src/components/Kyc/states/KycProcessing.tsx | 13 +++- src/hooks/useBridgeKycFlow.ts | 16 ++--- 7 files changed, 100 insertions(+), 64 deletions(-) diff --git a/src/components/Global/IconStack.tsx b/src/components/Global/IconStack.tsx index d08e06de4..5f3e46999 100644 --- a/src/components/Global/IconStack.tsx +++ b/src/components/Global/IconStack.tsx @@ -5,9 +5,10 @@ interface IconStackProps { icons: string[] iconSize?: number iconClassName?: string + imageClassName?: string } -const IconStack: React.FC = ({ icons, iconSize = 24, iconClassName = '' }) => { +const IconStack: React.FC = ({ icons, iconSize = 24, iconClassName = '', imageClassName }) => { return (
{icons.map((icon, index) => ( @@ -24,7 +25,7 @@ const IconStack: React.FC = ({ icons, iconSize = 24, iconClassNa alt={`icon-${index}`} width={iconSize} height={iconSize} - className="min-h-6 min-w-6 rounded-full" + className={twMerge('min-h-6 min-w-6 rounded-full', imageClassName)} />
))} diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index f624b97f9..774f69e23 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -6,57 +6,53 @@ import PeanutLoading from '@/components/Global/PeanutLoading' import { Drawer, DrawerContent } from '../Global/Drawer' import { BridgeKycStatus } from '@/utils' import { getKycDetails } from '@/app/actions/users' -import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' import IFrameWrapper from '../Global/IframeWrapper' +import { IUserKycVerification, MantecaKycStatus } from '@/interfaces' +import { useUserStore } from '@/redux/hooks' +import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' // a helper to categorize the kyc status from the user object -const getKycStatusCategory = (bridgeKycStatus: BridgeKycStatus): 'processing' | 'completed' | 'failed' => { - let category: 'processing' | 'completed' | 'failed' - switch (bridgeKycStatus) { - // note: not_started status is handled explicitly in KycStatusItem component +const getKycStatusCategory = (status: BridgeKycStatus | MantecaKycStatus): 'processing' | 'completed' | 'failed' => { + switch (status) { case 'approved': - category = 'completed' - break + case MantecaKycStatus.ACTIVE: + return 'completed' case 'rejected': - category = 'failed' - break + case MantecaKycStatus.INACTIVE: + return 'failed' case 'under_review': case 'incomplete': + case MantecaKycStatus.ONBOARDING: default: - category = 'processing' - break + return 'processing' } - return category } interface KycStatusDrawerProps { isOpen: boolean onClose: () => void - bridgeKycStatus: BridgeKycStatus - bridgeKycStartedAt?: string - bridgeKycApprovedAt?: string - bridgeKycRejectedAt?: string + verification?: IUserKycVerification + bridgeKycStatus?: BridgeKycStatus } // 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, - bridgeKycStatus, - bridgeKycStartedAt, - bridgeKycApprovedAt, - bridgeKycRejectedAt, -}: KycStatusDrawerProps) => { +export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus }: KycStatusDrawerProps) => { const [rejectionReason, setRejectionReason] = useState(null) const [isLoading, setIsLoading] = useState(false) + const { user } = useUserStore() const { handleInitiateKyc, iframeOptions, handleIframeClose, isLoading: isKycFlowLoading, } = useBridgeKycFlow({ onKycSuccess: onClose }) + // todo: add retry option for manteca kyc + // const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry as CountryData }) - const statusCategory = getKycStatusCategory(bridgeKycStatus) + const status = verification ? verification.status : bridgeKycStatus + const statusCategory = status ? getKycStatusCategory(status) : undefined + const countryCode = verification ? verification.mantecaGeo || verification.bridgeGeo : null + const isBridgeKyc = !verification && !!bridgeKycStatus useEffect(() => { // if the drawer is open and the kyc has failed, fetch the reason @@ -96,15 +92,29 @@ export const KycStatusDrawer = ({ switch (statusCategory) { case 'processing': - return + return ( + + ) case 'completed': - return + return ( + + ) case 'failed': return ( ) default: @@ -113,7 +123,7 @@ export const KycStatusDrawer = ({ } // don't render the drawer if the kyc status is unknown or not started - if (bridgeKycStatus === 'not_started') { + if (status === 'not_started' || !status) { return null } diff --git a/src/components/Kyc/KycStatusItem.tsx b/src/components/Kyc/KycStatusItem.tsx index bb0443751..ac4780fd8 100644 --- a/src/components/Kyc/KycStatusItem.tsx +++ b/src/components/Kyc/KycStatusItem.tsx @@ -8,14 +8,21 @@ import { useWebSocket } from '@/hooks/useWebSocket' import { BridgeKycStatus, formatDate } from '@/utils' import { HTMLAttributes } from 'react' import { twMerge } from 'tailwind-merge' +import { IUserKycVerification } from '@/interfaces' // this component shows the current kyc status and opens a drawer with more details on click export const KycStatusItem = ({ position = 'first', className, + verification, + bridgeKycStatus, + bridgeKycStartedAt, }: { position?: CardPosition className?: HTMLAttributes['className'] + verification?: IUserKycVerification + bridgeKycStatus?: BridgeKycStatus + bridgeKycStartedAt?: string }) => { const { user } = useUserStore() const [isDrawerOpen, setIsDrawerOpen] = useState(false) @@ -34,22 +41,25 @@ export const KycStatusItem = ({ }, }) - const birdgeKycStatus = wsBridgeKycStatus || user?.user?.bridgeKycStatus + const finalBridgeKycStatus = wsBridgeKycStatus || bridgeKycStatus || user?.user?.bridgeKycStatus + const kycStatus = verification ? verification.status : finalBridgeKycStatus const subtitle = useMemo(() => { - const bridgeKycStartedAt = user?.user?.bridgeKycStartedAt - if (!bridgeKycStartedAt) { + const date = verification + ? (verification.approvedAt ?? verification.updatedAt ?? verification.createdAt) + : bridgeKycStartedAt + if (!date) { return 'Verification in progress' } try { - return `Submitted on ${formatDate(new Date(bridgeKycStartedAt)).split(' - ')[0]}` + return `Submitted on ${formatDate(new Date(date)).split(' - ')[0]}` } catch (error) { - console.error('Failed to parse bridgeKycStartedAt date:', error) + console.error('Failed to parse date:', error) return 'Verification in progress' } - }, [user?.user?.bridgeKycStartedAt]) + }, [bridgeKycStartedAt, verification]) - if (!birdgeKycStatus || birdgeKycStatus === 'not_started') { + if (!kycStatus || kycStatus === 'not_started') { return null } @@ -74,10 +84,8 @@ export const KycStatusItem = ({ ) diff --git a/src/components/Kyc/states/KycCompleted.tsx b/src/components/Kyc/states/KycCompleted.tsx index 4daf99c7c..8ffd1f413 100644 --- a/src/components/Kyc/states/KycCompleted.tsx +++ b/src/components/Kyc/states/KycCompleted.tsx @@ -3,9 +3,18 @@ import { KYCStatusDrawerItem } from '../KycStatusItem' import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { useMemo } from 'react' import { formatDate } from '@/utils' +import { CountryRegionRow } from '../CountryRegionRow' // this component shows the kyc status when it's completed/approved. -export const KycCompleted = ({ bridgeKycApprovedAt }: { bridgeKycApprovedAt?: string }) => { +export const KycCompleted = ({ + bridgeKycApprovedAt, + countryCode, + isBridge, +}: { + bridgeKycApprovedAt?: string + countryCode?: string | null + isBridge?: boolean +}) => { const verifiedOn = useMemo(() => { if (!bridgeKycApprovedAt) return 'N/A' try { @@ -21,11 +30,8 @@ export const KycCompleted = ({ bridgeKycApprovedAt }: { bridgeKycApprovedAt?: st - + +
) diff --git a/src/components/Kyc/states/KycFailed.tsx b/src/components/Kyc/states/KycFailed.tsx index 855680e28..542e2b917 100644 --- a/src/components/Kyc/states/KycFailed.tsx +++ b/src/components/Kyc/states/KycFailed.tsx @@ -4,6 +4,7 @@ import { KYCStatusDrawerItem } from '../KycStatusItem' import Card from '@/components/Global/Card' import { useMemo } from 'react' import { formatDate } from '@/utils' +import { CountryRegionRow } from '../CountryRegionRow' // this component shows the kyc status when it's failed/rejected. // it displays the reason for the failure and provides a retry button. @@ -11,10 +12,14 @@ export const KycFailed = ({ reason, bridgeKycRejectedAt, onRetry, + countryCode, + isBridge, }: { reason: string | null bridgeKycRejectedAt?: string onRetry: () => void + countryCode?: string | null + isBridge?: boolean }) => { const rejectedOn = useMemo(() => { if (!bridgeKycRejectedAt) return 'N/A' @@ -32,9 +37,14 @@ export const KycFailed = ({ - + + + - {/* as requested, this button is currently for ui purposes and will be implemented later. */} diff --git a/src/components/Kyc/states/KycProcessing.tsx b/src/components/Kyc/states/KycProcessing.tsx index 74a2ad682..5fa1cfe4d 100644 --- a/src/components/Kyc/states/KycProcessing.tsx +++ b/src/components/Kyc/states/KycProcessing.tsx @@ -3,9 +3,18 @@ import { KYCStatusDrawerItem } from '../KycStatusItem' import Card from '@/components/Global/Card' import { useMemo } from 'react' import { formatDate } from '@/utils' +import { CountryRegionRow } from '../CountryRegionRow' // this component shows the kyc status while it's being processed. -export const KycProcessing = ({ bridgeKycStartedAt }: { bridgeKycStartedAt?: string }) => { +export const KycProcessing = ({ + bridgeKycStartedAt, + countryCode, + isBridge, +}: { + bridgeKycStartedAt?: string + countryCode?: string | null + isBridge?: boolean +}) => { const submittedOn = useMemo(() => { if (!bridgeKycStartedAt) return 'N/A' try { @@ -21,7 +30,7 @@ export const KycProcessing = ({ bridgeKycStartedAt }: { bridgeKycStartedAt?: str - + void @@ -24,10 +14,12 @@ interface UseKycFlowOptions { onManualClose?: () => void } -export type KycHistoryEntry = { +export interface KycHistoryEntry { isKyc: true uuid: string timestamp: string + verification?: IUserKycVerification + bridgeKycStatus?: BridgeKycStatus } // type guard to check if an entry is a KYC status item in history section From c32cd1be19535a95179d16007931253453d1932d Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:49:47 +0530 Subject: [PATCH 12/15] feat: handle home and history page entries for kyc --- src/app/(mobile-ui)/history/page.tsx | 43 ++++++++----- src/components/Home/HomeHistory.tsx | 94 +++++++++++++++++++--------- 2 files changed, 94 insertions(+), 43 deletions(-) diff --git a/src/app/(mobile-ui)/history/page.tsx b/src/app/(mobile-ui)/history/page.tsx index f0104a42d..f761e5074 100644 --- a/src/app/(mobile-ui)/history/page.tsx +++ b/src/app/(mobile-ui)/history/page.tsx @@ -12,7 +12,6 @@ import { useTransactionHistory } from '@/hooks/useTransactionHistory' import { useUserStore } from '@/redux/hooks' import { formatGroupHeaderDate, getDateGroup, getDateGroupKey } from '@/utils/dateGrouping.utils' import * as Sentry from '@sentry/nextjs' -import { usePathname } from 'next/navigation' import { isKycStatusItem } from '@/hooks/useBridgeKycFlow' import React, { useEffect, useMemo, useRef } from 'react' @@ -20,7 +19,6 @@ import React, { useEffect, useMemo, useRef } from 'react' * displays the user's transaction history with infinite scrolling and date grouping. */ const HistoryPage = () => { - const pathname = usePathname() const loaderRef = useRef(null) const { user } = useUserStore() @@ -63,17 +61,27 @@ const HistoryPage = () => { const allEntries = useMemo(() => historyData?.pages.flatMap((page) => page.entries) ?? [], [historyData]) const combinedAndSortedEntries = useMemo(() => { + if (isLoading) { + return [] + } const entries: Array = [...allEntries] - if ( - user?.user?.bridgeKycStatus && - user.user.bridgeKycStatus !== 'not_started' && - user.user.bridgeKycStartedAt - ) { - entries.push({ - isKyc: true, - timestamp: user.user.bridgeKycStartedAt, - uuid: 'kyc-status-item', + if (user) { + if (user.user?.bridgeKycStatus && user.user.bridgeKycStatus !== 'not_started') { + entries.push({ + isKyc: true, + timestamp: user.user.bridgeKycStartedAt ?? new Date(0).toISOString(), + uuid: 'bridge-kyc-status-item', + bridgeKycStatus: user.user.bridgeKycStatus, + }) + } + user.user.kycVerifications?.forEach((verification) => { + entries.push({ + isKyc: true, + timestamp: verification.approvedAt ?? new Date(0).toISOString(), + uuid: verification.providerUserId ?? `${verification.provider}-${verification.mantecaGeo}`, + verification, + }) }) } @@ -84,7 +92,7 @@ const HistoryPage = () => { }) return entries - }, [allEntries, user]) + }, [allEntries, user, isLoading]) if (isLoading && combinedAndSortedEntries.length === 0) { return @@ -101,7 +109,7 @@ const HistoryPage = () => { ) } - if (combinedAndSortedEntries.length === 0) { + if (!isLoading && combinedAndSortedEntries.length === 0) { return (
@@ -151,7 +159,14 @@ const HistoryPage = () => {
)} {isKycStatusItem(item) ? ( - + ) : ( (() => { const { transactionDetails, transactionCardType } = diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index e2570476e..6be08b5ab 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -15,7 +15,6 @@ import Card, { CardPosition, getCardPosition } from '../Global/Card' import EmptyState from '../Global/EmptyStates/EmptyState' import { KycStatusItem } from '../Kyc/KycStatusItem' import { isKycStatusItem, KycHistoryEntry } from '@/hooks/useBridgeKycFlow' -import { BridgeKycStatus } from '@/utils' import { useWallet } from '@/hooks/wallet/useWallet' import { useUserInteractions } from '@/hooks/useUserInteractions' @@ -36,7 +35,6 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern isError, error, } = useTransactionHistory({ mode, limit, username, filterMutualTxs, enabled: isLoggedIn }) - const bridgeKycStatus: BridgeKycStatus = user?.user?.bridgeKycStatus || 'not_started' // check if the username is the same as the current user const isSameUser = username === user?.user.username const { fetchBalance, getRewardWalletBalance } = useWallet() @@ -83,7 +81,7 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern const { interactions } = useUserInteractions(userIds) useEffect(() => { - if (historyData?.entries) { + if (!isLoading && historyData?.entries) { // Start with the fetched entries const entries: Array = [...historyData.entries] @@ -118,17 +116,22 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern // Add KYC status item if applicable and not on a public page // and the user is viewing their own history - if ( - isSameUser && - user?.user?.bridgeKycStatus && - user.user.bridgeKycStatus !== 'not_started' && - user.user.bridgeKycStartedAt && - !isPublic - ) { - entries.push({ - isKyc: true, - timestamp: user.user.bridgeKycStartedAt, - uuid: 'kyc-status-item', + if (isSameUser && !isPublic) { + if (user?.user?.bridgeKycStatus && user.user.bridgeKycStatus !== 'not_started') { + entries.push({ + isKyc: true, + timestamp: user.user.bridgeKycStartedAt ?? new Date(0).toISOString(), + uuid: 'bridge-kyc-status-item', + bridgeKycStatus: user.user.bridgeKycStatus, + }) + } + user?.user.kycVerifications?.forEach((verification) => { + entries.push({ + isKyc: true, + timestamp: verification.approvedAt ?? new Date(0).toISOString(), + uuid: verification.providerUserId ?? `${verification.provider}-${verification.mantecaGeo}`, + verification, + }) }) } @@ -142,7 +145,7 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern // Limit to the most recent entries setCombinedEntries(entries.slice(0, isPublic ? 20 : 5)) } - }, [historyData, wsHistoryEntries, isPublic, user]) + }, [historyData, wsHistoryEntries, isPublic, user, isLoading]) const pendingRequests = useMemo(() => { if (!combinedEntries.length) return [] @@ -182,22 +185,45 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern } // show empty state if no transactions exist - if (!combinedEntries.length) { + if (!isLoading && !combinedEntries.length) { return (
- {isSameUser && bridgeKycStatus !== 'not_started' && ( -
-

Activity

- -
- )} + {isSameUser && + (user?.user.bridgeKycStatus !== 'not_started' || + (user?.user.kycVerifications && user?.user.kycVerifications.length > 0)) && ( +
+

Activity

+ {user?.user.bridgeKycStatus && user?.user.bridgeKycStatus !== 'not_started' && ( + + )} + {user?.user.kycVerifications?.map((verification) => ( + + ))} +
+ )} -

Recent Transactions

- + {!user?.user.bridgeKycStatus && + (!user?.user.kycVerifications || user?.user.kycVerifications.length === 0) && ( + <> +

Recent Transactions

+ + + )}
) } @@ -257,7 +283,17 @@ const HomeHistory = ({ isPublic = false, username }: { isPublic?: boolean; usern // Render KYC status item if it's its turn in the sorted list if (isKycStatusItem(item)) { - return + return ( + + ) } // map the raw history entry to the format needed by the ui components From b39e62ae8d97a6f040ff2431634a20aafad74100 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:22:53 +0530 Subject: [PATCH 13/15] feat: handle retry for manteca --- .../add-money/[country]/bank/page.tsx | 4 +- .../AddWithdraw/AddWithdrawCountriesList.tsx | 6 +- .../Claim/Link/views/BankFlowManager.view.tsx | 4 +- src/components/Kyc/Country-Flag-And-Name.tsx | 2 - .../{index.tsx => InitiateBridgeKYCModal.tsx} | 10 +++- src/components/Kyc/KycStatusDrawer.tsx | 58 ++++++++++++++----- src/components/Kyc/states/KycFailed.tsx | 17 ++++-- .../views/ReqFulfillBankFlowManager.tsx | 4 +- 8 files changed, 72 insertions(+), 33 deletions(-) rename src/components/Kyc/{index.tsx => InitiateBridgeKYCModal.tsx} (93%) 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 00899daa9..fa3189c53 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -9,7 +9,6 @@ import { useOnrampFlow } from '@/context/OnrampFlowContext' import { useWallet } from '@/hooks/wallet/useWallet' import { formatAmount } from '@/utils' import { countryData } from '@/components/AddMoney/consts' -import { InitiateKYCModal } from '@/components/Kyc' import { BridgeKycStatus } from '@/utils/bridge-accounts.utils' import { useWebSocket } from '@/hooks/useWebSocket' import { useAuth } from '@/context/authContext' @@ -26,6 +25,7 @@ import AddMoneyBankDetails from '@/components/AddMoney/components/AddMoneyBankDe import { getCurrencyConfig, getCurrencySymbol, getMinimumAmount } from '@/utils/bridge.utils' import { OnrampConfirmationModal } from '@/components/AddMoney/components/OnrampConfirmationModal' import MercadoPago from '@/components/AddMoney/components/RegionalMethods/MercadoPago' +import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal' type AddStep = 'inputAmount' | 'kyc' | 'loading' | 'collectUserDetails' | 'showDetails' @@ -305,7 +305,7 @@ export default function OnrampBankPage() { if (step === 'kyc') { return (
- { initialData={{}} error={null} /> - setIsKycModalOpen(false)} onKycSuccess={handleKycSuccess} @@ -348,7 +348,7 @@ const AddWithdrawCountriesList = ({ flow }: AddWithdrawCountriesListProps) => { closeDrawer={() => setIsDrawerOpen(false)} /> )} - setIsKycModalOpen(false)} onKycSuccess={handleKycSuccess} diff --git a/src/components/Claim/Link/views/BankFlowManager.view.tsx b/src/components/Claim/Link/views/BankFlowManager.view.tsx index 74ff87028..ad33b0d11 100644 --- a/src/components/Claim/Link/views/BankFlowManager.view.tsx +++ b/src/components/Claim/Link/views/BankFlowManager.view.tsx @@ -24,13 +24,13 @@ import useSavedAccounts from '@/hooks/useSavedAccounts' import { ConfirmBankClaimView } from './Confirm.bank-claim.view' import { CountryListRouter } from '@/components/Common/CountryListRouter' import NavHeader from '@/components/Global/NavHeader' -import { InitiateKYCModal } from '@/components/Kyc' import { useWebSocket } from '@/hooks/useWebSocket' import { BridgeKycStatus } from '@/utils/bridge-accounts.utils' import { getCountryCodeForWithdraw } from '@/utils/withdraw.utils' import { useAppDispatch } from '@/redux/hooks' import { bankFormActions } from '@/redux/slices/bank-form-slice' import { sendLinksApi } from '@/services/sendLinks' +import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal' type BankAccountWithId = IBankAccountDetails & ( @@ -480,7 +480,7 @@ export const BankFlowManager = (props: IClaimScreenProps) => { initialData={{}} error={error} /> - setIsKycModalOpen(false)} onKycSuccess={handleKycSuccess} diff --git a/src/components/Kyc/Country-Flag-And-Name.tsx b/src/components/Kyc/Country-Flag-And-Name.tsx index 814ea9a36..dbc72d5de 100644 --- a/src/components/Kyc/Country-Flag-And-Name.tsx +++ b/src/components/Kyc/Country-Flag-And-Name.tsx @@ -35,5 +35,3 @@ export const CountryFlagAndName = ({ countryCode, isBridgeRegion }: CountryFlagA
) } - -// this component is used for the completed state of the kyc diff --git a/src/components/Kyc/index.tsx b/src/components/Kyc/InitiateBridgeKYCModal.tsx similarity index 93% rename from src/components/Kyc/index.tsx rename to src/components/Kyc/InitiateBridgeKYCModal.tsx index 9e62a2737..917468015 100644 --- a/src/components/Kyc/index.tsx +++ b/src/components/Kyc/InitiateBridgeKYCModal.tsx @@ -4,7 +4,7 @@ import IframeWrapper from '@/components/Global/IframeWrapper' import { KycVerificationInProgressModal } from './KycVerificationInProgressModal' import { IconName } from '@/components/Global/Icons/Icon' -interface KycModalFlowProps { +interface BridgeKycModalFlowProps { isOpen: boolean onClose: () => void onKycSuccess?: () => void @@ -12,7 +12,13 @@ interface KycModalFlowProps { flow?: 'add' | 'withdraw' | 'request_fulfillment' } -export const InitiateKYCModal = ({ isOpen, onClose, onKycSuccess, onManualClose, flow }: KycModalFlowProps) => { +export const InitiateBridgeKYCModal = ({ + isOpen, + onClose, + onKycSuccess, + onManualClose, + flow, +}: BridgeKycModalFlowProps) => { const { isLoading, error, diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index 774f69e23..55f46bb51 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -1,3 +1,6 @@ +// THIS COMPONENT IS MODIFIED FOR TESTING PURPOSES. +// PLEASE REVERT THE CHANGES AFTER TESTING. + import { useState, useEffect } from 'react' import { KycCompleted } from './states/KycCompleted' import { KycFailed } from './states/KycFailed' @@ -6,10 +9,12 @@ import PeanutLoading from '@/components/Global/PeanutLoading' import { Drawer, DrawerContent } from '../Global/Drawer' import { BridgeKycStatus } from '@/utils' import { getKycDetails } from '@/app/actions/users' -import IFrameWrapper from '../Global/IframeWrapper' import { IUserKycVerification, MantecaKycStatus } from '@/interfaces' import { useUserStore } from '@/redux/hooks' import { useBridgeKycFlow } from '@/hooks/useBridgeKycFlow' +import { useMantecaKycFlow } from '@/hooks/useMantecaKycFlow' +import { CountryData, countryData } from '@/components/AddMoney/consts' +import IFrameWrapper from '@/components/Global/IframeWrapper' // a helper to categorize the kyc status from the user object const getKycStatusCategory = (status: BridgeKycStatus | MantecaKycStatus): 'processing' | 'completed' | 'failed' => { @@ -40,19 +45,43 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus const [rejectionReason, setRejectionReason] = useState(null) const [isLoading, setIsLoading] = useState(false) const { user } = useUserStore() - const { - handleInitiateKyc, - iframeOptions, - handleIframeClose, - isLoading: isKycFlowLoading, - } = useBridgeKycFlow({ onKycSuccess: onClose }) - // todo: add retry option for manteca kyc - // const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry as CountryData }) const status = verification ? verification.status : bridgeKycStatus const statusCategory = status ? getKycStatusCategory(status) : undefined const countryCode = verification ? verification.mantecaGeo || verification.bridgeGeo : null const isBridgeKyc = !verification && !!bridgeKycStatus + const provider = verification ? verification.provider : 'BRIDGE' + + const { + handleInitiateKyc: initiateBridgeKyc, + iframeOptions: bridgeIframeOptions, + handleIframeClose: handleBridgeIframeClose, + isLoading: isBridgeLoading, + } = useBridgeKycFlow({ onKycSuccess: onClose, onManualClose: onClose }) + + const country = countryCode ? countryData.find((c) => c.id.toUpperCase() === countryCode.toUpperCase()) : undefined + + const { + openMantecaKyc, + iframeOptions: mantecaIframeOptions, + handleIframeClose: handleMantecaIframeClose, + isLoading: isMantecaLoading, + } = useMantecaKycFlow({ + onSuccess: onClose, + onClose: onClose, + onManualClose: onClose, + country: country as CountryData, + }) + + const onRetry = async () => { + if (provider === 'MANTECA') { + await openMantecaKyc(country as CountryData) + } else { + await initiateBridgeKyc() + } + } + + const isLoadingKyc = isBridgeLoading || isMantecaLoading useEffect(() => { // if the drawer is open and the kyc has failed, fetch the reason @@ -112,9 +141,10 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus ) default: @@ -132,12 +162,8 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus {renderContent()} - + + ) } diff --git a/src/components/Kyc/states/KycFailed.tsx b/src/components/Kyc/states/KycFailed.tsx index 542e2b917..459746149 100644 --- a/src/components/Kyc/states/KycFailed.tsx +++ b/src/components/Kyc/states/KycFailed.tsx @@ -11,15 +11,17 @@ import { CountryRegionRow } from '../CountryRegionRow' export const KycFailed = ({ reason, bridgeKycRejectedAt, - onRetry, countryCode, isBridge, + onRetry, + isLoading, }: { reason: string | null bridgeKycRejectedAt?: string - onRetry: () => void countryCode?: string | null isBridge?: boolean + onRetry: () => void + isLoading?: boolean }) => { const rejectedOn = useMemo(() => { if (!bridgeKycRejectedAt) return 'N/A' @@ -45,8 +47,15 @@ export const KycFailed = ({ hideBottomBorder />
-
) diff --git a/src/components/Request/views/ReqFulfillBankFlowManager.tsx b/src/components/Request/views/ReqFulfillBankFlowManager.tsx index 4e04f3663..9c6545931 100644 --- a/src/components/Request/views/ReqFulfillBankFlowManager.tsx +++ b/src/components/Request/views/ReqFulfillBankFlowManager.tsx @@ -10,7 +10,7 @@ import { useCreateOnramp } from '@/hooks/useCreateOnramp' import { usePaymentStore } from '@/redux/hooks' import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermineBankRequestType' import { createOnrampForGuest } from '@/app/actions/onramp' -import { InitiateKYCModal } from '@/components/Kyc' +import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal' import { UserDetailsForm, type UserDetailsFormData } from '@/components/AddMoney/UserDetailsForm' import { useMemo, useState, useRef, useEffect } from 'react' import NavHeader from '@/components/Global/NavHeader' @@ -158,7 +158,7 @@ export const ReqFulfillBankFlowManager = ({ parsedPaymentData }: { parsedPayment // main render logic based on the current flow step if (showVerificationModal) { return ( - { setIsKycModalOpen(false) From d09105ccc296389f81988d2338c701fa62efd8d5 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:25:05 +0530 Subject: [PATCH 14/15] chore: remove test comment --- src/components/Kyc/KycStatusDrawer.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index 55f46bb51..341083d3d 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -1,6 +1,3 @@ -// THIS COMPONENT IS MODIFIED FOR TESTING PURPOSES. -// PLEASE REVERT THE CHANGES AFTER TESTING. - import { useState, useEffect } from 'react' import { KycCompleted } from './states/KycCompleted' import { KycFailed } from './states/KycFailed' From d4226d460c7defc9025dd0eb16fdaf776709fd4f Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:46:25 +0530 Subject: [PATCH 15/15] chore: resolve cr comment --- src/components/Kyc/KycStatusDrawer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Kyc/KycStatusDrawer.tsx b/src/components/Kyc/KycStatusDrawer.tsx index c56b7e8ae..bf8834563 100644 --- a/src/components/Kyc/KycStatusDrawer.tsx +++ b/src/components/Kyc/KycStatusDrawer.tsx @@ -120,7 +120,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus case 'processing': return ( @@ -128,7 +128,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus case 'completed': return ( @@ -137,7 +137,7 @@ export const KycStatusDrawer = ({ isOpen, onClose, verification, bridgeKycStatus return (