Skip to content

chore: sync dev with main - #2713

Merged
66 commits merged into
devfrom
main
Aug 18, 2026
Merged

chore: sync dev with main#2713
66 commits merged into
devfrom
main

Conversation

@chip-peanut-bot

Copy link
Copy Markdown
Contributor

Branch Sync

main is 63 commits ahead of dev.

Commits to sync:

• fix(badges): make the invite-page campaign warning readable in Sentry
• feat(home): flag-gated user-interview invite card in the carousel
• Merge pull request #2651 from peanutprotocol/fix/invites-campaign-warn-payload
• review: await cal.com open, honest impression-proxy comment, full teardown checklist
• review: booking link is the live cal.com dynamic group URL
• copy: the call is with the team, not the founders
• Merge pull request #2654 from peanutprotocol/hotfix-user-interview-cta
• ui: render the waving mascot at 44px
• Merge pull request #2655 from peanutprotocol/hotfix-user-interview-cta
• feat(fx): consume the shared card-markup endpoint and drop both frontend copies
• fix(fx): mirror the corrected issuer-fee model in the locked-price recompute
• fix(fx): keep 'no comparison' and 'could not reach the model' apart
• Update content submodule to latest main
• Merge pull request #2659 from peanutprotocol/auto/update-content-20260811-100504
• fix(fx): accept every spelling of zero, reject overflow verification dates
• Update content submodule to latest main
• Merge pull request #2660 from peanutprotocol/auto/update-content-20260811-102159
• Update content submodule to latest main
• Merge pull request #2661 from peanutprotocol/auto/update-content-20260811-103319
• Merge pull request #2658 from peanutprotocol/feat/fx-card-markup-ui
• Update content submodule to latest main
• Merge pull request #2664 from peanutprotocol/auto/update-content-20260811-110227
• fix: fetch Crisp support token from the API instead of deriving it client-side
• fix: reset crisp token on account switch, verify identity, retry transient failures
• chore: delete the points leaderboard consumers (postmortem F2)
• chore: drop stale leaderboardRank from the e2e tier-info mock
• Merge pull request #2667 from peanutprotocol/hotfix/delete-points-leaderboard
• feat: add ACAI_POWERED badge artwork for Brazil creator campaign
• Merge pull request #2666 from peanutprotocol/hotfix/crisp-token-server-side
• fix: move Crisp proxy user data and token out of the URL into a postMessage handshake
• review: push payload updates over the channel, reuse setCrispUserData, harden handshake
• review: reply only to the mounted proxy iframe, reset status on token/locale remount
• Merge remote-tracking branch 'origin/main' into fix/crisp-proxy-pii-in-url
• review: never re-apply an unchanged prefill on live updates
• Merge pull request #2672 from peanutprotocol/feat/acai-powered-badge
• Merge pull request #2669 from peanutprotocol/fix/crisp-proxy-pii-in-url
• fix(qr): recognize scanned Solana and Tron addresses
• fix(qr): retry lowercased only for all-uppercase scans
• chore(qr): tighten the case-handling comment
• Update content submodule to latest main
• Merge pull request #2677 from peanutprotocol/auto/update-content-20260812-122901
• Update content submodule to latest main
• Merge pull request #2679 from peanutprotocol/auto/update-content-20260812-123904
• Merge pull request #2675 from peanutprotocol/fix/qr-scanner-base58-case
• fix(home): point user-interview card at the dynamic cal link that preselects both hosts and 15 min
• Merge pull request #2683 from peanutprotocol/hotfix-user-interview-cal-link
• feat(badges): add Surf's Up artwork and share line
• feat(badges): add ENS artwork and share line
• feat(badges): send the typed ENS name with the charge so the badge can be earned
• fix(badges): make the ENS share line true for both sides of the award
• refactor(badges): drop the withdraw ENS pairing check
• fix(shhhhh): list the full waitlist-skip badge set
• refactor(shhhhh): derive badge icons from the generated asset manifest
• fix(shhhhh): collapse the badge grid to 3 + a 'more' toggle
• Merge pull request #2698 from peanutprotocol/hotfix/shhhhh-badge-list
• feat(badges): add Tron Native artwork and share line
• fix(badges): draw the actual Tron mark, not a T in a triangle
• fix(badges): light coin and a bigger face so the eyes are legible
• feat(badges): pick the winking Tron mark from the matrix
• fix(ens): true trust comment; withdraw trims the name like useChargeManager
• Merge pull request #2691 from peanutprotocol/feat/surf-up-badge
• Update content submodule to latest main
• Merge pull request #2699 from peanutprotocol/auto/update-content-20260817-113843


Auto-generated by Chip 🐿️

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.
Hugo0 and others added 25 commits August 12, 2026 13:31
…0812-122901

content: publish latest to production (src/content → peanut-content@ea76eea)
…0812-123904

content: publish latest to production (src/content → peanut-content@4a153e2)
fix(qr): recognize scanned Solana and Tron addresses
…l-link

fix(home): correct user-interview cal link
Backend catalog gained SURF_UP for the summer 2026 campaign; this is the UI
half of the asset contract — artwork, regenerated manifest, and the bespoke
first-person share copy.
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.
@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 9:03am

Request Review

feat(badges): Splitter artwork + share line
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