Skip to content

Commit 6070d07

Browse files
authored
Merge pull request #1185 from peanutprotocol/feat/fix-claim-flow-redirect
[TASK-14061] Update claim flow redirect actions for guest users
2 parents 6203f02 + 101dccb commit 6070d07

14 files changed

Lines changed: 220 additions & 25 deletions

File tree

src/app/api/auto-claim/route.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { fetchWithSentry } from '@/utils'
3+
import { PEANUT_API_URL } from '@/constants'
4+
5+
/**
6+
* API route for automated link claiming without requiring user interaction.
7+
*
8+
* This route serves as a workaround for Next.js server action limitations:
9+
* Server actions cannot be directly called within useEffect hooks, which is
10+
* necessary for our automatic claim flow. By exposing this as an API route
11+
* instead, we can make the claim request safely from client-side effects.
12+
*/
13+
export async function POST(request: NextRequest) {
14+
try {
15+
const body = await request.json()
16+
const { pubKey, recipient, password } = body
17+
18+
if (!pubKey || !recipient || !password) {
19+
return NextResponse.json(
20+
{ error: 'Missing required parameters: pubKey, recipient, or password' },
21+
{ status: 400 }
22+
)
23+
}
24+
25+
const response = await fetchWithSentry(`${PEANUT_API_URL}/send-links/${pubKey}/claim`, {
26+
method: 'POST',
27+
headers: {
28+
'api-key': process.env.PEANUT_API_KEY!,
29+
'Content-Type': 'application/json',
30+
},
31+
body: JSON.stringify({
32+
recipient,
33+
password,
34+
}),
35+
})
36+
37+
if (!response.ok) {
38+
return NextResponse.json(
39+
{ error: `Failed to claim link: ${response.statusText}` },
40+
{ status: response.status }
41+
)
42+
}
43+
44+
const responseText = await response.text()
45+
console.log('response', responseText)
46+
return new NextResponse(responseText, {
47+
headers: {
48+
'Content-Type': 'application/json',
49+
},
50+
})
51+
} catch (error) {
52+
console.error('Error claiming send link:', error)
53+
return NextResponse.json({ error: 'Failed to claim send link' }, { status: 500 })
54+
}
55+
}

src/components/Claim/Claim.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import * as _consts from './Claim.consts'
2525
import FlowManager from './Link/FlowManager'
2626
import { type PeanutCrossChainRoute } from '@/services/swap'
2727
import { NotFoundClaimLink, WrongPasswordClaimLink, ClaimedView } from './Generic'
28+
import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext'
29+
import { useSearchParams } from 'next/navigation'
2830

2931
export const Claim = ({}) => {
3032
const [step, setStep] = useState<_consts.IClaimScreenState>(_consts.INIT_VIEW_STATE)
@@ -66,6 +68,9 @@ export const Claim = ({}) => {
6668
const senderId = claimLinkData?.sender.userId
6769
const { interactions } = useUserInteractions(senderId ? [senderId] : [])
6870

71+
const { setFlowStep: setClaimBankFlowStep } = useClaimBankFlow()
72+
const searchParams = useSearchParams()
73+
6974
const transactionForDrawer: TransactionDetails | null = useMemo(() => {
7075
if (!claimLinkData) return null
7176

@@ -254,6 +259,14 @@ export const Claim = ({}) => {
254259
}
255260
}, [linkState, transactionForDrawer])
256261

262+
// redirect to bank flow if user is KYC approved and step is bank
263+
useEffect(() => {
264+
const stepFromURL = searchParams.get('step')
265+
if (user?.user.bridgeKycStatus === 'approved' && stepFromURL === 'bank') {
266+
setClaimBankFlowStep(ClaimBankFlowStep.BankCountryList)
267+
}
268+
}, [user])
269+
257270
return (
258271
<PageContainer alignItems="center">
259272
{linkState === _consts.claimLinkStateType.LOADING && <PeanutLoading />}

src/components/Claim/Link/Initial.view.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {
103103
isXChain,
104104
setIsXChain,
105105
} = useContext(tokenSelectorContext)
106-
const { claimLink, claimLinkXchain } = useClaimLink()
106+
const { claimLink, claimLinkXchain, removeParamStep } = useClaimLink()
107107
const { isConnected: isPeanutWallet, address, fetchBalance } = useWallet()
108108
const router = useRouter()
109109
const { user } = useAuth()
@@ -157,7 +157,7 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {
157157
}, [recipientType, claimLinkData.chainId, isPeanutChain, claimLinkData.tokenAddress])
158158

159159
const handleClaimLink = useCallback(
160-
async (bypassModal = false) => {
160+
async (bypassModal = false, autoClaim = false) => {
161161
if (!isPeanutWallet && !bypassModal) {
162162
setShowConfirmationModal(true)
163163
return
@@ -175,8 +175,11 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {
175175
try {
176176
setLoadingState('Executing transaction')
177177
if (isPeanutWallet) {
178-
await sendLinksApi.claim(user?.user.username ?? address, claimLinkData.link)
179-
178+
if (autoClaim) {
179+
await sendLinksApi.autoClaimLink(user?.user.username ?? address, claimLinkData.link)
180+
} else {
181+
await sendLinksApi.claim(user?.user.username ?? address, claimLinkData.link)
182+
}
180183
setClaimType('claim')
181184
onCustom('SUCCESS')
182185
fetchBalance()
@@ -623,6 +626,14 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {
623626
}
624627
}
625628

629+
useEffect(() => {
630+
const stepFromURL = searchParams.get('step')
631+
if (user && claimLinkData.status !== 'CLAIMED' && stepFromURL === 'claim' && isPeanutWallet) {
632+
removeParamStep()
633+
handleClaimLink(false, true)
634+
}
635+
}, [user, searchParams, isPeanutWallet])
636+
626637
if (claimBankFlowStep) {
627638
return <BankFlowManager {...props} />
628639
}
@@ -780,9 +791,13 @@ export const InitialClaimLinkView = (props: IClaimScreenProps) => {
780791
modalPanelClassName="max-w-md mx-8"
781792
/>
782793
<GuestVerificationModal
794+
redirectToVerification
783795
secondaryCtaLabel="Claim with other method"
784-
isOpen={showVerificationModal && !user}
785-
onClose={() => setShowVerificationModal(false)}
796+
isOpen={showVerificationModal}
797+
onClose={() => {
798+
removeParamStep()
799+
setShowVerificationModal(false)
800+
}}
786801
description="The sender isn't verified, so please create an account and verify your identity to have the funds deposited to your bank."
787802
/>
788803
</div>

src/components/Claim/useClaimLink.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import { useWallet } from '@/hooks/wallet/useWallet'
1111
import { isTestnetChain } from '@/utils'
1212
import * as Sentry from '@sentry/nextjs'
1313
import { useAccount } from 'wagmi'
14+
import { usePathname, useSearchParams } from 'next/navigation'
1415

1516
const useClaimLink = () => {
1617
const { fetchBalance } = useWallet()
1718
const { chain: currentChain } = useAccount()
1819
const { switchChainAsync } = useSwitchChain()
20+
const pathname = usePathname()
21+
const searchParams = useSearchParams()
1922

2023
const { setLoadingState } = useContext(loadingStateContext)
2124

@@ -93,10 +96,29 @@ const useClaimLink = () => {
9396
}
9497
}
9598

99+
const addParamStep = (step: 'bank' | 'claim') => {
100+
const params = new URLSearchParams(searchParams)
101+
params.set('step', step)
102+
103+
const hash = window.location.hash
104+
const newUrl = `${pathname}?${params.toString()}${hash}`
105+
window.history.replaceState(null, '', newUrl)
106+
}
107+
108+
const removeParamStep = () => {
109+
const params = new URLSearchParams(searchParams)
110+
params.delete('step')
111+
const queryString = params.toString()
112+
const newUrl = `${pathname}${queryString ? `?${queryString}` : ''}${window.location.hash}`
113+
window.history.replaceState(null, '', newUrl)
114+
}
115+
96116
return {
97117
claimLink,
98118
claimLinkXchain,
99119
switchNetwork,
120+
addParamStep,
121+
removeParamStep,
100122
}
101123
}
102124

src/components/Common/ActionList.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Button } from '../0_Bruddle'
1313
import { PEANUT_LOGO_BLACK } from '@/assets/illustrations'
1414
import Image from 'next/image'
1515
import { saveRedirectUrl } from '@/utils'
16-
import { useRouter } from 'next/navigation'
16+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
1717
import { PEANUTMAN_LOGO } from '@/assets/peanut'
1818
import { BankClaimType, useDetermineBankClaimType } from '@/hooks/useDetermineBankClaimType'
1919
import useSavedAccounts from '@/hooks/useSavedAccounts'
@@ -24,6 +24,7 @@ import { BankRequestType, useDetermineBankRequestType } from '@/hooks/useDetermi
2424
import { GuestVerificationModal } from '../Global/GuestVerificationModal'
2525
import ActionListDaimoPayButton from './ActionListDaimoPayButton'
2626
import { ACTION_METHODS, PaymentMethod } from '@/constants/actionlist.consts'
27+
import useClaimLink from '../Claim/useClaimLink'
2728

2829
interface IActionListProps {
2930
flow: 'claim' | 'request'
@@ -50,6 +51,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
5051
const { requestType } = useDetermineBankRequestType(requesterUserId)
5152
const savedAccounts = useSavedAccounts()
5253
const { usdAmount } = usePaymentStore()
54+
const { addParamStep } = useClaimLink()
5355
const {
5456
setShowRequestFulfilmentBankFlowManager,
5557
setShowExternalWalletFulfilMethods,
@@ -68,6 +70,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
6870
case 'bank':
6971
{
7072
if (claimType === BankClaimType.GuestKycNeeded) {
73+
addParamStep('bank')
7174
setShowVerificationModal(true)
7275
} else {
7376
if (savedAccounts.length) {
@@ -136,7 +139,7 @@ export default function ActionList({ claimLinkData, isLoggedIn, flow, requestLin
136139
<Button
137140
shadowSize="4"
138141
onClick={() => {
139-
saveRedirectUrl()
142+
addParamStep('claim')
140143
// push to setup page with redirect uri, to prevent the user from losing the flow context
141144
const redirectUri = encodeURIComponent(
142145
window.location.pathname + window.location.search + window.location.hash

src/components/Global/GuestLoginCta/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { Button } from '@/components/0_Bruddle'
22
import Divider from '@/components/0_Bruddle/Divider'
33
import { useToast } from '@/components/0_Bruddle/Toast'
44
import { useZeroDev } from '@/hooks/useZeroDev'
5-
import { saveRedirectUrl } from '@/utils'
5+
import { sanitizeRedirectURL, saveRedirectUrl } from '@/utils'
66
import { useAppKit } from '@reown/appkit/react'
77
import * as Sentry from '@sentry/nextjs'
8-
import { useRouter } from 'next/navigation'
8+
import { useRouter, useSearchParams } from 'next/navigation'
99
import { useEffect } from 'react'
1010

1111
interface GuestLoginCtaProps {
@@ -18,6 +18,7 @@ const GuestLoginCta = ({ hideConnectWallet = false, view }: GuestLoginCtaProps)
1818
const toast = useToast()
1919
const router = useRouter()
2020
const { open: openReownModal } = useAppKit()
21+
const searchParams = useSearchParams()
2122

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

4041
try {
4142
await handleLogin()
43+
const redirect_uri = searchParams.get('redirect_uri')
44+
if (redirect_uri) {
45+
const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri)
46+
router.push(sanitizedRedirectUrl)
47+
}
4248
} catch (e) {
4349
toast.error('Error logging in')
4450
Sentry.captureException(e)

src/components/Global/GuestVerificationModal/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ interface GuestVerificationModalProps {
99
isOpen: boolean
1010
onClose: () => void
1111
secondaryCtaLabel: string
12+
redirectToVerification?: boolean
1213
}
1314

1415
export const GuestVerificationModal = ({
1516
isOpen,
1617
onClose,
1718
description,
1819
secondaryCtaLabel,
20+
redirectToVerification,
1921
}: GuestVerificationModalProps) => {
2022
const router = useRouter()
2123
return (
@@ -34,6 +36,10 @@ export const GuestVerificationModal = ({
3436
className: 'md:py-2.5',
3537
onClick: () => {
3638
saveRedirectUrl()
39+
if (redirectToVerification) {
40+
router.push('/setup?redirect_uri=/profile/identity-verification')
41+
return
42+
}
3743
router.push('/setup')
3844
},
3945
},

src/components/Global/PostSignupActionManager/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'
66
import ActionModal from '../ActionModal'
77
import { POST_SIGNUP_ACTIONS } from './post-signup-action.consts'
88
import { IconName } from '../Icons/Icon'
9+
import { useAuth } from '@/context/authContext'
910

1011
export const PostSignupActionManager = ({
1112
onActionModalVisibilityChange,
@@ -21,12 +22,12 @@ export const PostSignupActionManager = ({
2122
action: () => void
2223
} | null>(null)
2324
const router = useRouter()
25+
const { user } = useAuth()
2426

25-
useEffect(() => {
27+
const checkClaimModalAfterKYC = () => {
2628
const redirectUrl = getFromLocalStorage('redirect')
27-
if (redirectUrl) {
29+
if (user?.user.bridgeKycStatus === 'approved' && redirectUrl) {
2830
const matchedAction = POST_SIGNUP_ACTIONS.find((action) => action.pathPattern.test(redirectUrl))
29-
3031
if (matchedAction) {
3132
setActionConfig({
3233
...matchedAction.config,
@@ -39,7 +40,11 @@ export const PostSignupActionManager = ({
3940
setShowModal(true)
4041
}
4142
}
42-
}, [router])
43+
}
44+
45+
useEffect(() => {
46+
checkClaimModalAfterKYC()
47+
}, [router, user])
4348

4449
useEffect(() => {
4550
onActionModalVisibilityChange(showModal)

src/components/Global/PostSignupActionManager/post-signup-action.consts.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ export const POST_SIGNUP_ACTIONS = [
55
// this regex will match any path that contains the word "claim", this helps in determing if the user is coming from a claim link
66
pathPattern: /claim/,
77
config: {
8-
title: 'Claim your money',
9-
description: `You're almost done! Tap Claim Funds to move the money into your new Peanut Wallet.`,
10-
cta: 'Claim Funds',
11-
icon: 'dollar' as IconName,
8+
title: 'Verification complete!',
9+
description: `Your identity has been successfully verified. You can now claim money directly to your bank account.`,
10+
cta: 'Claim to bank',
11+
icon: 'check' as IconName,
1212
},
1313
},
1414
]

src/components/Profile/views/IdentityVerification.view.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { KycVerificationInProgressModal } from '@/components/Kyc/KycVerification
99
import { useAuth } from '@/context/authContext'
1010
import { useKycFlow } from '@/hooks/useKycFlow'
1111
import { useRouter } from 'next/navigation'
12-
import React, { useMemo, useRef, useState } from 'react'
12+
import React, { useEffect, useMemo, useRef, useState } from 'react'
1313

1414
const IdentityVerificationView = () => {
1515
const { user, fetchUser } = useAuth()
@@ -65,6 +65,13 @@ const IdentityVerificationView = () => {
6565
return {}
6666
}
6767

68+
// if kyc is already approved, redirect to profile
69+
useEffect(() => {
70+
if (user?.user.bridgeKycStatus === 'approved') {
71+
router.replace('/profile')
72+
}
73+
}, [user])
74+
6875
return (
6976
<div className="flex min-h-[inherit] flex-col">
7077
<NavHeader title="Identity Verification" onPrev={() => router.replace('/profile')} />

0 commit comments

Comments
 (0)