Skip to content

chore: back-merge main → dev (2026-07-22)#2477

Merged
jjramirezn merged 69 commits into
devfrom
chore/backmerge-main-to-dev-2026-07-22
Jul 24, 2026
Merged

chore: back-merge main → dev (2026-07-22)#2477
jjramirezn merged 69 commits into
devfrom
chore/backmerge-main-to-dev-2026-07-22

Conversation

@kushagrasarathe

Copy link
Copy Markdown
Contributor

Back-merge of all hotfixes/PRs currently on main but not yet on dev. Can't PR main→dev directly, so this branch is dev + origin/main merged in.

Merged cleanly — zero conflicts. 53 main commits ahead of dev at branch time (step-up auth, CSP report-only, native app lock, native push deep-link routing, Rhino min-withdrawal guard, Manteca QR wallet split, Rain PoA upload, sentry noise suppression, a11y dialog titles, etc.).

No manual edits — pure merge.

🤖 Generated with Claude Code

innolope-dev and others added 30 commits July 16, 2026 20:16
Array.prototype.sort is in-place, so the sortedBadges useMemo mutated the
caller-owned badges array during render. Sort a copy so the memo callback is
pure and safe under concurrent/StrictMode double-invocation.

No user-visible change today: the only caller passes locally-owned state that
is repopulated per mount, and the resulting order is the desired one anyway.
The anti-rot scan only consulted ITEMS_TO_DISABLE, so a page whose
server-only exports are already stripped by a P0_TRANSFORMS stub still
failed the native build. This is live on main: (mobile-ui)/claim/page.tsx
has force-dynamic and a matching stub, and the scan rejects it.

Skip files handled by a transform, and cover the scan with a unit test.
getPhysicalWaitlist types position as nullable and the joined branch is
gated on joinedAt, so a user on the list with no queue position was told
'You are #null on the list.' Drop the number for that case and format
real positions for readability.
The hardcoded "1,234" assumed an en-US default locale, so the test failed
under any locale with a different thousands separator (de_DE renders 1.234).
Assert against toLocaleString output instead — that is the actual contract.
Sync the OpenAPI snapshot + regenerated TS types with the new admin
diagnostics endpoint added in peanut-api-ts (surface failed notification
sends). Additive only — no existing paths change.
Hotfix port to main of peanut-ui#2438 (branch was dev-based; every
touched file is byte-identical between dev and main, so this is the
same reviewed content squashed onto main). Pairs with peanut-api-ts
hotfix/rain-poa-selfheal — deploy the API first.

When the rain rail carries the sumsub:proof_of_address next-action, the
card status screens (requires-info / rejected) render a primary
"Upload proof of address" CTA driving a dedicated lightweight flow:
resubmit action -> second SumsubKycWrapper -> inline initiation errors
-> optimistic "we received your document" banner (auto-reset when the
backend state takes over). Double-click guard; analytics tagged
source: poa-self-heal.

Full review trail on #2438.
feat(card): proof-of-address upload on stuck Rain applications
…itle

captureConsoleIntegration promotes every console.error/warn into a Sentry
event, and fetchWithSentry both captures the underlying failure AND
re-throws a ServiceUnavailableError wrapper that gets captured again by
global handlers. One API timeout could produce three Sentry events.

- sentry.utils: ignore ServiceUnavailableError (always a wrapper whose
  cause was already captured at the fetch site) — PEANUT-UI-QDJ
- fetchWithSentry: console.info instead of console.error for the raw
  fetch rejection; the explicit captures below carry the full context
- useTokenPrice: fallback log at console.info — the fallback is graceful
  and the failure is already reported — PEANUT-UI-MH5
- BadgeStatusDrawer: badge name becomes the DrawerTitle, fixing the Radix
  'DialogContent requires a DialogTitle' a11y warning — PEANUT-UI-MJS
The SKIP_REPORTING entry added in f3d8090 stopped expected 'Invalid
Invite' 400s from the setup-flow invite input flooding Sentry
(PEANUT-UI-QDR, ~2,850 events / 701 users in 14 days). Nothing pinned
that behavior: no test exercised the suppression path in fetchWithSentry
at all, so a refactor of the skip rules could silently bring the noise
back.

Cover the three contract points: /invites/validate 400 emits neither a
Sentry event nor a console.warn, a 500 on the same endpoint still
reports as an error, and unlisted endpoints still report their 400s.
Capacitor plugin calls executing inside the /crisp-proxy iframe on native
throw '"<Plugin>" plugin is not implemented on ios' because the bridge is
not injected into iframes. Pure noise, not a broken binary (PEANUT-UI-QV3:
2,211 events in 14 days from 6 users). Filter the whole message family
(StatusBar, Keyboard, App, ...) in shouldIgnoreError via a regex pattern,
and add unit coverage for the ignore rules.
Radix Dialog (via vaul) logs 'DialogContent requires a DialogTitle' for
screen-reader accessibility when a drawer opens without a title. The badge
unlock drawer on /home rendered no DrawerTitle, and captureConsole forwarded
the warning to Sentry (PEANUT-UI-MJS, ~1.7k events / 854 users in 14 days).

Add an accessibleTitle prop to DrawerContent that renders a visually hidden
DrawerTitle, use it in BadgeStatusDrawer, and cover the labelling contract
with tests.
… fields

Matching against one concatenated blob of message + exception value/type +
culprit let a pattern match across field boundaries and suppress an
unrelated event. Test each field independently; regexes now also run
against the original (non-lowercased) text with their own flags.
The /crisp-proxy iframe source of the plugin-not-implemented events was
already fixed (972f354); residual events come from stale binaries, and
a message filter would permanently mask a top-frame plugin failure on a
fresh binary — a genuinely broken build we want reported. The Sentry
issue is resolved in the dashboard instead (reopens as regressed if it
recurs).

Keep the per-field matching in shouldIgnoreError: the old concatenated
blob let a pattern match across unrelated fields and suppress legitimate
events. Tests now cover per-field matching, the cross-boundary case, and
the previously untested ignore list.
…a7f9ca)

Bumps src/content on production (main) from e3a05f7 → ca7f9ca
(peanut-content latest = mono@174e21d). Single-file submodule
pointer change. Publishes content already merged + mirrored from mono that the
dev-targeted auto-PRs never promote to main.
…202607201712

content: publish latest to production (src/content → peanut-content@ca7f9ca)
…ubstring safety

Adds root-level sentry.utils.test.ts for the SDK-config filter's alreadyReported
entry (distinct from src/utils/sentry.utils.ts, the fetchWithSentry wrapper), and
documents that substring-matching ServiceUnavailableError is safe only because it
is our internal wrapper name.
Rhino rejects bridge deposits under the route minimum (UNDER_MIN) and
parks the funds at the SDA with no auto-refund — a $2.50 withdrawal to
Ethereum (min $5) sat stranded since 07-15 with the intent COMPLETED.
Floor every crypto withdrawal at $0.50 on the amount step (red, blocking),
warn $0.50–$5 amounts that Ethereum needs $5, and hard-block chain-aware
minimums (ETH $5 / Tron $10) at review time before any charge exists.
The withdraw chain picker's NON_EVM_WITHDRAW_CHAINS entry carries
chainId 'tron', so the '728126428' map key was unreachable and Tron
silently degraded to the $0.50 default. Caught by adversarial replay
review before push.
…t tests

Adversarial review findings: (1) the review-time minimum error said 'pick
a different network' but nothing cleared it on chain change, so Review
stayed dead after following the instruction; (2) the Tron test asserted a
hand-typed id no picker record carries — now asserts the REAL registry
records; (3) the Ethereum notice read the app-wide token price on a
USD-pinned input; (4) stale 'no minimum' comment contradicted the new
floor; (5) notice copy works under the Send header too.
CodeRabbit major: the amount page's AmountInput is hard-pinned to USD,
but the min/balance gates multiplied by the app-wide selectedTokenData
price — a stale non-USD price could let $0.01 past the $0.50 floor (or
false-block valid amounts). The typed value IS the USD value; compare it
directly. Also: the Ethereum notice no longer overclaims that every
other network accepts the amount (Tron is $10).
Product call (Aleks): same-chain Arbitrum USDC withdrawals are direct
transfers — no Rhino, no minimum — and must keep parity with
send-via-link and P2P sends. Drop the blanket $0.50 amount-step floor;
warn there instead (destination unknown), and enforce the per-network
Rhino minimums at review time only for bridged/swapped routes.
Manteca asked (Facundo, 2026-07-21) that QR-payment funding stop going to the
shared deposit wallet: Argentine QRs (MercadoPago/QR3) now fund one wallet,
Pix — and eventually any non-AR QR — another. Withdrawals and claims keep
funding the legacy shared address. Backend counterpart validates per-rail and
tolerates the legacy address for stale clients — deploy the API change first.
…lection

Product call (Aleks): no yellow warning on the amount page; the
per-network Rhino minimum blocks at review, where the destination is
actually chosen.
Sessions persist for weeks, so an unlocked phone was enough to reach a
funded account. Gate the native app behind a local WebAuthn assertion on
cold start and after LOCK_AFTER_BACKGROUND_MS in the background.

The assertion is checked locally, not proven to the API: the threat is
physical device access, and the OS will not produce one without the
user's biometric or passcode. Server-side proof of freshness belongs to
step-up auth on sensitive endpoints, which is a separate change.

Fails open when there is no credential to prompt against or the platform
has no WebAuthn — a lock that cannot be lifted would strand the user.
The app ships no script-src today, so an XSS anywhere runs unconstrained
and can exfiltrate to any origin. Enforcing a guessed allow-list would
blank the app, so this ships REPORT-ONLY: it collects violations from
real traffic until the list is known complete, then gets promoted to the
enforcing header.

Reports go to Sentry via a report-uri derived from the browser DSN; the
policy still ships (without reporting) when the DSN is absent.

object-src 'none' and base-uri 'self' are added to the ENFORCING policy
now — the app embeds no plugins and sets no <base>, so neither can break
a working page.

Known-loose and to be tightened before promotion: 'unsafe-inline' and
'unsafe-eval' in script-src (Next's bootstrap and the wallet SDKs), and
a connect-src that cannot enumerate every env-driven chain RPC.
Card PAN/CVV, PIN, withdrawals and bank-account add were authorized by
the session cookie alone. They now carry an x-step-up-token proving a
WebAuthn assertion from the last five minutes.

The proof is cached for its lifetime, so a multi-step flow (approve then
prepare) costs one Face ID prompt rather than one per request, and is
dropped on logout so it can't outlive the session it belongs to.

Rain calls opt in with stepUp: true on the single rainRequest choke point
instead of threading a header through every call site.

Pairs with peanut-api-ts, where enforcement stays behind STEP_UP_ENFORCED
until a build carrying this ships.
The report URI hardcoded https and dropped everything but the last path
segment, so a path-prefixed DSN (self-hosted Sentry under a sub-path)
posted reports to an endpoint that doesn't exist, and an http DSN was
silently upgraded.

Verified against a standard ingest DSN, a path-prefixed one, and a plain
http localhost DSN.
feat(security): report-only CSP with a script-src allow-list
feat(security): prove a fresh passkey assertion on sensitive actions
…try-visibility

fix(notifications): surface native OneSignal failures in Sentry
…-routing

fix(native): route push notification taps inside the app
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 24, 2026 12:10pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 45cb984b-b9a9-4273-afef-9c7664ae95a8

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/backmerge-main-to-dev-2026-07-22

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

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6231.08 → 6263.81 (+32.73)
Findings: +19 net (+490 new, -471 resolved)

🆕 New findings (490)

  • critical complexity — src/app/(mobile-ui)/qr-pay/page.tsx — CC 305, MI 53.59, SLOC 971
  • critical complexity — src/components/Global/TokenSelector/TokenSelector.tsx — CC 126, MI 60.85, SLOC 331
  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 125, MI 57.27, SLOC 475
  • critical complexity — src/app/(mobile-ui)/withdraw/page.tsx — CC 124, MI 54.12, SLOC 334
  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 88, MI 52.06, SLOC 375
  • critical complexity — src/utils/native-routes.ts — CC 66, MI 59.68, SLOC 149
  • critical complexity — src/services/rain.ts — CC 65, MI 59.99, SLOC 264
  • critical complexity — src/hooks/useNotifications.ts — CC 55, MI 57.29, SLOC 260
  • critical complexity — src/app/(mobile-ui)/notifications/page.tsx — CC 50, MI 63.53, SLOC 154
  • high hotspot — src/app/(mobile-ui)/qr-pay/page.tsx — 89 commits, +994/-976 lines since 6 months ago
  • high structural-dup — types/api.generated.ts:421 — 83 duplicate lines / 384 tokens with types/api.generated.ts:1220
  • high structural-dup — types/api.generated.ts:431 — 74 duplicate lines / 344 tokens with types/api.generated.ts:1230
  • high structural-dup — types/api.generated.ts:214 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9646
  • high structural-dup — types/api.generated.ts:214 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9732
  • high structural-dup — types/api.generated.ts:214 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9323
  • high structural-dup — types/api.generated.ts:9268 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9420
  • high structural-dup — types/api.generated.ts:9268 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9591
  • high structural-dup — types/api.generated.ts:9268 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9849
  • high hotspot — src/app/(mobile-ui)/card/page.tsx — 44 commits, +1270/-494 lines since 6 months ago
  • high complexity — src/components/Global/AppLock/index.tsx — CC 37, MI 66.31, SLOC 96

…and 470 more.

✅ Resolved (471)

  • src/app/(mobile-ui)/qr-pay/page.tsx — CC 304, MI 53.59, SLOC 971
  • src/app/(mobile-ui)/withdraw/page.tsx — CC 125, MI 53.75, SLOC 339
  • src/components/Global/TokenSelector/TokenSelector.tsx — CC 125, MI 60.87, SLOC 331
  • src/app/(mobile-ui)/card/page.tsx — CC 106, MI 56.91, SLOC 419
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 78, MI 53.1, SLOC 335
  • src/services/rain.ts — CC 64, MI 60.26, SLOC 258
  • src/utils/native-routes.ts — CC 56, MI 60.83, SLOC 127
  • src/hooks/useNotifications.ts — CC 54, MI 57.75, SLOC 249
  • src/app/(mobile-ui)/qr-pay/page.tsx — 88 commits, +990/-974 lines since 6 months ago
  • types/api.generated.ts:346 — 83 duplicate lines / 384 tokens with types/api.generated.ts:1145
  • types/api.generated.ts:356 — 74 duplicate lines / 344 tokens with types/api.generated.ts:1155
  • types/api.generated.ts:139 — 72 duplicate lines / 269 tokens with types/api.generated.ts:9571
  • types/api.generated.ts:139 — 70 duplicate lines / 264 tokens with types/api.generated.ts:9657
  • types/api.generated.ts:139 — 67 duplicate lines / 249 tokens with types/api.generated.ts:9248
  • types/api.generated.ts:9193 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9345
  • types/api.generated.ts:9193 — 55 duplicate lines / 162 tokens with types/api.generated.ts:9516
  • types/api.generated.ts:9193 — 55 duplicate lines / 165 tokens with types/api.generated.ts:9774
  • src/app/(mobile-ui)/notifications/page.tsx — CC 49, MI 63.68, SLOC 153
  • src/app/(mobile-ui)/card/page.tsx — 43 commits, +1181/-489 lines since 6 months ago
  • src/app/actions/users.ts — CC 33, MI 53.31, SLOC 133

…and 451 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Global/AppLock/index.tsx 0.0 7.9 +7.9
src/app/(mobile-ui)/receipt/page.tsx 0.0 6.7 +6.7
src/utils/app-lock.ts 0.0 6.6 +6.6
src/services/step-up.ts 0.0 5.7 +5.7
src/hooks/useNativePlugins.ts 8.6 10.1 +1.4
src/app/(mobile-ui)/withdraw/crypto/page.tsx 17.1 18.4 +1.4
src/app/(mobile-ui)/card/page.tsx 16.1 17.4 +1.3
src/utils/native-routes.ts 8.0 8.9 +0.9
src/utils/cross-chain-fee.utils.ts 4.2 4.8 +0.7
src/components/Withdraw/views/Initial.withdraw.view.tsx 10.1 10.7 +0.7
src/services/onesignal/native.adapter.ts 5.2 5.8 +0.6
src/app/(mobile-ui)/withdraw/page.tsx 15.2 15.7 +0.6

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2127 ran, 0 failed, 0 skipped, 36.6s

📊 Coverage (unit)

metric %
statements 60.8%
branches 44.3%
functions 49.8%
lines 61.1%
⏱ 10 slowest test cases
time test
4.0s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/demo-balance.test.ts › resetDemoBalance refills and restarts the TTL window
0.2s src/utils/__tests__/demo-balance.test.ts › keeps a spent-down balance across a cold start within the TTL
0.2s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

abalinda and others added 15 commits July 23, 2026 11:53
Selecting USDT-Tron / USDT+USDC-Solana in the withdraw token drawer updated
context but the collapsed button resolved its label via areEvmAddressesEqual,
which is always false for base58 addresses — so the button kept reading
'Select a token' and the selection looked like it never happened. Fall back
to a case-insensitive literal match (keeping areEvmAddressesEqual for its
native-proxy aliasing on EVM tokens).
router.push during render is a React violation ('Cannot update Router while
rendering WithdrawCryptoPage') — Next 16's dev overlay hard-errors on any
direct entry or refresh of /withdraw/crypto, which blocked QA of the
non-EVM withdraw flow. Same behavior, one frame later, via useEffect.
…ithdraws

Same-chain crypto withdraws funded from Rain card collateral succeeded
on-chain but never completed their charge intent — the page skipped
recordPayment and never passed chargeId, so the charge rotted PENDING
until the reaper failed it. History hides both rows (phantom +
abandoned-draft filters), so users watched their balance drop with no
Activity entry (98 charges / 55 users in 14 days).

Mirror the direct-send contract: pass chargeDetails.uuid into sendMoney
(the backend uses the charge as the prep and settles it in /submit's
trusted completion) and always recordPayment — mixed spends rely on it,
collateral-only re-enters trusted completion idempotently as the
recovery net. recordPayment failures after a collateral-routed
same-chain spend degrade to the success view instead of a false
'failed': the funds have already moved.

Also refresh the three stale hook/service comments that instructed
callers to SKIP recordPayment on collateral-only — the guidance that
seeded this bug.
Adversarial review find: on a receipt-wait timeout the kernel path
returns only the userOp hash. Feeding that to the validator for an
untagged mixed charge can never match a tx — retry exhaustion would
flip the successful withdrawal to FAILED, worse than the pre-fix stuck
PENDING. Skip the record with a Sentry breadcrumb in that case
(collateral-only stays safe: backend-broadcast tx hash + trusted-path
settlement). Also pin cross-chain rethrow + this skip in tests.
…ce + isDiffToken routing

Structured review pass: the bug-seeding 'skip recordPayment' guidance
survived in two more comments (useSpendBundle prepare call-site,
useWallet sendMoney return doc) — the exact rot this PR exists to purge.
Also close the two coverage gaps a scoping regression could slip
through: mixed same-chain tolerance and token-boundary-only routing.
Base58 is case-significant and both operands come from the same registry —
a case-insensitive compare could only mask an upstream case-corruption bug,
never fix a legitimate mismatch. areEvmAddressesEqual keeps covering EVM
checksum-case variance and native-proxy aliasing.
…charge-completion-v2

fix(withdraw): complete the user-facing charge on collateral-routed crypto withdraws
…n-evm-label

fix(withdraw): show non-EVM (Tron/Solana) token selection in the selector button
Apple 5.1.1 rejected the iOS app because the seller name (Squirrel Labs
Ltd) has no visible association with the Peanut brand. Terms already say
'trading as Peanut'; this puts the same disclosure on every landing page
footer so the link is one click closer for reviewers (and standard UK
practice anyway).
hotfix(footer): Squirrel Labs Ltd legal-entity line on landing footer
@jjramirezn

Copy link
Copy Markdown
Contributor

Updated the backmerge for the sp-152 release cut:

🤖 Generated with Claude Code

@jjramirezn
jjramirezn merged commit 01a7c8d into dev Jul 24, 2026
18 of 20 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.

6 participants