Skip to content

chore: back-merge main → dev (2026-08-13) - #2688

Open
jjramirezn wants to merge 47 commits into
devfrom
chore/backmerge-main-into-dev-20260813
Open

chore: back-merge main → dev (2026-08-13)#2688
jjramirezn wants to merge 47 commits into
devfrom
chore/backmerge-main-into-dev-20260813

Conversation

@jjramirezn

Copy link
Copy Markdown
Contributor

Back-merge maindev so 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's notificationsApi import. All symbols from both sides are used in the merged body.
  • src/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsx — union: both sides appended a new describe block (dev: support unread badge, main: crisp-proxy init handshake). Kept both.
  • src/constants/general.consts.tsUSER_INTERVIEW_CAL_URL: took main's hotfixed dynamic link (fix(home): correct user-interview cal link #2683).
  • src/content submodule — took main's pointer 4a153e2 (verified fast-forward descendant of dev's 2a1c593).

Local verification

  • npm run typecheck — 0 errors
  • npm test -- SupportDrawer — 18/18 pass (both merged describe blocks)

🤖 Generated with Claude Code

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.
kushagrasarathe and others added 17 commits August 11, 2026 21:19
…, 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
…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
@vercel

vercel Bot commented Aug 13, 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 13, 2026 1:58pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 13, 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: a433a513-9191-4117-b5fa-9262564153da

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.11 → 7148.18 (-10.93)
Findings: -10 net (+178 new, -188 resolved)

🆕 New findings (178)

  • critical complexity — src/utils/demo-api.ts — CC 108, MI 59.46, SLOC 925
  • 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
  • high structural-dup — types/api.generated.ts:10194 — 83 duplicate lines / 388 tokens with types/api.generated.ts:10782
  • high structural-dup — types/api.generated.ts:10204 — 74 duplicate lines / 348 tokens with types/api.generated.ts:10792
  • high hotspot — src/constants/analytics.consts.ts — 44 commits, +358/-13 lines since 6 months ago
  • high complexity — src/app/crisp-proxy/page.tsx — CC 41, MI 60.18, SLOC 140
  • high complexity — src/services/points.ts — CC 40, MI 50.26, SLOC 185
  • high hotspot — src/hooks/useHomeCarouselCTAs.tsx — 38 commits, +479/-260 lines since 6 months ago
  • high method-complexity — src/components/Global/QRScannerOverlay/index.tsx:229 — CC 35 SLOC 157
  • medium high-mdd — src/components/Global/SupportDrawer/index.tsx:27 — SupportDrawer: MDD 97.7 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: MDD 68.8 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:8077 — 49 duplicate lines / 149 tokens with types/api.generated.ts:8501
  • medium structural-dup — types/api.generated.ts:7 — 48 duplicate lines / 185 tokens with types/api.generated.ts:10922
  • medium high-dlt — src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: DLT 44 (calls 44 distinct functions — high context load)
  • medium high-mdd — src/components/Global/QRScannerOverlay/index.tsx:229 — processQRCode: MDD 43.1 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:8580 — 43 duplicate lines / 123 tokens with types/api.generated.ts:8861
  • medium structural-dup — types/api.generated.ts:8580 — 42 duplicate lines / 125 tokens with types/api.generated.ts:8939
  • medium high-mdd — src/app/(mobile-ui)/dev/page.tsx:9 — DevToolsPage: MDD 41.3 (uses across many lines from declarations)
  • medium structural-dup — types/api.generated.ts:7499 — 40 duplicate lines / 116 tokens with types/api.generated.ts:8077

…and 158 more.

✅ Resolved (188)

  • src/utils/demo-api.ts — CC 109, MI 59.48, SLOC 934
  • 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:10276 — 74 duplicate lines / 348 tokens with types/api.generated.ts:10864
  • src/services/points.ts — CC 44, MI 49.75, SLOC 213
  • src/constants/analytics.consts.ts — 42 commits, +351/-12 lines since 6 months ago
  • src/app/crisp-proxy/page.tsx — CC 41, MI 61.23, SLOC 120
  • src/components/Global/QRScannerOverlay/index.tsx:229 — CC 34 SLOC 155
  • src/hooks/useHomeCarouselCTAs.tsx — 34 commits, +433/-252 lines since 6 months ago
  • src/app/(mobile-ui)/dev/leaderboard/page.tsx:69 — LeaderboardPage is 414 lines — split it
  • src/components/Global/SupportDrawer/index.tsx:21 — SupportDrawer: MDD 81.8 (uses across many lines from declarations)
  • src/app/(mobile-ui)/dev/leaderboard/page.tsx:69 — LeaderboardPage: MDD 81.4 (uses across many lines from declarations)
  • src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: MDD 65.7 (uses across many lines from declarations)
  • types/api.generated.ts:8149 — 49 duplicate lines / 149 tokens with types/api.generated.ts:8573
  • types/api.generated.ts:7 — 48 duplicate lines / 185 tokens with types/api.generated.ts:10994
  • src/app/(mobile-ui)/dev/leaderboard/page.tsx:69 — LeaderboardPage: DLT 45 (calls 45 distinct functions — high context load)
  • src/app/(mobile-ui)/dev/page.tsx:9 — DevToolsPage: MDD 43.3 (uses across many lines from declarations)
  • src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: DLT 43 (calls 43 distinct functions — high context load)

…and 168 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/constants/crisp.ts 1.5 3.8 +2.4
src/app/crisp-proxy/page.tsx 9.1 10.9 +1.8
src/components/Global/SupportDrawer/index.tsx 11.5 12.7 +1.3
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: 2992 ran, 0 failed, 0 skipped, 48.4s

📊 Coverage (unit)

metric %
statements 66.6%
branches 51.9%
functions 56.8%
lines 67.4%
⏱ 10 slowest test cases
time test
3.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
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.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s 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