chore: sync posthog changes to staging#1748
Conversation
…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
…0310-153855 Update content submodule
Track AI tool symlinks in git
- 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.
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
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 | 🟡 MinorAdd
deviceTypeto dependency arrays to prevent stale closures in analytics captures.Lines 78-96 and 124-143 reference
deviceTypein event handlers but omit it from their dependency arrays. This causes the callbacks to capture the value at creation time, potentially logging outdated or undefineddevice_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 | 🟡 MinorUse a card-specific analytics source here.
This screen is the card reservation success step, so
card_deposit_successlooks 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 | 🔴 CriticalOneSignal listeners capture stale closures across hook instance remounts.
The
__ONE_SIGNAL_LISTENERS_ADDED__singleton pattern registers callbacks once, but those callbacks capturerefreshPermissionState(),evaluateVisibility(), and thesetShowPermissionModalsetter from the first hook instance. Because the useEffect has no cleanup and the listener registration never re-runs even whenevaluateVisibilityorrefreshPermissionStatechange, any subsequent mount ofuseNotifications()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/slugvalidation + 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: Usevalidator.isEmail()instead of hand-rolled regex.The regex on line 112 is overly permissive and can miss invalid email edge cases. Since
validatoris already a project dependency, importisEmailfrom 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 addrel="noopener"for security.Two observations:
- The
/en/help/...path is hardcoded. Users viewing the app in other locales will be directed to English help content.- Links with
target="_blank"should includerel="noopener"to prevent the new page from accessingwindow.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_CLICKEDor a similar event exists inANALYTICS_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.jsLinkfor internal navigation.The
/en/help/passkeyspath appears to be an internal route. Using a plain<a>tag withtarget="_blank"works, but if this is an internal help page, you lose Next.js prefetching benefits. However, sincetarget="_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
⛔ Files ignored due to path filters (4)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/email/peanut-jail.pngis excluded by!**/*.pngpublic/email/peanut-wave.pngis excluded by!**/*.pngsrc/assets/illustrations/global-cash-local-feel.pngis excluded by!**/*.png
📒 Files selected for processing (98)
.cursorrules.cursorrules.env.example.github/copilot-instructions.md.gitignore.prettierignore.windsurfrulesAGENTS.mdCLAUDE.mdCONTRIBUTING.mdREADME.mddocs/archive/SOP.mdinstrumentation-client.tsnext.config.jspackage.jsonredirects.jsonscripts/validate-links.tssrc/app/(mobile-ui)/add-money/[country]/bank/page.tsxsrc/app/(mobile-ui)/add-money/crypto/page.tsxsrc/app/(mobile-ui)/dev/full-graph/page.tsxsrc/app/(mobile-ui)/home/page.tsxsrc/app/(mobile-ui)/points/page.tsxsrc/app/(mobile-ui)/withdraw/[country]/bank/page.tsxsrc/app/(mobile-ui)/withdraw/crypto/page.tsxsrc/app/(mobile-ui)/withdraw/manteca/page.tsxsrc/app/(mobile-ui)/withdraw/page.tsxsrc/app/[...recipient]/page.tsxsrc/app/[locale]/(marketing)/[country]/page.tsxsrc/app/[locale]/(marketing)/blog/[slug]/page.tsxsrc/app/[locale]/(marketing)/compare/[slug]/page.tsxsrc/app/[locale]/(marketing)/deposit/[exchange]/page.tsxsrc/app/[locale]/(marketing)/help/[slug]/page.tsxsrc/app/[locale]/(marketing)/help/page.tsxsrc/app/[locale]/(marketing)/pay-with/[method]/page.tsxsrc/app/[locale]/(marketing)/receive-money-from/[country]/page.tsxsrc/app/[locale]/(marketing)/send-money-from/[from]/to/[to]/page.tsxsrc/app/[locale]/(marketing)/send-money-to/[country]/page.tsxsrc/app/sitemap.tssrc/assets/illustrations/index.tssrc/components/AddMoney/components/MantecaAddMoney.tsxsrc/components/AddMoney/components/OnrampConfirmationModal.tsxsrc/components/AddWithdraw/AddWithdrawRouterView.tsxsrc/components/Card/CardPioneerModal.tsxsrc/components/Card/CardSuccessScreen.tsxsrc/components/Claim/Link/Initial.view.tsxsrc/components/Claim/Link/Onchain/Confirm.view.tsxsrc/components/Global/BackendErrorScreen/index.tsxsrc/components/Global/BalanceWarningModal/index.tsxsrc/components/Global/CopyToClipboard/index.tsxsrc/components/Global/DirectSendQR/index.tsxsrc/components/Global/EarlyUserModal/index.tsxsrc/components/Global/Footer/consts.tssrc/components/Global/InviteFriendsModal/index.tsxsrc/components/Global/InvitesGraph/index.tsxsrc/components/Global/NoMoreJailModal/index.tsxsrc/components/Home/KycCompletedModal/index.tsxsrc/components/Invites/InvitesPage.tsxsrc/components/Invites/JoinWaitlistPage.tsxsrc/components/LandingPage/Footer.tsxsrc/components/LandingPage/LandingPageClient.tsxsrc/components/LandingPage/Manteca.tsxsrc/components/LandingPage/RegulatedRails.tsxsrc/components/LandingPage/SEOFooter.tsxsrc/components/LandingPage/StickyMobileCTA.tsxsrc/components/LandingPage/hero.tsxsrc/components/LandingPage/landingPageData.tssrc/components/Marketing/ComparisonTable.tsxsrc/components/Marketing/index.tssrc/components/Marketing/pages/ReceiveMoneyContent.tsxsrc/components/Notifications/SetupNotificationsModal.tsxsrc/components/Payment/Views/Error.validation.view.tsxsrc/components/Profile/index.tsxsrc/components/Send/link/views/Initial.link.send.view.tsxsrc/components/Send/link/views/Success.link.send.view.tsxsrc/components/Send/views/SendRouter.view.tsxsrc/components/Setup/Views/InstallPWA.tsxsrc/components/Setup/Views/Landing.tsxsrc/components/Setup/Views/SetupPasskey.tsxsrc/components/Setup/Views/SignTestTransaction.tsxsrc/components/Setup/Views/Signup.tsxsrc/components/Setup/Views/Welcome.tsxsrc/constants/analytics.consts.tssrc/constants/chains.consts.tssrc/constants/routes.tssrc/constants/tooltips.tssrc/constants/tweets.consts.tssrc/constants/tweets.jsonsrc/context/authContext.tsxsrc/data/seo/exchanges.tssrc/data/seo/index.tssrc/features/limits/components/LimitsDocsLink.tsxsrc/features/payments/shared/components/PaymentSuccessView.tsxsrc/hooks/useMultiPhaseKycFlow.tssrc/hooks/useNotifications.tssrc/hooks/useZeroDev.tssrc/services/points.tssrc/utils/format.utils.tssrc/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
| 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, | ||
| }) |
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
🧩 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
skipTrailingSlashRedirectand don’t replace it with your own permanent redirect policy: both/pageand/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 -20Repository: 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 -20Repository: 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 -20Repository: 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 middlewareRepository: 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.
| const errorMessage = error instanceof Error ? error.message : 'Unknown error' | ||
| posthog.capture(ANALYTICS_EVENTS.DEPOSIT_FAILED, { | ||
| method_type: 'bank', | ||
| error_message: errorMessage, | ||
| }) |
There was a problem hiding this comment.
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).
| posthog.capture(ANALYTICS_EVENTS.WITHDRAW_FAILED, { | ||
| method_type: 'crypto', | ||
| error_message: errMsg, | ||
| }) |
There was a problem hiding this comment.
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.
| posthog.capture(ANALYTICS_EVENTS.DEPOSIT_AMOUNT_ENTERED, { | ||
| amount_usd: usdAmount, | ||
| method_type: 'manteca', | ||
| country: selectedCountryPath, | ||
| denomination: currentDenomination, | ||
| }) |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
🧩 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.tsxRepository: 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.
| <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.
| <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>{' '} |
There was a problem hiding this comment.
🧩 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 -C2Repository: 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 -C2Repository: 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 -50Repository: 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 -30Repository: peanutprotocol/peanut-ui
Length of output: 2013
🏁 Script executed:
#!/bin/bash
# Check SignTestTransaction.tsx imports and structure
head -50 src/components/Setup/Views/SignTestTransaction.tsxRepository: 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 -20Repository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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 -20Repository: 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 -20Repository: 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 -40Repository: 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_modulesRepository: 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 -20Repository: 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 -20Repository: 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 -20Repository: 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.
| /** | ||
| * 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) | ||
| } |
There was a problem hiding this comment.
🧩 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 tsxRepository: 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 2Repository: 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 tsRepository: 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 3Repository: 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.
| /** | |
| * 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" |
There was a problem hiding this comment.
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.
| 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.
| if (options?.includeNewDays && options.includeNewDays > 0) { | ||
| params.set('includeNewDays', options.includeNewDays.toString()) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
No description provided.