Conversation
Jail screen now asks for the inviter's Peanut username instead of a synthesized USERNAMEINVITESYOU123 code. generateInviteCodeLink emits ?code=<username-lowercase>; the obsolete generateInviteCodeSuffix helper + its FE/BE parity test are deleted. Fully BC: backend regex in peanut-api-ts/src/utils/invite.ts keeps matching old-shape URLs and pasted long-form input, so legacy share links from 2024-2025 stay alive forever. Phase 1 of engineering/projects/waitlist-who-invited-you/. Help article (Phase 2) and BE suffix removal (Phase 4) follow in separate PRs.
Replaces "You're still in Peanut jail" + "Prisoner #N" + the bail metaphor with the actual product positioning: invite-only access, with queue position framed as "You're #N in line". Error text mirrors the members-only voice. Why: the jail/prisoner metaphor reads as punishment for being on a waitlist they didn't sign up for; "invite-only" reads as exclusivity. Queue position data is unchanged — only the framing changes.
The mascot art was scattered and awkward: 4 GIFs lived in a separate `peanut-animations` git submodule behind a `@/animations` alias with cryptic names (512X512_ALPHA_GIF_konradurban_0N.gif), while stills sat in src/assets/peanut. ~30 call sites imported raw submodule paths under ad-hoc aliases (the same GIF was chillPeanutAnim / peanutAnim / PeanutGuyGIF). Vendor everything into src/assets/mascot/ with emotion-named files and a single labelled barrel; drop the submodule (+ .gitmodules entry, tsconfig alias) and route every consumer through @/assets/mascot. Names now say what they are: PeanutPeace / PeanutPointing / PeanutCheering / PeanutCrying / PeanutThinking. Also retires the legless peanut-raising-hands.svg on the card waitlist screen in favour of the cheering mascot. No runtime behaviour change. Optimization of the GIFs is handed off separately.
The 5 mascot animations were vendored as 512px GIFs (2.86 MB total) but render at most ~250px (mostly 96-128px). Downscale to 320px and convert to animated WebP (gif2webp -q 70 -m 6 -mixed) — alpha-safe, visually identical at render size. Total 2861 KB -> 939 KB; every file >=65% smaller. Consumers import named exports from @/assets/mascot, so only the 5 export lines change. Also add 'webp' to jest's image moduleNameMapper stub.
The mascot kit was always 6 distinct poses (konradurban_01–06); _04 (angry) and _06 (cool/shades) were dropped from the animations submodule back in the 2026-04-23 decomplexify pass purely because no screen referenced them — not because they were duds. Now that the assets have a single labelled home, bring the full roster in so the next person reaches for an existing mascot instead of re-importing or recreating one. Recovered from submodule history, given the same treatment as the rest (512→320px, animated WebP, gif2webp -q 70). Added as PeanutAngry / PeanutCool; both currently unused — available, not wired to any screen.
…ing-down)
Upstream peanut-animations actually shipped TWO batches: batch 1 (6 poses,
already here) and batch 2 ("thinking, pointing down, waving hello, walking",
4 poses). Only batch 2's "thinking" had ever made it into the app — via the
illustrations barrel, not the submodule. The other three poses lived solely in
the upstream repo and were invisible to anyone working in peanut-ui.
Pull them into the canonical home so the full 10-pose roster is in one labelled
place. Same treatment as the rest (512→320px, animated WebP). All three unused
for now — available, not wired to any screen.
The last raster mascot still (PEANUTMAN_MOBILE, used in the Setup flow) was a 1057x1385 PNG. Downscale to 800px and convert to WebP (cwebp -q 90 -alpha_q 100) — crisp outlines, alpha intact. 205 KB -> 54 KB. SVG stills left as-is (already vector).
Beyond the mascots, ~12MB of PNG/JPG was shipped unoptimized. Two methods: - public/ URL-referenced assets (badges, metadata/OG cards, merchant photos, email, icons): in-place pngquant+optipng / jpegoptim, same path & format — zero ref changes, OG-scraper safe. Flat illustrations -59..81%. - easter-egg modal images (8x detailed RGB PNG, 5.4MB): pngquant couldn't touch them; converted to WebP q80 (-84%), updated the 8 string refs. - landing iphone screenshots: manteca_ss was 1370x2732 but renders at 250px — downscaled to 900px + WebP (-98%); drop-a-link pair -81/86%. Updated imports. Repo raster total 12119 KB -> 2457 KB (-80%). Typecheck/tests/build green.
Two classes of issue from the consolidation: 1. Animated mascots in next/image lose their animation. next/image re-encodes to a single static frame unless 'unoptimized' is set (true for the new animated WebP just as it was for the GIFs). Only 2 of 15 sites had the flag; added it to the other 13 so the loops actually play. 2. no-restricted-imports: 6 consumers still pulled mascots through the '@/assets' root barrel. Route them through the canonical sub-barrels — mascots via '@/assets/mascot', and the non-mascot symbols sharing those lines via '@/assets/icons' / '@/assets/illustrations'. Pre-existing barrel debt unrelated to mascots (@/context in qr-pay, @/assets stills elsewhere) left untouched — out of scope for this PR.
Two follow-ups on the asset consolidation: 1. Rename three exports to match what the art actually depicts — PeanutPeace→PeanutWhistling (it's whistling behind the peace sign), PeanutAngry→PeanutSad (dejected, not angry), PeanutCool→PeanutTooCool. File basenames + barrel comments moved too. 2. Recast which mascot each screen shows so the emotion fits the moment: success/"you're in" screens land on Cheering, greetings on WavingHello, error/let-down on Sad/Crying, loading on Thinking. This de-overuses the old all-purpose "peanut guy" — PeanutWhistling now renders on no screens (kept as a spare export alongside Walking/PointingDown/etc). Stale alt text + the CardWaitlist doc-comment updated to match the new mascots; qr-pay test mock rekeyed to the swapped-in PeanutThinking.
Sync #2164 with dev (badge PR #2162, back-merges) so it goes mergeable. Sole conflict: src/components/Badges/badge.utils.ts — dev added a backend-registry doc comment and still imported PEANUTMAN_LOGO from the now-deleted '@/assets/peanut' barrel. Kept dev's comment, repointed the import to the canonical '@/assets/mascot'. Advances the src/content submodule to dev's pointer. Verified on the merged tree: typecheck 0 errors, 1297 tests pass, next build compiles (1254/1254 pages).
- Re-squeeze 16 already-tracked PNGs (card art, wallet-step screenshots, social/UI icons, one illustration): 566 KB -> 184 KB (-68%), dimensions unchanged. Verified no perceptible loss — diffs composited over a background show maxΔ ≤16 on >half the set; the -89.8% one is bit-lossless. - Replace github-white.png: the old 72px mark rendered visibly pixelated in the footer next to its SVG siblings (telegram/twitter). Regenerated a crisp 512px white mark from the 512px github.png (tint-to-white, alpha preserved) so it stays sharp at the 20px footer size and on retina.
The de-overuse pass left PeanutWhistling rendering on zero screens. Put it
back on two where its "chill / effortless / no-sweat" read fits — and
distinct from WavingHello's greeting role:
- Setup 'landing' step ("Peanut makes dollars easy") — whistling mirrors
the effortless pitch better than a generic wave.
- Marketing landing hero — relaxed brand vibe.
Both swapped from PeanutWavingHello, which still fronts the invites,
join-waitlist, and confirm-invite screens.
…iter's exact handle
The old copy ('Try another username') implies random retries; users who
mistype their inviter — or type their own future username — get no signal
about what's wrong. Nudge them back to the inviter's exact Peanut handle.
The handle pill had mb-4 but no top margin, so its top spacing depended on whether each consumer happened to pass padding onto DrawerContent. Consumers that pad inside via DrawerHeader (CardUnlockDrawer, KycStatusDrawer, TransactionDetailsDrawer, TokenSelector, ChooseNetworkDrawer, BadgeStatusDrawer, ContributorsDrawer) got a handle jammed flush against the rounded top edge. my-4 lets the primitive own symmetric spacing for every consumer at once.
#2127 dropped the INVITESYOU suffix from shared invite links, but four auto-redirect surfaces (claim, request-pot, send CTA) still hand-built the old `USERNAMEINVITESYOU` shape and pushed it into the /invite gate. Route them all through a single canonical toInviteCode() helper so every invite link the app emits is the clean ?code=<username> form. Legacy shapes stay fully accepted by the backend parser, so old links keep working.
…t mock Review follow-ups on the mascot consolidation: - barrel comments claimed PeanutWavingHello/PeanutPointingDown were 'not yet wired'; both are wired. Only PeanutWalking is spare now. - qr-pay test mocked the @/assets barrel, but the component imports PeanutThinking from @/assets/mascot and STAR_STRAIGHT_ICON from @/assets/icons directly, so the mock was a no-op. Re-point both to the real paths with requireActual spread so sibling exports stay intact.
…er-used PeanutCheering
The 'You are #N on the list — we'll let you know when cards are ready to
be shipped' state is a waiting/in-transit moment, not a celebration. The
walking mascot ('on the way / shipping') fits it better than cheering,
and this drops PeanutCheering from 7 screens to 6 while giving the
previously-spare PeanutWalking its first home.
Usage-scanned every asset touched by this PR (boundary-anchored, symbol-aware). These have zero references anywhere in the repo: - public/claim-metadata-img.jpg, raffle-metadata-img.png, redpacket-img.png, preview-bg.png — old per-feature OG/preview images, all superseded by the single /metadata-img.png (+ social-preview-bg.png) used app-wide. - public/badges/_archive/founder_house.png — archived PNG; the live badge uses /badges/founder_house.svg. - src/assets/icons/small-peanut.png — not in the icons barrel, no importers. - src/assets/mascot/peanut-waving.svg (PEANUTMAN_WAVING) — 0 consumers; barrel export removed too. Kept: peanut-pioneer.png (live badge registry entry). Email images + the WIP shhhhh cart-gradient-10-pixel.png left untouched pending external/owner check.
A below-minimum Pix charge surfaced the generic 'something went wrong' error, leaving the user with no idea why the payment failed. Detect PAYMENT_DESTINATION_BELOW_MINIMUM and tell them the BRL minimum so they know the next step.
The backend rejects below-minimum Pix charges with error code PIX_MIN_AMOUNT (manteca/qr-payment.ts), not PAYMENT_DESTINATION_BELOW_MINIMUM. Matching the wrong string meant the friendly minimum-amount message never rendered in production and the charge fell through to the generic error. Match the actual code so the fix works end-to-end.
… PeanutCheering for money celebrations
PeanutCheering (both-fists-up, confetti energy) was on 6 screens, several of
which aren't really celebrations. Moved the three low-key 'you're set / on the
list / welcome in' moments to PeanutWhistling, whose chill peace-sign read fits
a quiet success better than full cheering:
- card waitlist *joined* ('you're on the waitlist' — a wait, not a win)
- no-more-jail welcome modal ('you're in')
- setup 'your wallet is ready' / sign-test step (finish page + Setup.consts)
PeanutCheering now stays on the two genuine confetti money wins (claim success,
payment success). Net: Cheering 6->2, Whistling 2->5.
chore: sync main into dev (2026-06-06)
…ount Two card-flow fixes surfaced by CodeRabbit on the main→dev back-merge (#2190); both pre-existing in prod, not introduced by that PR. 1. pollUntilReady swallowed EVERY fetchReadiness error and, on timeout, returned false → the user always saw "Verification is taking longer than expected", masking persistent auth/5xx failures. It now remembers the most-recent error and re-throws it on a failed-poll timeout (the card page's existing catch then shows the real reason + captures CARD_APPLY_FAILED), while a clean healthy-but-not-ready timeout still returns false. Transient blips that recover do not surface a stale error. 2. The card-recovery completion screen rendered the mount-time preview.amountCents, which can be stale by the time the user confirms (collateral can change between preview and the signed submission). It now shows prep.amountCents — the amount actually prepared, signed and submitted. Also clears the pre-existing useSafeBack eslint error in card-recovery (the NavHeader back action used router.push('/home') → useSafeBack('/home')).
…ecovery-amount fix(card): surface real readiness errors + show actually-recovered amount
… card, clean year
Addresses the Waitlist Feedback doc (share-asset half):
- BG #efe4ff lavender → #90A8ED peanut blue (reused from the prod LP
businessBgColor / --background-color) so the asset pops on a timeline; the
lavender was off-brandbook and washed out.
- Card is the hero now: CARD_W 620→760 (~63% of canvas), centred — was ~52%
and read as "card too small / whitespace huge".
- Stamp year drops the leading apostrophe ('25 → 2025) — the tick read as a
buggy stray mark on the stamp.
- Low badge-count stamps bumped for thumbnail legibility; high counts unchanged
(the circumscribing-circle non-overlap invariant + jitter leaves no headroom).
- LP hero scale compensated 0.645→0.526 so its on-screen footprint is unchanged
by the shared CARD_W bump (hero left as-is; its framing is a separate follow-up).
Layout non-overlap + bounds tests stay green.
Extract the /invite campaign-resolution maps out of the InvitesPage client component into a pure-data campaign-maps.ts, and add a unit test asserting every value in INVITE_CODE_TO_CAMPAIGN_MAP + UTM_CAMPAIGN_TO_BADGE_MAP exists as a key in BADGES. These maps hold badge codes; the UI renders BADGES[code] for the awarded badge. A code with no BADGES entry silently falls back to the Peanutman logo + raw backend name — no conflict, no type error, no throw. That is exactly how TOKEN_NATION_SP_2026 + ETHFLORIPA_HUB evaporated from dev (May 29 event-badge hotfixes written against the pre-refactor parallel maps, dropped when merged across the single-BADGES refactor) and later regressed main (fixed by 32699f1, which this branch back-merges). The test fails on a dangling campaign code, so CI now catches the drop instead of prod.
deriveCardUnlockEntry derived the row from permanent inputs (skip badge / cardAccessGrantedAt) with no seen-gate, and HomeHistory force-kept it past the 5-item cap → it never left the feed (Hugo: "always there, buggy"). - deriveCardUnlockEntry now takes celebrationSeenAt and returns null once set. - HomeHistory passes cardInfo.cardWaitlistSkipCelebrationSeenAt + no longer pins the row (it ages out like any other entry). - card/page.tsx stamps the BE column (cardApi.markCelebrationSeen) when the celebration is acknowledged, alongside the localStorage gate. - CardInfoResponse + cardApi.markCelebrationSeen added; regression test asserts the row clears once celebrationSeenAt is set. Pairs with peanut-api-ts #990 (exposes + writes the column).
Pure rename — the hook returns generic /card info (waitlist state + gates), nothing Pioneer-specific. Renames the hook + its 4 call sites. No behaviour change.
Spends from public pay/send/request pages (useSendMoney on /send/<user> and /<user>/<amount>) dispatch rain:cooldown into a tree that never rendered the modal — users got a bare error with no explainer (Sentry PEANUT-UI-QJ1). The provider already lives at the root; the modal now mounts beside it so coverage can't drift per route group.
The qr-pay catch handles cooldown errors gracefully (no captureException), so these hits were invisible in Sentry — the PEANUT-UI-QJ1 investigation had to triangulate through a different user's send flow. PostHog event is now the telemetry for this expected-operational error.
…ures When a webpack chunk fails to load (deploy skew after a release, or a transient network failure), Next.js throws an uncaught ChunkLoadError and the user dead-ends on the static 'Application error: a client-side exception has occurred' screen — mid-flow, with no recovery affordance. TASK-19993: a user got stuck there on /withdraw/germany/bank and couldn't withdraw at all. Error boundaries cannot fix this class: error.tsx / global-error.jsx are themselves lazy chunks and fail to load under the exact same conditions (PostHog $exception, 2026-05-26 spike, 37 users: the top failing chunks WERE the error pages). The only code guaranteed to be in memory at error time is code shipped inline with the HTML — so the recovery is an inline beforeInteractive script in the root layout. On the first chunk failure it reloads once: refetching the HTML re-pins the page to the current deployment, whose chunks resolve. A sessionStorage timestamp (60s) guards against reload loops when the network is hard-down, and standalone PWA mode is excluded (reload there can bounce the user out to the browser — see the sw-registration script).
Audit follow-up to the inline recovery script: that script only sees UNCAUGHT chunk errors. When a React error boundary catches one, no window event fires, and every boundary handles it wrong for this class: - LazyLoadErrorBoundary renders null — a failed TransactionDetailsDrawer chunk means tapping a transaction silently does nothing; the six lazy home modals silently never appear. - [...recipient]/error.tsx and (marketing)/error.tsx offer 'Try again' (reset), which re-renders against the same dead deployment and can never succeed under skew. - MarketingErrorBoundary tells the user to refresh instead of doing it. - global-error.jsx reports to Sentry and dead-ends. Each now calls recoverFromChunkError(): a no-op for non-chunk errors, and the same sessionStorage-guarded one-time reload otherwise (shared guard key with the inline script, so the paths can't compound into a loop). When the guard blocks, the existing fallback UI stays — behavior degrades to exactly what it was before.
feat: advertise ETH on the crypto deposit screen (EVM only)
… choice Set built once at module scope kills the per-call Object.values rebuild and the double-cast; the friendly-error comment records why we match on message text (camera/clipboard NotAllowedError collision, wrapped errors losing .name) so the next reviewer doesn't 'fix' it into a regression.
…user-bleed fix(crisp): isolate support chat per account, stop cross-user history bleed
…ed on Capacitor Follow-up to #2209. That PR gated the web crisp-proxy iframe so a logged-in user never loads Crisp before their per-user token resolves. The native (Capacitor) path had the identical gap: `setTokenID` was token-gated but `openMessenger()` fired regardless, so on a shared device a logged-in user who opened support before the token resolved got the device-local Crisp session — the previous user's conversation, same class of leak. Reuse the existing `isAwaitingToken` guard: bail the native effect until a logged-in user's token resolves. The effect re-runs and opens once crispTokenId lands (it's already in the deps). Anonymous visitors (no userId, no token by design) still open immediately. Extends SupportDrawer.test.tsx with native-path coverage: messenger does not open while the token resolves, opens token-bound once it does, opens immediately (token-less) for anonymous visitors.
fix: actionable UX + telemetry for WebAuthn NotAllowedError during signing
Verified against the deployed preview HTML: next/script beforeInteractive does NOT emit an executing script — it queues the source into self.__next_s for Next's bootstrap chunk to drain. If the framework chunks fail on initial load, the recovery never registers, defeating the inline requirement. A raw <script dangerouslySetInnerHTML> executes synchronously during HTML parse with zero dependency on any chunk (same pattern as the JSON-LD script in this file).
/code-review altitude finding: RainCooldownError is constructed in one place (rainRequest's 425 branch) but thrown into 10+ spend surfaces — a qr-pay-only capture undercounts every other flow and was gated behind the rainMsg substring match, so a backend copy reword would silently kill the telemetry. Service-level capture also records the retryAfterSec-less shape that shows no cooldown UI (the QJ1 blind spot). flow prop dropped — PostHog's $pathname already carries it.
fix(crisp): gate native Crisp open on token, closing the same bleed on Capacitor
fix: auto-recover from chunk-load failures instead of dead-ending
…al-coverage # Conflicts: # src/constants/analytics.consts.ts
…-coverage fix(rain-cooldown): mount intro modal globally + track swallowed cooldowns
A below-minimum Pix charge is a deterministic 400 from the backend — retrying the lock query 3x with 3s delays can't change the outcome; it just burns requests after the error is already on screen. Hoist the non-retryable codes into one list so the retry callback and the error handler stay in sync as codes get added.
… '25 stamp year Konrad's waitlist-feedback note "the ''' look a bit buggy" was about sparkle.svg — three loose slash-strokes that read as stray tick marks / apostrophes sprinkled on the asset, not about the stamp year denomination (which an earlier pass 'fixed' to a full 4-digit year by mistake). - sparkle kind removed from the decoration pool; its six slots keep their exact geometry (position/size/rotation) and become peace ×3 (hand-peace.svg, newly exported — fits the existing hand family), star ×2, starAlt ×1. Identical bboxes → non-overlap invariants and all 24 layout tests unchanged. - stamp year reverted to the '25 apostrophe style (Hugo: it looked fine). The /shhhhh LP still floats the same sparkle.svg next to the TRY THE DOOR button — left alone (different context, Hugo to judge).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughConsolidates mascot/WebP asset barrel, updates card flows and application-status states, centralizes transaction types/predicates, normalizes invite codes, adds chunk-load recovery and tests, and adjusts support/crisp gating and rain telemetry. ChangesUnified product and infra updates
Estimated code review effort Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
|
Code-analysis diffPainscore total: 5845.67 → 5815.92 (-29.75) 🆕 New findings (612)
…and 592 more. ✅ Resolved (597)
…and 577 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
The amount-entry flow re-inits the payment lock inside handleMantecaPayment; a below-minimum amount there hit the generic 'contact support' catch and filed a Sentry exception for a deterministic, user-fixable rejection. Hoist the message to one constant shared by both paths.
…sparkle polish(share-asset): drop the tick-mark sparkle deco + bring back the '25 stamp year
…rror fix: show actionable Pix-minimum error on below-minimum charges
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/components/Card/cardState.utils.ts (1)
87-123:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBypass the outer gate for users who already have an application.
The
no-flow-accessreturn still runs before anyrailStatushandling. That means a user withflowEarlyAccess: falseand an existing application (PENDING,REQUIRES_INFORMATION,REQUIRES_SUPPORT, etc.) is forced tono-flow-accessinstead of seeing the remediation/review screen for the application they already started. Please exemptoverview.status.hasApplication/ known rail states here, and mirror the same condition insrc/app/(mobile-ui)/card/page.tsx.Suggested fix
- if (!cardInfo.flowEarlyAccess) return 'no-flow-access' - const rail = overview.status.railStatus const app = overview.status.applicationStatus + const hasApplication = overview.status.hasApplication || !!rail + + if (!cardInfo.flowEarlyAccess && !hasApplication) return 'no-flow-access'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Card/cardState.utils.ts` around lines 87 - 123, The early-return on cardInfo.flowEarlyAccess incorrectly blocks users who already have an application; change the check so it only returns 'no-flow-access' when flowEarlyAccess is false AND there is no existing application/rail (i.e., require !overview.status.hasApplication && !overview.status.railStatus), or equivalently move the flowEarlyAccess gate after handling existing rail states (the variables to adjust are cardInfo.flowEarlyAccess, overview.status.hasApplication, and the local rail variable), and make the identical change in the corresponding decision logic in page.tsx so both places consistently exempt users with an existing application/rail.src/app/(mobile-ui)/card/page.tsx (1)
121-135:⚠️ Potential issue | 🟠 MajorDon’t trigger the 404 by calling
notFound()inside a ClientuseEffect
src/app/(mobile-ui)/card/page.tsxcallsnotFound()fromuseEffect(121-135), butnotFound()is meant to abort during the render flow; throwing it fromuseEffectwon’t reliably render Next’s not-found boundary.- The page renders
return nullforstate === 'no-flow-access'(406-410), so if the effect throw doesn’t produce the 404 UI,/cardcan degrade into a blank screen.- Move this gate into render/server code (or use a client-safe navigation plus explicit fallback UI) to make the 404 deterministic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(mobile-ui)/card/page.tsx around lines 121 - 135, The useEffect in page.tsx currently calls notFound() (inside the effect that watches pioneerLoading, pioneerError, cardInfo, overview, overviewLoading) which is invalid for client effects; remove the notFound() call from the client useEffect and instead perform the gate in render/server code where notFound() can be thrown deterministically (check cardInfo, overview, overviewLoading, pioneerLoading/pioneerError and compute hasIssuedCard via overview.cards.some(...)) and call notFound() during render, or if you must keep client behavior, replace the effect's notFound() with a client-safe fallback: capture ANALYTICS_EVENTS.CARD_FLOW_GATED via posthog.capture and use client navigation (useRouter().replace) or set state to 'no-flow-access' and render the explicit fallback UI (the current state === 'no-flow-access' return) so the page never renders a blank screen.src/components/Global/IframeWrapper/StartVerificationView.tsx (1)
20-27:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winVerify
widthandheightprops match actual image dimensions.The
width={100}andheight={100}props are quite small (100×100px) but the rendered image uses responsive sizing (max-w-72= 288px on small screens,max-w-80= 320px on medium). These mismatched dimensions can cause incorrect aspect ratio hints and layout shifts during image loading.Consider using the actual intrinsic dimensions of
PeanutThinkingfor thewidthandheightprops, or omit them if the asset has embedded dimensions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Global/IframeWrapper/StartVerificationView.tsx` around lines 20 - 27, The Image usage in StartVerificationView.tsx currently sets width={100} and height={100} which mismatch the responsive classes; update the Image props for the PeanutThinking asset to use its intrinsic dimensions (or remove the explicit width/height so Next/Image uses embedded metadata) to avoid incorrect aspect-ratio hints and layout shifts—locate the Image element rendering PeanutThinking.src and either replace 100/100 with the asset's real width/height or omit those props so the built-in dimensions are used.src/components/Card/CardWaitlistScreen.tsx (1)
67-74:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winVerify
width/heightvsclassNamesize mismatch.The
width={180}andheight={180}props specify intrinsic dimensions of 180×180px, but theclassName="h-48 w-48"applies 192×192px (Tailwindh-48= 12rem = 192px). This mismatch can cause incorrect aspect ratio hints to the browser during layout and may lead to content shifts or incorrect placeholder sizing during loading.Consider aligning the
width/heightprops with the actual rendered dimensions or removing the explicit size fromclassNameif you want the intrinsic size to control the layout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Card/CardWaitlistScreen.tsx` around lines 67 - 74, The Image component uses mismatched intrinsic props vs CSS classes: Image (src=PeanutPointing.src) sets width={180} height={180} while className="h-48 w-48" renders 192×192; update either the width/height props to 192 (or 12rem equivalent) to match the Tailwind classes, or remove/adjust the className so its h-* and w-* match 180px (or use inline style or responsive classes that rely on intrinsic width/height) to ensure consistent layout/placeholder sizing for the Image component.src/components/Global/SupportDrawer/index.tsx (1)
35-65:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCancel in-flight native Crisp open when the drawer is dismissed.
Line 38 starts an async import, but without cleanup the callback can still execute after close/unmount and reopen native chat unexpectedly.
💡 Suggested fix
useEffect(() => { if (!isSupportModalOpen || !isCapacitor() || isAwaitingToken) return + let cancelled = false - import('`@capgo/capacitor-crisp`').then(({ CapacitorCrisp }) => { + import('`@capgo/capacitor-crisp`') + .then(({ CapacitorCrisp }) => { + if (cancelled) return // set user data before opening if (userData.email || userData.fullName) { CapacitorCrisp.setUser({ email: userData.email || undefined, nickname: userData.fullName || userData.username || undefined, avatar: userData.avatar || undefined, }) } if (crispTokenId) { CapacitorCrisp.setTokenID({ tokenID: crispTokenId }) } // set custom data for support agents if (userData.walletAddress) { CapacitorCrisp.setString({ key: 'wallet_address', value: userData.walletAddress }) } if (userData.userId) { CapacitorCrisp.setString({ key: 'user_id', value: userData.userId }) } if (prefilledMessage) { CapacitorCrisp.sendMessage({ value: prefilledMessage }) } CapacitorCrisp.openMessenger() // close our drawer since native UI takes over setIsSupportModalOpen(false) }) + .catch(() => { + // avoid unhandled rejection on plugin import failure + }) + + return () => { + cancelled = true + } }, [isSupportModalOpen, isAwaitingToken, userData, crispTokenId, prefilledMessage, setIsSupportModalOpen])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Global/SupportDrawer/index.tsx` around lines 35 - 65, The async dynamic import in the useEffect can complete after the drawer is dismissed causing CapacitorCrisp.openMessenger to run unexpectedly; update the effect to register a cancellation flag (or AbortController) and check it before calling any CapacitorCrisp methods (setUser, setTokenID, setString, sendMessage, openMessenger) and before calling setIsSupportModalOpen(false), and return a cleanup function that flips the flag so the imported promise callback is ignored if the component unmounts or isSupportModalOpen becomes false.
🧹 Nitpick comments (2)
src/hooks/useCardInfo.ts (1)
13-13: ConfirmuseCardPioneerInfohas no remaining hook callsites after rename (only a leftover comment).
useCardPioneerInfoappears only in a JSDoc/comment string insrc/hooks/useCardInfo.ts(“FormerlyuseCardPioneerInfo— Pioneer is gone; renamed.”), with no remaining imports/usages.
useCardInfois used in:src/hooks/useHomeCarouselCTAs.tsx,src/components/Home/HomeHistory.tsx,src/components/Profile/index.tsx,src/app/(mobile-ui)/home/page.tsx,src/app/(mobile-ui)/history/page.tsx.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/useCardInfo.ts` at line 13, The JSDoc/comment inside the useCardInfo function still references the old hook name useCardPioneerInfo; remove or update that leftover comment so it correctly reflects the rename to useCardInfo and does not mislead readers. Search the codebase for any remaining occurrences of useCardPioneerInfo to confirm there are no callsites or imports, and if none exist, delete the “Formerly useCardPioneerInfo” comment in the useCardInfo declaration. Ensure the exported hook remains named useCardInfo and that all current callsites use useCardInfo.src/components/Invites/campaign-maps.ts (1)
11-11: ⚡ Quick winReplace relative time reference with an absolute date.
The comment "temporary: maps to arbiverse until 12pm noon tomorrow" uses a relative time that will become stale immediately after merge. Future readers won't know which date "tomorrow" refers to, and the temporary mapping may persist indefinitely.
📅 Suggested fix
Replace with an absolute date or deadline, for example:
- squirrelinvitesyou: 'ARBIVERSE_DEVCONNECT_BA_2025', // temporary: maps to arbiverse until 12pm noon tomorrow + squirrelinvitesyou: 'ARBIVERSE_DEVCONNECT_BA_2025', // temporary: maps to arbiverse until 2026-06-12 12:00 UTCOr add a TODO with a date:
- squirrelinvitesyou: 'ARBIVERSE_DEVCONNECT_BA_2025', // temporary: maps to arbiverse until 12pm noon tomorrow + squirrelinvitesyou: 'ARBIVERSE_DEVCONNECT_BA_2025', // TODO(2026-06-12): remove after arbiverse event ends🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Invites/campaign-maps.ts` at line 11, Update the inline comment next to the mapping key "squirrelinvitesyou" so it uses an absolute date/deadline instead of "tomorrow"; find the line defining squirrelinvitesyou: 'ARBIVERSE_DEVCONNECT_BA_2025' and replace the relative note "temporary: maps to arbiverse until 12pm noon tomorrow" with a clear timestamp (e.g. "temporary: maps to arbiverse until 2025-06-12T12:00:00Z") or add a TODO with a specific date indicating when the mapping should be reviewed/removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`[...recipient]/error.tsx:
- Line 11: Remove the unused router binding to satisfy ESLint: either delete the
const router = useRouter() statement and also remove the useRouter import, or if
you need to keep it for future use rename it to _router (prefix with underscore)
so `@typescript-eslint/no-unused-vars` is satisfied; update the reference in the
component accordingly and remove the import from next/navigation if you delete
the binding.
In `@src/components/Badges/badge.utils.ts`:
- Line 12: getBadgeIcon currently returns PEANUTMAN_LOGO (StaticImageData) as a
fallback which breaks <img src={stamp.badge.iconUrl}> in ShareAssetD3.tsx
because shareAssetLayout.ts expects iconUrl: string; update getBadgeIcon in
badge.utils.ts to always return a string (for example use PEANUTMAN_LOGO.src or
ensure the mapping returns string URLs) so stamp.badge.iconUrl is always a
string and <img src> receives a valid src; adjust any callers or typings if
necessary to reflect the string return.
In `@src/components/Claim/useClaimLink.tsx`:
- Around line 445-452: addParamStep currently preserves an existing method query
param when extra is omitted, causing stale "method" (pix/mercadopago) to
persist; update addParamStep so that if extra?.method is provided you set it,
otherwise remove the method param (use params.delete('method')) when switching
to steps that should not carry a payment method (e.g., when step is not
'regional-claim' or 'regional-req-fulfill'); refer to the addParamStep function
and the URLSearchParams instance named params to implement this deletion before
returning/applying the new search params.
In `@src/components/Jobs/index.tsx`:
- Line 7: The mascot <img> in the Jobs component is missing an alt attribute;
update the JSX in the Jobs component where the image uses PeanutTooCool.src (the
<img src={PeanutTooCool.src} className="h-full w-auto md:h-fit md:w-fit" />) to
include an appropriate alt value—use alt="" if the image is purely decorative or
a short descriptive string (e.g., alt="Peanut mascot") if it conveys
information.
In `@src/components/Profile/views/UnlockedRegions.view.tsx`:
- Around line 113-121: The provider-rejection override currently only checks for
'fixable' or 'blocked' states, so users whose providerRejectionForRegion
(derived from clickedRegionProvider → bridgeRejection or mantecaRejection) is
'restart-identity' aren't being routed to the provider_rejection modal; update
the predicate in hasProviderRejectionForRegion to also treat
providerRejectionForRegion.state === 'restart-identity' as a provider rejection
so modalVariant becomes ('provider_rejection' as const) for that state instead
of falling back to baseModalVariant.
In `@src/components/TransactionDetails/transaction-details.utils.ts`:
- Around line 85-90: normalizeMerchantName currently decides casing based on the
whole string length, which mangles short acronyms inside multi-word names (e.g.,
"US BANK" -> "Us Bank"); update normalizeMerchantName to operate per word: split
the input into words, and for each word if word === word.toUpperCase() and
word.length <= ACRONYM_LENGTH_THRESHOLD keep it uppercase, else if word ===
word.toUpperCase() and word.length > ACRONYM_LENGTH_THRESHOLD convert that word
to title case, otherwise keep the original word; finally join the words back and
return the result (use the existing ACRONYM_LENGTH_THRESHOLD and the function
name normalizeMerchantName to locate where to change).
In `@src/utils/friendly-error.utils.tsx`:
- Around line 99-100: The case-sensitive substring check on text misses variants
like "Not allowed by the user agent"; update the matcher in the
friendly-error.utils.tsx (within the function handling error text, e.g., where
variable text is used) to normalize the input before comparing (e.g., compare
text.toLowerCase() against a lowercase literal "not allowed by the user agent")
so all casing variants hit this passkey-specific branch and return the same
user-friendly message.
In `@src/utils/general.utils.ts`:
- Around line 874-885: toInviteCode currently only strips a single leading '@'
via replace(/^`@/`, ''), so inputs like '@@alice' become '`@alice`'; update to strip
all leading '@' characters by using replace(/^@+/, '') (preserve the trim() and
toLowerCase() behavior) in the toInviteCode function (and the same change at the
other occurrence noted) to ensure canonical bare, lowercased usernames.
In `@src/utils/history.utils.ts`:
- Around line 282-295: The DIRECTION_TO_SIGN mapping incorrectly marks
claim_external as an outflow ('-'), causing claimed incoming links to render as
debits; update the DIRECTION_TO_SIGN record so claim_external maps to '+'
instead. Locate the DIRECTION_TO_SIGN constant in src/utils/history.utils.ts and
change the entry for claim_external from '-' to '+' (keeping other entries
intact) so it aligns with mapEntryStatusToUiStatus() behavior for SEND_LINK
handling and TransactionDirection semantics.
---
Outside diff comments:
In `@src/app/`(mobile-ui)/card/page.tsx:
- Around line 121-135: The useEffect in page.tsx currently calls notFound()
(inside the effect that watches pioneerLoading, pioneerError, cardInfo,
overview, overviewLoading) which is invalid for client effects; remove the
notFound() call from the client useEffect and instead perform the gate in
render/server code where notFound() can be thrown deterministically (check
cardInfo, overview, overviewLoading, pioneerLoading/pioneerError and compute
hasIssuedCard via overview.cards.some(...)) and call notFound() during render,
or if you must keep client behavior, replace the effect's notFound() with a
client-safe fallback: capture ANALYTICS_EVENTS.CARD_FLOW_GATED via
posthog.capture and use client navigation (useRouter().replace) or set state to
'no-flow-access' and render the explicit fallback UI (the current state ===
'no-flow-access' return) so the page never renders a blank screen.
In `@src/components/Card/cardState.utils.ts`:
- Around line 87-123: The early-return on cardInfo.flowEarlyAccess incorrectly
blocks users who already have an application; change the check so it only
returns 'no-flow-access' when flowEarlyAccess is false AND there is no existing
application/rail (i.e., require !overview.status.hasApplication &&
!overview.status.railStatus), or equivalently move the flowEarlyAccess gate
after handling existing rail states (the variables to adjust are
cardInfo.flowEarlyAccess, overview.status.hasApplication, and the local rail
variable), and make the identical change in the corresponding decision logic in
page.tsx so both places consistently exempt users with an existing
application/rail.
In `@src/components/Card/CardWaitlistScreen.tsx`:
- Around line 67-74: The Image component uses mismatched intrinsic props vs CSS
classes: Image (src=PeanutPointing.src) sets width={180} height={180} while
className="h-48 w-48" renders 192×192; update either the width/height props to
192 (or 12rem equivalent) to match the Tailwind classes, or remove/adjust the
className so its h-* and w-* match 180px (or use inline style or responsive
classes that rely on intrinsic width/height) to ensure consistent
layout/placeholder sizing for the Image component.
In `@src/components/Global/IframeWrapper/StartVerificationView.tsx`:
- Around line 20-27: The Image usage in StartVerificationView.tsx currently sets
width={100} and height={100} which mismatch the responsive classes; update the
Image props for the PeanutThinking asset to use its intrinsic dimensions (or
remove the explicit width/height so Next/Image uses embedded metadata) to avoid
incorrect aspect-ratio hints and layout shifts—locate the Image element
rendering PeanutThinking.src and either replace 100/100 with the asset's real
width/height or omit those props so the built-in dimensions are used.
In `@src/components/Global/SupportDrawer/index.tsx`:
- Around line 35-65: The async dynamic import in the useEffect can complete
after the drawer is dismissed causing CapacitorCrisp.openMessenger to run
unexpectedly; update the effect to register a cancellation flag (or
AbortController) and check it before calling any CapacitorCrisp methods
(setUser, setTokenID, setString, sendMessage, openMessenger) and before calling
setIsSupportModalOpen(false), and return a cleanup function that flips the flag
so the imported promise callback is ignored if the component unmounts or
isSupportModalOpen becomes false.
---
Nitpick comments:
In `@src/components/Invites/campaign-maps.ts`:
- Line 11: Update the inline comment next to the mapping key
"squirrelinvitesyou" so it uses an absolute date/deadline instead of "tomorrow";
find the line defining squirrelinvitesyou: 'ARBIVERSE_DEVCONNECT_BA_2025' and
replace the relative note "temporary: maps to arbiverse until 12pm noon
tomorrow" with a clear timestamp (e.g. "temporary: maps to arbiverse until
2025-06-12T12:00:00Z") or add a TODO with a specific date indicating when the
mapping should be reviewed/removed.
In `@src/hooks/useCardInfo.ts`:
- Line 13: The JSDoc/comment inside the useCardInfo function still references
the old hook name useCardPioneerInfo; remove or update that leftover comment so
it correctly reflects the rename to useCardInfo and does not mislead readers.
Search the codebase for any remaining occurrences of useCardPioneerInfo to
confirm there are no callsites or imports, and if none exist, delete the
“Formerly useCardPioneerInfo” comment in the useCardInfo declaration. Ensure the
exported hook remains named useCardInfo and that all current callsites use
useCardInfo.
🪄 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: 326d62fa-a892-45c0-93b0-0da09ca9ed07
⛔ Files ignored due to path filters (68)
public/badges/_archive/founder_house.pngis excluded by!**/*.pngpublic/badges/peanut-pioneer.pngis excluded by!**/*.pngpublic/claim-metadata-img.jpgis excluded by!**/*.jpgpublic/easter-eggs/antarctica.pngis excluded by!**/*.pngpublic/easter-eggs/bouvet.pngis excluded by!**/*.pngpublic/easter-eggs/christmas.pngis excluded by!**/*.pngpublic/easter-eggs/cocos.pngis excluded by!**/*.pngpublic/easter-eggs/heard.pngis excluded by!**/*.pngpublic/easter-eggs/pitcairn.pngis excluded by!**/*.pngpublic/easter-eggs/southgeorgia.pngis excluded by!**/*.pngpublic/easter-eggs/tokelau.pngis excluded by!**/*.pngpublic/email/peanut-jail.pngis excluded by!**/*.pngpublic/email/peanut-wave.pngis excluded by!**/*.pngpublic/game/1x-cloud.pngis excluded by!**/*.pngpublic/game/1x-horizon.pngis excluded by!**/*.pngpublic/game/1x-obstacle-large.pngis excluded by!**/*.pngpublic/game/1x-obstacle-small.pngis excluded by!**/*.pngpublic/game/1x-restart.pngis excluded by!**/*.pngpublic/game/1x-trex.pngis excluded by!**/*.pngpublic/game/2x-cloud.pngis excluded by!**/*.pngpublic/game/2x-horizon.pngis excluded by!**/*.pngpublic/game/2x-obstacle-large.pngis excluded by!**/*.pngpublic/game/2x-obstacle-small.pngis excluded by!**/*.pngpublic/game/2x-restart.pngis excluded by!**/*.pngpublic/game/2x-text.pngis excluded by!**/*.pngpublic/game/2x-trex.pngis excluded by!**/*.pngpublic/icons/apple-touch-icon-152x152-beta.pngis excluded by!**/*.pngpublic/icons/apple-touch-icon-beta.pngis excluded by!**/*.pngpublic/icons/icon-192x192-beta.pngis excluded by!**/*.pngpublic/icons/icon-192x192-maskable.pngis excluded by!**/*.pngpublic/icons/icon-512x512-beta.pngis excluded by!**/*.pngpublic/icons/icon-512x512-maskable.pngis excluded by!**/*.pngpublic/logo-favicon.pngis excluded by!**/*.pngpublic/merchants/badigitalnomads/coworking.jpgis excluded by!**/*.jpgpublic/merchants/stain/profile.jpgis excluded by!**/*.jpgpublic/merchants/stain/tripadvisor-2.jpgis excluded by!**/*.jpgpublic/metadata-img.pngis excluded by!**/*.pngpublic/preview-bg.pngis excluded by!**/*.pngpublic/raffle-metadata-img.pngis excluded by!**/*.pngpublic/redpacket-img.pngis excluded by!**/*.pngpublic/social-preview-bg.pngis excluded by!**/*.pngsrc/assets/cards/Cart Gradient 5.pngis excluded by!**/*.pngsrc/assets/cards/cart-gradient-10-pixel.pngis excluded by!**/*.pngsrc/assets/cards/visa-brand-mark.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-1.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-2.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-3.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-4.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-1.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-2.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-3.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-4.pngis excluded by!**/*.pngsrc/assets/icons/gitbook.pngis excluded by!**/*.pngsrc/assets/icons/github-white.pngis excluded by!**/*.pngsrc/assets/icons/github.pngis excluded by!**/*.pngsrc/assets/icons/small-peanut.pngis excluded by!**/*.pngsrc/assets/icons/star.pngis excluded by!**/*.pngsrc/assets/illustrations/global-cash-local-feel.pngis excluded by!**/*.pngsrc/assets/illustrations/thinking_peanut.gifis excluded by!**/*.gifsrc/assets/iphone-ss/iphone-drop-a-link-mobile.pngis excluded by!**/*.pngsrc/assets/iphone-ss/iphone-drop-a-link.pngis excluded by!**/*.pngsrc/assets/iphone-ss/manteca_ss.pngis excluded by!**/*.pngsrc/assets/mascot/peanut-beer.svgis excluded by!**/*.svgsrc/assets/mascot/peanut-logo.svgis excluded by!**/*.svgsrc/assets/mascot/peanut-pfp.svgis excluded by!**/*.svgsrc/assets/peanut/peanut-club.pngis excluded by!**/*.pngsrc/assets/peanut/peanut-raising-hands.svgis excluded by!**/*.svgsrc/assets/peanut/peanutman-waving.svgis excluded by!**/*.svg
📒 Files selected for processing (146)
.gitmodulespackage.jsonpublic/easter-eggs/antarctica.webppublic/easter-eggs/bouvet.webppublic/easter-eggs/christmas.webppublic/easter-eggs/cocos.webppublic/easter-eggs/heard.webppublic/easter-eggs/pitcairn.webppublic/easter-eggs/southgeorgia.webppublic/easter-eggs/tokelau.webpsrc/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsxsrc/app/(mobile-ui)/card-recovery/page.tsxsrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/dev/components/page.tsxsrc/app/(mobile-ui)/dev/ds/patterns/modal/page.tsxsrc/app/(mobile-ui)/history/page.tsxsrc/app/(mobile-ui)/home/page.tsxsrc/app/(mobile-ui)/layout.tsxsrc/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsxsrc/app/(mobile-ui)/qr-pay/page.tsxsrc/app/(setup)/setup/finish/page.tsxsrc/app/ClientProviders.tsxsrc/app/[...recipient]/error.tsxsrc/app/[locale]/(marketing)/error.tsxsrc/app/actions/history.tssrc/app/crisp-proxy/page.tsxsrc/app/dev/loading-words/page.tsxsrc/app/global-error.jsxsrc/app/layout.tsxsrc/app/maintenance/page.tsxsrc/app/receipt/[entryId]/page.tsxsrc/app/shhhhh/ShhhhhLandingPage.tsxsrc/assets/animationssrc/assets/illustrations/index.tssrc/assets/index.tssrc/assets/iphone-ss/iphone-drop-a-link-mobile.webpsrc/assets/iphone-ss/iphone-drop-a-link.webpsrc/assets/iphone-ss/manteca_ss.webpsrc/assets/mascot/index.tssrc/assets/mascot/peanut-cheering.webpsrc/assets/mascot/peanut-club.webpsrc/assets/mascot/peanut-crying.webpsrc/assets/mascot/peanut-pointing-down.webpsrc/assets/mascot/peanut-pointing.webpsrc/assets/mascot/peanut-sad.webpsrc/assets/mascot/peanut-thinking.webpsrc/assets/mascot/peanut-too-cool.webpsrc/assets/mascot/peanut-walking.webpsrc/assets/mascot/peanut-waving-hello.webpsrc/assets/mascot/peanut-whistling.webpsrc/assets/peanut/index.tssrc/components/Badges/badge.utils.tssrc/components/Card/ApplicationStatusScreen.tsxsrc/components/Card/CardFace.tsxsrc/components/Card/CardWaitlistJoinedScreen.tsxsrc/components/Card/CardWaitlistScreen.tsxsrc/components/Card/PhysicalCardScreen.tsxsrc/components/Card/__tests__/cardApply.utils.test.tssrc/components/Card/__tests__/cardState.utils.test.tssrc/components/Card/cardApply.utils.tssrc/components/Card/cardState.utils.tssrc/components/Card/cardUnlock.types.tssrc/components/Card/share-asset/PixelatedCardFace.tsxsrc/components/Card/share-asset/ShareAssetD3.tsxsrc/components/Card/share-asset/shareAssetLayout.tssrc/components/Claim/Generic/ClaimError.view.tsxsrc/components/Claim/Link/Initial.view.tsxsrc/components/Claim/Link/MantecaFlowManager.tsxsrc/components/Claim/Link/Onchain/Success.view.tsxsrc/components/Claim/Link/SendLinkActionList.tsxsrc/components/Claim/__tests__/claim-states.test.tsxsrc/components/Claim/useClaimLink.tsxsrc/components/Global/ConfirmInviteModal/index.tsxsrc/components/Global/Drawer/index.tsxsrc/components/Global/EasterEggModal/index.tsxsrc/components/Global/EmptyStates/NoDataEmptyState.tsxsrc/components/Global/IframeWrapper/StartVerificationView.tsxsrc/components/Global/KycVerifiedOrReviewModal/index.tsxsrc/components/Global/LazyLoadErrorBoundary/index.tsxsrc/components/Global/NoMoreJailModal/index.tsxsrc/components/Global/PeanutLoading/CyclingLoading.tsxsrc/components/Global/QRScanner/index.tsxsrc/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsxsrc/components/Global/SupportDrawer/index.tsxsrc/components/Home/HomeHistory.tsxsrc/components/Invites/InvitesPage.tsxsrc/components/Invites/JoinWaitlistPage.tsxsrc/components/Invites/campaign-maps.test.tssrc/components/Invites/campaign-maps.tssrc/components/Jobs/index.tsxsrc/components/LandingPage/Manteca.tsxsrc/components/LandingPage/dropLink.tsxsrc/components/LandingPage/hero.tsxsrc/components/Marketing/MarketingErrorBoundary.tsxsrc/components/Marketing/mdx/CTA.tsxsrc/components/Payment/Views/Error.validation.view.tsxsrc/components/Profile/index.tsxsrc/components/Profile/views/UnlockedRegions.view.tsxsrc/components/Setup/Setup.consts.tsxsrc/components/Setup/Views/JoinWaitlist.tsxsrc/components/TransactionDetails/TransactionAvatarBadge.tsxsrc/components/TransactionDetails/TransactionCard.tsxsrc/components/TransactionDetails/TransactionDetailsHeaderCard.tsxsrc/components/TransactionDetails/__tests__/transaction-predicates.test.tssrc/components/TransactionDetails/__tests__/useReceiptViewModel.test.tssrc/components/TransactionDetails/strategies/__tests__/registry.test.tssrc/components/TransactionDetails/strategies/intent/card.tssrc/components/TransactionDetails/strategies/registry.tssrc/components/TransactionDetails/strategies/types.tssrc/components/TransactionDetails/transaction-details.utils.tssrc/components/TransactionDetails/transaction-predicates.tssrc/components/TransactionDetails/transaction-types.tssrc/components/TransactionDetails/transactionTransformer.tssrc/components/TransactionDetails/useReceiptViewModel.tssrc/constants/__tests__/rhino.consts.test.tssrc/constants/analytics.consts.tssrc/constants/invites.consts.tssrc/constants/rhino.consts.tssrc/context/ClaimBankFlowContext.tsxsrc/features/limits/views/LimitsPageView.tsxsrc/features/payments/flows/contribute-pot/components/RequestPotActionList.tsxsrc/features/payments/shared/components/PaymentSuccessView.tsxsrc/features/payments/shared/components/SendWithPeanutCta.tsxsrc/hooks/useCardInfo.tssrc/hooks/useHomeCarouselCTAs.tsxsrc/hooks/useZeroDev.tssrc/hooks/wallet/useSignUserOp.tssrc/services/card.tssrc/services/invites.tssrc/services/rain.tssrc/types/api.generated.tssrc/types/api.openapi.jsonsrc/utils/__tests__/chunk-error-recovery.test.tssrc/utils/__tests__/friendly-error.utils.test.tsxsrc/utils/__tests__/general.utils.test.tssrc/utils/__tests__/history.utils.test.tssrc/utils/__tests__/invite-suffix.test.tssrc/utils/__tests__/regions.utils.test.tssrc/utils/__tests__/webauthn.utils.test.tssrc/utils/chunk-error-recovery.tssrc/utils/friendly-error.utils.tsxsrc/utils/general.utils.tssrc/utils/history.utils.tssrc/utils/regions.utils.tssrc/utils/webauthn.utils.tstsconfig.json
💤 Files with no reviewable changes (10)
- src/assets/illustrations/index.ts
- .gitmodules
- src/services/card.ts
- src/assets/peanut/index.ts
- src/utils/tests/invite-suffix.test.ts
- src/assets/animations
- src/components/Global/KycVerifiedOrReviewModal/index.tsx
- src/app/(mobile-ui)/layout.tsx
- src/app/(mobile-ui)/dev/ds/patterns/modal/page.tsx
- src/app/(mobile-ui)/dev/components/page.tsx
| import { recoverFromChunkError } from '@/utils/chunk-error-recovery' | ||
|
|
||
| export default function PaymentError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { | ||
| const router = useRouter() |
There was a problem hiding this comment.
Remove unused router to satisfy lint checks.
useRouter() at Line 11 is unused and currently flagged by ESLint (@typescript-eslint/no-unused-vars). Remove the binding (and import) or prefix intentionally unused vars with _.
🧰 Tools
🪛 ESLint
[error] 11-11: 'router' is assigned a value but never used. Allowed unused vars must match /^_/u.
(@typescript-eslint/no-unused-vars)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`[...recipient]/error.tsx at line 11, Remove the unused router
binding to satisfy ESLint: either delete the const router = useRouter()
statement and also remove the useRouter import, or if you need to keep it for
future use rename it to _router (prefix with underscore) so
`@typescript-eslint/no-unused-vars` is satisfied; update the reference in the
component accordingly and remove the import from next/navigation if you delete
the binding.
Source: Linters/SAST tools
| // src/acknowledgments/seed-definitions.ts). How the whole system works + how to | ||
| // add a badge: peanut-api-ts/docs/BADGES.md. | ||
| import { PEANUTMAN_LOGO } from '@/assets/peanut' | ||
| import { PEANUTMAN_LOGO } from '@/assets/mascot' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== mascot export and usages =="
rg -n -C2 'PEANUTMAN_LOGO|export .*PEANUTMAN' src/assets src/components/Badges/badge.utils.ts src/components/Card/share-asset/shareAssetLayout.ts src/components/Card/share-asset/ShareAssetD3.tsx
echo
echo "== getBadgeIcon call-chain =="
rg -n -C2 'getBadgeIcon\(|iconUrl:' src/components/Card/share-asset/shareAssetLayout.ts src/components/Card/share-asset/ShareAssetD3.tsxRepository: peanutprotocol/peanut-ui
Length of output: 2129
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== badge.utils.ts: BADGES definition + getBadgeIcon body/type evidence =="
rg -n --context 25 'export const BADGES|const BADGES|BADGES\[' src/components/Badges/badge.utils.ts
rg -n --context 25 'export function getBadgeIcon' src/components/Badges/badge.utils.ts
echo
echo "== shareAssetLayout.ts: iconUrl typing + assignment =="
rg -n --context 25 'iconUrl:|iconUrl' src/components/Card/share-asset/shareAssetLayout.ts
echo
echo "== ShareAssetD3.tsx: iconUrl usage in <img src=...> =="
rg -n --context 25 '<img[^>]+src=|src=\{.*iconUrl' src/components/Card/share-asset/ShareAssetD3.tsx
echo
echo "== mascot svg import consumption (string vs component) =="
rg -n --context 10 'from '\''\./peanut-logo\.svg'\''|PEANUTMAN_LOGO' src/assets/mascot/index.tsRepository: peanutprotocol/peanut-ui
Length of output: 15170
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find svg module declarations (e.g., *.svg -> string/React component) =="
rg -n --hidden --no-ignore-vcs -S "declare module ['\"].*\\.svg['\"]|\\.svg\\s*\\*|module.*svg" . -g'*.d.ts' -g'*.ts' -g'*.tsx'
echo
echo "== Find image type declarations for Next.js StaticImageData =="
rg -n --hidden --no-ignore-vcs -S "StaticImageData|declare module ['\"].*\\.(png|jpg|jpeg|webp)['\"]|\\.webp['\"]" . -g'*.d.ts'Repository: peanutprotocol/peanut-ui
Length of output: 50380
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Repo-local svg module declarations (excluding node_modules) =="
rg -n --hidden --no-ignore-vcs -S "declare module ['\"].*\\.svg['\"]" . --glob '!**/node_modules/**'
echo
echo "== Repo-local image module declarations (StaticImageData / next-image) =="
rg -n --hidden --no-ignore-vcs -S "StaticImageData|declare module ['\"].*\\.(png|jpe?g|webp|svg)['\"]" . --glob '!**/node_modules/**'Repository: peanutprotocol/peanut-ui
Length of output: 134
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Usages of PEANUTMAN_LOGO (and peers) =="
rg -n "PEANUTMAN_(LOGO|PFP|HOLDING_BEER|MOBILE)" src --glob '*.{ts,tsx}' || true
echo
echo "== Usages of getBadgeIcon =="
rg -n "getBadgeIcon\(" src/components --glob '*.{ts,tsx}' || true
echo
echo "== Any '.src' usage on badge/mascot images =="
rg -n "\.src\b.*(PEANUTMAN|getBadgeIcon|iconUrl)" src --glob '*.{ts,tsx}' || true
echo
echo "== TS declarations for image imports (global/next-env/typing files) =="
fd -t f --glob '*env.d.ts' . || true
fd -t f --glob '*.d.ts' . | head -n 50Repository: peanutprotocol/peanut-ui
Length of output: 6701
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== global.d.ts (top 200 lines) =="
sed -n '1,200p' global.d.ts
echo
echo "== src/types/global.d.ts (top 250 lines) =="
sed -n '1,250p' src/types/global.d.ts
echo
echo "== Search within repo d.ts files for svg/webp/png module declarations =="
rg -n "declare module ['\"].*\\.(svg|webp|png|jpg|jpeg)['\"]" -S --glob '*.{d.ts}' . --glob '!**/node_modules/**' || true
echo
echo "== Search within repo d.ts files for StaticImageData =="
rg -n "StaticImageData" -S --glob '*.{d.ts}' . --glob '!**/node_modules/**' || trueRepository: peanutprotocol/peanut-ui
Length of output: 602
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -la
echo "== next-env.d.ts =="
cat next-env.d.ts 2>/dev/null || echo "no next-env.d.ts"
echo
echo "== find likely Next/asset typing files =="
fd -t f -e d.ts -e ts --glob '*env*.d.ts' --glob '*next*.d.ts' . || trueRepository: peanutprotocol/peanut-ui
Length of output: 3670
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Repo-local d.ts files (excluding node_modules) =="
fd -t f --glob '*.d.ts' . --exclude '**/node_modules/**' | sort
echo
echo "== Search repo-local d.ts for svg/staticimage/image module declarations =="
rg -n --no-ignore-vcs -S "(svg|StaticImageData|declare module ['\"].*\\.svg['\"])"
--glob '*.d.ts' . --exclude '**/node_modules/**'Repository: peanutprotocol/peanut-ui
Length of output: 50446
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/components/Badges/index.tsx around getBadgeIcon usage =="
sed -n '1,140p' src/components/Badges/index.tsx
sed -n '1,220p' src/components/Badges/BadgesRow.tsx
sed -n '1,120p' src/components/Badges/BadgeStatusItem.tsx
sed -n '1,140p' src/components/Badges/BadgeStatusDrawer.tsxRepository: peanutprotocol/peanut-ui
Length of output: 18167
Fix badge icon fallback for share-stamp <img src> rendering
getBadgeIcon falls back to PEANUTMAN_LOGO (StaticImageData) at badge.utils.ts while shareAssetLayout.ts types iconUrl: string and ShareAssetD3.tsx renders it via <img src={stamp.badge.iconUrl} />; for unknown badge codes, the fallback ends up as a non-string src.
Suggested fix
-export function getBadgeIcon(code?: string) {
- return (code && BADGES[code]?.path) || PEANUTMAN_LOGO
+export function getBadgeIcon(code?: string): string {
+ return (code && BADGES[code]?.path) || PEANUTMAN_LOGO.src
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Badges/badge.utils.ts` at line 12, getBadgeIcon currently
returns PEANUTMAN_LOGO (StaticImageData) as a fallback which breaks <img
src={stamp.badge.iconUrl}> in ShareAssetD3.tsx because shareAssetLayout.ts
expects iconUrl: string; update getBadgeIcon in badge.utils.ts to always return
a string (for example use PEANUTMAN_LOGO.src or ensure the mapping returns
string URLs) so stamp.badge.iconUrl is always a string and <img src> receives a
valid src; adjust any callers or typings if necessary to reflect the string
return.
| const addParamStep = ( | ||
| step: 'bank' | 'claim' | 'regional-claim' | 'regional-req-fulfill', | ||
| extra?: { method: 'mercadopago' | 'pix' } | ||
| ) => { | ||
| const params = new URLSearchParams(searchParams) | ||
| params.set('step', step) | ||
| if (extra?.method) params.set('method', extra.method) | ||
|
|
There was a problem hiding this comment.
Clear stale method when switching away from regional claim.
addParamStep leaves any existing method query param intact when extra is omitted. That can preserve an old pix/mercadopago selection across later remounts and re-entry flows.
Proposed fix
const addParamStep = (
step: 'bank' | 'claim' | 'regional-claim' | 'regional-req-fulfill',
extra?: { method: 'mercadopago' | 'pix' }
) => {
const params = new URLSearchParams(searchParams)
params.set('step', step)
- if (extra?.method) params.set('method', extra.method)
+ if (extra?.method) {
+ params.set('method', extra.method)
+ } else {
+ params.delete('method')
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const addParamStep = ( | |
| step: 'bank' | 'claim' | 'regional-claim' | 'regional-req-fulfill', | |
| extra?: { method: 'mercadopago' | 'pix' } | |
| ) => { | |
| const params = new URLSearchParams(searchParams) | |
| params.set('step', step) | |
| if (extra?.method) params.set('method', extra.method) | |
| const addParamStep = ( | |
| step: 'bank' | 'claim' | 'regional-claim' | 'regional-req-fulfill', | |
| extra?: { method: 'mercadopago' | 'pix' } | |
| ) => { | |
| const params = new URLSearchParams(searchParams) | |
| params.set('step', step) | |
| if (extra?.method) { | |
| params.set('method', extra.method) | |
| } else { | |
| params.delete('method') | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Claim/useClaimLink.tsx` around lines 445 - 452, addParamStep
currently preserves an existing method query param when extra is omitted,
causing stale "method" (pix/mercadopago) to persist; update addParamStep so that
if extra?.method is provided you set it, otherwise remove the method param (use
params.delete('method')) when switching to steps that should not carry a payment
method (e.g., when step is not 'regional-claim' or 'regional-req-fulfill');
refer to the addParamStep function and the URLSearchParams instance named params
to implement this deletion before returning/applying the new search params.
| <div className="flex h-full flex-col-reverse items-center justify-center lg:flex-row"> | ||
| <div className="w-4/5 md:w-1/2"> | ||
| <img src={PeanutGuyGIF.src} className="h-full w-auto md:h-fit md:w-fit" /> | ||
| <img src={PeanutTooCool.src} className="h-full w-auto md:h-fit md:w-fit" /> |
There was a problem hiding this comment.
Add an alt attribute to the mascot image.
The <img> is missing alt. Use alt="" if decorative, or a meaningful description if informational.
Proposed fix
-<img src={PeanutTooCool.src} className="h-full w-auto md:h-fit md:w-fit" />
+<img src={PeanutTooCool.src} alt="" className="h-full w-auto md:h-fit md:w-fit" />📝 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.
| <img src={PeanutTooCool.src} className="h-full w-auto md:h-fit md:w-fit" /> | |
| <img src={PeanutTooCool.src} alt="" className="h-full w-auto md:h-fit md:w-fit" /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Jobs/index.tsx` at line 7, The mascot <img> in the Jobs
component is missing an alt attribute; update the JSX in the Jobs component
where the image uses PeanutTooCool.src (the <img src={PeanutTooCool.src}
className="h-full w-auto md:h-fit md:w-fit" />) to include an appropriate alt
value—use alt="" if the image is purely decorative or a short descriptive string
(e.g., alt="Peanut mascot") if it conveys information.
| // override modal variant when sumsub is approved but a provider rejected the user. | ||
| // Use the same provider→intent map as the cross-region check below for consistency. | ||
| const providerRejectionForRegion = | ||
| providerForRegionIntent(clickedRegionIntent) === 'bridge' ? bridgeRejection : mantecaRejection | ||
| // ROW has no provider (clickedRegionProvider null) → no provider rejection can apply. | ||
| const providerRejectionForRegion = clickedRegionProvider === 'bridge' ? bridgeRejection : mantecaRejection | ||
| const hasProviderRejectionForRegion = | ||
| !!selectedRegion && | ||
| clickedRegionProvider !== null && | ||
| isSumsubApproved && | ||
| (providerRejectionForRegion.state === 'fixable' || providerRejectionForRegion.state === 'blocked') | ||
| const modalVariant = hasProviderRejectionForRegion ? ('provider_rejection' as const) : baseModalVariant |
There was a problem hiding this comment.
Include restart-identity in the provider-rejection override.
providerRejectionForRegion.state === 'restart-identity' already has dedicated copy/CTA in the ActionModal below, but this predicate never promotes that state to provider_rejection. Those users fall back to the normal start flow instead of the restart-identity recovery path.
Suggested fix
const hasProviderRejectionForRegion =
!!selectedRegion &&
clickedRegionProvider !== null &&
isSumsubApproved &&
- (providerRejectionForRegion.state === 'fixable' || providerRejectionForRegion.state === 'blocked')
+ (providerRejectionForRegion.state === 'fixable' ||
+ providerRejectionForRegion.state === 'restart-identity' ||
+ providerRejectionForRegion.state === 'blocked')🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Profile/views/UnlockedRegions.view.tsx` around lines 113 -
121, The provider-rejection override currently only checks for 'fixable' or
'blocked' states, so users whose providerRejectionForRegion (derived from
clickedRegionProvider → bridgeRejection or mantecaRejection) is
'restart-identity' aren't being routed to the provider_rejection modal; update
the predicate in hasProviderRejectionForRegion to also treat
providerRejectionForRegion.state === 'restart-identity' as a provider rejection
so modalVariant becomes ('provider_rejection' as const) for that state instead
of falling back to baseModalVariant.
| const ACRONYM_LENGTH_THRESHOLD = 4 | ||
| export function normalizeMerchantName(raw: string): string { | ||
| if (raw !== raw.toUpperCase()) return raw | ||
| if (raw.length <= ACRONYM_LENGTH_THRESHOLD) return raw | ||
| return raw.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase()) | ||
| } |
There was a problem hiding this comment.
Acronyms inside multi-word merchant names are still being mangled.
The current threshold check uses total string length, so names like US BANK will normalize to Us Bank. Preserve acronym casing per word, not per full string.
Suggested fix
export function normalizeMerchantName(raw: string): string {
if (raw !== raw.toUpperCase()) return raw
- if (raw.length <= ACRONYM_LENGTH_THRESHOLD) return raw
- return raw.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase())
+ return raw
+ .split(/(\s+)/)
+ .map((part) => {
+ if (!/^[A-Z]+$/.test(part)) return part
+ if (part.length <= ACRONYM_LENGTH_THRESHOLD) return part
+ const lower = part.toLowerCase()
+ return lower.replace(/\b\w/g, (c) => c.toUpperCase())
+ })
+ .join('')
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/TransactionDetails/transaction-details.utils.ts` around lines
85 - 90, normalizeMerchantName currently decides casing based on the whole
string length, which mangles short acronyms inside multi-word names (e.g., "US
BANK" -> "Us Bank"); update normalizeMerchantName to operate per word: split the
input into words, and for each word if word === word.toUpperCase() and
word.length <= ACRONYM_LENGTH_THRESHOLD keep it uppercase, else if word ===
word.toUpperCase() and word.length > ACRONYM_LENGTH_THRESHOLD convert that word
to title case, otherwise keep the original word; finally join the words back and
return the result (use the existing ACRONYM_LENGTH_THRESHOLD and the function
name normalizeMerchantName to locate where to change).
| if (text.includes('not allowed by the user agent')) | ||
| return "Your device didn't complete the passkey confirmation. Try again — if it keeps failing, unlock your password manager (e.g. 1Password) or restart your device." |
There was a problem hiding this comment.
The passkey-specific matcher is case-sensitive and can miss real NotAllowed variants.
Normalize case before matching so "Not allowed by the user agent" and similar casing still hit this user-friendly branch.
Suggested fix
- if (text.includes('not allowed by the user agent'))
+ if (text.toLowerCase().includes('not allowed by the user agent'))
return "Your device didn't complete the passkey confirmation. Try again — if it keeps failing, unlock your password manager (e.g. 1Password) or restart your device."📝 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 (text.includes('not allowed by the user agent')) | |
| return "Your device didn't complete the passkey confirmation. Try again — if it keeps failing, unlock your password manager (e.g. 1Password) or restart your device." | |
| if (text.toLowerCase().includes('not allowed by the user agent')) | |
| return "Your device didn't complete the passkey confirmation. Try again — if it keeps failing, unlock your password manager (e.g. 1Password) or restart your device." |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/friendly-error.utils.tsx` around lines 99 - 100, The case-sensitive
substring check on text misses variants like "Not allowed by the user agent";
update the matcher in the friendly-error.utils.tsx (within the function handling
error text, e.g., where variable text is used) to normalize the input before
comparing (e.g., compare text.toLowerCase() against a lowercase literal "not
allowed by the user agent") so all casing variants hit this passkey-specific
branch and return the same user-friendly message.
| * Canonical invite-code shape: a bare, lowercased username (e.g. `alice`). | ||
| * | ||
| * Duplicated on the backend (peanut-api-ts/src/utils/invite.ts). Parity is | ||
| * enforced by shared test vectors in __tests__/invite-suffix.test.ts and | ||
| * peanut-api-ts/src/utils/invite.test.ts. Don't edit one without the other. | ||
| * Single source of truth — use this anywhere an invite code is built for | ||
| * `/invite?code=…` or `acceptInvite`. The legacy `ALICEINVITESYOU610` / | ||
| * `ALICEINVITESYOU` shapes are no longer emitted, but stay fully supported on | ||
| * the backend (peanut-api-ts `extractUsernameFromInvite` uppercases the input | ||
| * and matches the old suffixes), so existing shared links keep working. | ||
| * | ||
| * Also tolerates hand-typed input ("Who invited you?" asks for a username, so | ||
| * people paste `@alice ` or ` Alice`): trims whitespace and strips a leading @. | ||
| */ | ||
| export const generateInviteCodeSuffix = (username: string): string => { | ||
| const lowerUsername = username.toLowerCase() | ||
| // Create a simple hash from the username | ||
| const hash = lowerUsername.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) | ||
| // Generate 3 digits between 100-999 | ||
| const threeDigits = 100 + (hash % 900) | ||
| return threeDigits.toString() | ||
| } | ||
| export const toInviteCode = (username: string): string => username.trim().replace(/^@/, '').toLowerCase() |
There was a problem hiding this comment.
Strip all leading @ characters during invite-code normalization.
replace(/^@/, '') removes only a single leading @. Inputs like @@alice normalize to @alice, which violates the canonical bare-username format and can break invite matching for hand-typed handles.
💡 Proposed fix
-export const toInviteCode = (username: string): string => username.trim().replace(/^`@/`, '').toLowerCase()
+export const toInviteCode = (username: string): string => username.trim().replace(/^@+/, '').toLowerCase()Also applies to: 888-889
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/general.utils.ts` around lines 874 - 885, toInviteCode currently
only strips a single leading '@' via replace(/^`@/`, ''), so inputs like '@@alice'
become '`@alice`'; update to strip all leading '@' characters by using
replace(/^@+/, '') (preserve the trim() and toLowerCase() behavior) in the
toInviteCode function (and the same change at the other occurrence noted) to
ensure canonical bare, lowercased usernames.
| const DIRECTION_TO_SIGN: Record<TransactionDirection, '-' | '+'> = { | ||
| send: '-', | ||
| request_received: '-', | ||
| withdraw: '-', | ||
| bank_withdraw: '-', | ||
| bank_claim: '-', | ||
| claim_external: '-', | ||
| qr_payment: '-', | ||
| receive: '+', | ||
| request_sent: '+', | ||
| add: '+', | ||
| bank_deposit: '+', | ||
| bank_request_fulfillment: '+', | ||
| } |
There was a problem hiding this comment.
claim_external is signed as an outflow.
mapEntryStatusToUiStatus() in src/components/TransactionDetails/transactionTransformer.ts already treats claim_external as the claimed side of SEND_LINK (direction !== 'claim_external' is the sender-side branch). Mapping it to - here makes incoming claimed links render as debits in the feed.
Suggested fix
const DIRECTION_TO_SIGN: Record<TransactionDirection, '-' | '+'> = {
send: '-',
request_received: '-',
withdraw: '-',
bank_withdraw: '-',
bank_claim: '-',
- claim_external: '-',
+ claim_external: '+',
qr_payment: '-',
receive: '+',
request_sent: '+',
add: '+',
bank_deposit: '+',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const DIRECTION_TO_SIGN: Record<TransactionDirection, '-' | '+'> = { | |
| send: '-', | |
| request_received: '-', | |
| withdraw: '-', | |
| bank_withdraw: '-', | |
| bank_claim: '-', | |
| claim_external: '-', | |
| qr_payment: '-', | |
| receive: '+', | |
| request_sent: '+', | |
| add: '+', | |
| bank_deposit: '+', | |
| bank_request_fulfillment: '+', | |
| } | |
| const DIRECTION_TO_SIGN: Record<TransactionDirection, '-' | '+'> = { | |
| send: '-', | |
| request_received: '-', | |
| withdraw: '-', | |
| bank_withdraw: '-', | |
| bank_claim: '-', | |
| claim_external: '+', | |
| qr_payment: '-', | |
| receive: '+', | |
| request_sent: '+', | |
| add: '+', | |
| bank_deposit: '+', | |
| bank_request_fulfillment: '+', | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/history.utils.ts` around lines 282 - 295, The DIRECTION_TO_SIGN
mapping incorrectly marks claim_external as an outflow ('-'), causing claimed
incoming links to render as debits; update the DIRECTION_TO_SIGN record so
claim_external maps to '+' instead. Locate the DIRECTION_TO_SIGN constant in
src/utils/history.utils.ts and change the entry for claim_external from '-' to
'+' (keeping other entries intact) so it aligns with mapEntryStatusToUiStatus()
behavior for SEND_LINK handling and TransactionDirection semantics.
Prod release 2026-06-11. 14 PRs. 214 files, +16.4k/−16.6k — bulk is asset consolidation/optimization (~9.4MB saved); code surface is concentrated in Crisp, chunk recovery, card states, waitlist, history/receipts.
Pre-flight (already done)
mainhad 0 commits not indev.41aacd67a) already live on staging.peanut.me, deploy verified (200, sha match).src/assets/animationssubmodule deleted on dev (mascot assets moved in-repo) — already building green on Vercel staging.NEXT_PUBLIC_API_VERSIONbump needed (no breaking API change).Changelog
Reliability — every page (biggest blast radius)
(mobile-ui)) +rain_cooldown_hitanalytics (pairs with api fix: links v2 send flow qa fixes #1008).NotAllowedErrorduring signing (~45 users/30d, 89% iOS, previously generic "contact support").Support chat — privacy fix
Card
?t=receipt back-compat (t=3/9/10/11), per-type switches → config maps.Growth / waitlist
?code=<username>, suffix dropped from UX, hand-typed inviter usernames tolerated.Deposits
Assets
Risk-prioritized testing guide
P0 — blast radius / privacy / money
/,/home, a pay link, and a claim link — all render, no reload loop (script fires only on chunk-load failure). This script runs on every page; a bug here is a full-site incident. Watch Vercel runtime logs + Sentry for reload storms in the first hour.P1 — core funnels
?code=<username>works end-to-end; hand-typed wrong username → friendly "check the inviter's exact handle" error; signup completes and badge/invite attribution lands.?t=3receipt link → renders (no 404); card transactions show consistent sign + FX rate; recent send/claim/offramp rows unchanged.P2 — secondary surfaces
(mobile-ui)); countdown runs (with api chore: disable referral #1009's retryAfterSec)./shhhhhdoor, share asset — all images/animations render, no 404s (submodule removal + 80% raster rewrite).Deploy plan
main→ peanut.me.ops/post-release-monitoring.md— priority: chunk-reload telemetry, Crisp session errors,withdraw_failedPostHog,rain_cooldown_hit.Rollback: pure FE — instant Vercel rollback to previous deployment; no contract/migration coupling (api #1009 is backward-compatible with current prod UI).