feat(card): redesign launch share asset as a sticker collage#2274
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR adds a rejection-screen flow and reworks the share-asset builder, layout, and rendering pipeline around sticker placement, hero messages, username-pill styling, and share-text generation. ChangesShare-asset sticker-collage refactor
Rejection screen flow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code-analysis diffPainscore total: 5764.4 → 5801.56 (+37.16) 🆕 New findings (52)
…and 32 more. ✅ Resolved (37)
…and 17 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/app/(mobile-ui)/dev/share-builder/page.tsx (1)
49-62: 🚀 Performance & Scalability | 🔵 Trivial
heroMessageandusernameStyleare fresh object refs each render, but their performance impact is minimal.You memoized
badgesArraysoShareAssetD3's layout memo doesn't re-run unnecessarily. However,heroMessageandusernameStyleare rebuilt every render. While this is inconsistent with your memoization pattern forbadgesArray, the actual performance impact is limited:ShareAssetD3'sheroGeouseMemo keys on extracted properties ([hero?.text, hero?.variant, hero?.scale]), not the object reference, so freshheroMessageobjects don't trigger re-runs. Similarly,usernameStyleis read but not part of any memo dependency. If you want consistency and defensiveness against future changes, memoizing these would be low-effort.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx around lines 49 - 62, Memoize both heroMessage and usernameStyle objects for consistency with the badgesArray memoization pattern. Wrap the heroMessage object creation (the ternary that returns either null or an object with text, variant, scale, and tilt) in a useMemo hook with dependencies on heroVariant, heroText, heroScale, and heroTilt. Similarly, wrap the usernameStyle object creation (which contains bg, prefixRatio, scale, and letterSpacing properties) in a useMemo hook with dependencies on unameBg, unamePrefix, unameScale, and unameTracking. This ensures consistency with your memoization approach and defends against potential future changes to component dependencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/Card/share-asset/shareAsset.types.ts`:
- Around line 78-82: The JSDoc comments for the heroMessage and usernameStyle
properties in the shareAssetD3.types.ts interface file do not match the actual
component behavior. For the heroMessage property, the documentation incorrectly
states that omitted values mean no hero message, but the ShareAssetD3 component
treats undefined as the default hero and only null as no hero, so update the
comment to reflect that undefined (or omitted) uses the default hero while null
disables it. For the usernameStyle property, the documentation incorrectly
claims the default is pink with auto-fit sizing, but the actual component
default is white, so update the comment to correctly state the default color as
white while keeping any other relevant sizing details accurate.
In `@src/components/Card/share-asset/ShareAssetD3.tsx`:
- Around line 50-63: The heroGeometry function calculates dimensions that don't
match the actual rendered width of pill and banner stickers in HeroMessageEl,
causing the keep-out geometry to be inaccurate. Update the heroGeometry function
to calculate the width and height values that correspond to the actual padding
and content dimensions used when rendering the pill and banner variants, rather
than using the current formula that doesn't account for the actual rendered
width of these elements.
In `@src/components/Card/share-asset/shareAssetLayout.ts`:
- Around line 74-79: The PILL_KEEPOUT constant in the shareAssetLayout.ts file
only sets x0 to 690, which protects only the right side of the username pill.
Since the pill can render up to 780px wide from right: 56, its left edge extends
to approximately 364px, leaving the left portion unprotected from sticker
overlaps. Update the PILL_KEEPOUT object to extend its x0 value to approximately
364 (or the correct calculated left edge position) instead of 690 to ensure the
hitsPill collision detection covers the complete rendered width of the pill.
Additionally, verify that y0 is also properly sized to cover the top edge of the
pill's actual rendered footprint.
---
Nitpick comments:
In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx:
- Around line 49-62: Memoize both heroMessage and usernameStyle objects for
consistency with the badgesArray memoization pattern. Wrap the heroMessage
object creation (the ternary that returns either null or an object with text,
variant, scale, and tilt) in a useMemo hook with dependencies on heroVariant,
heroText, heroScale, and heroTilt. Similarly, wrap the usernameStyle object
creation (which contains bg, prefixRatio, scale, and letterSpacing properties)
in a useMemo hook with dependencies on unameBg, unamePrefix, unameScale, and
unameTracking. This ensures consistency with your memoization approach and
defends against potential future changes to component dependencies.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d8637f6f-dbf2-44ec-b414-0e986abcf9cb
📒 Files selected for processing (8)
src/app/(mobile-ui)/dev/share-builder/page.tsxsrc/components/Card/share-asset/PixelatedCardFace.tsxsrc/components/Card/share-asset/ShareAssetD3.tsxsrc/components/Card/share-asset/__tests__/shareAssetLayout.test.tssrc/components/Card/share-asset/shareAsset.types.tssrc/components/Card/share-asset/shareAssetLayout.tssrc/constants/routes.tssrc/styles/globals.css
💤 Files with no reviewable changes (1)
- src/styles/globals.css
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.
|
@coderabbitai review |
✅ Action performedReview finished.
|
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.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/Card/CardRejectionScreen.tsx`:
- Around line 95-116: The share flow in CardRejectionScreen should not treat a
missing captureRef node from ScaledRejectionAsset as a real share failure.
Update the handler around captureRef.current / captureShareAsset so the CTA is
disabled until useFitToWidth has mounted the capture node, or replace the thrown
error path with a retry/early-return state instead of entering the catch
fallback. Keep the AbortError behavior, but avoid falling through to the Twitter
intent fallback when the asset simply has not measured yet.
In `@src/components/Card/share-asset/RejectionAssetD3.tsx`:
- Around line 80-189: The exported canvas currently renders only the mascot and
rejection message, so the attribution/CTA is lost in image-only reposts. Update
RejectionAssetD3 to bake a persistent “@joinpeanut” mark into the JSX canvas
itself, alongside the existing headline and mascot, so it always appears in the
generated PNG regardless of where the asset is shared.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cc0cac71-69cd-41e7-b681-57a202c84856
📒 Files selected for processing (11)
src/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/dev/rejection-builder/page.tsxsrc/components/Card/CardEligibilityCheckScreen.tsxsrc/components/Card/CardRejectionScreen.tsxsrc/components/Card/CardWaitlistScreen.tsxsrc/components/Card/share-asset/RejectionAssetD3.tsxsrc/components/Card/share-asset/ScaledRejectionAsset.tsxsrc/components/Card/share-asset/ShareAssetD3.tsxsrc/components/Card/share-asset/rejectionCaptions.tssrc/components/Card/share-asset/shareAsset.types.tssrc/constants/routes.ts
💤 Files with no reviewable changes (1)
- src/components/Card/CardWaitlistScreen.tsx
✅ Files skipped from review due to trivial changes (2)
- src/components/Card/CardEligibilityCheckScreen.tsx
- src/components/Card/share-asset/rejectionCaptions.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/Card/share-asset/shareAsset.types.ts
- src/components/Card/share-asset/ShareAssetD3.tsx
…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.
|
@coderabbitai review |
✅ Action performedReview finished.
|
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.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
src/components/Card/share-asset/shareAssetLayout.ts (1)
118-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueStale magnitude in the
stickerSizedoc comment.Line 120 says the fallback "eases down a 2300/count curve," but
stickerSizeusesMath.round(2700 / count). Align the comment with the actual constant to avoid confusing future maintainers (2700 also keeps continuity better: count 11 → 245 vs the table's 280 at count 10).📝 Proposed doc fix
-// the repulsion packs them across the whole field (not a thin scatter). Past -// the table it eases down a 2300/count curve with a legibility floor. +// the repulsion packs them across the whole field (not a thin scatter). Past +// the table it eases down a 2700/count curve with a legibility floor.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Card/share-asset/shareAssetLayout.ts` around lines 118 - 138, The doc comment above STICKER_SIZE_BY_COUNT and stickerSize is out of sync with the implementation: it mentions a 2300/count fallback, but stickerSize() actually uses 2700/count. Update the comment so it matches the real fallback curve and keeps the description aligned with the behavior in stickerSize, preserving the existing tone about the size scaling and legibility floor.src/app/(mobile-ui)/dev/share-builder/page.tsx (1)
393-405: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winInclude
hideUsernamein the "Resulting props" panel.
hideUsernameis wired into<ShareAssetD3 />(Line 363) and is one of the PR's headline features, but the props panel devs copy from to wire real surfaces omits it.♻️ Add the missing field
heroMessage, usernameStyle, + hideUsername, animate,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx around lines 393 - 405, The “Resulting props” panel in the share builder is missing the hideUsername field even though ShareAssetD3 already consumes it. Update the JSON shown in the page component so the object passed to JSON.stringify includes hideUsername alongside username, badges, seedOverride, heroMessage, usernameStyle, and animate, using the existing share-builder page render logic as the reference point.src/components/Card/share-asset/ShareAssetD3.tsx (1)
259-278: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
PILL_MAX_Wfor the pillmaxWidthto keep the documented lockstep.Line 51 declares
PILL_MAX_W = 780and the comment states it is "Shared by the pill render and the layout keep-out estimate so they stay in lockstep," yet the render hardcodesmaxWidth: 780. The keep-out memo (pillBox) does usePILL_MAX_W, so if the constant is ever changed the render and keep-out silently diverge.♻️ Reference the shared constant
whiteSpace: 'nowrap', - maxWidth: 780, + maxWidth: PILL_MAX_W, overflow: 'hidden',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Card/share-asset/ShareAssetD3.tsx` around lines 259 - 278, The pill render in ShareAssetD3 is hardcoding the width instead of using the shared limit, which can make it drift from the keep-out estimate. Update the span style in ShareAssetD3 to use PILL_MAX_W for maxWidth so it stays in lockstep with pillBox and the documented shared constant. Keep the change localized to the pill render block and reference the existing PILL_MAX_W symbol rather than repeating the numeric value.src/components/Card/share-asset/ShareAssetActions.tsx (1)
102-106: 🚀 Performance & Scalability | 🔵 TrivialNative share targets can ignore the caption when a file is attached. If caption consistency matters on mobile share sheets, keep the text-only Twitter intent fallback as the reliable path or render the caption into the image itself.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Card/share-asset/ShareAssetActions.tsx` around lines 102 - 106, The native sharing path in ShareAssetActions can drop the caption once a file is attached, so don’t rely on navigator.share in the file-based flow. Update the share logic around captureShareAsset and the filename/File creation to keep the text-only Twitter intent fallback as the dependable caption path, or ensure the caption is baked into the generated image before sharing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/Card/share-asset/RejectionAssetD3.tsx`:
- Around line 5-14: The docstring in RejectionAssetD3 contradicts the rendered
JSX by claiming the `@joinpeanut` handle is baked into the image, while the asset
only draws the mascot and “not tonight, <username>”. Update the comment near the
RejectionAssetD3 component to match the actual canvas output and state that the
handle is not included in the image. Keep the wording aligned with the
share-asset behavior so future edits don’t infer the PNG contains attribution.
In `@src/components/Card/share-asset/rejectionCaptions.ts`:
- Around line 9-10: Update the stale comment in rejectionCaptions so it matches
the current behavior of RejectionAssetD3: the `@joinpeanut` handle is no longer
baked into the asset image and only survives through the caption. Adjust the
docstring text near the asset-caption logic to remove the PNG repost claim and
accurately describe caption-only tagging, keeping the wording aligned with the
current implementation.
---
Nitpick comments:
In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx:
- Around line 393-405: The “Resulting props” panel in the share builder is
missing the hideUsername field even though ShareAssetD3 already consumes it.
Update the JSON shown in the page component so the object passed to
JSON.stringify includes hideUsername alongside username, badges, seedOverride,
heroMessage, usernameStyle, and animate, using the existing share-builder page
render logic as the reference point.
In `@src/components/Card/share-asset/ShareAssetActions.tsx`:
- Around line 102-106: The native sharing path in ShareAssetActions can drop the
caption once a file is attached, so don’t rely on navigator.share in the
file-based flow. Update the share logic around captureShareAsset and the
filename/File creation to keep the text-only Twitter intent fallback as the
dependable caption path, or ensure the caption is baked into the generated image
before sharing.
In `@src/components/Card/share-asset/ShareAssetD3.tsx`:
- Around line 259-278: The pill render in ShareAssetD3 is hardcoding the width
instead of using the shared limit, which can make it drift from the keep-out
estimate. Update the span style in ShareAssetD3 to use PILL_MAX_W for maxWidth
so it stays in lockstep with pillBox and the documented shared constant. Keep
the change localized to the pill render block and reference the existing
PILL_MAX_W symbol rather than repeating the numeric value.
In `@src/components/Card/share-asset/shareAssetLayout.ts`:
- Around line 118-138: The doc comment above STICKER_SIZE_BY_COUNT and
stickerSize is out of sync with the implementation: it mentions a 2300/count
fallback, but stickerSize() actually uses 2700/count. Update the comment so it
matches the real fallback curve and keeps the description aligned with the
behavior in stickerSize, preserving the existing tone about the size scaling and
legibility floor.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 35d41f7f-e5ec-413d-975c-682701458969
📒 Files selected for processing (21)
src/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/dev/rejection-builder/page.tsxsrc/app/(mobile-ui)/dev/share-builder/page.tsxsrc/components/Card/BadgeSkipCelebration.tsxsrc/components/Card/CardEligibilityCheckScreen.tsxsrc/components/Card/CardRejectionScreen.tsxsrc/components/Card/CardUnlockDrawer.tsxsrc/components/Card/CardWaitlistScreen.tsxsrc/components/Card/share-asset/PixelatedCardFace.tsxsrc/components/Card/share-asset/RejectionAssetD3.tsxsrc/components/Card/share-asset/ScaledRejectionAsset.tsxsrc/components/Card/share-asset/ShareAssetActions.tsxsrc/components/Card/share-asset/ShareAssetD3.tsxsrc/components/Card/share-asset/__tests__/shareAssetLayout.test.tssrc/components/Card/share-asset/rejectionCaptions.tssrc/components/Card/share-asset/share.utils.tssrc/components/Card/share-asset/shareAsset.types.tssrc/components/Card/share-asset/shareAssetLayout.tssrc/components/Card/share-asset/winCaptions.tssrc/constants/routes.tssrc/styles/globals.css
💤 Files with no reviewable changes (2)
- src/styles/globals.css
- src/components/Card/CardWaitlistScreen.tsx
Hugo0
left a comment
There was a problem hiding this comment.
lgtm. rec to make appeal join waitlist
- 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
|
CodeRabbit nitpick triage (commit 2c1bb95):
The two actionable docstring comments were resolved by baking the @coderabbitai review |
|
Kicking off the review now. ✅ Action performedReview finished.
|
…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.
|
Pushed one commit to this branch (
Touches only |
… 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.
|
@coderabbitai review |
✅ Action performedReview finished.
|
Punchier rejection lines (drop the trailing justifications), add a 'bouncer now??' jab and two Devconnect-callback rejections, plus an Argentina/global callback in the win pool — all keep the @joinpeanut tag.
Summary
Card-launch shareable assets. Started as the win-share sticker collage; now bundles the full share surface for the closed beta:
peanut.me/<handle>pill. Force-directed relaxation keeps badges from piling up; keep-outs reserve the rotated hero + the rendered pill; a final separation pass kills a bottom-right corner-trap (verified: worst centre-gap 0.22→0.75×size across a 7k-layout sweep).visibilityso toggling is instant and the hidden handle is excluded from the captured PNG.@joinpeanutcaption) that doubles as the waitlist-join CTA; the friendly joined screen is the cooldown.Risk
/cardwaitlist+waitlist-skip-celebrationstates and the history-replay drawer.CardWaitlistScreen(removed) withCardRejectionScreenin the not-joined slot.applicants/admitted(213/7) are intentional static scarcity copy.QA
/dev/share-builder(collage + hide-username),/dev/rejection-builder(rejection screen).Not in this PR
.env.sampleCARD_PUBLIC_LAUNCH_DATEchange is a separate repo concern.