chore: back-merge main → dev (2026-07-22)#2477
Conversation
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
…to-dev-2026-07-22
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Code-analysis diffPainscore total: 6231.08 → 6263.81 (+32.73) 🆕 New findings (490)
…and 470 more. ✅ Resolved (471)
…and 451 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
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
…to-dev-2026-07-22
|
Updated the backmerge for the sp-152 release cut:
🤖 Generated with Claude Code |
Back-merge of all hotfixes/PRs currently on
mainbut not yet ondev. Can't PR main→dev directly, so this branch isdev+origin/mainmerged 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