Skip to content

chore: back-merge main → dev (IRL_NOMADS badge + Manteca outage hotfixes)#2324

Merged
Hugo0 merged 50 commits into
devfrom
main
Jul 2, 2026
Merged

chore: back-merge main → dev (IRL_NOMADS badge + Manteca outage hotfixes)#2324
Hugo0 merged 50 commits into
devfrom
main

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Routine back-merge per release flow. Brings dev up to date with today's main activity: IRL_NOMADS badge (#2320), Manteca outage kill-switch/per-currency/price-shape hotfixes (#2321 #2322 #2323). dev had 0 unique commits — clean merge.

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 20 commits June 29, 2026 11:59
…assure

The "We couldn't issue you a card" screen showed only a generic catch-all
body. Surface the vetted, region-aware reason from the capability read-model
(e.g. "Peanut cards aren't available in your state yet.") above a reassurance
body — a declined card doesn't touch the rest of the account, so point the
user back to crypto deposit / withdraw / pay, which still work.
…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.
fix(card): render share-asset hand as <img>, not a runtime <canvas> (blank-card hotfix)
…ally

/shhhhh's 'you're on the waitlist · #56' showed the user's REAL queue
position, while the in-app Berghain rejection shows the FOMO-inflated door
tally ('275 tried'). The small real number on the landing page undercut the
big scarcity number in the app. Scale the /shhhhh position by the same
DOOR_TALLY_FOMO_MULTIPLIER (x5) via a shared inflateWaitlistPosition() helper
so both surfaces tell one consistent exaggerated story (#56 -> #280).
Adds the 'Floripa Fuel' badge art + FE wiring for the Floripa 'Coffee
for Nomads' nomad event. New sticker asset (badge-draft/badges-recraft),
BADGES entry, and campaign-maps so /invite resolves the badge from the
invite code (floripacoffee), utm_campaign (floripa-coffee), and as a
bare vanity ?campaign=FLORIPA_COFFEE claim link. Backend whitelist in
peanut-api-ts (companion PR).
Swap the event badge to the nomad concept: new backpack asset
irl_nomads.svg, BADGES entry IRL_NOMADS ('Nomad Mode'), and campaign
maps (invite irl_nomads / utm irl-nomads / bare vanity irl_nomads).
Companion to peanut-api-ts IRL_NOMADS rename.
…NOMADS

Follow-up to the asset swap: the code-side rename (badge.utils BADGES entry
+ campaign-maps invite/utm/vanity keys) was left unstaged in the prior commit.
…er outage

Manteca onramp/offramp are down. There was no kill-switch for these flows —
disabledPaymentProviders only gates QR-pay — so users hit an ugly mid-flow
failure (sign a spend, then the provider call throws).

Adds a disableMantecaTransfers flag (default on) that renders the same
provider-outage screen as the QR path at the two Manteca flow entry points.
Placed after the Brazil-PIX delegation so PIX-over-QR sends stay open; QR and
crypto are untouched. Flip the flag to false to restore.
…-maintenance

hotfix(maintenance): block Manteca add-money + withdraw during provider outage
…e ARS

Manteca is recovering per-currency: ARS is back, BRL still down. Replaces the
blanket disableMantecaTransfers boolean with disabledMantecaCurrencies (mirrors
the disabledPaymentProviders blocklist convention). Only currencies in the list
show the outage screen; unlisted ones stay live.

Set to ['BRL'] — ARS add-money + withdraw go live again, BRL stays blocked.
Empty the array when Manteca is fully restored.
…ncy-block

hotfix(maintenance): per-currency Manteca outage block — re-enable ARS
…e BRL

Manteca changed the /prices/direct response on 2026-07-01: the effective rate
moved from top-level effectiveBuy/effectiveSell to nested effectivePrice.{buy,sell}.
Our currency action still read the old fields, so Number(undefined) = NaN threw
'Invalid buy rate from provider: NaN', blocking every Manteca currency (incl. the
now-recovered ARS) at the price-display step.

- currency.ts reads effectivePrice first, falls back to the legacy fields so a
  provider rollback can't re-break pricing.
- MantecaPrice type updated (new nested shape; legacy fields optional).
- Manteca fully restored, so empty disabledMantecaCurrencies (re-enables BRL).

Note: peanut-api-ts MantecaPrice type is stale the same way but is passthrough-only
(nothing reads the field at runtime) — trivial type follow-up, not a runtime bug.
…ectiveprice

hotfix(manteca): fix NaN price (new effectivePrice shape) + re-enable BRL
… main (#2320)

feat(badges): add IRL_NOMADS "Nomad Mode" badge art + campaign maps → main
@vercel

vercel Bot commented Jul 2, 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 12:30am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR spans multiple unrelated feature slices: share-asset capture readiness gating (fonts/image decode, canvas mount) with a new e2e test; per-currency Manteca maintenance kill-switches; Manteca effectivePrice FX parsing; a mandatory EEA uplift advisory verification flow with funnel analytics; card rejection/waitlist door-tally logic replacing a separate joined screen; KYC reject-label copy precedence; guest bank-claim maintenance messaging; a new badge/campaign entry; CTA banner restyling; and an increased Rain application timeout.

Changes

Share-asset capture readiness

Layer / File(s) Summary
Capture readiness contract and helper
src/components/Card/share-asset/shareAsset.types.ts, src/components/Card/share-asset/captureShareAsset.ts, src/components/Card/share-asset/__tests__/captureShareAsset.test.ts
Adds onReady prop and waitForAssetReady helper awaiting fonts/image decode before capture.
PixelatedCardFace pre-pixelated hand
src/components/Card/share-asset/PixelatedCardFace.tsx
Hand renders as a pre-pixelated PNG <img> firing onReady, with restructured shadow styling.
ShareAssetD3 wiring
src/components/Card/share-asset/ShareAssetD3.tsx
Forwards onReady and restructures username pill shadow.
ShareAssetActions readiness gating
src/components/Card/share-asset/ShareAssetActions.tsx
Adds ready prop disabling Share/Save until asset is ready.
Consumer wiring
src/components/Card/BadgeSkipCelebration.tsx, src/components/Card/CardUnlockDrawer.tsx
Tracks assetReady and passes it into ShareAssetActions.
Dev share-builder save flow and e2e test
src/app/(mobile-ui)/dev/share-builder/page.tsx, e2e/flows/share-asset-capture.spec.ts
Adds capture/download handling gated by readiness and a Playwright test verifying downloaded PNG content.

Manteca per-currency maintenance

Layer / File(s) Summary
Maintenance config
src/config/underMaintenance.config.ts
Adds disabledMantecaCurrencies and MantecaCurrency type.
Maintenance view component
src/components/Global/Banner/MantecaTransfersMaintenanceView.tsx
New outage screen for deposits/withdrawals.
Route gating
src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx, src/app/(mobile-ui)/withdraw/manteca/page.tsx
Blocks Manteca flows per disabled currency, returning the maintenance view.
Tests
src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
Asserts live vs. outage rendering.

Manteca FX parsing

Layer / File(s) Summary
Type update
src/services/manteca.ts
effectivePrice added, legacy fields made optional.
Parsing and tests
src/app/actions/currency.ts, src/app/actions/__tests__/currency.test.ts
Prefers nested effectivePrice.buy/sell with legacy fallback.

Mandatory EEA uplift verification

Layer / File(s) Summary
Analytics events
src/constants/analytics.consts.ts
Adds EEA_UPLIFT_STARTED/EEA_UPLIFT_COMPLETED.
Uplift funnel hook
src/hooks/useEeaUpliftFunnel.ts, src/hooks/useEeaUpliftFunnel.test.ts
Session-latched started/completed tracking with reset.
Advisory hard gate
src/hooks/useAdvisoryPreempt.ts, src/hooks/useAdvisoryPreempt.test.ts
Removes skip/close; blocks progression, auto-closes on clear, guards double-submit.
Modal UI
src/components/Kyc/AdvisoryPreemptModal.tsx
Non-dismissible modal with single Complete now action.
KYC flow callback
src/hooks/useMultiPhaseKycFlow.ts
Adds onKycApproved callback.
Bank page wiring
src/app/(mobile-ui)/add-money/[country]/bank/page.tsx, src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Connects funnel tracking to advisory completeNow and KYC approval.

Card rejection/waitlist door tally

Layer / File(s) Summary
Door tally utils
src/components/Card/doorTally.utils.ts, src/components/Card/__tests__/doorTally.utils.test.ts
Adds inflateApplicants, computeDoorTally, inflateWaitlistPosition.
Card service fields
src/services/card.ts
Adds waitlistTotal/admittedTotal.
CardRejectionScreen refactor
src/components/Card/CardRejectionScreen.tsx
Switches to tally-based props and local join state for immediate confirmation UI.
Card page wiring
src/app/(mobile-ui)/card/page.tsx
Always renders CardRejectionScreen; adds reasonMessage for rejected state.
Rejection-builder dev page
src/app/(mobile-ui)/dev/rejection-builder/page.tsx
Replaces applicant sliders with tally totals and alreadyJoined toggle.
ApplicationStatusScreen copy/tests
src/components/Card/ApplicationStatusScreen.tsx, src/components/Card/__tests__/ApplicationStatusScreen.test.tsx
Updates rejected copy and adds reasonMessage tests.
Position display formatting
src/components/Card/PhysicalCardScreen.tsx, src/app/shhhhh/ShhhhhLandingPage.tsx
Uses inflateWaitlistPosition for displayed position.

KYC reject-label copy precedence

Layer / File(s) Summary
Precedence logic and tests
src/components/Kyc/states/KycActionRequired.tsx, src/components/Kyc/states/__tests__/*
Prefers RejectLabelsList over generic actionMessage when labels exist.

Guest claim-to-bank maintenance

Layer / File(s) Summary
Bank option soon-state gating
src/components/Claim/Link/SendLinkActionList.tsx, src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx
Marks bank claim as "Soon!" and non-interactive for guest bank claimants.

Other independent updates

Layer / File(s) Summary
IRL_NOMADS badge/campaign mapping
src/components/Badges/badge.utils.ts, src/components/Invites/campaign-maps.ts
Adds new badge entry and invite/UTM/bare-vanity mappings.
CTA banner restyle
src/components/Home/CardLaunchCTA/CardLaunchCTABanner.tsx
Updates copy and styling to white card with purple CTA.
Rain applyForCard timeout
src/services/rain.ts
Increases request timeout to 60s.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: jjramirezn, kushagrasarathe

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately highlights the back-merge plus the main IRL_NOMADS and Manteca hotfix themes.
Description check ✅ Passed The description clearly matches the merged changes and summarizes the back-merge and key hotfix areas.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5837.33 → 5857.97 (+20.64)
Findings: +6 net (+144 new, -138 resolved)

🆕 New findings (144)

  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 155, MI 52.57, SLOC 528
  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 99, MI 54.88, SLOC 340
  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 95, MI 57.39, SLOC 376
  • critical complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 94, MI 57.82, SLOC 339
  • critical complexity — src/hooks/useMultiPhaseKycFlow.ts — CC 66, MI 57.68, SLOC 325
  • critical complexity — src/components/Claim/Link/SendLinkActionList.tsx — CC 64, MI 57.68, SLOC 175
  • critical complexity — src/app/(mobile-ui)/dev/share-builder/page.tsx — CC 55, MI 67.17, SLOC 221
  • critical complexity — src/services/rain.ts — CC 54, MI 60.61, SLOC 237
  • high hotspot — src/app/(mobile-ui)/withdraw/manteca/page.tsx — 69 commits, +688/-384 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 49 commits, +526/-423 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 45 commits, +451/-202 lines since 6 months ago
  • high complexity — src/app/shhhhh/ShhhhhLandingPage.tsx — CC 45, MI 57.6, SLOC 256
  • high method-complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx:101 — MantecaBankWithdrawFlow CC 44 SLOC 197
  • high complexity — src/services/manteca.ts — CC 44, MI 56.94, SLOC 201
  • high hotspot — src/app/(mobile-ui)/card/page.tsx — 40 commits, +1082/-477 lines since 6 months ago
  • high complexity — src/components/Card/share-asset/ShareAssetD3.tsx — CC 36, MI 49.7, SLOC 229
  • high hotspot — src/components/Badges/badge.utils.ts — 32 commits, +326/-106 lines since 6 months ago
  • high complexity — src/components/Card/share-asset/ShareAssetActions.tsx — CC 23, MI 49.98, SLOC 105
  • high complexity — src/components/Badges/badge.utils.ts — CC 10, MI 46.78, SLOC 150
  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 34.41, SLOC 153

…and 124 more.

✅ Resolved (138)

  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 152, MI 52.4, SLOC 520
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 98, MI 54.79, SLOC 327
  • src/app/(mobile-ui)/card/page.tsx — CC 95, MI 57.44, SLOC 376
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 93, MI 57.82, SLOC 326
  • src/hooks/useMultiPhaseKycFlow.ts — CC 66, MI 57.72, SLOC 324
  • src/components/Claim/Link/SendLinkActionList.tsx — CC 61, MI 57.9, SLOC 173
  • src/services/rain.ts — CC 54, MI 60.66, SLOC 236
  • src/app/(mobile-ui)/dev/share-builder/page.tsx — CC 51, MI 68.07, SLOC 199
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — 67 commits, +675/-380 lines since 6 months ago
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — 46 commits, +496/-413 lines since 6 months ago
  • src/app/shhhhh/ShhhhhLandingPage.tsx — CC 45, MI 57.64, SLOC 255
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx:92 — MantecaBankWithdrawFlow CC 44 SLOC 197
  • src/services/manteca.ts — CC 44, MI 56.95, SLOC 201
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 42 commits, +419/-191 lines since 6 months ago
  • src/app/(mobile-ui)/card/page.tsx — 36 commits, +1049/-455 lines since 6 months ago
  • src/components/Card/share-asset/ShareAssetD3.tsx — CC 36, MI 49.87, SLOC 226
  • src/components/Badges/badge.utils.ts — 30 commits, +317/-102 lines since 6 months ago
  • src/components/Card/share-asset/PixelatedCardFace.tsx — CC 30, MI 57.63, SLOC 154
  • src/components/Badges/badge.utils.ts — CC 10, MI 47.1, SLOC 146
  • src/constants/analytics.consts.ts — CC 1, MI 34.56, SLOC 151

…and 118 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/useEeaUpliftFunnel.ts 0.0 5.8 +5.8
src/components/Card/doorTally.utils.ts 0.0 5.5 +5.5
src/components/Global/Banner/MantecaTransfersMaintenanceView.tsx 0.0 3.8 +3.8
src/app/(mobile-ui)/dev/rejection-builder/page.tsx 4.1 5.8 +1.7
src/components/Card/CardRejectionScreen.tsx 8.6 9.7 +1.1
src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx 5.4 6.3 +0.9
src/app/(mobile-ui)/card/page.tsx 14.8 15.4 +0.6
src/config/underMaintenance.config.ts 7.5 8.0 +0.5
src/components/Card/share-asset/PixelatedCardFace.tsx 12.4 12.9 +0.5
src/components/Kyc/AdvisoryPreemptModal.tsx 5.5 5.0 -0.6
src/components/Card/CardWaitlistJoinedScreen.tsx 5.2 0.0 -5.2

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1642 ran, 0 failed, 0 skipped, 23.9s

📊 Coverage (unit)

metric %
statements 54.6%
branches 37.4%
functions 42.4%
lines 54.5%
⏱ 10 slowest test cases
time test
3.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid UK IBAN with spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jul 2, 2026
@Hugo0 Hugo0 merged commit 11c91fb into dev Jul 2, 2026
19 of 24 checks passed

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

🧹 Nitpick comments (6)
src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx (1)

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

Fix no-explicit-any ESLint errors.

Static analysis flags any at Line 20 (mock next/image props) and Line 126 (claimLinkData cast) as errors, not warnings — these will fail the lint gate.

🔧 Suggested fix
 jest.mock('next/image', () => ({
     __esModule: true,
-    default: (props: any) => {
+    default: (props: Record<string, unknown>) => {
         const { priority, fill, ...rest } = props
         return <img {...rest} />
     },
 }))

and for the claim link data, prefer a partial type of the real shape over as any, or add a scoped disable comment if a proper type isn't readily available.

Also applies to: 120-127

🤖 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/__tests__/SendLinkActionList.test.tsx` around lines
18 - 24, The test file has two `no-explicit-any` violations that will fail
linting: the `next/image` mock in the `jest.mock` block and the `claimLinkData`
cast later in the test. Update the mock component to use a concrete prop type
instead of `any`, and replace the `claimLinkData` `as any` usage with a properly
typed partial of the real claim link data shape (or a narrowly scoped lint
disable only if unavoidable). Use the `SendLinkActionList.test.tsx` mock and
`claimLinkData` setup as the places to fix.

Source: Linters/SAST tools

src/components/Claim/Link/SendLinkActionList.tsx (1)

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

Duplicated guest-maintenance condition.

method.id === 'bank' && isGuestBankClaim is duplicated between the isMethodUnavailable predicate (Line 122) and the soon prop (Line 282). Consider deriving both from a single memoized value to prevent the two checks from drifting apart in future edits.

♻️ Suggested consolidation
+    const isBankUnderGuestMaintenance = (methodId: string) => methodId === 'bank' && isGuestBankClaim
+
     const { filteredMethods: sortedActionMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({
         sortUnavailable: true,
         isMethodUnavailable: (method) =>
             method.soon ||
-            (method.id === 'bank' && (requiresVerification || isGuestBankClaim)) ||
+            (method.id === 'bank' && requiresVerification) ||
+            isBankUnderGuestMaintenance(method.id) ||
             (['mercadopago', 'pix'].includes(method.id) && !isMantecaPayEnabled),

and

-                            soon={method.id === 'bank' && isGuestBankClaim}
+                            soon={isBankUnderGuestMaintenance(method.id)}

Also applies to: 282-282

🤖 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` around lines 111 - 127, The
guest bank maintenance check is duplicated in SendLinkActionList and can drift
between the payment-method filtering and the rendered “Soon!” state. Derive a
single memoized boolean from the existing isGuestBankClaim /
requiresVerification logic in SendLinkActionList, then reuse it both in the
useGeoFilteredPaymentOptions isMethodUnavailable predicate and wherever the bank
option’s soon prop is set. This keeps the bank availability rules centralized
and consistent.
src/app/(mobile-ui)/withdraw/manteca/page.tsx (1)

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

Consider adding withdraw-flow test coverage symmetric to the add-money maintenance tests.

add-money-states.test.tsx was updated with live/outage tests for the deposit gate, but no equivalent test is provided here for the withdraw offramp gate (e.g., asserting MantecaBankWithdrawFlow vs MantecaTransfersMaintenanceView rendering per disabledMantecaCurrencies, and that the Brazil-PIX delegation still takes precedence). Given this gates a live money-movement flow, mirroring the add-money test would reduce regression risk.

🤖 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/manteca/page.tsx around lines 91 - 97, Add
symmetric withdraw-flow test coverage for the Manteca maintenance gate in the
withdraw page. Update or add tests around MantecaBankWithdrawFlow and
MantecaTransfersMaintenanceView to verify that disabledMantecaCurrencies
correctly switches the withdraw offramp into maintenance, and that the
Brazil-PIX delegation still wins when applicable. Mirror the structure and
intent of the existing add-money-states.test.tsx live/outage cases so the
withdraw gate behavior is covered end-to-end.
src/config/underMaintenance.config.ts (1)

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

Centralize the Manteca disabled-currency check

Both src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx and src/app/(mobile-ui)/withdraw/manteca/page.tsx repeat the same as string[] cast before .includes(). Exporting a small helper here, e.g. isMantecaCurrencyDisabled(currency?: string): boolean, would keep the outage gate in one place and remove the duplicated cast.

🤖 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/config/underMaintenance.config.ts` around lines 72 - 78, Centralize the
repeated Manteca outage gate by adding a small helper in the underMaintenance
config module, such as isMantecaCurrencyDisabled(currency?: string): boolean,
that owns the disabledMantecaCurrencies check and the string[] cast. Update both
add-money/[country]/[regional-method]/page.tsx and withdraw/manteca/page.tsx to
call this helper instead of duplicating the .includes logic, and keep the helper
alongside MantecaCurrency/disabledMantecaCurrencies so the check stays in one
place.
src/components/Card/share-asset/shareAsset.types.ts (1)

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

Update the readiness contract to match the <img> implementation.

Line 110 still documents a hand <canvas>, but the implementation now fires from the pre-pixelated hand <img>. Keeping the prop contract accurate matters because downstream gates depend on this signal.

Suggested wording
-     * Fires once the card face's async hand <canvas> has mounted (forwarded
+     * Fires once the card face's pre-pixelated hand <img> has loaded (forwarded
🤖 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/shareAsset.types.ts` around lines 109 - 115,
The onReady contract in shareAsset.types.ts is out of sync with the current
implementation: it still describes a hand <canvas> even though PixelatedCardFace
now fires it from the pre-pixelated hand <img>. Update the JSDoc on onReady to
accurately describe the <img>-based readiness signal and keep the wording
aligned with the gating behavior used by Share/Save capture flows.
src/components/Card/CardRejectionScreen.tsx (1)

214-218: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider aria-live for the join confirmation.

The button → confirmation swap is a meaningful state change but the replacement <div> isn't announced to screen readers. Adding aria-live="polite" would make the "you're on the list" transition accessible.

Suggested tweak
-                    <div className="flex h-13 items-center justify-center gap-2 text-center text-sm font-bold text-n-1">
+                    <div
+                        role="status"
+                        aria-live="polite"
+                        className="flex h-13 items-center justify-center gap-2 text-center text-sm font-bold text-n-1"
+                    >
🤖 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/CardRejectionScreen.tsx` around lines 214 - 218, The join
confirmation in CardRejectionScreen’s `showJoined` branch is a meaningful UI
state change but isn’t announced to assistive tech. Update the confirmation
`<div>` rendered when `showJoined` is true to use an appropriate live region,
such as `aria-live="polite"`, so the “you’re on the list” message is announced
after the button-to-confirmation swap.
🤖 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 `@e2e/flows/share-asset-capture.spec.ts`:
- Around line 63-69: The share-asset capture spec currently waits after Save
becomes enabled, which can hide mid-animation capture bugs. In
share-asset-capture.spec.ts, remove the post-enable wait after the assertion on
saveBtn and click Save immediately once the Save-ready condition is satisfied so
the test exercises the actual contract. Keep the readiness check tied to the
existing saveBtn expectation and the capture flow that follows it.

In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 90-101: The uplift latch is only reset on the Sumsub widget close
path, so a multi-phase modal exit can leave startedRef latched and allow a later
success to emit eea_uplift_completed with stale data. Update the
modal-abandonment flow in the add-money bank page, specifically handleModalClose
and the “Go to Home” path around useMultiPhaseKycFlow, to call resetUpliftFunnel
as well so any pending uplift state is cleared whenever the user closes the
modal.

In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx:
- Line 396: The Save action is being enabled too early because onReady only
tracks the asset load state, not when the cardSlide/sticker animations are
settled. Update the share builder flow around onReady, the disabled condition,
and the capture target’s animate prop so Save stays disabled until both the
asset is ready and animations are stable, or render the capture target with
animate={false} before allowing capture.

In `@src/app/`(mobile-ui)/withdraw/[country]/bank/page.tsx:
- Around line 101-109: The modal dismissal path in useMultiPhaseKycFlow
currently logs abandonment and closes the UI, but it does not trigger the
withdraw flow’s onManualClose cleanup. Update handleModalClose in
useMultiPhaseKycFlow so it invokes the supplied onManualClose callback
before/while closing, preserving the existing behavior and ensuring
resetUpliftFunnel from the bank page is cleared on wrapper close as well.

In `@src/components/Card/share-asset/captureShareAsset.ts`:
- Around line 56-73: Add a timeout guard to waitForAssetReady so asset
preparation cannot hang forever. The current Promise.all over img.decode() in
waitForAssetReady can stall indefinitely on a bad remote image, leaving
captureShareAsset unresolved. Wrap the image decode phase (and ideally the
fonts.ready wait as well) with a bounded timeout and fall back cleanly when the
timeout is hit, so captureShareAsset can continue or fail fast instead of
blocking ShareAssetActions.handleShare, ShareAssetActions.handleSave, and
CardRejectionScreen.handleAppeal.

In `@src/components/Card/share-asset/PixelatedCardFace.tsx`:
- Around line 289-300: Guard the callback in PixelatedHand so onReady only fires
once, since both the img onLoad handler and the ref callback can invoke it
multiple times across rerenders. Update PixelatedCardFace/PixelatedHand to track
whether readiness has already been reported, and in the ref callback only call
onReady for cached images that are actually valid by checking complete plus
naturalWidth > 0 before signaling ready.

In `@src/hooks/useAdvisoryPreempt.ts`:
- Around line 28-35: The advisory modal can be re-opened after the backend
clears the requirement because the rejection path in
useAdvisoryPreempt/onCompleteNow does not verify that the latest advisory still
exists before restoring the non-dismissible gate. Update the async completion
flow to track the current advisory state (or compare against the latest advisory
reference) and only call setVisible(true) when the rejected completion still
corresponds to an active advisory; otherwise leave the modal closed. Keep the
sync effect in useAdvisoryPreempt and the rejection handling around
onCompleteNow aligned so a cleared advisory is never resurrected.

---

Nitpick comments:
In `@src/app/`(mobile-ui)/withdraw/manteca/page.tsx:
- Around line 91-97: Add symmetric withdraw-flow test coverage for the Manteca
maintenance gate in the withdraw page. Update or add tests around
MantecaBankWithdrawFlow and MantecaTransfersMaintenanceView to verify that
disabledMantecaCurrencies correctly switches the withdraw offramp into
maintenance, and that the Brazil-PIX delegation still wins when applicable.
Mirror the structure and intent of the existing add-money-states.test.tsx
live/outage cases so the withdraw gate behavior is covered end-to-end.

In `@src/components/Card/CardRejectionScreen.tsx`:
- Around line 214-218: The join confirmation in CardRejectionScreen’s
`showJoined` branch is a meaningful UI state change but isn’t announced to
assistive tech. Update the confirmation `<div>` rendered when `showJoined` is
true to use an appropriate live region, such as `aria-live="polite"`, so the
“you’re on the list” message is announced after the button-to-confirmation swap.

In `@src/components/Card/share-asset/shareAsset.types.ts`:
- Around line 109-115: The onReady contract in shareAsset.types.ts is out of
sync with the current implementation: it still describes a hand <canvas> even
though PixelatedCardFace now fires it from the pre-pixelated hand <img>. Update
the JSDoc on onReady to accurately describe the <img>-based readiness signal and
keep the wording aligned with the gating behavior used by Share/Save capture
flows.

In `@src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx`:
- Around line 18-24: The test file has two `no-explicit-any` violations that
will fail linting: the `next/image` mock in the `jest.mock` block and the
`claimLinkData` cast later in the test. Update the mock component to use a
concrete prop type instead of `any`, and replace the `claimLinkData` `as any`
usage with a properly typed partial of the real claim link data shape (or a
narrowly scoped lint disable only if unavoidable). Use the
`SendLinkActionList.test.tsx` mock and `claimLinkData` setup as the places to
fix.

In `@src/components/Claim/Link/SendLinkActionList.tsx`:
- Around line 111-127: The guest bank maintenance check is duplicated in
SendLinkActionList and can drift between the payment-method filtering and the
rendered “Soon!” state. Derive a single memoized boolean from the existing
isGuestBankClaim / requiresVerification logic in SendLinkActionList, then reuse
it both in the useGeoFilteredPaymentOptions isMethodUnavailable predicate and
wherever the bank option’s soon prop is set. This keeps the bank availability
rules centralized and consistent.

In `@src/config/underMaintenance.config.ts`:
- Around line 72-78: Centralize the repeated Manteca outage gate by adding a
small helper in the underMaintenance config module, such as
isMantecaCurrencyDisabled(currency?: string): boolean, that owns the
disabledMantecaCurrencies check and the string[] cast. Update both
add-money/[country]/[regional-method]/page.tsx and withdraw/manteca/page.tsx to
call this helper instead of duplicating the .includes logic, and keep the helper
alongside MantecaCurrency/disabledMantecaCurrencies so the check stays in one
place.
🪄 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: 7ff07951-c9d4-4754-b7cf-33d531238a93

📥 Commits

Reviewing files that changed from the base of the PR and between efbb2bd and 6006ac5.

⛔ Files ignored due to path filters (2)
  • public/badges/irl_nomads.svg is excluded by !**/*.svg
  • src/assets/cards/peanut-card-hand-pixel.png is excluded by !**/*.png
📒 Files selected for processing (47)
  • e2e/flows/share-asset-capture.spec.ts
  • src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsx
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.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/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/actions/__tests__/currency.test.ts
  • src/app/actions/currency.ts
  • src/app/shhhhh/ShhhhhLandingPage.tsx
  • src/components/Badges/badge.utils.ts
  • 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/Global/Banner/MantecaTransfersMaintenanceView.tsx
  • src/components/Home/CardLaunchCTA/CardLaunchCTABanner.tsx
  • src/components/Invites/campaign-maps.ts
  • 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/config/underMaintenance.config.ts
  • 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/manteca.ts
  • src/services/rain.ts
💤 Files with no reviewable changes (1)
  • src/components/Card/CardWaitlistJoinedScreen.tsx

Comment on lines +63 to +69
await expect(saveBtn, 'Save must enable once the card face signals ready').toBeEnabled({ timeout: 60_000 })

// Let the card-slide / sticker-drop animations settle to their final
// frame so the card sits at its designed (centred, -8°) position before
// we capture. The capture itself overrides only the root scale; the
// card's own transform is whatever frame it's on.
await page.waitForTimeout(2_500)

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

Make the test exercise the actual Save-ready contract.

After the gate is fixed to include animation stability, remove this post-enable timeout and click Save immediately. Otherwise the spec can pass even when users can still capture a mid-animation frame.

🤖 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 `@e2e/flows/share-asset-capture.spec.ts` around lines 63 - 69, The share-asset
capture spec currently waits after Save becomes enabled, which can hide
mid-animation capture bugs. In share-asset-capture.spec.ts, remove the
post-enable wait after the assertion on saveBtn and click Save immediately once
the Save-ready condition is satisfied so the test exercises the actual contract.
Keep the readiness check tied to the existing saveBtn expectation and the
capture flow that follows it.

Comment on lines 90 to 101
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
fd -t f 'SumsubKycModals.tsx|useSumsubKycFlow.ts'
echo '--- SumsubKycModals close wiring ---'
fd 'SumsubKycModals.tsx' --exec rg -n -B3 -A15 'handleModalClose|onClose|onManualClose' {}
echo '--- useSumsubKycFlow onManualClose usage ---'
fd 'useSumsubKycFlow.ts' --exec rg -n -B5 -A15 'onManualClose' {}

Repository: peanutprotocol/peanut-ui

Length of output: 3977


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- useMultiPhaseKycFlow outline ---'
ast-grep outline src/hooks/useMultiPhaseKycFlow.ts --view expanded

echo '--- useMultiPhaseKycFlow close-related logic ---'
rg -n -B4 -A12 'handleModalClose|onManualClose|KYC_ABANDONED|resetUpliftFunnel|completeFlow|isModalOpen|showWrapper' src/hooks/useMultiPhaseKycFlow.ts src/components/Kyc/SumsubKycModals.tsx src/components/Kyc/KycVerificationInProgressModal.tsx src/app/\(mobile-ui\)/add-money/\[country\]/bank/page.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 23746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file sizes ---'
wc -l src/hooks/useMultiPhaseKycFlow.ts src/components/Kyc/KycVerificationInProgressModal.tsx src/components/Kyc/SumsubKycModals.tsx src/hooks/useSumsubKycFlow.ts

echo '--- useMultiPhaseKycFlow snippet ---'
sed -n '1,260p' src/hooks/useMultiPhaseKycFlow.ts

echo '--- KycVerificationInProgressModal snippet ---'
sed -n '1,260p' src/components/Kyc/KycVerificationInProgressModal.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 18286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- useEeaUpliftFunnel outline ---'
fd -t f 'useEeaUpliftFunnel.ts'
ast-grep outline src/hooks/useEeaUpliftFunnel.ts --view expanded

echo '--- useEeaUpliftFunnel implementation ---'
sed -n '1,240p' src/hooks/useEeaUpliftFunnel.ts

echo '--- bank page complete-now wiring ---'
rg -n -B4 -A14 'trackUpliftStarted|handleSelfHealResubmit|resetUpliftFunnel|onCompleteNow|eea_uplift_completed' src/app/\(mobile-ui\)/add-money/\[country\]/bank/page.tsx src/hooks/useEeaUpliftFunnel.ts

Repository: peanutprotocol/peanut-ui

Length of output: 12231


Clear the uplift latch on modal abandonment too. resetUpliftFunnel only runs for the Sumsub widget close path, so closing the multi-phase modal (handleModalClose / “Go to Home”) can leave startedRef latched and let a later unrelated success emit eea_uplift_completed with stale advisory data.

🤖 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 90 - 101,
The uplift latch is only reset on the Sumsub widget close path, so a multi-phase
modal exit can leave startedRef latched and allow a later success to emit
eea_uplift_completed with stale data. Update the modal-abandonment flow in the
add-money bank page, specifically handleModalClose and the “Go to Home” path
around useMultiPhaseKycFlow, to call resetUpliftFunnel as well so any pending
uplift state is cleared whenever the user closes the modal.

usernameStyle={usernameStyle}
hideUsername={hideUsername}
animate={animate}
onReady={() => setAssetReady(true)}

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 | 🟠 Major | ⚡ Quick win

Don’t enable Save until the animated asset is stable.

onReady only proves the hand image loaded. With animate=true, a cached image can enable Save before cardSlide/sticker animations settle, so a fast click captures the current transient frame. The e2e test’s extra 2.5s wait masks this.

Either include animation-settled state in disabled, or render the capture target with animate={false}.

Also applies to: 418-425

🤖 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)/dev/share-builder/page.tsx at line 396, The Save action
is being enabled too early because onReady only tracks the asset load state, not
when the cardSlide/sticker animations are settled. Update the share builder flow
around onReady, the disabled condition, and the capture target’s animate prop so
Save stays disabled until both the asset is ready and animations are stable, or
render the capture target with animate={false} before allowing capture.

Comment on lines +101 to +109
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(),
// 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

printf '\n== withdraw page slice ==\n'
sed -n '80,130p' 'src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx'

printf '\n== add-money page slice ==\n'
sed -n '75,120p' 'src/app/(mobile-ui)/add-money/[country]/bank/page.tsx'

printf '\n== hook location search ==\n'
rg -n "useMultiPhaseKycFlow|handleModalClose|onManualClose|resetUpliftFunnel" src -g '!**/node_modules/**'

Repository: peanutprotocol/peanut-ui

Length of output: 13369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

set -euo pipefail

printf '\n== locate hook definition ==\n'
rg -n "function useMultiPhaseKycFlow|const useMultiPhaseKycFlow|export .*useMultiPhaseKycFlow" src

printf '\n== hook file candidates ==\n'
fd 'useMultiPhaseKycFlow' src

printf '\n== nearby references ==\n'
rg -n "handleModalClose|onManualClose|resetUpliftFunnel|onKycApproved" src/app/(mobile-ui) -g '!**/node_modules/**'

Repository: peanutprotocol/peanut-ui

Length of output: 409


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== useMultiPhaseKycFlow slice 1 ==\n'
sed -n '98,260p' 'src/hooks/useMultiPhaseKycFlow.ts'

printf '\n== useMultiPhaseKycFlow slice 2 ==\n'
sed -n '260,460p' 'src/hooks/useMultiPhaseKycFlow.ts'

printf '\n== useSumsubKycFlow slice ==\n'
sed -n '300,345p' 'src/hooks/useSumsubKycFlow.ts'

Repository: peanutprotocol/peanut-ui

Length of output: 16100


Call onManualClose from the wrapper close path too. src/hooks/useMultiPhaseKycFlow.ts handles modal dismissal in handleModalClose, but that path only logs abandonment and closes the UI. On this withdraw flow, that leaves resetUpliftFunnel set until some later KYC success can misfire eea_uplift_completed.

🤖 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,
The modal dismissal path in useMultiPhaseKycFlow currently logs abandonment and
closes the UI, but it does not trigger the withdraw flow’s onManualClose
cleanup. Update handleModalClose in useMultiPhaseKycFlow so it invokes the
supplied onManualClose callback before/while closing, preserving the existing
behavior and ensuring resetUpliftFunnel from the bank page is cleared on wrapper
close as well.

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

export async function captureShareAsset(node: HTMLElement): Promise<Blob> {
try {
await waitForAssetReady(node)

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

Add a timeout guard to waitForAssetReady.

document.fonts.ready is safely wrapped in try/catch, but the Promise.all over img.decode() has no upper bound. If any <img> (e.g. a remote badge sticker, not the bundled hand PNG) stalls on the network without ever resolving/rejecting, this await never settles — captureShareAsset hangs forever, and every caller (ShareAssetActions.handleShare/handleSave, CardRejectionScreen.handleAppeal) is left stuck in its loading state indefinitely with no recourse for the user.

🛡️ Proposed fix: bound both awaits with a timeout
+async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
+    return Promise.race([promise, new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms))])
+}
+
 export async function waitForAssetReady(node: HTMLElement): Promise<void> {
     if (typeof document !== 'undefined' && document.fonts?.ready) {
         try {
-            await document.fonts.ready
+            await withTimeout(document.fonts.ready, 3000)
         } 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()
-        )
-    )
+    await withTimeout(
+        Promise.all(
+            Array.from(node.querySelectorAll('img')).map((img) =>
+                typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve()
+            )
+        ),
+        5000
+    )
 }
📝 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
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()
)
)
}
export async function captureShareAsset(node: HTMLElement): Promise<Blob> {
try {
await waitForAssetReady(node)
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
return Promise.race([promise, new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms))])
}
export async function waitForAssetReady(node: HTMLElement): Promise<void> {
if (typeof document !== 'undefined' && document.fonts?.ready) {
try {
await withTimeout(document.fonts.ready, 3000)
} 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()
)
),
5000
)
}
export async function captureShareAsset(node: HTMLElement): Promise<Blob> {
try {
await waitForAssetReady(node)
🤖 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 - 73,
Add a timeout guard to waitForAssetReady so asset preparation cannot hang
forever. The current Promise.all over img.decode() in waitForAssetReady can
stall indefinitely on a bad remote image, leaving captureShareAsset unresolved.
Wrap the image decode phase (and ideally the fonts.ready wait as well) with a
bounded timeout and fall back cleanly when the timeout is hit, so
captureShareAsset can continue or fail fast instead of blocking
ShareAssetActions.handleShare, ShareAssetActions.handleSave, and
CardRejectionScreen.handleAppeal.

Comment on lines +289 to +300
const PixelatedHand: FC<{ onReady?: () => void }> = ({ onReady }) => (
<img
src={ASSET_CARD_HAND_PIXEL}
alt=""
aria-hidden
draggable={false}
onLoad={() => 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?.()

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

printf '\n## File outline\n'
ast-grep outline src/components/Card/share-asset/PixelatedCardFace.tsx --view expanded || true

printf '\n## Relevant lines\n'
nl -ba src/components/Card/share-asset/PixelatedCardFace.tsx | sed -n '250,340p'

printf '\n## Search usages of PixelatedHand and onReady in this area\n'
rg -n "PixelatedHand|onReady" src/components/Card/share-asset src | sed -n '1,200p'

Repository: peanutprotocol/peanut-ui

Length of output: 1512


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Relevant lines from PixelatedCardFace.tsx\n'
sed -n '280,325p' src/components/Card/share-asset/PixelatedCardFace.tsx

printf '\n## Search for onReady call sites in repository\n'
rg -n "\bonReady\b" src | sed -n '1,200p'

printf '\n## Search for setAssetReady or similar consumers\n'
rg -n "setAssetReady|assetReady|capture gate|PixelatedCardFace" src | sed -n '1,240p'

Repository: peanutprotocol/peanut-ui

Length of output: 9020


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## shareAsset.types.ts around onReady docs\n'
sed -n '96,125p' src/components/Card/share-asset/shareAsset.types.ts

printf '\n## ShareAssetActions.tsx around readiness comment\n'
sed -n '60,90p' src/components/Card/share-asset/ShareAssetActions.tsx

printf '\n## ShareAssetD3.tsx around onReady wiring\n'
sed -n '210,235p' src/components/Card/share-asset/ShareAssetD3.tsx

Repository: peanutprotocol/peanut-ui

Length of output: 3584


🌐 Web query:

HTMLImageElement.complete true broken image naturalWidth 0 MDN

💡 Result:

For an HTMLImageElement, the complete property returns true even if the image is in a "broken" state (i.e., it failed to load due to an error, invalid format, or corruption) [1][2]. The property is intended to indicate that the browser has finished its attempt to fetch the image [3]. Because the browser has completed its attempt to process the image—regardless of whether that attempt was successful—it considers the operation "complete" [3][2]. To determine if an image failed to load, you can check if complete is true and naturalWidth is 0 [2]. Since naturalWidth (and naturalHeight) returns 0 when an image is not available or failed to load [3][4], this combination effectively identifies a broken image [2]. Note that this approach can have limitations, as some valid images (like certain SVGs or intentionally empty images) might also report a naturalWidth of 0 [5].

Citations:


Guard onReady inside PixelatedHand

onLoad and the callback ref can both invoke onReady, and the inline ref will re-fire on rerenders. That breaks the current “fires once” contract and can also treat a broken cached image as ready; gate the callback and ignore complete images with naturalWidth === 0.

🤖 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 289 -
300, Guard the callback in PixelatedHand so onReady only fires once, since both
the img onLoad handler and the ref callback can invoke it multiple times across
rerenders. Update PixelatedCardFace/PixelatedHand to track whether readiness has
already been reported, and in the ref callback only call onReady for cached
images that are actually valid by checking complete plus naturalWidth > 0 before
signaling ready.

Comment on lines 28 to +35
const completingRef = useRef(false)

// Keep the modal in sync with the requirement: if the backend clears the
// advisory (requirement resolved) while the modal is open, auto-close it so
// the gate doesn't linger over an already-unblocked transfer.
useEffect(() => {
if (!advisory) setVisible(false)
}, [advisory])

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

Don’t resurrect the gate after the advisory clears.

If advisory becomes undefined while onCompleteNow() is pending, the effect on Line 34 closes the modal, but a later rejection on Line 60 re-opens the non-dismissible modal with no active requirement. Re-show only when the latest advisory is still present.

Proposed fix
     const completingRef = useRef(false)
+    const latestAdvisoryRef = useRef(advisory)
 
     // Keep the modal in sync with the requirement: if the backend clears the
     // advisory (requirement resolved) while the modal is open, auto-close it so
     // the gate doesn't linger over an already-unblocked transfer.
     useEffect(() => {
+        latestAdvisoryRef.current = advisory
         if (!advisory) setVisible(false)
     }, [advisory])
@@
         } 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)
+            if (latestAdvisoryRef.current) {
+                setVisible(true)
+            }
             throw error
         } finally {

Also applies to: 57-60

🤖 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 28 - 35, The advisory modal can
be re-opened after the backend clears the requirement because the rejection path
in useAdvisoryPreempt/onCompleteNow does not verify that the latest advisory
still exists before restoring the non-dismissible gate. Update the async
completion flow to track the current advisory state (or compare against the
latest advisory reference) and only call setVisible(true) when the rejected
completion still corresponds to an active advisory; otherwise leave the modal
closed. Keep the sync effect in useAdvisoryPreempt and the rejection handling
around onCompleteNow aligned so a cleared advisory is never resurrected.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants