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
70 changes: 59 additions & 11 deletions src/components/Claim/Claim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { generateKeysFromString } from '@squirrel-labs/peanut-sdk'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'

import { fetchTokenDetails, fetchTokenPrice } from '@/app/actions/tokens'
import { Button } from '@/components/0_Bruddle'
import { type StatusType } from '@/components/Global/Badges/StatusBadge'
import { TransactionDetailsReceipt } from '@/components/TransactionDetails/TransactionDetailsReceipt'
import { type TransactionDetails, REWARD_TOKENS } from '@/components/TransactionDetails/transactionTransformer'
Expand All @@ -25,8 +26,7 @@ import PeanutLoading from '../Global/PeanutLoading'
import * as _consts from './Claim.consts'
import FlowManager from './Link/FlowManager'
import { type PeanutCrossChainRoute } from '@/services/swap'
import { NotFoundClaimLink, WrongPasswordClaimLink, ClaimedView } from './Generic'
import SupportCTA from '../Global/SupportCTA'
import { ClaimedView, ClaimErrorView } from './Generic'
import { twMerge } from 'tailwind-merge'
import { ClaimBankFlowStep, useClaimBankFlow } from '@/context/ClaimBankFlowContext'
import { useSearchParams } from 'next/navigation'
Expand Down Expand Up @@ -80,6 +80,8 @@ export const Claim = ({}) => {
data: sendLink,
isLoading: isSendLinkLoading,
error: sendLinkError,
refetch, // Get refetch function for manual retry
failureCount, // Track retry attempts for better UX
} = useQuery({
queryKey: ['sendLink', linkUrl],
queryFn: () => sendLinksApi.get(linkUrl),
Expand All @@ -88,6 +90,10 @@ export const Claim = ({}) => {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // Exponential: 1s, 2s, 4s, 8s (total ~15s)
staleTime: 0, // Don't cache (one-time use per link)
gcTime: 0, // Garbage collect immediately after use
// Refetch when window regains focus (helps with "close and reopen" scenario)
refetchOnWindowFocus: true,
// Refetch on mount (helps with navigation back scenarios)
refetchOnMount: true,
})

const transactionForDrawer: TransactionDetails | null = useMemo(() => {
Expand Down Expand Up @@ -286,14 +292,23 @@ export const Claim = ({}) => {
}
}, [user, isFetchingUser, claimLinkData])

// Handle sendLink fetch errors
// Handle sendLink fetch errors with better UX
useEffect(() => {
if (sendLinkError) {
console.error('Failed to load link:', sendLinkError)
setLinkState(_consts.claimLinkStateType.NOT_FOUND)
Sentry.captureException(sendLinkError)

// Don't immediately show NOT_FOUND - give user option to retry
// Link might have just been created
if (failureCount >= 4) {
// After all retries exhausted, show error with retry button
setLinkState(_consts.claimLinkStateType.NOT_FOUND)
} else {
// Still retrying, keep showing loading
setLinkState(_consts.claimLinkStateType.LOADING)
}
}
}, [sendLinkError])
}, [sendLinkError, failureCount])

useEffect(() => {
if (address) {
Expand Down Expand Up @@ -331,7 +346,23 @@ export const Claim = ({}) => {
alignItems="center"
className={twMerge('flex flex-col', !user && !isFetchingUser && 'min-h-[calc(100dvh-110px)]')}
>
{linkState === _consts.claimLinkStateType.LOADING && <PeanutLoading />}
{linkState === _consts.claimLinkStateType.LOADING && (
<div className="flex flex-col items-center gap-4 px-4">
<PeanutLoading />
{isSendLinkLoading && failureCount > 0 && (
<p className="text-center text-sm text-gray-600">
{failureCount < 3
? 'Loading your link...'
: 'This is taking longer than usual. The link might have just been created.'}
</p>
)}
{isSendLinkLoading && failureCount >= 3 && (
<p className="text-center text-xs text-gray-500">
We're still trying... (attempt {failureCount + 1}/5)
</p>
)}
</div>
)}
{linkState === _consts.claimLinkStateType.CLAIM && (
<FlowManager
recipientType={recipientType}
Expand Down Expand Up @@ -372,8 +403,28 @@ export const Claim = ({}) => {
}
/>
)}
{linkState === _consts.claimLinkStateType.WRONG_PASSWORD && <WrongPasswordClaimLink />}
{linkState === _consts.claimLinkStateType.NOT_FOUND && <NotFoundClaimLink />}
{linkState === _consts.claimLinkStateType.WRONG_PASSWORD && (
<ClaimErrorView
title="Wrong password!"
message="Are you sure you clicked on the right link?"
primaryButtonText="Try Again"
onPrimaryClick={() => {
setLinkState(_consts.claimLinkStateType.LOADING)
refetch()
}}
/>
)}
{linkState === _consts.claimLinkStateType.NOT_FOUND && (
<ClaimErrorView
title="This link seems broken!"
message="Are you sure you clicked on the right link? Was this link just created? Try again in a few seconds."
primaryButtonText="Retry Loading Link"
onPrimaryClick={() => {
setLinkState(_consts.claimLinkStateType.LOADING)
refetch()
}}
/>
)}
{/* Show this state only to guest users and receivers, never to the link creator */}
{linkState === _consts.claimLinkStateType.ALREADY_CLAIMED &&
selectedTransaction &&
Expand All @@ -389,9 +440,6 @@ export const Claim = ({}) => {
onClose={() => setLinkUrl(window.location.href)}
/>
)}

{/* Show only to guest users */}
{linkState !== _consts.claimLinkStateType.LOADING && !user && !isFetchingUser && <SupportCTA />}
</PageContainer>
)
}
47 changes: 47 additions & 0 deletions src/components/Claim/Generic/ClaimError.view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import { Button } from '@/components/0_Bruddle'
import { useSupportModalContext } from '@/context/SupportModalContext'
import Image from 'next/image'
import Link from 'next/link'
import PEANUTMAN_CRY from '@/animations/GIF_ALPHA_BACKGORUND/512X512_ALPHA_GIF_konradurban_05.gif'

type ClaimErrorViewProps = {
title: string
message: string
primaryButtonText: string
onPrimaryClick: () => void
}

export const ClaimErrorView = ({ title, message, primaryButtonText, onPrimaryClick }: ClaimErrorViewProps) => {
const { openSupportWithMessage } = useSupportModalContext()

return (
<div className="flex flex-col items-center justify-center space-y-4 rounded-lg text-center">
<Image src={PEANUTMAN_CRY.src} alt="Sad peanut 😢" width={96} height={96} />
<div className="space-y-2">
<h1 className="text-lg font-semibold">{title}</h1>
<p className="text-sm font-normal md:max-w-xs">{message}</p>
</div>
<div className="flex w-full flex-col gap-2">
<Button onClick={onPrimaryClick} size="medium" shadowSize="4" variant="purple" className="w-full">
{primaryButtonText}
</Button>
<Button
onClick={() => {
openSupportWithMessage(`I clicked on this link but got an error: ${window.location.href}`)
}}
Comment on lines +30 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard window access for SSR safety.

Direct access to window.location.href can throw during server-side rendering or initial hydration in Next.js, even in client components.

Apply this diff to safely access the window object:

                <Button
                    onClick={() => {
-                       openSupportWithMessage(`I clicked on this link but got an error: ${window.location.href}`)
+                       openSupportWithMessage(`I clicked on this link but got an error: ${typeof window !== 'undefined' ? window.location.href : ''}`)
                    }}
                    size="medium"
                    shadowSize="4"
                    variant="stroke"
                    className="w-full"
                >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
onClick={() => {
openSupportWithMessage(`I clicked on this link but got an error: ${window.location.href}`)
}}
<Button
onClick={() => {
openSupportWithMessage(`I clicked on this link but got an error: ${typeof window !== 'undefined' ? window.location.href : ''}`)
}}
size="medium"
shadowSize="4"
variant="stroke"
className="w-full"
>
🤖 Prompt for AI Agents
In src/components/Claim/Generic/ClaimError.view.tsx around lines 30 to 33, the
onClick handler directly reads window.location.href which can crash during
SSR/hydration; change it to use a safely-guarded value (e.g. const currentHref =
typeof window !== 'undefined' ? window.location.href : '' or compute it inside a
useEffect/handler after checking typeof window) and pass that safe currentHref
into openSupportWithMessage so the component never reads window on the server.

size="medium"
shadowSize="4"
variant="stroke"
className="w-full"
>
Talk to support
</Button>
<Link href="/home" className="mt-2 cursor-pointer text-sm text-grey-1 underline underline-offset-2">
Go back to home
</Link>
</div>
</div>
)
}
3 changes: 1 addition & 2 deletions src/components/Claim/Generic/NotFound.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ export const NotFoundClaimLink = () => {
return (
<ValidationErrorView
title="This link seems broken!"
message="Are you sure you clicked on the right link?"
message="Are you sure you clicked on the right link? Was this link just created? Try again in a few seconds."
buttonText="Go back to home"
redirectTo="/home"
showLearnMore={false}
supportMessageTemplate="I clicked on this link but got an error: {url}"
/>
)
}
1 change: 1 addition & 0 deletions src/components/Claim/Generic/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './NotFound.view'
export * from './WrongPassword.view'
export * from './Claimed.view'
export * from './ClaimError.view'
4 changes: 3 additions & 1 deletion src/components/Payment/Views/Error.validation.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ValidationErrorViewProps = {
redirectTo: string
showLearnMore?: boolean
supportMessageTemplate?: string
supportButtonText?: string
}

function ValidationErrorView({
Expand All @@ -24,6 +25,7 @@ function ValidationErrorView({
redirectTo,
showLearnMore = true,
supportMessageTemplate,
supportButtonText = 'Talk to support',
}: ValidationErrorViewProps) {
const router = useRouter()
const { openSupportWithMessage } = useSupportModalContext()
Expand Down Expand Up @@ -74,7 +76,7 @@ function ValidationErrorView({
variant="stroke"
className="w-full"
>
Talk to support
{supportButtonText}
</Button>
)}
</div>
Expand Down
Loading