refactor(assets): name the mascot a mascot, not a logo#2317
Conversation
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).
…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).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe PR renames the mascot asset export to ChangesMascot Asset Rename and Consumer Updates
Share Asset Readiness and Capture Gating
Card Rejection and Waitlist Presentation
KYC, Advisory, and EEA Uplift Banking Flows
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
Code-analysis diffPainscore total: 5860.07 → 5862.92 (+2.85) 🆕 New findings (2)
✅ Resolved (2)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (2)
src/app/(mobile-ui)/notifications/page.tsx (1)
12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider importing from
@/assets/mascotdirectly to resolve the barrel import violation.The static analysis flags
@/assetsas a restricted barrel import. While this file already used@/assetsforPEANUTMAN_LOGO, the rename is a good opportunity to align with the project rule and importPEANUTMANfrom@/assets/mascotinstead, matching whatsrc/app/dev/loading-words/page.tsxdoes.-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 winStale alt text "logo".
PR objective states the mascot alt text is updated from
"logo"to"Peanut mascot";PeanutLoading/index.tsxwas updated accordingly but this sibling component still usesalt="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
⛔ Files ignored due to path filters (3)
public/peanutman.svgis excluded by!**/*.svgsrc/assets/logos/peanut-logo-dark.svgis excluded by!**/*.svgsrc/assets/mascot/peanutman.svgis excluded by!**/*.svg
📒 Files selected for processing (21)
src/app/(mobile-ui)/notifications/page.tsxsrc/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsxsrc/app/dev/loading-words/page.tsxsrc/assets/illustrations/index.tssrc/assets/logos/index.tssrc/assets/mascot/index.tssrc/components/Badges/badge.utils.tssrc/components/Card/CardFace.tsxsrc/components/Card/share-asset/PixelatedCardFace.tsxsrc/components/Claim/Link/Initial.view.tsxsrc/components/Claim/Link/SendLinkActionList.tsxsrc/components/Global/ConfirmInviteModal/index.tsxsrc/components/Global/CreateAccountButton/index.tsxsrc/components/Global/NoMoreJailModal/index.tsxsrc/components/Global/PeanutLoading/CyclingLoading.tsxsrc/components/Global/PeanutLoading/index.tsxsrc/components/Global/QRScanner/index.tsxsrc/components/Profile/components/PublicProfile.tsxsrc/components/TransactionDetails/TransactionCard.tsxsrc/components/TransactionDetails/TransactionDetailsHeaderCard.tsxsrc/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' |
There was a problem hiding this comment.
📐 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.
| 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" /> |
There was a problem hiding this comment.
🎯 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.
| <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" /> |
There was a problem hiding this comment.
🎯 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.
| <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' |
There was a problem hiding this comment.
📐 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.
| 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' |
There was a problem hiding this comment.
📐 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.
| 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' |
There was a problem hiding this comment.
📐 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.
| 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' | |||
There was a problem hiding this comment.
📐 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.
| 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' |
There was a problem hiding this comment.
📐 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.
| 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' |
There was a problem hiding this comment.
📐 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.
| 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" /> |
There was a problem hiding this comment.
🎯 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.
| <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.
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.
|
@coderabbitai review in depth. Evaluate:
|
QA testing guide (concise)Pure asset rename (
Pass criteria: every spot above shows the peanut mascot, no broken images, no layout shift. |
|
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.tsLength 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 -100Length 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.
Risk of new bugs / breaking functionality — Low, one thing to double-check
Performance / latency — No impact
Code quality / DRY — Minor nit
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 |
|
@coderabbitai thanks — addressed:
|
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx (1)
101-109: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAvoid wrapping an already-stable callback.
trackUpliftCompletedis a zero-arguseCallbackmemoized 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 intouseMultiPhaseKycFlow'shandleSumsubApproved(memoized withonKycApprovedin its deps) and onward into itsuseSumsubKycFlowcall.♻️ 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 winConsider a timeout guard on
waitForAssetReady.Neither
document.fonts.readynorimg.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 setisSharing/isSavingfalse in afinallyafter 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 winDuplicated 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. aRoundedDropShadowcomponent takingoffset/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
⛔ Files ignored due to path filters (1)
src/assets/cards/peanut-card-hand-pixel.pngis excluded by!**/*.png
📒 Files selected for processing (36)
e2e/flows/share-asset-capture.spec.tssrc/app/(mobile-ui)/add-money/[country]/bank/page.tsxsrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/dev/rejection-builder/page.tsxsrc/app/(mobile-ui)/dev/share-builder/page.tsxsrc/app/(mobile-ui)/withdraw/[country]/bank/page.tsxsrc/components/Card/ApplicationStatusScreen.tsxsrc/components/Card/BadgeSkipCelebration.tsxsrc/components/Card/CardRejectionScreen.tsxsrc/components/Card/CardUnlockDrawer.tsxsrc/components/Card/CardWaitlistJoinedScreen.tsxsrc/components/Card/PhysicalCardScreen.tsxsrc/components/Card/__tests__/ApplicationStatusScreen.test.tsxsrc/components/Card/__tests__/doorTally.utils.test.tssrc/components/Card/doorTally.utils.tssrc/components/Card/share-asset/PixelatedCardFace.tsxsrc/components/Card/share-asset/ShareAssetActions.tsxsrc/components/Card/share-asset/ShareAssetD3.tsxsrc/components/Card/share-asset/__tests__/captureShareAsset.test.tssrc/components/Card/share-asset/captureShareAsset.tssrc/components/Card/share-asset/shareAsset.types.tssrc/components/Claim/Link/SendLinkActionList.tsxsrc/components/Claim/Link/__tests__/SendLinkActionList.test.tsxsrc/components/Home/CardLaunchCTA/CardLaunchCTABanner.tsxsrc/components/Kyc/AdvisoryPreemptModal.tsxsrc/components/Kyc/states/KycActionRequired.tsxsrc/components/Kyc/states/__tests__/KycActionRequired.rejectCopy.test.tsxsrc/components/Kyc/states/__tests__/KycStates.test.tsxsrc/constants/analytics.consts.tssrc/hooks/useAdvisoryPreempt.test.tssrc/hooks/useAdvisoryPreempt.tssrc/hooks/useEeaUpliftFunnel.test.tssrc/hooks/useEeaUpliftFunnel.tssrc/hooks/useMultiPhaseKycFlow.tssrc/services/card.tssrc/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
| 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, | ||
| }) |
There was a problem hiding this comment.
🎯 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"
doneRepository: 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.
| // 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) |
There was a problem hiding this comment.
🩺 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.tsxRepository: 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"
doneRepository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: peanutprotocol/peanut-ui
Length of output: 1188
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,240p' src/components/Card/share-asset/PixelatedCardFace.tsxRepository: 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.
| /** 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 | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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]) |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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] | ||
| ) |
There was a problem hiding this comment.
🎯 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.
| 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.
Summary
PEANUTMAN_LOGOwas 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):
PEANUTMAN_LOGO→PEANUTMANand filemascot/peanut-logo.svg→mascot/peanutman.svg; update all 18 call sites, the localASSET_var, and the mascotalttext ("logo"→"Peanut mascot").PEANUT_LOGO_BLACKout ofillustrations/intologos/(it's the brand logo); fix the one subpath import.public/peanutman-logo.svgunchanged as the public alias, so external consumers hardcodingpeanut.me/peanutman-logo.svgkeep resolving. (An earlier commit added a duplicatepublic/peanutman.svg; it was dropped in0a82a0f4since it was unreferenced and byte-identical to the existing alias — no new public path is introduced.)Risks / breaking changes
PEANUTMANis a new token (no collision) and consumers import via the@/assetsbarrel which re-exports it unchanged. Typecheck enforces completeness.public/peanutman-logo.svgis retained unchanged as the public alias.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).QA
npm run typecheck— clean (catches any missed rename).npm test— green (thecountryCurrencyMappingflag suite needs thecopy-flagsprebuild, which CI/build runs).grep -rn PEANUTMAN_LOGO src→ no hits.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.