Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/app/api/auto-claim/route.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
}
13 changes: 13 additions & 0 deletions src/components/Claim/Claim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 (
<PageContainer alignItems="center">
{linkState === _consts.claimLinkStateType.LOADING && <PeanutLoading />}
Expand Down
27 changes: 21 additions & 6 deletions src/components/Claim/Link/Initial.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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 <BankFlowManager {...props} />
}
Expand Down Expand Up @@ -780,9 +791,13 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {
modalPanelClassName="max-w-md mx-8"
/>
<GuestVerificationModal
redirectToVerification
secondaryCtaLabel="Claim with other method"
isOpen={showVerificationModal && !user}
onClose={() => 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."
/>
</div>
Expand Down
22 changes: 22 additions & 0 deletions src/components/Claim/useClaimLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/components/Common/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -136,7 +139,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
<Button
shadowSize="4"
onClick={() => {
saveRedirectUrl()
addParamStep('claim')
// push to setup page with redirect uri, to prevent the user from losing the flow context
const redirectUri = encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash
Expand Down
10 changes: 8 additions & 2 deletions src/components/Global/GuestLoginCta/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ 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 } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'

interface GuestLoginCtaProps {
Expand All @@ -18,6 +18,7 @@ const GuestLoginCta = ({ hideConnectWallet = false, view }: GuestLoginCtaProps)
const toast = useToast()
const router = useRouter()
const { open: openReownModal } = useAppKit()
const searchParams = useSearchParams()

// If user already has a passkey address, auto-redirect to avoid double prompting
useEffect(() => {
Expand All @@ -39,6 +40,11 @@ const GuestLoginCta = ({ hideConnectWallet = false, view }: GuestLoginCtaProps)

try {
await handleLogin()
const redirect_uri = searchParams.get('redirect_uri')
if (redirect_uri) {
const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri)
router.push(sanitizedRedirectUrl)
}
} catch (e) {
toast.error('Error logging in')
Sentry.captureException(e)
Expand Down
6 changes: 6 additions & 0 deletions src/components/Global/GuestVerificationModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ interface GuestVerificationModalProps {
isOpen: boolean
onClose: () => void
secondaryCtaLabel: string
redirectToVerification?: boolean
}

export const GuestVerificationModal = ({
isOpen,
onClose,
description,
secondaryCtaLabel,
redirectToVerification,
}: GuestVerificationModalProps) => {
const router = useRouter()
return (
Expand All @@ -34,6 +36,10 @@ export const GuestVerificationModal = ({
className: 'md:py-2.5',
onClick: () => {
saveRedirectUrl()
if (redirectToVerification) {
router.push('/setup?redirect_uri=/profile/identity-verification')
return
}
router.push('/setup')
},
},
Expand Down
13 changes: 9 additions & 4 deletions src/components/Global/PostSignupActionManager/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { useAuth } from '@/context/authContext'

export const PostSignupActionManager = ({
onActionModalVisibilityChange,
Expand All @@ -21,12 +22,12 @@ export const PostSignupActionManager = ({
action: () => void
} | null>(null)
const router = useRouter()
const { user } = useAuth()

useEffect(() => {
const checkClaimModalAfterKYC = () => {
const redirectUrl = getFromLocalStorage('redirect')
if (redirectUrl) {
if (user?.user.bridgeKycStatus === 'approved' && redirectUrl) {
const matchedAction = POST_SIGNUP_ACTIONS.find((action) => action.pathPattern.test(redirectUrl))

if (matchedAction) {
setActionConfig({
...matchedAction.config,
Expand All @@ -39,7 +40,11 @@ export const PostSignupActionManager = ({
setShowModal(true)
}
}
}, [router])
}

useEffect(() => {
checkClaimModalAfterKYC()
}, [router, user])

useEffect(() => {
onActionModalVisibilityChange(showModal)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ 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',
icon: 'dollar' as IconName,
title: 'Verification complete!',
description: `Your identity has been successfully verified. You can now claim money directly to your bank account.`,
cta: 'Claim to bank',
icon: 'check' as IconName,
},
},
]
9 changes: 8 additions & 1 deletion src/components/Profile/views/IdentityVerification.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -65,6 +65,13 @@ const IdentityVerificationView = () => {
return {}
}

// if kyc is already approved, redirect to profile
useEffect(() => {
if (user?.user.bridgeKycStatus === 'approved') {
router.replace('/profile')
}
}, [user])

return (
<div className="flex min-h-[inherit] flex-col">
<NavHeader title="Identity Verification" onPrev={() => router.replace('/profile')} />
Expand Down
Loading
Loading