fix(card): render share-asset hand as <img>, not a runtime <canvas> (blank-card hotfix)#2312
Conversation
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.
|
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: 4 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 (1)
📒 Files selected for processing (4)
Comment |
Code-analysis diffPainscore total: 5858.53 → 5859.06 (+0.53) 🆕 New findings (11)
✅ Resolved (10)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
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-imageclones a<canvas>by callingcanvas.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.share-assetcapture errors in Sentry (24h) despite live blank reports — consistent with the silent path, not a throw.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-imageinlines<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 byimage-rendering: pixelated, so it's visually identical to the old canvas.Bonus: removes the whole async-canvas readiness dance —
waitForAssetReadynow just awaits fonts +<img>decode.onReadystill 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.tsx—PixelatedHandis now an<img>(new assetpeanut-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.<img>gate; e2e capture spec (card centre must be non-blank) now guards the<img>path end-to-end.Gates
jest captureShareAsset✓ 6/6share-asset-capture.spec.tsruns 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.