Skip to content

chore: back-merge main → dev (pre release 2026-08-18) - #2724

Merged
Hugo0 merged 67 commits into
devfrom
chore/backmerge-main-into-dev-20260818
Aug 18, 2026
Merged

chore: back-merge main → dev (pre release 2026-08-18)#2724
Hugo0 merged 67 commits into
devfrom
chore/backmerge-main-into-dev-20260818

Conversation

@jjramirezn

Copy link
Copy Markdown
Contributor

Back-merge of main into dev ahead of today's prod release, so the dev→main release PR (#2642) is a clean superset.

Brings in the badge-artwork hotfixes (Splitter, Surf's Up, Tron, ENS, Açaí), the Crisp proxy postMessage handshake + token hardening, the FX card-markup UI, QR base58 scanner fixes, user-interview CTA, and content-submodule bumps.

Conflicts resolved (field-level union, no blanket strategy):

  • src/components/Global/SupportDrawer/index.tsx — main's crisp-proxy init-handshake imports + dev's notificationsApi (support unread badge). Both features' code coexists in the body.
  • src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx — both sides added a describe block in the same spot; kept both (18/18 pass locally).
  • src/constants/general.consts.ts — took main's USER_INTERVIEW_CAL_URL (newer hotfix: preselects hosts + 15 min).
  • src/content submodule — took main's pointer 9101046 (fast-forward descendant of dev's 2a1c593).

Supersedes stale back-merge PRs #2688 and the bot sync PR #2713.

Hugo0 and others added 30 commits August 9, 2026 19:53
#2647 fixed this defect at useZeroDev.ts:222 but missed its sibling here, so
half the campaign failures are still undiagnosable in production.

Sentry's console integration serializes each console argument. Passing an
object holding an array of claims lands in the issue as the literal
"[object Object]" — so the two facts the warning exists to carry, which
campaign and why, were both unreadable. PEANUT-UI-SJC is a live example from
today; the registration-side sibling now reads "utm:pix=unknown" instead.

Pre-join the pairs into one string, exactly as useZeroDev.ts does. The message
stays constant so Sentry keeps grouping these into a single issue rather than
opening one per campaign combination.

This matters right now: three live marketing tags (brazil, pix,
faster-payments) currently resolve to no campaign, and the invite page was the
surface that could not say which.

Test pins the payload is a string, names both campaigns and reasons, and pins
the message constant. Verified non-vacuous: reverting to the object payload
fails it, restoring the join passes.
Ask our most active users for a 15-min founder call (TASK-21189).
The cohort lives in the PostHog flag user-interviews-invite (release
condition on username), never in code - list edits and the kill
switch are one click in PostHog and no usernames ship in the bundle.
The card leads the carousel for targeted users, opens the shared
cal.com event via the Capacitor-safe helper, and inherits the
standard 7-day X-dismissal. Temporary campaign: delete the card,
flag, and i18n keys when it ends.
…n-payload

fix(badges): make the invite-page campaign warning readable in Sentry
…rdown checklist

Applied from the automated review pass: (1) await openExternalUrl so a
native Browser.open failure surfaces in CarouselCTA's onClick catch
instead of a detached rejection; (2) the analytics comment no longer
claims flag exposures equal impressions (they fire per evaluation,
prod-only, dismissed users included) and points readers at cal.com
bookings + a $host filter; (3) the delete-together checklist now names
the dev/home-ctas preview entry so teardown removes all three copies.
The committed aleks-n-hugo/15min event 404s (CodeRabbit catch, verified).
Replaced with the hugo0+abalinda dynamic group link, which resolves and
books both hosts' mutual availability.
Aleks isn't a founder; only Hugo is. All four locales, the dev preview,
and the code comments now say team.
feat(home): flag-gated user-interview invite card in the carousel
The shared icon container is size-8, and the Tailwind preflight img
max-width clamps any larger logo back to 32px - so the size bump rides
the existing iconContainerClassName prop (tailwind-merge lets size-11
override size-8). The asset already animates: it is the animated webp,
which Next serves untouched (ANIM chunk verified through /_next/image).
ui: render the waving mascot at 44px (follow-up to #2654)
…end copies

The card-vs-Peanut markup was modeled twice in the frontend — once as a server
action, once copied into the merchant page with a header saying to delete it
when #2108 landed. #2108 landed. Both computed their own dolarapi call and
their own issuer fee, and neither could agree with the rate on screen.

The hook keeps its signature and its never-fails contract: a backend outage
falls back to the same static table as before, so a frontend deploy that
precedes the backend degrades to today's numbers instead of blanking a
surface.
…compute

Also splits the two failure kinds: a 404 is the backend proving there is no
comparison to publish, so the row is hidden rather than filled with the static
assumption, which exists for an unreachable backend.
Review found the distinction collapsing at four points, each publishing a
saving claim on evidence there is none:

- a well-formed zero markup threw, so the hook fell back to the static 9.13%
- an out-of-band locked-price recompute silently returned the market markup,
  still tagged live — the exact failure the recompute exists to prevent
- the merchant page and the post-card-spend nudge both turned the hook's null
  into the static table, striking through every menu price by 9.13%

fetchCardMarkup now returns null for 'the backend published no comparison' and
throws only when the response could not be obtained or trusted. Also bounds the
live observation age client-side, formats the CompareSavings date in UTC (a
hydration mismatch on statically generated pages for any viewer west of
Greenwich), rejects a negative percent instead of publishing it as a range, and
formats amounts in the page locale.
…0811-100504

content: publish latest to production (src/content → peanut-content@062cdba)
…dates

Both CodeRabbit findings are the same defect class as the last round — a
literal check that misses an equivalent representation and falls through to the
static claim.

The wire pattern admits "0.0" and "0.00", not just "0". Matching the text
sent those down the invalid path, so a backend saying 'no gap to show' produced
a 9.13% saving claim. The check now compares the parsed value.

new Date('2026-02-30') is 2 March, so a typo in a hand-authored MDX date would
publish a verification date that never happened. verifiedAt is now round-tripped
against an exact YYYY-MM-DD.
…0811-102159

content: publish latest to production (src/content → peanut-content@c708cdd)
…0811-103319

content: publish latest to production (src/content → peanut-content@5954b68)
feat(fx): consume the shared card-markup endpoint, delete both frontend copies
…0811-110227

content: publish latest to production (src/content → peanut-content@2a1c593)
…ient-side

The token was computed in the browser as SHA-256 of a salt that ships in the
client bundle plus the userId, both public. Anyone could reproduce any user's
token and open their support conversation to read it and post as them.

Fetch it from the new authenticated GET /user/crisp-token instead, which
derives it server-side with a secret the browser never sees and issues it only
to the authenticated caller. Same undefined-until-resolved contract and cache,
so SupportDrawer's token gate is unchanged.

Requires the peanut-api-ts endpoint to be deployed first.
…sient failures

Address code review of the client hook:
- Reset the token to the current user's cached value (or undefined) before
  fetching, so an account switch never serves the previous user's token while
  the new one loads (cross-user exposure window).
- Verify the userId the route echoes matches the account we fetch for, so a
  stale auth bearer can't bind the widget to another user.
- Retry the fetch a few times, so a transient blip doesn't strand the support
  drawer's loading gate for the whole session.
Companion to peanut-api-ts#1326, which deletes GET /points/leaderboard
and GET /points/time-leaderboard — the unauthenticated ranked roster
the 2026-08-10 probing campaign used as its targeting oracle. This
removes the only consumer (the /dev/leaderboard page), the service
method, the demo mock, and the leaderboardRank field no component
ever rendered.
Found by code review — the untyped literal kept advertising a field
the API stopped sending.
…derboard

chore: delete the points leaderboard consumers (postmortem F2)
Anthropomorphic açaí bowl sticker, matching Peanut's badge house
style. Paired with the ACAI_POWERED backend catalog entry (waitlist
skip, capped campaign, expires 2026-08-17).
…r-side

fix: fetch Crisp support token from the API instead of deriving it client-side
…essage handshake

The crisp-proxy iframe URL carried email, legal name, userId, wallet and
provider links, plus the Crisp session token (a bearer credential after the
server-side token change). A query string leaks into Vercel logs, browser
history, Referer headers, and the $current_url of every analytics event
fired from the iframe — confirmed stored in PostHog. Postmortem F5,
TASK-21353.

The proxy now asks its parent for the init payload (CRISP_PROXY_REQUEST_INIT)
and boots Crisp only when the reply lands, so the parent can never post
before the iframe listens — same reliability the URL transport was built
for, with nothing identifying in the URL.
0xkkonrad and others added 20 commits August 13, 2026 18:24
The ENS badge is earned by paying at an ENS name, so the art has to be in
public/ before the API can award it: badge.utils.test.ts asserts every path
the backend catalog declares resolves to a real file, and the API's
check-badge-catalog.ts refuses a manifest that drifts from the catalog. Ship
the UI half first and merge order stops mattering.

iconUrl is not stated in the catalog entry — it falls out of the
/badges/${code.toLowerCase()}.svg default — so the manifest line is generated,
not chosen here.

Name and description stay backend-owned. The share line is the one piece of
badge copy the frontend owns, because it is the user speaking on their own
share card, not the catalog describing the badge in third person.
…n be earned

The API half of the ENS badge has been live and inert. It awards at settlement
from a name recorded on the charge intent, and we never recorded one: the app
resolves ENS in the browser and posts the resolved address, so by the time a
charge exists the name the payer typed is gone. Nothing on the server could
tell `/vitalik.eth` apart from `/0xd8dA…`.

So the charge now carries `requestProps.recipientEnsName`. The server treats it
as a hint, not evidence — it re-resolves the name at payment completion and
requires it to still point at the address the payment actually paid — which is
why sending it is safe, and why sending the *right* one is what matters.

The filter lives in createCharge rather than at each call site, so no caller has
to remember the rule: pass the raw identifier, and only a real name survives.
A name has a dot (`vitalik.eth`, `alice.peanut.me` — a Peanut name is an ENS
name); a bare handle `alice` and a `0x…` address do not, and are dropped. The
predicate is the existing validateEnsName/ENS_NAME_REGEX, not a new one.

Two flows carry a recipient the payer actually typed, and both now pass it:
the semantic-request flow (`/vitalik.eth`), and the crypto withdraw, where
withdrawing to a name is still paying at one — guarded there on the name still
matching the address being paid, because that input re-resolves per destination
chain and a stale pairing would describe a different address.

Direct send resolves a Peanut username to a wallet and has no name to send;
contribute-pot takes its recipient from the stored request, not from the payer;
requestByUsername's recipient is the requester's own address. None of them get
the field.

api.openapi.json is a captured snapshot and the API's own copy has not been
refreshed for this field, so the three /charges request bodies were patched by
hand to mirror the deployed TypeBox schema; api.generated.ts is regenerated
from it with gen:api, unedited.
The badge is earned by paying at a name AND by being paid at yours, but
the line only described the payer, so half of everyone who shared it was
saying something they had not done.
The check existed so a stale name never reached a server that would
re-resolve it; the server no longer re-resolves anything, and
`recipientEnsName` is read for one purpose — awarding a badge that grants
nothing. `validateEnsName` stays as the single filter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The /shhhhh landing page showed 3 of the 12 badges that skip the card
queue. The API's badge registry (SKIP_BADGE_CODES) is the source of
truth; the page now shows all of them. NITA is labeled 'Nita' because
'Nita's Recommendation' clips on the 375px tile.
Hand-copied /badges/* paths duplicate badge-assets.json and drift when
art is re-cut. getBadgeIcon() is the accessor every other badge surface
uses; it also gives fallback art for free.
Twelve tiles made door 01 four rows tall against door 02's two
paragraphs. Collapsed shows the original 3 with '+9 more'; door 02 no
longer stretches to match door 01's height (md:self-start).
fix(shhhhh): list the full waitlist-skip badge set
Art for the TRON catalog entry in the sibling API branch: the Tron mark drawn
as the kawaii character, matching how the Arbitrum badge treats its own chain
logo. Manifest regenerated from the API catalog rather than hand-edited.
The first pass invented the logo from memory and produced a triangle with a
letter T inside it, which is not the Tron mark. Regenerated against
src/assets/chains/tron.svg, our own Tron chain icon: a three-sided pyramid
seen from above, three facets radiating from an off-centre junction, apex
down, notch on the upper-right edge.
The wireframe reads best but the navy coin swallowed it: black pupils on dark
navy, seen through hollow facets, are invisible at the ~64px grid size the
badge list actually uses. Coin is now flat yellow and the eyes are sized to
fill the large left facet.
Owner pick out of the six light-coin wireframes. Copy is unchanged, so the
API catalog entry needs no edit.
…anager

The recipientEnsName doc promised a server-side re-resolve that
peanut-api-ts 2f570b02 deleted — validateEnsName at the call sites is
the only filter left, so the comment now says so.

The withdraw page also validated the raw input while useChargeManager
validates trim().toLowerCase(); a padded name failed validation there
and silently earned no badge. Both call paths now normalize the same
way.
feat(badges): add Surf's Up artwork and share line
…0817-113843

content: publish latest to production (src/content → peanut-content@9101046)
The Peanut Split glove-hand mark as a house-style sticker (badge-draft
pipeline, generated against the real logo, deliberately no face). Asset
manifest regenerated from the api catalog; pairs with the peanut-api-ts
SPLITTER registry PR.
Restore the badges-recraft output framing (viewBox 0 0 2048 2048, 1024px,
preserveAspectRatio none) instead of the hand-cropped square — the tight
crop rendered Splitter oversized next to sibling badges in grids. Only
the brand pink is snapped to #FF90E8.
feat(badges): Splitter artwork + share line
…into-dev-20260818

# Conflicts:
#	src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx
#	src/components/Global/SupportDrawer/index.tsx
#	src/constants/general.consts.ts
#	src/content
@vercel

vercel Bot commented Aug 18, 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 18, 2026 11:21am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 18, 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: 97f25f59-8eef-49cd-b166-39bbafe5dcc2

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: 7159.84 → 7151.83 (-8.01)
Findings: -10 net (+401 new, -411 resolved)

🆕 New findings (401)

  • critical complexity — src/utils/demo-api.ts — CC 108, MI 59.46, SLOC 925
  • critical complexity — src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 106, MI 49.09, SLOC 458
  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 95, MI 50.49, SLOC 419
  • critical complexity — src/components/Global/QRScannerOverlay/index.tsx — CC 81, MI 54.87, SLOC 346
  • critical complexity — src/components/Global/SupportDrawer/index.tsx — CC 68, MI 61.17, SLOC 200
  • critical complexity — src/app/shhhhh/ShhhhhLandingPage.tsx — CC 52, MI 56.57, SLOC 331
  • high structural-dup — types/api.generated.ts:10197 — 83 duplicate lines / 388 tokens with types/api.generated.ts:10785
  • high structural-dup — types/api.generated.ts:267 — 75 duplicate lines / 269 tokens with types/api.generated.ts:2525
  • high structural-dup — types/api.generated.ts:10207 — 74 duplicate lines / 348 tokens with types/api.generated.ts:10795
  • high structural-dup — types/api.generated.ts:267 — 73 duplicate lines / 264 tokens with types/api.generated.ts:2354
  • high structural-dup — types/api.generated.ts:267 — 68 duplicate lines / 249 tokens with types/api.generated.ts:2192
  • high structural-dup — types/api.generated.ts:5044 — 64 duplicate lines / 198 tokens with types/api.generated.ts:5146
  • high structural-dup — types/api.generated.ts:5047 — 61 duplicate lines / 188 tokens with types/api.generated.ts:5251
  • high structural-dup — types/api.generated.ts:2137 — 55 duplicate lines / 162 tokens with types/api.generated.ts:2470
  • high structural-dup — types/api.generated.ts:2137 — 55 duplicate lines / 162 tokens with types/api.generated.ts:2632
  • high structural-dup — types/api.generated.ts:2137 — 55 duplicate lines / 165 tokens with types/api.generated.ts:2719
  • high hotspot — src/components/Badges/badge.utils.ts — 51 commits, +593/-464 lines since 6 months ago
  • high hotspot — src/constants/analytics.consts.ts — 45 commits, +373/-14 lines since 6 months ago
  • high hotspot — src/app/(mobile-ui)/withdraw/crypto/page.tsx — 41 commits, +514/-265 lines since 6 months ago
  • high complexity — src/app/crisp-proxy/page.tsx — CC 41, MI 60.18, SLOC 140

…and 381 more.

✅ Resolved (411)

  • src/utils/demo-api.ts — CC 109, MI 59.48, SLOC 934
  • src/features/payments/flows/semantic-request/useSemanticRequestFlow.ts — CC 106, MI 49.11, SLOC 457
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 94, MI 50.61, SLOC 415
  • src/components/Global/QRScannerOverlay/index.tsx — CC 80, MI 54.95, SLOC 344
  • src/app/(mobile-ui)/dev/leaderboard/page.tsx — CC 72, MI 57.43, SLOC 384
  • src/components/Global/SupportDrawer/index.tsx — CC 63, MI 61.56, SLOC 173
  • types/api.generated.ts:10266 — 83 duplicate lines / 388 tokens with types/api.generated.ts:10854
  • types/api.generated.ts:267 — 75 duplicate lines / 269 tokens with types/api.generated.ts:2522
  • types/api.generated.ts:10276 — 74 duplicate lines / 348 tokens with types/api.generated.ts:10864
  • types/api.generated.ts:267 — 73 duplicate lines / 264 tokens with types/api.generated.ts:2351
  • types/api.generated.ts:267 — 68 duplicate lines / 249 tokens with types/api.generated.ts:2189
  • types/api.generated.ts:5041 — 64 duplicate lines / 198 tokens with types/api.generated.ts:5143
  • types/api.generated.ts:5044 — 61 duplicate lines / 188 tokens with types/api.generated.ts:5248
  • types/api.generated.ts:2134 — 55 duplicate lines / 162 tokens with types/api.generated.ts:2467
  • types/api.generated.ts:2134 — 55 duplicate lines / 162 tokens with types/api.generated.ts:2629
  • types/api.generated.ts:2134 — 55 duplicate lines / 165 tokens with types/api.generated.ts:2716
  • src/app/shhhhh/ShhhhhLandingPage.tsx — CC 48, MI 56.68, SLOC 304
  • src/components/Badges/badge.utils.ts — 46 commits, +588/-463 lines since 6 months ago
  • src/services/points.ts — CC 44, MI 49.75, SLOC 213
  • src/constants/analytics.consts.ts — 43 commits, +366/-13 lines since 6 months ago

…and 391 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/constants/crisp.ts 1.5 3.8 +2.3
src/app/crisp-proxy/page.tsx 9.0 10.8 +1.8
src/components/Global/SupportDrawer/index.tsx 11.4 12.7 +1.3
src/features/payments/shared/hooks/useChargeManager.ts 11.0 11.9 +1.0
src/components/Badges/badge.utils.ts 11.5 12.2 +0.7
src/app/(mobile-ui)/dev/leaderboard/page.tsx 10.0 0.0 -10.0
src/hooks/useCrispProxyUrl.ts 10.6 0.0 -10.6

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3086 ran, 0 failed, 0 skipped, 37.1s

📊 Coverage (unit)

metric %
statements 67.0%
branches 52.1%
functions 57.3%
lines 67.9%
⏱ 10 slowest test cases
time test
2.0s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.6s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/hooks/__tests__/useCrispTokenId.test.ts › retries then stays undefined when the endpoint keeps failing (no fallback token)
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.2s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.2s src/utils/__tests__/auth-token.test.ts › authReady does not park — hydrates the plain token without an unlock
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

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