Skip to content

fix(card): render share-asset hand as <img>, not a runtime <canvas> (blank-card hotfix)#2312

Merged
Hugo0 merged 1 commit into
mainfrom
hotfix/share-asset-hand-img
Jun 29, 2026
Merged

fix(card): render share-asset hand as <img>, not a runtime <canvas> (blank-card hotfix)#2312
Hugo0 merged 1 commit into
mainfrom
hotfix/share-asset-hand-img

Conversation

@Hugo0

@Hugo0 Hugo0 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

The blank share-asset card — actual root cause

Posts like @ghadi8798's kept showing the "I'M IN!" asset with a blank pink card face (badges, starburst, username all fine — just the hand missing), even though #2308's readiness gate went live at 15:02Z, ~57 min before his 15:58Z capture. So this was never a "fix not deployed yet" / timing problem.

Real cause: the pixelated hand was the only <canvas> in the captured asset, drawn from an SVG. html-to-image clones a <canvas> by calling canvas.toDataURL(), and when that returns empty it silently swaps in a blank canvas (node_modules/html-to-image/lib/clone-node.js:49) — no throw, no Sentry. iOS Safari returns empty for an SVG-tainted canvas. Capture "succeeds" → blank card, zero error signal.

Fix — make the hand bulletproof to capture

Render the hand as a plain pre-pixelated <img> instead of a runtime canvas — the exact path the badge stickers take (they never blank). html-to-image inlines <img> reliably on every browser, so there's no canvas to taint or silently drop. The PNG is baked at the 36px raster and upscaled by image-rendering: pixelated, so it's visually identical to the old canvas.

Bonus: removes the whole async-canvas readiness dance — waitForAssetReady now just awaits fonts + <img> decode. onReady still fires (on <img> load, with a cached-image fallback) so the Share/Save gate is preserved.

This solves both failure modes at once: the iOS SVG-canvas taint and any stale-bundle client that captured before the canvas mounted.

Scope

  • PixelatedCardFace.tsxPixelatedHand is now an <img> (new asset peanut-card-hand-pixel.png). The blurAll logo/number canvases (eligibility/LP tease surfaces, not captured) are untouched. CardFace.tsx (the real in-app card) already used the SVG as a plain <img> — untouched.
  • captureShareAsset.ts — dropped the canvas poll + now-dead timeout.
  • Unit tests rewritten for the <img> gate; e2e capture spec (card centre must be non-blank) now guards the <img> path end-to-end.

Gates

  • prettier ✓ · typecheck ✓ (0 errors) · jest captureShareAsset ✓ 6/6
  • e2e share-asset-capture.spec.ts runs in CI (the real render+capture proof)

QA

Open the Vercel preview/dev/share-builder, wait for "Save image" to enable, Save → the downloaded PNG must show the pixelated hand on the card (not blank pink).

Note (separate, not in this PR)

Returning installed-PWA users can run a stale pre-fix bundle until a cold start (controllerchange auto-reload is intentionally skipped for standalone PWAs to avoid the Android redirect loop). This PR makes the asset bulletproof once they're on it; forcing PWA reload is a separate decision.

Why: the "I'M IN!" / rejection share assets kept capturing with a blank
pink card face for some users (ghadi, ubong) even though #2308's readiness
gate had been live 57min before the capture. Root cause is NOT timing — the
hand was the only <canvas> in the asset, drawn from an SVG, and html-to-image
silently substitutes a blank canvas when canvas.toDataURL() returns empty
(node_modules/html-to-image/lib/clone-node.js), which iOS Safari does for an
SVG-tainted canvas. The capture "succeeds", so nothing reaches Sentry
(confirmed: zero share-asset capture errors despite live blank reports). A
mount-gate can't fix a capture-time serialisation failure.

Fix: render the hand as a plain pre-pixelated <img> — the same path the badge
stickers take, which never blank. html-to-image inlines <img> reliably on
every browser. The PNG is baked at the 36px raster and upscaled by
image-rendering:pixelated, so it reads identically to the old canvas. This
also drops the async-canvas dance from capture (waitForAssetReady now just
awaits fonts + <img> decode). onReady still fires on <img> load (with a
cached-image fallback) so the Share/Save gate is preserved; the e2e capture
spec guards the <img> path end-to-end.
@vercel

vercel Bot commented Jun 29, 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 Jun 29, 2026 4:36pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3ca88e2-9768-4b1c-ab90-05aa7d47fc94

📥 Commits

Reviewing files that changed from the base of the PR and between fdda3db and a2e8f33.

⛔ Files ignored due to path filters (1)
  • src/assets/cards/peanut-card-hand-pixel.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • e2e/flows/share-asset-capture.spec.ts
  • src/components/Card/share-asset/PixelatedCardFace.tsx
  • src/components/Card/share-asset/__tests__/captureShareAsset.test.ts
  • src/components/Card/share-asset/captureShareAsset.ts

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

@Hugo0 Hugo0 merged commit 6a5307a into main Jun 29, 2026
16 of 19 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5858.53 → 5859.06 (+0.53)
Findings: +1 net (+11 new, -10 resolved)

🆕 New findings (11)

  • medium complexity — src/components/Card/share-asset/PixelatedCardFace.tsx — CC 27, MI 58.65, SLOC 145
  • medium complexity — src/components/Card/share-asset/captureShareAsset.ts — CC 21, MI 60.1, SLOC 77
  • medium react-direct-dom — src/components/Card/share-asset/PixelatedCardFace.tsx:158 — direct DOM: document.createElement
  • medium react-direct-dom — src/components/Card/share-asset/PixelatedCardFace.tsx:175 — direct DOM: document.createElement
  • medium react-direct-dom — src/components/Card/share-asset/PixelatedCardFace.tsx:243 — direct DOM: document.createElement
  • medium nextjs-raw-img — src/components/Card/share-asset/PixelatedCardFace.tsx:98 — Use next/image
  • medium nextjs-raw-img — src/components/Card/share-asset/PixelatedCardFace.tsx:104 — Use next/image
  • medium nextjs-raw-img — src/components/Card/share-asset/PixelatedCardFace.tsx:290 — Use next/image
  • low high-mdd — src/components/Card/share-asset/PixelatedCardFace.tsx:230 — PixelatedText: MDD 13.8 (uses across many lines from declarations)
  • low high-mdd — src/components/Card/share-asset/PixelatedCardFace.tsx:241 — : MDD 13.3 (uses across many lines from declarations)
  • low missing-return-type — src/components/Card/share-asset/PixelatedCardFace.tsx:64 — PixelatedCardFace: exported fn missing return type annotation

✅ Resolved (10)

  • src/components/Card/share-asset/PixelatedCardFace.tsx — CC 30, MI 57.37, SLOC 157
  • src/components/Card/share-asset/captureShareAsset.ts — CC 28, MI 61.26, SLOC 94
  • src/components/Card/share-asset/PixelatedCardFace.tsx:163 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:180 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:248 — direct DOM: document.createElement
  • src/components/Card/share-asset/PixelatedCardFace.tsx:103 — Use next/image
  • src/components/Card/share-asset/PixelatedCardFace.tsx:109 — Use next/image
  • src/components/Card/share-asset/PixelatedCardFace.tsx:235 — PixelatedText: MDD 13.8 (uses across many lines from declarations)
  • src/components/Card/share-asset/PixelatedCardFace.tsx:246 — : MDD 13.3 (uses across many lines from declarations)
  • src/components/Card/share-asset/PixelatedCardFace.tsx:69 — PixelatedCardFace: exported fn missing return type annotation

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1629 ran, 0 failed, 0 skipped, 26.9s

📊 Coverage (unit)

metric %
statements 54.6%
branches 37.3%
functions 42.4%
lines 54.5%
⏱ 10 slowest test cases
time test
3.9s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.4s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Manteca PIX form ready shows merchant card + amount input + pay button
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid UK IBAN with spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address in lowercase
📍 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.

1 participant