From 44f9e178b35f8f4074d5c320e869758741ca1479 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 3 Sep 2025 13:02:33 +0530 Subject: [PATCH 01/12] initial implementation --- src/components/Claim/Claim.tsx | 13 +++++++++++++ src/components/Claim/Link/Initial.view.tsx | 7 +++++-- src/components/Claim/useClaimLink.tsx | 21 +++++++++++++++++++++ src/components/Common/ActionList.tsx | 5 ++++- src/utils/general.utils.ts | 1 + 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index 90ff30d8c..80a68c2f2 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -25,6 +25,8 @@ import * as _consts from './Claim.consts' import FlowManager from './Link/FlowManager' import { type PeanutCrossChainRoute } from '@/services/swap' import { NotFoundClaimLink, WrongPasswordClaimLink, ClaimedView } from './Generic' +import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext' +import { useSearchParams } from 'next/navigation' export const Claim = ({}) => { const [step, setStep] = useState<_consts.IClaimScreenState>(_consts.INIT_VIEW_STATE) @@ -66,6 +68,9 @@ export const Claim = ({}) => { const senderId = claimLinkData?.sender.userId const { interactions } = useUserInteractions(senderId ? [senderId] : []) + const { setFlowStep: setClaimBankFlowStep } = useClaimBankFlow() + const searchParams = useSearchParams() + const transactionForDrawer: TransactionDetails | null = useMemo(() => { if (!claimLinkData) return null @@ -253,6 +258,14 @@ export const Claim = ({}) => { } }, [linkState, transactionForDrawer]) + // redirect to bank flow if user is KYC approved and step is bank + useEffect(() => { + const stepFromURL = searchParams.get('step') + if (user?.user.kycStatus === 'approved' && stepFromURL === 'bank') { + setClaimBankFlowStep(ClaimBankFlowStep.BankCountryList) + } + }, [user]) + return ( {linkState === _consts.claimLinkStateType.LOADING && } diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 20fc7cbf2..2dc8afbcd 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -103,7 +103,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { isXChain, setIsXChain, } = useContext(tokenSelectorContext) - const { claimLink, claimLinkXchain } = useClaimLink() + const { claimLink, claimLinkXchain, removeParamStep } = useClaimLink() const { isConnected: isPeanutWallet, address, fetchBalance } = useWallet() const router = useRouter() const { user } = useAuth() @@ -774,7 +774,10 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { setShowVerificationModal(false)} + onClose={() => { + removeParamStep() + setShowVerificationModal(false) + }} description="The sender isn't verified, so please create an account and verify your identity to have the funds deposited to your bank." /> diff --git a/src/components/Claim/useClaimLink.tsx b/src/components/Claim/useClaimLink.tsx index d1d0197ce..736358884 100644 --- a/src/components/Claim/useClaimLink.tsx +++ b/src/components/Claim/useClaimLink.tsx @@ -11,11 +11,15 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { isTestnetChain } from '@/utils' import * as Sentry from '@sentry/nextjs' import { useAccount } from 'wagmi' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' const useClaimLink = () => { const { fetchBalance } = useWallet() const { chain: currentChain } = useAccount() const { switchChainAsync } = useSwitchChain() + const pathname = usePathname() + const searchParams = useSearchParams() + const router = useRouter() const { setLoadingState } = useContext(loadingStateContext) @@ -93,10 +97,27 @@ const useClaimLink = () => { } } + const addParamStep = () => { + const params = new URLSearchParams(searchParams) + params.set('step', 'bank') + + const hash = window.location.hash + router.replace(`${pathname}?${params.toString()}${hash}`) + } + + const removeParamStep = () => { + const params = new URLSearchParams(searchParams) + params.delete('step') + const queryString = params.toString() + router.replace(`${pathname}${queryString ? `?${queryString}` : ''}${window.location.hash}`, { scroll: false }) + } + return { claimLink, claimLinkXchain, switchNetwork, + addParamStep, + removeParamStep, } } diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index e02994fe8..a1b196d8a 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -13,7 +13,7 @@ import { Button } from '../0_Bruddle' import { PEANUT_LOGO_BLACK } from '@/assets/illustrations' import Image from 'next/image' import { saveRedirectUrl } from '@/utils' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { PEANUTMAN_LOGO } from '@/assets/peanut' import { BankClaimType, useDetermineBankClaimType } from '@/hooks/useDetermineBankClaimType' import useSavedAccounts from '@/hooks/useSavedAccounts' @@ -24,6 +24,7 @@ import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermi import { GuestVerificationModal } from '../Global/GuestVerificationModal' import ActionListDaimoPayButton from './ActionListDaimoPayButton' import { ACTION_METHODS, PaymentMethod } from '@/constants/actionlist.consts' +import useClaimLink from '../Claim/useClaimLink' interface IActionListProps { flow: 'claim' | 'request' @@ -50,6 +51,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin const { requestType } = useDetermineBankRequestType(requesterUserId) const savedAccounts = useSavedAccounts() const { usdAmount } = usePaymentStore() + const { addParamStep } = useClaimLink() const { setShowRequestFulfilmentBankFlowManager, setShowExternalWalletFulfilMethods, @@ -68,6 +70,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin case 'bank': { if (claimType === BankClaimType.GuestKycNeeded) { + addParamStep() setShowVerificationModal(true) } else { if (savedAccounts.length) { diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index c50658c9c..477571b13 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1213,6 +1213,7 @@ export function isStableCoin(tokenSymbol: string): boolean { export const saveRedirectUrl = () => { const currentUrl = new URL(window.location.href) + const relativeUrl = currentUrl.href.replace(currentUrl.origin, '') saveToLocalStorage('redirect', `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`) } From 5b28086d2610af36996d321e03ea0bfff827baca Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 3 Sep 2025 13:02:45 +0530 Subject: [PATCH 02/12] show verification modal --- src/components/Claim/Link/Initial.view.tsx | 1 + src/components/Claim/useClaimLink.tsx | 2 +- src/components/Global/ActionModal/index.tsx | 4 ++++ .../Global/GuestVerificationModal/index.tsx | 5 +++++ .../Global/PostSignupActionManager/index.tsx | 22 +++++++++++-------- .../post-signup-action.consts.ts | 6 ++--- src/components/Setup/Views/Welcome.tsx | 10 ++++----- src/utils/general.utils.ts | 2 +- 8 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 2dc8afbcd..93015c899 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -772,6 +772,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { modalPanelClassName="max-w-md mx-8" /> { diff --git a/src/components/Claim/useClaimLink.tsx b/src/components/Claim/useClaimLink.tsx index 736358884..2124987f6 100644 --- a/src/components/Claim/useClaimLink.tsx +++ b/src/components/Claim/useClaimLink.tsx @@ -102,7 +102,7 @@ const useClaimLink = () => { params.set('step', 'bank') const hash = window.location.hash - router.replace(`${pathname}?${params.toString()}${hash}`) + router.replace(`${pathname}?${params.toString()}${hash}`, { scroll: false }) } const removeParamStep = () => { diff --git a/src/components/Global/ActionModal/index.tsx b/src/components/Global/ActionModal/index.tsx index 8a9e0b03e..b6dabfa18 100644 --- a/src/components/Global/ActionModal/index.tsx +++ b/src/components/Global/ActionModal/index.tsx @@ -40,6 +40,7 @@ export interface ActionModalProps { descriptionClassName?: string buttonProps?: ButtonProps footer?: React.ReactNode + customContent?: React.ReactNode } const ActionModal: React.FC = ({ @@ -64,6 +65,7 @@ const ActionModal: React.FC = ({ descriptionClassName, buttonProps, footer, + customContent, }) => { const defaultModalPanelClasses = 'max-w-[85%]' const defaultIconContainerClassName = 'bg-primary-1' // default pink background @@ -129,6 +131,8 @@ const ActionModal: React.FC = ({ )} + {customContent && customContent} + {(checkbox || (ctas && ctas.length > 0)) && (
{checkbox && ( diff --git a/src/components/Global/GuestVerificationModal/index.tsx b/src/components/Global/GuestVerificationModal/index.tsx index 54bd2dab3..06087ede3 100644 --- a/src/components/Global/GuestVerificationModal/index.tsx +++ b/src/components/Global/GuestVerificationModal/index.tsx @@ -9,6 +9,7 @@ interface GuestVerificationModalProps { isOpen: boolean onClose: () => void secondaryCtaLabel: string + shouldShowVerificationModalOnSignup?: boolean } export const GuestVerificationModal = ({ @@ -16,6 +17,7 @@ export const GuestVerificationModal = ({ onClose, description, secondaryCtaLabel, + shouldShowVerificationModalOnSignup, }: GuestVerificationModalProps) => { const router = useRouter() return ( @@ -34,6 +36,9 @@ export const GuestVerificationModal = ({ className: 'md:py-2.5', onClick: () => { saveRedirectUrl() + if (shouldShowVerificationModalOnSignup) { + localStorage.setItem('showVerificationModal', 'true') + } router.push('/setup') }, }, diff --git a/src/components/Global/PostSignupActionManager/index.tsx b/src/components/Global/PostSignupActionManager/index.tsx index 16b2bfc8d..9e2fc2d8d 100644 --- a/src/components/Global/PostSignupActionManager/index.tsx +++ b/src/components/Global/PostSignupActionManager/index.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from 'react' import ActionModal from '../ActionModal' import { POST_SIGNUP_ACTIONS } from './post-signup-action.consts' import { IconName } from '../Icons/Icon' +import { KycFlow } from '@/components/Kyc/KycFlow' export const PostSignupActionManager = ({ onActionModalVisibilityChange, @@ -24,7 +25,8 @@ export const PostSignupActionManager = ({ useEffect(() => { const redirectUrl = getFromLocalStorage('redirect') - if (redirectUrl) { + const showVerificationModal = localStorage.getItem('showVerificationModal') + if (redirectUrl && showVerificationModal === 'true') { const matchedAction = POST_SIGNUP_ACTIONS.find((action) => action.pathPattern.test(redirectUrl)) if (matchedAction) { @@ -53,18 +55,20 @@ export const PostSignupActionManager = ({ onClose={() => { setShowModal(false) localStorage.removeItem('redirect') + localStorage.removeItem('showVerificationModal') }} title={actionConfig.title} description={actionConfig.description} icon={actionConfig.icon as IconName} - ctas={[ - { - text: actionConfig.cta, - onClick: actionConfig.action, - variant: 'purple', - shadowSize: '4', - }, - ]} + customContent={} + // ctas={[ + // { + // text: actionConfig.cta, + // onClick: actionConfig.action, + // variant: 'purple', + // shadowSize: '4', + // }, + // ]} /> ) } diff --git a/src/components/Global/PostSignupActionManager/post-signup-action.consts.ts b/src/components/Global/PostSignupActionManager/post-signup-action.consts.ts index d84e5a27e..9ccc8bc3d 100644 --- a/src/components/Global/PostSignupActionManager/post-signup-action.consts.ts +++ b/src/components/Global/PostSignupActionManager/post-signup-action.consts.ts @@ -5,9 +5,9 @@ export const POST_SIGNUP_ACTIONS = [ // this regex will match any path that contains the word "claim", this helps in determing if the user is coming from a claim link pathPattern: /claim/, config: { - title: 'Claim your money', - description: `You're almost done! Tap Claim Funds to move the money into your new Peanut Wallet.`, - cta: 'Claim Funds', + title: 'Identity verification required', + description: `To claim money to your bank account, you’ll need to complete a quick identity verification.`, + cta: 'Start verification', icon: 'dollar' as IconName, }, }, diff --git a/src/components/Setup/Views/Welcome.tsx b/src/components/Setup/Views/Welcome.tsx index d0a413be5..18e75d2d2 100644 --- a/src/components/Setup/Views/Welcome.tsx +++ b/src/components/Setup/Views/Welcome.tsx @@ -47,11 +47,11 @@ const WelcomeStep = () => { onClick={() => { handleLogin() .then(() => { - const localStorageRedirect = getFromLocalStorage('redirect') - if (localStorageRedirect) { - localStorage.removeItem('redirect') // Clear the redirect URL - push(localStorageRedirect) - } + // const localStorageRedirect = getFromLocalStorage('redirect') + // if (localStorageRedirect) { + // localStorage.removeItem('redirect') // Clear the redirect URL + // push(localStorageRedirect) + // } }) .catch((e) => { handleError(e) diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 477571b13..c7b81977a 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1214,7 +1214,7 @@ export function isStableCoin(tokenSymbol: string): boolean { export const saveRedirectUrl = () => { const currentUrl = new URL(window.location.href) const relativeUrl = currentUrl.href.replace(currentUrl.origin, '') - saveToLocalStorage('redirect', `${currentUrl.pathname}${currentUrl.search}${currentUrl.hash}`) + saveToLocalStorage('redirect', relativeUrl) } export const formatPaymentStatus = (status: string): string => { From 2baca60c45fd122acc3d365157ebf2c1b29f6dff Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Thu, 4 Sep 2025 13:18:02 +0530 Subject: [PATCH 03/12] implement claim to wallet after signup/login --- src/components/Claim/Link/Initial.view.tsx | 7 +++++++ src/components/Claim/useClaimLink.tsx | 13 +++++++------ src/components/Common/ActionList.tsx | 4 ++-- src/components/Global/GuestLoginCta/index.tsx | 7 ++++++- src/components/Setup/Views/SetupPasskey.tsx | 9 ++++++++- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 93015c899..dfc79e083 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -615,6 +615,13 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { } } + useEffect(() => { + const stepFromURL = searchParams.get('step') + if (user && claimLinkData.status !== 'CLAIMED' && stepFromURL === 'claim') { + handleClaimLink() + } + }, [user, searchParams]) + if (claimBankFlowStep) { return } diff --git a/src/components/Claim/useClaimLink.tsx b/src/components/Claim/useClaimLink.tsx index 2124987f6..4aeaf9b6c 100644 --- a/src/components/Claim/useClaimLink.tsx +++ b/src/components/Claim/useClaimLink.tsx @@ -11,7 +11,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { isTestnetChain } from '@/utils' import * as Sentry from '@sentry/nextjs' import { useAccount } from 'wagmi' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { usePathname, useSearchParams } from 'next/navigation' const useClaimLink = () => { const { fetchBalance } = useWallet() @@ -19,7 +19,6 @@ const useClaimLink = () => { const { switchChainAsync } = useSwitchChain() const pathname = usePathname() const searchParams = useSearchParams() - const router = useRouter() const { setLoadingState } = useContext(loadingStateContext) @@ -97,19 +96,21 @@ const useClaimLink = () => { } } - const addParamStep = () => { + const addParamStep = (step: 'bank' | 'claim') => { const params = new URLSearchParams(searchParams) - params.set('step', 'bank') + params.set('step', step) const hash = window.location.hash - router.replace(`${pathname}?${params.toString()}${hash}`, { scroll: false }) + const newUrl = `${pathname}?${params.toString()}${hash}` + window.history.replaceState(null, '', newUrl) } const removeParamStep = () => { const params = new URLSearchParams(searchParams) params.delete('step') const queryString = params.toString() - router.replace(`${pathname}${queryString ? `?${queryString}` : ''}${window.location.hash}`, { scroll: false }) + const newUrl = `${pathname}${queryString ? `?${queryString}` : ''}${window.location.hash}` + window.history.replaceState(null, '', newUrl) } return { diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index a1b196d8a..e94fbff6b 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -70,7 +70,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin case 'bank': { if (claimType === BankClaimType.GuestKycNeeded) { - addParamStep() + addParamStep('bank') setShowVerificationModal(true) } else { if (savedAccounts.length) { @@ -139,7 +139,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
- {customContent && customContent} - {(checkbox || (ctas && ctas.length > 0)) && (
{checkbox && ( diff --git a/src/components/Global/GuestVerificationModal/index.tsx b/src/components/Global/GuestVerificationModal/index.tsx index 7ff5be17f..279c45c32 100644 --- a/src/components/Global/GuestVerificationModal/index.tsx +++ b/src/components/Global/GuestVerificationModal/index.tsx @@ -9,7 +9,7 @@ interface GuestVerificationModalProps { isOpen: boolean onClose: () => void secondaryCtaLabel: string - shouldShowVerificationModalOnSignup?: boolean + redirectToVerification?: boolean } export const GuestVerificationModal = ({ @@ -17,7 +17,7 @@ export const GuestVerificationModal = ({ onClose, description, secondaryCtaLabel, - shouldShowVerificationModalOnSignup, + redirectToVerification, }: GuestVerificationModalProps) => { const router = useRouter() return ( @@ -36,7 +36,7 @@ export const GuestVerificationModal = ({ className: 'md:py-2.5', onClick: () => { saveRedirectUrl() - if (shouldShowVerificationModalOnSignup) { + if (redirectToVerification) { router.push('/setup?redirect_uri=/profile/identity-verification') return } From 61825be12b07141cbc8e87c6e413864765881cbc Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 5 Sep 2025 13:19:55 +0530 Subject: [PATCH 06/12] sanitize redirect url --- src/components/Global/GuestLoginCta/index.tsx | 5 +++-- src/components/Setup/Views/SetupPasskey.tsx | 5 +++-- src/components/Setup/Views/Welcome.tsx | 21 ++++++++----------- src/utils/general.utils.ts | 14 +++++++++++++ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/components/Global/GuestLoginCta/index.tsx b/src/components/Global/GuestLoginCta/index.tsx index 716b2ceed..121be04e7 100644 --- a/src/components/Global/GuestLoginCta/index.tsx +++ b/src/components/Global/GuestLoginCta/index.tsx @@ -2,7 +2,7 @@ import { Button } from '@/components/0_Bruddle' import Divider from '@/components/0_Bruddle/Divider' import { useToast } from '@/components/0_Bruddle/Toast' import { useZeroDev } from '@/hooks/useZeroDev' -import { saveRedirectUrl } from '@/utils' +import { sanitizeRedirectURL, saveRedirectUrl } from '@/utils' import { useAppKit } from '@reown/appkit/react' import * as Sentry from '@sentry/nextjs' import { useRouter, useSearchParams } from 'next/navigation' @@ -42,7 +42,8 @@ const GuestLoginCta = ({ hideConnectWallet = false, view }: GuestLoginCtaProps) await handleLogin() const redirect_uri = searchParams.get('redirect_uri') if (redirect_uri) { - router.push(redirect_uri) + const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri) + router.push(sanitizedRedirectUrl) } } catch (e) { toast.error('Error logging in') diff --git a/src/components/Setup/Views/SetupPasskey.tsx b/src/components/Setup/Views/SetupPasskey.tsx index efee3396e..5cfe73911 100644 --- a/src/components/Setup/Views/SetupPasskey.tsx +++ b/src/components/Setup/Views/SetupPasskey.tsx @@ -10,7 +10,7 @@ import * as Sentry from '@sentry/nextjs' import { WalletProviderType } from '@/interfaces' import { WebAuthnError } from '@simplewebauthn/browser' import Link from 'next/link' -import { getFromLocalStorage } from '@/utils' +import { getFromLocalStorage, sanitizeRedirectURL } from '@/utils' import { POST_SIGNUP_ACTIONS } from '@/components/Global/PostSignupActionManager/post-signup-action.consts' const SetupPasskey = () => { @@ -35,7 +35,8 @@ const SetupPasskey = () => { .then(() => { const redirect_uri = searchParams.get('redirect_uri') if (redirect_uri) { - router.push(redirect_uri) + const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri) + router.push(sanitizedRedirectUrl) return } diff --git a/src/components/Setup/Views/Welcome.tsx b/src/components/Setup/Views/Welcome.tsx index 8f40481d4..6bde37fd7 100644 --- a/src/components/Setup/Views/Welcome.tsx +++ b/src/components/Setup/Views/Welcome.tsx @@ -5,7 +5,7 @@ import { useToast } from '@/components/0_Bruddle/Toast' import { useAuth } from '@/context/authContext' import { useSetupFlow } from '@/hooks/useSetupFlow' import { useZeroDev } from '@/hooks/useZeroDev' -import { getFromLocalStorage } from '@/utils' +import { getFromLocalStorage, sanitizeRedirectURL } from '@/utils' import * as Sentry from '@sentry/nextjs' import { useRouter, useSearchParams } from 'next/navigation' import { useEffect } from 'react' @@ -21,7 +21,11 @@ const WelcomeStep = () => { useEffect(() => { if (!!user) { const localStorageRedirect = getFromLocalStorage('redirect') - if (localStorageRedirect) { + const redirect_uri = searchParams.get('redirect_uri') + if (redirect_uri) { + const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri) + push(sanitizedRedirectUrl) + } else if (localStorageRedirect) { localStorage.removeItem('redirect') push(localStorageRedirect) } else { @@ -54,16 +58,9 @@ const WelcomeStep = () => { className="h-11" variant="primary-soft" onClick={() => { - handleLogin() - .then(() => { - const redirect_uri = searchParams.get('redirect_uri') - if (redirect_uri) { - push(redirect_uri) - } - }) - .catch((e) => { - handleError(e) - }) + handleLogin().catch((e) => { + handleError(e) + }) }} > Log In diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 57c2dea4d..c30e97248 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -1217,6 +1217,20 @@ export const saveRedirectUrl = () => { saveToLocalStorage('redirect', relativeUrl) } +export const sanitizeRedirectURL = (redirectUrl: string): string => { + try { + const u = new URL(redirectUrl, window.location.origin) + if (u.origin === window.location.origin) { + return u.pathname + u.search + u.hash + } + } catch { + if (redirectUrl.startsWith('/')) { + return redirectUrl + } + } + return redirectUrl +} + export const formatPaymentStatus = (status: string): string => { switch (status.toUpperCase()) { case 'NEW': From ed3f2a3a391238d2f658bdc85318619572224ccb Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 5 Sep 2025 13:20:38 +0530 Subject: [PATCH 07/12] remove param before claiming --- src/components/Claim/Link/Initial.view.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 3ce1cb694..0828086e8 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -626,6 +626,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { useEffect(() => { const stepFromURL = searchParams.get('step') if (user && claimLinkData.status !== 'CLAIMED' && stepFromURL === 'claim') { + removeParamStep() handleClaimLink() } }, [user, searchParams]) From 746e07b3aef9caa9c509382e851195969e867bd3 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 5 Sep 2025 13:22:34 +0530 Subject: [PATCH 08/12] Add redirect to profile if KYC status is approved in IdentityVerificationView component --- .../Profile/views/IdentityVerification.view.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Profile/views/IdentityVerification.view.tsx b/src/components/Profile/views/IdentityVerification.view.tsx index 2849b1761..d672005a6 100644 --- a/src/components/Profile/views/IdentityVerification.view.tsx +++ b/src/components/Profile/views/IdentityVerification.view.tsx @@ -9,7 +9,7 @@ import { KycVerificationInProgressModal } from '@/components/Kyc/KycVerification import { useAuth } from '@/context/authContext' import { useKycFlow } from '@/hooks/useKycFlow' import { useRouter } from 'next/navigation' -import React, { useMemo, useRef, useState } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' const IdentityVerificationView = () => { const { user, fetchUser } = useAuth() @@ -65,6 +65,13 @@ const IdentityVerificationView = () => { return {} } + // if kyc is already approved, redirect to profile + useEffect(() => { + if (user?.user.kycStatus === 'approved') { + router.replace('/profile') + } + }, [user]) + return (
router.replace('/profile')} /> From 31dd819e53c34da4ab7a383ff2f766b0fadeaff1 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 5 Sep 2025 20:56:04 +0530 Subject: [PATCH 09/12] add connected peanutWallet check --- src/components/Claim/Link/Initial.view.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 0828086e8..f7297449b 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -625,11 +625,11 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { useEffect(() => { const stepFromURL = searchParams.get('step') - if (user && claimLinkData.status !== 'CLAIMED' && stepFromURL === 'claim') { + if (user && claimLinkData.status !== 'CLAIMED' && stepFromURL === 'claim' && isPeanutWallet) { removeParamStep() handleClaimLink() } - }, [user, searchParams]) + }, [user, searchParams, isPeanutWallet]) if (claimBankFlowStep) { return From d7255bae7ca6ec1d7388ca667b8b867090408f85 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 9 Sep 2025 14:00:11 +0530 Subject: [PATCH 10/12] fix: automatic claim not working --- src/app/api/auto-claim/route.ts | 55 ++++++++++++++++++++++ src/components/Claim/Link/Initial.view.tsx | 11 +++-- src/services/sendLinks.ts | 32 +++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/app/api/auto-claim/route.ts diff --git a/src/app/api/auto-claim/route.ts b/src/app/api/auto-claim/route.ts new file mode 100644 index 000000000..7c00ad546 --- /dev/null +++ b/src/app/api/auto-claim/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from 'next/server' +import { fetchWithSentry } from '@/utils' +import { PEANUT_API_URL } from '@/constants' + +/** + * API route for automated link claiming without requiring user interaction. + * + * This route serves as a workaround for Next.js server action limitations: + * Server actions cannot be directly called within useEffect hooks, which is + * necessary for our automatic claim flow. By exposing this as an API route + * instead, we can make the claim request safely from client-side effects. + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { pubKey, recipient, password } = body + + if (!pubKey || !recipient || !password) { + return NextResponse.json( + { error: 'Missing required parameters: pubKey, recipient, or password' }, + { status: 400 } + ) + } + + const response = await fetchWithSentry(`${PEANUT_API_URL}/send-links/${pubKey}/claim`, { + method: 'POST', + headers: { + 'api-key': process.env.PEANUT_API_KEY!, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + recipient, + password, + }), + }) + + if (!response.ok) { + return NextResponse.json( + { error: `Failed to claim link: ${response.statusText}` }, + { status: response.status } + ) + } + + const responseText = await response.text() + console.log('response', responseText) + return new NextResponse(responseText, { + headers: { + 'Content-Type': 'application/json', + }, + }) + } catch (error) { + console.error('Error claiming send link:', error) + return NextResponse.json({ error: 'Failed to claim send link' }, { status: 500 }) + } +} diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index f7297449b..54188abc7 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -157,7 +157,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { }, [recipientType, claimLinkData.chainId, isPeanutChain, claimLinkData.tokenAddress]) const handleClaimLink = useCallback( - async (bypassModal = false) => { + async (bypassModal = false, autoClaim = false) => { if (!isPeanutWallet && !bypassModal) { setShowConfirmationModal(true) return @@ -175,8 +175,11 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { try { setLoadingState('Executing transaction') if (isPeanutWallet) { - await sendLinksApi.claim(user?.user.username ?? address, claimLinkData.link) - + if (autoClaim) { + await sendLinksApi.autoClaimLink(user?.user.username ?? address, claimLinkData.link) + } else { + await sendLinksApi.claim(user?.user.username ?? address, claimLinkData.link) + } setClaimType('claim') onCustom('SUCCESS') fetchBalance() @@ -627,7 +630,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { const stepFromURL = searchParams.get('step') if (user && claimLinkData.status !== 'CLAIMED' && stepFromURL === 'claim' && isPeanutWallet) { removeParamStep() - handleClaimLink() + handleClaimLink(false, true) } }, [user, searchParams, isPeanutWallet]) diff --git a/src/services/sendLinks.ts b/src/services/sendLinks.ts index 1f22698d8..d00db2bb3 100644 --- a/src/services/sendLinks.ts +++ b/src/services/sendLinks.ts @@ -205,6 +205,38 @@ export const sendLinksApi = { return await claimSendLink(pubKey, recipient, params.password) }, + /** + * Automaticaly claim a send link without the need of the recipient to click the claim button - Calls the Next.js API route + * + * @param recipient - The recipient's address or username + * @param link - The link to claim + * @returns The claim link data + */ + autoClaimLink: async (recipient: string, link: string): Promise => { + try { + const params = getParamsFromLink(link) + const pubKey = generateKeysFromString(params.password).address + const response = await fetch(`/api/auto-claim`, { + method: 'POST', + body: JSON.stringify({ + pubKey, + recipient, + password: params.password, + }), + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result = await response.json() + return result + } catch (error) { + console.error('Failed to automatically claim link:', error) + throw error + } + }, + /** * associates a logged-in user with a claim transaction. * this is called after an external wallet claim is successful. From 7683b29bd3145bb5448d5d387414125dcb876344 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 9 Sep 2025 14:31:38 +0530 Subject: [PATCH 11/12] fix: response handling --- src/services/sendLinks.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/sendLinks.ts b/src/services/sendLinks.ts index 536e6cd6a..04b72d56e 100644 --- a/src/services/sendLinks.ts +++ b/src/services/sendLinks.ts @@ -218,6 +218,7 @@ export const sendLinksApi = { const pubKey = generateKeysFromString(params.password).address const response = await fetch(`/api/auto-claim`, { method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pubKey, recipient, @@ -226,10 +227,12 @@ export const sendLinksApi = { }) if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) + const errText = await response.text() + throw new Error(`HTTP error! status: ${response.status}, message: ${errText}`) } - const result = await response.json() + const text = await response.text() + const result: SendLink = jsonParse(text) return result } catch (error) { console.error('Failed to automatically claim link:', error) From 101dccb39d12ee78503e660b98bea85b3533575d Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 9 Sep 2025 14:46:55 +0530 Subject: [PATCH 12/12] fix: build errors --- src/components/Claim/Claim.tsx | 2 +- src/components/Global/PostSignupActionManager/index.tsx | 2 +- src/components/Profile/views/IdentityVerification.view.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Claim/Claim.tsx b/src/components/Claim/Claim.tsx index 8f60adb90..32e63187f 100644 --- a/src/components/Claim/Claim.tsx +++ b/src/components/Claim/Claim.tsx @@ -262,7 +262,7 @@ export const Claim = ({}) => { // redirect to bank flow if user is KYC approved and step is bank useEffect(() => { const stepFromURL = searchParams.get('step') - if (user?.user.kycStatus === 'approved' && stepFromURL === 'bank') { + if (user?.user.bridgeKycStatus === 'approved' && stepFromURL === 'bank') { setClaimBankFlowStep(ClaimBankFlowStep.BankCountryList) } }, [user]) diff --git a/src/components/Global/PostSignupActionManager/index.tsx b/src/components/Global/PostSignupActionManager/index.tsx index 0ab73f6cc..d1604c4b2 100644 --- a/src/components/Global/PostSignupActionManager/index.tsx +++ b/src/components/Global/PostSignupActionManager/index.tsx @@ -26,7 +26,7 @@ export const PostSignupActionManager = ({ const checkClaimModalAfterKYC = () => { const redirectUrl = getFromLocalStorage('redirect') - if (user?.user.kycStatus === 'approved' && redirectUrl) { + if (user?.user.bridgeKycStatus === 'approved' && redirectUrl) { const matchedAction = POST_SIGNUP_ACTIONS.find((action) => action.pathPattern.test(redirectUrl)) if (matchedAction) { setActionConfig({ diff --git a/src/components/Profile/views/IdentityVerification.view.tsx b/src/components/Profile/views/IdentityVerification.view.tsx index d672005a6..c36dbff12 100644 --- a/src/components/Profile/views/IdentityVerification.view.tsx +++ b/src/components/Profile/views/IdentityVerification.view.tsx @@ -67,7 +67,7 @@ const IdentityVerificationView = () => { // if kyc is already approved, redirect to profile useEffect(() => { - if (user?.user.kycStatus === 'approved') { + if (user?.user.bridgeKycStatus === 'approved') { router.replace('/profile') } }, [user])