Skip to content

release: dev→main 2026-06-11 — Crisp isolation, chunk recovery, card failure states, invite UX, asset overhaul#2218

Merged
Hugo0 merged 91 commits into
mainfrom
dev
Jun 11, 2026
Merged

release: dev→main 2026-06-11 — Crisp isolation, chunk recovery, card failure states, invite UX, asset overhaul#2218
Hugo0 merged 91 commits into
mainfrom
dev

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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)

  • ✅ No backmerge needed — main had 0 commits not in dev.
  • ✅ dev head (41aacd67a) already live on staging.peanut.me, deploy verified (200, sha match).
  • ✅ API contract: types regenerated against peanut-api-ts dev; openapi drift is tiny (celebration-seen revert only) and verified consistent. Merge peanut-api-ts chore: disable referral #1009 first.
  • src/assets/animations submodule deleted on dev (mascot assets moved in-repo) — already building green on Vercel staging.
  • ✅ No service-worker NEXT_PUBLIC_API_VERSION bump needed (no breaking API change).

Changelog

Reliability — every page (biggest blast radius)

Support chat — privacy fix

Card

Growth / waitlist

Deposits

Assets

Risk-prioritized testing guide

P0 — blast radius / privacy / money

  1. Chunk-recovery inline script: after deploy, hard-load /, /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.
  2. Crisp isolation: device with user A logged in → open support chat, send message → log out, log in as user B → chat must show no trace of A's history and still work for B. Repeat once on native app if available (fix(crisp): gate native Crisp open on token, closing the same bleed on Capacitor #2213).
  3. Signing flows (money path): send + claim + card withdraw on prod after deploy. On iOS: cancel the passkey prompt mid-sign → actionable retry message (not generic support copy); then complete it → succeeds. Confirms fix: actionable UX + telemetry for WebAuthn NotAllowedError during signing #2210 didn't disturb the happy path.

P1 — core funnels

  1. Card rail failure states: user with a FAILED/REQUIRES_INFORMATION rail lands on the remediation screen — not the add-card loop. Unknown/missing railStatus → default-deny (no crash).
  2. Invite funnel: fresh signup via bare ?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.
  3. Receipts/history back-compat: open an old ?t=3 receipt link → renders (no 404); card transactions show consistent sign + FX rate; recent send/claim/offramp rows unchanged.

P2 — secondary surfaces

  1. ETH deposit: deposit screen advertises ETH on EVM networks only; confirmation modal shows correct token/network; non-EVM rails unaffected.
  2. Rain cooldown modal: visible on a public qr-pay page during cooldown (previously only (mobile-ui)); countdown runs (with api chore: disable referral #1009's retryAfterSec).
  3. Mascot/assets sweep: landing hero, setup intro, card waitlist, /shhhhh door, share asset — all images/animations render, no 404s (submodule removal + 80% raster rewrite).
  4. KYC region loop: "Unlock " from a non-home region → progresses or honest unsupported message — no silent no-op loop (fix(kyc): close the remaining 'Unlock region' loop doors + claim-geo via URL #2206).

Deploy plan

  1. Merge after peanut-api-ts chore: disable referral #1009 is live on Render.
  2. Vercel auto-deploys main → peanut.me.
  3. Post-deploy sweep per ops/post-release-monitoring.md — priority: chunk-reload telemetry, Crisp session errors, withdraw_failed PostHog, 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).

0xkkonrad and others added 30 commits May 28, 2026 16:45
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.
…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.
Hugo0 and others added 17 commits June 10, 2026 23:30
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).
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jun 11, 2026 9:51am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c4276f3-36e8-4126-b4b4-eacbc455c524

📥 Commits

Reviewing files that changed from the base of the PR and between 41aacd6 and 766aa05.

📒 Files selected for processing (5)
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/assets/illustrations/index.ts
  • src/components/Card/share-asset/ShareAssetD3.tsx
  • src/components/Card/share-asset/shareAssetLayout.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/Card/share-asset/ShareAssetD3.tsx
  • src/assets/illustrations/index.ts

Walkthrough

Consolidates 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.

Changes

Unified product and infra updates

Layer / File(s) Summary
Assets and barrels
.gitmodules, tsconfig.json, src/assets/*, src/assets/mascot/*, src/assets/peanut/*
Removes animations submodule, adds @/assets/mascot barrel, reorders barrel exports, switches many components to mascot WebP/SVG assets and updates TypeScript paths.
App routes & global wiring
src/app/*, src/app/layout.tsx, src/app/ClientProviders.tsx, src/app/global-error.jsx
Injects chunk-recovery script, mounts RainCooldownIntroModal in client providers, updates global error handlers to call chunk recovery, and adjusts layout modal mounting order.
Card flows and history
src/app/(mobile-ui)/card/*, src/app/(mobile-ui)/card-recovery/page.tsx, src/app/(mobile-ui)/history/page.tsx, src/components/Card/*, src/components/Home/HomeHistory.tsx, src/hooks/useCardInfo.ts
Renames/uses useCardInfo, expands card top-level states (requires-info, requires-support), removes press-door early-access wiring, updates recovery completion amount, injects synthetic card-unlock history entries, and revises card state machine and polling helpers/tests.
Support drawer & Crisp gating
src/components/Global/SupportDrawer/*, src/app/crisp-proxy/page.tsx
Gates Crisp mounting/opening until token resolves for logged-in users, updates iframe/native flows, and adapts runtime config.
Invite/claim normalization & waitlist
src/utils/general.utils.ts, src/services/invites.ts, src/components/Invites/*, src/components/Claim/*, src/context/ClaimBankFlowContext.tsx
Adds toInviteCode, removes legacy suffix generator, normalizes invite payloads, persists regional claim method across auth, introduces invite/campaign maps and shared invite error constant.
Transaction details / receipts
src/components/TransactionDetails/*, src/utils/history.utils.ts, src/components/TransactionDetails/transaction-types.ts
Centralizes TransactionDirection/TransactionType, adds isFxBearingFlow and FIAT_RAIL_KINDS, refactors receipt-kind resolution/back-compat, normalizes merchant names, and updates sign/receipt routing logic with tests.
Rain, analytics & services
src/services/rain.ts, src/constants/analytics.consts.ts, src/services/card.ts, src/types/api.generated.ts
Adds RAIN_COOLDOWN_HIT metric, records rain cooldown telemetry, removes grantFlowEarlyAccess, updates API contract types and Rain card readiness/recover endpoints.
Chunk-load recovery and tests
src/utils/chunk-error-recovery.ts, src/utils/__tests__/*
Introduces isChunkLoadError, recoverFromChunkError, inline CHUNK_ERROR_RECOVERY_SCRIPT, and comprehensive tests plus hooks into error boundaries and global handlers.
Various UI swaps and tests
src/components/*, src/app/(mobile-ui)/qr-pay/*, many __tests__ files
Widespread mascot asset replacements, WebP swaps, QR pay PIX min-amount handling and tests, and added/updated unit tests across utilities and components.

Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jjramirezn
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5845.67 → 5815.92 (-29.75)
Findings: +15 net (+612 new, -597 resolved)

🆕 New findings (612)

  • critical complexity — src/app/(mobile-ui)/qr-pay/page.tsx — CC 295, MI 53.26, SLOC 956
  • critical complexity — src/components/Claim/Link/Initial.view.tsx — CC 212, MI 51.11, SLOC 686
  • critical complexity — src/utils/general.utils.ts — CC 199, MI 57.49, SLOC 755
  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 95, MI 57.45, SLOC 376
  • critical complexity — src/components/TransactionDetails/useReceiptViewModel.ts — CC 93, MI 61.89, SLOC 174
  • critical complexity — src/components/Home/HomeHistory.tsx — CC 91, MI 58.57, SLOC 294
  • critical method-complexity — src/app/(mobile-ui)/qr-pay/page.tsx:92 — QRPayPage CC 74 SLOC 329
  • critical complexity — src/components/Invites/JoinWaitlistPage.tsx — CC 67, MI 57.99, SLOC 204
  • critical complexity — src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx — CC 66, MI 48.41, SLOC 116
  • critical complexity — src/app/receipt/[entryId]/page.tsx — CC 60, MI 48.49, SLOC 147
  • critical complexity — src/components/Profile/views/UnlockedRegions.view.tsx — CC 56, MI 62.4, SLOC 150
  • critical complexity — src/app/(mobile-ui)/history/page.tsx — CC 54, MI 56.8, SLOC 218
  • critical complexity — src/components/Invites/InvitesPage.tsx — CC 54, MI 57.17, SLOC 122
  • critical complexity — src/services/rain.ts — CC 54, MI 60.71, SLOC 235
  • critical complexity — src/utils/friendly-error.utils.tsx — CC 51, MI 45.57, SLOC 139
  • critical complexity — src/components/TransactionDetails/TransactionCard.tsx — CC 50, MI 49.6, SLOC 152
  • high hotspot — src/app/(mobile-ui)/qr-pay/page.tsx — 87 commits, +1064/-1017 lines since 6 months ago
  • high structural-dup — src/types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with src/types/api.generated.ts:9584
  • high structural-dup — src/types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with src/types/api.generated.ts:9670
  • high structural-dup — src/types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with src/types/api.generated.ts:9261

…and 592 more.

✅ Resolved (597)

  • src/app/(mobile-ui)/qr-pay/page.tsx — CC 291, MI 53.27, SLOC 942
  • src/components/Claim/Link/Initial.view.tsx — CC 210, MI 51.21, SLOC 680
  • src/utils/general.utils.ts — CC 200, MI 57.46, SLOC 764
  • src/app/(mobile-ui)/card/page.tsx — CC 101, MI 57.21, SLOC 401
  • src/components/TransactionDetails/useReceiptViewModel.ts — CC 95, MI 61.88, SLOC 173
  • src/components/Home/HomeHistory.tsx — CC 93, MI 58.3, SLOC 300
  • src/app/(mobile-ui)/qr-pay/page.tsx:82 — QRPayPage CC 74 SLOC 329
  • src/components/Profile/views/UnlockedRegions.view.tsx — CC 67, MI 62.79, SLOC 173
  • src/components/TransactionDetails/TransactionCard.tsx — CC 67, MI 48.88, SLOC 159
  • src/components/Invites/JoinWaitlistPage.tsx — CC 66, MI 58.2, SLOC 204
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx — CC 66, MI 48.28, SLOC 116
  • src/app/receipt/[entryId]/page.tsx — CC 60, MI 48.71, SLOC 145
  • src/utils/history.utils.ts — CC 59, MI 45.51, SLOC 163
  • src/components/Invites/InvitesPage.tsx — CC 54, MI 56.51, SLOC 130
  • src/services/rain.ts — CC 54, MI 60.83, SLOC 232
  • src/app/(mobile-ui)/history/page.tsx — CC 50, MI 58.01, SLOC 200
  • src/utils/friendly-error.utils.tsx — CC 50, MI 45.82, SLOC 136
  • src/app/(mobile-ui)/qr-pay/page.tsx — 78 commits, +1022/-997 lines since 6 months ago
  • src/types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with src/types/api.generated.ts:9376
  • src/types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with src/types/api.generated.ts:9462

…and 577 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/chunk-error-recovery.ts 0.0 6.9 +6.9
src/hooks/useCardInfo.ts 0.0 6.3 +6.3
src/components/Invites/campaign-maps.ts 0.0 3.5 +3.5
src/components/Card/ApplicationStatusScreen.tsx 6.7 8.2 +1.5
src/assets/mascot/index.ts 0.0 1.5 +1.5
src/app/shhhhh/ShhhhhLandingPage.tsx 12.4 13.7 +1.3
src/components/TransactionDetails/strategies/registry.ts 5.3 6.5 +1.2
src/components/Card/cardState.utils.ts 5.5 6.6 +1.1
src/utils/regions.utils.ts 6.5 7.2 +0.7
src/components/TransactionDetails/transaction-predicates.ts 4.7 5.4 +0.7
src/components/Setup/Setup.consts.tsx 7.2 7.8 +0.6
src/app/[locale]/(marketing)/error.tsx 2.7 3.3 +0.6
src/components/Global/SupportDrawer/index.tsx 8.3 8.9 +0.6
src/constants/invites.consts.ts 0.0 0.6 +0.6
src/components/Invites/JoinWaitlistPage.tsx 12.1 12.7 +0.6
src/components/Card/CardWaitlistJoinedScreen.tsx 4.7 5.3 +0.6
src/app/(mobile-ui)/card-recovery/page.tsx 8.6 9.1 +0.5
src/components/Global/NoMoreJailModal/index.tsx 5.8 6.3 +0.5
src/app/(mobile-ui)/history/page.tsx 10.0 10.5 +0.5
src/components/Global/InvitesGraph/index.tsx 34.7 34.2 -0.5

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1427 ran, 0 failed, 0 skipped, 20.1s

📊 Coverage (unit)

metric %
statements 51.4%
branches 33.6%
functions 39.3%
lines 51.4%
⏱ 10 slowest test cases
time test
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
0.1s src/components/Claim/__tests__/claim-states.test.tsx › Initial render shows loading spinner
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle unresolvable ENS name
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid German IBAN
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (missing 0x prefix)
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ENS name
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 11, 2026
Hugo0 and others added 3 commits June 11, 2026 10:41
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Bypass the outer gate for users who already have an application.

The no-flow-access return still runs before any railStatus handling. That means a user with flowEarlyAccess: false and an existing application (PENDING, REQUIRES_INFORMATION, REQUIRES_SUPPORT, etc.) is forced to no-flow-access instead of seeing the remediation/review screen for the application they already started. Please exempt overview.status.hasApplication / known rail states here, and mirror the same condition in src/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 | 🟠 Major

Don’t trigger the 404 by calling notFound() inside a Client useEffect

  • src/app/(mobile-ui)/card/page.tsx calls notFound() from useEffect (121-135), but notFound() is meant to abort during the render flow; throwing it from useEffect won’t reliably render Next’s not-found boundary.
  • The page renders return null for state === 'no-flow-access' (406-410), so if the effect throw doesn’t produce the 404 UI, /card can 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 win

Verify width and height props match actual image dimensions.

The width={100} and height={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 PeanutThinking for the width and height props, 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 win

Verify width/height vs className size mismatch.

The width={180} and height={180} props specify intrinsic dimensions of 180×180px, but the className="h-48 w-48" applies 192×192px (Tailwind h-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/height props with the actual rendered dimensions or removing the explicit size from className if 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 win

Cancel 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: Confirm useCardPioneerInfo has no remaining hook callsites after rename (only a leftover comment).

useCardPioneerInfo appears only in a JSDoc/comment string in src/hooks/useCardInfo.ts (“Formerly useCardPioneerInfo — Pioneer is gone; renamed.”), with no remaining imports/usages.

useCardInfo is 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 win

Replace 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 UTC

Or 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

📥 Commits

Reviewing files that changed from the base of the PR and between d34c758 and 41aacd6.

⛔ Files ignored due to path filters (68)
  • public/badges/_archive/founder_house.png is excluded by !**/*.png
  • public/badges/peanut-pioneer.png is excluded by !**/*.png
  • public/claim-metadata-img.jpg is excluded by !**/*.jpg
  • public/easter-eggs/antarctica.png is excluded by !**/*.png
  • public/easter-eggs/bouvet.png is excluded by !**/*.png
  • public/easter-eggs/christmas.png is excluded by !**/*.png
  • public/easter-eggs/cocos.png is excluded by !**/*.png
  • public/easter-eggs/heard.png is excluded by !**/*.png
  • public/easter-eggs/pitcairn.png is excluded by !**/*.png
  • public/easter-eggs/southgeorgia.png is excluded by !**/*.png
  • public/easter-eggs/tokelau.png is excluded by !**/*.png
  • public/email/peanut-jail.png is excluded by !**/*.png
  • public/email/peanut-wave.png is excluded by !**/*.png
  • public/game/1x-cloud.png is excluded by !**/*.png
  • public/game/1x-horizon.png is excluded by !**/*.png
  • public/game/1x-obstacle-large.png is excluded by !**/*.png
  • public/game/1x-obstacle-small.png is excluded by !**/*.png
  • public/game/1x-restart.png is excluded by !**/*.png
  • public/game/1x-trex.png is excluded by !**/*.png
  • public/game/2x-cloud.png is excluded by !**/*.png
  • public/game/2x-horizon.png is excluded by !**/*.png
  • public/game/2x-obstacle-large.png is excluded by !**/*.png
  • public/game/2x-obstacle-small.png is excluded by !**/*.png
  • public/game/2x-restart.png is excluded by !**/*.png
  • public/game/2x-text.png is excluded by !**/*.png
  • public/game/2x-trex.png is excluded by !**/*.png
  • public/icons/apple-touch-icon-152x152-beta.png is excluded by !**/*.png
  • public/icons/apple-touch-icon-beta.png is excluded by !**/*.png
  • public/icons/icon-192x192-beta.png is excluded by !**/*.png
  • public/icons/icon-192x192-maskable.png is excluded by !**/*.png
  • public/icons/icon-512x512-beta.png is excluded by !**/*.png
  • public/icons/icon-512x512-maskable.png is excluded by !**/*.png
  • public/logo-favicon.png is excluded by !**/*.png
  • public/merchants/badigitalnomads/coworking.jpg is excluded by !**/*.jpg
  • public/merchants/stain/profile.jpg is excluded by !**/*.jpg
  • public/merchants/stain/tripadvisor-2.jpg is excluded by !**/*.jpg
  • public/metadata-img.png is excluded by !**/*.png
  • public/preview-bg.png is excluded by !**/*.png
  • public/raffle-metadata-img.png is excluded by !**/*.png
  • public/redpacket-img.png is excluded by !**/*.png
  • public/social-preview-bg.png is excluded by !**/*.png
  • src/assets/cards/Cart Gradient 5.png is excluded by !**/*.png
  • src/assets/cards/cart-gradient-10-pixel.png is excluded by !**/*.png
  • src/assets/cards/visa-brand-mark.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-1.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-2.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-3.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-4.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-1.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-2.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-3.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-4.png is excluded by !**/*.png
  • src/assets/icons/gitbook.png is excluded by !**/*.png
  • src/assets/icons/github-white.png is excluded by !**/*.png
  • src/assets/icons/github.png is excluded by !**/*.png
  • src/assets/icons/small-peanut.png is excluded by !**/*.png
  • src/assets/icons/star.png is excluded by !**/*.png
  • src/assets/illustrations/global-cash-local-feel.png is excluded by !**/*.png
  • src/assets/illustrations/thinking_peanut.gif is excluded by !**/*.gif
  • src/assets/iphone-ss/iphone-drop-a-link-mobile.png is excluded by !**/*.png
  • src/assets/iphone-ss/iphone-drop-a-link.png is excluded by !**/*.png
  • src/assets/iphone-ss/manteca_ss.png is excluded by !**/*.png
  • src/assets/mascot/peanut-beer.svg is excluded by !**/*.svg
  • src/assets/mascot/peanut-logo.svg is excluded by !**/*.svg
  • src/assets/mascot/peanut-pfp.svg is excluded by !**/*.svg
  • src/assets/peanut/peanut-club.png is excluded by !**/*.png
  • src/assets/peanut/peanut-raising-hands.svg is excluded by !**/*.svg
  • src/assets/peanut/peanutman-waving.svg is excluded by !**/*.svg
📒 Files selected for processing (146)
  • .gitmodules
  • package.json
  • public/easter-eggs/antarctica.webp
  • public/easter-eggs/bouvet.webp
  • public/easter-eggs/christmas.webp
  • public/easter-eggs/cocos.webp
  • public/easter-eggs/heard.webp
  • public/easter-eggs/pitcairn.webp
  • public/easter-eggs/southgeorgia.webp
  • public/easter-eggs/tokelau.webp
  • src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
  • src/app/(mobile-ui)/card-recovery/page.tsx
  • src/app/(mobile-ui)/card/page.tsx
  • src/app/(mobile-ui)/dev/components/page.tsx
  • src/app/(mobile-ui)/dev/ds/patterns/modal/page.tsx
  • src/app/(mobile-ui)/history/page.tsx
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(setup)/setup/finish/page.tsx
  • src/app/ClientProviders.tsx
  • src/app/[...recipient]/error.tsx
  • src/app/[locale]/(marketing)/error.tsx
  • src/app/actions/history.ts
  • src/app/crisp-proxy/page.tsx
  • src/app/dev/loading-words/page.tsx
  • src/app/global-error.jsx
  • src/app/layout.tsx
  • src/app/maintenance/page.tsx
  • src/app/receipt/[entryId]/page.tsx
  • src/app/shhhhh/ShhhhhLandingPage.tsx
  • src/assets/animations
  • src/assets/illustrations/index.ts
  • src/assets/index.ts
  • src/assets/iphone-ss/iphone-drop-a-link-mobile.webp
  • src/assets/iphone-ss/iphone-drop-a-link.webp
  • src/assets/iphone-ss/manteca_ss.webp
  • src/assets/mascot/index.ts
  • src/assets/mascot/peanut-cheering.webp
  • src/assets/mascot/peanut-club.webp
  • src/assets/mascot/peanut-crying.webp
  • src/assets/mascot/peanut-pointing-down.webp
  • src/assets/mascot/peanut-pointing.webp
  • src/assets/mascot/peanut-sad.webp
  • src/assets/mascot/peanut-thinking.webp
  • src/assets/mascot/peanut-too-cool.webp
  • src/assets/mascot/peanut-walking.webp
  • src/assets/mascot/peanut-waving-hello.webp
  • src/assets/mascot/peanut-whistling.webp
  • src/assets/peanut/index.ts
  • src/components/Badges/badge.utils.ts
  • src/components/Card/ApplicationStatusScreen.tsx
  • src/components/Card/CardFace.tsx
  • src/components/Card/CardWaitlistJoinedScreen.tsx
  • src/components/Card/CardWaitlistScreen.tsx
  • src/components/Card/PhysicalCardScreen.tsx
  • src/components/Card/__tests__/cardApply.utils.test.ts
  • src/components/Card/__tests__/cardState.utils.test.ts
  • src/components/Card/cardApply.utils.ts
  • src/components/Card/cardState.utils.ts
  • src/components/Card/cardUnlock.types.ts
  • src/components/Card/share-asset/PixelatedCardFace.tsx
  • src/components/Card/share-asset/ShareAssetD3.tsx
  • src/components/Card/share-asset/shareAssetLayout.ts
  • src/components/Claim/Generic/ClaimError.view.tsx
  • src/components/Claim/Link/Initial.view.tsx
  • src/components/Claim/Link/MantecaFlowManager.tsx
  • src/components/Claim/Link/Onchain/Success.view.tsx
  • src/components/Claim/Link/SendLinkActionList.tsx
  • src/components/Claim/__tests__/claim-states.test.tsx
  • src/components/Claim/useClaimLink.tsx
  • src/components/Global/ConfirmInviteModal/index.tsx
  • src/components/Global/Drawer/index.tsx
  • src/components/Global/EasterEggModal/index.tsx
  • src/components/Global/EmptyStates/NoDataEmptyState.tsx
  • src/components/Global/IframeWrapper/StartVerificationView.tsx
  • src/components/Global/KycVerifiedOrReviewModal/index.tsx
  • src/components/Global/LazyLoadErrorBoundary/index.tsx
  • src/components/Global/NoMoreJailModal/index.tsx
  • src/components/Global/PeanutLoading/CyclingLoading.tsx
  • src/components/Global/QRScanner/index.tsx
  • src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx
  • src/components/Global/SupportDrawer/index.tsx
  • src/components/Home/HomeHistory.tsx
  • src/components/Invites/InvitesPage.tsx
  • src/components/Invites/JoinWaitlistPage.tsx
  • src/components/Invites/campaign-maps.test.ts
  • src/components/Invites/campaign-maps.ts
  • src/components/Jobs/index.tsx
  • src/components/LandingPage/Manteca.tsx
  • src/components/LandingPage/dropLink.tsx
  • src/components/LandingPage/hero.tsx
  • src/components/Marketing/MarketingErrorBoundary.tsx
  • src/components/Marketing/mdx/CTA.tsx
  • src/components/Payment/Views/Error.validation.view.tsx
  • src/components/Profile/index.tsx
  • src/components/Profile/views/UnlockedRegions.view.tsx
  • src/components/Setup/Setup.consts.tsx
  • src/components/Setup/Views/JoinWaitlist.tsx
  • src/components/TransactionDetails/TransactionAvatarBadge.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
  • src/components/TransactionDetails/__tests__/transaction-predicates.test.ts
  • src/components/TransactionDetails/__tests__/useReceiptViewModel.test.ts
  • src/components/TransactionDetails/strategies/__tests__/registry.test.ts
  • src/components/TransactionDetails/strategies/intent/card.ts
  • src/components/TransactionDetails/strategies/registry.ts
  • src/components/TransactionDetails/strategies/types.ts
  • src/components/TransactionDetails/transaction-details.utils.ts
  • src/components/TransactionDetails/transaction-predicates.ts
  • src/components/TransactionDetails/transaction-types.ts
  • src/components/TransactionDetails/transactionTransformer.ts
  • src/components/TransactionDetails/useReceiptViewModel.ts
  • src/constants/__tests__/rhino.consts.test.ts
  • src/constants/analytics.consts.ts
  • src/constants/invites.consts.ts
  • src/constants/rhino.consts.ts
  • src/context/ClaimBankFlowContext.tsx
  • src/features/limits/views/LimitsPageView.tsx
  • src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx
  • src/features/payments/shared/components/PaymentSuccessView.tsx
  • src/features/payments/shared/components/SendWithPeanutCta.tsx
  • src/hooks/useCardInfo.ts
  • src/hooks/useHomeCarouselCTAs.tsx
  • src/hooks/useZeroDev.ts
  • src/hooks/wallet/useSignUserOp.ts
  • src/services/card.ts
  • src/services/invites.ts
  • src/services/rain.ts
  • src/types/api.generated.ts
  • src/types/api.openapi.json
  • src/utils/__tests__/chunk-error-recovery.test.ts
  • src/utils/__tests__/friendly-error.utils.test.tsx
  • src/utils/__tests__/general.utils.test.ts
  • src/utils/__tests__/history.utils.test.ts
  • src/utils/__tests__/invite-suffix.test.ts
  • src/utils/__tests__/regions.utils.test.ts
  • src/utils/__tests__/webauthn.utils.test.ts
  • src/utils/chunk-error-recovery.ts
  • src/utils/friendly-error.utils.tsx
  • src/utils/general.utils.ts
  • src/utils/history.utils.ts
  • src/utils/regions.utils.ts
  • src/utils/webauthn.utils.ts
  • tsconfig.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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.tsx

Repository: 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.ts

Repository: 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 50

Repository: 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/**' || true

Repository: 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' . || true

Repository: 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.tsx

Repository: 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.

Comment on lines +445 to 452
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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.

Comment on lines 113 to 121
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +85 to +90
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())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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).

Comment on lines +99 to +100
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."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested 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."
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.

Comment on lines +874 to +885
* 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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +282 to +295
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: '+',
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

@Hugo0 Hugo0 merged commit 33ff6b5 into main Jun 11, 2026
24 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants