chore: back-merge main → dev (2026-08-13) - #2688
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.
…, harden handshake /code-review findings: the handshake was one-shot, so identity fields resolving after boot (email/name during onboarding) or a new prefill never reached Crisp — the old URL transport remounted on any change. The parent now re-sends the payload when it changes and the proxy applies it live; token/locale changes still remount via the iframe key. Also: reuse setCrispUserData (util was the canonical setter; the payload now carries CrispUserData whole), shared handshake message constants, watchdog clears the 250ms request loop on failure, payload ref written in an effect instead of during render.
…/locale remount CodeRabbit round 2: gate CRISP_PROXY_INIT replies on event.source matching our iframe's contentWindow so no other same-origin frame can pull the token and user data; clear isCrispReady/isCrispFailed when a token or locale change swaps the iframe, so the loader shows until the new proxy reports.
Jota's review: a background user refresh re-fired the update effect and setCrispUserData unconditionally re-pushed message:text, overwriting whatever the user was typing in the composer. Prefill now applies at boot and, on updates, only when its value actually changed (a new support entry point) — metadata refreshes never touch the composer.
feat: ACAI_POWERED badge artwork (Brazil creator campaign)
fix: pass Crisp proxy user data via postMessage handshake instead of URL query string
The scanner lowercased the scanned payload before handing it to recognizeQr. Base58 carries meaning in its case, so that destroyed the address: an uppercase L is a valid Solana character while a lowercase l is not, and every Tron address starts with an uppercase T. About half of all Solana addresses and every Tron address fell through to the "Unrecognized QR code" modal. recognizeQr was always correct and its own suite even asserts case-sensitivity on purpose. Only the call site was wrong, so the new test covers the component, not the parser. Task: TASK-21111
Recognizing the raw scan alone was too strict in the other direction. QR alphanumeric mode encodes uppercase only and is much denser, so encoders routinely uppercase bech32 addresses, BOLT-11 invoices and hex addresses. Those payloads lost their case to the encoder, so lowercasing them back is safe. A payload holding any lowercase letter kept its original case, so a mixed-case EIP-55 checksum is the user's and viem must stay free to reject a bad one. The old blanket lowercase laundered a corrupted address into a valid-looking payment target; that stops here. Found by /code-review.
The explanation ran longer than the code it explained.
…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
…selects both hosts and 15 min
…l-link fix(home): correct user-interview cal link
…into-dev-20260813 # 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.11 → 7148.18 (-10.93) 🆕 New findings (178)
…and 158 more. ✅ Resolved (188)
…and 168 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Back-merge
main→devso dev is a clean superset of prod before the next release PR.What comes over from main
Conflicts resolved
src/components/Global/SupportDrawer/index.tsx— union: main's crisp-proxy imports + dev'snotificationsApiimport. All symbols from both sides are used in the merged body.src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx— union: both sides appended a newdescribeblock (dev: support unread badge, main: crisp-proxy init handshake). Kept both.src/constants/general.consts.ts—USER_INTERVIEW_CAL_URL: took main's hotfixed dynamic link (fix(home): correct user-interview cal link #2683).src/contentsubmodule — took main's pointer4a153e2(verified fast-forward descendant of dev's2a1c593).Local verification
npm run typecheck— 0 errorsnpm test -- SupportDrawer— 18/18 pass (both merged describe blocks)🤖 Generated with Claude Code