Skip to content

decomplexify(frontend): unified intents + debug panel + Rhino SDA claim + Squid eviction#1904

Merged
Hugo0 merged 129 commits intodevfrom
decomplexify
Apr 28, 2026
Merged

decomplexify(frontend): unified intents + debug panel + Rhino SDA claim + Squid eviction#1904
Hugo0 merged 129 commits intodevfrom
decomplexify

Conversation

@Hugo0
Copy link
Copy Markdown
Contributor

@Hugo0 Hugo0 commented Apr 26, 2026

TL;DR

The decomplexify giga-PR — frontend half. 108 commits, 357 files, +22,338 / −8,072 LoC vs dev. Backend half: peanutprotocol/peanut-api-ts#665. Mono docs+coordination: peanutprotocol/mono#12. Three should land together.

Scope-wise: full eviction of @squirrel-labs/peanut-sdk, @reown/appkit, @mui/*, @dicebear/*, @wagmi/core, jsqr, marked. MUI → Lucide icon migration. Squid cross-chain picker → Rhino SDA preview. Route rename /claim-v3/claim (cross-repo with the api-ts PR). Card UI merge (via feat/card-ui) + Native (Capacitor) merge (via origin/dev). Debug menu rehaul. Cross-cutting security/dep CVE cleanup (88 Dependabot warnings cleared).

What's in here

1. Dependency eviction — net −8 packages

Removed from package.json:

  • @squirrel-labs/peanut-sdk — fully evicted (0c9c0f46a decomplexify(stage 1b)). Claim/deposit/link logic inlined as viem fragments in src/utils/peanut-link.utils.ts + src/utils/peanut-claim.utils.ts + src/interfaces/peanut-sdk-types.ts. grep -r '@squirrel-labs' src/ returns 0.
  • @reown/appkit + @reown/appkit-adapter-wagmi — wallet-connect UI removed (6e83f4b85 TASK-19015). E2e regression specs added to lock the absence (32cbf7dac).
  • @mui/material + @mui/icons-material + @emotion/react + @emotion/styled — full MUI eviction in 8158a69c2 (TASK-19275). All icon imports rewritten to Lucide-react.
  • @dicebear/core + @dicebear/collection — avatar generation deleted (no consumers after the eviction sweep).
  • @safe-global/safe-apps-sdk — Safe-app embed flow removed; ~zero traffic.
  • @wagmi/core — dropped (b88399e2f).
  • jsqr, marked — small consolidations.

Added:

  • lucide-react — icon library (~10KB tree-shakeable).
  • circle-flags — country flags as SVG (67ff7348a migration from flagcdn.com CDN).
  • @zerodev/permissions + @zerodev/webauthn-key + @zerodev/ecdsa-validator — kernel SA + WebAuthn signer infra (matches the api-ts admin kernel).
  • playwright — e2e test infra.

Supply chain: matches the api-ts PR's hardening — minimum-release-age=20160 (14 days) in .npmrc, scripts/check-min-release-age.mjs CI gate. 88 GitHub Dependabot warnings (4 critical, 30 high, 43 moderate, 11 low) addressed via lockfile bumps + pnpm.overrides (b09ccb6bd allowlist protobufjs 7.5.5, 49ac7e925 transitive crit/high CVEs, 69d665fca Next.js DoS via Server Components GHSA, b09ccb6bd protobufjs RCE).

2. Icon migration — MUI → Lucide

8158a69c2 decomplexify (TASK-19275). ~200 component-level imports rewritten across the entire UI to use lucide-react. src/components/Global/Icons/Icon.tsx is the canonical entry point — every consumer reads from a single IconName union type so renames at the SVG layer can't drift.

Trickier corners stabilized in follow-up commits:

  • Optical-size mismatch — Lucide stroke-width-1.5 reads thinner than MUI's filled originals at small sizes. Bumped arrow + qr-code icons (f0b751292, 5da74ae1e, c6805e5ab).
  • .btn svg { fill: inherit } Tailwind rule (pre-existing, written for MUI's filled icons) collapsed Lucide's stroke-based open-curve paths into solid blobs. Workaround: inline style={{ fill: ... }} in LucideWrapper. Rule deletion + audit-of-non-<Icon>-SVGs-inside-.btn-callsites tracked as TODO added wagmi-->ethers signer conversion for sdk compatibility #1 in engineering/projects/decomplexify/TODO.md.
  • .btn svg { @apply icon-18 } global rule bypassed by default (dd419ccca); gated on :not(.custom-size) (a7e14b5b0) so Button can size its own icon wrapper (4ff3f8351, e843c5d8d).
  • Some MUI filled icons have no Lucide equivalent (star, trophy, shield, check-circle, gift). Pulled the MUI path inline as custom SVG components.
  • 11 declared-but-unused IconName members tracked as TODO feat: bigger peanutman and goerli/optigoerli balance #5.

3. Cross-chain claim — Squid → Rhino SDA

Squid's token/route picker fully removed (b3f254752 rip out, 90435394c full name purge, dce598718 final squidQuoteId payload + sdkErrorHandler rename):

  • src/app/actions/squid.ts deleted.
  • src/app/api/health/squid/ deleted.
  • useSquidChainsAndTokens, ISquidChain, ISquidToken, SQUID_API_BASE_URL purged across contexts (LinkSendFlowContext, tokenSelector.context, WithdrawFlowContext, SemanticRequestFlowContext), components (Withdraw Confirm/Initial, LinkSendFlowManager, TokenSelector glue), hooks (useTokenPrice, useTokenChainIcons), validators, services.
  • Cross-chain claim flow rewritten (9d4941e16 Rhino SDA frontend wiring): instead of a Squid quote, the user sees a Rhino SDA preview (deposit address, expected amount in/out, slippage). On commit, the relayer calls /claim with the SDA address as recipient; Rhino does the bridge.
  • squidQuoteId field stripped from the charge-create payload — backend already dropped it from the Charge model.

4. Route rename — /claim-v3/claim

9ca70965duseClaimLink.tsx, services/sendLinks.ts, useCrossChainTransfer.ts POST target swap. Same payload shape, same response. Lands together with the api-ts route rename in #665.

5. Debug menu — /dev/debug (rehauled from /dev/cheats)

d29edca58 — comprehensive grouped panel:

  • Presets: "Full setup → activated user" (one-click: KYC bridge+manteca+sumsub + fund $100 + simulate Bridge $25 deposit + auto-complete pending), "Complete every pending intent", "Approve KYC everywhere".
  • Funding: $10 / $25 / $100 USDC top-ups + simulate Bridge deposit.
  • KYC (per-provider): Bridge US/EU, Manteca AR, Sumsub US.
  • Bridge: prompt-driven impersonator (complete-onramp / complete-offramp / fail-transfer for a specific intent id).
  • State: live whoami panel that auto-refreshes after every preset, reset-user with confirm.

Every action fires a pink-banner console.log in DevTools with latency. window.debug.* / window.cheats.* (alias) for ad-hoc commands. Replaces ~7 sequential cheat clicks with one. Localhost-gated via DevLayout; NEXT_PUBLIC_TEST_HARNESS_SECRET required.

6. Card UI merge — Rain V2 card flow

d5dc8076b decomplexify: merge feat/card-ui — Rain card UI on top of native+serverFetch. Card setup, balance display, transactions list, KYC integration. Nominally tested in sandbox per engineering/projects/card-merge-coordination.

UI specifics:

  • "Spend anywhere Visa is accepted" activation CTA when user is funded but cardless and dismissable.
  • Card-info polling via cardApi.getInfo() with 30s staleTime.
  • "starter balance" copy (never "card balance" or "Peanut rewards" per CLAUDE.md messaging rule).

7. Native (Capacitor) merge

Pulled in via fd92ad248 merge of origin/dev (native-app + card-pioneers + bridge fee UI). iOS + Android shells via Capacitor — capacitor.config.ts at root, native plugins for camera (QR scanning), keychain (passkey + JWT storage), and push notifications.

⚠️ Native build NOT verified. Merge resolved cleanly but the binary build hasn't been smoke-tested on a physical iOS/Android device. Owner: Kushagra.

8. Country flags — flagcdn.com CDN → circle-flags

67ff7348a migrated from flagcdn.com to the circle-flags npm package, plus uses country flag in bank-tx avatars (422a0f5b1 fix bank icon black-on-black in cashout card-context). 23e0c7c6e tightened the migration code after review.

Trade-off: shipped flags add ~80KB to the bundle but eliminate a 3rd-party CDN dependency (no DNS hop, no rate-limit risk, no CDN outage).

9. WS history-cache invalidation

72b6d324c — after the api-ts ledger collapse, every history-affecting event flows through the same WS-broadcast pipeline. We can flush the tanstack TRANSACTIONS cache in one place instead of per-flow.

  • src/components/Home/HomeHistory.tsx — in the WS history-event handler, calls queryClient.invalidateQueries({ queryKey: [TRANSACTIONS] }) on every broadcast (alongside the existing fetchBalance for balance-affecting ones). Activity widget refetches immediately instead of waiting up to the 30s staleTime — visible across all intent kinds.
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — same invalidation immediately after the Bridge confirm step succeeds, before flipping to the SUCCESS view, so the user sees the pending OFFRAMP entry without waiting for the next WS push or staleTime tick.

Trade-off: more refetches during burst-y WS traffic. Acceptable — TRANSACTIONS query is small + indexed FE-side; tanstack dedupes in-flight calls so concurrent invalidations collapse.

10. Dead-code purges

3664b38e2 decomplexify: drop dead Account fields + Points-V1 ghost types:

  • Account-shape fields that were never read on the FE (legacy provider hints from the per-provider table era) dropped from src/interfaces/account.ts. Compile fails if any consumer is still importing them — none did.
  • Points-V1 ghost types — TransactionPoints, the V1 transaction-point shape, and adjacent enums removed. The points UI uses the V2 shape exclusively now.

Plus the Refund feature deleted entirely (peanut-ui 3c577555a, prior commit), switchNetwork helper removed from utils/general.utils.ts, 6 unused helpers dropped from useCreateLink.tsx.

11. FE hardening for ledger-collapse playtest

240f8d8ae decomplexify: harden FE for ledger-collapse playtest. 16437b365 decomplexify: associateClaim PATCH sends body={} (B5 from playtest). Surgical fixes surfaced during the 2026-04-27 sandbox playtest while validating the api-ts ledger collapse.

12. Security hardening

  • 4b0aef2c4 verify-content: write baseline through fd (CodeQL js/file-system-race).
  • ee070838e log-injection: validate inputs / strip CRLF before logging.
  • a0d8301e9 stories: encode slug in href (CodeQL js/stored-xss).
  • e53642efe ci: harden workflows (CodeQL: pin tag + minimal permissions).
  • 01729edab fix: trapped 'Having trouble' modal in Sumsub + iframe KYC wrappers (UX-flavored security fix — was leaving users stuck).

13. Testing infra

  • Playwright multi-project config:
    • test:e2e — mobile (default; iPhone 14 viewport, 375x667). Per CLAUDE.md mobile-first directive.
    • test:e2e:desktop — desktop opt-in.
    • test:e2e:all — both.
    • test:e2e:regression — runs against a live UI URL (used by Nutcracker dashboard).
  • 32cbf7dac e2e: add regression specs for icon rendering + AppKit / wagmi removal (locks the absence of evicted deps).
  • 10b0e465c test: ignore .claude/worktrees in jest scope.

14. Misc fixes

  • e5642edab fix(withdraw): prefer real tx hash over userOp hash for confirmOfframp (CR-flagged from previous review pass).
  • 0905edc65 analytics: instrument waitlist top-of-funnel.
  • cf32aa2e8 docs: point local-dev instructions at mono/scripts/dev.

Risks

Cross-repo coordination

Squid surface

  • Cross-chain claim now relies entirely on Rhino SDA preview. If a Rhino sandbox outage hits during the staging soak, cross-chain claim attempts surface the Rhino error directly; no Squid fallback. Documented; Rhino is the strategic choice.

Cache invalidation traffic

  • The WS-event handler now fires invalidateQueries on every history-broadcast event. For active users during a burst, this can mean 5–10 invalidations/sec. Tanstack dedupes in-flight queries so the network shouldn't spike, but if we see issues we can debounce or scope by intent kind.

Native (Capacitor) — UNTESTED BUILD

⚠️ Native build NOT verified. Merge resolved but the binary build hasn't been smoke-tested on a physical iOS/Android device. Owner: Kushagra.

Card flow — partial sandbox coverage

Card setup + balance + spend nominally tested in sandbox per engineering/projects/card-merge-coordination. Production data not exercised. Real Visa interchange + Rain V2 settlement + KYC geo edge cases need staging soak.

Icon migration regressions

200+ icon swaps. Stroke-based Lucide icons can read poorly at small sizes vs MUI's filled originals. TODO #2 tracks the visual QA pass on critical flows (/setup, /home, /send, /claim/*, /withdraw/*, /dev/ds/foundations/icons).

Test plan

  • pnpm typecheck — clean (BLOCKING gate per CLAUDE.md)
  • npm test (unit) — green
  • pnpm prettier --check . — clean
  • pnpm build — verify bundle size delta vs dev
  • npm run test:e2e — mobile Playwright suite
  • npm run test:e2e:desktop — desktop opt-in
  • Manual smoke (sandbox via /dev/debug → Full setup):
    • Send link create + claim end-to-end (Nutcracker e2e-send-link-create-claim covers this)
    • Bridge onramp → activity feed shows pending OFFRAMP entry within 1s of WS broadcast (cache-invalidation works)
    • Bridge offramp → success → activity feed flips to COMPLETED on next WS push
    • Cross-chain claim → Rhino SDA preview renders without Squid errors
    • Manteca QR pay (sandbox keyword qr3)
    • Rain V2 card setup + spend
  • Mobile: iPhone 14 viewport in DevTools — every multi-step flow renders without overflow
  • Capacitor: pnpm cap build && pnpm cap run ios — Kushagra to verify
  • Visual QA pass on icon migration (TODO Fetch Chain details from SDK #2 — tracked but not blocking)
  • Staging soak — 48h, then prod cutover per playbook

Why one giga-PR

Same reasoning as the api-ts PR. Tried small/gradual in past sprints, kept yielding partial states where reviewers couldn't see the full picture and reverts left ambiguous half-states. One bundled cutover means one set of reviewers, one staging soak window, one rollback plan, no overlapping-feature-flag soup.

Trade-off: review burden is higher. Mitigated by:

  • Per-section commit messages (git log --oneline | grep <theme> works)
  • This PR description split by theme
  • Coordinated WORKING.md handoff in mono so the next agent picks up context

jjramirezn and others added 30 commits April 12, 2026 22:15
- Peanut wallet chain/token configurable via NEXT_PUBLIC_PEANUT_WALLET_* env vars
- Primary chain in PUBLIC_CLIENTS_BY_CHAIN now follows PEANUT_WALLET_CHAIN
- Ultra Relay gas=0 scoped to Arbitrum mainnet only (testnets use standard estimation)
- @zerodev/permissions package for session key permission approvals
- Kernel client context imports reworked for chain-agnostic setup
…orts

De-Complexify M1 UI snapshots. Captures screenshots + a11y tree +
console logs + metadata at each step of core flows.

Flows (19 test cases × mobile 375x667 + desktop 1440x900 = 38 total):
  - home: balance, nav CTAs, history scroll
  - qr-pay: no-code error, Mercado Pago mock, PIX mock
  - send-link: landing, amount entry, URL state
  - withdraw: landing, crypto, manteca, AR/bank
  - add-money: landing, AR/bank (Manteca), US/bank (Bridge)
  - setup: setup, profile, points, history pages
  - claim: no pubKey, invalid pubKey

Infrastructure:
  - playwright.config.ts: mobile + desktop projects, serial execution
  - e2e/global-setup.ts: authenticates via /dev/test-session API endpoint
  - e2e/utils/capture.ts: captureStep (screenshot + a11y + meta), console collector
  - .gitignore: e2e/__results__, __report__, __snapshots__, .auth/

Auth bypass: global-setup hits the API's /dev/test-session (built in
peanut-api-ts-decomplexify), gets a JWT, stores as cookie, saves
storageState for all tests. No passkey ceremony needed.

Not yet run against live servers — structure validated via playwright test --list.
Home screen now shows smart account + Rain collateral as a single
spendable balance. Outgoing flows auto-route across both buckets
(collateral-only -> smart-only -> mixed) and produce a single history
entry via the backend's TransactionIntent pipeline.

- useSpendBundle: strategy routing + EIP-712 admin signing for Rain
withdraw (collateral-only direct-transfer; mixed bundles
withdrawAsset + usdc.transfer + subsequentCalls in one kernel UserOp)
- useSendMoney, useWallet.sendTransactions accept `kind` for history
categorization; 5 callers tagged (P2P send, QR pay, link create,
crypto withdraw, fiat offramp)
- One-time session-key grant (useGrantSessionKey) with a single
CallPolicy covering both USDC.transfer(collateralProxy) and
coordinator.withdrawAsset; inline grant on first collateral spend
- useRainCardOverview hook with 30s poll + WS invalidation on
rain_card_balance_changed; WebSocket singleton gated on auth ready
- Stable-value spendable balance pattern avoids flicker during
auto-balancer deposits (smart down + collateral up)
- hasSufficientBalance -> hasSufficientSpendableBalance across 7 flows;
crypto-withdraw skips recordPayment when strategy touched collateral
(no more orphan WITHDRAW charges)
- TRANSACTION_INTENT history entry type with kind-aware drawer mapping
- Dev page /dev/card-session-approve for testing the grant flow
Playwright-based behavioral snapshots for De-Complexify M2 verification.
Mobile viewport (390x844 Chromium + Pixel 7 UA) is the default — Peanut
is a mobile-first PWA.

Flows organized by auth requirement:
  - public.spec.ts: routes that work without auth (landing, support,
    claim, invite, qr) — the stable regression foundation
  - dev-showcase.spec.ts: /dev/components and /dev/ds — canonical design
    system targets; critical for M2 Redux/MUI/flow-context refactors
  - home.spec.ts, qr-pay.spec.ts, send-link.spec.ts, withdraw.spec.ts,
    add-money.spec.ts, setup.spec.ts, claim.spec.ts: authenticated flows
    (best-effort; the app's wallet init gates these in ways the harness
    can't fully fake — we snapshot whatever state we land in)

Infrastructure:
  playwright.config.ts: mobile + desktop projects, serial execution, 120s
    timeout per test, retain-on-failure traces.
  e2e/global-setup.ts: hits API /dev/test-session for a JWT, stores as
    'jwt-token' cookie (matching src/utils/cookie-migration.utils.ts),
    dismisses "Got it" + PWA install modals, saves storageState.
  e2e/utils/capture.ts: captureStep writes PNG + meta.json + aria.txt
    per step. collectConsoleLogs filters known noise, flushes on demand.
  e2e/utils/dismiss-modals.ts: loop-based modal dismissal (mobile has
    stacked PWA install + mobile-first modals).

package.json scripts:
  test:e2e         → mobile (default)
  test:e2e:desktop → desktop only
  test:e2e:all     → both viewports

.gitignore: e2e/__results__, __report__, __snapshots__, .auth/

Verified on desktop: 7 public routes captured at correct URLs.
Mobile run in progress — some auth-dependent flows still need more work
(the app's client-side wallet init is hard to fake). Public flows work.
…UTING with mono

- kernelClient + Create.request.link.view: localStorage bypass so
  mono/engineering/qa Playwright harness can drive authenticated screens
  with a seeded user (no real passkey).
- CONTRIBUTING.md: delegate shared rules to mono root, keep UI-specific
  overrides only.
- e2e/: in-progress flow specs + scaffolding (not yet wired to mono/qa).
…qa harness

UI-side changes to make the app drivable from a headless Playwright runner:
- kernelClient.context.tsx: when localStorage.__harness_skip_passkey=true and
  __harness_ecdsa_pk is set, bootstrap a kernel account via
  signerToEcdsaValidator instead of the passkey path. Behavior is identical
  post-init (same bundler, same paymaster, same kernel v3.1 / EP 0.7) — just
  a different validator so Playwright can actually sign userops.
- zerodev.consts.ts: PEANUT_WALLET_CHAIN + _TOKEN now pick from env
  (NEXT_PUBLIC_PEANUT_WALLET_CHAIN_ID / _TOKEN) so sandbox can target
  arbitrumSepolia + testnet USDC. Prod build path unchanged.
- app/actions/clients.ts: PUBLIC_CLIENTS_BY_CHAIN keyed off PEANUT_WALLET_CHAIN
  rather than hardcoded arbitrum.
- context/ReproduceBootstrap.tsx: new client component. When URL includes
  ?__reproduce=<sessionId>, fetches the manifest from /dev/reproduce/<id>,
  seeds localStorage + jwt-token cookie, strips the query, hard-reloads.
  Makes the Nutcracker gallery "Open live" button work end-to-end.
- app/ClientProviders.tsx: wire ReproduceBootstrap under a <Suspense> boundary
  (useSearchParams requires it in App Router).
- package.json: add @zerodev/ecdsa-validator dependency.
Skip the auth-redirect-to-/setup when a reproduce session is pending —
`?__reproduce=<id>` is present but ReproduceBootstrap hasn't finished
setting cookies yet. Without this guard, the redirect races the
bootstrap and bounces the user away before they land authed.

Gated behind NEXT_PUBLIC_HARNESS_SKIP_PASSKEY_CHECK so prod is
unaffected. Part of the Nutcracker "▶ play" flow (see
engineering/qa/VERIFICATION-PLAN §13a, Slice J).

Was previously uncommitted since the earlier session that introduced
the reproduce flow; consolidated onto decomplexify per
feedback_harness_branch_decomplexify.
…session

Without this, ▶ play against a browser that has cookies / SW caches
from a previous reproduce session would surface the wrong user in
the header despite the cookie being overwritten. Service worker
served stale /users/me responses. IndexedDB (TanStack Query persister)
carried over too.

Full wipe order before seeding the new scenario user:
- expire jwt-token cookie
- localStorage keys except reproduce session flag
- caches matching USER_DATA_CACHE_PATTERNS
- unregister every service worker on the origin
- delete all IndexedDB databases

Then seed new localStorage + new cookie + hard reload. Hard-refresh
removed as a debug step — a fresh Chrome window now does the right
thing out of the box.

Also: use history.replaceState instead of router.replace to strip the
?__reproduce param before reload (router.replace was async, racing the
reload, so the param survived into the post-reload URL).
Path B proof-of-concept. Companion to the QA harness runner (Node/
Playwright) that drives the same scenarios server-side. Both sides
consume the JSON action descriptors defined at
engineering/qa/lib/action-dsl.mjs.

ReproduceBootstrap: after wiping + re-seeding persistent state, stash
the manifest's accumulated stepActions into sessionStorage under
__harness_replay_actions, then hard-reload as before.

HarnessReplay: new component mounted alongside ReproduceBootstrap in
ClientProviders. On mount, reads the stashed descriptors, clears the
key (idempotent against strict-mode double-mounts), waits for providers
to stabilise (1.5s), then runs the descriptors sequentially:

  goto       - pushState + popstate dispatch
  wait       - setTimeout
  click-role - scan button/link/textbox for accessible name match
  click-text - treewalk for visible element whose textContent matches
  fill-amount- native setter + input event for React controlled inputs
  fill       - same for generic selectors
  wait-for-text / wait-for-selector - 100ms poll until visible / timeout

No-op outside sandbox mode (same env guard as ReproduceBootstrap). Keeps
WithdrawFlowContext / other useState-only contexts honest — sensitive
mid-flow state isn't persisted anywhere; it's rebuilt by driving the UI.
Two follow-ups to the first-cut HarnessReplay interpreter after running
it end-to-end through the test-path-b-replay validator:

1. Cross-page goto: `goto` action now uses `location.assign`
   (full navigation, Next.js fetches + mounts target route with effects
   firing naturally) and suspends the loop so the new page's
   HarnessReplay re-mount resumes from the next action via the
   HARNESS_REPLAY_INDEX_KEY cursor in sessionStorage. Previous
   pushState+popstate approach didn't trigger Next.js router, leaving
   subsequent actions to run against a stale route.

2. Strict-mode guard: module-level `replayInFlight` flag prevents
   React dev strict-mode double-mount from firing two parallel
   replay loops. Previously saw interleaved action logs.

3. findByRole: strip Unicode symbols from accessible name before
   regex-matching. Buttons like "↑ Withdraw" (arrow icon + text)
   weren't matching /^\s*withdraw\s*$/i because Playwright's
   accessible-name normalisation strips the icon children; our DOM
   walk kept them in textContent. Also falls back to anchor-less
   match when the ^...$ regex has icon prefix/suffix.

POC state: server-side DSL fully works (8/8 scenario green,
stepActions emitted in reproduce.json, gallery→/dev/reproduce→session
plumbing green). Client-side interpreter has the architecture in
place + above fixes, but some actions still time out on first-load
race conditions (paint delays, provider warm-up) — the production
polish (action retries, longer defaults, accessibility-tree find)
is tracked as next-session work.
…play

Path B client-side replay end-to-end for e2e-withdraw-bank-ach step 06 now
reliably reaches the success-receipt state. Five fixes, each addressing a
distinct failure mode from the POC pass:

1. goto no-op when already on target URL — avoids a needless full-page
   reload when the action DSL's goto matches current location.

2. Replace sessionStorage lease with a per-document in-flight flag.
   SessionStorage persisted across navigation and blocked the next page's
   HarnessReplay from firing; module-level resets per page load.

3. Bypass /crisp-proxy — Crisp chat widget iframe mounts ClientProviders
   which loads HarnessReplay again, running a parallel replay loop against
   empty DOM. Early-return when pathname starts with /crisp-proxy.

4. Accessible-name computation better matches Playwright's getByRole so
   click-role / click-text match the same elements the runner does.

5. Assorted logging + timeout bumps for slow Turbopack cold-compiles.
State machine on /card:
- computeCardState: pioneer → add-card → pending/manual-review/rejected
→ active (ENABLED+noCard correctly routes to add-card, not pending)
- findActiveCard helper centralizes card selection across subpages

Screens:
- AddCardEntryScreen: CardFace preview (sample PAN + Peanut Pioneer
NUTTY YOU + 06/69) + feature list + CTA
- CardTermsScreen: US 5-checkbox / INT 4-checkbox variants, E-Sign +
Issuer Privacy Policy links
- ApplicationStatusScreen: pending / manual-review / rejected
- YourCardScreen: CardFace + reveal + Card management + Red zone, nuqs
?action=lock|unlock|cancel
- CardPinScreen: masked view with eye-slash toggle, set/change via
?mode=set, auto-mask on blur/visibility/30s
- CardLimitsScreen: single per-transaction row (Weekly/Yearly not
supported by Rain)
- LockCardModal: SlideToAction for lock, Button for unlock
- CancelCardModal: 3-phase (confirm → feedback → thanks); defers
queryClient invalidation to handleClose so modal doesn't unmount
mid-flow
- PhysicalCardScreen: Coming soon → Join → "You are #N on the list"
- AddToWalletCarousel: 4-step iOS/Android via useWalletPlatform
- CardPinSetupFlow + PinInput (4-dot custom)

Apply flow end-to-end:
- /card dispatcher: isIssuing > pendingTerms > state machine; handles
incomplete (Sumsub) + terms-required + pending; "Setting up your
card…" loading screen on issue
- SumsubKycWrapper: onActionSubmitted listener + hasSubmittedRef skips
"Stop verification?" after successful submit

Supporting:
- CardFace: pink card, hand SVG rotated -15deg, peanut icon + Visa
brand mark, masked/revealed/preview modes
- Home: 'card' activation step between deposit + outbound, dismissable
via localStorage
- Profile: My Card entry (gated by disableCardPioneers, now false)
- 11 server actions → client-side fetches in services/rain.ts;
cardApi.getInfo/purchase migrated too (kills POST /card noise)
- Lock/Cancel/Limits modals get classWrap to fix mobile drawer issue
Inline the 6 pre-existing server actions in app/actions/rain.ts
(getRainCardOverview, getRainSessionKeyAddress, submitRainWithdraw-
SessionApproval, prepareRainWithdrawal, submitRainWithdrawal,
stampRainWithdrawal) into services/rain.ts using the existing
rainRequest helper + JWT cookie pattern. Drop app/actions/rain.ts.

All card calls now go through a single client-side rainApi object —
no `'use server'` indirection left for Rain. useGrantSessionKey
switches from the {data, error} return shape to try/catch.
CONTRIBUTING.md deleted; UI-specific rules (design system, URL-as-state,
imports, layouts) folded into mono/AGENTS.md under its peanut-ui section.
CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules all point to
../AGENTS.md so there's one source of truth across repos.
- AddToWalletCarousel: dropped the lavender "Step N preview" placeholder,
renders the real Apple/Google step images (exported from Figma) via
next/image
- Added 8 step PNGs under src/assets/cards/wallet-steps/ and exposed
APPLE_WALLET_STEPS / GOOGLE_WALLET_STEPS tuples in assets/cards/index.ts
- YourCardScreen: new "Add to Apple Wallet" / "Add to Google Wallet" row
in Card Management (hidden on desktop), links to existing
/card/add-to-wallet; Physical card row re-positioned so the stack
renders correctly
- next.config.js: added allowedDevOrigins (default peanut.mucu.dev,
extensible via NEXT_ALLOWED_DEV_ORIGINS) so Next.js 16 stops flagging
dev chunks and HMR when developing against a tunneled origin
One-click helpers for hand-operated local testing:
  - Send me $10 / $100 USDC (harness EOA → your SA)
  - Approve KYC (Bridge · US/EU) — creates real Bridge sandbox customer
  - Approve KYC (Manteca · AR) — binds ACTIVE sandbox user
  - Create Sumsub applicant
  - Simulate Bridge deposit ($25)
  - Reset my provider state (keeps passkey + session)

All actions call peanut-api-ts /dev/cheats/* which delegate to the
Nutcracker harness library (engineering/qa/lib/*). Zero duplicated logic.

Only mounts under /dev (DevLayout gates prod). Secret comes from
NEXT_PUBLIC_TEST_HARNESS_SECRET with a safe local default.

Also linked from /dev landing page.
…ight

passkeyPreflight only recognized 'localhost' as a secure context, rejected
'127.0.0.1' and '[::1]' even though all three are WebAuthn-compliant
secure contexts per W3C. Result: Set-up-passkey button disabled when
browsing http://127.0.0.1:3050 (the default servers.sh URL).

Fix: prefer window.isSecureContext (the browser's own determination),
fall back to hostname allowlist (localhost, 127.0.0.1, [::1], ::1) for
environments where it's unreliable.

usePasskeySupport.ts already did this correctly; only passkeyPreflight
was wrong.
Continue from the terms screen now prompts the session-key grant in the
same passkey tap that writes consent. Cancelled taps fail closed: the
user stays on the terms screen with an error and no card gets issued.

- useGrantSessionKey: split runSerialize() out of grant() and expose a
new serializeGrant() that does only the passkey + returns the
serialized string (no backend POST). grant() is unchanged for the
existing lazy-grant consumers (useSpendBundle, /dev/card-session-approve).
- services/rain.ts: applyForCard accepts optional serializedApproval,
forwarded to POST /rain/cards.
- /card/page.tsx: handleAcceptTerms checks for contractAddress +
coordinatorAddress on overview (re-issue branch). If present, collect
the tap first and pass the serialized string into applyForCard.
First-time applicants skip the tap — the collateral proxy doesn't
exist yet — and pick it up on the next re-issue pass.
kernelClient.context used to set maxFeePerGas=0 on any chain matching
PEANUT_WALLET_CHAIN.id, relying on ZeroDev UltraRelay to override.
Sandbox points PEANUT_WALLET_CHAIN at Arb Sepolia (421614), where the
ZeroDev bundler is standard Pimlico — it rejects zero-fee userops with
'maxFeePerGas must be at least N — use pimlico_getUserOperationGasPrice'.

Restrict the shortcut to mainnet Arb One (42161). Arb Sepolia, Ethereum
Sepolia, Base Sepolia fall through to standard viem gas estimation.
…pto paths

- .github/workflows/tests.yml: run `pnpm typecheck` before unit tests
- Install missing @testing-library/dom peer dep (unblocks screen/fireEvent
  imports that were failing TS2305 across 6+ test files).
- e2e/utils/*: switch to `import type` for Playwright types (verbatimModuleSyntax)
- add-money-states.test.tsx: add required `supportedChains` to mocks
- dev/cheats/page.tsx: coerce harnessSecret `| false` to string
- withdraw/crypto/page.tsx, useBalance.ts: cast PEANUT_WALLET_TOKEN as Address
- HarnessReplay.tsx: simplify findByRole signature away from the Action-derived
  conditional type that narrowed to `never` after an earlier refactor

Typecheck now passes. Gates CI so future PRs can't regress.
Mounts <PeanutDebug /> client component that installs helpers on window
when hostname is localhost/127.0.0.1/[::1]:

  peanutDebug.signOut()                  clear cookies+storage+IDB, reload
  peanutDebug.whoami()                   current user KYC/wallet state
  peanutDebug.fund("10")                 harness EOA → your SA ($10 USDC)
  peanutDebug.approveKyc("bridge","US")  bridge/manteca/sumsub
  peanutDebug.simulateBridgeDeposit("25")
  peanutDebug.resetMe()                  wipe your bridge/manteca/ledger
  peanutDebug.ledgerHealth()             ledger row counts + dual-write
  peanutDebug.ledgerHistory()            your ledger intents raw
  peanutDebug.help()                     list all commands

All wrap the peanut-api-ts /dev/cheats/* endpoints (same as /dev/cheats UI
panel), zero duplicated logic. Uses NEXT_PUBLIC_TEST_HARNESS_SECRET with
a safe local default. No-op on non-localhost hostnames.
peanutDebug.faucetHelp() from the browser console:
- prints the harness EOA (0x441D796c62548F74505DE578c458908d936A3B53)
- explains the refill procedure + why we can't use a mock (Bridge/Manteca
  hardcode Circle's canonical contract)
- opens https://faucet.circle.com/ in a new tab
Knip-flagged cleanup in peanut-ui:
- Remove `jsqr`, `marked`, `@serwist/build` — no source references (knip),
  verified by grep. `@serwist/next` and `serwist` itself are still used.
- PerkClaimModal exported both named and default for the same component;
  only the default is imported (HomeCarouselCTA). Drop the named export
  so the two forms can't drift.
Knip flagged all five as having no importers (verified by grep):
- Marketing/index.ts barrel — no consumers
- MarketingNav.tsx, Section.tsx, FAQSection.tsx — never imported
- RelatedPages.tsx — shadowed by mdx/RelatedPages.tsx (the one MDX
  consumers actually bind to)

Typecheck + 25 test suites (720 tests) pass.
- useWalletBalances (41) — TanStack Query helper, zero consumers
- useWalletType (69) — detection helper, zero consumers
- useCrispEmbedUrl (32) — Crisp helper, zero consumers (sibling
  useCrispProxyUrl / useCrispTokenId / useCrispUserData ARE used)
- useAvatar (15) — dicebear identicon helper, zero consumers

All verified by grep of the hook name across src/. Typecheck +
720 tests pass.
Both views are only referenced by the Setup/Views barrel, which itself
is consumed by Setup.consts.tsx — that file only imports InstallPWA,
SetupPasskey, SignupStep, LandingStep, SignTestTransaction. Welcome and
JoinBeta never made it into the step flow. Trim the barrel to match.
All verified as having zero live imports. The two home-page ones were
disabled via commented-out imports:
- CardPurchaseScreen (216) — Card flow rewrite landed without it
- Home/AddMoneyPromptModal (65) — zero consumers
- Home/FloatingReferralButton (35) — import commented out in home page
- Home/ReferralCampaignModal (84) — import commented out in home page
- Global/Contributors/index.tsx (23) — sibling ContributorCard is live,
  this barrel had no consumers
- Profile/components/CountryListSection (88) — zero consumers

Typecheck + 720 tests pass.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/Kyc/SumsubKycWrapper.tsx (1)

109-116: ⚠️ Potential issue | 🟠 Major

Multi-level flows are marked “submitted” too early, which bypasses close confirmation.

hasSubmittedRef.current is set before the multi-level early return. That makes the close button skip confirmation even when Level 2 is still pending.

🔧 Suggested fix
 const handleSubmitted = () => {
     console.log('[sumsub] onApplicantSubmitted fired')
-    hasSubmittedRef.current = true
     // for multi-level workflows (LATAM), the SDK transitions to Level 2
     // internally. don't close the modal on Level 1 submission.
     if (isMultiLevelRef.current) return
+    hasSubmittedRef.current = true
     stableOnComplete()
 }

 const handleActionSubmitted = () => {
     console.log('[sumsub] action submitted fired')
-    hasSubmittedRef.current = true
     if (isMultiLevelRef.current) return
+    hasSubmittedRef.current = true
     stableOnComplete()
 }

Also applies to: 128-133, 251-261

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Kyc/SumsubKycWrapper.tsx` around lines 109 - 116, The
hasSubmittedRef.current is being set before the multi-level check in
handleSubmitted, causing multi-level workflows to be treated as fully submitted
and skipping the close confirmation; update handleSubmitted (and the other
similar handlers at the other occurrences) so you only set
hasSubmittedRef.current = true after confirming the workflow is not multi-level
(i.e., after the if (isMultiLevelRef.current) return check) or only set it
inside the branch that calls stableOnComplete, ensuring isMultiLevelRef,
hasSubmittedRef, and stableOnComplete are used as before but the submitted flag
is only flipped when actually completing the flow.
src/components/Create/useCreateLink.tsx (1)

78-100: ⚠️ Potential issue | 🔴 Critical

Don't guess depositIdx from a preflight read when the receipt/logs are missing.

getNextDepositIndex() runs before the transaction, so the fallback can become stale as soon as any other deposit lands first. If sendTransactions returns no receipt, this can generate a claim link for the wrong deposit; and if the receipt exists but DepositEvent parsing fails, the current code crashes after funds may already be deposited. Only derive depositIdx from the confirmed tx/event (or a tx-hash-based receipt lookup), and fail/poll instead of returning a guessed link.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Create/useCreateLink.tsx` around lines 78 - 100, The code is
incorrectly falling back to the preflight getNextDepositIndex() when
sendTransactions() returns no receipt or when parseEventLogs() fails, which can
produce a stale/wrong depositIdx; change the flow in useCreateLink so depositIdx
is only derived from a confirmed transaction/event: stop using nextIndex as a
fallback, call sendTransactions() first (or if keeping the Promise.all pattern,
after sendTransactions resolve, if it returned a tx hash poll or fetch the
receipt by transactionHash), then parse DepositEvent from the confirmed receipt
via parseEventLogs and set depositIdx from topics[1]; if you cannot obtain a
valid receipt or cannot parse the DepositEvent, surface an error (or start a
polling retry) instead of returning a guessed index, and ensure txHash is used
for receipt lookup and error handling (references: getNextDepositIndex,
sendTransactions, parseEventLogs, DepositEvent, depositIdx).
♻️ Duplicate comments (2)
CLAUDE.md (1)

1-1: ⚠️ Potential issue | 🟠 Major

Verify shared rules target resolves (../AGENTS.md) across environments.

Line 1 points outside this repo. If the parent file is missing in CI/dev containers, agent-rule loading will silently break again.

#!/bin/bash
set -euo pipefail

files=("CLAUDE.md" ".cursorrules" ".windsurfrules" "AGENTS.md")

python - <<'PY'
import os

files = ["CLAUDE.md", ".cursorrules", ".windsurfrules", "AGENTS.md"]
for f in files:
    with open(f, "r", encoding="utf-8") as fh:
        target = fh.readline().strip()
    resolved = os.path.normpath(os.path.join(os.path.dirname(f), target))
    print(f"{f}: target='{target}' -> resolved='{resolved}' exists={os.path.exists(resolved)}")
PY

Expected result: all exists=True.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 1, CLAUDE.md's first-line target points outside the repo
(../AGENTS.md) which can break agent-rule loading; change the target line in
CLAUDE.md to a repo-local path (e.g., AGENTS.md or ./AGENTS.md) and add a small
verification step (using the provided existence-check logic) to the CI or
startup validation so files CLAUDE.md, .cursorrules, .windsurfrules, and
AGENTS.md are checked and fail the build if any resolved path does not exist;
reference the filenames CLAUDE.md, .cursorrules, .windsurfrules and AGENTS.md
and ensure the validation runs in CI/dev containers.
package.json (1)

158-164: ⚠️ Potential issue | 🟠 Major

Remove the stale Peanut SDK Jest escape hatch.

The dependency is gone, but Jest still both special-cases @squirrel-labs in transforms and aliases @squirrel-labs/peanut-sdk to a local mock. That keeps tests green even if a real import survives somewhere and would fail in the actual app/runtime.

#!/bin/bash
sed -n '156,166p' package.json
rg -nP 'from\s+["'\'']@squirrel-labs/peanut-sdk["'\'']|require\(\s*["'\'']@squirrel-labs/peanut-sdk["'\'']\s*\)' .
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 158 - 164, Remove the stale Peanut SDK Jest escape
hatch from package.json by deleting the "^@squirrel-labs/peanut-sdk$" entry in
"moduleNameMapper" and removing the special-case "@squirrel-labs" token from the
transform ignore pattern (the node_modules/(?!(`@squirrel-labs`|...)/) regex) so
Jest no longer aliases or exempts `@squirrel-labs` imports; update the transform
ignore regex and moduleNameMapper to reflect only currently needed packages
(remove the `@squirrel-labs` references) and run the provided grep commands to
verify no real imports to "@squirrel-labs/peanut-sdk" remain.
🟡 Minor comments (28)
src/components/Profile/index.tsx-50-53 (1)

50-53: ⚠️ Potential issue | 🟡 Minor

Fix conditional menu positioning for the card-disabled state.

When disableCardPioneers is true, Your Badges becomes the first item but still uses position="middle", which can break group styling.

Suggested fix
 {!underMaintenanceConfig.disableCardPioneers && (
     <ProfileMenuItem icon="credit-card" label="Your Card" href="/card" position="first" />
 )}
-<ProfileMenuItem icon="achievements" label="Your Badges" href="/badges" position="middle" />
+<ProfileMenuItem
+    icon="achievements"
+    label="Your Badges"
+    href="/badges"
+    position={underMaintenanceConfig.disableCardPioneers ? 'first' : 'middle'}
+/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Profile/index.tsx` around lines 50 - 53, The ProfileMenuItem
for "Your Badges" currently hardcodes position="middle" causing incorrect group
styling when underMaintenanceConfig.disableCardPioneers is true; update the
component so the position prop for the "Your Badges" ProfileMenuItem is
conditional (e.g., set to "first" when
underMaintenanceConfig.disableCardPioneers is true, otherwise "middle"), or
compute a small variable above (e.g., badgesPosition) and pass that to the
ProfileMenuItem to ensure proper positioning when the "Your Card" item is
omitted.
src/components/TransactionDetails/TransactionDetailsReceipt.tsx-410-412 (1)

410-412: ⚠️ Potential issue | 🟡 Minor

Use nullish checks for converted amount presence.

At Line 410, !amount will hide valid numeric 0. Prefer explicit null/undefined (and optional empty-string) checks.

Suggested fix
-        if (!code || !amount) return null
+        if (!code || amount === null || amount === undefined || amount === '') return null
         if (code.toUpperCase() === 'USD') return null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TransactionDetails/TransactionDetailsReceipt.tsx` around lines
410 - 412, The current guard in TransactionDetailsReceipt that uses `if (!code
|| !amount) return null` hides legitimate zero amounts; change the second
condition to an explicit null/undefined/empty-string check (e.g. `amount ==
null` or `amount === '' || amount == null`) so that numeric 0 is allowed, while
still returning null when amount is null/undefined/empty; keep the existing
`code` uppercasing logic (`code.toUpperCase() === 'USD'`) and the final `return
`${code.toUpperCase()} ${formatCurrency(amount)}`` unchanged.
e2e/utils/dismiss-modals.ts-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Fix Prettier formatting.

The pipeline indicates a Prettier formatting issue. Run pnpm prettier --write e2e/utils/dismiss-modals.ts to resolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/utils/dismiss-modals.ts` at line 1, Run Prettier to fix formatting in the
file that imports Page (import type { Page } from '@playwright/test'); execute
the project's Prettier formatter (e.g. pnpm prettier --write) targeting that
file, ensure trailing newline and consistent spacing are applied, and commit the
reformatted file.
src/config/wagmi.config.tsx-49-49 (1)

49-49: ⚠️ Potential issue | 🟡 Minor

Consider the root cause of the type cast requirement.

The wagmiConfig as Config cast is used only for cookieToInitialState (line 49) but not for WagmiProvider (line 52), despite both expecting the same config type. This inconsistency suggests createConfig's generic type inference may not be resolving correctly. While the cast works as a workaround, it masks a potential issue. Either investigate why the return type isn't being properly inferred from createConfig(), or add a comment explaining why the cast is specifically required for cookieToInitialState.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/wagmi.config.tsx` at line 49, The cast "wagmiConfig as Config" is
hiding a typing mismatch between createConfig()'s inferred return and
cookieToInitialState; either fix the source of inference by explicitly typing
the wagmiConfig variable to the expected Config type returned from createConfig
(or supply the proper generic to createConfig) so both wagmiConfig and
WagmiProvider use the same inferred type, or update the cookieToInitialState
function signature to accept the actual type returned by createConfig (e.g., use
ReturnType<typeof createConfig>), and if you must keep the cast, add a short
comment on why the cast is required mentioning wagmiConfig,
cookieToInitialState, WagmiProvider, and createConfig.
e2e/utils/capture.ts-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Run formatter to resolve the CI Prettier warning.

This file is currently tripping the formatting check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/utils/capture.ts` at line 1, This file is failing Prettier; run the
project's formatter (e.g., prettier --write or npm run format) on
e2e/utils/capture.ts (or the whole repo) to apply the required formatting
changes, then stage and commit the formatted file so the CI Prettier check
passes.
e2e/utils/capture.ts-79-102 (1)

79-102: ⚠️ Potential issue | 🟡 Minor

collectConsoleLogs should expose listener cleanup to avoid duplicate subscriptions.

When called multiple times on the same page, listeners accumulate and can duplicate entries.

Patch
 export function collectConsoleLogs(page: Page) {
 	const entries: Array<{ type: string; text: string }> = []
 
-	page.on('console', (msg) => {
+	const listener = (msg: Parameters<Page['on']>[1] extends (arg: infer T) => any ? T : never) => {
 		const text = msg.text()
 		if (CONSOLE_NOISE.some((p) => p.test(text))) return
 		entries.push({ type: msg.type(), text: text.slice(0, 500) })
-	})
+	}
+	page.on('console', listener)
 
 	return {
 		entries,
+		dispose: () => page.off('console', listener),
 		flush: (testInfo: TestInfo, name: string) => {
@@
 			}
 			return entries
 		},
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/utils/capture.ts` around lines 79 - 102, The collectConsoleLogs function
currently attaches a console listener via page.on('console', ...) but never
removes it, causing duplicate subscriptions when called multiple times; modify
collectConsoleLogs to store the listener callback in a const (e.g., const
handler = (msg) => { ... }), attach it with page.on('console', handler), and
expose a cleanup method (e.g., stop or dispose) in the returned object that
calls page.off('console', handler) (or page.removeListener if off isn't
available) to unregister the listener; keep entries and flush behavior the same
but ensure callers can call the new cleanup method to prevent listener
accumulation.
docs/TESTING.md-8-12 (1)

8-12: ⚠️ Potential issue | 🟡 Minor

Drop the hard-coded test counts and timings.

These numbers will go stale quickly and create needless doc churn. Keep the commands stable, and point readers to CI for current counts/durations instead.

📝 Suggested doc tweak
- npm test                              # Jest unit + component (710+ tests, ~25s)
- npx playwright test --project=mobile  # E2E smoke (49 tests, ~8 min)
- npx tsx e2e/scripts/generate-report.ts --save-baseline  # Visual regression baseline
- npx tsx e2e/scripts/generate-report.ts                  # Compare against baseline
+ npm test                              # Jest unit + component tests
+ npx playwright test --project=mobile  # Mobile E2E smoke
+ npx tsx e2e/scripts/generate-report.ts --save-baseline  # Save visual regression baseline
+ npx tsx e2e/scripts/generate-report.ts                  # Compare against baseline
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/TESTING.md` around lines 8 - 12, The doc contains hard-coded test counts
and timings next to the commands (e.g., "npm test", "npx playwright test
--project=mobile", "npx tsx e2e/scripts/generate-report.ts --save-baseline",
"npx tsx e2e/scripts/generate-report.ts") which will go stale; remove the
parenthetical counts/durations and instead keep only the commands and a short
note directing readers to check the CI system (or test dashboard) for current
test counts and runtimes.
src/app/(mobile-ui)/card/add-to-wallet/page.tsx-13-15 (1)

13-15: ⚠️ Potential issue | 🟡 Minor

Prevent duplicate "viewed" analytics events for a single page visit.

This effect re-fires whenever platform changes, which can overcount CARD_ADD_TO_WALLET_VIEWED for one screen view.

Suggested fix
-import { type FC, useEffect } from 'react'
+import { type FC, useEffect, useRef } from 'react'
@@
 const AddToWalletPage: FC = () => {
     const router = useRouter()
     const platform = useWalletPlatform()
+    const hasTrackedViewedRef = useRef(false)
+
     useEffect(() => {
+        if (hasTrackedViewedRef.current) return
         posthog.capture(ANALYTICS_EVENTS.CARD_ADD_TO_WALLET_VIEWED, { platform: platform ?? 'unknown' })
+        hasTrackedViewedRef.current = true
     }, [platform])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/card/add-to-wallet/page.tsx around lines 13 - 15, The
effect currently calls
posthog.capture(ANALYTICS_EVENTS.CARD_ADD_TO_WALLET_VIEWED) whenever platform
changes, which can double-count; change the logic in the useEffect that calls
posthog.capture (or wrap it) so it only runs once per mount—e.g., add a local
ref like hasTrackedRef and in the useEffect (watching platform if you need its
final value) check if hasTrackedRef.current is false, call
posthog.capture(ANALYTICS_EVENTS.CARD_ADD_TO_WALLET_VIEWED, { platform: platform
?? 'unknown' }), set hasTrackedRef.current = true, and prevent further captures;
alternatively, if platform value at mount is sufficient, run the useEffect with
an empty dependency array and capture platform ?? 'unknown' once.
e2e/utils/env.ts-14-23 (1)

14-23: ⚠️ Potential issue | 🟡 Minor

Resolve Prettier drift in getHarnessSecret block.

Pipeline already reports a formatting warning; Line 15 onward appears to use indentation that diverges from formatter output.

🎯 Minimal formatting fix
 export function getHarnessSecret(): string {
-	const secret = process.env.TEST_HARNESS_SECRET
-	if (!secret) {
-		throw new Error(
-			'TEST_HARNESS_SECRET is not set. The e2e harness must match the value on the API side — ' +
-				'set it in your shell (.env.e2e, bin/qa env, or direct export) before running tests.',
-		)
-	}
-	return secret
+    const secret = process.env.TEST_HARNESS_SECRET
+    if (!secret) {
+        throw new Error(
+            'TEST_HARNESS_SECRET is not set. The e2e harness must match the value on the API side — ' +
+                'set it in your shell (.env.e2e, bin/qa env, or direct export) before running tests.',
+        )
+    }
+    return secret
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/utils/env.ts` around lines 14 - 23, The getHarnessSecret function has
Prettier/indentation drift in its body (the const/if/throw/return lines and the
multiline error string); reformat that block to match the project's Prettier
style (fix indentation of the const secret, the if/throw block and the
concatenated error message) or run the project's formatter, ensuring the throw
message lines are indented consistently and the return is aligned with the
function body in getHarnessSecret.
e2e/flows/claim.spec.ts-19-40 (1)

19-40: ⚠️ Potential issue | 🟡 Minor

Prettier formatting needs fixing in this spec.

CI already reports a Prettier warning for this PR; this file uses inconsistent indentation and should be auto-formatted before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/claim.spec.ts` around lines 19 - 40, This spec has inconsistent
Prettier formatting (misaligned indentation in the two tests "claim page without
pubKey — error state" and "claim page with invalid pubKey") — run your project's
Prettier/formatter on this file and reformat the block containing the tests that
call collectConsoleLogs, captureStep and consoleLogs.flush so indentation and
spacing are consistent with the repo style, then commit the formatted file.
e2e/flows/dev-showcase.spec.ts-15-44 (1)

15-44: ⚠️ Potential issue | 🟡 Minor

Run Prettier on this spec.

CI is already reporting a formatting failure, and the tab-indented blocks in this new file look like the likely source.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/dev-showcase.spec.ts` around lines 15 - 44, The spec file fails CI
formatting; run Prettier on this test file and replace tab indentation with your
project's configured spacing so the AST and lint formatting match CI
expectations; specifically reformat the test.describe block and its nested tests
(references: test.describe, test('/dev/components landing', test('/dev — root
dev page', and test('/dev/ds — design system root')) as well as helper calls
collectConsoleLogs, dismissModals, captureStep) so all lines use the project's
Prettier settings and commit the rewritten file.
e2e/flows/home.spec.ts-18-83 (1)

18-83: ⚠️ Potential issue | 🟡 Minor

Run Prettier on this spec.

CI already reports a formatting failure, and the tab-indented blocks in this file are likely contributing to it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/home.spec.ts` around lines 18 - 83, This spec is failing CI
formatting due to tab-indented blocks; run Prettier (or your repo's formatter)
on the file to replace tabs with the repository's configured spaces and
normalize line breaks/indentation for the test.describe block and both test(...)
cases (the "authenticated home renders with core elements" and "home with
history persona — shows activity" tests), then save/commit the reformatted file
so CI passes.
src/components/Claim/__tests__/claim-states.test.tsx-336-344 (1)

336-344: ⚠️ Potential issue | 🟡 Minor

This test never verifies the retry message it claims to cover.

The case only asserts that the loading spinner is still visible. It does not check any retry copy or retry affordance, so the behavior in the test name is currently untested. Either assert the retry UI explicitly or rename the test to match the current expectation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Claim/__tests__/claim-states.test.tsx` around lines 336 - 344,
The test "Loading with retries shows retry message" currently only asserts the
loading spinner and never checks retry UI; update the test in
claim-states.test.tsx so it either (A) asserts the retry copy/affordance after
retries by advancing timers or awaiting the retry state and then checking for
the expected text/button via screen.getByText/getByRole, referencing
mockSendLinksApi.get and renderClaim to trigger the retry loop, or (B) if you
prefer not to test the retry UI here, rename the test to reflect that it only
confirms the loading spinner (e.g., "shows loading spinner when API fails") so
the test name matches the assertion. Ensure you reference the existing mocks
(mockSendLinksApi.get) and helper (renderClaim) when implementing the fix.
src/app/(mobile-ui)/home/page.tsx-56-56 (1)

56-56: ⚠️ Potential issue | 🟡 Minor

Keep the warning modal on the same balance source as the card.

This page now renders spendableBalance, but the warning effect still uses balance / isFetchingBalance. With pending holds, users can see a low spendable balance and still get the “high balance” warning. Please migrate the warning check to the spendable fields as well, or make the modal copy explicit that it is based on total balance.

Also applies to: 188-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/home/page.tsx at line 56, The warning modal currently
uses balance and isFetchingBalance while the UI shows spendableBalance; update
the warning logic to base its threshold/checks and loading state on
spendableBalance and isFetchingSpendableBalance (references: useWallet(),
balance, isFetchingBalance, spendableBalance, isFetchingSpendableBalance) so the
modal reflects the same balance source as the card, and also update any modal
copy (or the check) around the high-balance message for the related lines
(188-192) to explicitly reference "spendable balance" if you prefer to keep the
check on total balance.
e2e/flows/dev-showcase.spec.ts-36-37 (1)

36-37: ⚠️ Potential issue | 🟡 Minor

Check response status when navigating to optional routes.

page.goto() returns a Response object for HTTP error pages like 404, so the current check if (!res) won't skip them. The route will continue running against the app's 404 page instead. Use if (!res || !res.ok()) to properly skip when the route doesn't exist.

Suggested fix
-		const res = await page.goto('/dev/ds', { waitUntil: 'domcontentloaded' }).catch(() => null)
-		if (!res) return // not all repos have this route
+		const res = await page.goto('/dev/ds', { waitUntil: 'domcontentloaded' }).catch(() => null)
+		if (!res || !res.ok()) return // not all repos have this route
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/dev-showcase.spec.ts` around lines 36 - 37, The current navigation
check using page.goto(...) only tests for a null response and misses HTTP
errors; update the guard after the call to page.goto in dev-showcase.spec.ts to
also check the response status by using the Response object's ok() method (i.e.,
change the conditional that tests res to use if (!res || !res.ok()) ) so
optional routes that return 404/500 are skipped; locate the page.goto call and
the following if (!res) return and modify the conditional accordingly.
e2e/flows/send-link.spec.ts-45-47 (1)

45-47: ⚠️ Potential issue | 🟡 Minor

URL-state test does not verify the step state.

The assertion only checks the path, so it won’t fail if query-state parsing regresses.

Suggested change
-		// Check that URL contains step parameter
-		expect(page.url()).toContain('/send')
+		// Check that URL contains step parameter
+		expect(page.url()).toContain('step=inputAmount')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/send-link.spec.ts` around lines 45 - 47, The test currently only
asserts expect(page.url()).toContain('/send') and should also verify the
query-state by parsing the URL returned from page.url() and asserting the
presence and expected value of the "step" query parameter; update the assertion
near expect(page.url()).toContain('/send') to parse the URL (using URL or
URLSearchParams on page.url()) and add an assertion that
searchParams.get('step') exists and equals the expected step string (or at
minimum is truthy) so regressions in query-state parsing will fail the test.
e2e/flows/send-flow.spec.ts-87-91 (1)

87-91: ⚠️ Potential issue | 🟡 Minor

Back-navigation behavior is observed but not validated.

The test captures a screenshot after goBack() but never asserts the step actually changed.

Suggested change
 		await page.goBack()
 		await page.waitForTimeout(1000)
 		await captureStep(page, testInfo, { name: '03-send-after-back' })
+		expect(page.url()).toContain('step=inputAmount')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/send-flow.spec.ts` around lines 87 - 91, After calling
page.goBack() you must assert the UI actually moved to the previous step before
taking the screenshot; use the page.goBack() call followed by an assertion that
the step indicator/active step changed (e.g., check the step label or the active
CSS class of the step element) and only then call captureStep(page, testInfo, {
name: '03-send-after-back' }). Locate the DOM element used for step state in
this test (the same element captureStep visually documents) and use Playwright's
expect on its textContent or classList to verify the expected previous step
value.
e2e/flows/icon-regression.spec.ts-60-67 (1)

60-67: ⚠️ Potential issue | 🟡 Minor

Button-specific regression check can pass vacuously.

If no Lucide icons are rendered inside buttons, this loop runs zero times and the test still passes, so the target regression is not actually guarded.

Suggested change
 		const inButtons = all.filter((i) => i.inAnyButton)
+		expect(inButtons.length, 'expected at least one lucide icon inside a button').toBeGreaterThan(0)
 		for (const icon of inButtons) {
 			expect(icon.inlineFill, `${icon.name} inside a button must have fill:none inline`).toMatch(
 				/^(none|currentcolor)$/
 			)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/icon-regression.spec.ts` around lines 60 - 67, The test currently
lets the in-button checks pass vacuously when no Lucide icons appear inside
buttons; add an explicit assertion that the extracted inButtons array is
non-empty before iterating so the regression is actually enforced. Locate the
inButtons = all.filter((i) => i.inAnyButton) line and add an assertion like
expect(inButtons.length, 'expected at least one Lucide icon inside a button for
this regression test').toBeGreaterThan(0) (or throw/fail with a clear message)
immediately before the for (const icon of inButtons) loop so the test fails when
there are no in-button icons to validate.
e2e/flows/withdraw.spec.ts-52-75 (1)

52-75: ⚠️ Potential issue | 🟡 Minor

Close manual browser contexts in finally.

If any step fails before the last line, await context.close() is skipped and the worker keeps a leaked mobile context around. Wrap these two tests in try/finally so cleanup still happens on early failure.

Suggested fix pattern
 	test('withdraw/manteca — ARS/LATAM withdrawal (verified-ar)', async ({ browser }, testInfo) => {
 		const context = await browser.newContext({ ...devices['Pixel 7'] })
-		const persona = await usePersona(context, 'verified-ar')
-
-		const page = await context.newPage()
-		const consoleLogs = collectConsoleLogs(page)
-		await installApiMocks(page)
-
-		await page.goto('/withdraw/manteca')
-		await captureStep(page, testInfo, { name: '01-withdraw-manteca-initial' })
-
-		await page.waitForTimeout(2000)
-		await captureStep(page, testInfo, { name: '02-withdraw-manteca-loaded' })
-
-		if (persona) {
-			testInfo.annotations.push({
-				type: 'persona',
-				description: `verified-ar (${persona.userId})`,
-			})
-		}
-
-		consoleLogs.flush(testInfo, 'withdraw-manteca')
-		await context.close()
+		try {
+			const persona = await usePersona(context, 'verified-ar')
+			const page = await context.newPage()
+			const consoleLogs = collectConsoleLogs(page)
+			await installApiMocks(page)
+
+			await page.goto('/withdraw/manteca')
+			await captureStep(page, testInfo, { name: '01-withdraw-manteca-initial' })
+
+			await page.waitForTimeout(2000)
+			await captureStep(page, testInfo, { name: '02-withdraw-manteca-loaded' })
+
+			if (persona) {
+				testInfo.annotations.push({
+					type: 'persona',
+					description: `verified-ar (${persona.userId})`,
+				})
+			}
+
+			consoleLogs.flush(testInfo, 'withdraw-manteca')
+		} finally {
+			await context.close()
+		}
 	})

Also applies to: 77-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/withdraw.spec.ts` around lines 52 - 75, The tests "withdraw/manteca
— ARS/LATAM withdrawal (verified-ar)" (test block starting with test(...
'withdraw/manteca — ARS/LATAM withdrawal (verified-ar)')) leak the browser
context if an assertion or step throws before await context.close(); wrap the
body that uses context (creation, usePersona, newPage, collectConsoleLogs,
installApiMocks, navigation, captures, annotations, consoleLogs.flush) in a
try/finally and move await context.close() into the finally so the Playwright
context is always closed even on early failure; apply the same try/finally
pattern to the sibling test around lines 77-100 as noted.
src/components/Card/CardLimitsScreen.tsx-21-21 (1)

21-21: ⚠️ Potential issue | 🟡 Minor

Format card limits with fixed cents precision.

For non-round-dollar amounts, this currently renders values like $12.5. Limits are monetary UI, so they should stay at two fraction digits.

Suggested fix
-const formatDollars = (cents: number) => `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 0 })}`
+const formatDollars = (cents: number) =>
+    `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Card/CardLimitsScreen.tsx` at line 21, The formatDollars
helper currently formats cents to dollars with variable fraction digits, causing
outputs like "$12.5"; update the function formatDollars to always render two
decimal places by dividing cents by 100 and using toLocaleString('en-US', {
minimumFractionDigits: 2, maximumFractionDigits: 2 }) (or equivalent) so values
like 1250 -> "$12.50" and 125 -> "$1.25"; locate the formatDollars arrow
function in CardLimitsScreen and change its options accordingly.
e2e/flows/send-link-e2e.spec.ts-56-72 (1)

56-72: ⚠️ Potential issue | 🟡 Minor

Add a minimal render assertion to the /send?view=link smoke test.

Right now this passes on a blank/error shell as long as no wagmi-specific error is emitted. A tiny positive assertion, like the /claim test already has, would make the regression guard much harder to false-pass.

Suggested fix
 		await page.goto('/send?view=link', { waitUntil: 'domcontentloaded' })
 		await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {})
+
+		const body = await page.locator('body').innerText()
+		expect(body.length, 'send-link page should render some body content').toBeGreaterThan(0)
 
 		const wagmiErrors = errors.filter((e) =>
 			/useSignTypedData must be used|WagmiContext|WagmiProvider/.test(e)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/flows/send-link-e2e.spec.ts` around lines 56 - 72, The test "send-link
page renders without a wagmi provider error" currently only checks for absence
of console errors; add a minimal positive render assertion (like the `/claim`
test does) to ensure the page actually rendered content. Locate the test in
send-link-e2e.spec.ts and after the page.waitForLoadState call, assert that a
specific, stable UI element is present/visible (for example a heading or element
unique to the send link view such as "Send link" text, a button, or a
data-testid like send-link-form) so the test fails on blank/error shells as well
as wagmi errors.
src/components/Claim/Link/Initial.view.tsx-629-636 (1)

629-636: ⚠️ Potential issue | 🟡 Minor

Avoid stale route-cache writes in fetchRoute.

Because fetchRoute is async, [...routes, route] uses the array snapshot from when the request started. If two previews resolve out of order, one cache entry can overwrite the other. Use a functional updater here so concurrent fetches merge safely.

Suggested fix
-                setRoutes([...routes, route])
+                setRoutes((current) => {
+                    const alreadyCached = current.some(
+                        (existing) =>
+                            existing.chainId === route.chainId &&
+                            areEvmAddressesEqual(existing.tokenAddress, route.tokenAddress)
+                    )
+                    return alreadyCached ? current : [...current, route]
+                })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Claim/Link/Initial.view.tsx` around lines 629 - 636, The
current fetchRoute async handler appends a new route using the stale snapshot
[...routes, route], which can lead to lost entries when multiple previews
resolve out of order; fix this by switching the state update to a functional
updater so concurrent fetches merge safely (use setRoutes(prev => [...prev,
route]) in place of setRoutes([...routes, route]) inside the fetchRoute
resolution where route, routes and setRoutes are referenced).
src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx-1093-1123 (1)

1093-1123: ⚠️ Potential issue | 🟡 Minor

Render the PIX case only once, after the PIX-specific mock is in place.

This test mounts QRPayPage before overriding mockMantecaApi.initiateQrPayment, then mounts it again. The first instance can leave DOM behind that satisfies the assertion, so the test can pass for the wrong state.

Suggested fix
     test('PIX success shows PIX icon', async () => {
-        renderQrPay({ qrCode: 'pix://payment?id=123', type: 'PIX', t: '1' })
-
         mockMantecaApi.initiateQrPayment.mockResolvedValue({
             code: 'LOCK_PIX',
             type: 'PIX',
@@
         })
 
-        // Re-render with PIX type
         const { unmount } = renderQrPay({ qrCode: 'pix://payment?id=123', type: 'PIX', t: '1' })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx around lines
1093 - 1123, The test currently calls renderQrPay before setting
mockMantecaApi.initiateQrPayment which can leave a stale DOM; fix it by removing
the first renderQrPay call and only call renderQrPay({ qrCode:
'pix://payment?id=123', type: 'PIX', t: '1' }) after you set
mockMantecaApi.initiateQrPayment.mockResolvedValue(...), then perform the same
waitFor/assertions and finally call unmount(); this ensures the component
(renderQrPay) is mounted only once with the PIX-specific mock in place.
e2e/scripts/generate-report.ts-314-324 (1)

314-324: ⚠️ Potential issue | 🟡 Minor

Keep manual baseline mode sticky when using hold-B peek.

keyup always forces the toggle back off. If I already enabled “Baseline” manually and tap B, the report flips back to “Current” on release.

Suggested fix
 	(function() {
 		var toggle = document.getElementById('toggle-baseline')
 		var lblCurrent = document.getElementById('lbl-current')
 		var lblBaseline = document.getElementById('lbl-baseline')
+		var isPeeking = false
@@
 			document.addEventListener('keydown', function(e) {
 				if (e.key === 'b' && !e.repeat && !toggle.checked) {
+					isPeeking = true
 					toggle.checked = true
 					setView(true)
 				}
 			})
 			document.addEventListener('keyup', function(e) {
-				if (e.key === 'b' && toggle.checked) {
+				if (e.key === 'b' && isPeeking) {
+					isPeeking = false
 					toggle.checked = false
 					setView(false)
 				}
 			})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/scripts/generate-report.ts` around lines 314 - 324, The keyup handler
unconditionally turns off the baseline toggle causing manual baseline mode to be
lost; modify the keydown/keyup pair to track whether the hold-'B' action
initiated the checked state (e.g., introduce a local flag like
peekActivatedByHold or store previousChecked) so keyup only clears
toggle.checked and calls setView(false) if the flag indicates the toggle was
changed by the keydown handler; update the keydown handler to set that flag when
it sets toggle.checked = true and reset/clear the flag in the keyup branch after
reverting the view.
e2e/scripts/bootstrap-auth.ts-63-70 (1)

63-70: ⚠️ Potential issue | 🟡 Minor

Don't reuse partial bootstrap artifacts.

This fast path only checks bootstrap-storage.json, then immediately reads bootstrap-meta.json. If a previous run left storage behind without metadata, the script crashes instead of recreating the account. Guard both files together before reusing them, like global-setup.ts already does.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/scripts/bootstrap-auth.ts` around lines 63 - 70, The bootstrap()
fast-path currently only checks BOOTSTRAP_STATE_PATH before reading
BOOTSTRAP_META_PATH, which crashes if meta is missing; update the guard in
bootstrap() to require both BOOTSTRAP_STATE_PATH and BOOTSTRAP_META_PATH exist
(and still honor FORCE) before reusing artifacts, mirroring the pattern in
global-setup.ts, and if either file is missing proceed with normal creation flow
instead of reading bootstrap-meta.json.
src/app/(mobile-ui)/dev/debug/page.tsx-154-162 (1)

154-162: ⚠️ Potential issue | 🟡 Minor

kycAll never writes a result for its own row.

This preset stores outcomes under kycAllBridge / kycAllManteca / kycAllSumsub, but the renderer only reads results['kycAll'] for this card. If one provider step fails, the card still looks untouched unless you inspect console output. Aggregate the preset result under kycAll or surface the child-step results here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/dev/debug/page.tsx around lines 154 - 162, The preset
with key 'kycAll' runs three child calls ('kycAllBridge', 'kycAllManteca',
'kycAllSumsub') in its run handler but never writes a result for 'kycAll', so
the UI (which reads results['kycAll']) appears unchanged; fix by capturing each
call's response (await the three call(...) results inside the run function),
aggregate their statuses/errors into a single summary object, and write that
summary into the results store under the 'kycAll' key (so the renderer sees the
outcome); keep the existing child keys if desired but ensure the run function
for key 'kycAll' sets results['kycAll'] (or otherwise returns/dispatches an
aggregated result) after the three calls and before refreshWhoami().
e2e/global-setup.ts-71-74 (1)

71-74: ⚠️ Potential issue | 🟡 Minor

Align both createAllPersonas call sites to pass consistent persona identifier types.

Bootstrap mode (line 72) passes meta.username, but fallback mode (line 184) passes user.email as the third argument. While the codebase pattern elsewhere treats email as an acceptable fallback for username, inconsistency across initialization paths creates personas with different username representations. Both call sites should pass the same type of value (preferably the username when available, or explicitly fall back to email consistently).

Also applies to: 183-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/global-setup.ts` around lines 71 - 74, The two call sites of
createAllPersonas pass different identifier types (one passes meta.username, the
other passes user.email); update both call sites to pass a consistent persona
identifier by using the same expression (e.g., prefer username with an explicit
fallback to email), for example change both third arguments to something like
(meta.username || meta.email) or (user.username || user.email) so both bootstrap
and fallback paths create personas with the same identifier type; ensure you
update the calls where createAllPersonas is invoked (the bootstrap call using
meta and the fallback call using user) to use the unified expression.
e2e/utils/personas.ts-160-194 (1)

160-194: ⚠️ Potential issue | 🟡 Minor

Don't cast a partial persona set to a full PersonaMap.

This function explicitly skips failed persona creations, but its return type says every PersonaId is present. That removes the missing-persona case from the type system and makes direct indexing look safe when it isn't. Return a partial type that guarantees only default.

Possible fix
 type PersonaMap = Record<PersonaId, Persona>
+type AvailablePersonaMap = Partial<PersonaMap> & Pick<PersonaMap, 'default'>
@@
-export async function createAllPersonas(defaultToken: string, defaultUserId: string, defaultUsername: string): Promise<PersonaMap> {
-	const personas: Partial<PersonaMap> = {
+export async function createAllPersonas(defaultToken: string, defaultUserId: string, defaultUsername: string): Promise<AvailablePersonaMap> {
+	const personas: AvailablePersonaMap = {
@@
-	return personas as PersonaMap
+	return personas
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/utils/personas.ts` around lines 160 - 194, The function createAllPersonas
incorrectly asserts a full PersonaMap even though some persona creations may
fail; change its signature and return value to a partial type (e.g.,
Promise<Partial<PersonaMap>> or a custom type guaranteeing only default exists
like Promise<{ default: Persona } & Partial<PersonaMap>>), remove the unsafe
cast "as PersonaMap" at the end, and keep the internal personas variable typed
as Partial<PersonaMap>; update any call sites that assume every PersonaId exists
to handle missing entries or narrow to the guaranteed default entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2b23611-b6cc-4721-b865-75875b09beb6

📥 Commits

Reviewing files that changed from the base of the PR and between 8f9c308 and 6c1ac94.

⛔ Files ignored due to path filters (18)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/peanut_guy.gif is excluded by !**/*.gif
  • public/peanut_guy_black.gif is excluded by !**/*.gif
  • src/assets/cards/peanut-card-hand.svg is excluded by !**/*.svg
  • src/assets/cards/visa-brand-mark.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-1.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-2.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-3.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/apple-step-4.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-1.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-2.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-3.png is excluded by !**/*.png
  • src/assets/cards/wallet-steps/google-step-4.png is excluded by !**/*.png
  • src/assets/illustrations/peanut_guy.gif is excluded by !**/*.gif
  • src/assets/illustrations/peanutguy.png is excluded by !**/*.png
  • src/assets/iphone-ss/iphone-your-money-1.png is excluded by !**/*.png
  • src/assets/iphone-ss/iphone-your-money-2.png is excluded by !**/*.png
  • src/assets/iphone-ss/iphone-your-money-3.png is excluded by !**/*.png
📒 Files selected for processing (282)
  • .cursorrules
  • .env.example
  • .github/workflows/indexnow.yml
  • .github/workflows/preview.yaml
  • .github/workflows/supply-chain-check.yml
  • .github/workflows/tests.yml
  • .gitignore
  • .npmrc
  • .windsurfrules
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • docs/TESTING.md
  • e2e/flows/add-money.spec.ts
  • e2e/flows/claim-flow.spec.ts
  • e2e/flows/claim.spec.ts
  • e2e/flows/dev-showcase.spec.ts
  • e2e/flows/home.spec.ts
  • e2e/flows/icon-regression.spec.ts
  • e2e/flows/kyc-gate.spec.ts
  • e2e/flows/profile-history.spec.ts
  • e2e/flows/public.spec.ts
  • e2e/flows/qr-pay.spec.ts
  • e2e/flows/request-flow.spec.ts
  • e2e/flows/send-flow.spec.ts
  • e2e/flows/send-link-e2e.spec.ts
  • e2e/flows/send-link.spec.ts
  • e2e/flows/setup.spec.ts
  • e2e/flows/withdraw-flow.spec.ts
  • e2e/flows/withdraw.spec.ts
  • e2e/global-setup.ts
  • e2e/scripts/bootstrap-auth.ts
  • e2e/scripts/generate-report.ts
  • e2e/utils/capture.ts
  • e2e/utils/dismiss-modals.ts
  • e2e/utils/env.ts
  • e2e/utils/mock-api.ts
  • e2e/utils/personas.ts
  • e2e/utils/seed.ts
  • jest.setup.ts
  • next.config.js
  • package.json
  • playwright.config.ts
  • playwright.regression.config.ts
  • scripts/check-min-release-age.mjs
  • scripts/copy-flags.mjs
  • scripts/verify-content.ts
  • src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
  • src/app/(mobile-ui)/card/add-to-wallet/page.tsx
  • src/app/(mobile-ui)/card/limit/page.tsx
  • src/app/(mobile-ui)/card/page.tsx
  • src/app/(mobile-ui)/card/physical/page.tsx
  • src/app/(mobile-ui)/card/pin/page.tsx
  • src/app/(mobile-ui)/dev/card-session-approve/page.tsx
  • src/app/(mobile-ui)/dev/components/page.tsx
  • src/app/(mobile-ui)/dev/debug/page.tsx
  • src/app/(mobile-ui)/dev/page.tsx
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/refund/page.tsx
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/app/(mobile-ui)/withdraw/page.tsx
  • src/app/ClientProviders.tsx
  • src/app/[...recipient]/payment-layout-wrapper.tsx
  • src/app/[locale]/(marketing)/stories/page.tsx
  • src/app/actions/card.ts
  • src/app/actions/claimLinks.ts
  • src/app/actions/clients.ts
  • src/app/actions/onramp-quote.ts
  • src/app/actions/squid.ts
  • src/app/actions/supported-chains.ts
  • src/app/actions/users.ts
  • src/app/api/exchange-rate/route.ts
  • src/app/api/health/route.ts
  • src/app/api/health/squid/route.ts
  • src/app/api/peanut/user/get-user-from-cookie/__tests__/route.test.ts
  • src/app/api/peanut/user/get-user-from-cookie/route.ts
  • src/app/lp/card/CardLandingPage.tsx
  • src/app/quests/constants.ts
  • src/app/quests/utils/styling.ts
  • src/assets/animations
  • src/assets/cards/index.ts
  • src/components/0_Bruddle/CloudsBackground.tsx
  • src/components/AddMoney/components/AddMoneyBankDetails.tsx
  • src/components/AddMoney/components/DepositMethodList.tsx
  • src/components/AddMoney/components/MantecaDepositShareDetails.tsx
  • src/components/AddMoney/consts/index.ts
  • src/components/Badges/badge.utils.ts
  • src/components/Blog/index.tsx
  • src/components/Card/AddCardEntryScreen.tsx
  • src/components/Card/AddToWalletCarousel.tsx
  • src/components/Card/ApplicationStatusScreen.tsx
  • src/components/Card/CancelCardModal.tsx
  • src/components/Card/CardFace.tsx
  • src/components/Card/CardInfoScreen.tsx
  • src/components/Card/CardLimitEditModal.tsx
  • src/components/Card/CardLimitsScreen.tsx
  • src/components/Card/CardPinScreen.tsx
  • src/components/Card/CardPinSetupFlow.tsx
  • src/components/Card/CardPioneerFlow.tsx
  • src/components/Card/CardSuccessScreen.tsx
  • src/components/Card/CardTermsScreen.tsx
  • src/components/Card/LockCardModal.tsx
  • src/components/Card/PhysicalCardScreen.tsx
  • src/components/Card/PinInput.tsx
  • src/components/Card/SlideToAction.tsx
  • src/components/Card/YourCardScreen.tsx
  • src/components/Card/__tests__/cardExpiry.utils.test.ts
  • src/components/Card/__tests__/cardState.utils.test.ts
  • src/components/Card/__tests__/pin.utils.test.ts
  • src/components/Card/cardExpiry.utils.ts
  • src/components/Card/cardState.utils.ts
  • src/components/Card/pin.utils.ts
  • src/components/Claim/Claim.consts.ts
  • src/components/Claim/Claim.interfaces.ts
  • src/components/Claim/Claim.tsx
  • src/components/Claim/Claim.utils.ts
  • src/components/Claim/Generic/NotFound.view.tsx
  • src/components/Claim/Generic/WrongPassword.view.tsx
  • src/components/Claim/Generic/index.ts
  • src/components/Claim/Link/FlowManager.tsx
  • src/components/Claim/Link/Initial.view.tsx
  • src/components/Claim/Link/Onchain/Confirm.view.tsx
  • src/components/Claim/Link/views/BankFlowManager.view.tsx
  • src/components/Claim/__tests__/claim-states.test.tsx
  • src/components/Claim/useClaimLink.tsx
  • src/components/Common/CountryList.tsx
  • src/components/Common/SavedAccountsView.tsx
  • src/components/Create/Create.utils.ts
  • src/components/Create/useCreateLink.tsx
  • src/components/CrispChat.tsx
  • src/components/Global/Banner/index.tsx
  • src/components/Global/Contributors/index.tsx
  • src/components/Global/DirectSendQR/index.tsx
  • src/components/Global/Drawer/index.tsx
  • src/components/Global/ExchangeRateWidget/index.tsx
  • src/components/Global/Footer/consts.ts
  • src/components/Global/GeneralRecipientInput/index.tsx
  • src/components/Global/Icons/Icon.tsx
  • src/components/Global/Icons/bulb.tsx
  • src/components/Global/Icons/docs.tsx
  • src/components/Global/Icons/double-check.tsx
  • src/components/Global/Icons/invite-heart.tsx
  • src/components/Global/Icons/peanut-support.tsx
  • src/components/Global/Icons/txn-off.tsx
  • src/components/Global/Icons/upload-cloud.tsx
  • src/components/Global/Icons/wallet-cancel.tsx
  • src/components/Global/IframeWrapper/index.tsx
  • src/components/Global/Layout/index.tsx
  • src/components/Global/PeanutActionDetailsCard/index.tsx
  • src/components/Global/PeanutFactsLoading/index.tsx
  • src/components/Global/QRCodeWrapper/index.tsx
  • src/components/Global/ScreenOrientationLocker.tsx
  • src/components/Global/Testimonials/index.tsx
  • src/components/Global/TokenSelector/Components/TokenListItem.tsx
  • src/components/Global/TokenSelector/TokenSelector.consts.ts
  • src/components/Global/TokenSelector/TokenSelector.tsx
  • src/components/Global/USBankAccountInput/index.tsx
  • src/components/Home/ActivationCTAs.tsx
  • src/components/Home/AddMoneyPromptModal/index.tsx
  • src/components/Home/FloatingReferralButton/index.tsx
  • src/components/Home/HomeHistory.tsx
  • src/components/Home/HomeLink.tsx
  • src/components/Home/PerkClaimModal.tsx
  • src/components/Home/ReferralCampaignModal/index.tsx
  • src/components/Invites/JoinWaitlistPage.tsx
  • src/components/Kyc/CountryFlagAndName.tsx
  • src/components/Kyc/KycFlow.tsx
  • src/components/Kyc/SumsubKycWrapper.tsx
  • src/components/LandingPage/CurrencySelect.tsx
  • src/components/Marketing/FAQSection.tsx
  • src/components/Marketing/MarketingNav.tsx
  • src/components/Marketing/RelatedPages.tsx
  • src/components/Marketing/Section.tsx
  • src/components/Marketing/index.ts
  • src/components/Notifications/NotificationNavigation.tsx
  • src/components/Offramp/Offramp.consts.ts
  • src/components/Points/PerkClaimCard.tsx
  • src/components/Profile/AvatarWithBadge.tsx
  • src/components/Profile/components/CountryListSection.tsx
  • src/components/Profile/index.tsx
  • src/components/Refund/index.tsx
  • src/components/Request/__tests__/request-states.test.tsx
  • src/components/Request/link/views/Create.request.link.view.tsx
  • src/components/Send/__tests__/send-states.test.tsx
  • src/components/Send/link/LinkSendFlowManager.tsx
  • src/components/Send/link/views/Initial.link.send.view.tsx
  • src/components/Setup/Views/InstallPWA.tsx
  • src/components/Setup/Views/JoinBeta.tsx
  • src/components/Setup/Views/JoinWaitlist.tsx
  • src/components/Setup/Views/Welcome.tsx
  • src/components/Setup/Views/index.ts
  • src/components/Setup/context/SetupFlowContext.tsx
  • src/components/TransactionDetails/TransactionAvatarBadge.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/transactionTransformer.ts
  • src/components/User/UserCard.tsx
  • src/components/Withdraw/views/Confirm.withdraw.view.tsx
  • src/components/Withdraw/views/Initial.withdraw.view.tsx
  • src/components/index.ts
  • src/components/utils/utils.ts
  • src/config/index.ts
  • src/config/peanut.config.tsx
  • src/config/theme.config.tsx
  • src/config/underMaintenance.config.ts
  • src/config/wagmi.config.tsx
  • src/constants/actionlist.consts.ts
  • src/constants/analytics.consts.ts
  • src/constants/cache.consts.ts
  • src/constants/chain-details.json
  • src/constants/countryCurrencyMapping.ts
  • src/constants/general.consts.ts
  • src/constants/harness.consts.ts
  • src/constants/points.consts.ts
  • src/constants/query.consts.ts
  • src/constants/rain.consts.ts
  • src/constants/rhino.consts.ts
  • src/constants/routes.ts
  • src/constants/token-details.json
  • src/constants/tokens.consts.ts
  • src/constants/tooltips.ts
  • src/constants/zerodev.consts.ts
  • src/content
  • src/context/HarnessBootstrap.tsx
  • src/context/HarnessReplay.tsx
  • src/context/LinkSendFlowContext.tsx
  • src/context/PeanutDebug.tsx
  • src/context/ReproduceBootstrap.tsx
  • src/context/RequestFulfillmentFlowContext.tsx
  • src/context/WithdrawFlowContext.tsx
  • src/context/authContext.tsx
  • src/context/kernelClient.context.tsx
  • src/context/tokenSelector.context.tsx
  • src/data/seo/corridors.ts
  • src/data/seo/index.ts
  • src/data/team.ts
  • src/features/limits/consts.ts
  • src/features/limits/hooks/useLimitsValidation.ts
  • src/features/limits/utils.ts
  • src/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsx
  • src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx
  • src/features/payments/flows/contribute-pot/useContributePotFlow.ts
  • src/features/payments/flows/direct-send/useDirectSendFlow.ts
  • src/features/payments/flows/semantic-request/SemanticRequestFlowContext.tsx
  • src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts
  • src/features/payments/flows/semantic-request/views/SemanticRequestConfirmView.tsx
  • src/features/payments/flows/semantic-request/views/SemanticRequestInputView.tsx
  • src/features/payments/shared/components/PaymentMethodActionList.tsx
  • src/features/payments/shared/components/PaymentSuccessView.tsx
  • src/features/payments/shared/hooks/useChargeManager.ts
  • src/features/payments/shared/hooks/useCrossChainTransfer.ts
  • src/features/payments/shared/hooks/usePaymentRecorder.ts
  • src/features/payments/shared/hooks/useRouteCalculation.ts
  • src/hooks/__tests__/useCardReveal.test.ts
  • src/hooks/query/user.ts
  • src/hooks/useAccountSetup.ts
  • src/hooks/useAccountSetupRedirect.ts
  • src/hooks/useActivationStatus.ts
  • src/hooks/useAvatar.tsx
  • src/hooks/useCardReveal.ts
  • src/hooks/useCrispEmbedUrl.ts
  • src/hooks/useIdentityVerification.tsx
  • src/hooks/useOnrampQuote.ts
  • src/hooks/useRainCardOverview.ts
  • src/hooks/useSupportedChainsAndTokens.ts
  • src/hooks/useTokenChainIcons.ts
  • src/hooks/useTokenPrice.ts
  • src/hooks/useWalletBalances.ts
  • src/hooks/useWalletPlatform.ts
  • src/hooks/useWalletType.tsx
  • src/hooks/useWebSocket.ts
  • src/hooks/useZeroDev.ts
  • src/hooks/wallet/__tests__/useSendMoney.test.tsx
  • src/hooks/wallet/__tests__/useSpendBundle.test.ts
  • src/hooks/wallet/useBalance.ts
💤 Files with no reviewable changes (45)
  • src/components/Claim/Generic/NotFound.view.tsx
  • src/components/Claim/Generic/WrongPassword.view.tsx
  • src/config/index.ts
  • src/app/(mobile-ui)/refund/page.tsx
  • src/components/Setup/Views/index.ts
  • src/components/Marketing/Section.tsx
  • src/components/Notifications/NotificationNavigation.tsx
  • src/components/Global/Footer/consts.ts
  • src/components/Claim/Generic/index.ts
  • src/components/Marketing/index.ts
  • src/components/Home/AddMoneyPromptModal/index.tsx
  • src/components/Global/Banner/index.tsx
  • src/config/theme.config.tsx
  • src/components/Claim/Claim.interfaces.ts
  • src/components/Kyc/KycFlow.tsx
  • src/components/Badges/badge.utils.ts
  • src/components/Blog/index.tsx
  • src/components/Marketing/MarketingNav.tsx
  • src/components/Global/PeanutFactsLoading/index.tsx
  • src/app/quests/constants.ts
  • src/components/Global/Drawer/index.tsx
  • src/components/Setup/Views/JoinBeta.tsx
  • src/app/quests/utils/styling.ts
  • src/components/Claim/Claim.utils.ts
  • src/components/Home/HomeLink.tsx
  • src/components/Setup/Views/Welcome.tsx
  • src/components/Home/FloatingReferralButton/index.tsx
  • src/components/Global/Icons/upload-cloud.tsx
  • src/components/Global/Testimonials/index.tsx
  • src/config/peanut.config.tsx
  • src/components/CrispChat.tsx
  • src/components/Profile/components/CountryListSection.tsx
  • src/app/actions/squid.ts
  • src/components/Global/Contributors/index.tsx
  • src/components/Marketing/FAQSection.tsx
  • src/components/Home/ReferralCampaignModal/index.tsx
  • src/components/Marketing/RelatedPages.tsx
  • src/components/index.ts
  • src/app/api/health/squid/route.ts
  • src/components/Refund/index.tsx
  • src/components/Points/PerkClaimCard.tsx
  • src/components/Global/USBankAccountInput/index.tsx
  • src/components/Create/Create.utils.ts
  • src/components/Setup/context/SetupFlowContext.tsx
  • src/components/utils/utils.ts

Comment thread src/app/(mobile-ui)/qr-pay/page.tsx
Earlier fix only cleared the cookie on 401 (expired/invalid signature),
but the same recovery story applies to 404 — typically when the local
DB gets re-seeded out from under a stale cookie. Without clearing,
the client redirect to /setup re-fires the same dead cookie on every
navigation and never escapes.
The balance gate is `!!address && !!userAddress && address === userAddress`.
When any of these is false, useBalance is disabled and the FE silently
falls through to '$0.00' even when chain has real funds. Today's card
playtest spent an hour chasing this — the only symptom is "balance is 0",
no log, no error.

Add a dev-only console.warn that fires once when the gate blocks AND the
user has finished loading, naming the exact failing condition. One of:
  - kernel address undefined (useZeroDev not ready / init failed)
  - no PEANUT_WALLET account in user.accounts (BE response shape bug)
  - address mismatch: kernel=0x… db=0x… (drift between webAuthnKey-derived
    SA and the DB row, e.g. ZeroDev project id changed across sessions)

Diagnostic-only — no behavior change. Pairs with TODO #57b (wire-shape
contract) and the broader card-playtest hardening.
…d full-setup copy

Pairs with the api-ts cheat additions (grant-card-access, fund-rain-collateral
routes; full-setup chain expanded). New Card section in /dev/debug exposes the
three card-test buttons and updates the full-setup description to mention the
real-vs-synthetic on-chain steps so testers know what they're triggering.
1. qr-pay receipt undefined-vs-null. sendMoney's collateral-only path
   returns receipt: undefined (not null). Loose `receipt !== null` would
   fail to catch undefined and pass it into isTxReverted, which expects
   a TransactionReceipt. Widen the type to include undefined and use
   `!= null` (nullish-safe) so both forms short-circuit.

2. package.json engines.node: drop ^18.18.0 — Next.js 16 requires
   Node 20.9+, no point listing 18 as supported.

3. package.json jest config: drop the @squirrel-labs/peanut-sdk Jest
   alias + transformIgnorePatterns entry. The SDK was uninstalled on
   this branch; the alias was tracking dead code and would mask any
   accidental real-import regression. Mock file at
   src/utils/__mocks__/peanut-sdk.ts deleted.
55 files reformatted by `pnpm prettier --write`. CI's prettier check was
failing on whitespace/quote-style drift across the e2e/ tree + recent
decomplexify edits. No semantic changes.

Closes the prettier-format gate on PR #1904.
Hugo0 added 2 commits April 27, 2026 15:30
tsconfig.json includes .next/types/**/*.ts. next-env.d.ts triple-slash
references next/image-types/global (declares .svg/.gif/.webp module
shapes) and imports .next/dev/types/routes.d.ts. Both come from a
Next.js build/dev/typegen step.

CI was failing typecheck on every static-asset import (~200 TS2307
errors) because no Next command had populated .next/types/. Adding
`pnpm next typegen` is the cheap fix — generates types without a full
build (~10s vs 90s).

Verified locally: rm -rf .next, pnpm next typegen, pnpm typecheck → clean.
… 14-day floor

Three deps were ≤3-8 days old, blocked by check-min-release-age:
- lucide-react 1.9.0 (3d) → 1.8.0 (18d)
- circle-flags 2.8.3 (8d) → 2.8.2 (99d)
- posthog-node 5.30.1 (3d) → 5.29.2 (18d)

All within the same minor series — no behavioural changes the codebase
relies on. Bump back when each ages past the floor.
Users with an ACTIVE Rain card who never granted the one-tap session
key were silently locked out of auto-balance: the rebalance loop
filters out cards without serializedApproval, so every deposit was a
no-op and the only path to grant was a card spend that touched
collateral. A user who only deposits — or whose first spend doesn't
need collateral — would never see the prompt.

Adds EnableAutoBalanceBanner: a non-dismissible ActionModal on /home
that shows up when the card is ACTIVE, has no withdraw approval, and
Rain has provisioned the contracts (so grant() can actually succeed).
preventClose + hideModalCloseButton guarantee the user has to act.
On grant the overview refetch flips hasWithdrawApproval and the modal
hides itself naturally.

Independent of the activation funnel — an unactivated user can still
hold an ACTIVE card and benefit from this surface.
Runs the painscore + complexity + dup + churn analyzer (lives in
mono/engineering/code-analysis) on every PR. Posts an idempotent diff
comment with new findings, resolved findings, and per-file painscore Δ.

Auth: SUBMODULE_TOKEN (already used for content-sync) clones mono.
Ref: vars.MONO_REF (default 'decomplexify' until the analyzer lands on
mono main, then flip to 'main').

Regression gate is advisory (continue-on-error) for the first 2 weeks.
Tighten by removing continue-on-error after the team is acclimatised.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Code-analysis diff

Painscore total: 5668.14 → 5402.79 (-265.35)
Findings: +24 net (+663 new, -639 resolved)

🆕 New findings (663)

  • critical complexity — src/app/(mobile-ui)/qr-pay/page.tsx — CC 321, MI 52.29, SLOC 1120
  • critical complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 287, MI 55.89, SLOC 538
  • critical complexity — src/components/Claim/Link/Initial.view.tsx — CC 208, MI 51.1, SLOC 668
  • critical complexity — src/utils/general.utils.ts — CC 197, MI 57.42, SLOC 757
  • critical method-complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:77 — CC 143 SLOC 209
  • critical complexity — src/components/TransactionDetails/transactionTransformer.ts — CC 129, MI 26.25, SLOC 393
  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 127, MI 53.47, SLOC 471
  • critical method-complexity — src/components/TransactionDetails/transactionTransformer.ts:164 — mapTransactionDataForDrawer CC 127 SLOC 389
  • critical complexity — src/app/(mobile-ui)/withdraw/page.tsx — CC 119, MI 54.34, SLOC 322
  • critical complexity — src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 104, MI 49.69, SLOC 439
  • critical complexity — src/components/Request/link/views/Create.request.link.view.tsx — CC 94, MI 54.8, SLOC 301
  • critical complexity — src/context/HarnessReplay.tsx — CC 91, MI 55.51, SLOC 339
  • critical complexity — src/components/Global/PeanutActionDetailsCard/index.tsx — CC 86, MI 56.49, SLOC 99
  • critical complexity — src/components/AddMoney/components/AddMoneyBankDetails.tsx — CC 83, MI 58.65, SLOC 145
  • critical complexity — src/components/Global/Icons/Icon.tsx — CC 83, MI 73.76, SLOC 255
  • critical complexity — src/components/Home/HomeHistory.tsx — CC 81, MI 60.19, SLOC 257
  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 79, MI 50.41, SLOC 269
  • critical complexity — src/components/Global/DirectSendQR/index.tsx — CC 77, MI 56.16, SLOC 298
  • critical complexity — src/app/(mobile-ui)/home/page.tsx — CC 76, MI 63.08, SLOC 212
  • critical method-complexity — src/app/(mobile-ui)/qr-pay/page.tsx:79 — QRPayPage CC 75 SLOC 322

…and 643 more.

✅ Resolved (639)

  • src/app/(mobile-ui)/qr-pay/page.tsx — CC 321, MI 52.36, SLOC 1118
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 282, MI 56.09, SLOC 531
  • src/components/Claim/Link/Initial.view.tsx — CC 211, MI 50.99, SLOC 665
  • src/utils/general.utils.ts — CC 202, MI 57.39, SLOC 800
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:76 — CC 143 SLOC 208
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 127, MI 53.5, SLOC 469
  • src/app/(mobile-ui)/withdraw/page.tsx — CC 119, MI 54.37, SLOC 321
  • src/components/TransactionDetails/transactionTransformer.ts — CC 116, MI 27.79, SLOC 345
  • src/components/TransactionDetails/transactionTransformer.ts:164 — mapTransactionDataForDrawer CC 114 SLOC 341
  • src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 114, MI 50.7, SLOC 454
  • src/components/Request/link/views/Create.request.link.view.tsx — CC 87, MI 54.21, SLOC 288
  • src/components/Global/PeanutActionDetailsCard/index.tsx — CC 86, MI 56.7, SLOC 97
  • src/components/Global/Icons/Icon.tsx — CC 85, MI 73.84, SLOC 251
  • src/components/AddMoney/components/AddMoneyBankDetails.tsx — CC 83, MI 58.57, SLOC 146
  • src/components/Home/HomeHistory.tsx — CC 81, MI 60.39, SLOC 252
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 78, MI 50.77, SLOC 260
  • src/components/Global/DirectSendQR/index.tsx — CC 78, MI 56.02, SLOC 301
  • src/app/(mobile-ui)/home/page.tsx — CC 76, MI 63.24, SLOC 209
  • src/app/(mobile-ui)/qr-pay/page.tsx:78 — QRPayPage CC 75 SLOC 322
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 73, MI 54.8, SLOC 312

…and 619 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/context/HarnessReplay.tsx 0.0 14.7 +14.7
src/context/PeanutDebug.tsx 0.0 13.7 +13.7
src/features/payments/shared/hooks/useCrossChainTransfer.ts 0.0 12.8 +12.8
src/app/(mobile-ui)/dev/debug/page.tsx 0.0 12.5 +12.5
src/utils/friendly-error.utils.tsx 0.0 10.3 +10.3
src/hooks/wallet/useGrantSessionKey.ts 0.0 10.0 +10.0
src/components/Card/CardLimitEditModal.tsx 0.0 8.2 +8.2
src/context/ReproduceBootstrap.tsx 0.0 7.9 +7.9
src/components/Card/CancelCardModal.tsx 0.0 7.8 +7.8
src/components/Card/LockCardModal.tsx 0.0 7.7 +7.7
src/components/Card/CardPinSetupFlow.tsx 0.0 7.6 +7.6
src/components/Card/CardPioneerFlow.tsx 0.0 7.5 +7.5
src/components/Card/SlideToAction.tsx 0.0 7.5 +7.5
src/app/actions/onramp-quote.ts 0.0 7.2 +7.2
src/utils/peanut-claim.utils.ts 0.0 7.0 +7.0
src/services/rain.ts 0.0 6.6 +6.6
src/hooks/useCardReveal.ts 0.0 6.4 +6.4
src/components/Card/CardPinScreen.tsx 0.0 6.4 +6.4
src/components/Card/YourCardScreen.tsx 0.0 6.4 +6.4
src/app/actions/supported-chains.ts 0.0 6.4 +6.4

- concurrency.group keyed on (workflow, PR number || ref) cancels stale
  in-flight runs when a newer commit lands. Big cost lever on
  rapid-iteration PRs (~80% of stale-run minutes recovered).
- workflow_dispatch added to tests, code-analysis, supply-chain, preview
  — manual rerun from Actions tab or `gh workflow run`
- paths-ignore on code-analysis only (not on tests — would risk blocking
  required status checks under branch protection)

Tests (push: branches: ['**']) is the most-stale workflow today; this is
where the savings will show up first.
@Hugo0 Hugo0 merged commit 785620f into dev Apr 28, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants