Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1522abf
fix(kyc): surface per-label reject copy in action-required drawer
jjramirezn Jun 26, 2026
b0a1b09
fix(kyc): collapse duplicated reject-labels branch + test real copy
jjramirezn Jun 26, 2026
1f7fab0
fix(card): grey + "Soon!" the guest claim-to-bank option (under maint…
Hugo0 Jun 26, 2026
ece9cb5
Merge pull request #2294 from peanutprotocol/hotfix/guest-bank-soon
Hugo0 Jun 27, 2026
cfc9955
Merge pull request #2298 from peanutprotocol/dev
Hugo0 Jun 29, 2026
323fa61
fix(card): bring launch CTA in line with the activation CTAs
Hugo0 Jun 29, 2026
8d78395
fix(card): match activation-CTA border weight (1px) + spell shhhh
Hugo0 Jun 29, 2026
80ffc2d
Merge pull request #2299 from peanutprotocol/hotfix/card-launch-cta-d…
Hugo0 Jun 29, 2026
91be7b6
Merge pull request #2293 from peanutprotocol/fix/kyc-reject-label-pre…
Hugo0 Jun 29, 2026
248c33e
fix(card): wait for the card-face canvas before capturing the share a…
Hugo0 Jun 29, 2026
aeb2c46
Merge pull request #2302 from peanutprotocol/hotfix/share-asset-blank…
Hugo0 Jun 29, 2026
574ce1b
feat(card): end the waitlist flow on the rejection/appeal screen (dro…
Hugo0 Jun 29, 2026
4e8af84
fix(card): match joined-state indicator to button height (no layout s…
Hugo0 Jun 29, 2026
622968b
Merge pull request #2303 from peanutprotocol/hotfix/waitlist-end-on-r…
Hugo0 Jun 29, 2026
eb6646b
fix(card): address CodeRabbit on the terminal waitlist screen
Hugo0 Jun 29, 2026
55f7677
Merge pull request #2304 from peanutprotocol/hotfix/rejection-screen-…
Hugo0 Jun 29, 2026
4b0c8e3
test(card): regression guard for the blank share-asset capture race
Hugo0 Jun 29, 2026
1bd95df
Merge pull request #2305 from peanutprotocol/test/share-asset-capture…
Hugo0 Jun 29, 2026
7cd807a
fix(card): raise apply-for-card fetch timeout to 60s
jjramirezn Jun 29, 2026
2ec8b5d
Merge pull request #2306 from peanutprotocol/hotfix/rain-apply-timeout
Hugo0 Jun 29, 2026
3dbc068
feat(card): real FOMO door tally on rejection screen
Hugo0 Jun 29, 2026
b6e187d
fix(card): bulletproof share-asset capture — faithful shadows + deter…
Hugo0 Jun 29, 2026
2b621d1
fix(card): make waitlistTotal/admittedTotal optional on CardInfoResponse
Hugo0 Jun 29, 2026
4092778
feat(kyc): make EEA-uplift verification a mandatory gate + funnel ana…
kushagrasarathe Jun 29, 2026
06928d6
test(card): decode captured PNG in-browser, drop the sharp dep
Hugo0 Jun 29, 2026
09345b9
tune(card): door-tally FOMO multiplier x3 -> x5
Hugo0 Jun 29, 2026
2793c87
Merge pull request #2307 from peanutprotocol/feat/door-tally-real-counts
Hugo0 Jun 29, 2026
33f7d7e
fix(analytics): clear eea-uplift start on KYC abandon
kushagrasarathe Jun 29, 2026
9de604b
Merge pull request #2308 from peanutprotocol/fix/share-asset-bulletproof
Hugo0 Jun 29, 2026
e6a6aa9
fix(kyc): address review — fire uplift-completed at approval; sync mo…
kushagrasarathe Jun 29, 2026
0185213
feat(card): show the specific reason on the rejected-card screen + re…
jjramirezn Jun 29, 2026
3780741
test(card): silence next/image stub lint warning in rejected-screen test
jjramirezn Jun 29, 2026
216912a
fix(card): render rejected screen immediately + assert reason placeme…
jjramirezn Jun 29, 2026
e187dcc
fix(card): force space in physical-waitlist position copy via templat…
Hugo0 Jun 29, 2026
70272d1
Merge pull request #2310 from peanutprotocol/feat/card-rejection-reason
Hugo0 Jun 29, 2026
ee38e7d
Merge pull request #2311 from peanutprotocol/hotfix/card-waitlist-pos…
Hugo0 Jun 29, 2026
fdda3db
Merge pull request #2309 from peanutprotocol/feat/eea-uplift-hard-gate
Hugo0 Jun 29, 2026
a2e8f33
fix(card): render share-asset hand as <img>, not a runtime <canvas>
Hugo0 Jun 29, 2026
6a5307a
Merge pull request #2312 from peanutprotocol/hotfix/share-asset-hand-img
Hugo0 Jun 29, 2026
9df5c58
fix(shhhhh): inflate the waitlist position to match the in-app door t…
Hugo0 Jun 30, 2026
6acdc53
feat(badges): add Floripa Fuel (FLORIPA_COFFEE) badge
0xkkonrad Jul 1, 2026
ef47083
refactor(badges): FLORIPA_COFFEE → IRL_NOMADS (Nomad Mode, backpack)
0xkkonrad Jul 1, 2026
839d057
refactor(badges): rename BADGES + campaign-maps FLORIPA_COFFEE → IRL_…
0xkkonrad Jul 1, 2026
35314dd
hotfix(maintenance): block Manteca add-money + withdraw during provid…
jjramirezn Jul 1, 2026
b79c68a
Merge pull request #2321 from peanutprotocol/hotfix/manteca-transfers…
Hugo0 Jul 1, 2026
7e9a25f
hotfix(maintenance): make Manteca outage block per-currency; re-enabl…
jjramirezn Jul 1, 2026
5994413
Merge pull request #2322 from peanutprotocol/hotfix/manteca-per-curre…
Hugo0 Jul 1, 2026
ce10e96
hotfix(manteca): read effectivePrice from new /prices shape; re-enabl…
jjramirezn Jul 1, 2026
7b80273
Merge pull request #2323 from peanutprotocol/hotfix/manteca-price-eff…
Hugo0 Jul 1, 2026
6006ac5
feat(badges): add IRL_NOMADS "Nomad Mode" badge art + campaign maps →…
Hugo0 Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions e2e/flows/share-asset-capture.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Share-asset capture regression — the card face must NOT be blank.
*
* The launch-day bug: <PixelatedCardFace /> painted its pixelated hand into a
* runtime <canvas>, which html-to-image silently dropped when it couldn't
* serialise it (toDataURL() returns empty on iOS Safari for an SVG-sourced
* canvas) → blank pink card, and the capture SUCCEEDED so nothing reached Sentry.
*
* The fix renders the hand as a plain pre-pixelated <img> (the same reliable
* path the badge stickers take), and still gates Save/Share on PixelatedCardFace's
* `onReady` (the hand <img> loading) so a capture can't fire before it's ready.
*
* This spec proves the guard end-to-end against the real html-to-image path:
* 1. /dev/share-builder renders <ShareAssetD3 /> and wires "Save image" to
* captureShareAsset + downloadBlob (the same capture code the in-app
* Share/Save buttons use).
* 2. Save stays disabled until the card face signals ready — we wait for it
* to enable (proves the gate releases only after the hand <img> loads).
* 3. We click Save, intercept the downloaded PNG, decode it IN-BROWSER (an
* <img> + <canvas>.getImageData — no extra Node dep), and sample a region
* in the CENTRE of the card — the hand's territory, away from the top-left
* logo and bottom-left card number. If that region is ENTIRELY the card
* pink (#FF90E8) the hand never rendered → the blank-card bug. We assert
* it contains non-background pixels (the hand was captured).
*
* No harness auth needed — /dev/share-builder is a pure client-render dev page.
*/

import { test, expect } from '@playwright/test'
import { readFileSync } from 'fs'

// Asset + card geometry — mirror of shareAssetLayout.ts (CANVAS_W/H, CARD_*).
// Hardcoded (not imported) to match the e2e convention of not pulling app code
// through the '@/' alias into the Playwright tsconfig.
const CANVAS_W = 1200
const CANVAS_H = 900

// Card-pink (PixelatedCardFace background) and asset-blue (ShareAssetD3 bg) as
// [r,g,b]. "Background" = either of these; the hand is neither.
const CARD_PINK: [number, number, number] = [0xff, 0x90, 0xe8]
const ASSET_BLUE: [number, number, number] = [0x90, 0xa8, 0xed]
const COLOR_TOL = 12

// Central card region in 1200×900 canvas coords. The card is centred
// (CARD_LEFT 220, CARD_TOP 210, CARD_W 760, CARD_H ≈ 479 → centre ≈ 600,449).
// This window sits well inside the card and is HAND-ONLY: it excludes the
// peanut logo (top-left ~248–300 px) and the "????" number (bottom-left,
// canvas-y ≳ 615). So a blank card → this window is pure pink; the hand
// present → many non-background pixels. Small enough that the card's -8°
// final rotation can't rotate any background (blue) into it.
const REGION = { x0: 430, y0: 330, x1: 770, y1: 570 }
const SAMPLE_STEP = 6 // sample every 6th canvas-px → a dense grid

test.describe('Share-asset capture (card face is not blank)', () => {
test('captured PNG card region contains the hand, not just background', async ({ page }, testInfo) => {
await page.goto('/dev/share-builder', { waitUntil: 'domcontentloaded' })

const saveBtn = page.getByTestId('save-image')

// The readiness gate: Save is disabled until the card face's hand <img>
// loads (onReady). If it never enables, the gate is broken OR the card
// never painted — both are failures this spec must catch.
await expect(saveBtn, 'Save must enable once the card face signals ready').toBeEnabled({ timeout: 60_000 })

// Let the card-slide / sticker-drop animations settle to their final
// frame so the card sits at its designed (centred, -8°) position before
// we capture. The capture itself overrides only the root scale; the
// card's own transform is whatever frame it's on.
await page.waitForTimeout(2_500)
Comment on lines +63 to +69

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the test exercise the actual Save-ready contract.

After the gate is fixed to include animation stability, remove this post-enable timeout and click Save immediately. Otherwise the spec can pass even when users can still capture a mid-animation frame.

🤖 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 `@e2e/flows/share-asset-capture.spec.ts` around lines 63 - 69, The share-asset
capture spec currently waits after Save becomes enabled, which can hide
mid-animation capture bugs. In share-asset-capture.spec.ts, remove the
post-enable wait after the assertion on saveBtn and click Save immediately once
the Save-ready condition is satisfied so the test exercises the actual contract.
Keep the readiness check tied to the existing saveBtn expectation and the
capture flow that follows it.


// Click Save → captureShareAsset(node) → downloadBlob() fires a real
// download of the native-resolution PNG.
const downloadPromise = page.waitForEvent('download', { timeout: 30_000 })
await saveBtn.click()
const download = await downloadPromise
const pngPath = testInfo.outputPath('share-asset-capture.png')
await download.saveAs(pngPath)

// Decode + sample IN-BROWSER: load the PNG into an <img>, paint it to a
// <canvas>, read it back with getImageData, and count non-background
// pixels in the central card window. Done in the page (not Node) so we
// need no PNG-decoder dependency.
const base64 = readFileSync(pngPath).toString('base64')
const result = await page.evaluate(
async ({ b64, canvasW, canvasH, region, pink, blue, tol, step }) => {
const img = new Image()
img.src = `data:image/png;base64,${b64}`
await img.decode()

const cv = document.createElement('canvas')
cv.width = img.naturalWidth
cv.height = img.naturalHeight
const ctx = cv.getContext('2d')
if (!ctx) throw new Error('no 2d context')
ctx.drawImage(img, 0, 0)

const width = cv.width
const height = cv.height
const buf = ctx.getImageData(0, 0, width, height).data

// Output is captured at pixelRatio 2 (≈2400×1800) — scale canvas
// coords into output pixels.
const sx = width / canvasW
const sy = height / canvasH
const near = (r: number, g: number, b: number, c: number[]): boolean =>
Math.abs(r - c[0]) <= tol && Math.abs(g - c[1]) <= tol && Math.abs(b - c[2]) <= tol

let sampled = 0
let nonBackground = 0
for (let cy = region.y0; cy <= region.y1; cy += step) {
for (let cx = region.x0; cx <= region.x1; cx += step) {
const px = Math.min(width - 1, Math.round(cx * sx))
const py = Math.min(height - 1, Math.round(cy * sy))
const idx = (py * width + px) * 4
const r = buf[idx]
const g = buf[idx + 1]
const b = buf[idx + 2]
sampled++
if (!(near(r, g, b, pink) || near(r, g, b, blue))) nonBackground++
}
}
return { width, height, sampled, nonBackground }
},
{
b64: base64,
canvasW: CANVAS_W,
canvasH: CANVAS_H,
region: REGION,
pink: CARD_PINK as number[],
blue: ASSET_BLUE as number[],
tol: COLOR_TOL,
step: SAMPLE_STEP,
}
)

const fraction = result.nonBackground / result.sampled
// Blank card → fraction ≈ 0 (pure pink). Hand present → a large chunk of
// the centre is non-pink. A 2% floor cleanly separates the two while
// staying far below the hand's real coverage (avoids flake).
expect(
fraction,
`card centre is ${(fraction * 100).toFixed(1)}% non-background ` +
`(${result.nonBackground}/${result.sampled} px; output ${result.width}×${result.height}). ` +
`≈0% means the pixelated hand never rendered — the blank-card capture bug.`
).toBeGreaterThan(0.02)
})
})
Loading
Loading