chore: back-merge main → dev (pre release 2026-08-18) - #2724
Conversation
#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.
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
|
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:
Comment |
Code-analysis diffPainscore total: 7159.84 → 7151.83 (-8.01) 🆕 New findings (401)
…and 381 more. ✅ Resolved (411)
…and 391 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Back-merge of
mainintodevahead 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'snotificationsApi(support unread badge). Both features' code coexists in the body.src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx— both sides added adescribeblock in the same spot; kept both (18/18 pass locally).src/constants/general.consts.ts— took main'sUSER_INTERVIEW_CAL_URL(newer hotfix: preselects hosts + 15 min).src/contentsubmodule — took main's pointer9101046(fast-forward descendant of dev's2a1c593).Supersedes stale back-merge PRs #2688 and the bot sync PR #2713.