Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR introduces a complete QR code claiming flow with new pages and hooks, establishes centralized routing constants with reserved-route guards, enhances DirectSendQR with path normalization and redirect logic, and updates various UI components and configurations including modal styling, share functionality, and SDK version pinning. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
Comment |
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: true | ||
| - uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 9 | ||
| - name: Install Vercel CLI | ||
| run: pnpm add --global vercel@latest | ||
| - name: Link to Project | ||
| run: vercel link --yes --project=peanut-wallet --token=${{ secrets.VERCEL_TOKEN }} | ||
| - name: Pull Vercel Environment Information | ||
| run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} | ||
| - name: Build Project Artifacts | ||
| run: vercel build --target=preview --token=${{ secrets.VERCEL_TOKEN }} | ||
| - name: Deploy Project Artifacts to Vercel | ||
| run: vercel deploy --prebuilt --target=preview --token=${{ secrets.VERCEL_TOKEN }} |
Check warning
Code scanning / CodeQL
Workflow does not contain permissions Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
The best way to fix this problem is to explicitly set the GITHUB_TOKEN permissions for the workflow or for the job. Based on the steps involved—checking out code and running build/publish via Vercel—the required minimal permission is likely contents: read. You should add a permissions: block at the top level of the workflow (before jobs:) to cover all jobs, or at the job level if only certain jobs require those permissions. In this case, placing it at the root level is cleanest. The block should look exactly like:
permissions:
contents: readThis change is to be added above the jobs: section in .github/workflows/preview.yaml.
| @@ -5,6 +5,8 @@ | ||
| - peanut-wallet | ||
| - peanut-wallet-dev | ||
|
|
||
| permissions: | ||
| contents: read | ||
| jobs: | ||
| Deploy-Preview: | ||
| runs-on: ubuntu-latest |
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: true | ||
| - uses: pnpm/action-setup@v4 |
Check warning
Code scanning / CodeQL
Unpinned tag for a non-immutable Action in workflow Medium
|
|
||
| const vapidKeys = webpush.generateVAPIDKeys() | ||
|
|
||
| console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=' + vapidKeys.publicKey) |
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix the issue, we should prevent the script from logging (outputting) the VAPID private key in clear text. The line logging the public key is acceptable since public keys are not sensitive. The best approach is to remove or comment out the line that logs the private key, possibly replacing it with a message instructing the user to handle the key securely. If output of the private key is required for configuration, consider writing it to a protected file with appropriate access permissions, not to the console.
Changes needed:
- In
scripts/generate.mjs, remove (or comment out) the line logging the private key to the console (console.log('VAPID_PRIVATE_KEY=' + vapidKeys.privateKey)). - Optionally, add a comment or log message reminding users to save the private key securely.
| @@ -4,4 +4,3 @@ | ||
| const vapidKeys = webpush.generateVAPIDKeys() | ||
|
|
||
| console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=' + vapidKeys.publicKey) | ||
| console.log('VAPID_PRIVATE_KEY=' + vapidKeys.privateKey) |
| const vapidKeys = webpush.generateVAPIDKeys() | ||
|
|
||
| console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=' + vapidKeys.publicKey) | ||
| console.log('VAPID_PRIVATE_KEY=' + vapidKeys.privateKey) |
Check failure
Code scanning / CodeQL
Clear-text logging of sensitive information High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix this vulnerability, avoid printing the VAPID private key directly to the console. Instead, guide the user to securely store these values, such as by writing the keys to a local .env file (with clear file permission instructions) or another secure method suited for secret distribution. For this fix, the script will write both the public and private keys to a .env file (or another suitable filename) and inform the user (via the console) that the secrets have been written, without displaying the cleartext private key. This removes risks from console output while preserving usability.
Specific steps:
- Add the
fsmodule import for file writing. - Write both environment variable assignments (
NEXT_PUBLIC_VAPID_PUBLIC_KEY=...andVAPID_PRIVATE_KEY=...) to a.env.generatedfile (to avoid overwriting the user's main.envfile). - Remove the logging of the private key to the console.
- Add a
console.logstatement to inform the user where the secrets have been written, without showing the key itself.
| @@ -1,7 +1,14 @@ | ||
| // Generates VAPID key pair for web push notifications and outputs them as environment variables | ||
| import webpush from 'web-push' | ||
| import fs from 'fs' | ||
|
|
||
| const vapidKeys = webpush.generateVAPIDKeys() | ||
|
|
||
| console.log('NEXT_PUBLIC_VAPID_PUBLIC_KEY=' + vapidKeys.publicKey) | ||
| console.log('VAPID_PRIVATE_KEY=' + vapidKeys.privateKey) | ||
| // Write environment variables to a .env.generated file (do NOT log sensitive data to console) | ||
| fs.writeFileSync( | ||
| '.env.generated', | ||
| `NEXT_PUBLIC_VAPID_PUBLIC_KEY=${vapidKeys.publicKey}\nVAPID_PRIVATE_KEY=${vapidKeys.privateKey}\n`, | ||
| { encoding: 'utf-8', flag: 'w', mode: 0o600 } | ||
| ); | ||
|
|
||
| console.log('VAPID keys generated and written to .env.generated. Secure this file and add variables to your environment as needed.'); |
|
|
||
| return data | ||
| } catch (error) { | ||
| console.error(`Error creating external account for ${customerId}:`, error) |
Check failure
Code scanning / CodeQL
Use of externally-controlled format string High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
The best fix is to avoid embedding the external value customerId directly within the template literal for use as the primary message in console.error. Instead, pass a constant format string and provide the potentially untrusted value as a separate argument. This prevents any possibility of confusion caused by malicious or malformed values in customerId from impacting the structure of the log message.
Specifically, in src/app/actions/external-accounts.ts, on line 37, change
console.error(`Error creating external account for ${customerId}:`, error)to
console.error('Error creating external account for %s:', customerId, error)No additional imports or method definitions are required.
| @@ -34,7 +34,7 @@ | ||
|
|
||
| return data | ||
| } catch (error) { | ||
| console.error(`Error creating external account for ${customerId}:`, error) | ||
| console.error('Error creating external account for %s:', customerId, error) | ||
| if (error instanceof Error) { | ||
| return { error: error.message } | ||
| } |
|
|
||
| return data | ||
| } catch (error) { | ||
| console.error(`Error creating external account for ${customerId}:`, error) |
Check warning
Code scanning / CodeQL
Log injection Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix the problem, we should sanitize customerId before logging it. Specifically, remove any newline characters (\n and \r) that could be used to forge new log entries. We should also clearly delimit user input in the log entry, so it cannot be confused with the log format itself.
Changes:
- Line 37 needs to change so that
customerIdis sanitized before being included in the log message. - This can be done by using
customerId.replace(/\r?\n/g, "")(or/\n|\r/g), inline within the log statement or assigned to a variable for clarity. - No new dependencies need to be imported for this fix; JS string methods suffice.
| @@ -34,7 +34,8 @@ | ||
|
|
||
| return data | ||
| } catch (error) { | ||
| console.error(`Error creating external account for ${customerId}:`, error) | ||
| const safeCustomerId = customerId.replace(/[\r\n]/g, ""); | ||
| console.error(`Error creating external account for [${safeCustomerId}]:`, error); | ||
| if (error instanceof Error) { | ||
| return { error: error.message } | ||
| } |
|
|
||
| return data | ||
| } catch (error) { | ||
| console.error(`Error creating external account for ${customerId}:`, error) |
Check warning
Code scanning / CodeQL
Log injection Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix this issue, sanitize the customerId prior to logging it. For plain text logs, this means removing line breaks and potentially other control characters. The best way is to preprocess customerId so that it cannot introduce new log entries or break the log format. Specifically, in createBridgeExternalAccountForGuest, before logging, replace any \n or \r characters with an empty string (using .replace(/\r?\n/g, '')). Optionally, clarify in the log entry that customerId is user-controlled, but the essential fix is the sanitization.
You'll need to update line 37 in src/app/actions/external-accounts.ts to sanitize customerId before logging. No extra imports are needed for this basic replacement.
| @@ -34,7 +34,8 @@ | ||
|
|
||
| return data | ||
| } catch (error) { | ||
| console.error(`Error creating external account for ${customerId}:`, error) | ||
| const sanitizedCustomerId = customerId.replace(/\r?\n/g, ""); | ||
| console.error(`Error creating external account for customerId="${sanitizedCustomerId}":`, error) | ||
| if (error instanceof Error) { | ||
| return { error: error.message } | ||
| } |
| clearInterval(pollInterval) | ||
| // track completed event from iframe and close the modal | ||
| useEffect(() => { | ||
| const handleMessage = (event: MessageEvent) => { |
Check warning
Code scanning / CodeQL
Missing origin verification in `postMessage` handler Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
The best way to fix this problem is to verify the event.origin property in the handleMessage function against a whitelist of expected origins. Let the developer specify the trusted domain(s) (e.g., only accept messages from the same origin as the iframe src, or a hardcoded domain string). Since the iframe's origin is configurable via the src prop, a good, maintainable fix is to extract the origin from the src whenever possible and compare event.origin against this extracted origin (for robustness and flexibility).
Specifically, in src/components/Global/IframeWrapper/index.tsx, within the handleMessage function in the useEffect at lines 94-108, add code to determine the trusted origin and then check if event.origin matches before processing sensitive logic. Additionally, ensure robust parsing of the src and fallback as needed. Optionally, add a console warning for unexpected origins.
No external library is required—URL is a globally available API in all modern browsers and Node.js.
| @@ -91,7 +91,21 @@ | ||
|
|
||
| // track completed event from iframe and close the modal | ||
| useEffect(() => { | ||
| // Determine trusted origin from iframe src, fallback to empty string | ||
| let trustedOrigin = ''; | ||
| try { | ||
| // src may be relative or absolute. Handle both cases. | ||
| const url = new URL(src, window.location.origin); | ||
| trustedOrigin = url.origin; | ||
| } catch (err) { | ||
| // If src is invalid, trustedOrigin remains empty. | ||
| } | ||
| const handleMessage = (event: MessageEvent) => { | ||
| if (event.origin !== trustedOrigin) { | ||
| // Optionally: warn about rejected messages or just silently ignore | ||
| // console.warn(`Blocked postMessage from untrusted origin: ${event.origin}`); | ||
| return; | ||
| } | ||
| const data = event.data | ||
| if (data?.name === 'complete' && data?.metadata?.status === 'completed') { | ||
| onClose('completed') | ||
| @@ -105,7 +118,7 @@ | ||
|
|
||
| window.addEventListener('message', handleMessage) | ||
| return () => window.removeEventListener('message', handleMessage) | ||
| }, [onClose]) | ||
| }, [onClose, src]) | ||
|
|
||
| return ( | ||
| <Modal |
src/components/TransactionDetails/TransactionDetailsReceipt.tsx
Dismissed
Show dismissed
Hide dismissed
| let fromAmount: string | ||
| let response: SquidRouteResponse | ||
|
|
||
| console.info('getRoute', { from, to }, amount) |
Check warning
Code scanning / CodeQL
Log injection Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix this log injection issue, sanitize all user-input values before logging them. The best approach is to replace any newline (\n) and carriage return (\r) characters in all string values in the amount object before including it in the log entry. Since amount can contain multiple properties, we should shallow-clone and sanitize each string property. For non-string or null/undefined properties, log them as-is.
This can be accomplished by constructing a new object (e.g. sanitizedAmount) where for each property in amount, if the property is a string, replace all \n and \r with empty strings. Then, log sanitizedAmount in place of the original user-supplied amount object.
All changes are limited to the code region around the logging call in src/services/swap.ts, and do not impact functionality elsewhere.
| @@ -428,7 +428,13 @@ | ||
| let fromAmount: string | ||
| let response: SquidRouteResponse | ||
|
|
||
| console.info('getRoute', { from, to }, amount) | ||
| // Sanitize user-controlled amount fields to prevent log injection | ||
| const sanitizedAmount = Object.fromEntries( | ||
| Object.entries(amount).map(([key, value]) => | ||
| [key, typeof value === 'string' ? value.replace(/[\n\r]/g, '') : value] | ||
| ) | ||
| ); | ||
| console.info('getRoute', { from, to }, sanitizedAmount) | ||
|
|
||
| if (amount.fromAmount) { | ||
| fromAmount = amount.fromAmount.toString() |
| break | ||
| } | ||
| } catch (error) { | ||
| console.error('Error parsing WebSocket message:', error, event.data) |
Check warning
Code scanning / CodeQL
Log injection Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix the log injection vulnerability, we should sanitize event.data before including it in log output. For plain text logs (as here), the most important step is removing any linebreak characters (carriage return, line feed) and clearly differentiating user input from log structure. The best solution is to convert event.data to string (if not already), then remove all \n and \r characters using String.prototype.replace or a utility function. Only the affected line needs to be changed for this fix, requiring no additional dependencies or methods. If event.data is not a string, convert it via String(event.data) or JSON.stringify (if object).
No imports or project-wide changes are needed; just sanitize the event data before logging on line 137 of src/services/websocket.ts.
| @@ -134,7 +134,8 @@ | ||
| break | ||
| } | ||
| } catch (error) { | ||
| console.error('Error parsing WebSocket message:', error, event.data) | ||
| const sanitizedData = String(event.data).replace(/[\n\r]/g, ""); | ||
| console.error('Error parsing WebSocket message:', error, sanitizedData) | ||
| } | ||
| } | ||
|
|
| const response = await fetch(url, { | ||
| ...options, | ||
| signal: controller.signal, | ||
| }) |
Check failure
Code scanning / CodeQL
Server-side request forgery Critical
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To address SSRF risk, ensure that any user-supplied value interpolated into a URL path—especially for outgoing server-to-server requests—cannot contain malicious data. Specifically:
- Restrict
customerIdto allow only known-safe patterns (for example: a valid UUID, or strict numeric ID). - Validate the value before constructing the URL in
createBridgeExternalAccountForGuest. - If the ID is expected to match a certain format, use a regular expression to enforce this (for example, a UUID).
- If the value does not conform, throw an error or return early with an error message.
The best way to implement this with the minimum change and risk is to add this validation at the point where the untrusted data enters, i.e., immediately upon function entry for createBridgeExternalAccountForGuest. This should occur within src/app/actions/external-accounts.ts. No changes are required to fetchWithSentry in src/utils/sentry.utils.ts since endpoint composition should be secure before this utility is called.
If customerId is expected to be a UUID, verify using a regex. If it's meant to be an alphanumeric string, restrict accordingly.
| @@ -11,6 +11,11 @@ | ||
| customerId: string, | ||
| accountDetails: AddBankAccountPayload | ||
| ): Promise<IBridgeAccount | { error: string; source?: string }> { | ||
| // Validate that customerId is a UUID (edit the regex below if your customerId is another format) | ||
| const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; | ||
| if (!uuidRegex.test(customerId)) { | ||
| return { error: "Invalid customerId format." }; | ||
| } | ||
| try { | ||
| const response = await fetchWithSentry(`${API_URL}/bridge/customers/${customerId}/external-accounts`, { | ||
| method: 'POST', |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Global/DirectSendQR/index.tsx (1)
358-375: Use window navigation for external redirect URLs.When a redirect QR is already claimed, the API returns an invite link like
https://peanut.me/invite?.... Passing that absolute URL torouter.pushwill throw (Invariant: attempted to navigate to a URL outside the app), so scanners hit an error instead of the invite. Detect absolute URLs and fall back towindow.locationbefore using the router for internal paths.- router.push(redirectUrl) + if (/^https?:\/\//.test(redirectUrl)) { + window.location.href = redirectUrl + } else { + router.push(redirectUrl) + } setIsQRScannerOpen(false)
🧹 Nitpick comments (1)
src/hooks/useHoldToClaim.ts (1)
56-56: Extract magic number to a constant.The
500ms preview duration is hardcoded. Consider extracting it to a named constant at the file or module level for better maintainability.+const PREVIEW_DURATION_MS = 500 + export function useHoldToClaim({ onComplete, holdDuration = PERK_HOLD_DURATION_MS, disabled = false, }: UseHoldToClaimOptions): UseHoldToClaimReturn {Then use it in
cancelHold:const cancelHold = useCallback(() => { - const PREVIEW_DURATION_MS = 500 - // Calculate how long the user held
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/app/(mobile-ui)/qr/[code]/page.tsx(1 hunks)src/app/(mobile-ui)/qr/[code]/success/page.tsx(1 hunks)src/app/[...recipient]/page.tsx(2 hunks)src/components/Global/DirectSendQR/index.tsx(1 hunks)src/components/Global/DirectSendQR/utils.ts(1 hunks)src/components/Invites/InvitesPage.tsx(2 hunks)src/hooks/useHoldToClaim.ts(1 hunks)src/hooks/useRedirectQrStatus.ts(1 hunks)src/middleware.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (25)
📓 Common learnings
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page.
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page after scanning MERCADO_PAGO or PIX QR codes.
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 942
File: src/components/AddMoney/consts/index.ts:2151-2162
Timestamp: 2025-06-30T10:44:08.048Z
Learning: Hugo0 often agrees with refactoring suggestions but defers implementation due to time constraints, preferring to track improvements as follow-up issues when they're part of larger architectural changes.
📚 Learning: 2024-10-23T09:38:27.670Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-64
Timestamp: 2024-10-23T09:38:27.670Z
Learning: In `src/app/request/pay/page.tsx`, if `linkRes` is not OK in the `generateMetadata` function, the desired behavior is to use the standard title and preview image without throwing an error.
Applied to files:
src/app/[...recipient]/page.tsx
📚 Learning: 2025-05-02T19:14:25.010Z
Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 828
File: src/app/(mobile-ui)/request/[...username]/page.tsx:17-17
Timestamp: 2025-05-02T19:14:25.010Z
Learning: In Next.js App Router, catch-all routes (like `/request/[...username]`) will only match URLs with at least one parameter segment, ensuring that the page component only renders when there is at least one value in the params array.
Applied to files:
src/app/[...recipient]/page.tsx
📚 Learning: 2024-10-22T18:11:36.864Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-49
Timestamp: 2024-10-22T18:11:36.864Z
Learning: In `src/app/request/pay/page.tsx`, the `id` parameter is accessed via `searchParams.id` in the `generateMetadata` function.
Applied to files:
src/app/[...recipient]/page.tsx
📚 Learning: 2024-10-08T20:13:42.967Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Pay.tsx:113-123
Timestamp: 2024-10-08T20:13:42.967Z
Learning: In the `PayRequestLink` component (`src/components/Request/Pay/Pay.tsx`), when resolving ENS names, handle errors by displaying an appropriate error message to the user if the ENS cannot be resolved.
Applied to files:
src/app/[...recipient]/page.tsx
📚 Learning: 2025-05-13T10:05:24.057Z
Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 845
File: src/components/Request/link/views/Create.request.link.view.tsx:81-81
Timestamp: 2025-05-13T10:05:24.057Z
Learning: In the peanut-ui project, pages that handle request flows (like Create.request.link.view.tsx) are only accessible to logged-in users who will always have a username, making null checks for user?.user.username unnecessary in these contexts.
Applied to files:
src/app/[...recipient]/page.tsxsrc/components/Invites/InvitesPage.tsx
📚 Learning: 2025-09-05T07:31:11.396Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1185
File: src/components/Claim/useClaimLink.tsx:14-0
Timestamp: 2025-09-05T07:31:11.396Z
Learning: In the peanut-ui codebase, `window.history.replaceState` is preferred over `router.replace` when immediate/synchronous URL parameter updates are required, as `router.replace` is asynchronous and doesn't guarantee instant URL changes that subsequent code can rely on. This pattern is used consistently across usePaymentInitiator.ts, Confirm.payment.view.tsx, and useClaimLink.tsx.
Applied to files:
src/app/[...recipient]/page.tsxsrc/components/Global/DirectSendQR/index.tsxsrc/app/(mobile-ui)/qr/[code]/success/page.tsxsrc/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2024-10-23T09:38:04.446Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-64
Timestamp: 2024-10-23T09:38:04.446Z
Learning: Within `src/app/request/pay/page.tsx`, extracting the `getBaseUrl` function does not add significant readability, and the host URL construction code is expected to change soon.
Applied to files:
src/app/[...recipient]/page.tsx
📚 Learning: 2025-05-23T19:26:58.220Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 873
File: src/components/Withdraw/views/Initial.withdraw.view.tsx:95-95
Timestamp: 2025-05-23T19:26:58.220Z
Learning: The GeneralRecipientInput component supports username validation and resolution through the validateAndResolveRecipient function in src/lib/validation/recipient.ts. The function automatically detects usernames (inputs that don't contain '.' for ENS and don't start with '0x' for addresses), validates them via API HEAD request, fetches user data, and resolves them to Ethereum addresses from the user's PEANUT_WALLET account.
Applied to files:
src/app/[...recipient]/page.tsxsrc/components/Global/DirectSendQR/utils.ts
📚 Learning: 2025-09-08T03:13:09.111Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page.
Applied to files:
src/app/[...recipient]/page.tsxsrc/components/Global/DirectSendQR/index.tsxsrc/hooks/useRedirectQrStatus.tssrc/components/Global/DirectSendQR/utils.tssrc/app/(mobile-ui)/qr/[code]/success/page.tsxsrc/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2025-09-08T03:13:09.111Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page after scanning MERCADO_PAGO or PIX QR codes.
Applied to files:
src/components/Global/DirectSendQR/index.tsxsrc/app/(mobile-ui)/qr/[code]/success/page.tsxsrc/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2025-07-24T13:26:10.290Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1014
File: src/components/Claim/Link/Initial.view.tsx:413-413
Timestamp: 2025-07-24T13:26:10.290Z
Learning: In the peanut-ui repository, the change from `${SQUID_API_URL}/route` to `${SQUID_API_URL}/v2/route` in src/components/Claim/Link/Initial.view.tsx was a typo fix, not an API migration, as the codebase was already using Squid API v2.
Applied to files:
src/components/Global/DirectSendQR/index.tsxsrc/components/Global/DirectSendQR/utils.tssrc/app/(mobile-ui)/qr/[code]/page.tsxsrc/middleware.ts
📚 Learning: 2025-09-12T09:06:22.741Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1207
File: src/components/Global/PostSignupActionManager/post-signup-action.consts.ts:0-0
Timestamp: 2025-09-12T09:06:22.741Z
Learning: In the peanut-ui codebase, redirect URLs are always stored as relative paths, never as absolute paths. This means regex patterns can safely anchor to `^\/` when matching stored redirect URLs.
Applied to files:
src/components/Global/DirectSendQR/index.tsx
📚 Learning: 2025-08-07T12:53:50.946Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1072
File: src/app/(setup)/setup/page.tsx:173-175
Timestamp: 2025-08-07T12:53:50.946Z
Learning: In the peanut-ui setup flow at `src/app/(setup)/setup/page.tsx`, when handling unsupported scenarios, both device not supported and browser not supported cases should show the same "Unsupported browser" message using the `UnsupportedBrowserModal` component, rather than having distinct messaging for each scenario.
Applied to files:
src/components/Global/DirectSendQR/index.tsx
📚 Learning: 2025-10-02T15:23:01.513Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1266
File: src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:46-57
Timestamp: 2025-10-02T15:23:01.513Z
Learning: In the withdraw flow at src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx, the points calculation query intentionally uses crypto.randomUUID() in the queryKey dependency array to bypass React Query caching, ensuring fresh points estimates on every render. This is the intended behavior.
Applied to files:
src/hooks/useRedirectQrStatus.ts
📚 Learning: 2024-10-29T16:06:38.812Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 495
File: src/components/Create/useCreateLink.tsx:647-657
Timestamp: 2024-10-29T16:06:38.812Z
Learning: In the React code for `useCreateLink` in `src/components/Create/useCreateLink.tsx`, the `switchNetwork` function used within `useCallback` hooks is stable and does not need to be included in the dependency arrays.
Applied to files:
src/hooks/useRedirectQrStatus.tssrc/components/Invites/InvitesPage.tsx
📚 Learning: 2025-08-14T14:42:54.411Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1094
File: src/utils/withdraw.utils.ts:181-191
Timestamp: 2025-08-14T14:42:54.411Z
Learning: The countryCodeMap in src/components/AddMoney/consts/index.ts uses uppercase 3-letter country codes as keys (like 'AUT', 'BEL', 'CZE') that map to 2-letter country codes, requiring input normalization to uppercase for proper lookups.
Applied to files:
src/components/Global/DirectSendQR/utils.ts
📚 Learning: 2025-08-12T17:44:04.268Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1089
File: src/components/LandingPage/dropLink.tsx:35-42
Timestamp: 2025-08-12T17:44:04.268Z
Learning: In the Peanut UI project, opening the `/setup` route in a new tab from landing page CTAs is intentional design behavior to keep users on the marketing page while they start the setup process.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-09-29T18:34:33.596Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1251
File: src/components/Invites/JoinWaitlistPage.tsx:41-55
Timestamp: 2025-09-29T18:34:33.596Z
Learning: In the JoinWaitlistPage component, after successfully accepting an invite via invitesApi.acceptInvite(), calling fetchUser() is sufficient to update the user state and automatically display the app. No manual navigation to /home or other pages is required since the user is already on the home page and the app will be displayed once user.hasAppAccess is updated.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsxsrc/components/Invites/InvitesPage.tsx
📚 Learning: 2025-10-08T17:13:13.155Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1299
File: src/app/(mobile-ui)/points/page.tsx:41-51
Timestamp: 2025-10-08T17:13:13.155Z
Learning: In `src/app/(mobile-ui)/points/page.tsx`, the icon name "invite-heart" is intentionally used (not "inviter-heart") when displaying who invited the current user, as this is a deliberate design choice despite semantic differences with UserHeader usage.
Applied to files:
src/components/Invites/InvitesPage.tsx
📚 Learning: 2025-09-18T09:30:42.901Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1230
File: src/app/(mobile-ui)/withdraw/page.tsx:92-97
Timestamp: 2025-09-18T09:30:42.901Z
Learning: In src/app/(mobile-ui)/withdraw/page.tsx, the useEffect that calls setShowAllWithdrawMethods(true) when amountFromContext exists is intentionally designed to run only on component mount (empty dependency array), not when amountFromContext changes. This is the correct behavior for the withdraw flow where showing all methods should only happen on initial load when an amount is already present.
Applied to files:
src/components/Invites/InvitesPage.tsx
📚 Learning: 2025-07-05T16:58:25.340Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 0
File: :0-0
Timestamp: 2025-07-05T16:58:25.340Z
Learning: Hugo0 successfully refactored sessionStorage usage to React Context in the onramp flow, demonstrating preference for centralized state management over browser storage for component-shared state in React applications.
Applied to files:
src/components/Invites/InvitesPage.tsx
📚 Learning: 2024-12-02T17:19:18.532Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 551
File: src/components/Request/Create/Views/Initial.view.tsx:151-156
Timestamp: 2024-12-02T17:19:18.532Z
Learning: In the `InitialView` component at `src/components/Request/Create/Views/Initial.view.tsx`, when setting the default chain and token in the `useEffect` triggered by `isPeanutWallet`, it's acceptable to omit the setters from the dependency array and not include additional error handling for invalid defaults.
Applied to files:
src/components/Invites/InvitesPage.tsx
📚 Learning: 2025-09-11T17:46:12.507Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1200
File: src/app/(mobile-ui)/recover-funds/page.tsx:9-9
Timestamp: 2025-09-11T17:46:12.507Z
Learning: Functions in Next.js that are not marked with "use server" and contain secrets are unsafe to import in client components, as they get bundled into the client JavaScript and can leak environment variables to the browser.
Applied to files:
src/components/Invites/InvitesPage.tsx
📚 Learning: 2025-05-22T15:38:48.586Z
Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 869
File: src/app/(mobile-ui)/withdraw/page.tsx:82-88
Timestamp: 2025-05-22T15:38:48.586Z
Learning: The country-specific withdrawal route exists at src/app/(mobile-ui)/withdraw/[...country]/page.tsx and renders the AddWithdrawCountriesList component with flow="withdraw".
Applied to files:
src/app/(mobile-ui)/qr/[code]/page.tsx
🔇 Additional comments (3)
src/hooks/useHoldToClaim.ts (3)
1-28: LGTM! Well-structured type definitions.The type definitions are clear and comprehensive. The inclusion of keyboard handlers and accessibility-focused styling props in
UseHoldToClaimReturndemonstrates good attention to accessibility.
34-53: LGTM! Proper cleanup on unmount.The cleanup effect correctly clears all timers and resets references on unmount, preventing memory leaks.
165-200: LGTM! Well-implemented button props with accessibility support.The button props provide comprehensive event handling for both pointer and keyboard interactions. The prevention of context menus, text selection, and tap highlights ensures a smooth touch experience. The keyboard support (Enter and Space keys) makes the interaction accessible.
| if (isLoading || !redirectQrData) { | ||
| return ( | ||
| <div className="flex min-h-[inherit] flex-col gap-8"> | ||
| <NavHeader title="Success" /> | ||
| <div className="flex h-full items-center justify-center"> | ||
| <PeanutLoading /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
There was a problem hiding this comment.
Handle redirect status errors on success page.
If useRedirectQrStatus rejects (network error, 404, etc.), redirectQrData stays undefined while isLoading is already false, so this branch renders the loading screen forever and the user never sees a failure message. Please surface an explicit error state before the loader to let users recover.
-import { useRedirectQrStatus } from '@/hooks/useRedirectQrStatus'
+import { useRedirectQrStatus } from '@/hooks/useRedirectQrStatus'
+import ErrorAlert from '@/components/Global/ErrorAlert'
…
- const { data: redirectQrData, isLoading } = useRedirectQrStatus(code)
+ const { data: redirectQrData, isLoading, error } = useRedirectQrStatus(code)
…
- if (isLoading || !redirectQrData) {
+ if (error) {
+ return (
+ <div className="flex min-h-[inherit] flex-col gap-8">
+ <NavHeader title="Success" />
+ <div className="flex h-full items-center justify-center px-6">
+ <ErrorAlert description="We couldn't load your QR details. Please try again." />
+ </div>
+ </div>
+ )
+ }
+
+ if (isLoading || !redirectQrData) {
return (
<div className="flex min-h-[inherit] flex-col gap-8">
<NavHeader title="Success" />
<div className="flex h-full items-center justify-center">
<PeanutLoading />
</div>
</div>
)
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/(mobile-ui)/qr/[code]/success/page.tsx around lines 45 to 55, the
component returns the loading UI when redirectQrData is undefined even if the
useRedirectQrStatus hook has errored; update the render logic to explicitly
check the hook's error state (e.g., isError or error) and, before showing the
loader, render an error state/alert with a user-facing message and recovery
options (retry button or link back) plus the NavHeader; keep the loader path
only for isLoading === true and ensure the error rendering covers network/404
cases so the user is not stuck on the spinner.
| const startHold = useCallback(() => { | ||
| if (disabled) return | ||
|
|
||
| setHoldProgress(0) | ||
| setIsShaking(true) | ||
|
|
||
| const startTime = Date.now() | ||
| holdStartTimeRef.current = startTime | ||
| let lastIntensity: ShakeIntensity = 'weak' | ||
|
|
||
| // Update progress and shake intensity | ||
| const interval = setInterval(() => { | ||
| const elapsed = Date.now() - startTime | ||
| const progress = Math.min((elapsed / holdDuration) * 100, 100) | ||
| setHoldProgress(progress) | ||
|
|
||
| // Progressive shake intensity with haptic feedback | ||
| let newIntensity: ShakeIntensity = 'weak' | ||
| if (progress < 25) { | ||
| newIntensity = 'weak' | ||
| } else if (progress < 50) { | ||
| newIntensity = 'medium' | ||
| } else if (progress < 75) { | ||
| newIntensity = 'strong' | ||
| } else { | ||
| newIntensity = 'intense' | ||
| } | ||
|
|
||
| // Trigger haptic feedback when intensity changes | ||
| if (newIntensity !== lastIntensity && 'vibrate' in navigator) { | ||
| // Progressive vibration patterns that match shake intensity | ||
| switch (newIntensity) { | ||
| case 'weak': | ||
| navigator.vibrate(50) // Short but noticeable pulse | ||
| break | ||
| case 'medium': | ||
| navigator.vibrate([100, 40, 100]) // Medium pulse pattern | ||
| break | ||
| case 'strong': | ||
| navigator.vibrate([150, 40, 150, 40, 150]) // Strong pulse pattern | ||
| break | ||
| case 'intense': | ||
| navigator.vibrate([200, 40, 200, 40, 200, 40, 200]) // INTENSE pulse pattern | ||
| break | ||
| } | ||
| lastIntensity = newIntensity | ||
| } | ||
|
|
||
| setShakeIntensity(newIntensity) | ||
|
|
||
| if (progress >= 100) { | ||
| clearInterval(interval) | ||
| } | ||
| }, 50) | ||
|
|
||
| progressIntervalRef.current = interval | ||
|
|
||
| // Complete after hold duration | ||
| const timer = setTimeout(() => { | ||
| onComplete() | ||
| }, holdDuration) | ||
|
|
||
| holdTimerRef.current = timer | ||
| }, [onComplete, holdDuration, disabled]) |
There was a problem hiding this comment.
Critical: Race condition with preview reset timer.
When startHold is called, it doesn't clear existing timers before setting new ones. This creates a race condition:
- User taps briefly →
startHoldsets completion timer - User releases quickly →
cancelHoldsets preview reset timer inholdTimerRef(line 84) - User taps again before preview completes →
startHoldoverwritesholdTimerRefwithout clearing the preview timer - The orphaned preview reset timer fires → resets state while user is actively holding the button
Apply this diff to clear existing timers before starting a new hold:
const startHold = useCallback(() => {
if (disabled) return
+
+ // Clear any existing timers to prevent race conditions
+ if (holdTimerRef.current) clearTimeout(holdTimerRef.current)
+ if (progressIntervalRef.current) clearInterval(progressIntervalRef.current)
+ holdTimerRef.current = null
+ progressIntervalRef.current = null
setHoldProgress(0)
setIsShaking(true)📝 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.
| const startHold = useCallback(() => { | |
| if (disabled) return | |
| setHoldProgress(0) | |
| setIsShaking(true) | |
| const startTime = Date.now() | |
| holdStartTimeRef.current = startTime | |
| let lastIntensity: ShakeIntensity = 'weak' | |
| // Update progress and shake intensity | |
| const interval = setInterval(() => { | |
| const elapsed = Date.now() - startTime | |
| const progress = Math.min((elapsed / holdDuration) * 100, 100) | |
| setHoldProgress(progress) | |
| // Progressive shake intensity with haptic feedback | |
| let newIntensity: ShakeIntensity = 'weak' | |
| if (progress < 25) { | |
| newIntensity = 'weak' | |
| } else if (progress < 50) { | |
| newIntensity = 'medium' | |
| } else if (progress < 75) { | |
| newIntensity = 'strong' | |
| } else { | |
| newIntensity = 'intense' | |
| } | |
| // Trigger haptic feedback when intensity changes | |
| if (newIntensity !== lastIntensity && 'vibrate' in navigator) { | |
| // Progressive vibration patterns that match shake intensity | |
| switch (newIntensity) { | |
| case 'weak': | |
| navigator.vibrate(50) // Short but noticeable pulse | |
| break | |
| case 'medium': | |
| navigator.vibrate([100, 40, 100]) // Medium pulse pattern | |
| break | |
| case 'strong': | |
| navigator.vibrate([150, 40, 150, 40, 150]) // Strong pulse pattern | |
| break | |
| case 'intense': | |
| navigator.vibrate([200, 40, 200, 40, 200, 40, 200]) // INTENSE pulse pattern | |
| break | |
| } | |
| lastIntensity = newIntensity | |
| } | |
| setShakeIntensity(newIntensity) | |
| if (progress >= 100) { | |
| clearInterval(interval) | |
| } | |
| }, 50) | |
| progressIntervalRef.current = interval | |
| // Complete after hold duration | |
| const timer = setTimeout(() => { | |
| onComplete() | |
| }, holdDuration) | |
| holdTimerRef.current = timer | |
| }, [onComplete, holdDuration, disabled]) | |
| const startHold = useCallback(() => { | |
| if (disabled) return | |
| // Clear any existing timers to prevent race conditions | |
| if (holdTimerRef.current) clearTimeout(holdTimerRef.current) | |
| if (progressIntervalRef.current) clearInterval(progressIntervalRef.current) | |
| holdTimerRef.current = null | |
| progressIntervalRef.current = null | |
| setHoldProgress(0) | |
| setIsShaking(true) | |
| const startTime = Date.now() | |
| holdStartTimeRef.current = startTime | |
| let lastIntensity: ShakeIntensity = 'weak' | |
| // Update progress and shake intensity | |
| const interval = setInterval(() => { | |
| const elapsed = Date.now() - startTime | |
| const progress = Math.min((elapsed / holdDuration) * 100, 100) | |
| setHoldProgress(progress) | |
| // Progressive shake intensity with haptic feedback | |
| let newIntensity: ShakeIntensity = 'weak' | |
| if (progress < 25) { | |
| newIntensity = 'weak' | |
| } else if (progress < 50) { | |
| newIntensity = 'medium' | |
| } else if (progress < 75) { | |
| newIntensity = 'strong' | |
| } else { | |
| newIntensity = 'intense' | |
| } | |
| // Trigger haptic feedback when intensity changes | |
| if (newIntensity !== lastIntensity && 'vibrate' in navigator) { | |
| // Progressive vibration patterns that match shake intensity | |
| switch (newIntensity) { | |
| case 'weak': | |
| navigator.vibrate(50) // Short but noticeable pulse | |
| break | |
| case 'medium': | |
| navigator.vibrate([100, 40, 100]) // Medium pulse pattern | |
| break | |
| case 'strong': | |
| navigator.vibrate([150, 40, 150, 40, 150]) // Strong pulse pattern | |
| break | |
| case 'intense': | |
| navigator.vibrate([200, 40, 200, 40, 200, 40, 200]) // INTENSE pulse pattern | |
| break | |
| } | |
| lastIntensity = newIntensity | |
| } | |
| setShakeIntensity(newIntensity) | |
| if (progress >= 100) { | |
| clearInterval(interval) | |
| } | |
| }, 50) | |
| progressIntervalRef.current = interval | |
| // Complete after hold duration | |
| const timer = setTimeout(() => { | |
| onComplete() | |
| }, holdDuration) | |
| holdTimerRef.current = timer | |
| }, [onComplete, holdDuration, disabled]) |
- Remove console.log debug statements from QR claim page - Remove console.log debug statements from InvitesPage - Restore RESERVED_ROUTES and route guards in catch-all page - Restore QR scanner integration for redirect QR codes - Restore middleware QR route handling
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Global/ShareButton/index.tsx (1)
44-67: Refactor to use shared clipboard utility and fix execCommand bug.This function duplicates logic from
src/utils/general.utils.ts(lines 392-418) and introduces a subtle bug: Line 59 doesn't check the return value ofdocument.execCommand('copy'), so the function always returnstrueat line 61 even if the copy fails. This causes the component to show "Link copied" toast when the clipboard operation actually failed.Consider updating the shared
copyTextToClipboardWithFallbackutility to return a boolean indicating success, then import and use it here:- const copyTextToClipboardWithFallback = async (text: string) => { - try { - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text) - return true - } else { - // Fallback for older browsers - const textArea = document.createElement('textarea') - textArea.value = text - textArea.style.position = 'fixed' - textArea.style.left = '-999999px' - textArea.style.top = '-999999px' - document.body.appendChild(textArea) - textArea.focus() - textArea.select() - document.execCommand('copy') - document.body.removeChild(textArea) - return true - } - } catch (err) { - console.error('Failed to copy: ', err) - return false - } - }In
src/utils/general.utils.ts, update the function to return success status:export async function copyTextToClipboardWithFallback(text: string): Promise<boolean> { if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text) return true } catch (err) { Sentry.captureException(err) console.error('Clipboard API failed, trying fallback method. Error:', err) } } try { const textarea = document.createElement('textarea') textarea.value = text textarea.setAttribute('readonly', '') textarea.style.position = 'absolute' textarea.style.left = '-9999px' document.body.appendChild(textarea) textarea.select() const successful = document.execCommand('copy') document.body.removeChild(textarea) return successful } catch (err) { Sentry.captureException(err) console.error('Fallback method failed. Error:', err) return false } }Then import and use it in this component:
import { copyTextToClipboardWithFallback } from '@/utils/general.utils'
♻️ Duplicate comments (1)
src/app/(mobile-ui)/qr/[code]/success/page.tsx (1)
36-45: Handle fetch errors before showing the spinner.If
useRedirectQrStatusrejects (network failure, QR already claimed, etc.),redirectQrDatastaysundefinedwhileisLoadingbecomesfalse, so this branch renders the loading screen indefinitely and the user never sees what went wrong. Surface an explicit error path (e.g., checkerror/isErrorfrom the query) before the loader so people can retry or navigate away instead of being stuck on a spinner.
🧹 Nitpick comments (2)
src/components/Global/ActionModal/index.tsx (1)
109-109: Border removal works correctly; consider simplification.The addition of
!border-0ensures border removal with!importantprecedence. Since the base Modal component now includesborder-0in its default classes (line 80 of Modal/index.tsx), the!modifier provides extra override protection but may be redundant. The current implementation functions correctly viatwMergeconflict resolution.If the
!importantoverride isn't needed for specific conflicts, you could simplify by removing!border-0here and relying on Modal'sborder-0:- 'sm:m-auto sm:self-center self-center m-4 bg-white rounded-none !border-0 z-50', + 'sm:m-auto sm:self-center self-center m-4 bg-white rounded-none z-50',src/constants/routes.ts (1)
94-97: Remove unsafe type cast and handle undefined explicitly.The
as anycast bypasses TypeScript's type safety. SincefirstSegmentcan beundefinedwhen the path has no segments, handle this case explicitly.Apply this diff:
export function isReservedRoute(path: string): boolean { const firstSegment = path.split('/')[1]?.toLowerCase() - return RESERVED_ROUTES.includes(firstSegment as any) + return firstSegment ? RESERVED_ROUTES.includes(firstSegment) : false }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
docs/ROUTES_CONFIG.md(1 hunks)knip.json(1 hunks)public/game/peanut-game.html(2 hunks)public/onesignal/OneSignalSDKWorker.js(1 hunks)src/app/(mobile-ui)/layout.tsx(2 hunks)src/app/(mobile-ui)/qr/[code]/page.tsx(1 hunks)src/app/(mobile-ui)/qr/[code]/success/page.tsx(1 hunks)src/app/[...recipient]/page.tsx(2 hunks)src/app/layout.tsx(1 hunks)src/components/Global/ActionModal/index.tsx(1 hunks)src/components/Global/Modal/index.tsx(1 hunks)src/components/Global/ShareButton/index.tsx(2 hunks)src/components/Invites/InvitesPage.tsx(2 hunks)src/components/Profile/components/PublicProfile.tsx(1 hunks)src/constants/index.ts(1 hunks)src/constants/routes.ts(1 hunks)src/hooks/query/user.ts(2 hunks)src/middleware.ts(2 hunks)
✅ Files skipped from review due to trivial changes (2)
- src/app/layout.tsx
- docs/ROUTES_CONFIG.md
🚧 Files skipped from review as they are similar to previous changes (3)
- src/middleware.ts
- src/app/(mobile-ui)/qr/[code]/page.tsx
- src/components/Invites/InvitesPage.tsx
🧰 Additional context used
🧠 Learnings (19)
📓 Common learnings
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page.
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page after scanning MERCADO_PAGO or PIX QR codes.
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 942
File: src/components/AddMoney/consts/index.ts:2151-2162
Timestamp: 2025-06-30T10:44:08.048Z
Learning: Hugo0 often agrees with refactoring suggestions but defers implementation due to time constraints, preferring to track improvements as follow-up issues when they're part of larger architectural changes.
📚 Learning: 2025-07-24T13:26:10.290Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1014
File: src/components/Claim/Link/Initial.view.tsx:413-413
Timestamp: 2025-07-24T13:26:10.290Z
Learning: In the peanut-ui repository, the change from `${SQUID_API_URL}/route` to `${SQUID_API_URL}/v2/route` in src/components/Claim/Link/Initial.view.tsx was a typo fix, not an API migration, as the codebase was already using Squid API v2.
Applied to files:
src/app/(mobile-ui)/layout.tsxsrc/app/[...recipient]/page.tsx
📚 Learning: 2024-10-23T09:38:04.446Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-64
Timestamp: 2024-10-23T09:38:04.446Z
Learning: Within `src/app/request/pay/page.tsx`, extracting the `getBaseUrl` function does not add significant readability, and the host URL construction code is expected to change soon.
Applied to files:
src/app/(mobile-ui)/layout.tsxsrc/app/[...recipient]/page.tsx
📚 Learning: 2025-09-05T07:31:11.396Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1185
File: src/components/Claim/useClaimLink.tsx:14-0
Timestamp: 2025-09-05T07:31:11.396Z
Learning: In the peanut-ui codebase, `window.history.replaceState` is preferred over `router.replace` when immediate/synchronous URL parameter updates are required, as `router.replace` is asynchronous and doesn't guarantee instant URL changes that subsequent code can rely on. This pattern is used consistently across usePaymentInitiator.ts, Confirm.payment.view.tsx, and useClaimLink.tsx.
Applied to files:
src/app/(mobile-ui)/layout.tsxsrc/app/[...recipient]/page.tsxsrc/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-09-12T09:06:22.741Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1207
File: src/components/Global/PostSignupActionManager/post-signup-action.consts.ts:0-0
Timestamp: 2025-09-12T09:06:22.741Z
Learning: In the peanut-ui codebase, redirect URLs are always stored as relative paths, never as absolute paths. This means regex patterns can safely anchor to `^\/` when matching stored redirect URLs.
Applied to files:
src/app/(mobile-ui)/layout.tsx
📚 Learning: 2024-10-23T09:38:27.670Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-64
Timestamp: 2024-10-23T09:38:27.670Z
Learning: In `src/app/request/pay/page.tsx`, if `linkRes` is not OK in the `generateMetadata` function, the desired behavior is to use the standard title and preview image without throwing an error.
Applied to files:
src/app/[...recipient]/page.tsxsrc/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2024-10-22T18:11:36.864Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 469
File: src/app/request/pay/page.tsx:32-49
Timestamp: 2024-10-22T18:11:36.864Z
Learning: In `src/app/request/pay/page.tsx`, the `id` parameter is accessed via `searchParams.id` in the `generateMetadata` function.
Applied to files:
src/app/[...recipient]/page.tsx
📚 Learning: 2024-10-08T20:13:42.967Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 422
File: src/components/Request/Pay/Pay.tsx:113-123
Timestamp: 2024-10-08T20:13:42.967Z
Learning: In the `PayRequestLink` component (`src/components/Request/Pay/Pay.tsx`), when resolving ENS names, handle errors by displaying an appropriate error message to the user if the ENS cannot be resolved.
Applied to files:
src/app/[...recipient]/page.tsxsrc/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-05-02T19:14:25.010Z
Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 828
File: src/app/(mobile-ui)/request/[...username]/page.tsx:17-17
Timestamp: 2025-05-02T19:14:25.010Z
Learning: In Next.js App Router, catch-all routes (like `/request/[...username]`) will only match URLs with at least one parameter segment, ensuring that the page component only renders when there is at least one value in the params array.
Applied to files:
src/app/[...recipient]/page.tsxsrc/constants/routes.ts
📚 Learning: 2025-05-13T10:05:24.057Z
Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 845
File: src/components/Request/link/views/Create.request.link.view.tsx:81-81
Timestamp: 2025-05-13T10:05:24.057Z
Learning: In the peanut-ui project, pages that handle request flows (like Create.request.link.view.tsx) are only accessible to logged-in users who will always have a username, making null checks for user?.user.username unnecessary in these contexts.
Applied to files:
src/app/[...recipient]/page.tsx
📚 Learning: 2025-07-24T10:57:15.315Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1000
File: src/components/og/ProfileCardOG.tsx:0-0
Timestamp: 2025-07-24T10:57:15.315Z
Learning: In `src/components/og/ProfileCardOG.tsx`, the scribble image should have an empty alt attribute (alt="") to prevent layout issues if the image fails to load. Since it's a decorative element positioned absolutely over the username text, showing alt text would interfere with the layout and username display.
Applied to files:
src/components/Profile/components/PublicProfile.tsx
📚 Learning: 2025-09-08T03:13:09.111Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-09-08T03:13:09.111Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page after scanning MERCADO_PAGO or PIX QR codes.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-08-26T15:25:53.328Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1132
File: src/app/[...recipient]/client.tsx:394-397
Timestamp: 2025-08-26T15:25:53.328Z
Learning: In `src/components/Common/ActionListDaimoPayButton.tsx`, the `handleCompleteDaimoPayment` function should not display error messages to users when DB update fails because the Daimo payment itself has succeeded - showing errors would be confusing since the payment was successful.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-08-07T12:53:50.946Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1072
File: src/app/(setup)/setup/page.tsx:173-175
Timestamp: 2025-08-07T12:53:50.946Z
Learning: In the peanut-ui setup flow at `src/app/(setup)/setup/page.tsx`, when handling unsupported scenarios, both device not supported and browser not supported cases should show the same "Unsupported browser" message using the `UnsupportedBrowserModal` component, rather than having distinct messaging for each scenario.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2024-11-18T21:36:11.486Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 535
File: src/components/Claim/Claim.tsx:142-146
Timestamp: 2024-11-18T21:36:11.486Z
Learning: In `src/components/Claim/Claim.tsx`, external calls like token price fetching and cross-chain details retrieval are already encapsulated within existing `try...catch` blocks, so additional error handling may be unnecessary.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2024-12-11T10:13:22.806Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 564
File: src/components/Request/Pay/Views/Initial.view.tsx:430-430
Timestamp: 2024-12-11T10:13:22.806Z
Learning: In the React TypeScript file `src/components/Request/Pay/Views/Initial.view.tsx`, when reviewing the `InitialView` component, do not flag potential issues with using non-null assertion `!` on the `slippagePercentage` variable, as handling undefined values in this context is considered out of scope.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-08-22T07:28:32.281Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1104
File: src/components/Payment/PaymentForm/index.tsx:522-545
Timestamp: 2025-08-22T07:28:32.281Z
Learning: In `src/components/Payment/PaymentForm/index.tsx`, the `handleCompleteDaimoPayment` function is only for updating payment status in the backend after a successful Daimo payment. Payment success/failure is handled by Daimo itself, so try/catch error handling and error display are not needed for backend sync failures - users shouldn't see errors if payment succeeded but database update failed.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
📚 Learning: 2025-08-12T17:44:04.268Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1089
File: src/components/LandingPage/dropLink.tsx:35-42
Timestamp: 2025-08-12T17:44:04.268Z
Learning: In the Peanut UI project, opening the `/setup` route in a new tab from landing page CTAs is intentional design behavior to keep users on the marketing page while they start the setup process.
Applied to files:
src/app/(mobile-ui)/qr/[code]/success/page.tsx
🧬 Code graph analysis (5)
src/app/(mobile-ui)/layout.tsx (1)
src/constants/routes.ts (1)
PUBLIC_ROUTES_REGEX(60-60)
src/app/[...recipient]/page.tsx (3)
src/constants/routes.ts (1)
RESERVED_ROUTES(48-48)src/lib/hosting/get-origin.ts (1)
getOrigin(3-16)src/constants/general.consts.ts (1)
BASE_URL(52-52)
src/components/Global/ShareButton/index.tsx (1)
src/utils/general.utils.ts (1)
copyTextToClipboardWithFallback(393-419)
src/hooks/query/user.ts (1)
src/hooks/useGetDeviceType.ts (1)
useDeviceType(37-42)
src/app/(mobile-ui)/qr/[code]/success/page.tsx (7)
src/components/0_Bruddle/Toast.tsx (1)
useToast(111-117)src/hooks/useRedirectQrStatus.ts (1)
useRedirectQrStatus(22-30)src/components/Global/DirectSendQR/utils.ts (1)
BASE_URL(108-108)src/utils/confetti.ts (1)
confettiPresets(70-84)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)src/components/Global/Icons/Icon.tsx (1)
Icon(209-218)src/components/0_Bruddle/Button.tsx (1)
Button(78-269)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (6)
public/onesignal/OneSignalSDKWorker.js (1)
1-6: Documentation improvement approved.The expanded comments clearly explain OneSignal's version pinning strategy for the v16 CDN channel, including automatic security updates and the self-hosting alternative. The reference link has been verified and is accessible.
src/hooks/query/user.ts (1)
9-9: LGTM! Clean telemetry enhancement.The addition of device type tracking to the login metric is well-integrated. The
useDeviceTypehook is called correctly at the component level, and the device type is properly included in the analytics payload alongside the existingisPwaflag.Also applies to: 13-13, 22-25
src/components/Global/Modal/index.tsx (1)
80-80: LGTM!Adding
border-0to the base Dialog.Panel classes ensures consistent border removal across modal components. The change aligns with the ActionModal updates for unified modal presentation.src/components/Global/ShareButton/index.tsx (1)
69-111: LGTM: Clipboard-first approach improves UX.The new flow significantly improves user experience:
- Clipboard copy happens first (lines 75-80), ensuring users always have the content even if the share dialog fails or isn't available
- Immediate feedback via toast notification confirms the copy succeeded
- Native share dialog is attempted as a bonus feature (lines 83-89)
- Error handling gracefully distinguishes between user cancellation (AbortError) and actual failures (lines 94-110)
- Fallback copy attempt (lines 99-106) only triggers if the initial copy failed
This approach works well across desktop (clipboard + optional share) and mobile (clipboard + share dialog) contexts.
src/app/(mobile-ui)/layout.tsx (1)
23-23: LGTM: Centralized routing constant usage.The refactoring to use the imported
PUBLIC_ROUTES_REGEXfrom the centralized routes configuration is clean and reduces duplication. Note that the regex itself has an issue flagged insrc/constants/routes.ts(line 60) that should be addressed.Also applies to: 79-79
src/app/[...recipient]/page.tsx (1)
177-182: LGTM: Reserved route guard is correctly implemented.The guard properly prevents the catch-all route from handling reserved routes by calling
notFound(). The code safely handles the undefined case sincerecipientis defaulted to[]on line 175.
|
@coderabbitai review in depth. Be especially vigilant towards breaking backwards functionality and risks of code. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 1629 🏁 Script executed: Length of output: 10872 🏁 Script executed: Length of output: 10319 🏁 Script executed: Length of output: 36070 🏁 Script executed: Length of output: 5272 🏁 Script executed: Length of output: 4094 🏁 Script executed: Length of output: 2493 🏁 Script executed: Length of output: 110 🏁 Script executed: Length of output: 3753 🏁 Script executed: Length of output: 3002 🏁 Script executed: Length of output: 5010 🏁 Script executed: Length of output: 671 🏁 Script executed: Length of output: 3824 🏁 Script executed: Length of output: 664 🏁 Script executed: Length of output: 1772 🏁 Script executed: Length of output: 232 @Hugo0, I've completed an in-depth security and backwards compatibility review. I've identified several critical security vulnerabilities and backwards compatibility risks that need immediate attention: 🚨 Critical Security Issues1. Open Redirect Vulnerability (HIGH SEVERITY)The QR claim page accepts Location: if (redirectQrData.claimed && redirectQrData.redirectUrl) {
try {
const url = new URL(redirectQrData.redirectUrl)
const isExternal = url.origin !== window.location.origin
if (isExternal) {
window.location.href = redirectQrData.redirectUrl // ⚠️ UNSAFE
} else {
router.push(invitePath)
}
} catch (error) {
window.location.href = redirectQrData.redirectUrl // ⚠️ UNSAFE - fallback also vulnerable
}
}Attack Scenario:
The codebase HAS a sanitization utility ( Same issue in if (data.claimed && data.redirectUrl) {
redirectUrl = data.redirectUrl // ⚠️ No validation
}Fix Required: // Import the existing utility
import { sanitizeRedirectURL } from '@/utils'
// In the useEffect:
if (redirectQrData.claimed && redirectQrData.redirectUrl) {
const sanitizedUrl = sanitizeRedirectURL(redirectQrData.redirectUrl)
if (!sanitizedUrl) {
// Reject external redirects
console.error('Rejecting unsafe redirect URL:', redirectQrData.redirectUrl)
router.push('/home')
return
}
router.push(sanitizedUrl)
return
}2. Missing Backend Validation ConfirmationWhile the frontend should validate, the backend MUST be the primary defense. I cannot see the backend code, but please confirm:
Recommended backend validation (if not already implemented): // Backend: POST /qr/:code/claim
const ALLOWED_REDIRECT_PATTERN = /^\/invite\?code=[A-Z0-9]+INVITESYOU$/i;
function validateRedirectUrl(targetUrl: string): boolean {
try {
const url = new URL(targetUrl, 'https://peanut.me');
// Only allow same-origin
if (url.origin !== 'https://peanut.me') {
return false;
}
// Only allow invite paths
const path = url.pathname + url.search;
if (!ALLOWED_REDIRECT_PATTERN.test(path)) {
return false;
}
return true;
} catch {
return false;
}
}3. JWT Exposure via js-cookie (MEDIUM SEVERITY)The JWT token is accessed via Location: const response = await fetch(`${PEANUT_API_URL}/qr/${code}/claim`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Cookies.get('jwt-token')}`, // ⚠️ Accessible to JS
},
body: JSON.stringify({}),
})Concern: If the cookie is not httpOnly, it's vulnerable to XSS attacks. While I see that the server-side API routes DO set httpOnly: // src/app/api/peanut/user/login-user/route.ts:39
httpOnly: true,
sameSite: 'strict',However,
Action Required: Please verify the JWT cookie configuration. If it's not httpOnly, this is a vulnerability. 4. No CSRF ProtectionThe claim endpoint uses only Bearer token authentication without CSRF tokens. While this is acceptable for API endpoints, ensure the backend validates:
|
|
@coderabbitai review latest changes |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 742 🏁 Script executed: Length of output: 2579 🏁 Script executed: Length of output: 1232 🏁 Script executed: Length of output: 2552 🏁 Script executed: Length of output: 2368 🏁 Script executed: Length of output: 3213 🏁 Script executed: Length of output: 160 @Hugo0, I've reviewed the latest changes. Here's the status: ✅ Fixed IssuesGood work on these:
🚨 CRITICAL ISSUE STILL UNFIXEDThe open redirect vulnerability is still present and must be fixed before merging. Problem Locations:1. QR Claim Page (src/app/(mobile-ui)/qr/[code]/page.tsx:38-56) The code still redirects to if (redirectQrData.claimed && redirectQrData.redirectUrl) {
try {
const url = new URL(redirectQrData.redirectUrl)
const isExternal = url.origin !== window.location.origin
if (isExternal) {
window.location.href = redirectQrData.redirectUrl // ⚠️ UNSAFE
} else {
const invitePath = `${url.pathname}${url.search}`
router.push(invitePath)
}
} catch (error) {
window.location.href = redirectQrData.redirectUrl // ⚠️ UNSAFE
}
return
}2. DirectSendQR Component (src/components/Global/DirectSendQR/index.tsx:234-237) if (data.claimed && data.redirectUrl) {
redirectUrl = data.redirectUrl // ⚠️ No validation
}The Fix (Copy-Paste Ready):For src/app/(mobile-ui)/qr/[code]/page.tsx (around line 38): if (redirectQrData.claimed && redirectQrData.redirectUrl) {
// SECURITY: Validate redirect URL before using it
const sanitizedUrl = sanitizeRedirectURL(redirectQrData.redirectUrl)
if (!sanitizedUrl) {
console.error('Unsafe redirect URL rejected:', redirectQrData.redirectUrl)
router.push('/home')
return
}
// Use the sanitized URL (always internal after validation)
router.push(sanitizedUrl)
return
}Don't forget to add the import at the top: import { saveRedirectUrl, generateInviteCodeLink, sanitizeRedirectURL } from '@/utils'For src/components/Global/DirectSendQR/index.tsx (around line 234): if (data.claimed && data.redirectUrl) {
// SECURITY: Validate redirect URL
const sanitizedUrl = sanitizeRedirectURL(data.redirectUrl)
if (sanitizedUrl) {
redirectUrl = sanitizedUrl
} else {
console.error('Unsafe redirect URL rejected:', data.redirectUrl)
redirectUrl = `/qr/${redirectQrCode}`
}
}Add the import: import { sanitizeRedirectURL } from '@/utils'📊 Attack Scenario Without This Fix
Your existing
|
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/(mobile-ui)/qr/[code]/page.tsx(1 hunks)src/app/[...recipient]/page.tsx(2 hunks)src/constants/routes.ts(1 hunks)src/utils/general.utils.ts(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/utils/general.utils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/app/[...recipient]/page.tsx
- src/constants/routes.ts
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page.
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page after scanning MERCADO_PAGO or PIX QR codes.
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 942
File: src/components/AddMoney/consts/index.ts:2151-2162
Timestamp: 2025-06-30T10:44:08.048Z
Learning: Hugo0 often agrees with refactoring suggestions but defers implementation due to time constraints, preferring to track improvements as follow-up issues when they're part of larger architectural changes.
📚 Learning: 2025-09-08T03:13:09.111Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page.
Applied to files:
src/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2025-09-08T03:13:09.111Z
Learnt from: jjramirezn
Repo: peanutprotocol/peanut-ui PR: 1190
File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176
Timestamp: 2025-09-08T03:13:09.111Z
Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page after scanning MERCADO_PAGO or PIX QR codes.
Applied to files:
src/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2025-09-05T07:31:11.396Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1185
File: src/components/Claim/useClaimLink.tsx:14-0
Timestamp: 2025-09-05T07:31:11.396Z
Learning: In the peanut-ui codebase, `window.history.replaceState` is preferred over `router.replace` when immediate/synchronous URL parameter updates are required, as `router.replace` is asynchronous and doesn't guarantee instant URL changes that subsequent code can rely on. This pattern is used consistently across usePaymentInitiator.ts, Confirm.payment.view.tsx, and useClaimLink.tsx.
Applied to files:
src/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2025-05-22T15:38:48.586Z
Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 869
File: src/app/(mobile-ui)/withdraw/page.tsx:82-88
Timestamp: 2025-05-22T15:38:48.586Z
Learning: The country-specific withdrawal route exists at src/app/(mobile-ui)/withdraw/[...country]/page.tsx and renders the AddWithdrawCountriesList component with flow="withdraw".
Applied to files:
src/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2025-07-24T13:26:10.290Z
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1014
File: src/components/Claim/Link/Initial.view.tsx:413-413
Timestamp: 2025-07-24T13:26:10.290Z
Learning: In the peanut-ui repository, the change from `${SQUID_API_URL}/route` to `${SQUID_API_URL}/v2/route` in src/components/Claim/Link/Initial.view.tsx was a typo fix, not an API migration, as the codebase was already using Squid API v2.
Applied to files:
src/app/(mobile-ui)/qr/[code]/page.tsx
📚 Learning: 2025-08-12T17:44:04.268Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1089
File: src/components/LandingPage/dropLink.tsx:35-42
Timestamp: 2025-08-12T17:44:04.268Z
Learning: In the Peanut UI project, opening the `/setup` route in a new tab from landing page CTAs is intentional design behavior to keep users on the marketing page while they start the setup process.
Applied to files:
src/app/(mobile-ui)/qr/[code]/page.tsx
🧬 Code graph analysis (1)
src/app/(mobile-ui)/qr/[code]/page.tsx (9)
src/context/authContext.tsx (1)
useAuth(192-198)src/hooks/useRedirectQrStatus.ts (1)
useRedirectQrStatus(22-30)src/utils/general.utils.ts (2)
saveRedirectUrl(786-790)generateInviteCodeLink(892-897)src/constants/general.consts.ts (1)
PEANUT_API_URL(43-47)src/hooks/useHoldToClaim.ts (1)
useHoldToClaim(34-200)src/utils/perk.utils.ts (1)
getShakeClass(13-27)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)src/components/Global/Icons/Icon.tsx (1)
Icon(209-218)src/components/0_Bruddle/Button.tsx (1)
Button(78-269)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (3)
src/app/(mobile-ui)/qr/[code]/page.tsx (3)
68-108: Verify backend implements rate limiting and token validation for claim endpoint.The claim handler makes an authenticated POST request without client-side rate limiting or additional abuse prevention. While the hold-to-claim UX provides some friction, ensure the backend enforces:
- Rate limiting on POST
/qr/:code/claim(e.g., max 5 attempts per minute per user)- JWT validation and revocation checks
- Idempotency handling for duplicate claims
To confirm backend protection is in place, ask the backend team or review the API implementation. Additionally, monitor for abuse patterns in production.
As per PR objectives, this is a known concern that should be addressed on the backend before deployment.
38-58: CRITICAL: Open redirect vulnerability - validate and sanitizeredirectUrlbefore navigation.The
redirectUrlfrom the backend is used directly for navigation without client-side validation or sanitization. An attacker who compromises the backend or exploits a backend validation gap could redirect users to phishing sites.Attack scenario:
- Attacker sets malicious
targetUrlin backend (e.g.,https://evil.com/phishing)- User scans QR code
- Lines 47 or 55 redirect user to attacker-controlled site
Apply this diff to sanitize the redirect URL:
// If QR is already claimed, redirect to the target URL if (redirectQrData.claimed && redirectQrData.redirectUrl) { + // Sanitize the redirect URL to prevent open redirect attacks + const sanitizedUrl = sanitizeRedirectURL(redirectQrData.redirectUrl) + + if (!sanitizedUrl) { + // Invalid or unsafe URL - redirect to a safe fallback + router.push('/home') + return + } + try { - const url = new URL(redirectQrData.redirectUrl) + const url = new URL(sanitizedUrl) // Check if external redirect (different domain) const isExternal = url.origin !== window.location.origin if (isExternal) { // External redirect - use full URL navigation - window.location.href = redirectQrData.redirectUrl + window.location.href = sanitizedUrl } else { // Internal redirect - extract path for Next.js router (better UX, no page reload) const invitePath = `${url.pathname}${url.search}` // e.g., /invite?code=XYZINVITESYOU router.push(invitePath) } } catch (error) { - // Fallback for invalid URLs - window.location.href = redirectQrData.redirectUrl + // Fallback to safe page for invalid URLs + router.push('/home') } return }Additionally, ensure backend validation is enforced as primary defense (restrict
targetUrlto allowed patterns, rejectjavascript:,data:,file:schemes).⛔ Skipped due to learnings
Learnt from: Zishan-7 Repo: peanutprotocol/peanut-ui PR: 1207 File: src/components/Global/PostSignupActionManager/post-signup-action.consts.ts:0-0 Timestamp: 2025-09-12T09:06:22.741Z Learning: In the peanut-ui codebase, redirect URLs are always stored as relative paths, never as absolute paths. This means regex patterns can safely anchor to `^\/` when matching stored redirect URLs.Learnt from: Zishan-7 Repo: peanutprotocol/peanut-ui PR: 1185 File: src/components/Claim/useClaimLink.tsx:14-0 Timestamp: 2025-09-05T07:31:11.396Z Learning: In the peanut-ui codebase, `window.history.replaceState` is preferred over `router.replace` when immediate/synchronous URL parameter updates are required, as `router.replace` is asynchronous and doesn't guarantee instant URL changes that subsequent code can rely on. This pattern is used consistently across usePaymentInitiator.ts, Confirm.payment.view.tsx, and useClaimLink.tsx.Learnt from: jjramirezn Repo: peanutprotocol/peanut-ui PR: 1190 File: src/app/(mobile-ui)/qr-pay/page.tsx:156-176 Timestamp: 2025-09-08T03:13:09.111Z Learning: In the peanut-ui mobile app, the `/qr-pay` route is only accessed through the DirectSendQR component which always includes the qrCode parameter in the URL when redirecting users to the QR pay page.
87-87: I need to examine how the authentication actually works in the QR code page to understand if this is dead code or if there's an alternative mechanism:
The original review comment is based on an incorrect premise. While the server automatically includes the httpOnly cookie in subsequent HTTP requests to the same domain, this cookie remains invisible to any JavaScript running on the page. The code review correctly configured the JWT cookie with
httpOnly: truein multiple server endpoints (e.g.,src/app/api/peanut/user/login-user/route.ts,src/app/api/peanut/user/register-user/route.ts), which means a cookie with the HttpOnly attribute can't be accessed by JavaScript—therefore the concern about XSS token extraction viaCookies.get('jwt-token')does not apply. The authentication token is secure and protected from XSS attacks through the httpOnly flag.Likely an incorrect or invalid review comment.
jjramirezn
left a comment
There was a problem hiding this comment.
Looking good, lets go
User-Tied QR Sheets
Branch:
feat/qr-sheetsPhysical sticker sheets with unique QR codes. First scan by authenticated user permanently claims the QR and redirects to their invite link.
Changes
Backend (
peanut-api-ts)Redirecttable:inputPath,targetUrl,claimedByUserId,claimedAtGET /qr/:code- Check claim status (public)POST /qr/:code/claim- Claim QR (authenticated, first-scan-wins)https://peanut.me/invite?code={USERNAME}INVITESYOUhttps://google.com)Frontend (
peanut-ui)/qr/[code]- Claim page with 1.5s hold-to-claim button (progressive shake/haptic)/qr/[code]/success- Success page with confetti and shareable QRuseHoldToClaimhook - Reusable hold patternuseRedirectQrStatushook - TanStack Query for status fetchingpeanut.me/qr/*URLs