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
11 changes: 9 additions & 2 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,32 @@
## 💻 Code Quality

- **Boy scout rule**: leave code better than you found it.
- **Use explicit imports** where possible
- **DRY** - do not repeat yourself. Reuse existing code and abstract shared functionality. Less code is better code.
- this also means to use shared consts (e.g. check src/constants)
- **Separate business logic from interface** - this is important for readability, debugging and testability.
- **Use explicit imports** where possible.
- **Reuse existing components and functions** - don't hardcode hacky solutions.
- **Warn about breaking changes** - when making changes, ensure you're not breaking existing functionality, and if there's a risk, explicitly WARN about it.
- **Mention refactor opportunities** - if you notice an opportunity to refactor or improve existing code, mention it. DO NOT make any changes you were not explicitly told to do. Only mention the potential change to the user.
- **Performance is important** - cache where possible, make sure to not make unnecessary re-renders or data fetching.
- **Separate business logic from interface** - this is important for readability, debugging and testability.
- **Flag breaking changes** - always flag if changes done in Frontend are breaking and require action on Backend (or viceversa)

## 🧪 Testing

- **Test new code** - where tests make sense, test new code. Especially with fast unit tests.
- **Tests live with code** - tests should live where the code they test is, not in a separate folder
- **Run tests**: `npm test` (fast, ~5s)

## 📁 Documentation

- **All docs go in `docs/`** (except root `README.md`)
- **Keep it concise** - docs should be kept quite concise. AI tends to make verbose logs. No one reads that, keep it short and informational.
- **Check existing docs** before creating new ones - merge instead of duplicate
- **Log significant changes** in `docs/CHANGELOG.md` following semantic versioning
- **Maintain PR.md for PRs** - When working on a PR, maintain `docs/PR.md` with:
1. Summary of changes
2. Risks (what might break)
3. QA guidelines (what to test)

## 🚀 Performance

Expand Down
60 changes: 42 additions & 18 deletions src/app/(mobile-ui)/claim/page.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,64 @@
import { getLinkDetails } from '@/app/actions/claimLinks'
import { Claim } from '@/components'
import { BASE_URL } from '@/constants'
import { BASE_URL, PEANUT_WALLET_TOKEN_DECIMALS, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants'
import { formatAmount, resolveAddressToUsername } from '@/utils'
import { type Metadata } from 'next'
import getOrigin from '@/lib/hosting/get-origin'
import { sendLinksApi } from '@/services/sendLinks'
import { formatUnits } from 'viem'

export const dynamic = 'force-dynamic'

async function getClaimLinkData(searchParams: { [key: string]: string | string[] | undefined }, siteUrl: string) {
if (!searchParams.i || !searchParams.c) return null

try {
const queryParams = new URLSearchParams()
Object.entries(searchParams).forEach(([key, val]) => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

did we need this? what about the other params? what do we need them for?

if (Array.isArray(val)) {
val.forEach((v) => queryParams.append(key, v))
} else if (val) {
queryParams.append(key, val)
}
})
// Use backend API with belt-and-suspenders logic (DB + blockchain fallback)
const contractVersion = (searchParams.v as string) || 'v4.3'

const url = `${siteUrl}/claim?${queryParams.toString()}`
const linkDetails = await getLinkDetails(url)
const sendLink = await sendLinksApi.getByParams({
chainId: searchParams.c as string,
depositIdx: searchParams.i as string,
contractVersion,
})
Comment on lines +18 to 22
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

Server bundle pulls in js-cookie via sendLinksApi; SSR hazard.

sendLinksApi imports js-cookie at module scope. Using it in a server file can break SSR (“document is not defined”) or bloat the server bundle.

Fix: fetch directly here (server-safe) or create a server-only wrapper module.

Example inline replacement:

import { PEANUT_API_URL } from '@/constants'
import { fetchWithSentry, jsonParse } from '@/utils'

const sendLink = await (async () => {
  const url = `${PEANUT_API_URL}/send-links?c=${searchParams.c}&v=${searchParams.v}&i=${searchParams.i}`
  const res = await fetchWithSentry(url, { method: 'GET' })
  if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
  return jsonParse(await res.text())
})()

Alternatively, add a new src/services/sendLinks.server.ts exporting only getByParams without js-cookie.

🤖 Prompt for AI Agents
In src/app/(mobile-ui)/claim/page.tsx around lines 16 to 20 the code calls
sendLinksApi.getByParams which imports js-cookie at module scope and therefore
pulls client-only code into the server bundle; replace this call with a
server-safe fetch or create a server-only wrapper. Either: inline a server-safe
fetch here that builds the PEANUT_API_URL send-links URL from searchParams, uses
the app's server-side fetchWithSentry/jsonParse utilities, checks res.ok and
throws on non-2xx before returning parsed JSON; or create
src/services/sendLinks.server.ts that exports getByParams implemented with
server-safe fetch (no js-cookie imported) and import that file here instead of
the current sendLinksApi.

// Backend always provides token details (from DB or blockchain fallback)
// Use fallback from consts if not available
const tokenDecimals = sendLink.tokenDecimals ?? PEANUT_WALLET_TOKEN_DECIMALS
const tokenSymbol = sendLink.tokenSymbol ?? PEANUT_WALLET_TOKEN_SYMBOL

// Transform to linkDetails format for metadata`
const linkDetails = {
senderAddress: sendLink.senderAddress,
tokenAmount: formatUnits(sendLink.amount, tokenDecimals),
tokenSymbol,
claimed: sendLink.status === 'CLAIMED' || sendLink.status === 'CANCELLED',
}

// Get username from sender address
const username = linkDetails?.senderAddress
? await resolveAddressToUsername(linkDetails.senderAddress, siteUrl)
: null
// Get username from sender - use sender.username if available (from backend)
let username: string | null = sendLink.sender?.username || null

// If no username in backend data, try ENS resolution with timeout
if (!username && linkDetails.senderAddress) {
try {
// ENS race condition - catch errors to prevent Promise.race from throwing
const timeoutPromise = new Promise<null>((resolve) => setTimeout(() => resolve(null), 3000))
const resolvePromise = resolveAddressToUsername(linkDetails.senderAddress, siteUrl).catch((err) => {
console.error('ENS resolution failed:', err)
return null
})
username = await Promise.race([resolvePromise, timeoutPromise])
} catch (ensError) {
console.error('ENS resolution failed:', ensError)
username = null
}
}

if (username) {
console.log('username', username)
console.log('Resolved username:', username)
}
Comment on lines 55 to 57
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 | 🟡 Minor

Avoid logging usernames in server logs.

console.log('Resolved username:', username) can leak PII in logs. Remove or guard behind a debug flag.

-        if (username) {
-            console.log('Resolved username:', username)
-        }
+        // optionally emit only in debug builds

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/(mobile-ui)/claim/page.tsx around lines 72 to 74, remove the
console.log that prints the resolved username to avoid leaking PII; either
delete the log entirely or wrap it behind a runtime debug check (e.g., only log
when an explicit DEBUG env flag is true) so production/server logs never contain
usernames, and ensure any remaining logging redacts or masks the username.


return { linkDetails, username }
} catch (e) {
console.log('error: ', e)
console.error('Error fetching claim link data:', e)
return null
}
}
Expand Down
24 changes: 18 additions & 6 deletions src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
dispatch(paymentActions.setView('INITIAL'))
}
} else {
setError(getErrorProps({ error, isUser: !!user }))
setError(getErrorProps({ error, isUser: !!user, recipient }))
}
}

Expand Down Expand Up @@ -587,14 +587,26 @@ const getDefaultError: (isUser: boolean) => ValidationErrorViewProps = (isUser)
redirectTo: isUser ? '/home' : '/setup',
})

function getErrorProps({ error, isUser }: { error: ParseUrlError; isUser: boolean }): ValidationErrorViewProps {
function getErrorProps({
error,
isUser,
recipient,
}: {
error: ParseUrlError
isUser: boolean
recipient: string[]
}): ValidationErrorViewProps {
const username = recipient[0] || 'unknown'
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 | 🟡 Minor

Add fallback for potential undefined recipient.

If the recipient array is empty, recipient[0] will be undefined, resulting in the error message displaying "We don't know any @undefined".

Consider this defensive adjustment:

-    const username = recipient[0] || 'unknown'
+    const username = (recipient && recipient[0]) || 'this user'

This provides a more natural fallback message if the recipient array is empty or undefined.

📝 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
const username = recipient[0] || 'unknown'
const username = (recipient && recipient[0]) || 'this user'
🤖 Prompt for AI Agents
In src/app/[...recipient]/client.tsx around line 599, the current read of
recipient[0] can throw or yield undefined when recipient is missing or empty;
change the assignment to defensively read the first element only if recipient
exists and has items (e.g., use optional chaining/nullish fallback or check
Array.isArray and length) and use a friendly default like "someone" instead of
allowing "@undefined" to appear in messages.


switch (error.message) {
case EParseUrlError.INVALID_RECIPIENT:
return {
title: 'Invalid Recipient',
message: 'The recipient you are trying to pay is invalid. Please check the URL and try again.',
buttonText: isUser ? 'Go to home' : 'Create your Peanut Wallet',
redirectTo: isUser ? '/home' : '/setup',
title: `We don't know any @${username}`,
message: 'Are you sure you clicked on the right link?',
buttonText: 'Go back to home',
redirectTo: '/home',
showLearnMore: false,
supportMessageTemplate: 'I clicked on this link but got an error: {url}',
}
case EParseUrlError.INVALID_CHAIN:
return {
Expand Down
4 changes: 3 additions & 1 deletion src/app/[...recipient]/payment-layout-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import GuestLoginModal from '@/components/Global/GuestLoginModal'
import SupportDrawer from '@/components/Global/SupportDrawer'
import TopNavbar from '@/components/Global/TopNavbar'
import WalletNavigation from '@/components/Global/WalletNavigation'
import { ThemeProvider } from '@/config'
Expand Down Expand Up @@ -64,8 +65,9 @@ export default function PaymentLayoutWrapper({ children }: { children: React.Rea
</div>
</div>

{/* Modal */}
{/* Modals */}
<GuestLoginModal />
<SupportDrawer />
</div>
)
}
28 changes: 0 additions & 28 deletions src/app/actions/claimLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,31 +82,3 @@ export async function getNextDepositIndex(contractVersion: string): Promise<numb
args: [],
})) as number
}

export async function claimSendLink(
pubKey: string,
recipient: string,
password: string,
waitForTx: boolean
): Promise<SendLink | { error: string }> {
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,
waitForTx,
}),
})
if (!response.ok) {
const body = await response.json()
if (!!body.error || !!body.message) {
return { error: body.message ?? body.error }
}
return { error: `HTTP error! status: ${response.status}` }
}
return jsonParse(await response.text()) as SendLink
}
55 changes: 0 additions & 55 deletions src/app/api/auto-claim/route.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/components/0_Bruddle/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ const Toast: React.FC<ToastMessage> = ({ type = 'info', message }) => {
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 80 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
className={twMerge(`border-2 px-6 py-1`, 'card shadow-4 min-w-fit', colors[type])}
className={twMerge(`border-2 px-6 py-1`, 'card shadow-4 min-w-fit max-w-[90vw] md:max-w-md', colors[type])}
>
<p className="text-sm font-bold">{message}</p>
<p className="break-words text-sm font-bold">{message}</p>
</motion.div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Claim/Claim.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export interface IClaimScreenProps {
setRecipient: (recipient: { name: string | undefined; address: string }) => void
tokenPrice: number
setTokenPrice: (price: number) => void
transactionHash: string
setTransactionHash: (hash: string) => void
transactionHash: string | undefined
setTransactionHash: (hash: string | undefined) => void
estimatedPoints: number
setEstimatedPoints: (points: number) => void
attachment: { message: string | undefined; attachmentUrl: string | undefined }
Expand Down
13 changes: 8 additions & 5 deletions src/components/Claim/Generic/NotFound.view.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use client'

import StatusViewWrapper from '@/components/Global/StatusViewWrapper'
import ValidationErrorView from '@/components/Payment/Views/Error.validation.view'

export const NotFoundClaimLink = () => {
return (
<StatusViewWrapper
title="Sorryyy"
description="This link is malformed. Are you sure you copied it correctly?"
supportCtaText="Deposit not found. Are you sure your link is correct?"
<ValidationErrorView
title="This link seems broken!"
message="Are you sure you clicked on the right link?"
buttonText="Go back to home"
redirectTo="/home"
showLearnMore={false}
supportMessageTemplate="I clicked on this link but got an error: {url}"
/>
)
}
12 changes: 8 additions & 4 deletions src/components/Claim/Generic/WrongPassword.view.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
'use client'

import StatusViewWrapper from '@/components/Global/StatusViewWrapper'
import ValidationErrorView from '@/components/Payment/Views/Error.validation.view'

export const WrongPasswordClaimLink = () => {
return (
<StatusViewWrapper
title="Sorryyy"
description="This link is malformed. Are you sure you copied it correctly?"
<ValidationErrorView
title="This link seems broken!"
message="Are you sure you clicked on the right link?"
buttonText="Go back to home"
redirectTo="/home"
showLearnMore={false}
supportMessageTemplate="I clicked on this link but got an error: {url}"
/>
)
}
Loading
Loading