Skip to content

chore: back-merge main → dev (pre Sprint 157 release) - #2803

Merged
jjramirezn merged 27 commits into
devfrom
chore/backmerge-main-into-dev-20260824
Aug 24, 2026
Merged

chore: back-merge main → dev (pre Sprint 157 release)#2803
jjramirezn merged 27 commits into
devfrom
chore/backmerge-main-into-dev-20260824

Conversation

@jjramirezn

Copy link
Copy Markdown
Contributor

Back-merge of main into dev ahead of the Sprint 157 prod release, so the dev→main release PR is a clean superset.

Carries the content-submodule bumps, SEO/perf landing hotfixes, #2779 (userOpHash), #2776, #2783 (stray pages). Clean merge — no conflicts.

Part of the Sprint 157 release prep.

jjramirezn and others added 27 commits August 20, 2026 22:57
…try (TASK-21147)

Five send flows fell back to txResult.userOpHash when the receipt wait
gave up. A userOp hash is bundler-internal — eth_getTransactionReceipt
can never resolve it, so backend validation burned its full retry window
and marked SUCCESSFUL payments FAILED (11 in the 28-day prod baseline).

- useZeroDev: when waitForUserOperationReceipt fails, re-poll
  getUserOperationReceipt (6x3s) before giving up — the tx usually
  landed seconds later; rescued receipts recorded to PostHog.
- resolveSettledTxHash: shared resolution for all 5 sites — receipt
  hash first, coordinator txHash next, userOp hash only as measured
  last resort (send_txhash_fallback event per use).
- direct-send flow: send_latency_breakdown event splits the client leg
  (charge create / sign+bundler+receipt / record payment) — the p50
  7.9s block prod DB timing cannot attribute.
- Receipt rescue is a capped retry of viem's own wait (15s wall clock,
  500ms polling) instead of a hand-rolled 3s-step poller; skipped
  entirely after a genuinely exhausted 120s timeout; checks the userOp
  success flag so a mined-but-REVERTED op is never rescued into a
  recordable hash (fail-open risk); carries elapsed_ms + chain_id.
- mixedEphemeralSpend gets the same rescue (its stamp path was the
  remaining userOpHash-as-txHash producer); a rescued reverted op takes
  the ok:false exit so the passkey fallback still runs.
- resolveSettledTxHash applied to the 4 sites the first pass missed
  (withdraw/crypto x2, useSpendBundle stamps x2, bank withdraw) — the
  exact pre-fix ordering bug lived on in withdraw/crypto.
- Demo mode no longer pollutes send_txhash_fallback / latency metrics;
  an all-empty txResult throws instead of recording undefined; latency
  capture is throw-guarded; stale fallback comments removed; timing
  consts scoped inside the try.
The normal return exposed a reverted userOp's successful EntryPoint
bundle receipt — downstream flows recorded a transfer that never moved
funds as a successful payment. Same guard the rescue path already has.
…ions only

/careers was a 30-word interstitial: a mascot, one sentence, and a link out
to Notion. No <h1>, no footer, no Crisp, and metadata still selling 'Instant
Global P2P Payments' from the protocol era. The footer's Jobs link bypassed
it entirely, pointing at a second URL for the same Notion page.

The page now carries its own content — what Peanut is, how the work works,
and the open roles — sourced from content/press/en.md so nothing is invented.
Notion is reached only by 'Apply on Notion'. The footer Jobs link now points
at /careers instead of routing around it.

Uses the public *.notion.site URL; the old www.notion.so workspace URL
bounced logged-out visitors to a login.
shhhhh.faq.q6 read 'We're rolling out to select regions first and adding more
as our partners' coverage expands. Eligibility is shown during signup.' That is
issuance and availability framing; product/card.md requires public card copy to
describe acceptance only and never name countries or promise eligibility.

Now answers where the card can be used — accepted wherever Visa is accepted —
and sends people to the door for their own account status.

es-AR gains a q6 answer override for the voseo imperatives, since es-419 uses
tuteo here. The key exists in es-419, so the delta stays a strict subset.

The $10 reward claims this FAQ used to carry were already removed on dev.
Three marketing routes that carried no unique content, all now permanent
redirects. (/lp/card was retired separately on dev by #2760.)

- /exchange -> /. Rendered the same NoFees component already on the homepage
  plus a footer, and shipped an empty client shell: 'ZERO TRANSFER FEES',
  'no hidden fees' and 'See the markup on' all return zero matches in the live
  server HTML, while it sat in the sitemap at priority 0.7. Its metadata title
  promised 'Live Exchange Rates' while its H1 said 'ZERO TRANSFER FEES'.
  NoFees itself is untouched and still used by the homepage and /quests.
- /:locale/blog -> /:locale/content. Byte-identical to /content with
  fixedType='blog', which /content's own Blog chip already produces. Only the
  index is deleted; all 21 posts under blog/[slug] survive.
- /:locale/team -> /:locale/press. /press already renders the same two members
  from the same content file, with real photos.

Fallout cleaned up in the same pass: robots.ts no longer allows a route that
is now a 308, and the blog post breadcrumb — which feeds a BreadcrumbList
JSON-LD — points at /content?type=blog instead of naming a redirecting URL.
That form matches how /compare and /use-cases already link back, so the crumb
still reads 'Blog'.

Also drops the now-dead 'team' entry from ROUTE_SLUGS and refreshes three
comments that cited the retired routes as live.
…root identity

- /crisp-proxy, /kyc/success and /maintenance defined no metadata, so all
  three inherited the root layout's canonical: '/' and declared the homepage
  as their canonical while serving index, follow. Each is a client component,
  so metadata cannot live in the page — added sibling layouts carrying a
  self-canonical plus robots noindex, the pattern /exchange already used.
- Root title was 'Peanut - Instant Global P2P Payments in Digital Dollars',
  protocol-era framing inherited by every page without an override. The root
  description said the same thing. Both now describe the money app.
- The X handle @peanutprotocol appeared in six places while the footer links
  x.com/joinpeanut. All six now say @joinpeanut, including the Organization
  JSON-LD sameAs and llms-full.txt.
- Adds the five card legal pages to the sitemap. They return 200 with index,
  follow and were listed nowhere. English only: the localized copies serve
  English and already canonicalize to /en, so listing all twenty would create
  fifteen duplicates competing with their own canonical.
…ate, localize /press

- /en/content and /en/blog emitted zero article links in server HTML while the
  sitemap listed 21 posts. ContentLanding is a client component read through
  nuqs, so Next prerenders the Suspense fallback — and the fallback was a
  pulsing skeleton. The list rendering is now an exported ContentLinkList that
  touches no URL state, and the fallback renders it, so the links exist in the
  prerendered HTML. Interactive filtering is unchanged.
- Content cards rendered the raw frontmatter title, leaking the SEO suffix into
  the UI ('Lynn Pham: The Brand Girl | Peanut'). Stripped at render time only;
  sitemap and metadata still read the raw value.
- /en/stories linked to /en/stories/index, a live second copy of the hub with a
  self-referencing canonical and different hero copy. The page now applies the
  same slug === 'index' guard the sitemap already had.
- /press rendered all six section headings as hardcoded English on a [locale]
  route. They now come from the marketing catalogs, translated in all four.
- /press absorbs the team data: its Organization JSON-LD gains the member[]
  array that only /team used to emit, so deleting /team does not drop the
  site's only Person schema.

Drops the now-unused fixedType prop and the teamTitle/teamSubtitle keys, both
orphaned by the routes retired in this branch.
…nce and the waitlist

The ragdoll had no prefers-reduced-motion handling at all. It now follows the
same shape as confetti.ts — matchMedia read live per mount — and degrades
rather than disappearing: the world is stepped off-screen until the peanut
settles, then painted once, with no RAF loop and no pointer listeners.

Two new homes, both non-scrolling panes with a definite height:

- /maintenance replaces a static crying-peanut image. The copy was the worst
  voice on the site ('We are currently going through maintenance... Sorry for
  the inconvenience') and a button labelled 'Contact Support?'. Rewritten to
  match the 404's register.
- The waitlist jail pane, gated on step === 'jail' so plain /invite — a signup
  conversion surface — does not get a draggable toy next to its CTA. The
  illustration pane moves from min-h to a definite h-[55dvh]; min-h gives
  h-full nothing to resolve against, so the canvas would have silently
  collapsed to zero height while the loop kept running.

Fixes a real bug found while placing it: the (mobile-ui) layout registers
pull-to-refresh on document before the waitlist early-return, and its
#scrollable-content guard is absent on that screen, so the handler fell
through to true. Dragging the peanut down ~160px would have reloaded the page
and wiped the user's typed invite code. The host now swallows touchstart — a
native listener, because React hydrates on document and a synthetic
stopPropagation would not stop a same-target sibling.

startRagdoll now runs in a try/catch: it throws synchronously on a failed 2D
context, which would white-screen /maintenance, the one page whose job is to
work when nothing else does.

Also retires the stale 'triple-click Rewards title' comments — that easter egg
was deliberately cut in 347757d and never shipped.
The footer nav now points at /careers rather than the Notion board, so the
label should name the page it opens. Renames footerJobs to footerCareers and
sets the English value to 'Careers' (it read 'Career'). pt-BR moves from
'Vagas' — openings — to 'Carreiras'; es keeps 'Empleo', which already reads
correctly.

Note the SEO footer's own jobs entry, which still carries a Notion href in the
content manifest, is already suppressed by RESOURCES_MOVED_ELSEWHERE, so it
renders nothing and needs no change here.
The hub delink hid the duplicate but generateStaticParams still emitted
/{locale}/stories/index. Filter it out so dynamicParams=false 404s it.
chore(marketing): consolidate the stray pages, and give /careers a real one
- Rescued-but-REVERTED ops now THROW from the rescue path too (they
  returned success-shaped results and flowed into the userOpHash
  fallback + success screen).
- returnRevertedReceipt opt-in restores the kernel-migration contract:
  the noop wiring sites receive the bundle receipt on revert so
  ensureRootValidatorMigrated verifies on-chain truth instead of
  hard-failing a healthy wallet.
- Rescue extracted to shared rescueUserOpReceipt (used by useZeroDev +
  mixedEphemeralSpend) with telemetry on BOTH paths + 4 tests.
roboto-flex-bold.ttf was the full 13-axis Roboto Flex variable font —
1.6 MB on disk, 964 KB over the wire, on every page. The CSS only ever
drives the wght axis, so the woff2 is instanced down to wght 100–1000
(all other axes pinned to their defaults — rendering is identical) and
subsetted to latin + latin-ext plus the arrows/math symbols the UI uses
(fonttools). 964 KB -> 53 KB transfer.

knerd-outline/knerd-filled get the same subset treatment (113 KB -> 73 KB
combined). Their .ttf sources stay: the OG image routes read them at
runtime (satori needs TTF). roboto-flex-bold.ttf had no other reader, so
it's gone.

Lighthouse (local prod build): font transfer 1,244 KB -> 311 KB,
total page weight -926 KiB.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wrRmcBRas8FdBr4n8btrL
…usel

The whistling mascot is the mobile LCP element, but without `priority`
next/image emits loading="lazy" — Lighthouse measured a 7s image load
delay inside a 19.5s LCP. With priority the image is preloaded and the
load delay drops to ~0.8s (LCP 19.5s -> 6.9–14.7s on a throttled run;
the remainder is main-thread render delay from the app-wide JS bundle,
not the image).

TweetCarousel statically imported the testimonials manifest (~64 KB of
JSON) into the landing chunk while rendering far below the fold — moved
behind next/dynamic (SSR stays on, so crawlers still see the tweets).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wrRmcBRas8FdBr4n8btrL
Lighthouse a11y/SEO fixes, no visual change:
- RegulatedRails wrapped its "Learn more" link in an h6 under an h1 —
  it's a link line, not a heading, so it's a p now; the link also gets
  an sr-only suffix (the section heading) so it isn't a bare generic
  "Learn more" for screen readers or the SEO link-text audit.
- securityBuiltIn's section heading sat as an h1 directly above h3
  feature titles (heading-order skip) — h2 now.
- PartnerLockup's aria-label replaced the visible "Learn more" text
  instead of containing it (WCAG 2.5.3 label-in-name) — the visible
  label now prefixes the description.

Landing a11y 93 -> 95, SEO 92 -> 100.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wrRmcBRas8FdBr4n8btrL
…images

Next 16 deprecates next/image's `priority` in favor of `preload`
(same semantics: preload || priority disables lazy loading and emits
the preload link). Flagged by review on #2788.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wrRmcBRas8FdBr4n8btrL
…phash

fix: never record a userOp hash as a tx hash + client-leg send telemetry (TASK-21147)
…0821-191753

content: publish latest to production (src/content → peanut-content@4eb2c3f)
…-speed-mngr5x

perf(landing): fix lazy-loaded LCP, ship subsetted woff2 fonts, a11y/SEO audit fixes
…cation-error-uzgmxr

fix(claim): stop blaming the sender, and offer a retry when one would work
…0824-100955

content: publish latest to production (src/content → peanut-content@26a4263)
…0824-115455

content: publish latest to production (src/content → peanut-content@41ba07b)
@vercel

vercel Bot commented Aug 24, 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 24, 2026 12:55pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 24, 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: 0805c6e0-64a2-4843-8d35-54a8c70db83b

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

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

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7317.77 → 7334.68 (+16.91)
Findings: -9 net (+166 new, -175 resolved)

🆕 New findings (166)

  • critical complexity — src/components/PeanutRagdoll/ragdoll.ts — CC 140, MI 51.34, SLOC 779
  • critical complexity — src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 106, MI 49.12, SLOC 462
  • critical complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 105, MI 53.1, SLOC 394
  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 95, MI 50.32, SLOC 426
  • critical complexity — src/components/Invites/JoinWaitlistPage.tsx — CC 84, MI 56.03, SLOC 293
  • critical complexity — src/components/LandingPage/LandingPageClient.tsx — CC 55, MI 56.28, SLOC 221
  • high hotspot — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 60 commits, +587/-299 lines since 6 months ago
  • high hotspot — src/constants/analytics.consts.ts — 51 commits, +419/-23 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/crypto/page.tsx — 45 commits, +539/-274 lines since 6 months ago
  • high hotspot — src/hooks/useZeroDev.ts — 44 commits, +474/-279 lines since 6 months ago
  • high complexity — src/app/[locale]/(marketing)/press/page.tsx — CC 42, MI 61.59, SLOC 97
  • high complexity — src/features/payments/flows/contribute-pot/useContributePotFlow.ts — CC 39, MI 50.89, SLOC 238
  • high hotspot — src/components/LandingPage/hero.tsx — 38 commits, +283/-178 lines since 6 months ago
  • high complexity — src/hooks/wallet/useGrantSessionKey.ts — CC 35, MI 52.85, SLOC 294
  • high hotspot — src/hooks/wallet/useSpendBundle.ts — 31 commits, +1032/-611 lines since 6 months ago
  • high method-complexity — src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:58 — WithdrawBankPage CC 30 SLOC 166
  • high hotspot — src/components/Invites/JoinWaitlistPage.tsx — 30 commits, +463/-204 lines since 6 months ago
  • high complexity — src/app/sitemap.ts — CC 29, MI 47.39, SLOC 228
  • high complexity — src/features/payments/flows/direct-send/useDirectSendFlow.ts — CC 26, MI 48.61, SLOC 206
  • high complexity — src/hooks/wallet/useSpendBundle.ts — CC 23, MI 46.12, SLOC 269

…and 146 more.

✅ Resolved (175)

  • src/components/PeanutRagdoll/ragdoll.ts — CC 131, MI 51.47, SLOC 753
  • src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 106, MI 49.09, SLOC 458
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — CC 105, MI 53.21, SLOC 390
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 95, MI 50.49, SLOC 419
  • src/components/Invites/JoinWaitlistPage.tsx — CC 84, MI 56.04, SLOC 293
  • src/components/LandingPage/LandingPageClient.tsx — CC 54, MI 55.86, SLOC 216
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx — 59 commits, +585/-298 lines since 6 months ago
  • src/constants/analytics.consts.ts — 48 commits, +393/-14 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — 44 commits, +535/-272 lines since 6 months ago
  • src/app/[locale]/(marketing)/press/page.tsx — CC 40, MI 61.93, SLOC 87
  • src/hooks/useZeroDev.ts — 40 commits, +406/-234 lines since 6 months ago
  • src/features/payments/flows/contribute-pot/useContributePotFlow.ts — CC 39, MI 50.89, SLOC 236
  • src/components/LandingPage/hero.tsx — 36 commits, +274/-175 lines since 6 months ago
  • src/hooks/wallet/useGrantSessionKey.ts — CC 35, MI 52.89, SLOC 293
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:57 — WithdrawBankPage CC 30 SLOC 166
  • src/components/Home/ActivationCTAs.tsx — 30 commits, +668/-250 lines since 6 months ago
  • src/components/Invites/InvitesPage.tsx — 30 commits, +555/-348 lines since 6 months ago
  • src/app/sitemap.ts — CC 26, MI 43.05, SLOC 219
  • src/features/payments/flows/direct-send/useDirectSendFlow.ts — CC 25, MI 49.93, SLOC 184
  • src/hooks/wallet/useSpendBundle.ts — CC 23, MI 46.39, SLOC 262

…and 155 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/settled-tx-hash.utils.ts 0.0 5.6 +5.6
src/utils/userop-rescue.utils.ts 0.0 5.2 +5.2
src/components/Jobs/openRoles.ts 0.0 3.3 +3.3
src/app/crisp-proxy/layout.tsx 0.0 3.0 +3.0
src/app/kyc/success/layout.tsx 0.0 3.0 +3.0
src/app/maintenance/layout.tsx 0.0 3.0 +3.0
src/components/Jobs/Careers.tsx 0.0 2.4 +2.4
src/components/Jobs/RoleCard.tsx 0.0 1.8 +1.8
src/components/PeanutRagdoll/index.tsx 4.2 5.4 +1.2
src/features/payments/flows/direct-send/useDirectSendFlow.ts 11.6 12.6 +1.0
src/hooks/wallet/mixedEphemeralSpend.ts 9.7 10.3 +0.6
src/hooks/useZeroDev.ts 8.5 9.1 +0.5
src/app/sitemap.ts 11.5 10.6 -0.9
src/components/Jobs/index.tsx 1.7 0.0 -1.7
src/app/exchange/page.tsx 2.1 0.0 -2.1
src/app/exchange/layout.tsx 2.5 0.0 -2.5
src/app/[locale]/(marketing)/blog/page.tsx 5.5 0.0 -5.5
src/app/[locale]/(marketing)/team/page.tsx 7.4 0.0 -7.4

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3472 ran, 0 failed, 0 skipped, 59.9s

📊 Coverage (unit)

metric %
statements 68.3%
branches 53.6%
functions 59.4%
lines 69.1%
⏱ 10 slowest test cases
time test
3.9s 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/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.7s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
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 › retries configuration on the next open after a failure
0.4s src/utils/__tests__/crisp.test.ts › configures once across repeated support opens
0.4s src/utils/__tests__/crisp.test.ts › resets the native session on logout once support has been opened
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@jjramirezn
jjramirezn merged commit f5dfbc5 into dev Aug 24, 2026
20 checks passed
@jjramirezn
jjramirezn deleted the chore/backmerge-main-into-dev-20260824 branch August 24, 2026 13:00
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.

5 participants