Skip to content

Prod Release Sprint 158 — FE: native v1.1.0 · send/claim reliability · receipts (dev-snapshot-2026-08-28 → main) - #2879

Merged
jjramirezn merged 255 commits into
mainfrom
dev-snapshot-2026-08-28
Aug 31, 2026
Merged

Prod Release Sprint 158 — FE: native v1.1.0 · send/claim reliability · receipts (dev-snapshot-2026-08-28 → main)#2879
jjramirezn merged 255 commits into
mainfrom
dev-snapshot-2026-08-28

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Release of dev-snapshot-2026-08-28 (6bbb98c5e) into main. 254 commits, 404 files.

What this is

The frozen release point, cut from dev after the main → dev back-merge (#2874) landed. main is strictly contained in this snapshot (git rev-list --count dev-snapshot-2026-08-28..main = 0), so the merge carries no conflicts.

The shipped native build came from this exact commit. v1.1.0 is tagged here and is already on TestFlight and the Play internal track, with the matching 1.1.0 production bundle published to Capgo. Web and native therefore ship the same tree.

Deliberately excluded

dev has moved 338 commits / 53 merges past this point since the freeze — principally the design-system release (#2813) and app registration/onboarding (#2876). Those are not in this release by design; that is what the snapshot exists to prevent.

Fixes applied during the back-merge review

Bot review of #2874 raised findings against the hotfix work main carried. Applied after verifying each:

  • lib/content.ts — locale fallback returned the first existing locale while existingPageLocale picks the first published one, so links pointed at pages the renderer discarded.
  • SumsubKycWrapperhandleResubmitted never learned about multi-level (added to handleSubmitted in Aug), so an EU/LATAM retry closed the SDK before the follow-up questionnaire, stranding the applicant with a level owed.
  • useMultiPhaseKycFlowKYC_REJECTED now reports once per attempt rather than once per status value; liveKycStatus stays REJECTED across a retry, so keying on it alone either duplicated the event or swallowed a real second rejection.
  • kernelClient.contextpaymasterUrl was asserted before the sponsored flag was read; bundler validation now runs on both paths, paymaster only where a paymaster transport is built.
  • robots.ts — computed its production gate from BASE_URL, whose production fallback made an env-less preview serve the production crawl policy. Now reads the raw env, matching layout.tsx.
  • chip-pr-review.yml — dropped CONTRIBUTOR from the manual dispatch gate (permanent after one merged commit, survives access revocation). Known cost recorded in the file header.

Two findings were refuted rather than applied, with reasoning in the #2874 thread: the ONESIGNAL_WEBHOOK required-key finding (the native adapter never reads that key) and the /support locale finding (contradicts two explicit tests).

Verification

  • Typecheck clean; full suite 344 suites / 4,229 tests green at this commit.
  • Four rounds of Chip re-review; every finding against this merge's own changes is fixed or answered.
  • CI green on 6bbb98c5e.

Not covered

The multi-level KYC retry path has unit coverage but has not been exercised against real Sumsub. It is on TestFlight and Play internal for exactly that reason — worth walking a rejected-then-retried EU/LATAM verification before promoting to production.

innolope-dev and others added 30 commits August 18, 2026 16:55
`shouldIgnoreError` only ever inspected `exception.values[0]`. Sentry orders
that array root-cause-first, so for any error carrying a `cause` the wrapper
sits at the end — and `fetchWithSentry` always sets `userError.cause`.

The `alreadyReported` filter added in 10ee160 to stop double-counting fetch
failures has therefore been inert for its own motivating case ever since:
PEANUT-UI-SNP (the ServiceUnavailableError wrapper) kept being reported
alongside PEANUT-UI-QEY (the timeout it wraps). Sentry confirms the shape —
`error.type` on those events reads "Error, ServiceUnavailableError".

Scan every value's type and message, and collect extension stack frames from
every value rather than just the first.

Also suppress Capgo's background-updater chatter, which captureConsoleIntegration
promotes to ~95 events/day on native. `disable_auto_update_under_native` and
checksum mismatches stay reported: those mean OTA is actually broken for a
build, not that one download hiccuped.
Native reported 970 Sentry events in 24h, 691 of them level:info. Almost all
of that is instrumentation written to watch the CapacitorHttp → direct-fetch
switch (PEANUT-UI-R44). That switch has shipped; the probes have not.

- native-canary: five captureMessage calls per app launch (~550 events/day),
  plus five extra API round-trips on every cold start, to answer a question
  we already answered. Deleted.
- legacy-cookie native transport engaged / native http fallback engaged:
  once-per-session census notes for the same migration. Deleted.
- onesignal subscription snapshot: still useful, but it's a state fact, not a
  fault — moved to PostHog as notification_subscription_snapshot. The failure
  variant stays in Sentry, since failing to read the state is a real error.

Transport behaviour is unchanged; the fallback and prefer-native paths still
work exactly as before, and their tests now assert the paths stay silent.
Same shape as the fetch wrappers. useZeroDev classifies the raw WebAuthn
failure, captures it with full context, and throws a curated user-facing
PasskeyError — and for a plain user cancel it deliberately captures nothing
on web.

Three call sites re-report that wrapper: Landing and JoinWaitlist call
Sentry.captureException on it directly, and GuestLoginModal console.errors it.
The result is a second, context-free event, and LOGIN_CANCELED showing up at
error level despite the deliberate silence — PEANUT-UI-QRW and PEANUT-UI-R20,
19 events yesterday.
useZeroDev classifies a WebAuthn failure, captures the raw error with full
context, and throws a curated PasskeyError for display. For a plain user
cancel it deliberately captures nothing on web — "Cancel saved no state".

Four call sites undid that:

- InvitesPage fired `void handleLoginClick()` with no catch, so cancelling the
  prompt became an unhandled rejection. Now caught, and the curated message is
  surfaced the way every other login entry point surfaces it.
- GuestLoginModal console.error'd the wrapper, which captureConsoleIntegration
  turns into an event.
- Landing and JoinWaitlist called Sentry.captureException on the wrapper —
  a second, context-free copy of an error already reported at the throw site,
  and the reason LOGIN_CANCELED showed up at error level at all.

Landing and JoinWaitlist still report anything that isn't a PasskeyError, so an
unexpected failure in the login path is not silenced.

PEANUT-UI-QRW and PEANUT-UI-R20: 19 events yesterday, all expected outcomes.
…dentity

`capabilities` is optional on /get-user during the capability migration. Deriving
gates over the empty fallback state made every operation read `needs-identity`,
which a support agent cannot tell apart from a genuinely unverified user — the
exact misreading this snapshot exists to prevent. Report an empty `gates` when
the read-model is absent; a read-model that is present but empty still derives
normally, since needs-identity is the truth there.
5343f1d landed in the same function. Both changes keep their meaning:

- Critical-flow captures still bypass every group but userRejected, and the
  early return now sits ahead of the Capgo check so a money-flow event can
  never be dropped by it either.
- The chain scan is narrowed to exception TYPES only. Class names are exact,
  so matching them across the chain can only catch our own wrappers. Scanning
  chained MESSAGES the same way would suppress more, not less — which is the
  failure 5343f1d fixed, where viem's "Details: Failed to fetch" ate real
  payment errors through the networkIssues group. values[0].value keeps the
  matching reach it had before.

Added a guard test for that boundary: a wrapper whose message contains a noise
pattern is still reported.
Android's Capacitor WebView cannot host a third-party subframe.
BridgeWebViewClient.shouldOverrideUrlLoading hands EVERY navigation to
Bridge.launchIntent without checking request.isForMainFrame(), and
launchIntent cancels the load for any host outside the app origin that
isn't listed in server.allowNavigation — which capacitor.config.ts does
not set. The Bridge ToS iframe therefore painted pure white inside our
verification chrome, no signedAgreementId postMessage ever arrived, and
Bridge kept the terms pending. All 39 post-deploy ToS confirmations came
from web sessions; the one native user in the set is the failure. Sumsub
KYC itself is unaffected on native because SumsubKycWrapper routes
Capacitor to the Cordova SDK, not an iframe.

allowNavigation was deliberately NOT the fix: it is baked into the
binary (every installed app stays broken until a store release), and on
android it also registers the host as a WebViewLocalServer authority —
its HTML would be proxied through handleProxyRequest with Capacitor's
bridge JS injected — and widens the androidBridge trusted-origin set.

Android now opens the ToS in the system browser (@capacitor/browser,
already in the binary since April) and treats `browserFinished` as a
`returned` signal — "the user came back", not "the user accepted".
/users/bridge-tos-confirm re-reads has_accepted_terms_of_service from
Bridge, so the detour needs no postMessage; confirmBridgeTosAndAwaitRails
now returns that verdict and the callers use it to tell a real acceptance
from an abandoned one (no phantom completion, no unearned KYC_TOS_ACCEPTED).
When the caller observed no acceptance and Bridge says no, the helper
stops after the confirm retry instead of arming the 30s submission
window and polling rails that will never change.

iOS is untouched — its navigation delegate already gates the same detour
on targetFrame.isMainFrame, so the iframe works there. The web iframe path
is unchanged. Ships over the air; no store release needed.
A fast unmount or visibility change while Browser.addListener was still
resolving removed the listener but still opened the Custom Tab — the ToS
could pop after its owning flow had closed. Late arrivals now drop the
listener and stop.
Before a request exists the QR already encodes the profile payment link
for the entered amount (/{username}/{amount}USDC), so there is no reason
to keep it blurred until the create button is clicked. It now unblurs on
a positive amount and re-blurs when the amount is cleared or zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQHgKufMZzqzwCviP4gXKs
On native the pull refetches via react-query instead of reloading the page,
so nothing on screen blinks and the gesture reads as having done nothing.
Give the indicator the full state sequence instead:

- pulling: the indicator scales in and the arrow rotates toward upright,
  flipping at the release threshold with a light haptic on the crossing
- refreshing: a readable spinner arc (was a thin quarter-circle path)
- done: green checkmark with a pop + success haptic, held briefly, and the
  content fades back in so the screen visibly re-renders

Also restyles the indicator to the app's brutalist look (black border + hard
shadow), guards Element.animate for WebViews that lack it, honours
prefers-reduced-motion for the content fade, and clears pending timeouts on
unmount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zVw46ZCvXYp7b4dpXvJiq
Going /profile → "Exchange rates and fees" → "Try it!" lands the user in the
add-money or withdraw root, depending on their balance and unlocked regions.
Both roots deliberately reset to /home on back rather than calling
router.back(), because their own sub-pages push back to the root and back()
ping-pongs there. That is right for a tab-bar entry, but it strands anyone who
arrived from another screen: back never returns to the widget they came from.

Add a `returnTo` query param the caller sets and the flow roots honour, so the
origin travels with the navigation instead of being guessed at the destination:

- new `withReturnTo` / `readReturnTo` helpers — same-origin only (reusing
  sanitizeRedirectURL), and a target pointing at the current page is dropped,
  since re-pushing the page you are on is a back button that does nothing.
- the exchange-rate CTA passes its own path *and* query string, so back
  restores the currency pair and amount the user was looking at.
- add-money and withdraw check it before falling back to /home. The send-flow
  /send branch and the in-page steps (country list → method selection, amount
  → method selection) keep priority, so back still unwinds one step at a time.

Tests: unit coverage for the helpers (including the off-origin and
self-referential rejections) plus back-navigation cases on both flow roots and
the exchange-rate CTA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbALh4dJvcBUAinjnwQ53q
…-app-fixes

Brings in the pull-to-refresh feedback work (arrow arming, spinner, success
checkmark + haptics, content settle) along with the current main base.
Reverses the iOS gate from 4564b40 and adds an iOS-only copy layer that
presents the referral programme as cashback. Web and Android are
unchanged: every pre-existing catalog string is byte-identical to dev, and
the two new UI elements are behind isIOSNative().

Hiding the programme while the backend kept accruing — and while
referral.reward.earned kept pushing "You earned $X! <name> joined Peanut
with your invite" to the same devices — was the worse position. Guideline
2.3.1 treats hidden-but-discoverable features as grounds for removal. The
programme will be disclosed in Notes for Review instead.

The old citation was also wrong: appStoreCompliance.ts cited 3.1.5(ii),
which is Mining. The clause about offering currency for encouraging
downloads is 3.1.5(v).

appStoreCompliance.ts is deleted along with its seven call sites — the
/rewards and /rewards/invites route guards, the home pill, the profile
row, both invite carousel CTAs, the surprise-claim treatment and the
receipt points row. Removing the route guard also fixes the
referral.reward.earned deep link, which pointed at a guarded route and
bounced iOS users to /home.

The cross-chain withdraw gate is untouched: it lives in
underMaintenance.config.ts, rests on 3.1.5(iii) (Exchanges, which needs
per-region licensing), and multi-chain swaps are the strongest crypto-app
signal we ship.

useAppTranslations wraps useTranslations and prefers an `iosCopy.<key>`
override when one exists, falling back to the base string otherwise.
Overrides sit inside the namespace they belong to, so call sites keep
their existing keys and only the hook name changes. The block is called
iosCopy, not ios, because profile.backup.steps.ios is already content — a
namespace whose own content sat under `ios` would have had every key
silently redirected. Platform is read at render time; the Capacitor bridge
is absent during prerender.

45 overrides per full locale plus voseo deltas for es-AR:

- rewards -> cashback. "Cashback" as a loanword is the standard term in
  both LatAm markets.
- "used Peanut" -> "paid with Peanut", "the more they use" -> "the more
  they pay". Ties the money to a transaction rather than a signup;
  downloading is what 3.1.5(v) names.
- A four-step "How cashback works" card on /rewards. The earn instruction
  used to be a half-sentence on the lifetime total; as its own card it
  reads better and states the process without pinning a dollar to a
  person.
- Dropped "friends & their friends" from qrPay.claim.inviteQrDescription
  and "contribute towards your points forever" from the sticker copy — the
  only two places the UI stated the transitive structure.

Push notification copy is deliberately unchanged. PushChannel targets by
userId and OneSignal fans one notification out to every subscription a
user has, so there is no per-platform copy path and a single notification
cannot say two different things to the same person's devices.

Tests cover both platforms, the fall-through, all four locales, and two
catalog invariants: every override shadows a real base key (a typo would
otherwise silently never resolve), and iosCopy stays distinct from the
existing ios content key.
…o-dev-20260820-2

chore: back-merge main → dev (2026-08-20 release + #2760)
Removing the route guard took the file's only useEffect with it, and
no-unused-vars is an error rather than a warning — the single eslint error
in the run, on top of the 65 pre-existing warnings.
feat(rewards): iOS-only cashback copy, referral surfaces restored
…cel drawer

Four unrelated mobile-app fixes.

**Home tab has no active state.** WalletNavigation compared `usePathname()`
to the nav href with `===`. The native build sets `trailingSlash: true`, so
the pathname is `/home/` there and the comparison never matched — every
active state in the app was silently lost, not just Home. Added
`isSameRoute()` next to the other route helpers and routed the mobile and
desktop nav through it.

**Withdraw's final screen loads twice.** `withdraw/page.tsx` called
`React.lazy()` inside the render body for its two native `?country=` views.
That hands back a fresh, unresolved lazy on every render, so each re-render
re-suspended: React hid the rendered view and swapped in the Suspense
fallback (null) until the import re-resolved a microtask later. The screen
blanked and loaded again — and the success view triggers a re-render itself
when it invalidates the transactions query. Hoisted both to module scope.
The regression test asserts the view isn't display:none straight after a
re-render; it fails against the old code.

**Stray rule under the receipt's last row.** The details card underlines
every row and drops the rule on the last one, but `shouldHideBorder` only
reaches rows the receipt renders itself. `BridgeDepositInstructions` expands
into rows of its own and doesn't take the flag, so the pending bank-deposit
receipt ends on a rule sitting directly on the card border. Rows with a
second runtime gate (a token icon still being fetched, a rate missing from
the payload) fail the same way — the config calls them last, the DOM never
gets them. Let the container decide with `[&>*:last-child]:border-b-0`.

**Cancel-link confirmation is a modal.** Replaced it with a bottom drawer.
It opens from two places — the transaction details drawer and the send-link
success page — so `Drawer` gained a `nested` prop that switches to vaul's
`NestedRoot`; a plain Root inside a Root double-applies the background scale
and fights over the scroll lock. That also retires the `!z-[10]` shuffle the
parent drawer needed to get out of the modal's way. Verified in Chromium
that the content fits without scrolling from 320x568 up, and that the
drawer's `max-h-[80vh] overflow-auto` takes over below that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbALh4dJvcBUAinjnwQ53q
…mobile-app-fixes

Brings in five mobile fixes:
- back navigation after the exchange-rate widget CTA (?returnTo)
- home tab active state on the trailing-slash native build
- withdraw's final screen re-suspending and loading twice
- stray dashed rule under the receipt's last row
- cancel-link confirmation converted from a modal to a drawer
The top strip was forced black in bd9a1b3 so it would stop flipping between
black and beige above the pink beta feedback ribbon. That ribbon is now hidden
on iOS (e56254e), and the underlying inconsistency was really a sizing/source
problem, fixed in 9b3384e by reading Capacitor's natively measured insets.

Recolor only — the safe zone keeps h-safe-top, so the natively measured inset on
Android 15+ and the env() fallback everywhere else are unchanged. Style.Light
pairs dark status-bar icons with the light strip.
fix(mobile): batch of native app fixes
…cation-error-uzgmxr

fix(claim): stop blaming the sender, and offer a retry when one would work (dev)
Artwork and manifest entry for the Help Shape Peanut interview badge
(catalog entry lands in peanut-api-ts). PNG like OFFRAMP_USER — the
vectorized SVG can replace it later without a code change.
TASK-21713
…rame

fix(native): accept Bridge ToS via the system browser on android
feat(badges): PEANUT_SHAPER asset + share line
users.locale on the BE was never written — notification emails could not
know the user's language. LocaleSync (below AppIntlProvider + AuthProvider)
sends the resolved app locale to /update-user, deduped per (user, locale)
via localStorage, synced on startup resolution and on a manual switch in
Settings. Pairs with peanut-api-ts#1387 (locale field + localized
badge-unlocked email).
--no-verify: pre-commit secret scan flags the Hardhat test key in
scripts/spike-session-key-1271.mjs, which comes in from origin/dev
unchanged (public Anvil account #0, not a secret).
…ll mode (TASK-21220)

Strip the parked payment-network.v2 machinery: contract/version checks,
cookie session exchange + renewal, signed focus tokens, and step-up
reveal plumbing. The data layer is now a plain authed serverFetch of
GET /invites/graph?mode=full (+ topNodes), typed to the deployed
response (nodes/edges/p2pEdges/stats). The endpoint never 401s, so an
expired session surfaces as the same clean 403 state as a
non-whitelisted user. Filters move client-side over p2pEdges; the only
server param is topNodes. The 120-day completed-only window is fixed
server-side and becomes a label, not a filter.
Filter panel now offers edge type, direction (honest one-way vs
both-ways over directed edges), min tx count, min total USD, and the
server-side top-N select; a static label states the fixed 120-day
completed-only window. Usernames render directly (endpoint is
team-gated), so the reveal form, pseudonym states and signed-focus flow
are gone; ?user=<username> is the plain focus deep-link, resolved
client-side after data arrives. Canvas colors edges by payment type and
sizes nodes by points; table and inspector show count/total USD.
The feature dir joins the dev-tool exemptions in eslint.config.js
(English-only copy, like InvitesGraph), replacing per-file disables.
innolope-dev and others added 7 commits August 28, 2026 13:09
Conflicts resolved:
- src/content: take main's newer pointer (dev's is an ancestor of it).
- LandingPage/Footer: take main's FooterChrome extraction, port dev's
  prefetch={false} into FooterChrome.
- LandingPageContent/Client: keep dev's server-built FAQ, source its
  learn-more links from main's contentHrefsFor so they resolve to the
  content owner locale instead of a hardcoded /${locale}/help path.
- RegulatedRails: main's hrefKey + contentHrefs, with dev's prefetch={false}.
- useCreateLink: dev's strategy wrapper, main's BASE_URL fallback.
- peanut.config: dev's marketing/app provider split and lazy country
  registration, main's bounded passkey-shim retry. Ceremony telemetry now
  installs after the RETRY settles, not the first attempt — patching before
  the shim owns navigator.credentials wraps the API the shim replaces.
- useZeroDev / app-lock: telemetry wraps the ceremony guard rather than
  replacing it (withCeremonyPurpose outside, guardPasskeyCeremony inside).
- useSumsubKycFlow: keep both additions to handleSdkComplete.
Four real defects surfaced by the bot review of #2874, plus two comment
fixes on the merge's own resolutions.

- content: readPageContentLocalizedResolved returned the first EXISTING
  locale while existingPageLocale picks the first PUBLISHED one, so a slug
  unpublished in the requested locale had its links point at a page the
  renderer then dropped. Skip unpublished during fallback.
- kyc: KYC_REJECTED captured the STATE, not the transition. showWrapper and
  isMultiLevel are dependencies, so re-initiating after a rejection re-ran
  the effect and fired a second event for the same rejection.
- zerodev: paymasterUrl was asserted before the sponsored flag was read,
  rejecting a deliberately unsponsored harness run that never needs one.
- qr test: assert on the bare https://peanut.me so the case exercises the
  shared BASE_URL fallback instead of recognizeQr's explicit peanut.me/
  branch, which stayed green regardless of the constant.
- landing: drop a memo comment orphaned by the server-side FAQ move, and
  restore the nav-row locale-overflow rationale lost in the FooterChrome
  extraction.

Not applied, with reasons in the PR thread: the ONESIGNAL_WEBHOOK required-key
finding (the native adapter never reads that key) and the /support locale
finding (contradicts two explicit tests).
Moving the whole assertion into the sponsored branch dropped bundlerUrl
validation from the unsponsored one. http(undefined) falls back to the
chain's public RPC, which has no ERC-4337 methods, so the harness client
reported ready and then failed every userOp — the env-less bundler failure
assertZeroDevRpcUrls exists to catch.

Split the helper: bundler asserted unconditionally, paymaster only where a
paymaster transport is actually built.
robots.ts computed its production gate from BASE_URL, whose 'https://peanut.me'
fallback made a preview built without NEXT_PUBLIC_BASE_URL serve the production
crawl policy — the exact case layout.tsx guards against in this same merge.
Read the raw env, same as the layout, and cover the fail-closed branch.

Revert the KYC_REJECTED dedupe: keying on the raw status suppressed a SECOND,
legitimate rejection. liveKycStatus is only ever written from a backend
response and no retry path resets it, so REJECTED -> retry -> REJECTED lost
both the event and the fetchUser() that refreshes the rejection reason. That
is worse than the duplicate event it removed; a correct fix needs an
attempt generation threaded through the initiate/retry entry points, which
does not belong in a back-merge.
…port to an attempt

- SumsubKycWrapper: handleSubmitted learned about multi-level in acde2e1
  (2026-08-22); handleResubmitted was left on its July behaviour and still
  closed unconditionally. evaluateSumsubStatusEvent never auto-closes a
  multi-level session, so a RED on Level 1 keeps the SDK open — and the
  in-session retry then closed it before Sumsub could advance to the
  follow-up questionnaire, stranding an EU/LATAM applicant looking submitted
  with a level still owed. Mirror handleSubmitted; cover both branches.

- useMultiPhaseKycFlow: report a terminal status once per ATTEMPT, not once
  per status value. liveKycStatus stays REJECTED across a retry, so keying on
  it alone either duplicates the event (re-opened SDK) or swallows a real
  second rejection. Clearing on each submit signal separates the two.

- chip-pr-review: drop CONTRIBUTOR from the manual dispatch gate. It is
  permanent after one merged commit on a public repo and survives access
  being revoked, so it let a former collaborator open an SSH session to the
  review box. Header records the known cost: an org admin with private
  membership reads as CONTRIBUTOR and their /chip commands are now skipped.
The multi-level branch reused handleSubmitted's first-submit gate, but that
gate exists to collapse the SDK's duplicate twin event — reusing it also
suppressed the attempt boundary the caller needs. In the real sequence
(submit -> RED -> resubmit -> RED) hasSubmittedRef was already true, so the
retry emitted no submit signal, useMultiPhaseKycFlow never cleared its
per-attempt marker, and the retry's own REJECTED went unreported along with
the fetchUser() that refreshes the rejection reason.

Report each logical retry and collapse only the idCheck twin, on a 1s window
— the SDK gives no per-event identity and a real retry takes a user seconds.
…o-dev-20260828

chore: back-merge main into dev (2026-08-28)
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 29, 2026 7:04am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 400 files, which is 250 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to Pro+ to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ba7572c-2974-4479-aa5a-8663f5b2a17b

📥 Commits

Reviewing files that changed from the base of the PR and between a10ec5b and 3d8dd6a.

⛔ Files ignored due to path filters (4)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/badges/peanut_shaper.png is excluded by !**/*.png
  • src/assets/illustrations/landing-countries.svg is excluded by !**/*.svg, !**/*.svg
  • src/types/api.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (400)
  • .github/workflows/android-release.yml
  • .github/workflows/capgo-deploy.yml
  • .github/workflows/chip-pr-review.yml
  • .github/workflows/content-publish-automerge.yml
  • .github/workflows/ios-release.yml
  • .github/workflows/release-native.yml
  • .github/workflows/release-ota.yml
  • .github/workflows/tests.yml
  • capacitor.config.ts
  • docs/NATIVE-RELEASE.md
  • eslint.config.js
  • instrumentation-client.ts
  • next.config.js
  • package.json
  • redirects.json
  • scripts/__tests__/native-build-scan.test.js
  • scripts/__tests__/release-version.test.js
  • scripts/__tests__/semver-newer.test.js
  • scripts/generate-marketing-messages.js
  • scripts/native-build.js
  • scripts/native-ios-postsync.js
  • scripts/release-version.mjs
  • scripts/semver-newer.mjs
  • sentry.client.config.ts
  • sentry.edge.config.ts
  • sentry.server.config.ts
  • sentry.utils.test.ts
  • sentry.utils.ts
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
  • src/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsx
  • src/app/(mobile-ui)/add-money/page.tsx
  • src/app/(mobile-ui)/dev/ceremony-log/page.tsx
  • src/app/(mobile-ui)/dev/layout.tsx
  • src/app/(mobile-ui)/dev/page.tsx
  • src/app/(mobile-ui)/dev/payment-graph/page.tsx
  • src/app/(mobile-ui)/history/__tests__/hidden-balance.test.tsx
  • src/app/(mobile-ui)/history/page.tsx
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/app/(mobile-ui)/profile/exchange-rate/__tests__/exchange-rate-page.test.tsx
  • src/app/(mobile-ui)/profile/exchange-rate/page.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)/receipt/page.tsx
  • src/app/(mobile-ui)/rewards/invites/page.tsx
  • src/app/(mobile-ui)/rewards/page.tsx
  • src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx
  • src/app/(mobile-ui)/withdraw/crypto/__tests__/crypto-withdraw-confirm.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/AppGlobals.tsx
  • src/app/ClientProviders.tsx
  • src/app/[locale]/(marketing)/status/StatusBoard.test.tsx
  • src/app/[locale]/(marketing)/status/StatusBoard.tsx
  • src/app/[locale]/(marketing)/status/feed.test.ts
  • src/app/[locale]/(marketing)/status/feed.ts
  • src/app/[locale]/(marketing)/status/page.tsx
  • src/app/[locale]/(marketing)/status/parseStatusSummary.test.ts
  • src/app/[locale]/(marketing)/status/types.ts
  • src/app/__tests__/ClientProviders.test.tsx
  • src/app/__tests__/payment-network-sw-privacy.test.ts
  • src/app/__tests__/robots.test.ts
  • src/app/actions/__tests__/sumsub-refusals.test.ts
  • src/app/actions/sumsub.ts
  • src/app/actions/supported-chains.ts
  • src/app/actions/types/sumsub.types.ts
  • src/app/crisp-proxy/page.tsx
  • src/app/global-error.jsx
  • src/app/layout.tsx
  • src/app/payment-network-sw-privacy.ts
  • src/app/receipt/[entryId]/page.tsx
  • src/app/receipt/[entryId]/pdf/ReceiptPdfDocument.tsx
  • src/app/receipt/[entryId]/pdf/__tests__/receipt-pdf-model.test.ts
  • src/app/receipt/[entryId]/pdf/__tests__/receipt-pdf-render.test.ts
  • src/app/receipt/[entryId]/pdf/__tests__/route.test.ts
  • src/app/receipt/[entryId]/pdf/receipt-pdf-model.ts
  • src/app/receipt/[entryId]/pdf/receipt-pdf-wordmark.tsx
  • src/app/receipt/[entryId]/pdf/route.ts
  • src/app/robots.ts
  • src/app/sw.ts
  • src/components/0_Bruddle/Toast.tsx
  • src/components/0_Bruddle/ToastStack.tsx
  • src/components/AddMoney/components/InputAmountStep.tsx
  • src/components/AddMoney/components/MantecaAddMoney.tsx
  • src/components/AddMoney/components/__tests__/MantecaAddMoney.test.tsx
  • src/components/AddMoney/views/RhinoDeposit.view.tsx
  • src/components/AddMoney/views/__tests__/RhinoDeposit.view.test.tsx
  • src/components/Badges/badge.utils.ts
  • src/components/Claim/Link/MantecaFlowManager.tsx
  • src/components/Claim/Link/Onchain/Success.view.tsx
  • src/components/Claim/Link/Onchain/__tests__/useClaimSuccessPolling.test.tsx
  • src/components/Claim/Link/Onchain/useClaimSuccessPolling.ts
  • src/components/Claim/Link/SendLinkActionList.tsx
  • src/components/Claim/Link/views/MantecaReviewStep.tsx
  • src/components/Claim/__tests__/claim-success-poll-failure.test.tsx
  • src/components/Create/useCreateLink.tsx
  • src/components/Global/AddressLink/index.tsx
  • src/components/Global/CancelSendLinkDrawer/__tests__/CancelSendLinkDrawer.test.tsx
  • src/components/Global/CancelSendLinkDrawer/index.tsx
  • src/components/Global/CancelSendLinkModal/index.tsx
  • src/components/Global/DirectSendQR/__tests__/recognizeQr.test.ts
  • src/components/Global/Drawer/index.tsx
  • src/components/Global/GuestLoginModal/index.tsx
  • src/components/Global/IframeWrapper/__tests__/IframeWrapper.test.tsx
  • src/components/Global/IframeWrapper/index.tsx
  • src/components/Global/InviteFriendsModal/index.tsx
  • src/components/Global/InvitesGraph/index.tsx
  • src/components/Global/QRScanner/__tests__/useQRScanner.test.ts
  • src/components/Global/QRScanner/index.tsx
  • src/components/Global/QRScanner/useQRScanner.ts
  • src/components/Global/QRScannerOverlay/index.tsx
  • src/components/Global/RateUnavailable/index.tsx
  • src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx
  • src/components/Global/SupportDrawer/index.tsx
  • src/components/Global/WalletNavigation/index.tsx
  • src/components/Home/ActivationCTAs.tsx
  • src/components/Home/HomeHistory.tsx
  • src/components/Home/PerkClaimModal.tsx
  • src/components/Home/__tests__/ActivationCTAs.test.tsx
  • src/components/Home/__tests__/homeHistory.utils.test.ts
  • src/components/Home/homeHistory.utils.ts
  • src/components/Invites/InvitesPage.test.tsx
  • src/components/Invites/InvitesPage.tsx
  • src/components/Kyc/BridgeTosStep.tsx
  • src/components/Kyc/InitiateKycModal.tsx
  • src/components/Kyc/SumsubKycWrapper.tsx
  • src/components/Kyc/__tests__/BridgeTosStep.test.tsx
  • src/components/Kyc/__tests__/SumsubKycWrapper.test.tsx
  • src/components/LandingPage/Footer.test.tsx
  • src/components/LandingPage/FooterChrome.tsx
  • src/components/LandingPage/LandingPageCapacitorGate.tsx
  • src/components/LandingPage/LandingPageClient.tsx
  • src/components/LandingPage/LandingPageContent.tsx
  • src/components/LandingPage/PartnerLockup.tsx
  • src/components/LandingPage/RegulatedRails.tsx
  • src/components/LandingPage/SEOFooter.tsx
  • src/components/LandingPage/SendInSecondsCTA.tsx
  • src/components/LandingPage/ShhhhhFold.tsx
  • src/components/LandingPage/StickyMobileCTA.tsx
  • src/components/LandingPage/SupportedRailsFaqAnswer.tsx
  • src/components/LandingPage/exchangeCtaRoute.ts
  • src/components/LandingPage/faq.tsx
  • src/components/LandingPage/hero.tsx
  • src/components/LandingPage/noFees.tsx
  • src/components/LandingPage/securityBuiltIn.tsx
  • src/components/LandingPage/yourMoney.tsx
  • src/components/Profile/components/PublicProfile.tsx
  • src/components/Profile/components/__tests__/PublicProfile.test.tsx
  • src/components/Profile/index.tsx
  • src/components/Profile/views/UnlockedRegions.view.tsx
  • src/components/Request/Pay/Pay.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/views/Initial.link.send.view.tsx
  • src/components/Send/link/views/Success.link.send.view.tsx
  • src/components/Send/link/views/__tests__/Initial.link.send.view.test.tsx
  • src/components/Send/views/Contacts.view.tsx
  • src/components/Send/views/SendRouter.view.tsx
  • src/components/Settings/DeleteAccountButton.tsx
  • src/components/Settings/LanguageView.tsx
  • src/components/Settings/__tests__/DeleteAccountButton.test.tsx
  • src/components/Setup/Views/JoinWaitlist.tsx
  • src/components/Setup/Views/Landing.tsx
  • src/components/Setup/Views/SetupPasskey.tsx
  • src/components/TransactionDetails/DownloadReceiptPdfLink.tsx
  • src/components/TransactionDetails/ReceiptUnavailable.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/components/TransactionDetails/TransactionDetailsDrawer.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/__tests__/ReceiptUnavailable.test.tsx
  • src/components/TransactionDetails/__tests__/TransactionCard.test.tsx
  • src/components/TransactionDetails/__tests__/receipt-download-pdf.test.tsx
  • src/components/TransactionDetails/__tests__/receipt-trailing-divider.test.ts
  • src/components/TransactionDetails/__tests__/transactionTransformer.test.ts
  • src/components/TransactionDetails/transaction-predicates.ts
  • src/components/TransactionDetails/transactionTransformer.ts
  • src/components/TransactionDetails/useReceiptViewModel.ts
  • src/components/UserHeader/index.tsx
  • src/config/AppStateProviders.tsx
  • src/config/__tests__/ios-store-gating.test.ts
  • src/config/__tests__/wagmi-storage.test.ts
  • src/config/appStoreCompliance.ts
  • src/config/justaname.config.tsx
  • src/config/peanut.config.tsx
  • src/config/queryClient.ts
  • src/config/wagmi.config.tsx
  • src/constants/__tests__/nonEvmLeak.test.ts
  • src/constants/__tests__/payment.consts.test.ts
  • src/constants/__tests__/routes.test.ts
  • src/constants/__tests__/sumsub-reject-labels.consts.test.ts
  • src/constants/analytics.consts.ts
  • src/constants/app-release.ts
  • src/constants/general.consts.ts
  • src/constants/payment.consts.ts
  • src/constants/routes.ts
  • src/constants/sumsub-reject-labels.consts.ts
  • src/constants/support.ts
  • src/constants/token-registry.consts.ts
  • src/constants/userop.consts.ts
  • src/constants/wallet-token.consts.ts
  • src/constants/zerodev.consts.ts
  • src/context/ModalsContext.tsx
  • src/context/__tests__/ModalsContext.test.tsx
  • src/context/appFlowProviders.tsx
  • src/context/authContext.tsx
  • src/context/contextProvider.tsx
  • src/context/kernelClient.context.tsx
  • src/features/payment-network-explorer/DesktopGuard.tsx
  • src/features/payment-network-explorer/ExplorerHeader.tsx
  • src/features/payment-network-explorer/ExplorerStatePanel.tsx
  • src/features/payment-network-explorer/ExplorerSummary.tsx
  • src/features/payment-network-explorer/FacetChecklist.tsx
  • src/features/payment-network-explorer/FilterPanel.tsx
  • src/features/payment-network-explorer/FocusBanner.tsx
  • src/features/payment-network-explorer/InfoTooltip.tsx
  • src/features/payment-network-explorer/Inspector.tsx
  • src/features/payment-network-explorer/NetworkCanvas.tsx
  • src/features/payment-network-explorer/PaymentNetworkExplorer.tsx
  • src/features/payment-network-explorer/RelationshipDetails.tsx
  • src/features/payment-network-explorer/RelationshipTable.tsx
  • src/features/payment-network-explorer/SearchBox.tsx
  • src/features/payment-network-explorer/__tests__/ExplorerSummary.test.tsx
  • src/features/payment-network-explorer/__tests__/InfoTooltip.test.tsx
  • src/features/payment-network-explorer/__tests__/Inspector.test.tsx
  • src/features/payment-network-explorer/__tests__/PaymentNetworkExplorer.test.tsx
  • src/features/payment-network-explorer/__tests__/api.test.ts
  • src/features/payment-network-explorer/__tests__/format.test.ts
  • src/features/payment-network-explorer/__tests__/headers.test.ts
  • src/features/payment-network-explorer/__tests__/privacy.test.ts
  • src/features/payment-network-explorer/__tests__/query.test.ts
  • src/features/payment-network-explorer/__tests__/reducedMotion.test.tsx
  • src/features/payment-network-explorer/__tests__/selectors.test.ts
  • src/features/payment-network-explorer/__tests__/sentryServerEdge.test.ts
  • src/features/payment-network-explorer/__tests__/useExplorerUrlState.test.tsx
  • src/features/payment-network-explorer/__tests__/usePaymentNetworkExplorer.test.tsx
  • src/features/payment-network-explorer/api.ts
  • src/features/payment-network-explorer/format.ts
  • src/features/payment-network-explorer/privacy.ts
  • src/features/payment-network-explorer/query.ts
  • src/features/payment-network-explorer/selectors.ts
  • src/features/payment-network-explorer/types.ts
  • src/features/payment-network-explorer/useDesktopViewport.ts
  • src/features/payment-network-explorer/useExplorerUrlState.ts
  • src/features/payment-network-explorer/usePaymentNetworkExplorer.ts
  • src/features/payment-network-explorer/useReducedMotion.ts
  • src/features/payments/flows/direct-send/useDirectSendFlow.ts
  • src/features/payments/flows/direct-send/views/SendInputView.tsx
  • src/hooks/__tests__/confirmBridgeTosAndAwaitRails.test.ts
  • src/hooks/__tests__/useCrispUserData.test.tsx
  • src/hooks/__tests__/useCurrency.test.tsx
  • src/hooks/__tests__/useNativeAppLinks.test.tsx
  • src/hooks/__tests__/usePrimaryNameServer.test.tsx
  • src/hooks/__tests__/usePullToRefresh.test.tsx
  • src/hooks/__tests__/useSplashGate.test.tsx
  • src/hooks/__tests__/useSumsubKycFlow.test.ts
  • src/hooks/__tests__/useTransactionHistory.test.tsx
  • src/hooks/useCrispUserData.ts
  • src/hooks/useCurrency.ts
  • src/hooks/useHomeCarouselCTAs.tsx
  • src/hooks/useMultiPhaseKycFlow.ts
  • src/hooks/useNativeAppLinks.ts
  • src/hooks/useNativePlugins.ts
  • src/hooks/useNotifications.ts
  • src/hooks/useOtaUpdates.ts
  • src/hooks/usePrimaryNameServer.ts
  • src/hooks/usePullToRefresh.ts
  • src/hooks/useRecipientDisplay.ts
  • src/hooks/useSplashGate.ts
  • src/hooks/useSumsubKycFlow.ts
  • src/hooks/useSupportClientContext.ts
  • src/hooks/useTransactionHistory.ts
  • src/hooks/useZeroDev.ts
  • src/hooks/wallet/__tests__/spendPreflight.test.ts
  • src/hooks/wallet/useBalance.ts
  • src/hooks/wallet/useGrantSessionKey.ts
  • src/hooks/wallet/useSendMoney.ts
  • src/hooks/wallet/useSignSpendBundle.ts
  • src/hooks/wallet/useSignUserOp.ts
  • src/hooks/wallet/useSpendBundle.ts
  • src/i18n/app/AppIntlProvider.tsx
  • src/i18n/app/IntlCore.tsx
  • src/i18n/app/LocaleSync.tsx
  • src/i18n/app/MarketingIntlProvider.tsx
  • src/i18n/app/__tests__/locale-store.test.ts
  • src/i18n/app/__tests__/locale-sync.test.ts
  • src/i18n/app/__tests__/marketing-messages.test.ts
  • src/i18n/app/__tests__/messages.test.ts
  • src/i18n/app/__tests__/useAppTranslations.test.tsx
  • src/i18n/app/deep-merge.ts
  • src/i18n/app/locale-context.ts
  • src/i18n/app/locale-store.ts
  • src/i18n/app/locale-sync.ts
  • src/i18n/app/marketing-namespaces.json
  • src/i18n/app/messages.marketing.ts
  • src/i18n/app/messages.ts
  • src/i18n/app/messages/en.json
  • src/i18n/app/messages/en.marketing.json
  • src/i18n/app/messages/es-419.json
  • src/i18n/app/messages/es-419.marketing.json
  • src/i18n/app/messages/es-AR.json
  • src/i18n/app/messages/es-AR.marketing.json
  • src/i18n/app/messages/pt-BR.json
  • src/i18n/app/messages/pt-BR.marketing.json
  • src/i18n/app/useAppTranslations.ts
  • src/i18n/config.ts
  • src/i18n/en.json
  • src/i18n/es-419.json
  • src/i18n/es-ar.json
  • src/i18n/pt-br.json
  • src/i18n/types.ts
  • src/lib/content.ts
  • src/services/__tests__/manteca-withdraw-transport.test.ts
  • src/services/__tests__/points-calculate.test.ts
  • src/services/__tests__/users.test.ts
  • src/services/card.ts
  • src/services/manteca.ts
  • src/services/onesignal/native.adapter.ts
  • src/services/points.ts
  • src/services/sendLinks.ts
  • src/services/services.types.ts
  • src/services/users.ts
  • src/styles/globals.css
  • src/types/api.openapi.json
  • src/types/badge-assets.json
  • src/types/capabilities.ts
  • src/utils/__mocks__/justaname.ts
  • src/utils/__mocks__/yoga-wasm-cjs.ts
  • src/utils/__tests__/balance.utils.test.ts
  • src/utils/__tests__/camera-failure.test.ts
  • src/utils/__tests__/capgo-updater.test.ts
  • src/utils/__tests__/crisp-session-fields.test.ts
  • src/utils/__tests__/csp-report.utils.test.ts
  • src/utils/__tests__/dateGrouping.utils.test.ts
  • src/utils/__tests__/deferred-link.test.ts
  • src/utils/__tests__/ens-name.utils.test.ts
  • src/utils/__tests__/friendly-error.utils.test.tsx
  • src/utils/__tests__/history.utils.test.ts
  • src/utils/__tests__/marketing-routes.test.ts
  • src/utils/__tests__/migration.utils.test.ts
  • src/utils/__tests__/native-canary.test.ts
  • src/utils/__tests__/native-routes.test.ts
  • src/utils/__tests__/network-triage.test.ts
  • src/utils/__tests__/return-to.utils.test.ts
  • src/utils/__tests__/safe-storage.test.ts
  • src/utils/__tests__/sentry-fallback.test.ts
  • src/utils/__tests__/sentry-fingerprint-url.test.ts
  • src/utils/__tests__/sentry-prefer-native.test.ts
  • src/utils/__tests__/sentry.utils.test.ts
  • src/utils/__tests__/support-cache.test.ts
  • src/utils/__tests__/support-context.test.ts
  • src/utils/__tests__/support-verification.test.ts
  • src/utils/__tests__/to-error.test.ts
  • src/utils/__tests__/webauthn-ceremony-telemetry.test.ts
  • src/utils/__tests__/withdraw.utils.test.ts
  • src/utils/app-lock.ts
  • src/utils/balance.utils.ts
  • src/utils/camera-failure.ts
  • src/utils/capacitor.ts
  • src/utils/capgo-updater.ts
  • src/utils/cookie-url.utils.ts
  • src/utils/crisp.ts
  • src/utils/csp-report.utils.ts
  • src/utils/dateGrouping.utils.ts
  • src/utils/deep-link-state.ts
  • src/utils/defer-analytics.ts
  • src/utils/deferred-link.ts
  • src/utils/demo.ts
  • src/utils/ens-name.utils.ts
  • src/utils/ens-onchain.utils.ts
  • src/utils/ens.utils.ts
  • src/utils/ephemeralSpendKey.ts
  • src/utils/friendly-error.utils.tsx
  • src/utils/general.utils.ts
  • src/utils/history.utils.ts
  • src/utils/invite-code.utils.ts
  • src/utils/kernelMigration.utils.ts
  • src/utils/marketing-routes.ts
  • src/utils/migration.utils.ts
  • src/utils/native-auth-capture.ts
  • src/utils/native-canary.ts
  • src/utils/native-routes.ts
  • src/utils/network-triage.ts
  • src/utils/private-routes.ts
  • src/utils/provider-rejection.utils.test.ts
  • src/utils/provider-rejection.utils.ts
  • src/utils/return-to.utils.ts
  • src/utils/safe-storage.ts
  • src/utils/sentry-init.ts
  • src/utils/sentry-lazy.ts
  • src/utils/sentry.utils.ts
  • src/utils/support-cache.ts
  • src/utils/support-context.ts
  • src/utils/support-verification.ts
  • src/utils/to-error.ts
  • src/utils/webauthn-ceremony-telemetry.ts
  • src/utils/webauthn.utils.ts
  • tailwind.config.js
  • vercel.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7346.17 → 7756.6 (+410.43)
Findings: +105 net (+1016 new, -911 resolved)

🆕 New findings (1016)

  • critical complexity — src/components/Global/InvitesGraph/index.tsx — CC 515, MI 53.32, SLOC 1722
  • critical complexity — src/app/(mobile-ui)/qr-pay/page.tsx — CC 308, MI 52.35, SLOC 1120
  • critical complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 179, MI 51.39, SLOC 453
  • critical complexity — src/utils/general.utils.ts — CC 178, MI 57.81, SLOC 676
  • critical complexity — src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 161, MI 51.05, SLOC 619
  • critical complexity — src/app/(mobile-ui)/withdraw/page.tsx — CC 121, MI 53.11, SLOC 366
  • critical method-complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:102 — CC 121 SLOC 243
  • critical complexity — src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 115, MI 57.28, SLOC 396
  • critical complexity — src/lib/content.ts — CC 103, MI 58.26, SLOC 388
  • critical complexity — src/components/TransactionDetails/transactionTransformer.ts — CC 102, MI 43.17, SLOC 293
  • critical complexity — src/components/TransactionDetails/useReceiptViewModel.ts — CC 101, MI 61.53, SLOC 186
  • critical complexity — src/context/kernelClient.context.tsx — CC 101, MI 57.01, SLOC 480
  • critical complexity — src/hooks/useSumsubKycFlow.ts — CC 98, MI 51.24, SLOC 438
  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 97, MI 49.9, SLOC 440
  • critical complexity — src/utils/friendly-error.utils.tsx — CC 96, MI 45.78, SLOC 301
  • critical complexity — src/components/Request/link/views/Create.request.link.view.tsx — CC 94, MI 53.36, SLOC 324
  • critical complexity — src/components/Global/QRScanner/useQRScanner.ts — CC 92, MI 56.75, SLOC 389
  • critical complexity — src/features/payment-network-explorer/NetworkCanvas.tsx — CC 90, MI 61.51, SLOC 220
  • critical complexity — src/utils/native-routes.ts — CC 90, MI 55.22, SLOC 216
  • critical complexity — src/components/Invites/InvitesPage.tsx — CC 87, MI 55.1, SLOC 214

…and 996 more.

✅ Resolved (911)

  • src/components/Global/InvitesGraph/index.tsx — CC 515, MI 53.18, SLOC 1713
  • src/app/(mobile-ui)/qr-pay/page.tsx — CC 306, MI 52.68, SLOC 1089
  • src/utils/general.utils.ts — CC 201, MI 57.64, SLOC 765
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 174, MI 51.7, SLOC 444
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx — CC 158, MI 51.52, SLOC 597
  • src/app/(mobile-ui)/withdraw/page.tsx — CC 121, MI 53.24, SLOC 364
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:100 — CC 116 SLOC 234
  • src/app/(mobile-ui)/add-money/[country]/bank/page.tsx — CC 113, MI 57.31, SLOC 396
  • src/components/Home/HomeHistory.tsx — CC 105, MI 57.64, SLOC 321
  • src/lib/content.ts — CC 103, MI 58.31, SLOC 387
  • src/context/kernelClient.context.tsx — CC 101, MI 57.03, SLOC 479
  • src/components/TransactionDetails/transactionTransformer.ts — CC 98, MI 43.69, SLOC 286
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 95, MI 50.32, SLOC 426
  • src/components/TransactionDetails/useReceiptViewModel.ts — CC 94, MI 61.68, SLOC 177
  • src/components/Request/link/views/Create.request.link.view.tsx — CC 93, MI 53.48, SLOC 323
  • src/hooks/useSumsubKycFlow.ts — CC 87, MI 52.42, SLOC 391
  • src/app/(mobile-ui)/home/page.tsx — CC 86, MI 62.76, SLOC 240
  • src/components/Invites/InvitesPage.tsx — CC 84, MI 54.71, SLOC 209
  • src/components/Global/QRScannerOverlay/index.tsx — CC 81, MI 54.84, SLOC 347
  • src/app/(mobile-ui)/qr-pay/page.tsx:94 — QRPayPage CC 78 SLOC 409

…and 891 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/app/receipt/[entryId]/pdf/receipt-pdf-model.ts 0.0 11.3 +11.3
src/features/payment-network-explorer/NetworkCanvas.tsx 0.0 10.6 +10.6
src/utils/support-context.ts 0.0 9.4 +9.4
src/features/payment-network-explorer/PaymentNetworkExplorer.tsx 0.0 8.3 +8.3
src/features/payment-network-explorer/Inspector.tsx 0.0 8.0 +8.0
src/utils/cookie-url.utils.ts 0.0 7.8 +7.8
src/app/receipt/[entryId]/pdf/route.ts 0.0 7.7 +7.7
src/i18n/app/IntlCore.tsx 0.0 7.5 +7.5
src/features/payment-network-explorer/selectors.ts 0.0 7.1 +7.1
src/app/[locale]/(marketing)/status/page.tsx 0.0 7.1 +7.1
src/utils/webauthn-ceremony-telemetry.ts 0.0 7.1 +7.1
src/features/payment-network-explorer/RelationshipTable.tsx 0.0 7.1 +7.1
src/hooks/useSupportClientContext.ts 0.0 6.9 +6.9
src/utils/network-triage.ts 0.0 6.9 +6.9
src/app/[locale]/(marketing)/status/StatusBoard.tsx 0.0 6.7 +6.7
src/utils/sentry-init.ts 0.0 6.7 +6.7
src/features/payment-network-explorer/InfoTooltip.tsx 0.0 6.7 +6.7
src/app/receipt/[entryId]/pdf/ReceiptPdfDocument.tsx 0.0 6.6 +6.6
src/components/TransactionDetails/DownloadReceiptPdfLink.tsx 0.0 6.4 +6.4
src/components/Claim/Link/Onchain/useClaimSuccessPolling.ts 0.0 6.3 +6.3

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 4233 ran, 0 failed, 0 skipped, 1.3m

📊 Coverage (unit)

metric %
statements 71.5%
branches 56.4%
functions 62.5%
lines 72.5%
⏱ 10 slowest test cases
time test
3.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
0.7s src/app/receipt/[entryId]/pdf/__tests__/receipt-pdf-render.test.ts › produces a real, non-trivial PDF document
0.6s src/utils/__tests__/crisp.test.ts › configures once across repeated support opens
0.6s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
0.6s src/utils/__tests__/crisp.test.ts › retries configuration on the next open after a failure
0.4s src/i18n/app/__tests__/shhhhh-catalog.test.ts › es-419 keeps the tag the hero interpolates
0.4s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.4s src/utils/__tests__/crisp.test.ts › resets the native session on logout once support has been opened
0.4s scripts/__tests__/release-version.test.js › rides the current build with the commit count as its OTA component
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Chip review — changes requested

Request changes: native deep-link telemetry exports send-link bearer secrets to analytics; the other reviewed high-risk release paths and exact-head CI were clean.

Findings

  • BLOCKING · src/hooks/useNativeAppLinks.ts:67 · Redact send-link secrets before deep-link telemetry
    Opening a normal native send link routes /claim?...#p=<password> through this callback. deepLinkToNativePath intentionally preserves that fragment, so both raw here and mapped on the next line contain the password, and posthog.capture exports it. That password derives the private claim key, so an analytics reader could claim unspent funds. Emit only a route class/source/outcome (or strip query and fragment from both fields), and add a native-link telemetry regression test with a claim URL.

  • MAJOR · .github/workflows/android-release.yml:236 · [moonshotai/kimi-k3] Production OTA signing key exposed to unpinned @capgo/cli@latest fetched at release time
    The new 'Publish matching production OTA bundle' step runs npx @capgo/cli@latest bundle upload ... --key-data-v2 "$CAPGO_PRIVATE_KEY". npx resolves @latest from the npm registry at run time, so every native release downloads and executes whatever code is currently published as @capgo/cli with CAPGO_PRIVATE_KEY — the key that signs OTA bundles accepted by every production install — and CAPGO_API_KEY in scope. A compromised or hijacked @capgo/cli release exfiltrates the signing key, after which an attacker can push arbitrary JS to the entire fleet; that is the same blast radius this pipeline redesign exists to protect. The PR adds several new @latest invocations (this step, the OTA floor checks in android-release.yml and ios-release.yml, the 'Verify channel serves the new bundle' step in capgo-deploy.yml, and the resolver in release-ota.yml); the pre-existing upload step in capgo-deploy.yml has the same flaw. Fix: pin @capgo/cli to an exact version everywhere it is given secrets (e.g. @capgo/cli@x.y.z), or add it as a locked devDependency and run it from node_modules so the lockfile pins the integrity hash.

  • MINOR · .github/workflows/android-release.yml:225 · [moonshotai/kimi-k3] Native auto-publish writes the production Capgo channel from a job with no environment declared
    capgo-deploy.yml declares environment: Production for production deploys, and its header comment promises that adding required reviewers to that environment will gate production deploys 'with no change to this file'. The new auto-publish step in android-release.yml (and the mirrored one in ios-release.yml) writes the same production channel from the build job, which declares no environment at all. Once the team adds protection rules to the Production environment — the documented plan — the release-ota path will queue for a second approver while the native-release auto-publish (the path that strands the fleet's OTA if it ships the wrong thing) keeps shipping ungated, contradicting the in-file promise. Today the environment has no rules (documented), so nothing changes yet; the gap is latent in the intended control. Fix: move the publish step into its own job with environment: Production (gating the whole build job would also gate the store upload), or declare the environment on the build job and document that choice.

  • MINOR · .github/workflows/android-release.yml:180 · [moonshotai/kimi-k3] OTA floor check fails open if the scraped Capgo CLI output contains a spurious high version
    CURRENT is scraped with grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?' | tail -n1 from npx @capgo/cli@latest channel currentBundle production --quiet — an unpinned CLI whose stdout format is not contractual. If stdout ever contains a version-like token that sorts above the release version (a warning line, a format change), semver-newer.mjs "$VERSION_NAME" "$CURRENT" returns false, needs_ota=false, and the matching production bundle publish is silently skipped — recreating the TASK-21793 failure this guard exists to prevent (new binary refuses every existing bundle, CI stays green, fleet OTA dead). The empty-output case fails closed, but a garbage-high value does not. In the designed scheme a production bundle strictly ahead of a brand-new binary's versionName is anomalous, so also error when semver-newer.mjs "$CURRENT" "$VERSION_NAME" is true, or read the version from a structured Capgo API field instead of grep. Same pattern in ios-release.yml's floor check and release-ota.yml's resolver.

Checked clean

  • Verified the detached HEAD, supplied base, and merge base exactly
  • Exact-head format, ESLint, typecheck, unit, e2e, aggregate CI, deploy preview, and CodeQL language analyses completed successfully
  • Release Native/OTA version resolution, concurrency, environment, and tag-write boundaries
  • Multi-level KYC resubmission and per-attempt rejection reporting
  • Receipt PDF public-kind whitelist, cache scoping, masking, and filename sanitization
  • Payment-network explorer backend authorization plus telemetry and service-worker suppression
  • Claim success polling terminal states and retryable failure handling
  • Crisp support snapshot owner-scoped cache reads and URL redaction
  • Focused local Jest rerun was unavailable because the detached worktree has no node_modules; exact-head unit CI passed

Second opinion by moonshotai/kimi-k3: 3 finding(s), marked with the model name. It reads the diff only, so treat its findings as advice.

Exact head: 6bbb98c5e596 · Context: repo, mono, ci

Comment thread src/hooks/useNativeAppLinks.ts Outdated
Comment thread .github/workflows/android-release.yml
Comment thread .github/workflows/android-release.yml
Comment thread .github/workflows/android-release.yml Outdated
…bad OTA floor read

- BLOCKING: native deep-link telemetry sent send-link passwords to PostHog.
  deepLinkToNativePath deliberately preserves `#p=<password>` so the claim
  page can read it (native-routes.ts), and captureLink then exported both the
  raw and mapped URL verbatim. That password derives the private claim key, so
  an analytics reader could claim unspent funds. Capture the path only — query
  and fragment are dropped — with a regression test on a real claim URL.

- Pin @capgo/cli to 8.42.4 at all 11 call sites (4 workflows + 2 package.json
  scripts). npx resolved @latest at run time while CAPGO_PRIVATE_KEY — the key
  that signs bundles every production install accepts — was in scope, so a
  hijacked release could have exfiltrated it and pushed arbitrary JS to the
  fleet. 8.42.4 is the version yesterday's v1.1.0 release actually ran.

- OTA floor check now fails closed when the scraped current bundle sorts ABOVE
  the release version. That is impossible in the version scheme, so it means an
  unreliable read; previously it silently set needs_ota=false, skipped the
  matching publish and recreated TASK-21793 with CI green.

Not applied: the `environment: Production` finding. Both release workflows
already declare it on the `release` job that runs the publish step; there is
no unguarded `build` job.

@chip-peanut-bot chip-peanut-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Chip review — no blocking findings — this is not an approval

Clean at the exact head. The earlier deep-link telemetry secret exposure is fixed, and the single follow-up commit introduces no actionable defect.

Checked clean

  • Prior blocking finding — FIXED: native deep-link telemetry now strips query strings and fragments from both raw and mapped values, with regression coverage for a claim password and identifiers.
  • Current-head delta: exactly one commit since the prior reviewed SHA; all 11 Capgo CLI call sites are pinned to 8.42.4 and Android/iOS OTA-floor reads fail closed when the current bundle anomalously sorts above the release.
  • Correctness, security, adversarial, and slop passes over the follow-up commit found no surviving defect; navigation still receives the complete deep link because redaction is confined to captureLink.
  • Exact-head typecheck, ESLint, format, unit, e2e, analysis, human-authors, and aggregate CI checks passed. The targeted Jest command could not run in the detached worktree because dependencies are not installed.
  • The failing CodeQL aggregate was refuted: its stored-XSS annotation targets a stories-page blob identical on base and head, and the href is root-relative with the filesystem slug encoded. GitHub also reported that the PR was too large to identify the introducing diff reliably.
  • Detached worktree HEAD, PR head/base metadata, and merge base match the supplied SHAs.

Second opinion skipped: openrouter-empty-reply.

Exact head: 3d8dd6a17b2d · Context: repo, ci

@innolope-dev
innolope-dev dismissed chip-peanut-bot[bot]’s stale review August 29, 2026 09:08

Superseded by chip's clean review on 3d8dd6a — telemetry redaction, Capgo pin, and fail-closed floor check all landed

@kushagrasarathe kushagrasarathe changed the title release: dev-snapshot-2026-08-28 → main Prod Release Sprint 158 — FE: native v1.1.0 · send/claim reliability · receipts (dev-snapshot-2026-08-28 → main) Aug 31, 2026
@kushagrasarathe

Copy link
Copy Markdown
Contributor

Sprint 158 release page (changelog · migrations · QA checklist · runbook): https://app.notion.com/p/peanutprotocol/prod-release-158-3cd8381175798069b855d63ca3ae5f09

Merge order: peanut-api-ts#1461 first (carries the migrations, which now auto-apply during the Render build), then this PR.

@Hugo0 Hugo0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving for the Sprint 158 release (FE snapshot dev-snapshot-2026-08-28, head 3d8dd6a17).

Basis for this approval:

  • All checks green at 3d8dd6a17 — typecheck, unit, e2e, Deploy-Preview, CodeQL.
  • Native v1.1.0 is tagged at this exact commit and already on TestFlight + Play internal, so web and native ship the same tree.
  • Security pass over the main→snapshot diff (404 files): no new outbound hosts (added URLs are peanut.me / api.peanut.me / Sentry / arbiscan, the rest are .test / .example fixtures), no key or seed material leaving the client. The only new Function is in src/utils/__mocks__/yoga-wasm-cjs.ts — a Jest mock that is never bundled; every innerHTML write in usePullToRefresh.ts assigns a static SVG constant, no user input. Capgo CLI pinned from @latest to 8.42.4.

Order of operations: merge BE peanutprotocol/peanut-api-ts#1461 first (after the manual prisma migrate deploy), then this one.

Manual smoke on the preview is still outstanding per the release page — receipt PDF download, activity rendering, send-link create/claim/cancel, residence KYC, and QR pay + withdraw are the flagged risk areas.

@Hugo0

Hugo0 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

@jjramirezn tagging you on the FE half of the Sprint 158 release — pairs with peanutprotocol/peanut-api-ts#1461.

Approved and green at 3d8dd6a17. Native v1.1.0 is tagged at this exact commit, so web and native ship the same tree. Merge order: BE first (after the manual prisma migrate deploy), then this.

Smoke list still open — receipt PDF download, activity rendering, send-link create/claim/cancel, residence KYC, QR pay + withdraw. The QR-pay withdraw triage (#2869) pairs with your #1457 on the BE side.

@jjramirezn
jjramirezn merged commit 85079d7 into main Aug 31, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants