Conversation
Replace the postage-stamp layout with a force-directed sticker collage — the pixelated card sits centred, badges are placed by a repulsion-based solver that fills the field without heavy overlap, and a hero "I'M IN!" burst + a peanut.me/<handle> pill frame it. The card's peanut logo and pixel hand stay uncovered via sticker-half-inflated keep-outs. Why: the launch "I got in" share moment needed a louder, more on-brand asset. The old stamp framing + EDITION/tier/points/stats chrome read as cluttered, and the prior ring layout crowded the edges at high badge counts. The look ships via component defaults, so existing surfaces (BadgeSkipCelebration, CardUnlockDrawer) pick it up with no caller changes. Also exposes /dev/share-builder as a dev-only public route (prod still 404s) for iterating on the asset.
CodeRabbit review on #2274: - Size the username-pill keep-out to the rendered pill (peanut.me/<handle> widens with the handle); a fixed x0 only guarded the right edge, so a sticker could land on a long handle. Pill box now computed from the rendered geometry and passed into placeStamps; add a regression test. - Reserve the *rotated* hero bounding box so a tilted hero sticker's corners aren't covered; extract shared heroTilt() used by render + keep-out. - Correct the heroMessage/usernameStyle prop docs (undefined=default hero, null=none; pill defaults to white).
Users who pass the eligibility hold but lack a card-access badge hit a
flat "you don't have the required badge :(" wall. Replace it with a
shareable door rejection that doubles as a growth loop: a "not tonight,
<username>" asset (smug peanut bouncer, scarcity tally as screen copy)
plus a primary "Tweet to appeal" share that attaches the asset and tags
@joinpeanut with a random caption. The secondary "Join the waitlist
anyway" still calls joinWaitlist, flipping the state machine to the
friendly joined screen as the post-share cooldown.
CardRejectionScreen owns the join itself (mirrors the old
CardWaitlistScreen contract: joinWaitlist + posthog + loading/error), so
the /card state machine drops it straight into the not-joined slot.
Removes the now-orphaned CardWaitlistScreen. Adds /dev/rejection-builder
to iterate on copy, the door tally, and the bouncer mascot.
react/no-unknown-property flags the styled-jsx `jsx` attr; scope a disable to keep the new file lint-clean, matching ShareAssetD3's pattern.
handleAppeal logged + Sentry-captured a capture/native-share failure but showed the user nothing — the spinner just stopped and no tweet went out. The appeal is the whole point of the rejection screen, so fall back to the text-only twitter intent on any non-abort error: the @joinpeanut tag still goes out even if html-to-image or the OS share sheet fails.
The keep-out-box approach still let stickers pile up / heavily overlap on dense badge sets. Replace it with a force-directed relaxation: pairwise repulsion between stickers, keep-clear ellipses around the hero + card, and a pill keep-out with exit vectors, iterated to a stable spread. Drops the now-unused pillKeepoutBox / PILL_RIGHT / PILL_BOTTOM exports and updates the layout tests to assert the no-heavy-overlap invariant.
…ner-trap Merging the force-directed engine onto the branch dropped two keep-out refinements the earlier collage had, and a self-review found the engine itself could still pile badges up: - Rotated hero keep-out: reserve the *rotated* hero bounding box again (shared heroTilt() helper drives both the render and the keep-out, so they stay in lockstep) — badges no longer clip the tilted hero corners. - Dynamic pill keep-out: size the bottom-right keep-out to the *rendered* username pill (pillKeepoutBox, threaded through placeStamps) instead of a static box, so badges clear the whole pill for long handles. - Corner-trap: the soft edge + pill pulls could deadlock two stickers against the bottom-right corner (Gauss-Seidel local minimum) — across a 7k-layout sweep the worst centre gap was 0.22×size, well into "heavy overlap". Add a final separation-only pass (separation + hard keep-outs + clamp, no edge/pill pull) so pairwise spread wins the last word; worst gap is now 0.75×size. Broaden the overlap test to a 160-seed sweep so the corner-trap regime is actually exercised. Also: appeal share treats an unmeasured asset ref as a graceful text-only fallback rather than a Sentry-logged error.
Some users don't want their peanut.me/<handle> on a public victory post. Add a `hideUsername` prop to ShareAssetD3: when set, the username pill doesn't render and its bottom-right keep-out is pushed off-canvas so the badge collage reclaims that corner. Surfaced as a simple "Hide username" checkbox directly beneath the asset (above the share buttons) on both share surfaces — BadgeSkipCelebration and CardUnlockDrawer — and in the /dev/share-builder iterator. The captured PNG honours the toggle, so a hidden handle never leaks into the shared image.
Hiding then un-hiding the username left the pill invisible for ~2.3s: the conditional unmount re-ran the pill's staggered entrance animation (600ms fade + 1700ms delay) on every toggle, so it looked like the handle never came back. Keep the pill mounted and drive the toggle with `visibility` instead — the entrance plays once on the initial reveal, and toggling is instant. `visibility: hidden` is also excluded from the html-to-image capture, so a single capture-at-share-time still produces the correct PNG (verified: with-handle vs no-handle) — no need to pre-render two images.
The win share posted one identical line ("I got my Peanut card. shhhh.")
for everyone. Add a win-caption pool (winCaptions.ts, Hugo's picks: hype
+ invite/FOMO + Devconnect callbacks + "shhhh" + anti-bank) and pick one
at random per mount in ShareAssetActions — used for both the native share
sheet and the desktop twitter intent (shareCardOnTwitter now takes the
caption), so the two paths post the same line. Drops the now-unused
shareText prop from the celebration + history-replay callers.
Self-review caught one curly apostrophe (U+2019) in the 'can't talk' line while the other 15 use straight ASCII; normalize so the tweet text reads consistently.
The file/caption comments promised the @joinpeanut tag was rendered into the pixels so it survives an image-only re-post, but the asset only drew the headline + mascot — the growth tag lived only in the share caption. A user who re-posts just the PNG would carry no tag, silently breaking the screen's entire referral rationale. Render the handle bottom-right.
- shareAssetLayout: doc comment said 2300/count, code uses 2700/count - share-builder: surface hideUsername in the 'Resulting props' panel devs copy from (it's a headline feature, was omitted) - ShareAssetD3: pill render now uses the shared PILL_MAX_W constant instead of a hardcoded 780, keeping render + keep-out estimate in lockstep
…to-access)
The 'Tweet to appeal' CTA tweeted but left no in-app record — the appealer
wasn't on the waitlist, so the manual-grant workflow ('we let them in by
hand') had nothing to release from. Now appeal fires joinWaitlist() in
parallel with the share: drops them into the userId-keyed queue and flips
to the friendly cooldown after sharing. Joining is NOT access — release
stays manual (admin grant / grant-card-access), so no one gets auto-access.
PostHog tags the join source:'appeal' so appealers are distinguishable from
quiet 'Join anyway' joiners. onJoined() is deferred to finally so the
unmount can't race captureShareAsset's read of captureRef.
… handle The @joinpeanut tag belongs in the share text, not the pixels. Revert the baked-in handle on the rejection asset (keep the image clean) and instead ensure every shareable caption carries the handle: the rejection captions already did, so add @joinpeanut to all 16 win captions. Now both win and rejection shares tag the brand via the tweet text on every post.
… fees Cross-chain withdraw was disabled after a customer paid ~$4 in fees on a ~$5 mainnet withdraw while the confirm screen showed the fee struck through as 'Sponsored by Peanut!'. The Rhino fee (destination gas + 0.07%) is real and user-paid; only the kernel execution gas is actually sponsored. - Confirm screen now shows the honest network fee + a 'You pay' total (amount + fee), separating the user-paid bridge fee from the sponsored kernel gas. Fixes the misleading 'Recipient receives' tooltip. - Disproportionate-fee hard-stop (cross-chain-fee.utils): blocks the CTA when the fee exceeds 5% of the amount. Flat mainnet gas makes small mainnet withdrawals fail this — exactly the case that got the feature disabled. - Thread the quote economics (feeUsd/payAmount/receiveAmount) from preview into provision so the backend can persist + ledger them. - Re-enable: flip disableXchainWithdraw only; TokenSelector restricts withdraw to USDC/USDT across Rhino chains. Non-stable bridge path stays out (its fee accounting is unfixed); claim / cross-chain pay-request stay disabled.
fix: minor ui bug fixes
The reveal error path piped the raw thrown Error.message straight to the card face — CardFace renders the error string verbatim. For any real API failure that message is the backend's raw error (e.g. an upstream Rain 500 body), because rainRequest throws `new Error(err.error)`. The existing 'Failed to load card details' fallback only covered non-Error throws, which never happen on this path, so users saw internal error text on the card instead of a usable message. Always render a friendly, actionable message; keep the raw text on the PostHog CARD_PAN_FAILED event so diagnostics aren't lost.
…ut of analytics CodeRabbit: forwarding the full backend error message to PostHog could push raw upstream/provider detail (Rain 500 body, correlationId) into client analytics. Cap it to a 120-char slice — enough to segment failures — since the complete sanitized detail is already in Sentry via fetchWithSentry.
…-error fix(card): friendly message when card reveal fails
… block) Per the cross-chain withdraw decision (keep mainnet + L2, stable + eth; fix is honesty not removing features): - Remove the stablecoin-only TokenSelector restriction — ETH/non-stable destinations are allowed again (they route through the existing swaps path). - Turn the 5% fee hard-stop into a non-blocking heads-up: the CTA is no longer disabled on a high fee ratio; instead a muted note flags when the fee is a large share of the amount. The fee is shown honestly and the user decides, same as fiat withdrawals. Note: the SDA (stablecoin) path books the fee on the ledger; the swaps (ETH) path shows the fee honestly pre-sign but doesn't yet book a ledger entry — that's a follow-up (the swaps settlement is a separate path, kept as-is).
Per CodeRabbit — the comment said 'USDC/USDT only' but withdrawals now allow non-stable (ETH) destinations via the swaps path too. Reword to match.
…add up Per CodeRabbit — the 'Network fee' row rounds to 2 decimals but 'You pay' summed the full-precision fee, so they could disagree by a cent. Use the same rounded fee value in the total.
The Squid-era selector exposed chains/tokens Rhino rejects — e.g. USDC on Scroll → '400 SCROLL is disabled' on preview. Add a curated RHINO_WITHDRAW_SUPPORTED_TOKENS_BY_CHAIN map (from Rhino's live SDA + bridge config) and, in withdraw mode, filter the network list, popular buttons, and per-chain token list to it. Drops Scroll entirely and native gas tokens Rhino can't bridge (POL on Polygon, xDAI on Gnosis); keeps USDC/USDT everywhere supported plus ETH (arb/eth/op) and BNB. EVM-only, matches the current selectable chain set.
Arbitrum's chain icon pointed at an arbiscan.io-hosted SVG that blocks
hotlinking, so next/image failed and the token selector fell back to initials
('AO'). Add a local-asset override map and prefer the bundled arbitrum.svg.
Now visible because cross-chain withdraw re-enables the full network selector.
evmChainIdToRhinoName returned our display names (POLYGON, BNB), but Rhino's API expects MATIC_POS and BINANCE — so Polygon/BNB withdrawals 400'd with 'Invalid chain'. The map conflated display ChainName with Rhino's API name; they coincide for most chains but not these two. Emit the correct Rhino names (per peanut-api-ts CHAINS_CONFIG) and decouple the return type from the display-name union. Fixes withdraw + claim cross-chain to those chains.
…ulating The 'Recipient receives', 'Network fee', and 'You pay' rows depend on the Rhino quote; while it's being fetched they were hidden (rendered only once the value existed), so the confirm screen looked empty with just a disabled CTA. Render them during isCalculating with PaymentInfoRow's existing loading prop so they show a spinner, then the real values. Same-chain (no quote) is unaffected.
The non-blocking fee heads-up reused ErrorAlert with muted styling; switch to the InfoCard component (variant=info) so it reads as an informational note, not an error.
* feat(badges): add Psyops Division badge art + invite mapping Adds psyops_division.svg (winged agency-seal mascot from the badge-draft skill), the BADGES entry (icon + copy + displayName), and the psyops -> PSYOPS_DIVISION invite-code campaign mapping. Pairs with the peanut-api-ts registry PR; campaign-maps.test.ts guard passes. * feat(badges): replace Card Pioneer with Founding Pioneer badge The grandfathered CARD_PIONEER code stays (it gates card access + cashback), so we keep the backend code and only repoint the FE asset/copy/name to the new Founding Pioneer (Flag Lander) — existing holders re-render the new badge. Also wires FOUNDING_PIONEER as a new invite-activated community badge for the early crew (invite code "founding").
On top of the toast core (523b046): adds the missing BadgeEarnToast component test (the /home gate, single-vs-coalesced label, detail-modal-vs-/badges tap), excludes BETA_TESTER (every signup earns it -> noise, not a moment), and updates the detection tests. 23 badge tests green, typecheck clean.
… fixes) Addresses the 3 fire-once bugs from the high-effort code review: (1) cold-start re-fire - seen is now derived synchronously via useMemo, not a useState+useEffect that lagged the async user load by one render; (2) staggered drop - per-batch toast id (not a fixed id) so a 2nd badge earned within the window surfaces instead of being marked-seen-but-never-shown; (3) private-mode nag - in-memory fallback when localStorage writes throw so the toast does not re-fire every /home visit. Also dismiss on nav away from /home; compute getBadgeIcon once. +3 tests; 26 badge tests green, typecheck + eslint clean.
… union (festa/cardalpha + psyops/founding) Secret-scan pre-commit hook flagged 0x-hex in dev's supported-chains.ts / withdraw-crypto files — verified public contract addresses + bytes32 config already in dev, no private-key material. False positive.
feat(badges): detail modal (2x image + tappable drawer) + non-blocking earn toast (TASK-19791)
- gate visible on overview loading + dismissed!==null so dismissed users and card-holders don't flash the banner / fire a premature VIEWED event - disableHaptics on the inner Button (Bruddle Button self-triggers haptic; handleTryDoor already fires one) to avoid a double buzz
feat(card): home card-launch CTA (fat /shhhhh-tone splash on launch day)
…collage feat(card): redesign launch share asset as a sticker collage
feat(dev): /dev/home-ctas preview for every home CTA
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (72)
Comment |
Code-analysis diffPainscore total: 5751.17 → 5846.48 (+95.31) 🆕 New findings (165)
…and 145 more. ✅ Resolved (131)
…and 111 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Unify the activation-funnel card step to render the #2295 /shhhhh-tone CardLaunchCTABanner so non-activated card-eligible users get the same mysterious CTA as the activated launch splash (keeps the /card routing + Maybe-later dismiss). Fix Visa merchant count to 150M+ (was 40m in ActivationCTAs, 140M in AddCardEntryScreen) to match /shhhhh. Add reliable keyless Arb-Sepolia RPC fallbacks (publicnode + drpc) ahead of the 503-prone official endpoint so harness smart-wallet deploys stop failing on transient 503s. Sandbox-relevant testnet RPC entry; prod wallet chain (arbitrum mainnet) untouched.
…itlist only on re-visit Bare /shhhhh door no longer joins the waitlist inline. Post-launch it routes to /card, where the press-and-hold eligibility -> Berghain 'not tonight' rejection IS the join moment (matches the home launch CTA). Reads BE waitlist state on mount so a RETURNING joined user sees the inline 'on the waitlist #N' pill instead of the door. Signed-out door -> signup -> /card. Drops the joinWaitlistAfterSignup cookie entirely (kills the stale-cookie auto-join surprise). Pre-launch (no access, /card 404s) still joins inline.
A returning joined user can tap the inline 'on the waitlist #N' confirmation to re-open /card (their waitlist-joined status) instead of it being a dead pill.
…aunchCTA Konrad's call: max the launch buzz on Twitter, hold the in-app nudge and convert the existing base on our own schedule. The mysterious card CTA only ever reaches existing users today (funnel card step gates on a card-access badge; home splash gates on isActivated), so muting both paths delays it for everyone who'd see it. New users keep the normal verify → deposit → outbound funnel, and /card stays reachable (door, waitlist pill, direct link). Default true (off) now; flip to false ~2-3 days post-launch to start in-app conversion.
… explainer Konrad's call: ship the in-app nudge now (not delayed), but rework it so it actually converts. Copy is now just the whisper — title 'shhh' + 'Tap to find out if you're in' + 'Try the door →'. Routes to /shhhhh (the explainer landing) instead of /card directly: the bare card flow doesn't explain anything, so we funnel through the landing which walks users into the canonical flow. disableCardLaunchCTA flips to false (CTA live) and stays as a kill-switch.
On mobile the rotated PixelatedCardFace in the hero column sat practically flush against the marquee below — the rotated card's bounding box overflows its layout box downward and ate the section's bottom padding. Add pb-16 on mobile (reset md:pb-0 — desktop is a 2-col layout, no collision). Launch-day polish, direct to dev.
Prod Release Sprint 149 — cross-chain withdraw · card-launch CTA · share-asset · badges (2026-06-29)
Pairs with peanut-api-ts dev→main (#1075
isPublicLaunched, badges, EEA test fix). Deploy order: BE first, then FE. No DB migrations in this release.Changes (13 PRs)
Cross-chain withdraw (headline)
Card launch
isPublicLaunchedfrom api-ts hot-fix: show mexico accounts in saved accounts #1075 → BE must ship first/together.Badges
Withdraw / add-money / bank flows
Compliance / guest claims
Misc / dev
/dev/home-ctaspreview (dev tool, no prod surface).Risks
isPublicLaunched; deploy BE first or the CTA mis-gates.QA checklist
psyopsawards it; detail modal opens; earn toast non-blockingDeploy / monitor
main → dev