Skip to content

chore: sync posthog changes to staging#1748

Open
kushagrasarathe wants to merge 100 commits intodevfrom
sync/main-to-dev
Open

chore: sync posthog changes to staging#1748
kushagrasarathe wants to merge 100 commits intodevfrom
sync/main-to-dev

Conversation

@kushagrasarathe
Copy link
Contributor

No description provided.

chip-peanut-bot bot and others added 30 commits March 9, 2026 12:17
…le-use

copy: clarify reference code is one-time use
copy: update landing page — YOUR MONEY. YOUR RULES.
- Change new user node color from blue to green to match legend
- Debounce topNodes slider (500ms) to prevent refetch on every tick
- Pass includeNewDays to backend so new users always appear regardless of topNodes filter
docs.peanut.me was redirecting to peanut.me (homepage) instead of
/en/help. Add host-based redirect to route docs subdomain traffic
to the help center.
…tent submodule

- SEOFooter: filter hardcoded link lists against actual COUNTRIES_SEO/
  COMPETITORS/EXCHANGES data so broken links never render
- deposit/compare/receive-money-from pages: remove React fallback
  rendering paths (all content now has MDX), require MDX or 404
- Delete dead code: ComparisonTable.tsx, ReceiveMoneyContent.tsx
- Update content submodule to include 32 new content files (8 country
  hubs, 8 send-to pages, 15 exchange deposit guides, 1 Chile entity)
Use displaySettingsRef to read current activityFilter.activityDays
so includeNewDays always reflects the latest value without adding
activityFilter to the useEffect dependency array.
fix: full-graph new users render green + debounce topNodes slider
Prevents FOUC where the page shell (hero + footer) renders
with a blank gap while HelpLanding client component hydrates.
The old JoinWaitlistPage gated the invite code input behind
isPermissionGranted, permanently trapping users who denied notifications.

New flow:
1. Email collection (required) — saved via updateUserById server action
2. Enable notifications (skippable) — soft prompt with "Not now" link
3. Jail screen — always shows invite code input regardless of notification state

Also adds a subtle notification banner on the jail screen for users who
skipped step 2.
…module

- generateMetadata() now checks for published MDX content before
  emitting canonical/hreflang tags (mirrors compare and deposit pages)
- Content submodule updated with CR fixes: diacritics, broken links,
  TBD placeholders
The previous commit replaced all docs.peanut.me links with /en/help
too aggressively — losing deep link paths and removing target="_blank"
from contextual links (tooltips, setup flows, modals, error views).

Navigation links (footer, sidebar) correctly stay as /en/help in same
tab. Contextual links now open in new tabs with proper article slugs:
- tooltips, limits → /en/help/transaction-limits
- passkey setup/test → /en/help/passkeys
- payment error → /en/help/request-money
- regulated rails → /en/help/transaction-limits
- early user modal → /en/help (new tab)
Footer.tsx is imported by client-routed pages (e.g. /exchange), so
importing @/data/seo (which depends on fs via @/lib/content) breaks
the webpack build. Revert to inlined data with all 8 countries that
now have published content. Updated comment explains the constraint.
Remove duplicated testing/formatting sections — that info now lives
in CONTRIBUTING.md. README stays focused on setup and project context.
Signed-off-by: Hugo Montenegro <hugo@peanut.to>
Add CONTRIBUTING.md as single source of truth for dev rules
Previous PR gitignored the symlinks, meaning devs had to create them
manually. Now CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, and
.github/copilot-instructions.md are tracked symlinks pointing to
CONTRIBUTING.md.
- validateInviteCode: wrap in try/finally so isLoading always clears
- handleLogout: wrap in try/finally so isLoggingOut always clears
- Email input: add aria-label for accessibility
- Step 1: email collection (required, server action)
- Step 2: enable notifications (skippable)
- Step 3: jail screen with invite code input (always accessible)

Uses useQueryState from nuqs for step management, matching the
codebase convention for multi-step flows. Step survives refresh,
supports deep-linking, and reacts to auth state changes via useEffect.

Fixes bug where users got permanently stuck on "Enable notifications"
screen when denying permission.
Hugo0 and others added 26 commits March 10, 2026 22:07
…events

refactor: migrate InviteFriendsModal to unified modal tracking
Static PNG frames extracted from existing peanut character GIFs
(konradurban_01 and konradurban_02) for use in OneSignal email
templates. Used in jail and welcome email flows.
feat: add email template assets for waitlist notifications
Changes per LP audit (Notion):
- Move stats fold (YourMoney) up after MercadoPago/PIX fold
- Hero: TAP.SEND → TAP.SCAN, cities → Buenos Aires/São Paulo/Mexico City, add "No local ID" line
- Waitlist subtext → "Join +10,000 happy users"
- Add "Works with" label above bank/partner logo marquee
- Add sticky mobile CTA bar (hides near existing CTAs)
- Curate Wall of Love to 11 shortlisted tweets per audit
…weets

Changes:
- Replace hero SVG image with HTML using knerd font (Title component)
- Hero subtitle: RECEIVE. PAY. ANYWHERE.
- Waitlist subtext: cool people (not happy users)
- MercadoPago fold: new copy per Notion audit
- Wall of Love: expanded from 11 to 17 curated tweets
- Replace SVG with new "GLOBAL CASH LOCAL FEEL" PNG from Konrad
- Subtitle: TAP. SCAN. ANYWHERE.
- Cities: Buenos Aires. São Paulo. Floripa.
- Add "No local id or bank required" tiny line
…(624:276)

The new PNG has a different aspect ratio. Wrapping in a container with
the original 624:276 ratio + object-fit:contain keeps the peanut mascot
overlap balanced across all breakpoints.
…bottom

- Renamed GET PEANUT → SIGN UP NOW
- Hide bar at page bottom to prevent scroll-back blocking
- Added pointer-events-none on container so touch events pass through
- Only show after scrolling past hero (300px)
- New PNG from Konrad matching original dimensions
- Subtitle: TAP. SCAN. ANYWHERE.
- Cities: Buenos Aires. São Paulo. Floripa.
- No local ID or bank required.
- PeanutMascot component measures h2 position and places itself
  so only 3% of its height (feet) overlaps the subtitle
- Subtitle: TAP. SCAN. ANYWHERE.
- Cities: Buenos Aires. São Paulo. Floripa.
- No local ID or bank required.
- Fix StickyMobileCTA flickering: add rAF throttle, change guard
  (lastVisible ref), and increase atBottom threshold from 20→100px
  for momentum scroll
- Use variant="purple" on sticky CTA button per design system
- Remove unused useState import from hero.tsx
- Fix PeanutMascot comment (3% → 6% to match code)
- Remove trailing space in RegulatedRails className
Show after hero, hide at page bottom. No more hiding near section CTAs —
that logic caused the flickering.
All 77 tweets preserved with visible: true/false. Only 17 visible ones
shown in carousel. Hidden tweets can be re-enabled by flipping the flag.
feat: landing page audit — reorder, copy, sticky CTA, curate tweets
@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Mar 13, 2026 3:02pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

Walkthrough

This pull request introduces comprehensive PostHog analytics instrumentation throughout the application, centralizes project contribution guidelines into CONTRIBUTING.md, migrates external documentation links to internal help routes, refactors marketing pages to use MDX-based content rendering, updates landing page UI and messaging, and adds PostHog configuration (environment variables, reverse proxy, instrumentation client).

Changes

Cohort / File(s) Summary
Documentation & Guidelines
CONTRIBUTING.md, .cursorrules, .windsurfrules, .claude.md, AGENTS.md, CLAUDE.md, README.md
New CONTRIBUTING.md consolidates development guidelines, Git workflows, and code standards. AI tool configuration files reference it. README sections for formatting and testing replaced with concise Contributing reference.
PostHog Analytics Infrastructure
package.json, .env.example, instrumentation-client.ts, next.config.js, src/constants/analytics.consts.ts
Added posthog-js dependency and environment variables (NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST). New instrumentation-client.ts initializes PostHog in browsers. Next.js config adds reverse proxy rewrites for /ingest routes. New analytics.consts.ts defines centralized event taxonomy (ANALYTICS_EVENTS, MODAL_TYPES).
Configuration & Ignores
.gitignore, .prettierignore, src/constants/chains.consts.ts
Added .claude/worktrees/ to gitignore. Added src/content/ to prettierignore. Added instructional comment for adding chains.
Route Management & Redirects
redirects.json, scripts/validate-links.ts, src/constants/routes.ts, src/app/[...recipient]/page.tsx
New redirect for docs.peanut.me to /en/help. Added /help routes to link validation. New isLocaleSegment helper and isReservedRoute function for locale-aware route detection. Refactored reserved route checks to use new function.
Analytics Instrumentation - Deposit/Withdraw Flows
src/app/(mobile-ui)/add-money/*, src/app/(mobile-ui)/withdraw/*, src/components/AddMoney/components/*, src/components/AddWithdraw/AddWithdrawRouterView.tsx
Integrated PostHog analytics tracking across deposit/withdrawal flows: DEPOSIT_AMOUNT_ENTERED, DEPOSIT_CONFIRMED, DEPOSIT_FAILED, WITHDRAW_CONFIRMED, WITHDRAW_COMPLETED, WITHDRAW_FAILED events with relevant context (amount, method, country).
Analytics Instrumentation - Send/Claim Flows
src/components/Send/link/views/*, src/components/Claim/Link/*, src/hooks/useZeroDev.ts
Added analytics for send link creation/sharing (SEND_LINK_CREATED, SEND_LINK_SHARED, SEND_METHOD_SELECTED) and claim flows (CLAIM_LINK_VIEWED, CLAIM_LINK_STARTED, CLAIM_LINK_COMPLETED, CLAIM_LINK_FAILED, CLAIM_RECIPIENT_SELECTED).
Analytics Instrumentation - User Flows & Modals
src/components/Setup/Views/*, src/components/Home/KycCompletedModal/index.tsx, src/components/Global/..., src/components/Card/CardPioneerModal.tsx, src/components/Notifications/SetupNotificationsModal.tsx, src/hooks/useMultiPhaseKycFlow.ts, src/hooks/useNotifications.ts, src/context/authContext.tsx
Analytics tracking for signup flow (SIGNUP_CLICKED, SIGNUP_LOGIN_ERROR, SIGNUP_CREATE_WALLET_CLICKED), PWA installation (PWA_INSTALL_CLICKED, PWA_INSTALL_COMPLETED, PWA_INSTALL_DISMISSED), KYC flow variants, notification permissions, modal lifecycle (MODAL_SHOWN, MODAL_DISMISSED, MODAL_CTA_CLICKED), and user identification on login/logout.
Analytics Instrumentation - Pages & Interactions
src/app/(mobile-ui)/home/page.tsx, src/app/(mobile-ui)/points/page.tsx, src/components/Invites/InvitesPage.tsx, src/features/payments/shared/components/PaymentSuccessView.tsx, src/components/Global/DirectSendQR/index.tsx, src/components/Global/BackendErrorScreen/index.tsx
Analytics for page views, balance visibility toggle, invite interactions (INVITE_PAGE_VIEWED, INVITE_CLAIM_CLICKED, INVITE_ACCEPTED), points earned, QR scanning, and backend error handling.
Documentation Links Migration
src/components/Global/Footer/consts.ts, src/components/LandingPage/Footer.tsx, src/components/LandingPage/RegulatedRails.tsx, src/constants/tooltips.ts, src/features/limits/components/LimitsDocsLink.tsx, src/components/Payment/Views/Error.validation.view.tsx, src/components/Setup/Views/SetupPasskey.tsx, src/components/Setup/Views/SignTestTransaction.tsx
Replaced external docs.peanut.me URLs with internal /en/help/* paths throughout footer, tooltips, and component links. Removed external rel attributes where appropriate.
Locale-aware Breadcrumbs
src/app/[locale]/(marketing)/*/page.tsx (blog, help, compare, deposit, receive-money-from, send-money-from, send-money-to, pay-with)
Updated home breadcrumb links from '/' to /${locale} for locale-specific routing consistency across marketing pages.
Help Landing & Page Structure
src/app/[locale]/(marketing)/help/page.tsx, src/app/[locale]/(marketing)/help/[slug]/page.tsx
Added HelpLandingSkeleton fallback for Suspense. Updated breadcrumbs to locale-aware paths. Updated home link handling.
Content Pages Refactoring - Compare & Deposit
src/app/[locale]/(marketing)/compare/[slug]/page.tsx, src/app/[locale]/(marketing)/deposit/[exchange]/page.tsx
Replaced React component rendering with MDX-based ContentPage rendering. Removed fallback metadata generation; now relies solely on MDX frontmatter. Introduced deposit slug parsing (parseDepositSlug, resolveDeposit) mapping exchange/rail slugs to structured deposit objects. Removed old Marketing components (MarketingHero, ComparisonTable, etc.).
Content Pages Refactoring - Receive Money
src/app/[locale]/(marketing)/receive-money-from/[country]/page.tsx
Removed ReceiveMoneyContent import. Replaced with MDX-based rendering via ContentPage. Updated to locale-based breadcrumbs.
Deposit Data & Sitemap
src/data/seo/exchanges.ts, src/data/seo/index.ts, src/app/sitemap.ts
New DEPOSIT_RAILS constant mapping rails to display names (fiat methods + crypto networks). Re-exported from seo/index.ts. Updated sitemap.ts to generate deposit routes for all rails via /${locale}/deposit/via-${rail} pattern.
Landing Page UI Updates
src/components/LandingPage/hero.tsx, src/components/LandingPage/Manteca.tsx, src/components/LandingPage/LandingPageClient.tsx, src/components/LandingPage/StickyMobileCTA.tsx, src/components/LandingPage/landingPageData.ts, src/components/LandingPage/SEOFooter.tsx
New StickyMobileCTA component for mobile bottom sticky CTA. Updated hero heading to "TAP. SCAN. ANYWHERE." Updated Manteca copy and RegulatedRails branding. Changed hero asset to GlobalCashLocalFeel with mascot positioning. Updated hero CTA subtext. Reordered page sections. Added HeroConfig.primaryCta.subtext update.
Modal & Component API Updates
src/components/Global/InviteFriendsModal/index.tsx, src/components/Card/CardSuccessScreen.tsx, src/components/Profile/index.tsx, src/components/Global/CopyToClipboard/index.tsx
Added source prop to InviteFriendsModal for analytics context. Updated call sites (CardSuccessScreen, Profile, PointsPage) to pass source. Added onCopy callback to CopyToClipboard component.
Complex Component Refactoring
src/components/Invites/JoinWaitlistPage.tsx, src/components/Global/InvitesGraph/index.tsx, src/app/(mobile-ui)/dev/full-graph/page.tsx
JoinWaitlistPage: refactored to multi-step flow (email → notifications → jail) with URL-backed state and analytics integration. InvitesGraph: added visual status filtering via hiddenStatuses state and renderOverlays API extension; added debounced data refetch; threaded hiddenStatuses through rendering logic. DevGraph: replaced static legend with clickable toggles for status visibility.
Component Additions & Updates
src/components/Global/BalanceWarningModal/index.tsx, src/components/Global/EarlyUserModal/index.tsx, src/components/Global/NoMoreJailModal/index.tsx, src/components/Card/CardPioneerModal.tsx, src/components/Marketing/ComparisonTable.tsx, src/components/Marketing/pages/ReceiveMoneyContent.tsx
Added modal analytics tracking (MODAL_SHOWN, MODAL_DISMISSED, MODAL_CTA_CLICKED). EarlyUserModal: changed docs link to /en/help. ComparisonTable and ReceiveMoneyContent: deleted (migrated to MDX). CardPioneerModal: added analytics tracking.
Utility & Service Updates
src/utils/format.utils.ts, src/utils/withdraw.utils.ts, src/services/points.ts
New isValidEmail utility function. Updated validatePixKey to use isValidEmail. Extended getInvitesGraph to support includeNewDays option for recent signups in graph queries.
Tweets & Assets
src/constants/tweets.consts.ts, src/constants/tweets.json, src/assets/illustrations/index.ts
Added visible field to Tweet interface; filtered TWEETS to exclude invisible ones. Updated tweets.json with visibility flags and curated shortlist note. New GlobalCashLocalFeel illustration export.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning No description was provided by the author; the PR description field is empty, making it impossible to evaluate relevance to the changeset. Add a pull request description explaining the PostHog integration, the scope of analytics events added, and any breaking changes or migration notes for reviewers.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore: sync posthog changes to staging' accurately describes the primary change: integrating PostHog analytics throughout the codebase and synchronizing branch changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sync/main-to-dev
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/components/Setup/Views/InstallPWA.tsx (1)

78-96: ⚠️ Potential issue | 🟡 Minor

Add deviceType to dependency arrays to prevent stale closures in analytics captures.

Lines 78-96 and 124-143 reference deviceType in event handlers but omit it from their dependency arrays. This causes the callbacks to capture the value at creation time, potentially logging outdated or undefined device_type.

Suggested fix
-    }, [])
+    }, [deviceType])

At line 96, and:

-    }, [deferredPrompt, toast])
+    }, [deferredPrompt, toast, deviceType])

At line 143.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Setup/Views/InstallPWA.tsx` around lines 78 - 96, The
useEffect callbacks (notably the handler handleAppInstalled that calls
posthog.capture(ANALYTICS_EVENTS.PWA_INSTALL_COMPLETED, { device_type:
deviceType }) and the other event handler later that also references deviceType)
currently close over a stale deviceType; add deviceType to the dependency arrays
of those useEffect hooks so the handlers are recreated when deviceType changes
and will log the correct device_type. Locate the useEffect that defines
handleAppInstalled and the other effect around the later event handler (they
reference setInstallComplete, setIsInstallInProgress, setInstallCancelled and
call posthog.capture) and include deviceType in each dependency array. Ensure
you only add deviceType (no other state changes) to avoid unintended behavior.
src/components/Card/CardSuccessScreen.tsx (1)

135-140: ⚠️ Potential issue | 🟡 Minor

Use a card-specific analytics source here.

This screen is the card reservation success step, so card_deposit_success looks like a copied source label and will misclassify the invite-modal events from this funnel.

🏷️ Example rename
-                source="card_deposit_success"
+                source="card_reservation_success"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Card/CardSuccessScreen.tsx` around lines 135 - 140, The
InviteFriendsModal is using a generic source prop "card_deposit_success" which
mislabels analytics for this card reservation success screen; update the source
prop on the InviteFriendsModal instance in CardSuccessScreen (the
InviteFriendsModal call with props visible, onClose, username, source) to a
card-specific label such as "card_reservation_success" (or whatever the
project's naming convention requires) so invite-modal events are correctly
attributed to this funnel.
src/hooks/useNotifications.ts (1)

191-240: ⚠️ Potential issue | 🔴 Critical

OneSignal listeners capture stale closures across hook instance remounts.

The __ONE_SIGNAL_LISTENERS_ADDED__ singleton pattern registers callbacks once, but those callbacks capture refreshPermissionState(), evaluateVisibility(), and the setShowPermissionModal setter from the first hook instance. Because the useEffect has no cleanup and the listener registration never re-runs even when evaluateVisibility or refreshPermissionState change, any subsequent mount of useNotifications() will have its state updates ignored by the OneSignal listeners.

This is a real issue: the hook is consumed across multiple pages/components (SetupNotificationsModal, JoinWaitlistPage, Home page, useHomeCarouselCTAs), and during navigation, the hook will be unmounted from one component and remounted in another. The new instance's state (showPermissionModal, etc.) will never be updated by OneSignal events because the listeners are stuck calling the old instance's (unmounted) setters.

Move listener registration outside the hook using a custom initialization module or OneSignal context provider to ensure listeners update the correct instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useNotifications.ts` around lines 191 - 240, The OneSignal
listeners registered under w.__ONE_SIGNAL_LISTENERS_ADDED__ capture stale
closures (refreshPermissionState, evaluateVisibility, setShowPermissionModal)
from the first hook instance and never update on remount; fix by moving listener
registration out of the hook into a shared initializer or context that wires
listeners to stable refs/handlers, or alternatively re-register with cleanup on
each mount while using refs for mutable callbacks (keep externalIdRef and
disableExternalIdLoginRef usage), and ensure the
OneSignal.User.PushSubscription.addEventListener and
OneSignal.Notifications.addEventListener handlers call the latest functions via
refs or are registered by a central init that accepts/upserts current callbacks
so new hook instances get correct behavior.
🧹 Nitpick comments (10)
src/app/[locale]/(marketing)/compare/[slug]/page.tsx (1)

27-37: Consider extracting shared route/content resolution to avoid drift.

locale/slug validation + competitor lookup + MDX availability checks are duplicated across metadata and page rendering. A small shared helper would reduce future divergence risk.

♻️ Suggested refactor sketch
+type ResolvedComparison = {
+  locale: string
+  slug: string
+  competitor: (typeof COMPETITORS)[string]
+  mdx: { body: string; frontmatter: ContentFrontmatter }
+}
+
+async function resolveComparison(params: PageProps['params']): Promise<ResolvedComparison | null> {
+  const { locale, slug: rawSlug } = await params
+  if (!isValidLocale(locale)) return null
+  const slug = parseSlug(rawSlug)
+  if (!slug) return null
+  const competitor = COMPETITORS[slug]
+  if (!competitor) return null
+  const mdx = readPageContentLocalized<ContentFrontmatter>('compare', slug, locale)
+  if (!mdx || mdx.frontmatter.published === false) return null
+  return { locale, slug, competitor, mdx }
+}

Also applies to: 53-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/(marketing)/compare/[slug]/page.tsx around lines 27 - 37,
Extract the repeated locale/slug/competitor/mdx resolution logic into a shared
helper (e.g., resolveCompareRoute or getCompareData) and call it from
generateMetadata and the page renderer; the helper should accept params, call
isValidLocale and parseSlug, resolve COMPETITORS[slug], and invoke
readPageContentLocalized<ContentFrontmatter>('compare', slug, locale), returning
a single unified result object or null for early exits so both generateMetadata
and the page component use the same checks and avoid drift.
scripts/validate-links.ts (1)

109-113: Register bare help routes once outside the locale loop.

This works as-is, but Lines 109 and 112 re-add the same non-locale paths on every locale iteration. Moving them outside the loop reduces repeated work and makes intent clearer.

♻️ Suggested cleanup
+    // Bare help routes (non-locale) used in content links
+    paths.add('/help')
+    for (const slug of helpSlugs) {
+        paths.add(`/help/${slug}`)
+    }
+
     for (const locale of SUPPORTED_LOCALES) {
@@
-        paths.add('/help')
         for (const slug of helpSlugs) {
             paths.add(`/${locale}/help/${slug}`)
-            paths.add(`/help/${slug}`)
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/validate-links.ts` around lines 109 - 113, The code is re-adding
non-locale help routes on every iteration of the locale loop; move the
registrations of the bare routes so they run once outside the loop: add
paths.add('/help') and for each slug in helpSlugs add paths.add(`/help/${slug}`)
before entering the loop that iterates over locale (the loop that uses the
locale variable and also adds `/${locale}/help/${slug}`), leaving the per-locale
additions (paths.add(`/${locale}/help/${slug}`)) inside the loop. Ensure you
reference the existing paths and helpSlugs variables and keep the
locale-specific additions unchanged.
.gitignore (1)

81-82: Redundant ignore entry can be removed.

.claude/ at Line 79 already ignores /.claude/worktrees/, so this extra rule is a no-op.

🧹 Minimal cleanup
-# AI tool worktrees
-.claude/worktrees/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 81 - 82, Remove the redundant gitignore entry
".claude/worktrees/" from .gitignore because the broader ".claude/" rule already
covers it; locate the ".claude/worktrees/" line and delete it so the file only
keeps the parent ".claude/" ignore entry.
src/utils/format.utils.ts (1)

112-112: Use validator.isEmail() instead of hand-rolled regex.

The regex on line 112 is overly permissive and can miss invalid email edge cases. Since validator is already a project dependency, import isEmail from it for a more robust and maintainable solution.

✅ Suggested change
+import isEmail from 'validator/lib/isEmail'
@@
-export const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
+export const isValidEmail = (email: string): boolean => isEmail(email.trim())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/format.utils.ts` at line 112, Replace the hand-rolled regex in
isValidEmail with validator's robust implementation: import isEmail from
'validator' (or import { isEmail } from 'validator' depending on your bundler)
and change the isValidEmail implementation (export const isValidEmail) to call
isEmail(email). Ensure the new import is added at the top of the module and the
function signature remains (email: string) => boolean.
CONTRIBUTING.md (1)

136-137: Minor: Capitalize for consistency with other headings.

The last bullet point starts with lowercase, breaking the pattern established throughout the document.

✏️ Suggested fix
 - **Be descriptive** — focus on the "why", not the "what".
-- follow conventional commits.
+- **Follow conventional commits** — use types like `feat:`, `fix:`, `chore:`, etc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` around lines 136 - 137, Update the inconsistent bullet
casing by capitalizing the first letter of the last list item in
CONTRIBUTING.md: change "- follow conventional commits." to start with an
uppercase letter (e.g., "- Follow conventional commits.") so it matches the
capitalization pattern used by the other bullets.
src/components/Global/Footer/consts.ts (1)

19-23: Consider updating the icon to match the new "Help" branding.

The entry was renamed from 'gitbook' to 'Help', but still uses GITBOOK_ICON. While functional, the icon may not visually align with "Help" semantics for users.

If a help/question-mark icon exists in the icon set, consider using it instead for better visual consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Global/Footer/consts.ts` around lines 19 - 23, The 'Help'
footer item still uses GITBOOK_ICON; replace it with the matching help icon
constant (e.g., HELP_ICON or QUESTION_ICON) from the icons set so the visual
matches the renamed entry—update the object in consts.ts where the footer item
with name 'Help' is defined (change logoSrc: icons.GITBOOK_ICON.src to logoSrc:
icons.HELP_ICON.src or icons.QUESTION_ICON.src) and ensure the chosen icon
constant exists and is imported/available in the icons module.
src/constants/tooltips.ts (1)

6-6: Consider locale-aware links and add rel="noopener" for security.

Two observations:

  1. The /en/help/... path is hardcoded. Users viewing the app in other locales will be directed to English help content.
  2. Links with target="_blank" should include rel="noopener" to prevent the new page from accessing window.opener.
✏️ Suggested fix for security
-<a href="/en/help/supported-geographies" target="_blank" class="underline text-blue-600">Supported regions</a>`,
+<a href="/en/help/supported-geographies" target="_blank" rel="noopener" class="underline text-blue-600">Supported regions</a>`,

The hardcoded locale appears intentional given similar patterns elsewhere in this PR. If locale-aware help links are desired in the future, consider making the locale dynamic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/constants/tooltips.ts` at line 6, The anchor HTML string '<a
href="/en/help/supported-geographies" target="_blank" class="underline
text-blue-600">Supported regions</a>' should include rel="noopener" for security
and, if you want locale-aware links, avoid the hardcoded "/en/" — either inject
the current locale variable (e.g. replace "/en/" with `/${locale}/`) or build
the URL via a helper like getHelpUrl(locale, 'supported-geographies'); update
the string in src/constants/tooltips.ts (the literal anchor) to add
rel="noopener" and optionally parameterize the locale or call the helper so the
link can be locale-aware.
src/components/Setup/Views/Welcome.tsx (1)

77-90: Consider adding analytics for the Login button.

The "Log In" button doesn't capture an analytics event. Given that SIGNUP_CLICKED or a similar event exists in ANALYTICS_EVENTS, you may want to track login attempts for funnel analysis.

♻️ Optional: Add login click tracking
 <Button
     loading={isLoggingIn}
     shadowSize="4"
     disabled={isLoggingIn}
     className="h-11"
     variant="primary-soft"
     onClick={() => {
+        posthog.capture(ANALYTICS_EVENTS.SIGNUP_CLICKED)
         handleLogin().catch((e) => {
             handleError(e)
         })
     }}
 >
     Log In
 </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Setup/Views/Welcome.tsx` around lines 77 - 90, Add an
analytics event when the Log In button is clicked by invoking the app's tracking
API (e.g., trackEvent or analytics.track) with the appropriate constant from
ANALYTICS_EVENTS (e.g., ANALYTICS_EVENTS.LOGIN_CLICKED) inside the Button
onClick before calling handleLogin; update the onClick handler that currently
calls handleLogin().catch(handleError) to first fire the analytics event (and
swallow any tracking errors) then proceed to call handleLogin and preserve the
existing .catch(handleError) behavior so login errors are still handled.
src/hooks/useZeroDev.ts (1)

112-115: Consider sanitizing error messages before sending to analytics.

String(e) may include stack traces or internal error details that could expose implementation details. Consider extracting just the error message or using a sanitized error code.

🔧 Suggested improvement
                 } catch (e) {
+                    const errorMessage = e instanceof Error ? e.message : 'Unknown error'
                     posthog.capture(ANALYTICS_EVENTS.INVITE_ACCEPT_FAILED, {
                         invite_code: userInviteCode,
-                        error_message: String(e),
+                        error_message: errorMessage,
                     })
                     console.error('Error accepting invite', e)
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useZeroDev.ts` around lines 112 - 115, The analytics call using
posthog.capture for ANALYTICS_EVENTS.INVITE_ACCEPT_FAILED currently sends
String(e) which can include stack traces or sensitive internals; update the
capture payload to send a sanitized error value (e.g., if e is an Error use
e.message, otherwise map to a safe error code or truncated/scrubbed string) and
avoid including stack or full error objects; change the code around the
posthog.capture call in useZeroDev.ts (the block where invite_code =
userInviteCode is set) to compute a safeError variable and pass that instead.
src/components/Setup/Views/SetupPasskey.tsx (1)

136-143: Consider using Next.js Link for internal navigation.

The /en/help/passkeys path appears to be an internal route. Using a plain <a> tag with target="_blank" works, but if this is an internal help page, you lose Next.js prefetching benefits. However, since target="_blank" opens a new tab anyway, this is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Setup/Views/SetupPasskey.tsx` around lines 136 - 143, The
anchor in SetupPasskey.tsx uses a plain <a> for an internal route; replace it
with Next.js Link to get prefetching and proper client-side navigation: import
Link from 'next/link' in the SetupPasskey component and change the <a
href="/en/help/passkeys" ...> to <Link href="/en/help/passkeys" ...> (preserve
className, target="_blank" and rel="noopener noreferrer" if you still want a new
tab) so the route uses Next.js navigation while keeping the same styling and
attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@instrumentation-client.ts`:
- Around line 3-11: The code calls posthog.init with
process.env.NEXT_PUBLIC_POSTHOG_KEY using a non-null assertion; instead add a
runtime guard before calling posthog.init to ensure the key is defined (when
typeof window !== 'undefined' and process.env.NODE_ENV !== 'development'), e.g.,
read const key = process.env.NEXT_PUBLIC_POSTHOG_KEY and if (!key) skip
initialization and emit a clear warning/error via console.warn or a logger; only
call posthog.init(key, {...}) when key is present so posthog.init is never
invoked with undefined.

In `@next.config.js`:
- Line 98: The config option skipTrailingSlashRedirect in next.config.js is
turning off Next's automatic trailing-slash normalization, which can cause
duplicate crawlable URLs; either remove skipTrailingSlashRedirect: true from
next.config.js to restore default behavior, or add a custom Middleware (e.g.,
middleware.js / middleware.ts) that inspects request.nextUrl.pathname and issues
a permanent redirect via NextResponse.redirect to a canonical URL (consistently
with or without trailing slash), handling root and asset paths and preserving
query strings; implement the Middleware logic and enable it instead of leaving
skipTrailingSlashRedirect enabled.

In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 239-243: Don't send raw exception text to analytics: replace the
direct use of error.message when calling posthog.capture for
ANALYTICS_EVENTS.DEPOSIT_FAILED with a sanitized/category value. Update the code
around errorMessage and the posthog.capture call so it maps known Error
instances (or error.message) to a small set of normalized codes or categories
(e.g., "validation_error", "network_error", "timeout", "unknown_error") or run
the message through a sanitize function that strips sensitive details before
sending; use that sanitizedCode or category in the error_message/error_code
field passed to posthog.capture (refer to errorMessage, posthog.capture, and
ANALYTICS_EVENTS.DEPOSIT_FAILED).

In `@src/app/`(mobile-ui)/withdraw/crypto/page.tsx:
- Around line 298-301: Replace sending raw error text in the posthog payload:
instead of passing errMsg into posthog.capture(ANALYTICS_EVENTS.WITHDRAW_FAILED,
...), derive and send a normalized error identifier (e.g., error_code or
error_type) and any safe boolean flags (e.g., provider_timeout: true) for
analytics; keep the full errMsg only in Sentry/logging calls (e.g.,
captureException or processLogger) for diagnostics. Update all similar calls in
the withdraw flow that use posthog.capture to follow this pattern and reference
the posthog.capture and ANALYTICS_EVENTS.WITHDRAW_FAILED usages as the locations
to change.

In `@src/components/AddMoney/components/MantecaAddMoney.tsx`:
- Around line 154-159: The analytics event is sending usdAmount as a string;
update the posthog.capture calls (the one using
ANALYTICS_EVENTS.DEPOSIT_AMOUNT_ENTERED and the similar call around the other
block) to pass a numeric value by converting usdAmount (component state) to a
number (e.g., Number(usdAmount) or parseFloat(usdAmount)) and handle NaN
fallback if necessary so amount_usd is always a numeric type while leaving other
properties (method_type, country/selectedCountryPath,
denomination/currentDenomination) unchanged.

In `@src/components/Global/InviteFriendsModal/index.tsx`:
- Around line 29-35: The show event is only sent once because hasTrackedShow
(useRef) is never reset; update the effect (or add a second effect watching
visible) so that when visible becomes false you set hasTrackedShow.current =
false, ensuring the next time visible turns true the posthog.capture call in the
useEffect (which checks hasTrackedShow, ANALYTICS_EVENTS.MODAL_SHOWN,
MODAL_TYPES.INVITE and source) will run again.

In `@src/components/Global/InvitesGraph/index.tsx`:
- Around line 863-868: The initial invites fetch uses
displaySettingsRef.current.activityFilter.activityDays but the fetch useEffect
(which checks isInitialFetchRef.current and dependencies [isMinimal, !isMinimal
&& props.apiKey, mode, topNodes]) omits activityFilter so saved preferences can
be overwritten by the default; update the effect to either include the
activityFilter value (e.g.
displaySettingsRef.current.activityFilter.activityDays or the state variable
that tracks it) in the dependency array so the effect re-runs when preferences
load, or gate the initial fetch behind a preferencesLoaded flag (set when
preferences restoration completes) and check that flag inside the effect before
performing the API call (affecting the getInvitesGraph call that uses
includeNewDays and the isInitialFetchRef logic).

In `@src/components/Global/NoMoreJailModal/index.tsx`:
- Around line 14-17: The onClose handler is being used as the generic modal
close handler but also emits MODAL_CTA_CLICKED, causing non-CTA dismisses to be
counted; remove the posthog.capture call from onClose so it only performs
setisOpen(false) and sessionStorage.removeItem('showNoMoreJailModal'), and add a
new CTA-specific handler (e.g., handleStartUsingClick or onStartUsingClick) that
calls posthog.capture(ANALYTICS_EVENTS.MODAL_CTA_CLICKED, { modal_type:
MODAL_TYPES.POST_SIGNUP, cta: 'start_using' }) and then closes the modal by
invoking setisOpen(false) and sessionStorage.removeItem('showNoMoreJailModal');
update the CTA button to use this new handler and check other instances
mentioned (the similar capture at the second occurrence around the component
where lines 55-56 are referenced) to ensure only CTA clicks fire the analytics
event.

In `@src/components/Invites/InvitesPage.tsx`:
- Around line 60-70: hasTrackedPageView is never reset so switching query ?code=
values only logs the first invite; modify the logic to reset the guard whenever
the inviteCode changes by tracking the last seen code and clearing the flag for
new codes: update the useEffect(s) around hasTrackedPageView/ inviteCode/
inviteCodeData so that when inviteCode differs from the stored value you set
hasTrackedPageView.current = false (or store inviteCode in the ref and compare)
before the capture block, ensuring the capture in the existing useEffect fires
for each distinct inviteCode; refer to hasTrackedPageView, useEffect,
inviteCode, and inviteCodeData to locate where to add the reset.

In `@src/components/LandingPage/RegulatedRails.tsx`:
- Around line 64-69: The anchor in RegulatedRails uses a hard-coded
"/en/help/supported-geographies" path which forces English; update the link to
use the centralized route constant or locale-aware path builder instead (e.g.
ROUTES.HELP_SUPPORTED_GEOGRAPHIES or a helper like
buildLocalizedPath/withLocale) and obtain the current locale via the app's
locale hook or router (e.g. useRouter()/useLocale()) so the href is constructed
dynamically; modify the <a> (or Link) in RegulatedRails to reference that
constant/helper instead of the literal "/en/..." string.

In `@src/components/LandingPage/StickyMobileCTA.tsx`:
- Around line 46-49: In StickyMobileCTA, remove the outer anchor wrapper around
the Button to avoid nesting interactive elements; instead make the Button itself
handle navigation (e.g., add an onClick that routes to "/setup" or use the app's
Link/Router API to render the Button as an anchor if Button supports an
href/asLink prop). Update the Button usage in StickyMobileCTA so it is the sole
interactive element (refer to the Button component and StickyMobileCTA) and
ensure keyboard focus and a11y semantics are preserved.

In `@src/components/Setup/Views/SignTestTransaction.tsx`:
- Around line 196-203: The help link is hardcoded to /en/help/passkeys and
should be locale-aware: update SignTestTransaction (and the exported
PasskeyDocsLink) to accept a locale prop and build the href as
`/${locale}/help/passkeys` (or have the parent pass the correct localized path),
and update callers (including SetupPasskey) to forward the locale;
alternatively, if the setup flow must remain English-only, add a clear code
comment in SignTestTransaction and PasskeyDocsLink explaining the intentional
design choice and why locale is not used.

In `@src/constants/routes.ts`:
- Around line 147-162: Update the comment above
LOCALE_WITH_SUBTAG/isLocaleSegment to accurately state that DEDICATED_ROUTES
does not actually reserve bare 2-3 letter locale codes (so don’t claim “en, es,
pt” are listed there); note instead that DEDICATED_ROUTES contains non-locale
routes like 'qr', 'api', 'setup', etc., and clarify that username validation
uses underscores ([a-z0-9_]) so hyphenated locale variants (e.g., 'pt-br') won’t
collide with valid usernames; adjust the text near LOCALE_WITH_SUBTAG and the
isLocaleSegment docstring to reflect this accurately.

In `@src/features/limits/components/LimitsDocsLink.tsx`:
- Line 4: The link in the LimitsDocsLink component is hard-coded to
"/en/help/transaction-limits"; update the href to be locale-aware by deriving
the current locale (e.g., use Next.js router.locale or the app's i18n helper) or
by using a locale-agnostic path ("/help/transaction-limits") and/or passing
locale to the Link component so the route preserves the user's locale; modify
the href usage in LimitsDocsLink accordingly.

In `@src/services/points.ts`:
- Around line 316-318: The code currently serializes options.includeNewDays
blindly allowing non-integer positive numbers; update the validation in the
function that builds query params (the branch that checks
options?.includeNewDays and calls params.set) to only accept finite integers > 0
(e.g., Number.isInteger-like check) and otherwise omit the param or
coerce/normalize (choose to omit invalid values) before calling
params.set('includeNewDays', ...); ensure you reference options.includeNewDays
and the params.set call so invalid floats like 1.5 are not serialized.

---

Outside diff comments:
In `@src/components/Card/CardSuccessScreen.tsx`:
- Around line 135-140: The InviteFriendsModal is using a generic source prop
"card_deposit_success" which mislabels analytics for this card reservation
success screen; update the source prop on the InviteFriendsModal instance in
CardSuccessScreen (the InviteFriendsModal call with props visible, onClose,
username, source) to a card-specific label such as "card_reservation_success"
(or whatever the project's naming convention requires) so invite-modal events
are correctly attributed to this funnel.

In `@src/components/Setup/Views/InstallPWA.tsx`:
- Around line 78-96: The useEffect callbacks (notably the handler
handleAppInstalled that calls
posthog.capture(ANALYTICS_EVENTS.PWA_INSTALL_COMPLETED, { device_type:
deviceType }) and the other event handler later that also references deviceType)
currently close over a stale deviceType; add deviceType to the dependency arrays
of those useEffect hooks so the handlers are recreated when deviceType changes
and will log the correct device_type. Locate the useEffect that defines
handleAppInstalled and the other effect around the later event handler (they
reference setInstallComplete, setIsInstallInProgress, setInstallCancelled and
call posthog.capture) and include deviceType in each dependency array. Ensure
you only add deviceType (no other state changes) to avoid unintended behavior.

In `@src/hooks/useNotifications.ts`:
- Around line 191-240: The OneSignal listeners registered under
w.__ONE_SIGNAL_LISTENERS_ADDED__ capture stale closures (refreshPermissionState,
evaluateVisibility, setShowPermissionModal) from the first hook instance and
never update on remount; fix by moving listener registration out of the hook
into a shared initializer or context that wires listeners to stable
refs/handlers, or alternatively re-register with cleanup on each mount while
using refs for mutable callbacks (keep externalIdRef and
disableExternalIdLoginRef usage), and ensure the
OneSignal.User.PushSubscription.addEventListener and
OneSignal.Notifications.addEventListener handlers call the latest functions via
refs or are registered by a central init that accepts/upserts current callbacks
so new hook instances get correct behavior.

---

Nitpick comments:
In @.gitignore:
- Around line 81-82: Remove the redundant gitignore entry ".claude/worktrees/"
from .gitignore because the broader ".claude/" rule already covers it; locate
the ".claude/worktrees/" line and delete it so the file only keeps the parent
".claude/" ignore entry.

In `@CONTRIBUTING.md`:
- Around line 136-137: Update the inconsistent bullet casing by capitalizing the
first letter of the last list item in CONTRIBUTING.md: change "- follow
conventional commits." to start with an uppercase letter (e.g., "- Follow
conventional commits.") so it matches the capitalization pattern used by the
other bullets.

In `@scripts/validate-links.ts`:
- Around line 109-113: The code is re-adding non-locale help routes on every
iteration of the locale loop; move the registrations of the bare routes so they
run once outside the loop: add paths.add('/help') and for each slug in helpSlugs
add paths.add(`/help/${slug}`) before entering the loop that iterates over
locale (the loop that uses the locale variable and also adds
`/${locale}/help/${slug}`), leaving the per-locale additions
(paths.add(`/${locale}/help/${slug}`)) inside the loop. Ensure you reference the
existing paths and helpSlugs variables and keep the locale-specific additions
unchanged.

In `@src/app/`[locale]/(marketing)/compare/[slug]/page.tsx:
- Around line 27-37: Extract the repeated locale/slug/competitor/mdx resolution
logic into a shared helper (e.g., resolveCompareRoute or getCompareData) and
call it from generateMetadata and the page renderer; the helper should accept
params, call isValidLocale and parseSlug, resolve COMPETITORS[slug], and invoke
readPageContentLocalized<ContentFrontmatter>('compare', slug, locale), returning
a single unified result object or null for early exits so both generateMetadata
and the page component use the same checks and avoid drift.

In `@src/components/Global/Footer/consts.ts`:
- Around line 19-23: The 'Help' footer item still uses GITBOOK_ICON; replace it
with the matching help icon constant (e.g., HELP_ICON or QUESTION_ICON) from the
icons set so the visual matches the renamed entry—update the object in consts.ts
where the footer item with name 'Help' is defined (change logoSrc:
icons.GITBOOK_ICON.src to logoSrc: icons.HELP_ICON.src or
icons.QUESTION_ICON.src) and ensure the chosen icon constant exists and is
imported/available in the icons module.

In `@src/components/Setup/Views/SetupPasskey.tsx`:
- Around line 136-143: The anchor in SetupPasskey.tsx uses a plain <a> for an
internal route; replace it with Next.js Link to get prefetching and proper
client-side navigation: import Link from 'next/link' in the SetupPasskey
component and change the <a href="/en/help/passkeys" ...> to <Link
href="/en/help/passkeys" ...> (preserve className, target="_blank" and
rel="noopener noreferrer" if you still want a new tab) so the route uses Next.js
navigation while keeping the same styling and attributes.

In `@src/components/Setup/Views/Welcome.tsx`:
- Around line 77-90: Add an analytics event when the Log In button is clicked by
invoking the app's tracking API (e.g., trackEvent or analytics.track) with the
appropriate constant from ANALYTICS_EVENTS (e.g.,
ANALYTICS_EVENTS.LOGIN_CLICKED) inside the Button onClick before calling
handleLogin; update the onClick handler that currently calls
handleLogin().catch(handleError) to first fire the analytics event (and swallow
any tracking errors) then proceed to call handleLogin and preserve the existing
.catch(handleError) behavior so login errors are still handled.

In `@src/constants/tooltips.ts`:
- Line 6: The anchor HTML string '<a href="/en/help/supported-geographies"
target="_blank" class="underline text-blue-600">Supported regions</a>' should
include rel="noopener" for security and, if you want locale-aware links, avoid
the hardcoded "/en/" — either inject the current locale variable (e.g. replace
"/en/" with `/${locale}/`) or build the URL via a helper like getHelpUrl(locale,
'supported-geographies'); update the string in src/constants/tooltips.ts (the
literal anchor) to add rel="noopener" and optionally parameterize the locale or
call the helper so the link can be locale-aware.

In `@src/hooks/useZeroDev.ts`:
- Around line 112-115: The analytics call using posthog.capture for
ANALYTICS_EVENTS.INVITE_ACCEPT_FAILED currently sends String(e) which can
include stack traces or sensitive internals; update the capture payload to send
a sanitized error value (e.g., if e is an Error use e.message, otherwise map to
a safe error code or truncated/scrubbed string) and avoid including stack or
full error objects; change the code around the posthog.capture call in
useZeroDev.ts (the block where invite_code = userInviteCode is set) to compute a
safeError variable and pass that instead.

In `@src/utils/format.utils.ts`:
- Line 112: Replace the hand-rolled regex in isValidEmail with validator's
robust implementation: import isEmail from 'validator' (or import { isEmail }
from 'validator' depending on your bundler) and change the isValidEmail
implementation (export const isValidEmail) to call isEmail(email). Ensure the
new import is added at the top of the module and the function signature remains
(email: string) => boolean.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc37b94e-3bc3-4eb5-845f-46968118a159

📥 Commits

Reviewing files that changed from the base of the PR and between a43e2b5 and a5d5287.

⛔ Files ignored due to path filters (4)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/email/peanut-jail.png is excluded by !**/*.png
  • public/email/peanut-wave.png is excluded by !**/*.png
  • src/assets/illustrations/global-cash-local-feel.png is excluded by !**/*.png
📒 Files selected for processing (98)
  • .cursorrules
  • .cursorrules
  • .env.example
  • .github/copilot-instructions.md
  • .gitignore
  • .prettierignore
  • .windsurfrules
  • AGENTS.md
  • CLAUDE.md
  • CONTRIBUTING.md
  • README.md
  • docs/archive/SOP.md
  • instrumentation-client.ts
  • next.config.js
  • package.json
  • redirects.json
  • scripts/validate-links.ts
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/add-money/crypto/page.tsx
  • src/app/(mobile-ui)/dev/full-graph/page.tsx
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/points/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/(mobile-ui)/withdraw/page.tsx
  • src/app/[...recipient]/page.tsx
  • src/app/[locale]/(marketing)/[country]/page.tsx
  • src/app/[locale]/(marketing)/blog/[slug]/page.tsx
  • src/app/[locale]/(marketing)/compare/[slug]/page.tsx
  • src/app/[locale]/(marketing)/deposit/[exchange]/page.tsx
  • src/app/[locale]/(marketing)/help/[slug]/page.tsx
  • src/app/[locale]/(marketing)/help/page.tsx
  • src/app/[locale]/(marketing)/pay-with/[method]/page.tsx
  • src/app/[locale]/(marketing)/receive-money-from/[country]/page.tsx
  • src/app/[locale]/(marketing)/send-money-from/[from]/to/[to]/page.tsx
  • src/app/[locale]/(marketing)/send-money-to/[country]/page.tsx
  • src/app/sitemap.ts
  • src/assets/illustrations/index.ts
  • src/components/AddMoney/components/MantecaAddMoney.tsx
  • src/components/AddMoney/components/OnrampConfirmationModal.tsx
  • src/components/AddWithdraw/AddWithdrawRouterView.tsx
  • src/components/Card/CardPioneerModal.tsx
  • src/components/Card/CardSuccessScreen.tsx
  • src/components/Claim/Link/Initial.view.tsx
  • src/components/Claim/Link/Onchain/Confirm.view.tsx
  • src/components/Global/BackendErrorScreen/index.tsx
  • src/components/Global/BalanceWarningModal/index.tsx
  • src/components/Global/CopyToClipboard/index.tsx
  • src/components/Global/DirectSendQR/index.tsx
  • src/components/Global/EarlyUserModal/index.tsx
  • src/components/Global/Footer/consts.ts
  • src/components/Global/InviteFriendsModal/index.tsx
  • src/components/Global/InvitesGraph/index.tsx
  • src/components/Global/NoMoreJailModal/index.tsx
  • src/components/Home/KycCompletedModal/index.tsx
  • src/components/Invites/InvitesPage.tsx
  • src/components/Invites/JoinWaitlistPage.tsx
  • src/components/LandingPage/Footer.tsx
  • src/components/LandingPage/LandingPageClient.tsx
  • src/components/LandingPage/Manteca.tsx
  • src/components/LandingPage/RegulatedRails.tsx
  • src/components/LandingPage/SEOFooter.tsx
  • src/components/LandingPage/StickyMobileCTA.tsx
  • src/components/LandingPage/hero.tsx
  • src/components/LandingPage/landingPageData.ts
  • src/components/Marketing/ComparisonTable.tsx
  • src/components/Marketing/index.ts
  • src/components/Marketing/pages/ReceiveMoneyContent.tsx
  • src/components/Notifications/SetupNotificationsModal.tsx
  • src/components/Payment/Views/Error.validation.view.tsx
  • src/components/Profile/index.tsx
  • src/components/Send/link/views/Initial.link.send.view.tsx
  • src/components/Send/link/views/Success.link.send.view.tsx
  • src/components/Send/views/SendRouter.view.tsx
  • src/components/Setup/Views/InstallPWA.tsx
  • src/components/Setup/Views/Landing.tsx
  • src/components/Setup/Views/SetupPasskey.tsx
  • src/components/Setup/Views/SignTestTransaction.tsx
  • src/components/Setup/Views/Signup.tsx
  • src/components/Setup/Views/Welcome.tsx
  • src/constants/analytics.consts.ts
  • src/constants/chains.consts.ts
  • src/constants/routes.ts
  • src/constants/tooltips.ts
  • src/constants/tweets.consts.ts
  • src/constants/tweets.json
  • src/context/authContext.tsx
  • src/data/seo/exchanges.ts
  • src/data/seo/index.ts
  • src/features/limits/components/LimitsDocsLink.tsx
  • src/features/payments/shared/components/PaymentSuccessView.tsx
  • src/hooks/useMultiPhaseKycFlow.ts
  • src/hooks/useNotifications.ts
  • src/hooks/useZeroDev.ts
  • src/services/points.ts
  • src/utils/format.utils.ts
  • src/utils/withdraw.utils.ts
💤 Files with no reviewable changes (3)
  • src/components/Marketing/index.ts
  • src/components/Marketing/ComparisonTable.tsx
  • src/components/Marketing/pages/ReceiveMoneyContent.tsx

Comment on lines +3 to +11
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'development') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: '/ingest',
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'identified_only',
capture_pageview: true,
capture_pageleave: true,
autocapture: true,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against undefined PostHog key in production.

The non-null assertion (!) on NEXT_PUBLIC_POSTHOG_KEY assumes the env var is always defined in production. If accidentally unset, posthog.init would receive undefined, potentially causing runtime issues or silent failures.

🛡️ Proposed defensive check
-if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'development') {
+const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
+if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'development' && posthogKey) {
-    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
+    posthog.init(posthogKey, {
         api_host: '/ingest',
         ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
         person_profiles: 'identified_only',
         capture_pageview: true,
         capture_pageleave: true,
         autocapture: 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.

Suggested change
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'development') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: '/ingest',
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'identified_only',
capture_pageview: true,
capture_pageleave: true,
autocapture: true,
})
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'development' && posthogKey) {
posthog.init(posthogKey, {
api_host: '/ingest',
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'identified_only',
capture_pageview: true,
capture_pageleave: true,
autocapture: true,
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@instrumentation-client.ts` around lines 3 - 11, The code calls posthog.init
with process.env.NEXT_PUBLIC_POSTHOG_KEY using a non-null assertion; instead add
a runtime guard before calling posthog.init to ensure the key is defined (when
typeof window !== 'undefined' and process.env.NODE_ENV !== 'development'), e.g.,
read const key = process.env.NEXT_PUBLIC_POSTHOG_KEY and if (!key) skip
initialization and emit a clear warning/error via console.warn or a logger; only
call posthog.init(key, {...}) when key is present so posthog.init is never
invoked with undefined.

return config
},
reactStrictMode: false,
skipTrailingSlashRedirect: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Next.js skipTrailingSlashRedirect purpose and SEO implications

💡 Result:

What skipTrailingSlashRedirect is for

By default, Next.js normalizes trailing slashes by redirecting (e.g. /about//about, or the reverse if you enable trailingSlash: true).[1]
skipTrailingSlashRedirect: true turns off Next.js’s automatic “add/remove trailing slash” redirects so you can implement your own per-path behavior in Middleware (useful for incremental migrations where some sections keep legacy URL shapes and others don’t).[2]

SEO implications

  • With the default redirect behavior (recommended for most sites): you effectively get one canonical URL per page via a redirect, which helps avoid slash/non-slash duplicates.[1]
  • If you enable skipTrailingSlashRedirect and don’t replace it with your own permanent redirect policy: both /page and /page/ can become separately crawlable (duplicate URLs), which can dilute signals (links/internal links) and create inconsistent indexing.
  • Best practice if you disable it: implement a single, consistent canonical URL format and redirect the other variant with a permanent redirect (HTTP 308 in Next.js for permanent redirects), and keep internal links consistent.[3]

Rule of thumb: only use skipTrailingSlashRedirect when you need custom slash logic in middleware; otherwise rely on trailingSlash + Next.js’s built-in normalization.[1][2]

Sources: Next.js trailingSlash config docs[1], Next.js Middleware “Advanced Middleware Flags” docs (includes skipTrailingSlashRedirect)[2], Next.js redirects status codes (307/308) docs[3].


🏁 Script executed:

# Look for middleware files in the project
git ls-files | grep -i middleware

# Also check for Middleware.ts or middleware.js at the root or src level
fd -i middleware -t f | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

# Search for any custom redirect/rewrite logic related to trailing slashes
rg -i "trailing.*slash|slash.*redirect" --type js --type ts | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 347


🏁 Script executed:

# Check the full next.config.js around line 98 and lines 115-125 to understand context
cat -n next.config.js | sed -n '90,130p'

Repository: peanutprotocol/peanut-ui

Length of output: 1745


🏁 Script executed:

# More explicit search for middleware in src directory
find src -name "middleware.*" -o -name "*middleware*" 2>/dev/null | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

# Check entire next.config.js for trailingSlash setting
cat next.config.js | grep -i "trailingslash"

Repository: peanutprotocol/peanut-ui

Length of output: 104


🏁 Script executed:

# Verify there are no middleware files at the root
ls -la | grep -i middleware

Repository: peanutprotocol/peanut-ui

Length of output: 50


skipTrailingSlashRedirect is enabled without custom Middleware.

This setting disables Next.js's automatic trailing slash normalization and requires custom Middleware to implement proper redirects. Without it, both /path and /path/ become separately crawlable, causing duplicate content and SEO issues. Either remove this setting to rely on Next.js's default behavior, or implement custom Middleware to enforce a single canonical URL format with permanent redirects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next.config.js` at line 98, The config option skipTrailingSlashRedirect in
next.config.js is turning off Next's automatic trailing-slash normalization,
which can cause duplicate crawlable URLs; either remove
skipTrailingSlashRedirect: true from next.config.js to restore default behavior,
or add a custom Middleware (e.g., middleware.js / middleware.ts) that inspects
request.nextUrl.pathname and issues a permanent redirect via
NextResponse.redirect to a canonical URL (consistently with or without trailing
slash), handling root and asset paths and preserving query strings; implement
the Middleware logic and enable it instead of leaving skipTrailingSlashRedirect
enabled.

Comment on lines +239 to +243
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
posthog.capture(ANALYTICS_EVENTS.DEPOSIT_FAILED, {
method_type: 'bank',
error_message: errorMessage,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid sending raw exception text in analytics payloads.

Line 239–243 forwards error.message directly to PostHog. Backend error text can include sensitive or internal details; prefer normalized error categories/codes.

🔒 Suggested fix (sanitize analytics error field)
-            const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+            const errorType = error instanceof Error ? error.name : 'UnknownError'
             posthog.capture(ANALYTICS_EVENTS.DEPOSIT_FAILED, {
                 method_type: 'bank',
-                error_message: errorMessage,
+                error_type: errorType,
+                country: selectedCountryPath,
             })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx around lines 239 -
243, Don't send raw exception text to analytics: replace the direct use of
error.message when calling posthog.capture for ANALYTICS_EVENTS.DEPOSIT_FAILED
with a sanitized/category value. Update the code around errorMessage and the
posthog.capture call so it maps known Error instances (or error.message) to a
small set of normalized codes or categories (e.g., "validation_error",
"network_error", "timeout", "unknown_error") or run the message through a
sanitize function that strips sensitive details before sending; use that
sanitizedCode or category in the error_message/error_code field passed to
posthog.capture (refer to errorMessage, posthog.capture, and
ANALYTICS_EVENTS.DEPOSIT_FAILED).

Comment on lines +298 to +301
posthog.capture(ANALYTICS_EVENTS.WITHDRAW_FAILED, {
method_type: 'crypto',
error_message: errMsg,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid sending raw error text to PostHog payloads.

At Line 300, error_message: errMsg can include sensitive backend/provider details. Prefer normalized error_code/error_type for analytics and keep raw diagnostics in Sentry/logs only. This same pattern appears in other withdraw flow additions in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/withdraw/crypto/page.tsx around lines 298 - 301, Replace
sending raw error text in the posthog payload: instead of passing errMsg into
posthog.capture(ANALYTICS_EVENTS.WITHDRAW_FAILED, ...), derive and send a
normalized error identifier (e.g., error_code or error_type) and any safe
boolean flags (e.g., provider_timeout: true) for analytics; keep the full errMsg
only in Sentry/logging calls (e.g., captureException or processLogger) for
diagnostics. Update all similar calls in the withdraw flow that use
posthog.capture to follow this pattern and reference the posthog.capture and
ANALYTICS_EVENTS.WITHDRAW_FAILED usages as the locations to change.

Comment on lines +154 to +159
posthog.capture(ANALYTICS_EVENTS.DEPOSIT_AMOUNT_ENTERED, {
amount_usd: usdAmount,
method_type: 'manteca',
country: selectedCountryPath,
denomination: currentDenomination,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Emit amount_usd as a number.

usdAmount is string state in this component, so these events currently send "123.45" instead of 123.45. src/app/(mobile-ui)/withdraw/page.tsx emits the same property as numeric usdVal on Lines 255-262, and mixing types will break amount-based filters and aggregations.

🔢 Proposed fix
+            const amountUsd = Number.parseFloat(usdAmount)
             posthog.capture(ANALYTICS_EVENTS.DEPOSIT_AMOUNT_ENTERED, {
-                amount_usd: usdAmount,
+                amount_usd: amountUsd,
                 method_type: 'manteca',
                 country: selectedCountryPath,
                 denomination: currentDenomination,
             })
...
             posthog.capture(ANALYTICS_EVENTS.DEPOSIT_CONFIRMED, {
-                amount_usd: usdAmount,
+                amount_usd: amountUsd,
                 method_type: 'manteca',
                 country: selectedCountryPath,
             })

Also applies to: 179-183

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AddMoney/components/MantecaAddMoney.tsx` around lines 154 -
159, The analytics event is sending usdAmount as a string; update the
posthog.capture calls (the one using ANALYTICS_EVENTS.DEPOSIT_AMOUNT_ENTERED and
the similar call around the other block) to pass a numeric value by converting
usdAmount (component state) to a number (e.g., Number(usdAmount) or
parseFloat(usdAmount)) and handle NaN fallback if necessary so amount_usd is
always a numeric type while leaving other properties (method_type,
country/selectedCountryPath, denomination/currentDenomination) unchanged.

Comment on lines +46 to +49
<a href="/setup" className="pointer-events-auto block">
<Button variant="purple" shadowSize="4" className="w-full py-3 text-base font-extrabold">
SIGN UP NOW
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== StickyMobileCTA markup ==="
sed -n '36,55p' src/components/LandingPage/StickyMobileCTA.tsx

echo
echo "=== Button root element ==="
sed -n '150,210p' src/components/0_Bruddle/Button.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 2950


Remove the <a> wrapper from Button; nest interactive elements causes invalid HTML and accessibility breakage.

The Button component at lines 46-49 is wrapped in an anchor tag, but Button renders a native <button>. Nesting interactive elements breaks keyboard navigation and screen reader behavior.

Suggested fix
+import { useRouter } from 'next/navigation'
 ...
 export function StickyMobileCTA() {
+    const router = useRouter()
     const [visible, setVisible] = useState(false)
 ...
-                    <a href="/setup" className="pointer-events-auto block">
-                        <Button variant="purple" shadowSize="4" className="w-full py-3 text-base font-extrabold">
+                    <Button
+                        onClick={() => router.push('/setup')}
+                        variant="purple"
+                        shadowSize="4"
+                        className="pointer-events-auto w-full py-3 text-base font-extrabold"
+                    >
                             SIGN UP NOW
-                        </Button>
-                    </a>
+                    </Button>
📝 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
<a href="/setup" className="pointer-events-auto block">
<Button variant="purple" shadowSize="4" className="w-full py-3 text-base font-extrabold">
SIGN UP NOW
</Button>
import { useRouter } from 'next/navigation'
...
export function StickyMobileCTA() {
const router = useRouter()
const [visible, setVisible] = useState(false)
...
<Button
onClick={() => router.push('/setup')}
variant="purple"
shadowSize="4"
className="pointer-events-auto w-full py-3 text-base font-extrabold"
>
SIGN UP NOW
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/LandingPage/StickyMobileCTA.tsx` around lines 46 - 49, In
StickyMobileCTA, remove the outer anchor wrapper around the Button to avoid
nesting interactive elements; instead make the Button itself handle navigation
(e.g., add an onClick that routes to "/setup" or use the app's Link/Router API
to render the Button as an anchor if Button supports an href/asLink prop).
Update the Button usage in StickyMobileCTA so it is the sole interactive element
(refer to the Button component and StickyMobileCTA) and ensure keyboard focus
and a11y semantics are preserved.

Comment on lines +196 to +203
<a
className="underline underline-offset-2"
href="https://docs.peanut.me/passkeys"
href="/en/help/passkeys"
target="_blank"
rel="noopener noreferrer"
>
Learn more about what Passkeys are
</Link>{' '}
</a>{' '}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a consistent pattern for handling localized help links elsewhere
rg -n "help/passkeys" --type=ts --type=tsx -C2

Repository: peanutprotocol/peanut-ui

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Fix the rg command - use proper syntax for TypeScript files
rg -n "help/passkeys" -t ts -C2

Repository: peanutprotocol/peanut-ui

Length of output: 1544


🏁 Script executed:

#!/bin/bash
# Check the actual file to see both instances of hardcoded paths
cat -n src/components/Setup/Views/SignTestTransaction.tsx | sed -n '190,225p'

Repository: peanutprotocol/peanut-ui

Length of output: 1585


🏁 Script executed:

#!/bin/bash
# Search for how locales are typically handled in the codebase
rg -n "useParams|useLocale|useRouter" -t ts -C1 | head -50

Repository: peanutprotocol/peanut-ui

Length of output: 3211


🏁 Script executed:

#!/bin/bash
# Look for other help link patterns to understand the convention
rg -n "/help/" -t ts | head -30

Repository: peanutprotocol/peanut-ui

Length of output: 2013


🏁 Script executed:

#!/bin/bash
# Check SignTestTransaction.tsx imports and structure
head -50 src/components/Setup/Views/SignTestTransaction.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 2465


🏁 Script executed:

#!/bin/bash
# Check how HelpLanding.tsx accesses locale to understand the pattern
head -30 src/components/Marketing/HelpLanding.tsx | grep -E "^import|useParams|useLocale|locale"

Repository: peanutprotocol/peanut-ui

Length of output: 371


🏁 Script executed:

#!/bin/bash
# Search for locale utility functions or custom hooks
rg -n "useLocale|getLocale|getDefaultLocale" -t ts | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 304


🏁 Script executed:

#!/bin/bash
# Check if these components are client components and look for locale context
rg -n "use client|'use client'" src/components/Setup/Views/SignTestTransaction.tsx src/components/Setup/Views/SetupPasskey.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check first 5 lines of SignTestTransaction for 'use client'
head -5 src/components/Setup/Views/SignTestTransaction.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 333


🏁 Script executed:

#!/bin/bash
# Find where SignTestTransaction is imported/used
rg -n "SignTestTransaction" -t ts --no-heading | grep -v "^src/components/Setup/Views/SignTestTransaction.tsx"

Repository: peanutprotocol/peanut-ui

Length of output: 743


🏁 Script executed:

#!/bin/bash
# Check SetupPasskey similarly
head -5 src/components/Setup/Views/SetupPasskey.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 325


🏁 Script executed:

#!/bin/bash
# Find where SetupPasskey is imported/used
rg -n "SetupPasskey" -t ts | grep -v "^src/components/Setup/Views/SetupPasskey.tsx"

Repository: peanutprotocol/peanut-ui

Length of output: 390


🏁 Script executed:

#!/bin/bash
# Check the app directory structure to understand locale routing
find src/app -type d -name "setup" -o -name "*locale*" | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Check if (setup) is a route group and where it sits relative to [locale]
ls -la src/app/ | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 1195


🏁 Script executed:

#!/bin/bash
# Check the setup/finish/page.tsx to see what it receives
cat -n src/app/\(setup\)/setup/finish/page.tsx | head -40

Repository: peanutprotocol/peanut-ui

Length of output: 1831


🏁 Script executed:

#!/bin/bash
# Search for other components with hardcoded locale links to see if there's a pattern
rg -n 'href="/en/' -t ts | grep -v node_modules

Repository: peanutprotocol/peanut-ui

Length of output: 1362


🏁 Script executed:

#!/bin/bash
# Check if there's a locale context or utility the setup components can use
rg -n "LocaleContext|LocaleProvider|useLocale\(\)" -t ts | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check if setup pages should be using locale - look for params in page structure
rg -n "setup.*locale|locale.*setup" -t ts -i | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check how other non-locale-aware pages handle help links (if any)
rg -n "href=\"/.*help/" -t ts | grep -v "\${locale}\|useParams\|/\[locale\]" | head -20

Repository: peanutprotocol/peanut-ui

Length of output: 990


Remove hardcoded locale from help link or make it locale-aware.

The help link uses hardcoded /en/help/passkeys, which doesn't respect the user's locale. The app supports multi-locale help content at /{locale}/help/[slug], but this component is outside the [locale] routing context. Either accept locale as a prop from the parent component, or if the setup flow intentionally remains English-only, document this design choice.

Note: The suggested useParams approach won't work here since (setup) is a root-level route group, not nested under [locale].

This also applies to the PasskeyDocsLink export (lines 214-221) and similar patterns in SetupPasskey.tsx (line 138).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Setup/Views/SignTestTransaction.tsx` around lines 196 - 203,
The help link is hardcoded to /en/help/passkeys and should be locale-aware:
update SignTestTransaction (and the exported PasskeyDocsLink) to accept a locale
prop and build the href as `/${locale}/help/passkeys` (or have the parent pass
the correct localized path), and update callers (including SetupPasskey) to
forward the locale; alternatively, if the setup flow must remain English-only,
add a clear code comment in SignTestTransaction and PasskeyDocsLink explaining
the intentional design choice and why locale is not used.

Comment on lines +147 to +162
/**
* Matches locale tags with a required subtag to avoid false-positives on short
* strings like "go", "no", "max" that are valid usernames. Covers patterns like
* "pt-br", "es-419", "zh-Hans", "zh-Hans-CN" but NOT bare 2-letter codes (those
* must be listed explicitly in DEDICATED_ROUTES).
*/
const LOCALE_WITH_SUBTAG = /^[a-z]{2,3}-[a-z0-9]{2,8}(-[a-z0-9]{2,8})*$/i

/**
* Helper to check if a path segment looks like a locale code.
* Bare 2-3 letter codes (en, es, pt) are caught by DEDICATED_ROUTES.
* This handles subtag variants (pt-br, es-419, zh-Hans) that aren't listed explicitly.
*/
export function isLocaleSegment(segment: string): boolean {
return LOCALE_WITH_SUBTAG.test(segment)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's username validation that restricts hyphens or short segments
rg -n -C3 'username.*valid|validateUsername|isValidUsername|username.*regex|USERNAME.*REGEX' --type ts --type tsx

Repository: peanutprotocol/peanut-ui

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Search for DEDICATED_ROUTES definition
rg -n "DEDICATED_ROUTES" src/constants/routes.ts -A 10 -B 2

Repository: peanutprotocol/peanut-ui

Length of output: 1782


🏁 Script executed:

#!/bin/bash
# Search for username validation logic in the codebase
rg -n -C3 'username.*valid|validateUsername|isValidUsername|username.*regex' --type ts

Repository: peanutprotocol/peanut-ui

Length of output: 7648


🏁 Script executed:

#!/bin/bash
# Search for any routing logic that might interact with locale/username
rg -n "isLocaleSegment|LOCALE_WITH_SUBTAG" --type ts -C 3

Repository: peanutprotocol/peanut-ui

Length of output: 1486


Update the comment to accurately reflect which bare codes are in DEDICATED_ROUTES.

The comment at line 157 claims "Bare 2-3 letter codes (en, es, pt) are caught by DEDICATED_ROUTES," but reviewing the actual DEDICATED_ROUTES definition shows none of these bare codes are listed (it contains routes like 'qr', 'api', 'setup', 'home', 'history', 'settings', 'points', 'claim', 'pay'). Update the comment to clarify which bare codes are actually reserved:

-* Bare 2-3 letter codes (en, es, pt) are caught by DEDICATED_ROUTES.
+* Bare 2-3 letter codes reserved elsewhere are checked in DEDICATED_ROUTES.

The username collision concern from the original review is not a practical issue—username validation in the codebase uses underscores ([a-z0-9_]) rather than hyphens, so hyphen-separated locale patterns like 'pt-br' won't conflict with valid usernames.

📝 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
/**
* Matches locale tags with a required subtag to avoid false-positives on short
* strings like "go", "no", "max" that are valid usernames. Covers patterns like
* "pt-br", "es-419", "zh-Hans", "zh-Hans-CN" but NOT bare 2-letter codes (those
* must be listed explicitly in DEDICATED_ROUTES).
*/
const LOCALE_WITH_SUBTAG = /^[a-z]{2,3}-[a-z0-9]{2,8}(-[a-z0-9]{2,8})*$/i
/**
* Helper to check if a path segment looks like a locale code.
* Bare 2-3 letter codes (en, es, pt) are caught by DEDICATED_ROUTES.
* This handles subtag variants (pt-br, es-419, zh-Hans) that aren't listed explicitly.
*/
export function isLocaleSegment(segment: string): boolean {
return LOCALE_WITH_SUBTAG.test(segment)
}
/**
* Matches locale tags with a required subtag to avoid false-positives on short
* strings like "go", "no", "max" that are valid usernames. Covers patterns like
* "pt-br", "es-419", "zh-Hans", "zh-Hans-CN" but NOT bare 2-letter codes (those
* must be listed explicitly in DEDICATED_ROUTES).
*/
const LOCALE_WITH_SUBTAG = /^[a-z]{2,3}-[a-z0-9]{2,8}(-[a-z0-9]{2,8})*$/i
/**
* Helper to check if a path segment looks like a locale code.
* Bare 2-3 letter codes reserved elsewhere are checked in DEDICATED_ROUTES.
* This handles subtag variants (pt-br, es-419, zh-Hans) that aren't listed explicitly.
*/
export function isLocaleSegment(segment: string): boolean {
return LOCALE_WITH_SUBTAG.test(segment)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/constants/routes.ts` around lines 147 - 162, Update the comment above
LOCALE_WITH_SUBTAG/isLocaleSegment to accurately state that DEDICATED_ROUTES
does not actually reserve bare 2-3 letter locale codes (so don’t claim “en, es,
pt” are listed there); note instead that DEDICATED_ROUTES contains non-locale
routes like 'qr', 'api', 'setup', etc., and clarify that username validation
uses underscores ([a-z0-9_]) so hyphenated locale variants (e.g., 'pt-br') won’t
collide with valid usernames; adjust the text near LOCALE_WITH_SUBTAG and the
isLocaleSegment docstring to reflect this accurately.

return (
<a
href="https://docs.peanut.me/limits"
href="/en/help/transaction-limits"
Copy link
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 hard-coded English locale in internal help link.

Line 4 always opens the English page, even when users are in a different locale. Make the locale dynamic (or locale-agnostic) to preserve routing consistency.

Suggested change
-export default function LimitsDocsLink() {
+export default function LimitsDocsLink({ locale = 'en' }: { locale?: string }) {
     return (
         <a
-            href="/en/help/transaction-limits"
+            href={`/${locale}/help/transaction-limits`}
             target="_blank"
             rel="noopener noreferrer"
             className="text-center text-sm underline"
         >
             See more about limits
         </a>
     )
 }
📝 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
href="/en/help/transaction-limits"
export default function LimitsDocsLink({ locale = 'en' }: { locale?: string }) {
return (
<a
href={`/${locale}/help/transaction-limits`}
target="_blank"
rel="noopener noreferrer"
className="text-center text-sm underline"
>
See more about limits
</a>
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/limits/components/LimitsDocsLink.tsx` at line 4, The link in the
LimitsDocsLink component is hard-coded to "/en/help/transaction-limits"; update
the href to be locale-aware by deriving the current locale (e.g., use Next.js
router.locale or the app's i18n helper) or by using a locale-agnostic path
("/help/transaction-limits") and/or passing locale to the Link component so the
route preserves the user's locale; modify the href usage in LimitsDocsLink
accordingly.

Comment on lines +316 to +318
if (options?.includeNewDays && options.includeNewDays > 0) {
params.set('includeNewDays', options.includeNewDays.toString())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden includeNewDays input validation before serializing query params.

Line 316 currently accepts any positive number, including non-integers (e.g., 1.5). If the backend expects whole days, this can cause avoidable request failures.

Suggested defensive tweak
-        if (options?.includeNewDays && options.includeNewDays > 0) {
-            params.set('includeNewDays', options.includeNewDays.toString())
-        }
+        if (
+            options?.includeNewDays !== undefined &&
+            Number.isInteger(options.includeNewDays) &&
+            options.includeNewDays > 0
+        ) {
+            params.set('includeNewDays', options.includeNewDays.toString())
+        }
📝 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
if (options?.includeNewDays && options.includeNewDays > 0) {
params.set('includeNewDays', options.includeNewDays.toString())
}
if (
options?.includeNewDays !== undefined &&
Number.isInteger(options.includeNewDays) &&
options.includeNewDays > 0
) {
params.set('includeNewDays', options.includeNewDays.toString())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/points.ts` around lines 316 - 318, The code currently serializes
options.includeNewDays blindly allowing non-integer positive numbers; update the
validation in the function that builds query params (the branch that checks
options?.includeNewDays and calls params.set) to only accept finite integers > 0
(e.g., Number.isInteger-like check) and otherwise omit the param or
coerce/normalize (choose to omit invalid values) before calling
params.set('includeNewDays', ...); ensure you reference options.includeNewDays
and the params.set call so invalid floats like 1.5 are not serialized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants