Skip to content

fix(card): render the share asset's hand crisp, not pixelated#2516

Open
0xkkonrad wants to merge 3 commits into
devfrom
feat/share-asset-crisp-hand
Open

fix(card): render the share asset's hand crisp, not pixelated#2516
0xkkonrad wants to merge 3 commits into
devfrom
feat/share-asset-crisp-hand

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

The card share asset rendered its hand through a pre-pixelated PNG, so the one image users actually post looked low-fidelity. Nothing on that asset is hidden — the card number is already ???? and the Visa mark is dropped via hideVisa — so the pixelation had nothing left to obscure. It was only costing fidelity.

Pixelation belongs to the closed-beta tease, which is exactly what the existing blurAll prop already marks. The hand now follows that switch instead of being unconditional:

  • Share asset (ShareAssetD3, no blurAll) → crisp peanut-card-hand.svg, image-rendering: pixelated dropped.
  • Tease surfaces (all pass blurAll) → pre-pixelated PNG, unchanged: AddCardEntryScreen, CardEligibilityCheckScreen, BadgeSkipCelebration, /shhhhh LP hero.

Two files, +23/−17.

Risks / breaking changes

None cross-repo. Frontend-only, no API surface touched.

The one risk worth naming is the capture path. PixelatedCardFace carries a comment about the launch-day "blank share asset" bug, so the swap was made to preserve that fix rather than reopen it:

  • The hand stays a plain <img>. It is not returned to a runtime <canvas> — that was the actual cause of the blank card (canvas.toDataURL() returns empty on iOS Safari for an SVG-sourced canvas), and nothing here reintroduces it.
  • The bug was never about an SVG-sourced <img>. The peanut logo on this same card face is already <img src=…svg> and survives html-to-image today.
  • Verified directly anyway: the crisp face pushed through html-to-image at pixelRatio: 2 produces a 1560×1000 PNG with the hand fully present (screenshot 3 below).
  • ⚠️ That verification was desktop Chromium only. The original blank-card bug was iOS Safari–specific, and I have not reproduced the capture there. The precedent above (peanut logo, same face, same inline path, shipping today) is the reason I believe it holds — not a test on the affected browser. Worth one manual Save-image on an iPhone before merge.

onReady still fires off the hand's onLoad / complete, so the Share/Save capture gate is unchanged.

QA

  1. /dev/share-builder — the card's hand renders crisp; badges, burst and @username pill are untouched.
  2. Hit Save image — the downloaded PNG shows the crisp hand, not a blank card. This is the regression that matters.
  3. /card eligibility check, add-card entry, badge-skip celebration, /shhhhh — hand still pixelated, whole card still reads as one consistent tease.

Local gate: prettier --check . clean · typecheck clean · npm test 162/162 suites, 2143 tests · next build compiles.

Design notes / accepted trade-offs

Recorded so the next reader doesn't have to re-derive them.

  • Hand placement left as-is (deliberate). CardFace.tsx — the in-app card — renders this same SVG bottom-right at h-[90%], commented as matching the finalised Rain card art. The share asset renders it top-right at 74% of card width, overflowing the top edge, rotated −15°. While pixelated that read as an abstract blur; crisp, it's a recognisable hand in a placement that disagrees with the canonical art. Reviewed and kept — re-anchoring is a design change, not a cleanup, and belongs in its own PR.
  • Component keeps the name PixelatedCardFace even though pixelation is now the non-default. A rename touches 5 consumers plus tests; not worth widening a 2-file diff.
  • Pre-existing smell this PR reveals, NOT fixed here: CardFace.tsx and PixelatedCardFace.tsx are two independent implementations of the same card artwork. This change makes them diverge visually rather than converge. Out of blast radius — follow-up.
  • No test pins the blurAll → source mapping. Nothing stops a future refactor from routing the hand back through a <canvas> and silently reviving the blank-share-asset bug, which is the expensive failure mode here. This code doesn't move money or mutate shared state, so it isn't a hard gate — but it is the real coverage gap.

Screenshots

1. Real ShareAssetD3, after — native 1200×900 at 80%, username edcriptofi, 6 badges after
2. Card face, before vs after — identical hand box (top:-40 right:-20 560×471 rotate(-15deg)); only fidelity changes, the hand does not move before-after
3. Through html-to-image — what a user's shared PNG actually is captured

⚠️ How these were produced, precisely — this is not the standard phase-2b real-app-with-backend pipeline, so don't read it as one:

  • added wagmi-->ethers signer conversion for sdk compatibility #1 is the real <ShareAssetD3 /> component with fixed props, rendered via a throwaway route outside the (mobile-ui) auth gate (deleted; not in this diff). The API harness was not running, so the component was driven directly rather than by a seeded user.
  • Fetch Chain details from SDK #2 reproduces PixelatedCardFace's exact markup and geometry in a standalone page, in order to show both hand sources side by side — the pixelated variant cannot otherwise be rendered next to the crisp one.
  • hotfix: sdk version update #3 is the genuine html-to-image capture path from captureShareAsset, run against Fetch Chain details from SDK #2's crisp face.
  • No 375×667 mobile shots: this asset is a fixed 1200×900 share image, not a responsive screen.

Assets branch pr-assets-2516 is deleted post-merge.

Summary by CodeRabbit

  • New Features
    • Shared card images now show a crisp hand illustration by default, switching to the pixelated version when full blur is enabled.
  • Bug Fixes
    • Share capture readiness is now correctly aligned with when the card face’s hand image has loaded.
  • Documentation
    • Updated inline guidance and prop descriptions to reflect the new readiness behavior.

The share asset hides nothing — the card number is already "????" and the
Visa mark is dropped — so pixelating the hand only made the one image users
actually post look low-fidelity. Pixelation belongs to the closed-beta tease,
which is exactly what `blurAll` already marks, so the hand now follows that
switch instead of being unconditional.

Keeps the <img> capture path untouched (the pre-pixelated PNG swaps for the
vector source, same element, same box): the peanut logo on this card face is
already an <img src=…svg> that survives html-to-image, and a local capture
through html-to-image confirms the hand does too — the blank-share-asset bug
came from a runtime <canvas>, not from an SVG-sourced <img>.
@vercel

vercel Bot commented Jul 25, 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, Comment Jul 25, 2026 8:56am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3521923c-bd27-45ad-a1da-b1b647adb3fc

📥 Commits

Reviewing files that changed from the base of the PR and between 6576d2b and 7fc62b0.

📒 Files selected for processing (2)
  • local/scratch/pr-peanut-ui-feat-share-asset-crisp-hand.md
  • src/app/(mobile-ui)/dev/share-builder/page.tsx

📝 Walkthrough

Walkthrough

PixelatedCardFace now selects a crisp SVG or pre-pixelated PNG hand based on blurAll, while readiness documentation consistently references the hand image loading before capture.

Changes

Card hand rendering and readiness

Layer / File(s) Summary
Conditional card hand assets
src/components/Card/share-asset/PixelatedCardFace.tsx
Adds the crisp SVG hand asset and introduces CardHand, which selects crisp or pixelated rendering based on blurAll.
Image-based readiness contract
src/components/Card/share-asset/ShareAssetActions.tsx, src/components/Card/share-asset/captureShareAsset.ts, src/components/Card/share-asset/shareAsset.types.ts, src/components/Card/share-asset/PixelatedCardFace.tsx, src/app/(mobile-ui)/dev/share-builder/page.tsx
Updates readiness documentation to describe the hand image loading rather than canvas mounting.
PR tracking notes
local/scratch/pr-peanut-ui-feat-share-asset-crisp-hand.md
Adds PR status, environment, build, and screenshot notes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: hugo0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: making the share asset hand render crisp instead of pixelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/share-asset-crisp-hand

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6268.09 → 6268.99 (+0.9)
Findings: 0 net (+10 new, -10 resolved)

🆕 New findings (10)

  • medium complexity — src/components/Card/share-asset/PixelatedCardFace.tsx — CC 29, MI 58.5, SLOC 146
  • medium react-direct-dom — src/components/Card/share-asset/PixelatedCardFace.tsx:162 — direct DOM: document.createElement
  • medium react-direct-dom — src/components/Card/share-asset/PixelatedCardFace.tsx:179 — direct DOM: document.createElement
  • medium react-direct-dom — src/components/Card/share-asset/PixelatedCardFace.tsx:247 — direct DOM: document.createElement
  • medium nextjs-raw-img — src/components/Card/share-asset/PixelatedCardFace.tsx:102 — Use next/image
  • medium nextjs-raw-img — src/components/Card/share-asset/PixelatedCardFace.tsx:108 — Use next/image
  • medium nextjs-raw-img — src/components/Card/share-asset/PixelatedCardFace.tsx:297 — Use next/image
  • low high-mdd — src/components/Card/share-asset/PixelatedCardFace.tsx:234 — PixelatedText: MDD 13.8 (uses across many lines from declarations)
  • low high-mdd — src/components/Card/share-asset/PixelatedCardFace.tsx:245 — : MDD 13.3 (uses across many lines from declarations)
  • low missing-return-type — src/components/Card/share-asset/PixelatedCardFace.tsx:68 — PixelatedCardFace: exported fn missing return type annotation

✅ Resolved (10)

  • src/components/Card/share-asset/PixelatedCardFace.tsx — CC 27, MI 58.65, SLOC 145
  • src/components/Card/share-asset/PixelatedCardFace.tsx:158 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:175 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:243 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:98 — Use next/image
  • src/components/Card/share-asset/PixelatedCardFace.tsx:104 — Use next/image
  • src/components/Card/share-asset/PixelatedCardFace.tsx:290 — Use next/image
  • src/components/Card/share-asset/PixelatedCardFace.tsx:230 — PixelatedText: MDD 13.8 (uses across many lines from declarations)
  • src/components/Card/share-asset/PixelatedCardFace.tsx:241 — : MDD 13.3 (uses across many lines from declarations)
  • src/components/Card/share-asset/PixelatedCardFace.tsx:64 — PixelatedCardFace: exported fn missing return type annotation

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2146 ran, 0 failed, 0 skipped, 36.4s

📊 Coverage (unit)

metric %
statements 60.8%
branches 44.4%
functions 50.0%
lines 61.2%
⏱ 10 slowest test cases
time test
3.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
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/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/utils/__tests__/demo-balance.test.ts › auto-refills a stored balance that has no timestamp (legacy install)
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/demo-balance.test.ts › resetDemoBalance refills and restarts the TTL window
0.2s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

The hand stopped being a <canvas> two commits before this branch, but three
docblocks still described it as one — and PixelatedCardFace's header still
claimed a 620×391 native size that CARD_W/CARD_H outgrew (they're 760×479).
Restating layout constants in prose is what let it drift, so the header now
points at shareAssetLayout instead of repeating the numbers.

Also drops the conditional spread for imageRendering in favour of a plain
ternary — same output, one less thing to parse.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/Card/share-asset/PixelatedCardFace.tsx (1)

302-308: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make onReady idempotent.

onLoad triggers a parent re-render, and React later reattaches the inline callback ref with the same completed image DOM node. Since onReady is documented to fire once, guard both the onLoad and cached img.complete paths with a component-level useRef flag before calling it.

🤖 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/PixelatedCardFace.tsx` around lines 302 -
308, Make the onReady callback in PixelatedCardFace idempotent by adding a
component-level useRef flag that tracks whether readiness has been signaled.
Guard both the onLoad handler and the cached-image img.complete path in the ref
callback, setting the flag before invoking onReady so re-renders and ref
reattachments cannot trigger it more than once.
🤖 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.

Outside diff comments:
In `@src/components/Card/share-asset/PixelatedCardFace.tsx`:
- Around line 302-308: Make the onReady callback in PixelatedCardFace idempotent
by adding a component-level useRef flag that tracks whether readiness has been
signaled. Guard both the onLoad handler and the cached-image img.complete path
in the ref callback, setting the flag before invoking onReady so re-renders and
ref reattachments cannot trigger it more than once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 37d9cf2c-9545-474b-aa14-144689aa85d6

📥 Commits

Reviewing files that changed from the base of the PR and between b806cdc and 6576d2b.

📒 Files selected for processing (4)
  • src/components/Card/share-asset/PixelatedCardFace.tsx
  • src/components/Card/share-asset/ShareAssetActions.tsx
  • src/components/Card/share-asset/captureShareAsset.ts
  • src/components/Card/share-asset/shareAsset.types.ts

Missed in the previous sweep — same rot, same reason: the hand has been an
<img> since before this branch.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

302-308Make onReady idempotent

Valid observation, declining for this PR. Reasons, in order:

  1. Pre-existing and outside this diff. The onLoad + img.complete pair predates the branch; this PR only changes which src the <img> gets. CodeRabbit correctly labelled it "outside diff range". Repo convention here is to mention adjacent issues rather than fold them into an unrelated diff.
  2. Inert with every current consumer. All four call sites do exactly setAssetReady(true)BadgeSkipCelebration.tsx:171, CardUnlockDrawer.tsx:74, dev/share-builder/page.tsx:396, and ShareAssetD3 forwarding to those. Same-value setState, which React bails out on, so the re-fire converges instead of looping. Checked each one rather than assuming.
  3. The latent risk is real but hypothetical today: a future consumer doing something non-idempotent in onReady (a PostHog event, say) would double-count. That's worth fixing — on its own, where the change is reviewable as what it is.

The docblock wording is the actual source of the ambiguity: "fires once the hand <img> has loaded" means as soon as, not exactly once. Not tightening it here so the diff stays about the pixelation, but noted for the follow-up.

No other findings from this review; nothing else open.

@0xkkonrad
0xkkonrad marked this pull request as ready for review July 25, 2026 09:59
@0xkkonrad
0xkkonrad requested a review from Hugo0 July 25, 2026 09:59
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

1 participant