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/Claim.tsx b/src/components/Claim/Claim.tsx index 6bb914532..32e63187f 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 @@ -254,6 +259,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.bridgeKycStatus === '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 9b06079f4..d1de4a4b2 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() @@ -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() @@ -623,6 +626,14 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { } } + useEffect(() => { + const stepFromURL = searchParams.get('step') + if (user && claimLinkData.status !== 'CLAIMED' && stepFromURL === 'claim' && isPeanutWallet) { + removeParamStep() + handleClaimLink(false, true) + } + }, [user, searchParams, isPeanutWallet]) + if (claimBankFlowStep) { return } @@ -780,9 +791,13 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => { modalPanelClassName="max-w-md mx-8" /> setShowVerificationModal(false)} + isOpen={showVerificationModal} + 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..4aeaf9b6c 100644 --- a/src/components/Claim/useClaimLink.tsx +++ b/src/components/Claim/useClaimLink.tsx @@ -11,11 +11,14 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { isTestnetChain } from '@/utils' import * as Sentry from '@sentry/nextjs' import { useAccount } from 'wagmi' +import { usePathname, useSearchParams } from 'next/navigation' const useClaimLink = () => { const { fetchBalance } = useWallet() const { chain: currentChain } = useAccount() const { switchChainAsync } = useSwitchChain() + const pathname = usePathname() + const searchParams = useSearchParams() const { setLoadingState } = useContext(loadingStateContext) @@ -93,10 +96,29 @@ const useClaimLink = () => { } } + const addParamStep = (step: 'bank' | 'claim') => { + const params = new URLSearchParams(searchParams) + params.set('step', step) + + const hash = window.location.hash + 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() + const newUrl = `${pathname}${queryString ? `?${queryString}` : ''}${window.location.hash}` + window.history.replaceState(null, '', newUrl) + } + return { claimLink, claimLinkXchain, switchNetwork, + addParamStep, + removeParamStep, } } diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index e02994fe8..e94fbff6b 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('bank') setShowVerificationModal(true) } else { if (savedAccounts.length) { @@ -136,7 +139,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin