Skip to content

refactor(assets): name the mascot a mascot, not a logo#2317

Closed
0xkkonrad wants to merge 42 commits into
devfrom
chore/rename-mascot-logo-assets
Closed

refactor(assets): name the mascot a mascot, not a logo#2317
0xkkonrad wants to merge 42 commits into
devfrom
chore/rename-mascot-logo-assets

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

PEANUTMAN_LOGO was the peanut-character illustration mislabeled as a "logo" — a distinct asset from the app icon (peanut-icon.svg) and the PEANUT wordmark (PEANUT_LOGO). The name described where it was first used (a header), not what it depicts. This untangles the three overlapping "logo" meanings so the names say what the assets are.

Changes (no behavior change — same pixels, accurate names):

  • Rename export PEANUTMAN_LOGOPEANUTMAN and file mascot/peanut-logo.svgmascot/peanutman.svg; update all 18 call sites, the local ASSET_ var, and the mascot alt text ("logo""Peanut mascot").
  • Move PEANUT_LOGO_BLACK out of illustrations/ into logos/ (it's the brand logo); fix the one subpath import.
  • Keep public/peanutman-logo.svg unchanged as the public alias, so external consumers hardcoding peanut.me/peanutman-logo.svg keep resolving. (An earlier commit added a duplicate public/peanutman.svg; it was dropped in 0a82a0f4 since it was unreferenced and byte-identical to the existing alias — no new public path is introduced.)

Risks / breaking changes

  • Low. Pure rename; PEANUTMAN is a new token (no collision) and consumers import via the @/assets barrel which re-exports it unchanged. Typecheck enforces completeness.
  • No public URL broken: public/peanutman-logo.svg is retained unchanged as the public alias.
  • Deliberately NOT touched: public/logo-favicon.png — it's the favicon/app-icon (accurately named) and is load-bearing for the pending Card Closed Beta email drip (16 templates hardcode its URL). Renaming it was verified unsafe (OneSignal templates clean, but the email drafts hardcode it).
  • No cross-repo impact.

QA

  • npm run typecheck — clean (catches any missed rename).
  • npm test — green (the countryCurrencyMapping flag suite needs the copy-flags prebuild, which CI/build runs).
  • Grep sanity: grep -rn PEANUTMAN_LOGO src → no hits.
  • Visual: mascot renders unchanged on the loading screen, public profile, claim/link, card share, tx details.

Local build compiles (webpack + SW bundle OK); the content-gen step OOMs on my box (~5 GB free) — the known worktree build-OOM, deferred to CI.

jjramirezn and others added 30 commits June 26, 2026 17:31
The action-required drawer checked actionMessage first, but the backend
always sends a generic "resubmit your documents" actionMessage for every
action_required state — so the specific reject-label copy (e.g.
DUPLICATE_EMAIL → "Email already in use, sign in to that account or contact
support") was never reachable. Users hitting an email collision saw a
misleading "verify your ID" prompt with no actionable next step. Prefer
RejectLabelsList when reject labels are present; fall back to the generic
actionMessage only when there are none.
Address /code-review findings: collapse the three-branch ternary to two
(RejectLabelsList already renders its own empty-state fallback, so the
duplicated branch was dead weight), and add an integration test that renders
the real RejectLabelsList so the DUPLICATE_EMAIL → 'Email already in use'
copy mapping is actually exercised, not mocked away.
…enance)

The guest claim-to-bank off-ramp is paused for the Monday Card launch — the
backend now 503s POST /bridge/offramp/create-for-guest (pairs with the BE
hotfix). Letting unverified claimers enter that flow would dead-end on a 503,
so the bank method is now rendered greyed + non-interactive with a "Soon!"
badge whenever the claim resolves to GuestBankClaim (claimer unverified, sender
can receive a bank off-ramp).

The authenticated self off-ramp (UserBankClaim → /users/accounts +
/bridge/offramp/create) is untouched and stays fully clickable.
fix(card): guest claim-to-bank under maintenance (grey + SOON)
Prod Release Sprint 149 — cross-chain withdraw · card-launch CTA · share-asset · badges (2026-06-29)
Launch-day design review (Konrad): the banner was off-system — pink (pink=clickable in Peanut), a black drop-shadow that misaligned the bounding box, and a black CTA used nowhere else. Now a white card, no drop shadow, standard purple primary CTA — matching the other activation CTAs. Copy + /shhhhh routing + gating unchanged; cosmetic only.
Border was border-2 (2px) vs the activation CTAs' 1px (Global Card = border border-black); drop to 1px. Title shhh → shhhh (one more h).
…esign-system

fix(card): bring launch CTA in line with the activation CTAs
…cedence

fix(kyc): surface per-label reject copy in action-required drawer
…sset

The launch-day blank share asset: PixelatedCardFace paints the pixelated hand into a <canvas> appended asynchronously (new Image() -> onload -> appendChild), and captureShareAsset called html-to-image without waiting for it. html-to-image awaits <img> but not a not-yet-mounted <canvas>, so a fast/first-of-session share captured a blank card — just the pink box + its floating drop-shadow (the 'fucked shadow'). Silent: capture succeeds, nothing hits Sentry. Gate capture on document.fonts.ready + every <img>.decode() + the hand <canvas> being mounted, bounded by a 2.5s timeout so it never hangs. Win/celebration asset only; rejection asset has no canvas and is unaffected.
…-capture

fix(card): blank share asset — wait for the card-face canvas before capture
…p the cooldown)

Stuck-outside users used to flip from CardRejectionScreen (shareable 'not tonight' asset + Tweet to appeal) to a bare CardWaitlistJoinedScreen cooldown once they joined — and every later /card visit showed that cooldown, so they could never re-grab/re-share their appeal asset. Make the rejection screen the TERMINAL waitlist screen: once joined (alreadyJoined) the 'Join anyway' button becomes an 'on the list' confirmation, but the asset + Tweet-to-appeal stay. Symmetric with the in-crowd. Delete CardWaitlistJoinedScreen; /dev/rejection-builder gets an alreadyJoined toggle to QA both states.
…hift)

The 'on the list' confirmation that replaces the 'Join anyway' button was py-2 (~36px) vs the button's h-13 (52px), so the CTA block shrank on join and shifted the layout. Fix the indicator to h-13 so the swap is height-stable.
…ejection

feat(card): waitlist ends on the rejection/appeal screen (drop the cooldown)
1) Local 'locallyJoined' override (set only on a confirmed join) so the 'Join anyway' CTA swaps to the on-the-list state immediately instead of flickering back until the /card refetch resolves. 2) CARD_WAITLIST_VIEWED now reports the real already_joined (the screen can mount already-joined). 3) handleAppeal clears any stale joinError so 'Failed to join' can't render alongside 'You're on the list'.
…cr-fixes

fix(card): address CodeRabbit on the terminal waitlist screen
Export waitForAssetReady (with an injectable timeout) and add 4 tests proving the capture gate blocks the html-to-image snapshot until the async card-face <canvas> has mounted — the exact element whose absence produced the launch-day blank share asset (#2302) — plus: bounded so it never hangs, awaits every <img>.decode(), and awaits document.fonts.ready. 6/6 pass.
…-readiness

test(card): regression guard for the blank share-asset capture
The first-time Rain card application path is structurally 7-13s (7 sequential
Sumsub calls + a 2.5s readiness sleep + Rain createApplication + optional inline
issueCard). The global 10s fetchWithSentry default aborts that tail client-side
while the backend keeps running to completion, so users see a false 'application
failed' error on a card that was actually submitted.

PostHog: the rain/cards timeout exception fires ~280x/week across ~40 distinct
users; of 120 users who hit it in 30d only 28 reached card_apply_succeeded.

Override timeoutMs for this one call only (browser-side fetch, so the 15s Vercel
function ceiling that caps the global default does not apply here).
fix(card): raise apply-for-card fetch timeout to 60s
The 'not tonight' Berghain screen rendered a hardcoded '213 tried · 7 got in'
(props defaulted to 213/7 and the page never passed real values). Now /card
returns waitlistTotal + admittedTotal; the screen inflates 'tried' for FOMO
(real waitlist size x3, floored at 213 — mirrors the /shhhhh ScarcityCounter
fake-scarcity flex) and shows the real 'got in'. Inflation is a pure,
deterministic fn so it never jitters between renders; a sane 213/7 fallback
covers the still-loading window. /dev/rejection-builder now drives the real
counts and previews the inflated result.

Requires the BE PR (waitlistTotal/admittedTotal on /card) deployed first.
…ministic ready gate

The card launched today; the share asset users post had two capture bugs.

1) Square shadows. html-to-image renders CSS box-shadow on a rounded element
   as a SQUARE block, so the captured PNG showed square shadows behind the
   rounded peanut.me/<user> pill and the card itself. Replace those box-shadows
   with offset black sibling elements that share the border-radius — they
   capture as faithful rounded shadows. (Hero stickers use filter:drop-shadow,
   which captures fine — left as is.)

2) Blank card. PixelatedCardFace paints its pixelated hand into a <canvas>
   appended asynchronously, so a capture firing before the canvas mounted
   snapshotted a blank pink card — and the capture SUCCEEDED, so nothing
   reached Sentry. The bounded waitForAssetReady wait (PR #2302) can time out
   under load. Deterministic fix: PixelatedCardFace fires onReady once the hand
   canvas mounts; it threads up through ShareAssetD3 to the Share/Save buttons,
   which stay disabled until the asset signals ready. waitForAssetReady stays
   as a belt-and-suspenders fallback.

Also wires the /dev/share-builder "Save image" button to the real capture path
and adds an e2e regression guard that decodes the captured PNG and asserts the
card centre (the hand's territory) is not entirely background — proving the
card face actually renders.
CI typecheck caught existing CardInfoResponse mocks (e.g. cardState.utils
.test.ts) breaking because the new fields were required. Optional is also the
correct contract: the BE deploys first, but the FE must tolerate undefined
during the rollout window (and for any older API) — computeDoorTally already
falls back to 213/7. Full `tsc --noEmit` + `npm test` now green.
…lytics

Users were skipping the EEA-uplift KYC via the modal's "Not now" / close and
then continuing to transact, so the requirement never got completed before a
bank transfer. Make the pre-empt mandatory: the modal is non-closable and
non-skippable (preventClose + hideModalCloseButton, "Not now" removed), and
the hook blocks the deferred deposit/withdraw action until the requirement
clears. Only "Complete now" moves forward.

Add dedicated PostHog events (eea_uplift_started / eea_uplift_completed, with
channel + requirement_key) so the start->finish funnel can be filtered
directly. Completion is ref-guarded so generic KYC successes on the bank pages
don't mis-fire it.

Scope: the advisory hard-gate path on add-money/[country]/bank and
withdraw/[country]/bank. Front-end enforcement only — does not change Bridge's
underlying requirement.
CI typecheck couldn't resolve 'sharp' (not a declared dependency). Decode +
sample the captured PNG in-page via an <img> + <canvas>.getImageData inside
page.evaluate instead — same not-empty assertion, zero new dependencies, so
the supply-chain min-release-age gate stays untouched.
At x3 the real number is masked by the 213 floor (prod waitlist ~55 -> 55x3
= 165 < 213, so it just renders the hardcoded-looking 213). x5 = 275 clears
the floor, so the real inflated count actually shows. Counts source +
floor + fallback unchanged.
feat(card): real FOMO door tally on rejection screen
The startedRef latch was only cleared on success, so an abandoned uplift
attempt could leave it set and a later unrelated KYC success on the same
mounted page would mis-fire eea_uplift_completed (inflating the funnel).
Reset the pending start via the KYC flow's onManualClose (abandon) on both
bank pages. Covers the main mis-attribution path surfaced in review.
fix(card): bulletproof share-asset capture — faithful shadows + deterministic ready gate
…dal lifecycle

CodeRabbit + review findings:
- Move eea_uplift_completed off the end-of-flow onKycSuccess onto a new
  onKycApproved callback fired at the Sumsub approval transition, so it isn't
  lost when the user drops during post-approval ToS/preparing (undercount) and
  isn't fired on a ToS-skip without finishing (overcount).
- useAdvisoryPreempt: auto-close the modal when the advisory clears while open,
  and re-show it if the launch fails — the hard gate must not silently vanish or
  linger over an already-cleared requirement.
- useEeaUpliftFunnel: require a real advisory for trackStarted (no partial
  started payloads).
jjramirezn and others added 8 commits June 29, 2026 12:30
…nt (CodeRabbit)

Don't spinner-gate the rejected screen on capabilities — it's useful without the
reason (reassurance + support CTA), so show it now and fill reasonMessage once
capabilities resolve. Strengthen the test to assert the reason renders ABOVE the
body (DOM order) and to count paragraphs generically instead of phrase-matching.
…e literal

The "You are #N on the list" copy ships to prod AND staging as
`["You are #", position, "on the list…"]` — no leading space — so it
renders "You are #16on the list". The source has a same-line space
(prettier enforces it and reverts `{' '}`), and a clean next-swc compile
keeps the space, so the live no-space bundle is a stale build artifact.
A template literal bakes the space into one JS string (immune to JSX
whitespace handling) and changes the module so the stale artifact can't
be reused. Matches the existing pattern in JoinWaitlistPage.tsx.
feat(card): specific reason on the rejected-card screen + crypto reassurance
…ition-space

fix(card): space in physical-waitlist position copy ("#16on" → "#16 on")
feat(kyc): mandatory EEA-uplift gate + funnel analytics
Why: the "I'M IN!" / rejection share assets kept capturing with a blank
pink card face for some users (ghadi, ubong) even though #2308's readiness
gate had been live 57min before the capture. Root cause is NOT timing — the
hand was the only <canvas> in the asset, drawn from an SVG, and html-to-image
silently substitutes a blank canvas when canvas.toDataURL() returns empty
(node_modules/html-to-image/lib/clone-node.js), which iOS Safari does for an
SVG-tainted canvas. The capture "succeeds", so nothing reaches Sentry
(confirmed: zero share-asset capture errors despite live blank reports). A
mount-gate can't fix a capture-time serialisation failure.

Fix: render the hand as a plain pre-pixelated <img> — the same path the badge
stickers take, which never blank. html-to-image inlines <img> reliably on
every browser. The PNG is baked at the 36px raster and upscaled by
image-rendering:pixelated, so it reads identically to the old canvas. This
also drops the async-canvas dance from capture (waitForAssetReady now just
awaits fonts + <img> decode). onReady still fires on <img> load (with a
cached-image fallback) so the Share/Save gate is preserved; the e2e capture
spec guards the <img> path end-to-end.
PEANUTMAN_LOGO was the peanut-character illustration mislabeled as a
"logo" — a distinct asset from the app icon (peanut-icon) and the PEANUT
wordmark. The name described where it was first used (a header), not what
it depicts.

- rename export PEANUTMAN_LOGO → PEANUTMAN and file peanut-logo.svg →
  peanutman.svg; update all 18 call sites, the local ASSET_ var, and the
  mascot alt text ("logo" → "Peanut mascot")
- move PEANUT_LOGO_BLACK out of illustrations/ into logos/ where the brand
  logo belongs; fix the one subpath import (@/assets/illustrations → logos)

No behavior change — same pixels, accurate names.
Adds the correctly-named public copy of the mascot while KEEPING
public/peanutman-logo.svg in place, so any external consumer that
hardcodes peanut.me/peanutman-logo.svg (marketing emails, embeds, the
native app) keeps resolving. Public asset paths are an external API —
rename by adding, never by moving.

Note: public/logo-favicon.png is intentionally left untouched — it is the
favicon/app-icon (accurately named) and is load-bearing for the pending
Card Closed Beta email drip (16 templates hardcode its URL).
@vercel

vercel Bot commented Jul 1, 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 Jul 2, 2026 6:27am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR renames the mascot asset export to PEANUTMAN and updates consumers. It also adds share-asset readiness gating and capture waiting, reworks card waitlist/rejection rendering with backend tally fields, and changes bank/KYC advisory flows to a mandatory EEA uplift gate with new analytics.

Changes

Mascot Asset Rename and Consumer Updates

Layer / File(s) Summary
Asset export rename and reorganization
src/assets/mascot/index.ts, src/assets/illustrations/index.ts, src/assets/logos/index.ts
PEANUTMAN_LOGO is renamed to PEANUTMAN, PEANUT_LOGO_BLACK is removed from illustrations, and the black logo export is added to logos.
Core UI consumers
src/components/Badges/badge.utils.ts, src/components/Card/CardFace.tsx, src/components/Profile/components/PublicProfile.tsx, src/components/TransactionDetails/TransactionCard.tsx, src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
Badge fallbacks, card faces, profile header, and transaction detail avatars switch to the PEANUTMAN asset.
Claim, send, modal, and loading consumers
src/components/Claim/Link/Initial.view.tsx, src/components/Claim/Link/SendLinkActionList.tsx, src/features/payments/shared/components/SendWithPeanutCta.tsx, src/components/Global/*, src/components/Global/QRScanner/index.tsx
Claim, send, modal, button, loading, and QR scanner images update to the renamed mascot asset, with the loading alt text adjusted.
Pages and test comment
src/app/(mobile-ui)/notifications/page.tsx, src/app/dev/loading-words/page.tsx, src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
Notifications and loading preview pages use PEANUTMAN, and the QR-pay test comment matches the renamed export.

Share Asset Readiness and Capture Gating

Layer / File(s) Summary
Share asset contracts and capture wait
src/components/Card/share-asset/shareAsset.types.ts, src/components/Card/share-asset/ShareAssetActions.tsx, src/components/Card/share-asset/ShareAssetD3.tsx, src/components/Card/share-asset/captureShareAsset.ts, src/components/Card/share-asset/__tests__/captureShareAsset.test.ts
Share-asset props gain readiness callbacks/flags, action buttons disable until ready, capture waits for fonts and images, and the helper is covered by new tests.
Pixelated card face rendering
src/components/Card/share-asset/PixelatedCardFace.tsx
The share card face switches to the new mascot asset, uses a pre-pixelated hand image, adds onReady, and changes the shadow/layout structure.
Share asset host wiring
src/components/Card/BadgeSkipCelebration.tsx, src/components/Card/CardUnlockDrawer.tsx, src/app/(mobile-ui)/dev/share-builder/page.tsx
Share/save flows track asset readiness, pass onReady, disable actions until ready, and add PNG capture/download behavior with reset gating on reroll and animation changes.
Share asset e2e regression
e2e/flows/share-asset-capture.spec.ts
A Playwright flow verifies the saved PNG is produced only after readiness and contains non-background pixels.

Card Rejection and Waitlist Presentation

Layer / File(s) Summary
Door tally contract and tests
src/components/Card/doorTally.utils.ts, src/components/Card/__tests__/doorTally.utils.test.ts
The new tally utility defines inflation/fallback constants and helpers, with tests covering scaling, floors, and admitted-count fallback behavior.
Rejection screen and supporting copy
src/components/Card/CardRejectionScreen.tsx, src/components/Card/ApplicationStatusScreen.tsx, src/components/Card/__tests__/ApplicationStatusScreen.test.tsx
The rejection screen now derives counts from backend totals, tracks local join state, and passes through reason copy; the rejected status copy and its tests are updated accordingly.
Card page and builder wiring
src/app/(mobile-ui)/card/page.tsx, src/app/(mobile-ui)/dev/rejection-builder/page.tsx, src/components/Card/PhysicalCardScreen.tsx, src/components/Card/CardWaitlistJoinedScreen.tsx
The card page stops rendering a separate joined screen, the rejection builder uses backend counts and updated preview props, the physical-card join text is simplified, and the joined waitlist screen is removed.

KYC, Advisory, and EEA Uplift Banking Flows

Layer / File(s) Summary
Mandatory advisory pre-empt
src/hooks/useAdvisoryPreempt.ts, src/components/Kyc/AdvisoryPreemptModal.tsx, src/hooks/useAdvisoryPreempt.test.ts
The advisory pre-empt becomes non-skippable and non-dismissible, and the tests are updated for the new blocking behavior.
EEA uplift tracking and KYC hooks
src/constants/analytics.consts.ts, src/hooks/useEeaUpliftFunnel.ts, src/hooks/useEeaUpliftFunnel.test.ts, src/hooks/useMultiPhaseKycFlow.ts
New start/completed analytics are added, the uplift hook records and clears funnel state, and KYC approval now exposes an earlier callback boundary.
Bank page wiring
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx, src/services/rain.ts
The add-money and withdraw bank pages wire the uplift funnel into Sumsub callbacks and advisory completion, and the rain card request timeout is increased.
KYC reject-label copy
src/components/Kyc/states/KycActionRequired.tsx, src/components/Kyc/states/__tests__/KycActionRequired.rejectCopy.test.tsx, src/components/Kyc/states/__tests__/KycStates.test.tsx
Reject-label copy now takes precedence over generic action text, with tests covering the duplicate-email and fallback paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: Hugo0

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the asset rename theme of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description accurately matches the asset rename changes and related public asset handling in this PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5860.07 → 5862.92 (+2.85)
Findings: 0 net (+2 new, -2 resolved)

🆕 New findings (2)

  • high hotspot — src/components/Badges/badge.utils.ts — 33 commits, +328/-108 lines since 6 months ago
  • medium hotspot — src/components/TransactionDetails/TransactionCard.tsx — 27 commits, +192/-140 lines since 6 months ago

✅ Resolved (2)

  • src/components/Badges/badge.utils.ts — 32 commits, +326/-106 lines since 6 months ago
  • src/components/TransactionDetails/TransactionCard.tsx — 26 commits, +190/-138 lines since 6 months ago

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1643 ran, 0 failed, 0 skipped, 26.2s

📊 Coverage (unit)

metric %
statements 54.6%
branches 37.4%
functions 42.4%
lines 54.6%
⏱ 10 slowest test cases
time test
3.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
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.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claimed shows shake class + go home button
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/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Manteca PIX form ready shows merchant card + amount input + pay button
0.1s src/services/__tests__/resolveClaimLink.test.ts › restores the pristine password after a redirect mangles the fragment
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@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: 10

🧹 Nitpick comments (2)
src/app/(mobile-ui)/notifications/page.tsx (1)

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider importing from @/assets/mascot directly to resolve the barrel import violation.

The static analysis flags @/assets as a restricted barrel import. While this file already used @/assets for PEANUTMAN_LOGO, the rename is a good opportunity to align with the project rule and import PEANUTMAN from @/assets/mascot instead, matching what src/app/dev/loading-words/page.tsx does.

-import { PEANUTMAN } from '`@/assets`'
+import { PEANUTMAN } from '`@/assets/mascot`'
🤖 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)/notifications/page.tsx at line 12, The import in the
notifications page is using the restricted barrel entry for the mascot asset.
Update the `PEANUTMAN` import in the `notifications/page.tsx` module to come
directly from `@/assets/mascot`, matching the existing project pattern used
elsewhere, and keep the rest of the asset imports aligned with the direct-path
rule.

Source: Linters/SAST tools

src/components/Global/PeanutLoading/CyclingLoading.tsx (1)

29-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale alt text "logo".

PR objective states the mascot alt text is updated from "logo" to "Peanut mascot"; PeanutLoading/index.tsx was updated accordingly but this sibling component still uses alt="logo".

✏️ Suggested fix
-                    <img src={PEANUTMAN.src} alt="logo" className="h-10" />
+                    <img src={PEANUTMAN.src} alt="Peanut mascot" className="h-10" />
🤖 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/PeanutLoading/CyclingLoading.tsx` at line 29, The alt
text in CyclingLoading still uses the stale value "logo"; update the <img> in
CyclingLoading.tsx to match the mascot wording used elsewhere, aligning it with
the existing PeanutLoading/index.tsx change by using the same descriptive alt
text for PEANUTMAN.src.
🤖 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/components/Claim/Link/Initial.view.tsx`:
- Line 45: The import in Initial.view.tsx is using the restricted `@/assets`
barrel, so replace it with direct source imports for PEANUT_LOGO_BLACK and
PEANUTMAN. Update the import near the top of the component to reference the
specific asset modules instead of the barrel, keeping the same identifiers so
the rest of the file (including Initial.view.tsx usage) remains unchanged.
- Line 825: The mascot image in Initial.view.tsx still uses the old alt text, so
update the PEANUTMAN Image element in the relevant Claim/Link markup to use
"Peanut mascot" instead of "Peanut Logo". Locate the change by the Image
component with src={PEANUTMAN} and adjust only its alt attribute to match the PR
objective.

In `@src/components/Claim/Link/SendLinkActionList.tsx`:
- Line 240: The PEANUTMAN image in SendLinkActionList still uses the wrong alt
text, so update the Image element’s alt prop from the current logo wording to
"Peanut mascot" to match the PR objective. Use the PEANUTMAN reference in
SendLinkActionList.tsx to locate the JSX and replace only the alt text value.

In `@src/components/Global/ConfirmInviteModal/index.tsx`:
- Line 4: The import in ConfirmInviteModal is using the `@/assets` barrel, which
violates the ESLint import rule. Update the top-level import in the
ConfirmInviteModal component to pull PEANUT_LOGO_BLACK and PEANUTMAN from their
specific asset modules instead of the aggregated barrel, using the existing
asset names to locate the correct source files.

In `@src/components/Global/CreateAccountButton/index.tsx`:
- Line 3: The CreateAccountButton module is importing assets through the barrel
export, which violates the ESLint import rule. Update the import in
CreateAccountButton/index.tsx to use the direct asset module paths instead of
the aggregated `@/assets` barrel, keeping the referenced symbols PEANUT_LOGO_BLACK
and PEANUTMAN intact.

In `@src/components/Global/NoMoreJailModal/index.tsx`:
- Line 6: The import in NoMoreJailModal should avoid the barrel module and use
direct asset imports instead. Update the import in the component to reference
the specific asset source for PEANUT_LOGO_BLACK and PEANUTMAN, matching the same
ESLint-friendly pattern used elsewhere in this cohort.

In `@src/components/Global/PeanutLoading/index.tsx`:
- Line 1: The import in PeanutLoading is using the barrel export from the assets
module, which violates the ESLint barrel-import rule. Update the PEANUTMAN
import in the PeanutLoading component to point to the direct asset source
instead of importing from '`@/assets`', and keep the symbol name unchanged so the
component continues to reference PEANUTMAN correctly.

In `@src/components/Profile/components/PublicProfile.tsx`:
- Line 3: The import in PublicProfile.tsx is using the `@/assets` barrel, which
violates the no-restricted-imports rule and can pull in unnecessary re-exports.
Update the imports for HandThumbsUpV2, PEANUT_LOGO_BLACK, and PEANUTMAN to come
directly from their specific asset files instead of the barrel, keeping the rest
of PublicProfile unchanged.

In `@src/features/payments/shared/components/SendWithPeanutCta.tsx`:
- Line 103: Update the PEANUTMAN image in SendWithPeanutCta so its alt text
matches the PR objective: change the Image component’s alt from “Peanut Logo” to
“Peanut mascot”. Use the existing PEANUTMAN usage in SendWithPeanutCta to locate
the change and keep the rest of the component unchanged.
- Line 11: The import in SendWithPeanutCta should not use the restricted
`@/assets` barrel; replace the combined asset import with direct source imports
for PEANUT_LOGO_BLACK and PEANUTMAN. Update the import statement in
SendWithPeanutCta to reference the specific asset modules directly so the
component no longer depends on the barrel.

---

Nitpick comments:
In `@src/app/`(mobile-ui)/notifications/page.tsx:
- Line 12: The import in the notifications page is using the restricted barrel
entry for the mascot asset. Update the `PEANUTMAN` import in the
`notifications/page.tsx` module to come directly from `@/assets/mascot`,
matching the existing project pattern used elsewhere, and keep the rest of the
asset imports aligned with the direct-path rule.

In `@src/components/Global/PeanutLoading/CyclingLoading.tsx`:
- Line 29: The alt text in CyclingLoading still uses the stale value "logo";
update the <img> in CyclingLoading.tsx to match the mascot wording used
elsewhere, aligning it with the existing PeanutLoading/index.tsx change by using
the same descriptive alt text for PEANUTMAN.src.
🪄 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: 7099a9d4-7279-4d94-b5e5-943f317f14b3

📥 Commits

Reviewing files that changed from the base of the PR and between efbb2bd and 8e5802d.

⛔ Files ignored due to path filters (3)
  • public/peanutman.svg is excluded by !**/*.svg
  • src/assets/logos/peanut-logo-dark.svg is excluded by !**/*.svg
  • src/assets/mascot/peanutman.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • src/app/(mobile-ui)/notifications/page.tsx
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/dev/loading-words/page.tsx
  • src/assets/illustrations/index.ts
  • src/assets/logos/index.ts
  • src/assets/mascot/index.ts
  • src/components/Badges/badge.utils.ts
  • src/components/Card/CardFace.tsx
  • src/components/Card/share-asset/PixelatedCardFace.tsx
  • src/components/Claim/Link/Initial.view.tsx
  • src/components/Claim/Link/SendLinkActionList.tsx
  • src/components/Global/ConfirmInviteModal/index.tsx
  • src/components/Global/CreateAccountButton/index.tsx
  • src/components/Global/NoMoreJailModal/index.tsx
  • src/components/Global/PeanutLoading/CyclingLoading.tsx
  • src/components/Global/PeanutLoading/index.tsx
  • src/components/Global/QRScanner/index.tsx
  • src/components/Profile/components/PublicProfile.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
  • src/features/payments/shared/components/SendWithPeanutCta.tsx
💤 Files with no reviewable changes (1)
  • src/assets/illustrations/index.ts

import { Button } from '@/components/0_Bruddle/Button'
import Image from 'next/image'
import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use specific import paths instead of the @/assets barrel.

The @/assets barrel import is restricted (see no-restricted-imports in CLAUDE.md). Split this into direct source imports:

-import { PEANUT_LOGO_BLACK, PEANUTMAN } from '`@/assets`'
+import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
+import { PEANUTMAN } from '`@/assets/mascot`'
📝 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
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'
import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
import { PEANUTMAN } from '`@/assets/mascot`'
🧰 Tools
🪛 ESLint

[error] 45-45: '@/assets' import is restricted from being used. Import from a specific file instead of the '@/assets' barrel — barrels force the bundler to load every re-export and hurt build perf. See CLAUDE.md.

(no-restricted-imports)

🤖 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/Link/Initial.view.tsx` at line 45, The import in
Initial.view.tsx is using the restricted `@/assets` barrel, so replace it with
direct source imports for PEANUT_LOGO_BLACK and PEANUTMAN. Update the import
near the top of the component to reference the specific asset modules instead of
the barrel, keeping the same identifiers so the rest of the file (including
Initial.view.tsx usage) remains unchanged.

Source: Linters/SAST tools

<div>Receive on </div>
<div className="flex items-center gap-1">
<Image src={PEANUTMAN_LOGO} alt="Peanut Logo" className="size-5" />
<Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update mascot alt to "Peanut mascot" per PR objectives.

The PR description states mascot alt text should change to "Peanut mascot". The PEANUTMAN image here still carries alt="Peanut Logo".

-                        <Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />
+                        <Image src={PEANUTMAN} alt="Peanut mascot" className="size-5" />
📝 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
<Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />
<Image src={PEANUTMAN} alt="Peanut mascot" className="size-5" />
🤖 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/Link/Initial.view.tsx` at line 825, The mascot image in
Initial.view.tsx still uses the old alt text, so update the PEANUTMAN Image
element in the relevant Claim/Link markup to use "Peanut mascot" instead of
"Peanut Logo". Locate the change by the Image component with src={PEANUTMAN} and
adjust only its alt attribute to match the PR objective.

{showDevconnectMethod ? <div>Claim on</div> : <div>Continue with </div>}
<div className="flex items-center gap-1">
<Image src={PEANUTMAN_LOGO} alt="Peanut Logo" className="size-5" />
<Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update mascot alt to "Peanut mascot" per PR objectives.

The PEANUTMAN image here still carries alt="Peanut Logo" instead of "Peanut mascot" as described in the PR summary.

-                        <Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />
+                        <Image src={PEANUTMAN} alt="Peanut mascot" className="size-5" />
📝 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
<Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />
<Image src={PEANUTMAN} alt="Peanut mascot" className="size-5" />
🤖 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/Link/SendLinkActionList.tsx` at line 240, The PEANUTMAN
image in SendLinkActionList still uses the wrong alt text, so update the Image
element’s alt prop from the current logo wording to "Peanut mascot" to match the
PR objective. Use the PEANUTMAN reference in SendLinkActionList.tsx to locate
the JSX and replace only the alt text value.

import { type FC } from 'react'
import Image from 'next/image'
import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Fix barrel import per ESLint rule.

Importing from @/assets barrel is flagged by the lint rule; import from the specific file (e.g. @/assets/mascot, @/assets/logos) instead.

🔧 Suggested fix
-import { PEANUT_LOGO_BLACK, PEANUTMAN } from '`@/assets`'
+import { PEANUTMAN } from '`@/assets/mascot`'
+import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
📝 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
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'
import { PEANUTMAN } from '`@/assets/mascot`'
import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
🧰 Tools
🪛 ESLint

[error] 4-4: '@/assets' import is restricted from being used. Import from a specific file instead of the '@/assets' barrel — barrels force the bundler to load every re-export and hurt build perf. See CLAUDE.md.

(no-restricted-imports)

🤖 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/ConfirmInviteModal/index.tsx` at line 4, The import in
ConfirmInviteModal is using the `@/assets` barrel, which violates the ESLint
import rule. Update the top-level import in the ConfirmInviteModal component to
pull PEANUT_LOGO_BLACK and PEANUTMAN from their specific asset modules instead
of the aggregated barrel, using the existing asset names to locate the correct
source files.

Source: Linters/SAST tools

'use client'

import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Fix barrel import per ESLint rule.

Same barrel-import violation as elsewhere in this cohort.

🔧 Suggested fix
-import { PEANUT_LOGO_BLACK, PEANUTMAN } from '`@/assets`'
+import { PEANUTMAN } from '`@/assets/mascot`'
+import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
📝 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
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'
import { PEANUTMAN } from '`@/assets/mascot`'
import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
🧰 Tools
🪛 ESLint

[error] 3-3: '@/assets' import is restricted from being used. Import from a specific file instead of the '@/assets' barrel — barrels force the bundler to load every re-export and hurt build perf. See CLAUDE.md.

(no-restricted-imports)

🤖 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/CreateAccountButton/index.tsx` at line 3, The
CreateAccountButton module is importing assets through the barrel export, which
violates the ESLint import rule. Update the import in
CreateAccountButton/index.tsx to use the direct asset module paths instead of
the aggregated `@/assets` barrel, keeping the referenced symbols PEANUT_LOGO_BLACK
and PEANUTMAN intact.

Source: Linters/SAST tools

import { ANALYTICS_EVENTS, MODAL_TYPES } from '@/constants/analytics.consts'
import Image from 'next/image'
import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Fix barrel import per ESLint rule.

Same barrel-import violation as elsewhere in this cohort.

🔧 Suggested fix
-import { PEANUT_LOGO_BLACK, PEANUTMAN } from '`@/assets`'
+import { PEANUTMAN } from '`@/assets/mascot`'
+import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
📝 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
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'
import { PEANUTMAN } from '`@/assets/mascot`'
import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
🧰 Tools
🪛 ESLint

[error] 6-6: '@/assets' import is restricted from being used. Import from a specific file instead of the '@/assets' barrel — barrels force the bundler to load every re-export and hurt build perf. See CLAUDE.md.

(no-restricted-imports)

🤖 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/NoMoreJailModal/index.tsx` at line 6, The import in
NoMoreJailModal should avoid the barrel module and use direct asset imports
instead. Update the import in the component to reference the specific asset
source for PEANUT_LOGO_BLACK and PEANUTMAN, matching the same ESLint-friendly
pattern used elsewhere in this cohort.

Source: Linters/SAST tools

@@ -1,4 +1,4 @@
import { PEANUTMAN_LOGO } from '@/assets'
import { PEANUTMAN } from '@/assets'

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Fix barrel import per ESLint rule.

Same barrel-import violation flagged by ESLint elsewhere in this cohort.

🔧 Suggested fix
-import { PEANUTMAN } from '`@/assets`'
+import { PEANUTMAN } from '`@/assets/mascot`'
📝 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
import { PEANUTMAN } from '@/assets'
import { PEANUTMAN } from '`@/assets/mascot`'
🧰 Tools
🪛 ESLint

[error] 1-1: '@/assets' import is restricted from being used. Import from a specific file instead of the '@/assets' barrel — barrels force the bundler to load every re-export and hurt build perf. See CLAUDE.md.

(no-restricted-imports)

🤖 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/PeanutLoading/index.tsx` at line 1, The import in
PeanutLoading is using the barrel export from the assets module, which violates
the ESLint barrel-import rule. Update the PEANUTMAN import in the PeanutLoading
component to point to the direct asset source instead of importing from
'`@/assets`', and keep the symbol name unchanged so the component continues to
reference PEANUTMAN correctly.

Source: Linters/SAST tools

'use client'

import { HandThumbsUpV2, PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
import { HandThumbsUpV2, PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Barrel import violates no-restricted-imports lint rule.

ESLint flags import { HandThumbsUpV2, PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets' as restricted; imports must come from specific asset files instead of the @/assets barrel to avoid loading every re-export.

🔧 Proposed fix
-import { HandThumbsUpV2, PEANUT_LOGO_BLACK, PEANUTMAN } from '`@/assets`'
+import { HandThumbsUpV2 } from '`@/assets/illustrations`'
+import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
+import { PEANUTMAN } from '`@/assets/mascot`'
📝 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
import { HandThumbsUpV2, PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'
import { HandThumbsUpV2 } from '`@/assets/illustrations`'
import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
import { PEANUTMAN } from '`@/assets/mascot`'
🧰 Tools
🪛 ESLint

[error] 3-3: '@/assets' import is restricted from being used. Import from a specific file instead of the '@/assets' barrel — barrels force the bundler to load every re-export and hurt build perf. See CLAUDE.md.

(no-restricted-imports)

🤖 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/components/PublicProfile.tsx` at line 3, The import in
PublicProfile.tsx is using the `@/assets` barrel, which violates the
no-restricted-imports rule and can pull in unnecessary re-exports. Update the
imports for HandThumbsUpV2, PEANUT_LOGO_BLACK, and PEANUTMAN to come directly
from their specific asset files instead of the barrel, keeping the rest of
PublicProfile unchanged.

Source: Linters/SAST tools

*/

import { PEANUT_LOGO_BLACK, PEANUTMAN_LOGO } from '@/assets'
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use specific import paths instead of the @/assets barrel.

The @/assets barrel import is restricted. Split into direct source imports:

-import { PEANUT_LOGO_BLACK, PEANUTMAN } from '`@/assets`'
+import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
+import { PEANUTMAN } from '`@/assets/mascot`'
📝 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
import { PEANUT_LOGO_BLACK, PEANUTMAN } from '@/assets'
import { PEANUT_LOGO_BLACK } from '`@/assets/logos`'
import { PEANUTMAN } from '`@/assets/mascot`'
🧰 Tools
🪛 ESLint

[error] 11-11: '@/assets' import is restricted from being used. Import from a specific file instead of the '@/assets' barrel — barrels force the bundler to load every re-export and hurt build perf. See CLAUDE.md.

(no-restricted-imports)

🤖 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/features/payments/shared/components/SendWithPeanutCta.tsx` at line 11,
The import in SendWithPeanutCta should not use the restricted `@/assets` barrel;
replace the combined asset import with direct source imports for
PEANUT_LOGO_BLACK and PEANUTMAN. Update the import statement in
SendWithPeanutCta to reference the specific asset modules directly so the
component no longer depends on the barrel.

Source: Linters/SAST tools

return (
<div className="flex items-center gap-1">
<Image src={PEANUTMAN_LOGO} alt="Peanut Logo" className="size-5" />
<Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update mascot alt to "Peanut mascot" per PR objectives.

The PEANUTMAN image here still carries alt="Peanut Logo" instead of "Peanut mascot" as described in the PR summary.

-                <Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />
+                <Image src={PEANUTMAN} alt="Peanut mascot" className="size-5" />
📝 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
<Image src={PEANUTMAN} alt="Peanut Logo" className="size-5" />
<Image src={PEANUTMAN} alt="Peanut mascot" className="size-5" />
🤖 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/features/payments/shared/components/SendWithPeanutCta.tsx` at line 103,
Update the PEANUTMAN image in SendWithPeanutCta so its alt text matches the PR
objective: change the Image component’s alt from “Peanut Logo” to “Peanut
mascot”. Use the existing PEANUTMAN usage in SendWithPeanutCta to locate the
change and keep the rest of the component unchanged.

@0xkkonrad 0xkkonrad added the chore Maintenance / tooling / renames — no product behavior change label Jul 1, 2026
It duplicated the existing public/peanutman-logo.svg (identical blob) and
was referenced by nothing. The in-app mascot resolves via the src import
(@/assets/mascot -> peanutman.svg); external emails/embeds use the existing
peanutman-logo.svg alias. A second never-deployed public copy was dead weight.
Brings in dev's PixelatedCardFace share-asset fixes (a2e8f33, b6e187d)
to resolve the conflict; does NOT pull later unrelated dev history.

# Conflicts:
#	src/components/Card/share-asset/PixelatedCardFace.tsx
@kushagrasarathe kushagrasarathe marked this pull request as ready for review July 2, 2026 06:22
@kushagrasarathe

Copy link
Copy Markdown
Contributor

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

@kushagrasarathe

Copy link
Copy Markdown
Contributor

QA testing guide (concise)

Pure asset rename (PEANUTMAN_LOGOPEANUTMAN, PEANUT_LOGO_BLACK moved to logos/). Same pixels — no visual change expected. Verify the mascot still renders (not a broken-image icon) on:

  • Loading spinner — any in-app loading state (e.g. payment flow) shows the spinning peanut mascot. Also /dev/loading-words.
  • Card face/card: mascot in the top-left of the card; and the share/save asset (/dev/share-builder) renders the pixelated mascot.
  • Claim link — open a claim link: "Receive on 🥜 Peanut" mascot + wordmark row.
  • CTA buttons — "Create account" / "Join" / "Send with Peanut" buttons show the mascot + wordmark.
  • QR scanner — scan sheet lists Peanut with its mascot icon.
  • Transactions — a test transaction row/detail shows the mascot.
  • Notifications/notifications: items with no icon fall back to the mascot.
  • Public profile (logged out, mobile) — header shows mascot + wordmark.
  • Badges — a badge with no custom art falls back to the mascot.

Pass criteria: every spot above shows the peanut mascot, no broken images, no layout shift.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for leftover PEANUTMAN_LOGO references ==="
rg -n 'PEANUTMAN_LOGO' --type=ts --type=tsx 2>/dev/null || rg -n 'PEANUTMAN_LOGO'

echo "=== Search for old file path references ==="
rg -n "peanut-logo\.svg" .

echo "=== Search for PEANUT_LOGO_BLACK usages ==="
rg -n "PEANUT_LOGO_BLACK" -C2

echo "=== List mascot asset dir ==="
fd . src/assets/mascot

echo "=== List illustrations dir for peanut-logo-dark ==="
fd . src/assets/illustrations | rg -i "peanut"

echo "=== List logos dir ==="
fd . src/assets/logos

echo "=== Check public svg files ==="
fd -i "peanutman" public

echo "=== Diff sizes of public svgs ==="
if [ -f public/peanutman.svg ] && [ -f public/peanutman-logo.svg ]; then
  diff public/peanutman.svg public/peanutman-logo.svg && echo "IDENTICAL" || echo "DIFFERENT"
fi

echo "=== Check mascot/index.ts content ==="
cat src/assets/mascot/index.ts

echo "=== Check illustrations/index.ts content ==="
cat src/assets/illustrations/index.ts

echo "=== Check logos/index.ts content ==="
cat src/assets/logos/index.ts

Length of output: 1436


🏁 Script executed:

#!/bin/bash
echo "=== Search for leftover PEANUTMAN_LOGO references ==="
rg -n 'PEANUTMAN_LOGO' . || echo "none found"

echo "=== Search for old file path references peanut-logo.svg ==="
rg -n "peanut-logo\.svg" . || echo "none found"

echo "=== Search for PEANUT_LOGO_BLACK usages ==="
rg -n "PEANUT_LOGO_BLACK" -C2 . || echo "none found"

echo "=== List mascot asset dir ==="
fd . src/assets/mascot || echo "dir not found"

echo "=== List illustrations dir for peanut-logo-dark ==="
fd . src/assets/illustrations || echo "dir not found"

echo "=== List logos dir ==="
fd . src/assets/logos || echo "dir not found"

echo "=== Check public svg files ==="
fd -i "peanutman" public || echo "none found"

echo "=== Check mascot/index.ts content ==="
cat src/assets/mascot/index.ts || echo "not found"

echo "=== Check illustrations/index.ts content ==="
cat src/assets/illustrations/index.ts || echo "not found"

echo "=== Check logos/index.ts content ==="
cat src/assets/logos/index.ts || echo "not found"

Length of output: 15606


🏁 Script executed:

#!/bin/bash
echo "=== Search for peanutman.svg across whole repo ==="
fd -i "peanutman.svg" .

echo "=== List public dir root svgs matching peanut ==="
fd -i peanut public

echo "=== Check src/assets/index.ts aggregator ==="
find src/assets -maxdepth 1 -name "index.ts" -exec cat {} \;

echo "=== Check git log for this PR diff on public dir ==="
git log --oneline -5

echo "=== show diff of public folder in last few commits ==="
git diff HEAD~3 HEAD -- public | head -100

Length of output: 1666


🏁 Script executed:

#!/bin/bash
echo "=== Show commit that added public/peanutman.svg ==="
git show 8e5802d2 --stat

echo "=== Show commit that dropped it ==="
git show 0a82a0f4 --stat
git show 0a82a0f4

echo "=== Search where public/peanutman.svg might be referenced expecting existence ==="
rg -n "peanutman.svg" . 
rg -n "peanutman-logo.svg" .

Length of output: 8205


🏁 Script executed:

#!/bin/bash
echo "=== Search for direct file path imports possibly missed ==="
rg -n "assets/mascot/peanut-logo" .
rg -n "assets/illustrations/peanut-logo-dark" .
rg -n "peanut-logo-dark.svg" .

echo "=== Check any remaining references importing PEANUT_LOGO_BLACK from illustrations ==="
rg -n "from '`@/assets/illustrations`'" -A0 . | rg -i "PEANUT_LOGO_BLACK" || echo "clean"

echo "=== Check import consistency: PEANUT_LOGO_BLACK import sources ==="
rg -n "import.*PEANUT_LOGO_BLACK" .

Length of output: 1736


Confirmed all findings I needed. Ready to reply.

@kushagrasarathe I did a deep pass over the diff (including the two follow-up commits 8e5802d2 and 0a82a0f4). Summary below.

Risk of new bugs / breaking functionality — Low, one thing to double-check

  • No stray references to PEANUTMAN_LOGO or the old mascot/peanut-logo.svg file path remain anywhere in src/. The rename is clean.
  • PEANUT_LOGO_BLACK was fully moved out of src/assets/illustrations/index.ts into src/assets/logos/index.ts, and every call site (SendWithPeanutCta.tsx, SendLinkActionList.tsx, Initial.view.tsx, PublicProfile.tsx, ConfirmInviteModal/index.tsx, CreateAccountButton/index.tsx, NoMoreJailModal/index.tsx) correctly resolves it via @/assets or @/assets/logos — no dangling imports from illustrations.
  • One notable discrepancy vs. the stated PR objective: the objective says "adds public/peanutman.svg as the correctly named file while keeping public/peanutman-logo.svg as an alias." However, the follow-up commit 0a82a0f4 ("drop redundant public/peanutman.svg alias") removed public/peanutman.svg again, reasoning that it was unreferenced and duplicated public/peanutman-logo.svg (identical blob). So in the current HEAD, only public/peanutman-logo.svg exists in public/public/peanutman.svg does not exist. That's a reasonable call (no dead weight), but the PR description/summary is now stale and should be updated to avoid confusion for reviewers/future readers, since it explicitly promises the new public path exists.
  • No behavior/rendering differences were found — this is purely an identifier/path rename plus one dropped duplicate public file.

Performance / latency — No impact

  • Pure export/identifier renames and an import path change (illustrationslogos) resolve to the same underlying SVG bytes; bundlers will tree-shake/hash identically. No new network requests, no additional bundle size, no runtime cost.

Code quality / DRY — Minor nit

  • Import style for PEANUT_LOGO_BLACK is inconsistent: most call sites import it from the barrel @/assets, but src/components/Claim/Link/SendLinkActionList.tsx imports it directly from @/assets/logos. Both work today because @/assets/index.ts re-exports ./logos, but it'd be more consistent (and less surprising if the barrel structure changes later) to standardize on importing from @/assets everywhere, matching the sibling files.
  • Everything else (alt-text change from "logo" to "Peanut mascot", ASSET_ variable renames in PixelatedCardFace.tsx) is a straightforward, well-scoped rename with no logic changes.

Overall this is a safe, mechanical rename with good discipline (checked all call sites, no leftover references). The only action items are: (1) reconcile the PR description with the actual final state regarding public/peanutman.svg, and (2) optionally align the one inconsistent import path for PEANUT_LOGO_BLACK.

@kushagrasarathe

Copy link
Copy Markdown
Contributor

@coderabbitai thanks — addressed:

  1. Stale description — updated. The body now reflects that public/peanutman.svg was dropped (0a82a0f4) and only public/peanutman-logo.svg remains as the public alias. No new public path is introduced.

  2. Import path for PEANUT_LOGO_BLACK — leaving SendLinkActionList.tsx on the specific import @/assets/logos. Our repo guideline (CLAUDE.md, peanut-ui import rules) explicitly prefers specific-file imports over barrel imports for build performance, so @/assets/logos is the preferred form here — standardizing the other way would move toward the barrel. Not changing it to keep the diff mechanical.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@kushagrasarathe Thanks for the update and the clarification, both make sense.

  1. Confirmed the PR description now accurately reflects that public/peanutman.svg was removed in 0a82a0f4 and only public/peanutman-logo.svg remains as the public alias.
  2. Good call on keeping @/assets/logos in SendLinkActionList.tsx — that's a reasonable tradeoff for build performance, and I'll keep it in mind for future reviews of this repo.

✏️ Learnings added
Learnt from: kushagrasarathe
URL: https://github.com/peanutprotocol/peanut-ui/pull/2317

Timestamp: 2026-07-02T06:30:54.665Z
Learning: In the peanut-ui repository, per repo guideline in CLAUDE.md ("peanut-ui import rules"), specific-file imports (e.g., `@/assets/logos`) are preferred over barrel imports (e.g., `@/assets`) for build performance reasons. Do not flag specific-file imports as an inconsistency when a barrel export also exists.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@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: 5

🧹 Nitpick comments (3)
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx (1)

101-109: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid wrapping an already-stable callback.

trackUpliftCompleted is a zero-arg useCallback memoized on [channel] (channel is the constant 'withdraw'), so it's already referentially stable. Wrapping it in () => trackUpliftCompleted() creates a new function identity every render, which propagates into useMultiPhaseKycFlow's handleSumsubApproved (memoized with onKycApproved in its deps) and onward into its useSumsubKycFlow call.

♻️ Proposed fix
     const sumsubFlow = useMultiPhaseKycFlow({
         // Fire completed at Sumsub approval (verification submitted), not at
         // end-of-flow — so it isn't lost if the user drops during the
         // post-approval ToS / preparing steps.
-        onKycApproved: () => trackUpliftCompleted(),
+        onKycApproved: trackUpliftCompleted,
         // Abandoned attempt: clear the pending start so a later unrelated KYC
         // success on this page can't mis-fire eea_uplift_completed.
         onManualClose: resetUpliftFunnel,
     })
🤖 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)/withdraw/[country]/bank/page.tsx around lines 101 - 109,
Avoid wrapping the already-stable trackUpliftCompleted callback in the
sumsubFlow setup. In the withdrawal bank page’s useMultiPhaseKycFlow call, pass
trackUpliftCompleted directly as onKycApproved so the function identity stays
stable and does not retrigger handleSumsubApproved or useSumsubKycFlow
unnecessarily.
src/components/Card/share-asset/captureShareAsset.ts (1)

56-69: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a timeout guard on waitForAssetReady.

Neither document.fonts.ready nor img.decode() have an upper bound here. A stalled promise (rare, but possible on flaky networks) would leave the Share/Save buttons stuck in a loading state indefinitely, since callers only set isSharing/isSaving false in a finally after this resolves.

🛡️ Proposed fix to bound the wait
 export async function waitForAssetReady(node: HTMLElement): Promise<void> {
-    if (typeof document !== 'undefined' && document.fonts?.ready) {
-        try {
-            await document.fonts.ready
-        } catch {
-            // fonts.ready can reject in odd states — capture anyway.
-        }
-    }
-    await Promise.all(
-        Array.from(node.querySelectorAll('img')).map((img) =>
-            typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve()
-        )
-    )
+    const withTimeout = (p: Promise<unknown>, ms = 5000) =>
+        Promise.race([p, new Promise((resolve) => setTimeout(resolve, ms))])
+
+    if (typeof document !== 'undefined' && document.fonts?.ready) {
+        try {
+            await withTimeout(document.fonts.ready)
+        } catch {
+            // fonts.ready can reject in odd states — capture anyway.
+        }
+    }
+    await withTimeout(
+        Promise.all(
+            Array.from(node.querySelectorAll('img')).map((img) =>
+                typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve()
+            )
+        )
+    )
 }
🤖 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/share-asset/captureShareAsset.ts` around lines 56 - 69,
Add a timeout guard to waitForAssetReady so it cannot wait forever on
document.fonts.ready or img.decode. Update the helper in captureShareAsset.ts to
wrap those awaits with a bounded timeout/fallback, and keep the existing
best-effort behavior by continuing even if the timeout or either promise fails.
Use the waitForAssetReady symbol as the place to implement the guard.
src/components/Card/share-asset/ShareAssetD3.tsx (1)

260-289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated offset-shadow workaround.

This "absolute sibling instead of box-shadow" pattern (to work around html-to-image squaring rounded box-shadows) is now duplicated here and in PixelatedCardFace's card shadow, each with its own hardcoded offset/inset. Consider extracting a small shared helper (e.g. a RoundedDropShadow component taking offset/radius/color) to keep the workaround centralized.

🤖 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/share-asset/ShareAssetD3.tsx` around lines 260 - 289, The
offset-shadow workaround for rounded elements is duplicated in ShareAssetD3 and
PixelatedCardFace, with hardcoded inset/translate values in each place. Extract
this pattern into a shared helper component or utility (for example, a
RoundedDropShadow that accepts offset, radius, and color) and update the pill
shadow here to use it so the html-to-image workaround stays centralized.
🤖 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/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 84-101: The EEA uplift funnel in the bank page KYC flow is not
being cleared when Sumsub ends in terminal failure states, so a later approval
can complete an earlier pending start. Update the `useMultiPhaseKycFlow`
handling around `onKycApproved`, `onKycSuccess`, and `onManualClose` so that
terminal outcomes like `REJECTED` and `ACTION_REQUIRED` also call
`resetUpliftFunnel`, or otherwise tie `trackUpliftCompleted` to the specific
launch that started the funnel.

In `@src/components/Card/BadgeSkipCelebration.tsx`:
- Around line 64-66: The readiness gate in BadgeSkipCelebration can dead-end
because assetReady only flips from PixelatedHand success paths; add an
error/timeout fallback so Share/Save can still enable if the hand asset fails to
load. Update the PixelatedHand wiring and the local state handling in
BadgeSkipCelebration to trigger the same ready path on failure, keeping the
buttons from staying permanently disabled.

In `@src/components/Card/share-asset/PixelatedCardFace.tsx`:
- Around line 58-62: The readiness gate in PixelatedCardFace can get stuck when
the hand <img> fails to load, because the component only triggers onReady from
onLoad or the initial complete check. Update the image handling in
PixelatedCardFace so the same ready-state path is reached from an onError case
too, and make sure the existing ready callback logic still fires from the shared
load-checking code. Use the PixelatedCardFace component and its onReady flow as
the reference points when wiring the fallback.

In `@src/hooks/useAdvisoryPreempt.ts`:
- Around line 51-65: The failure path in completeNow currently re-opens the
modal unconditionally, which can restore a gate that is no longer needed if
advisory was cleared while onCompleteNow() was running. Update
useAdvisoryPreempt’s completeNow callback to check that advisory is still
pending before calling setVisible(true) in the catch block, using the advisory
state/value already available in the hook. Keep the existing completableRef and
auto-close behavior intact, but only re-show the gate when the requirement still
exists.

In `@src/hooks/useEeaUpliftFunnel.ts`:
- Around line 26-40: `trackStarted` in `useEeaUpliftFunnel` is emitting
`EEA_UPLIFT_STARTED` on every call, which double-counts retries for the same
advisory. Make the start event idempotent by checking `startedRef.current`
inside `trackStarted` and only capturing when the current advisory/requirement
hasn’t already been started, while still updating the ref on the first call.
Keep the existing event payload fields (`requirement_key`, `action_key`,
`effective_date`) and preserve the `trackStarted`/`startedRef` flow so all
callers, including `onCompleteNow`, benefit from the dedupe.

---

Nitpick comments:
In `@src/app/`(mobile-ui)/withdraw/[country]/bank/page.tsx:
- Around line 101-109: Avoid wrapping the already-stable trackUpliftCompleted
callback in the sumsubFlow setup. In the withdrawal bank page’s
useMultiPhaseKycFlow call, pass trackUpliftCompleted directly as onKycApproved
so the function identity stays stable and does not retrigger
handleSumsubApproved or useSumsubKycFlow unnecessarily.

In `@src/components/Card/share-asset/captureShareAsset.ts`:
- Around line 56-69: Add a timeout guard to waitForAssetReady so it cannot wait
forever on document.fonts.ready or img.decode. Update the helper in
captureShareAsset.ts to wrap those awaits with a bounded timeout/fallback, and
keep the existing best-effort behavior by continuing even if the timeout or
either promise fails. Use the waitForAssetReady symbol as the place to implement
the guard.

In `@src/components/Card/share-asset/ShareAssetD3.tsx`:
- Around line 260-289: The offset-shadow workaround for rounded elements is
duplicated in ShareAssetD3 and PixelatedCardFace, with hardcoded inset/translate
values in each place. Extract this pattern into a shared helper component or
utility (for example, a RoundedDropShadow that accepts offset, radius, and
color) and update the pill shadow here to use it so the html-to-image workaround
stays centralized.
🪄 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: b1a415e0-3e0a-4c79-a78a-a40281b6e180

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5802d and ffddcc1.

⛔ Files ignored due to path filters (1)
  • src/assets/cards/peanut-card-hand-pixel.png is excluded by !**/*.png
📒 Files selected for processing (36)
  • e2e/flows/share-asset-capture.spec.ts
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/card/page.tsx
  • src/app/(mobile-ui)/dev/rejection-builder/page.tsx
  • src/app/(mobile-ui)/dev/share-builder/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/components/Card/ApplicationStatusScreen.tsx
  • src/components/Card/BadgeSkipCelebration.tsx
  • src/components/Card/CardRejectionScreen.tsx
  • src/components/Card/CardUnlockDrawer.tsx
  • src/components/Card/CardWaitlistJoinedScreen.tsx
  • src/components/Card/PhysicalCardScreen.tsx
  • src/components/Card/__tests__/ApplicationStatusScreen.test.tsx
  • src/components/Card/__tests__/doorTally.utils.test.ts
  • src/components/Card/doorTally.utils.ts
  • src/components/Card/share-asset/PixelatedCardFace.tsx
  • src/components/Card/share-asset/ShareAssetActions.tsx
  • src/components/Card/share-asset/ShareAssetD3.tsx
  • src/components/Card/share-asset/__tests__/captureShareAsset.test.ts
  • src/components/Card/share-asset/captureShareAsset.ts
  • src/components/Card/share-asset/shareAsset.types.ts
  • src/components/Claim/Link/SendLinkActionList.tsx
  • src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx
  • src/components/Home/CardLaunchCTA/CardLaunchCTABanner.tsx
  • src/components/Kyc/AdvisoryPreemptModal.tsx
  • src/components/Kyc/states/KycActionRequired.tsx
  • src/components/Kyc/states/__tests__/KycActionRequired.rejectCopy.test.tsx
  • src/components/Kyc/states/__tests__/KycStates.test.tsx
  • src/constants/analytics.consts.ts
  • src/hooks/useAdvisoryPreempt.test.ts
  • src/hooks/useAdvisoryPreempt.ts
  • src/hooks/useEeaUpliftFunnel.test.ts
  • src/hooks/useEeaUpliftFunnel.ts
  • src/hooks/useMultiPhaseKycFlow.ts
  • src/services/card.ts
  • src/services/rain.ts
💤 Files with no reviewable changes (1)
  • src/components/Card/CardWaitlistJoinedScreen.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/Card/PhysicalCardScreen.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/Claim/Link/SendLinkActionList.tsx

Comment on lines +84 to 101
const {
trackStarted: trackUpliftStarted,
trackCompleted: trackUpliftCompleted,
reset: resetUpliftFunnel,
} = useEeaUpliftFunnel('deposit')

const sumsubFlow = useMultiPhaseKycFlow({
// Fire completed at Sumsub approval (verification submitted), not at
// end-of-flow — so it isn't lost if the user drops during the
// post-approval ToS / preparing steps.
onKycApproved: () => trackUpliftCompleted(),
onKycSuccess: () => {
setUrlState({ step: 'inputAmount' })
},
// Abandoned attempt: clear the pending start so a later unrelated KYC
// success on this page can't mis-fire eea_uplift_completed.
onManualClose: resetUpliftFunnel,
})

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files and inspect the surrounding code.
git ls-files 'src/app/(mobile-ui)/add-money/[country]/bank/page.tsx' 'src/**/useMultiPhaseKycFlow.ts' 'src/**/useEeaUpliftFunnel*' 'src/**/Kyc*' | sed 's#^`#FILE` #'

echo
echo "=== page.tsx outline ==="
ast-grep outline 'src/app/(mobile-ui)/add-money/[country]/bank/page.tsx' --view expanded || true

echo
echo "=== useMultiPhaseKycFlow outline ==="
fd 'useMultiPhaseKycFlow.*' src -t f -a | head -n 20 | while read -r f; do
  echo "--- $f ---"
  ast-grep outline "$f" --view expanded || true
done

echo
echo "=== Relevant references in page.tsx ==="
rg -n "useMultiPhaseKycFlow|onKycApproved|onKycSuccess|onManualClose|KYC_REJECTED|ACTION_REQUIRED|fixable-rejection|trackUpliftStarted|trackUpliftCompleted|resetUpliftFunnel|eea_uplift_completed" 'src/app/(mobile-ui)/add-money/[country]/bank/page.tsx'

echo
echo "=== Relevant references in useMultiPhaseKycFlow files ==="
for f in $(fd 'useMultiPhaseKycFlow.*' src -t f -a); do
  echo "--- $f ---"
  rg -n "onManualClose|ACTION_REQUIRED|KYC_REJECTED|onKycApproved|onKycSuccess|reset|fetchUser|liveKycStatus" "$f"
done

echo
echo "=== Nearby code around the relevant area in page.tsx ==="
sed -n '1,220p' 'src/app/(mobile-ui)/add-money/[country]/bank/page.tsx'

echo
echo "=== Relevant useMultiPhaseKycFlow implementation slice(s) ==="
for f in $(fd 'useMultiPhaseKycFlow.*' src -t f -a); do
  echo "--- $f (first 260 lines) ---"
  sed -n '1,260p' "$f"
done

Repository: peanutprotocol/peanut-ui

Length of output: 27691


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== useMultiPhaseKycFlow.ts around rejection / close / manual-close paths ==="
sed -n '220,460p' 'src/hooks/useMultiPhaseKycFlow.ts'

echo
echo "=== useSumsubKycFlow.ts outline and relevant callbacks ==="
fd 'useSumsubKycFlow.*' src -t f -a | while read -r f; do
  echo "--- $f ---"
  ast-grep outline "$f" --view expanded || true
  echo
  rg -n "onManualClose|handleClose|ACTION_REQUIRED|REJECTED|handleRestartIdentity|handleSelfHealResubmit|handleStartAction|handleSdkComplete|handleInitiateKyc|closeVerificationProgressModal" "$f"
done

echo
echo "=== page.tsx around the fixable-rejection branch ==="
sed -n '430,500p' 'src/app/(mobile-ui)/add-money/[country]/bank/page.tsx'

echo
echo "=== useEeaUpliftFunnel.ts ==="
sed -n '1,220p' 'src/hooks/useEeaUpliftFunnel.ts'

Repository: peanutprotocol/peanut-ui

Length of output: 17036


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '360,470p' 'src/hooks/useSumsubKycFlow.ts'

Repository: peanutprotocol/peanut-ui

Length of output: 4754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== useSumsubKycFlow.ts status-transition block ==="
sed -n '60,120p' 'src/hooks/useSumsubKycFlow.ts'

echo
echo "=== useSumsubKycFlow.ts close/manual-close handlers ==="
sed -n '300,345p' 'src/hooks/useSumsubKycFlow.ts'

Repository: peanutprotocol/peanut-ui

Length of output: 4740


Reset the uplift latch on terminal KYC failures

REJECTED / ACTION_REQUIRED closes the KYC modal without clearing the EEA uplift start state, so a later approved resubmission on the same page can still emit eea_uplift_completed for the earlier attempt. Reset the funnel on those terminal outcomes, or scope completion to the specific launch that started it.

🤖 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)/add-money/[country]/bank/page.tsx around lines 84 - 101,
The EEA uplift funnel in the bank page KYC flow is not being cleared when Sumsub
ends in terminal failure states, so a later approval can complete an earlier
pending start. Update the `useMultiPhaseKycFlow` handling around
`onKycApproved`, `onKycSuccess`, and `onManualClose` so that terminal outcomes
like `REJECTED` and `ACTION_REQUIRED` also call `resetUpliftFunnel`, or
otherwise tie `trackUpliftCompleted` to the specific launch that started the
funnel.

Comment on lines +64 to +66
// Gate the Share/Save buttons until the card face's async hand <canvas>
// mounts — otherwise an early capture snapshots a blank card.
const [assetReady, setAssetReady] = useState(false)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and surrounding symbols.
ast-grep outline src/components/Card/BadgeSkipCelebration.tsx --view expanded || true

printf '\n--- line count ---\n'
wc -l src/components/Card/BadgeSkipCelebration.tsx

printf '\n--- relevant excerpt ---\n'
sed -n '1,260p' src/components/Card/BadgeSkipCelebration.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 9708


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the readiness callback contract and any fallback/error handling in nearby components.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  "onReady|assetReady|ScaledShareAsset|ShareAssetD3|ShareAssetActions" src

printf '\n--- files with onReady ---\n'
git ls-files src | xargs rg -n "onReady" -- {}

Repository: peanutprotocol/peanut-ui

Length of output: 11289


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the implementation of the source of the callback if found.
for f in $(git ls-files src | grep -E 'ScaledShareAsset|ShareAssetD3|ShareAssetActions|BadgeSkipCelebration'); do
  echo "===== $f ====="
  wc -l "$f"
done

Repository: peanutprotocol/peanut-ui

Length of output: 627


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PixelatedCardFace excerpt ---'
sed -n '260,330p' src/components/Card/share-asset/PixelatedCardFace.tsx

printf '\n%s\n' '--- ShareAssetActions excerpt ---'
sed -n '60,120p' src/components/Card/share-asset/ShareAssetActions.tsx

printf '\n%s\n' '--- ShareAssetD3 excerpt ---'
sed -n '90,240p' src/components/Card/share-asset/ShareAssetD3.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 12041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- CardUnlockDrawer excerpt ---'
sed -n '1,120p' src/components/Card/CardUnlockDrawer.tsx

printf '\n%s\n' '--- dev share-builder excerpt ---'
sed -n '40,430p' src/app/'(mobile-ui)'/dev/share-builder/page.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 24556


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for onError / timeout / fallback in the share asset pipeline.
rg -n "onError|setAssetReady\(true\)|complete\)|fail-safe|timeout|fallback" src/components/Card/share-asset src/components/Card/CardUnlockDrawer.tsx src/components/Card/BadgeSkipCelebration.tsx src/app/'(mobile-ui)'/dev/share-builder/page.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 1188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' src/components/Card/share-asset/PixelatedCardFace.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 10261


Add a fallback for the readiness gate — a failed hand load can strand Share/Save.

PixelatedHand only calls onReady on load or cached-complete; if the asset errors, assetReady never flips and both buttons stay disabled. Add an onError or timeout path so the celebration screen doesn’t dead-end.

🤖 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/BadgeSkipCelebration.tsx` around lines 64 - 66, The
readiness gate in BadgeSkipCelebration can dead-end because assetReady only
flips from PixelatedHand success paths; add an error/timeout fallback so
Share/Save can still enable if the hand asset fails to load. Update the
PixelatedHand wiring and the local state handling in BadgeSkipCelebration to
trigger the same ready path on failure, keeping the buttons from staying
permanently disabled.

Comment on lines +58 to 62
/** Fires once the pixelated hand <img> has loaded — i.e. the card face is
* fully painted. Capture surfaces gate the Share/Save buttons on this so a
* snapshot can never fire before the hand is ready. */
onReady?: () => void
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing onError handler leaves the readiness gate stuck if the hand image fails to load.

onReady only fires on onLoad or an already-complete image at mount. If the pixelated-hand <img> errors out after mount (network blip, CDN hiccup), complete becomes true but nothing re-checks it, and onLoad never fires — so onReady is never called and the Share/Save buttons stay disabled forever, in addition to a broken-image icon on the card face.

🛠️ Proposed fix
     <img
         src={ASSET_CARD_HAND_PIXEL}
         alt=""
         aria-hidden
         draggable={false}
         onLoad={() => onReady?.()}
+        onError={() => onReady?.()}
         ref={(img) => {
             // A cached image can already be `complete` before React attaches
             // onLoad, so the load event never fires — that would leave the
             // capture gate stuck disabled. Fire onReady directly in that case.
             if (img?.complete) onReady?.()
         }}

Also applies to: 289-312

🤖 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/share-asset/PixelatedCardFace.tsx` around lines 58 - 62,
The readiness gate in PixelatedCardFace can get stuck when the hand <img> fails
to load, because the component only triggers onReady from onLoad or the initial
complete check. Update the image handling in PixelatedCardFace so the same
ready-state path is reached from an onError case too, and make sure the existing
ready callback logic still fires from the shared load-checking code. Use the
PixelatedCardFace component and its onReady flow as the reference points when
wiring the fallback.

Comment on lines 51 to 65
const completeNow = useCallback(async () => {
if (completingRef.current) return
completingRef.current = true
setDismissed(true)
setVisible(false)
pendingProceed.current = null
try {
await onCompleteNow()
} catch (error) {
// Launch failed — re-show the gate so the user isn't left with a
// silently dismissed mandatory step and a still-pending requirement.
setVisible(true)
throw error
} finally {
completingRef.current = false
}
}, [onCompleteNow])

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Re-showing the modal on failure should check advisory is still pending.

If the backend clears advisory while onCompleteNow() is in flight and it then rejects, setVisible(true) unconditionally reopens the mandatory gate even though the requirement is already resolved — and the auto-close effect won't re-fire again since advisory isn't changing a second time.

🐛 Proposed fix
     const completeNow = useCallback(async () => {
         if (completingRef.current) return
         completingRef.current = true
         setVisible(false)
         try {
             await onCompleteNow()
         } catch (error) {
-            // Launch failed — re-show the gate so the user isn't left with a
-            // silently dismissed mandatory step and a still-pending requirement.
-            setVisible(true)
+            // Launch failed — re-show the gate, but only if the requirement is
+            // still outstanding (it may have cleared while the launch was in flight).
+            if (advisory) setVisible(true)
             throw error
         } finally {
             completingRef.current = false
         }
-    }, [onCompleteNow])
+    }, [onCompleteNow, advisory])
📝 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 completeNow = useCallback(async () => {
if (completingRef.current) return
completingRef.current = true
setDismissed(true)
setVisible(false)
pendingProceed.current = null
try {
await onCompleteNow()
} catch (error) {
// Launch failed — re-show the gate so the user isn't left with a
// silently dismissed mandatory step and a still-pending requirement.
setVisible(true)
throw error
} finally {
completingRef.current = false
}
}, [onCompleteNow])
const completeNow = useCallback(async () => {
if (completingRef.current) return
completingRef.current = true
setVisible(false)
try {
await onCompleteNow()
} catch (error) {
// Launch failed — re-show the gate, but only if the requirement is
// still outstanding (it may have cleared while the launch was in flight).
if (advisory) setVisible(true)
throw error
} finally {
completingRef.current = false
}
}, [onCompleteNow, advisory])
🤖 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/useAdvisoryPreempt.ts` around lines 51 - 65, The failure path in
completeNow currently re-opens the modal unconditionally, which can restore a
gate that is no longer needed if advisory was cleared while onCompleteNow() was
running. Update useAdvisoryPreempt’s completeNow callback to check that advisory
is still pending before calling setVisible(true) in the catch block, using the
advisory state/value already available in the hook. Keep the existing
completableRef and auto-close behavior intact, but only re-show the gate when
the requirement still exists.

Comment on lines +26 to +40
const trackStarted = useCallback(
// `advisory` is required: callers gate on it before launching, and the
// funnel contract needs requirement_key / action_key / effective_date
// always present on the event.
(advisory: GateAdvisory) => {
startedRef.current = advisory
posthog.capture(ANALYTICS_EVENTS.EEA_UPLIFT_STARTED, {
channel,
requirement_key: advisory.requirementKey,
action_key: advisory.actionKey,
effective_date: advisory.effectiveDate,
})
},
[channel]
)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Retry after a failed launch double-counts EEA_UPLIFT_STARTED.

trackStarted fires unconditionally on every call. In add-money/[country]/bank/page.tsx, onCompleteNow calls trackUpliftStarted(advisory) before handleSelfHealResubmit; if that call fails, useAdvisoryPreempt.completeNow re-shows the modal and a retry click fires EEA_UPLIFT_STARTED again for the same requirement before any COMPLETED is ever recorded, inflating the funnel's start count relative to completions.

Making the start idempotent per requirement fixes this for every caller of the hook.

🔧 Proposed fix
     const trackStarted = useCallback(
         // `advisory` is required: callers gate on it before launching, and the
         // funnel contract needs requirement_key / action_key / effective_date
         // always present on the event.
         (advisory: GateAdvisory) => {
+            // Idempotent per requirement: a retry after a failed launch attempt
+            // shouldn't inflate the funnel with a second `started` event.
+            if (startedRef.current?.requirementKey === advisory.requirementKey) return
             startedRef.current = advisory
             posthog.capture(ANALYTICS_EVENTS.EEA_UPLIFT_STARTED, {
                 channel,
                 requirement_key: advisory.requirementKey,
                 action_key: advisory.actionKey,
                 effective_date: advisory.effectiveDate,
             })
         },
         [channel]
     )
📝 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 trackStarted = useCallback(
// `advisory` is required: callers gate on it before launching, and the
// funnel contract needs requirement_key / action_key / effective_date
// always present on the event.
(advisory: GateAdvisory) => {
startedRef.current = advisory
posthog.capture(ANALYTICS_EVENTS.EEA_UPLIFT_STARTED, {
channel,
requirement_key: advisory.requirementKey,
action_key: advisory.actionKey,
effective_date: advisory.effectiveDate,
})
},
[channel]
)
const trackStarted = useCallback(
// `advisory` is required: callers gate on it before launching, and the
// funnel contract needs requirement_key / action_key / effective_date
// always present on the event.
(advisory: GateAdvisory) => {
// Idempotent per requirement: a retry after a failed launch attempt
// shouldn't inflate the funnel with a second `started` event.
if (startedRef.current?.requirementKey === advisory.requirementKey) return
startedRef.current = advisory
posthog.capture(ANALYTICS_EVENTS.EEA_UPLIFT_STARTED, {
channel,
requirement_key: advisory.requirementKey,
action_key: advisory.actionKey,
effective_date: advisory.effectiveDate,
})
},
[channel]
)
🤖 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/useEeaUpliftFunnel.ts` around lines 26 - 40, `trackStarted` in
`useEeaUpliftFunnel` is emitting `EEA_UPLIFT_STARTED` on every call, which
double-counts retries for the same advisory. Make the start event idempotent by
checking `startedRef.current` inside `trackStarted` and only capturing when the
current advisory/requirement hasn’t already been started, while still updating
the ref on the first call. Keep the existing event payload fields
(`requirement_key`, `action_key`, `effective_date`) and preserve the
`trackStarted`/`startedRef` flow so all callers, including `onCompleteNow`,
benefit from the dedupe.

@kushagrasarathe

Copy link
Copy Markdown
Contributor

Superseded by #2327 — same change, squashed to a single clean commit off a fresh base (this branch could not be force-pushed to clean up its merge history). Closing in favor of #2327.

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

Labels

chore Maintenance / tooling / renames — no product behavior change enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants