Conversation
The action-required drawer checked actionMessage first, but the backend always sends a generic "resubmit your documents" actionMessage for every action_required state — so the specific reject-label copy (e.g. DUPLICATE_EMAIL → "Email already in use, sign in to that account or contact support") was never reachable. Users hitting an email collision saw a misleading "verify your ID" prompt with no actionable next step. Prefer RejectLabelsList when reject labels are present; fall back to the generic actionMessage only when there are none.
Address /code-review findings: collapse the three-branch ternary to two (RejectLabelsList already renders its own empty-state fallback, so the duplicated branch was dead weight), and add an integration test that renders the real RejectLabelsList so the DUPLICATE_EMAIL → 'Email already in use' copy mapping is actually exercised, not mocked away.
…enance) The guest claim-to-bank off-ramp is paused for the Monday Card launch — the backend now 503s POST /bridge/offramp/create-for-guest (pairs with the BE hotfix). Letting unverified claimers enter that flow would dead-end on a 503, so the bank method is now rendered greyed + non-interactive with a "Soon!" badge whenever the claim resolves to GuestBankClaim (claimer unverified, sender can receive a bank off-ramp). The authenticated self off-ramp (UserBankClaim → /users/accounts + /bridge/offramp/create) is untouched and stays fully clickable.
fix(card): guest claim-to-bank under maintenance (grey + SOON)
Prod Release Sprint 149 — cross-chain withdraw · card-launch CTA · share-asset · badges (2026-06-29)
Launch-day design review (Konrad): the banner was off-system — pink (pink=clickable in Peanut), a black drop-shadow that misaligned the bounding box, and a black CTA used nowhere else. Now a white card, no drop shadow, standard purple primary CTA — matching the other activation CTAs. Copy + /shhhhh routing + gating unchanged; cosmetic only.
Border was border-2 (2px) vs the activation CTAs' 1px (Global Card = border border-black); drop to 1px. Title shhh → shhhh (one more h).
…esign-system fix(card): bring launch CTA in line with the activation CTAs
…cedence fix(kyc): surface per-label reject copy in action-required drawer
…sset The launch-day blank share asset: PixelatedCardFace paints the pixelated hand into a <canvas> appended asynchronously (new Image() -> onload -> appendChild), and captureShareAsset called html-to-image without waiting for it. html-to-image awaits <img> but not a not-yet-mounted <canvas>, so a fast/first-of-session share captured a blank card — just the pink box + its floating drop-shadow (the 'fucked shadow'). Silent: capture succeeds, nothing hits Sentry. Gate capture on document.fonts.ready + every <img>.decode() + the hand <canvas> being mounted, bounded by a 2.5s timeout so it never hangs. Win/celebration asset only; rejection asset has no canvas and is unaffected.
…-capture fix(card): blank share asset — wait for the card-face canvas before capture
…p the cooldown) Stuck-outside users used to flip from CardRejectionScreen (shareable 'not tonight' asset + Tweet to appeal) to a bare CardWaitlistJoinedScreen cooldown once they joined — and every later /card visit showed that cooldown, so they could never re-grab/re-share their appeal asset. Make the rejection screen the TERMINAL waitlist screen: once joined (alreadyJoined) the 'Join anyway' button becomes an 'on the list' confirmation, but the asset + Tweet-to-appeal stay. Symmetric with the in-crowd. Delete CardWaitlistJoinedScreen; /dev/rejection-builder gets an alreadyJoined toggle to QA both states.
…hift) The 'on the list' confirmation that replaces the 'Join anyway' button was py-2 (~36px) vs the button's h-13 (52px), so the CTA block shrank on join and shifted the layout. Fix the indicator to h-13 so the swap is height-stable.
…ejection feat(card): waitlist ends on the rejection/appeal screen (drop the cooldown)
1) Local 'locallyJoined' override (set only on a confirmed join) so the 'Join anyway' CTA swaps to the on-the-list state immediately instead of flickering back until the /card refetch resolves. 2) CARD_WAITLIST_VIEWED now reports the real already_joined (the screen can mount already-joined). 3) handleAppeal clears any stale joinError so 'Failed to join' can't render alongside 'You're on the list'.
…cr-fixes fix(card): address CodeRabbit on the terminal waitlist screen
Export waitForAssetReady (with an injectable timeout) and add 4 tests proving the capture gate blocks the html-to-image snapshot until the async card-face <canvas> has mounted — the exact element whose absence produced the launch-day blank share asset (#2302) — plus: bounded so it never hangs, awaits every <img>.decode(), and awaits document.fonts.ready. 6/6 pass.
…-readiness test(card): regression guard for the blank share-asset capture
The first-time Rain card application path is structurally 7-13s (7 sequential Sumsub calls + a 2.5s readiness sleep + Rain createApplication + optional inline issueCard). The global 10s fetchWithSentry default aborts that tail client-side while the backend keeps running to completion, so users see a false 'application failed' error on a card that was actually submitted. PostHog: the rain/cards timeout exception fires ~280x/week across ~40 distinct users; of 120 users who hit it in 30d only 28 reached card_apply_succeeded. Override timeoutMs for this one call only (browser-side fetch, so the 15s Vercel function ceiling that caps the global default does not apply here).
fix(card): raise apply-for-card fetch timeout to 60s
The 'not tonight' Berghain screen rendered a hardcoded '213 tried · 7 got in' (props defaulted to 213/7 and the page never passed real values). Now /card returns waitlistTotal + admittedTotal; the screen inflates 'tried' for FOMO (real waitlist size x3, floored at 213 — mirrors the /shhhhh ScarcityCounter fake-scarcity flex) and shows the real 'got in'. Inflation is a pure, deterministic fn so it never jitters between renders; a sane 213/7 fallback covers the still-loading window. /dev/rejection-builder now drives the real counts and previews the inflated result. Requires the BE PR (waitlistTotal/admittedTotal on /card) deployed first.
…ministic ready gate The card launched today; the share asset users post had two capture bugs. 1) Square shadows. html-to-image renders CSS box-shadow on a rounded element as a SQUARE block, so the captured PNG showed square shadows behind the rounded peanut.me/<user> pill and the card itself. Replace those box-shadows with offset black sibling elements that share the border-radius — they capture as faithful rounded shadows. (Hero stickers use filter:drop-shadow, which captures fine — left as is.) 2) Blank card. PixelatedCardFace paints its pixelated hand into a <canvas> appended asynchronously, so a capture firing before the canvas mounted snapshotted a blank pink card — and the capture SUCCEEDED, so nothing reached Sentry. The bounded waitForAssetReady wait (PR #2302) can time out under load. Deterministic fix: PixelatedCardFace fires onReady once the hand canvas mounts; it threads up through ShareAssetD3 to the Share/Save buttons, which stay disabled until the asset signals ready. waitForAssetReady stays as a belt-and-suspenders fallback. Also wires the /dev/share-builder "Save image" button to the real capture path and adds an e2e regression guard that decodes the captured PNG and asserts the card centre (the hand's territory) is not entirely background — proving the card face actually renders.
CI typecheck caught existing CardInfoResponse mocks (e.g. cardState.utils .test.ts) breaking because the new fields were required. Optional is also the correct contract: the BE deploys first, but the FE must tolerate undefined during the rollout window (and for any older API) — computeDoorTally already falls back to 213/7. Full `tsc --noEmit` + `npm test` now green.
…lytics Users were skipping the EEA-uplift KYC via the modal's "Not now" / close and then continuing to transact, so the requirement never got completed before a bank transfer. Make the pre-empt mandatory: the modal is non-closable and non-skippable (preventClose + hideModalCloseButton, "Not now" removed), and the hook blocks the deferred deposit/withdraw action until the requirement clears. Only "Complete now" moves forward. Add dedicated PostHog events (eea_uplift_started / eea_uplift_completed, with channel + requirement_key) so the start->finish funnel can be filtered directly. Completion is ref-guarded so generic KYC successes on the bank pages don't mis-fire it. Scope: the advisory hard-gate path on add-money/[country]/bank and withdraw/[country]/bank. Front-end enforcement only — does not change Bridge's underlying requirement.
CI typecheck couldn't resolve 'sharp' (not a declared dependency). Decode + sample the captured PNG in-page via an <img> + <canvas>.getImageData inside page.evaluate instead — same not-empty assertion, zero new dependencies, so the supply-chain min-release-age gate stays untouched.
At x3 the real number is masked by the 213 floor (prod waitlist ~55 -> 55x3 = 165 < 213, so it just renders the hardcoded-looking 213). x5 = 275 clears the floor, so the real inflated count actually shows. Counts source + floor + fallback unchanged.
feat(card): real FOMO door tally on rejection screen
The startedRef latch was only cleared on success, so an abandoned uplift attempt could leave it set and a later unrelated KYC success on the same mounted page would mis-fire eea_uplift_completed (inflating the funnel). Reset the pending start via the KYC flow's onManualClose (abandon) on both bank pages. Covers the main mis-attribution path surfaced in review.
fix(card): bulletproof share-asset capture — faithful shadows + deterministic ready gate
…dal lifecycle CodeRabbit + review findings: - Move eea_uplift_completed off the end-of-flow onKycSuccess onto a new onKycApproved callback fired at the Sumsub approval transition, so it isn't lost when the user drops during post-approval ToS/preparing (undercount) and isn't fired on a ToS-skip without finishing (overcount). - useAdvisoryPreempt: auto-close the modal when the advisory clears while open, and re-show it if the launch fails — the hard gate must not silently vanish or linger over an already-cleared requirement. - useEeaUpliftFunnel: require a real advisory for trackStarted (no partial started payloads).
…assure The "We couldn't issue you a card" screen showed only a generic catch-all body. Surface the vetted, region-aware reason from the capability read-model (e.g. "Peanut cards aren't available in your state yet.") above a reassurance body — a declined card doesn't touch the rest of the account, so point the user back to crypto deposit / withdraw / pay, which still work.
…nt (CodeRabbit) Don't spinner-gate the rejected screen on capabilities — it's useful without the reason (reassurance + support CTA), so show it now and fill reasonMessage once capabilities resolve. Strengthen the test to assert the reason renders ABOVE the body (DOM order) and to count paragraphs generically instead of phrase-matching.
…e literal
The "You are #N on the list" copy ships to prod AND staging as
`["You are #", position, "on the list…"]` — no leading space — so it
renders "You are #16on the list". The source has a same-line space
(prettier enforces it and reverts `{' '}`), and a clean next-swc compile
keeps the space, so the live no-space bundle is a stale build artifact.
A template literal bakes the space into one JS string (immune to JSX
whitespace handling) and changes the module so the stale artifact can't
be reused. Matches the existing pattern in JoinWaitlistPage.tsx.
feat(card): specific reason on the rejected-card screen + crypto reassurance
…ition-space
fix(card): space in physical-waitlist position copy ("#16on" → "#16 on")
feat(kyc): mandatory EEA-uplift gate + funnel analytics
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.
fix(card): render share-asset hand as <img>, not a runtime <canvas> (blank-card hotfix)
…ally /shhhhh's 'you're on the waitlist · #56' showed the user's REAL queue position, while the in-app Berghain rejection shows the FOMO-inflated door tally ('275 tried'). The small real number on the landing page undercut the big scarcity number in the app. Scale the /shhhhh position by the same DOOR_TALLY_FOMO_MULTIPLIER (x5) via a shared inflateWaitlistPosition() helper so both surfaces tell one consistent exaggerated story (#56 -> #280).
Adds the 'Floripa Fuel' badge art + FE wiring for the Floripa 'Coffee for Nomads' nomad event. New sticker asset (badge-draft/badges-recraft), BADGES entry, and campaign-maps so /invite resolves the badge from the invite code (floripacoffee), utm_campaign (floripa-coffee), and as a bare vanity ?campaign=FLORIPA_COFFEE claim link. Backend whitelist in peanut-api-ts (companion PR).
Swap the event badge to the nomad concept: new backpack asset
irl_nomads.svg, BADGES entry IRL_NOMADS ('Nomad Mode'), and campaign
maps (invite irl_nomads / utm irl-nomads / bare vanity irl_nomads).
Companion to peanut-api-ts IRL_NOMADS rename.
…NOMADS Follow-up to the asset swap: the code-side rename (badge.utils BADGES entry + campaign-maps invite/utm/vanity keys) was left unstaged in the prior commit.
…er outage Manteca onramp/offramp are down. There was no kill-switch for these flows — disabledPaymentProviders only gates QR-pay — so users hit an ugly mid-flow failure (sign a spend, then the provider call throws). Adds a disableMantecaTransfers flag (default on) that renders the same provider-outage screen as the QR path at the two Manteca flow entry points. Placed after the Brazil-PIX delegation so PIX-over-QR sends stay open; QR and crypto are untouched. Flip the flag to false to restore.
…-maintenance hotfix(maintenance): block Manteca add-money + withdraw during provider outage
…e ARS Manteca is recovering per-currency: ARS is back, BRL still down. Replaces the blanket disableMantecaTransfers boolean with disabledMantecaCurrencies (mirrors the disabledPaymentProviders blocklist convention). Only currencies in the list show the outage screen; unlisted ones stay live. Set to ['BRL'] — ARS add-money + withdraw go live again, BRL stays blocked. Empty the array when Manteca is fully restored.
…ncy-block hotfix(maintenance): per-currency Manteca outage block — re-enable ARS
…e BRL
Manteca changed the /prices/direct response on 2026-07-01: the effective rate
moved from top-level effectiveBuy/effectiveSell to nested effectivePrice.{buy,sell}.
Our currency action still read the old fields, so Number(undefined) = NaN threw
'Invalid buy rate from provider: NaN', blocking every Manteca currency (incl. the
now-recovered ARS) at the price-display step.
- currency.ts reads effectivePrice first, falls back to the legacy fields so a
provider rollback can't re-break pricing.
- MantecaPrice type updated (new nested shape; legacy fields optional).
- Manteca fully restored, so empty disabledMantecaCurrencies (re-enables BRL).
Note: peanut-api-ts MantecaPrice type is stale the same way but is passthrough-only
(nothing reads the field at runtime) — trivial type follow-up, not a runtime bug.
…ectiveprice hotfix(manteca): fix NaN price (new effectivePrice shape) + re-enable BRL
… main (#2320) feat(badges): add IRL_NOMADS "Nomad Mode" badge art + campaign maps → main
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR spans multiple unrelated feature slices: share-asset capture readiness gating (fonts/image decode, canvas mount) with a new e2e test; per-currency Manteca maintenance kill-switches; Manteca effectivePrice FX parsing; a mandatory EEA uplift advisory verification flow with funnel analytics; card rejection/waitlist door-tally logic replacing a separate joined screen; KYC reject-label copy precedence; guest bank-claim maintenance messaging; a new badge/campaign entry; CTA banner restyling; and an increased Rain application timeout. ChangesShare-asset capture readiness
Manteca per-currency maintenance
Manteca FX parsing
Mandatory EEA uplift verification
Card rejection/waitlist door tally
KYC reject-label copy precedence
Guest claim-to-bank maintenance
Other independent updates
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
Code-analysis diffPainscore total: 5837.33 → 5857.97 (+20.64) 🆕 New findings (144)
…and 124 more. ✅ Resolved (138)
…and 118 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: 7
🧹 Nitpick comments (6)
src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx (1)
18-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFix
no-explicit-anyESLint errors.Static analysis flags
anyat Line 20 (mocknext/imageprops) and Line 126 (claimLinkDatacast) as errors, not warnings — these will fail the lint gate.🔧 Suggested fix
jest.mock('next/image', () => ({ __esModule: true, - default: (props: any) => { + default: (props: Record<string, unknown>) => { const { priority, fill, ...rest } = props return <img {...rest} /> }, }))and for the claim link data, prefer a partial type of the real shape over
as any, or add a scoped disable comment if a proper type isn't readily available.Also applies to: 120-127
🤖 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/Claim/Link/__tests__/SendLinkActionList.test.tsx` around lines 18 - 24, The test file has two `no-explicit-any` violations that will fail linting: the `next/image` mock in the `jest.mock` block and the `claimLinkData` cast later in the test. Update the mock component to use a concrete prop type instead of `any`, and replace the `claimLinkData` `as any` usage with a properly typed partial of the real claim link data shape (or a narrowly scoped lint disable only if unavoidable). Use the `SendLinkActionList.test.tsx` mock and `claimLinkData` setup as the places to fix.Source: Linters/SAST tools
src/components/Claim/Link/SendLinkActionList.tsx (1)
111-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated guest-maintenance condition.
method.id === 'bank' && isGuestBankClaimis duplicated between theisMethodUnavailablepredicate (Line 122) and thesoonprop (Line 282). Consider deriving both from a single memoized value to prevent the two checks from drifting apart in future edits.♻️ Suggested consolidation
+ const isBankUnderGuestMaintenance = (methodId: string) => methodId === 'bank' && isGuestBankClaim + const { filteredMethods: sortedActionMethods, isLoading: isGeoLoading } = useGeoFilteredPaymentOptions({ sortUnavailable: true, isMethodUnavailable: (method) => method.soon || - (method.id === 'bank' && (requiresVerification || isGuestBankClaim)) || + (method.id === 'bank' && requiresVerification) || + isBankUnderGuestMaintenance(method.id) || (['mercadopago', 'pix'].includes(method.id) && !isMantecaPayEnabled),and
- soon={method.id === 'bank' && isGuestBankClaim} + soon={isBankUnderGuestMaintenance(method.id)}Also applies to: 282-282
🤖 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/Claim/Link/SendLinkActionList.tsx` around lines 111 - 127, The guest bank maintenance check is duplicated in SendLinkActionList and can drift between the payment-method filtering and the rendered “Soon!” state. Derive a single memoized boolean from the existing isGuestBankClaim / requiresVerification logic in SendLinkActionList, then reuse it both in the useGeoFilteredPaymentOptions isMethodUnavailable predicate and wherever the bank option’s soon prop is set. This keeps the bank availability rules centralized and consistent.src/app/(mobile-ui)/withdraw/manteca/page.tsx (1)
91-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding withdraw-flow test coverage symmetric to the add-money maintenance tests.
add-money-states.test.tsxwas updated with live/outage tests for the deposit gate, but no equivalent test is provided here for the withdraw offramp gate (e.g., assertingMantecaBankWithdrawFlowvsMantecaTransfersMaintenanceViewrendering perdisabledMantecaCurrencies, and that the Brazil-PIX delegation still takes precedence). Given this gates a live money-movement flow, mirroring the add-money test would reduce regression risk.🤖 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)/withdraw/manteca/page.tsx around lines 91 - 97, Add symmetric withdraw-flow test coverage for the Manteca maintenance gate in the withdraw page. Update or add tests around MantecaBankWithdrawFlow and MantecaTransfersMaintenanceView to verify that disabledMantecaCurrencies correctly switches the withdraw offramp into maintenance, and that the Brazil-PIX delegation still wins when applicable. Mirror the structure and intent of the existing add-money-states.test.tsx live/outage cases so the withdraw gate behavior is covered end-to-end.src/config/underMaintenance.config.ts (1)
72-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCentralize the Manteca disabled-currency check
Both
src/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsxandsrc/app/(mobile-ui)/withdraw/manteca/page.tsxrepeat the sameas string[]cast before.includes(). Exporting a small helper here, e.g.isMantecaCurrencyDisabled(currency?: string): boolean, would keep the outage gate in one place and remove the duplicated cast.🤖 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/config/underMaintenance.config.ts` around lines 72 - 78, Centralize the repeated Manteca outage gate by adding a small helper in the underMaintenance config module, such as isMantecaCurrencyDisabled(currency?: string): boolean, that owns the disabledMantecaCurrencies check and the string[] cast. Update both add-money/[country]/[regional-method]/page.tsx and withdraw/manteca/page.tsx to call this helper instead of duplicating the .includes logic, and keep the helper alongside MantecaCurrency/disabledMantecaCurrencies so the check stays in one place.src/components/Card/share-asset/shareAsset.types.ts (1)
109-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUpdate the readiness contract to match the
<img>implementation.Line 110 still documents a hand
<canvas>, but the implementation now fires from the pre-pixelated hand<img>. Keeping the prop contract accurate matters because downstream gates depend on this signal.Suggested wording
- * Fires once the card face's async hand <canvas> has mounted (forwarded + * Fires once the card face's pre-pixelated hand <img> has loaded (forwarded🤖 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/shareAsset.types.ts` around lines 109 - 115, The onReady contract in shareAsset.types.ts is out of sync with the current implementation: it still describes a hand <canvas> even though PixelatedCardFace now fires it from the pre-pixelated hand <img>. Update the JSDoc on onReady to accurately describe the <img>-based readiness signal and keep the wording aligned with the gating behavior used by Share/Save capture flows.src/components/Card/CardRejectionScreen.tsx (1)
214-218: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueConsider
aria-livefor the join confirmation.The button → confirmation swap is a meaningful state change but the replacement
<div>isn't announced to screen readers. Addingaria-live="polite"would make the "you're on the list" transition accessible.Suggested tweak
- <div className="flex h-13 items-center justify-center gap-2 text-center text-sm font-bold text-n-1"> + <div + role="status" + aria-live="polite" + className="flex h-13 items-center justify-center gap-2 text-center text-sm font-bold text-n-1" + >🤖 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/CardRejectionScreen.tsx` around lines 214 - 218, The join confirmation in CardRejectionScreen’s `showJoined` branch is a meaningful UI state change but isn’t announced to assistive tech. Update the confirmation `<div>` rendered when `showJoined` is true to use an appropriate live region, such as `aria-live="polite"`, so the “you’re on the list” message is announced after the button-to-confirmation swap.
🤖 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 `@e2e/flows/share-asset-capture.spec.ts`:
- Around line 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.
In `@src/app/`(mobile-ui)/add-money/[country]/bank/page.tsx:
- Around line 90-101: The uplift latch is only reset on the Sumsub widget close
path, so a multi-phase modal exit can leave startedRef latched and allow a later
success to emit eea_uplift_completed with stale data. Update the
modal-abandonment flow in the add-money bank page, specifically handleModalClose
and the “Go to Home” path around useMultiPhaseKycFlow, to call resetUpliftFunnel
as well so any pending uplift state is cleared whenever the user closes the
modal.
In `@src/app/`(mobile-ui)/dev/share-builder/page.tsx:
- Line 396: The Save action is being enabled too early because onReady only
tracks the asset load state, not when the cardSlide/sticker animations are
settled. Update the share builder flow around onReady, the disabled condition,
and the capture target’s animate prop so Save stays disabled until both the
asset is ready and animations are stable, or render the capture target with
animate={false} before allowing capture.
In `@src/app/`(mobile-ui)/withdraw/[country]/bank/page.tsx:
- Around line 101-109: The modal dismissal path in useMultiPhaseKycFlow
currently logs abandonment and closes the UI, but it does not trigger the
withdraw flow’s onManualClose cleanup. Update handleModalClose in
useMultiPhaseKycFlow so it invokes the supplied onManualClose callback
before/while closing, preserving the existing behavior and ensuring
resetUpliftFunnel from the bank page is cleared on wrapper close as well.
In `@src/components/Card/share-asset/captureShareAsset.ts`:
- Around line 56-73: Add a timeout guard to waitForAssetReady so asset
preparation cannot hang forever. The current Promise.all over img.decode() in
waitForAssetReady can stall indefinitely on a bad remote image, leaving
captureShareAsset unresolved. Wrap the image decode phase (and ideally the
fonts.ready wait as well) with a bounded timeout and fall back cleanly when the
timeout is hit, so captureShareAsset can continue or fail fast instead of
blocking ShareAssetActions.handleShare, ShareAssetActions.handleSave, and
CardRejectionScreen.handleAppeal.
In `@src/components/Card/share-asset/PixelatedCardFace.tsx`:
- Around line 289-300: Guard the callback in PixelatedHand so onReady only fires
once, since both the img onLoad handler and the ref callback can invoke it
multiple times across rerenders. Update PixelatedCardFace/PixelatedHand to track
whether readiness has already been reported, and in the ref callback only call
onReady for cached images that are actually valid by checking complete plus
naturalWidth > 0 before signaling ready.
In `@src/hooks/useAdvisoryPreempt.ts`:
- Around line 28-35: The advisory modal can be re-opened after the backend
clears the requirement because the rejection path in
useAdvisoryPreempt/onCompleteNow does not verify that the latest advisory still
exists before restoring the non-dismissible gate. Update the async completion
flow to track the current advisory state (or compare against the latest advisory
reference) and only call setVisible(true) when the rejected completion still
corresponds to an active advisory; otherwise leave the modal closed. Keep the
sync effect in useAdvisoryPreempt and the rejection handling around
onCompleteNow aligned so a cleared advisory is never resurrected.
---
Nitpick comments:
In `@src/app/`(mobile-ui)/withdraw/manteca/page.tsx:
- Around line 91-97: Add symmetric withdraw-flow test coverage for the Manteca
maintenance gate in the withdraw page. Update or add tests around
MantecaBankWithdrawFlow and MantecaTransfersMaintenanceView to verify that
disabledMantecaCurrencies correctly switches the withdraw offramp into
maintenance, and that the Brazil-PIX delegation still wins when applicable.
Mirror the structure and intent of the existing add-money-states.test.tsx
live/outage cases so the withdraw gate behavior is covered end-to-end.
In `@src/components/Card/CardRejectionScreen.tsx`:
- Around line 214-218: The join confirmation in CardRejectionScreen’s
`showJoined` branch is a meaningful UI state change but isn’t announced to
assistive tech. Update the confirmation `<div>` rendered when `showJoined` is
true to use an appropriate live region, such as `aria-live="polite"`, so the
“you’re on the list” message is announced after the button-to-confirmation swap.
In `@src/components/Card/share-asset/shareAsset.types.ts`:
- Around line 109-115: The onReady contract in shareAsset.types.ts is out of
sync with the current implementation: it still describes a hand <canvas> even
though PixelatedCardFace now fires it from the pre-pixelated hand <img>. Update
the JSDoc on onReady to accurately describe the <img>-based readiness signal and
keep the wording aligned with the gating behavior used by Share/Save capture
flows.
In `@src/components/Claim/Link/__tests__/SendLinkActionList.test.tsx`:
- Around line 18-24: The test file has two `no-explicit-any` violations that
will fail linting: the `next/image` mock in the `jest.mock` block and the
`claimLinkData` cast later in the test. Update the mock component to use a
concrete prop type instead of `any`, and replace the `claimLinkData` `as any`
usage with a properly typed partial of the real claim link data shape (or a
narrowly scoped lint disable only if unavoidable). Use the
`SendLinkActionList.test.tsx` mock and `claimLinkData` setup as the places to
fix.
In `@src/components/Claim/Link/SendLinkActionList.tsx`:
- Around line 111-127: The guest bank maintenance check is duplicated in
SendLinkActionList and can drift between the payment-method filtering and the
rendered “Soon!” state. Derive a single memoized boolean from the existing
isGuestBankClaim / requiresVerification logic in SendLinkActionList, then reuse
it both in the useGeoFilteredPaymentOptions isMethodUnavailable predicate and
wherever the bank option’s soon prop is set. This keeps the bank availability
rules centralized and consistent.
In `@src/config/underMaintenance.config.ts`:
- Around line 72-78: Centralize the repeated Manteca outage gate by adding a
small helper in the underMaintenance config module, such as
isMantecaCurrencyDisabled(currency?: string): boolean, that owns the
disabledMantecaCurrencies check and the string[] cast. Update both
add-money/[country]/[regional-method]/page.tsx and withdraw/manteca/page.tsx to
call this helper instead of duplicating the .includes logic, and keep the helper
alongside MantecaCurrency/disabledMantecaCurrencies so the check stays in one
place.
🪄 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: 7ff07951-c9d4-4754-b7cf-33d531238a93
⛔ Files ignored due to path filters (2)
public/badges/irl_nomads.svgis excluded by!**/*.svgsrc/assets/cards/peanut-card-hand-pixel.pngis excluded by!**/*.png
📒 Files selected for processing (47)
e2e/flows/share-asset-capture.spec.tssrc/app/(mobile-ui)/add-money/[country]/[regional-method]/page.tsxsrc/app/(mobile-ui)/add-money/[country]/bank/page.tsxsrc/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsxsrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/dev/rejection-builder/page.tsxsrc/app/(mobile-ui)/dev/share-builder/page.tsxsrc/app/(mobile-ui)/withdraw/[country]/bank/page.tsxsrc/app/(mobile-ui)/withdraw/manteca/page.tsxsrc/app/actions/__tests__/currency.test.tssrc/app/actions/currency.tssrc/app/shhhhh/ShhhhhLandingPage.tsxsrc/components/Badges/badge.utils.tssrc/components/Card/ApplicationStatusScreen.tsxsrc/components/Card/BadgeSkipCelebration.tsxsrc/components/Card/CardRejectionScreen.tsxsrc/components/Card/CardUnlockDrawer.tsxsrc/components/Card/CardWaitlistJoinedScreen.tsxsrc/components/Card/PhysicalCardScreen.tsxsrc/components/Card/__tests__/ApplicationStatusScreen.test.tsxsrc/components/Card/__tests__/doorTally.utils.test.tssrc/components/Card/doorTally.utils.tssrc/components/Card/share-asset/PixelatedCardFace.tsxsrc/components/Card/share-asset/ShareAssetActions.tsxsrc/components/Card/share-asset/ShareAssetD3.tsxsrc/components/Card/share-asset/__tests__/captureShareAsset.test.tssrc/components/Card/share-asset/captureShareAsset.tssrc/components/Card/share-asset/shareAsset.types.tssrc/components/Claim/Link/SendLinkActionList.tsxsrc/components/Claim/Link/__tests__/SendLinkActionList.test.tsxsrc/components/Global/Banner/MantecaTransfersMaintenanceView.tsxsrc/components/Home/CardLaunchCTA/CardLaunchCTABanner.tsxsrc/components/Invites/campaign-maps.tssrc/components/Kyc/AdvisoryPreemptModal.tsxsrc/components/Kyc/states/KycActionRequired.tsxsrc/components/Kyc/states/__tests__/KycActionRequired.rejectCopy.test.tsxsrc/components/Kyc/states/__tests__/KycStates.test.tsxsrc/config/underMaintenance.config.tssrc/constants/analytics.consts.tssrc/hooks/useAdvisoryPreempt.test.tssrc/hooks/useAdvisoryPreempt.tssrc/hooks/useEeaUpliftFunnel.test.tssrc/hooks/useEeaUpliftFunnel.tssrc/hooks/useMultiPhaseKycFlow.tssrc/services/card.tssrc/services/manteca.tssrc/services/rain.ts
💤 Files with no reviewable changes (1)
- src/components/Card/CardWaitlistJoinedScreen.tsx
| 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) |
There was a problem hiding this comment.
🎯 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.
| const sumsubFlow = useMultiPhaseKycFlow({ | ||
| // Fire completed at Sumsub approval (verification submitted), not at | ||
| // end-of-flow — so it isn't lost if the user drops during the | ||
| // post-approval ToS / preparing steps. | ||
| onKycApproved: () => trackUpliftCompleted(), | ||
| onKycSuccess: () => { | ||
| setUrlState({ step: 'inputAmount' }) | ||
| }, | ||
| // Abandoned attempt: clear the pending start so a later unrelated KYC | ||
| // success on this page can't mis-fire eea_uplift_completed. | ||
| onManualClose: resetUpliftFunnel, | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -t f 'SumsubKycModals.tsx|useSumsubKycFlow.ts'
echo '--- SumsubKycModals close wiring ---'
fd 'SumsubKycModals.tsx' --exec rg -n -B3 -A15 'handleModalClose|onClose|onManualClose' {}
echo '--- useSumsubKycFlow onManualClose usage ---'
fd 'useSumsubKycFlow.ts' --exec rg -n -B5 -A15 'onManualClose' {}Repository: peanutprotocol/peanut-ui
Length of output: 3977
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- useMultiPhaseKycFlow outline ---'
ast-grep outline src/hooks/useMultiPhaseKycFlow.ts --view expanded
echo '--- useMultiPhaseKycFlow close-related logic ---'
rg -n -B4 -A12 'handleModalClose|onManualClose|KYC_ABANDONED|resetUpliftFunnel|completeFlow|isModalOpen|showWrapper' src/hooks/useMultiPhaseKycFlow.ts src/components/Kyc/SumsubKycModals.tsx src/components/Kyc/KycVerificationInProgressModal.tsx src/app/\(mobile-ui\)/add-money/\[country\]/bank/page.tsxRepository: peanutprotocol/peanut-ui
Length of output: 23746
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- file sizes ---'
wc -l src/hooks/useMultiPhaseKycFlow.ts src/components/Kyc/KycVerificationInProgressModal.tsx src/components/Kyc/SumsubKycModals.tsx src/hooks/useSumsubKycFlow.ts
echo '--- useMultiPhaseKycFlow snippet ---'
sed -n '1,260p' src/hooks/useMultiPhaseKycFlow.ts
echo '--- KycVerificationInProgressModal snippet ---'
sed -n '1,260p' src/components/Kyc/KycVerificationInProgressModal.tsxRepository: peanutprotocol/peanut-ui
Length of output: 18286
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- useEeaUpliftFunnel outline ---'
fd -t f 'useEeaUpliftFunnel.ts'
ast-grep outline src/hooks/useEeaUpliftFunnel.ts --view expanded
echo '--- useEeaUpliftFunnel implementation ---'
sed -n '1,240p' src/hooks/useEeaUpliftFunnel.ts
echo '--- bank page complete-now wiring ---'
rg -n -B4 -A14 'trackUpliftStarted|handleSelfHealResubmit|resetUpliftFunnel|onCompleteNow|eea_uplift_completed' src/app/\(mobile-ui\)/add-money/\[country\]/bank/page.tsx src/hooks/useEeaUpliftFunnel.tsRepository: peanutprotocol/peanut-ui
Length of output: 12231
Clear the uplift latch on modal abandonment too. resetUpliftFunnel only runs for the Sumsub widget close path, so closing the multi-phase modal (handleModalClose / “Go to Home”) can leave startedRef latched and let a later unrelated success emit eea_uplift_completed with stale advisory data.
🤖 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)/add-money/[country]/bank/page.tsx around lines 90 - 101,
The uplift latch is only reset on the Sumsub widget close path, so a multi-phase
modal exit can leave startedRef latched and allow a later success to emit
eea_uplift_completed with stale data. Update the modal-abandonment flow in the
add-money bank page, specifically handleModalClose and the “Go to Home” path
around useMultiPhaseKycFlow, to call resetUpliftFunnel as well so any pending
uplift state is cleared whenever the user closes the modal.
| usernameStyle={usernameStyle} | ||
| hideUsername={hideUsername} | ||
| animate={animate} | ||
| onReady={() => setAssetReady(true)} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Don’t enable Save until the animated asset is stable.
onReady only proves the hand image loaded. With animate=true, a cached image can enable Save before cardSlide/sticker animations settle, so a fast click captures the current transient frame. The e2e test’s extra 2.5s wait masks this.
Either include animation-settled state in disabled, or render the capture target with animate={false}.
Also applies to: 418-425
🤖 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 at line 396, The Save action
is being enabled too early because onReady only tracks the asset load state, not
when the cardSlide/sticker animations are settled. Update the share builder flow
around onReady, the disabled condition, and the capture target’s animate prop so
Save stays disabled until both the asset is ready and animations are stable, or
render the capture target with animate={false} before allowing capture.
| const sumsubFlow = useMultiPhaseKycFlow({ | ||
| // Fire completed at Sumsub approval (verification submitted), not at | ||
| // end-of-flow — so it isn't lost if the user drops during the | ||
| // post-approval ToS / preparing steps. | ||
| onKycApproved: () => trackUpliftCompleted(), | ||
| // Abandoned attempt: clear the pending start so a later unrelated KYC | ||
| // success on this page can't mis-fire eea_uplift_completed. | ||
| onManualClose: resetUpliftFunnel, | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== withdraw page slice ==\n'
sed -n '80,130p' 'src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx'
printf '\n== add-money page slice ==\n'
sed -n '75,120p' 'src/app/(mobile-ui)/add-money/[country]/bank/page.tsx'
printf '\n== hook location search ==\n'
rg -n "useMultiPhaseKycFlow|handleModalClose|onManualClose|resetUpliftFunnel" src -g '!**/node_modules/**'Repository: peanutprotocol/peanut-ui
Length of output: 13369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
set -euo pipefail
printf '\n== locate hook definition ==\n'
rg -n "function useMultiPhaseKycFlow|const useMultiPhaseKycFlow|export .*useMultiPhaseKycFlow" src
printf '\n== hook file candidates ==\n'
fd 'useMultiPhaseKycFlow' src
printf '\n== nearby references ==\n'
rg -n "handleModalClose|onManualClose|resetUpliftFunnel|onKycApproved" src/app/(mobile-ui) -g '!**/node_modules/**'Repository: peanutprotocol/peanut-ui
Length of output: 409
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== useMultiPhaseKycFlow slice 1 ==\n'
sed -n '98,260p' 'src/hooks/useMultiPhaseKycFlow.ts'
printf '\n== useMultiPhaseKycFlow slice 2 ==\n'
sed -n '260,460p' 'src/hooks/useMultiPhaseKycFlow.ts'
printf '\n== useSumsubKycFlow slice ==\n'
sed -n '300,345p' 'src/hooks/useSumsubKycFlow.ts'Repository: peanutprotocol/peanut-ui
Length of output: 16100
Call onManualClose from the wrapper close path too. src/hooks/useMultiPhaseKycFlow.ts handles modal dismissal in handleModalClose, but that path only logs abandonment and closes the UI. On this withdraw flow, that leaves resetUpliftFunnel set until some later KYC success can misfire eea_uplift_completed.
🤖 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)/withdraw/[country]/bank/page.tsx around lines 101 - 109,
The modal dismissal path in useMultiPhaseKycFlow currently logs abandonment and
closes the UI, but it does not trigger the withdraw flow’s onManualClose
cleanup. Update handleModalClose in useMultiPhaseKycFlow so it invokes the
supplied onManualClose callback before/while closing, preserving the existing
behavior and ensuring resetUpliftFunnel from the bank page is cleared on wrapper
close as well.
| export async function waitForAssetReady(node: HTMLElement): Promise<void> { | ||
| if (typeof document !== 'undefined' && document.fonts?.ready) { | ||
| try { | ||
| await document.fonts.ready | ||
| } catch { | ||
| // fonts.ready can reject in odd states — capture anyway. | ||
| } | ||
| } | ||
| await Promise.all( | ||
| Array.from(node.querySelectorAll('img')).map((img) => | ||
| typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve() | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| export async function captureShareAsset(node: HTMLElement): Promise<Blob> { | ||
| try { | ||
| await waitForAssetReady(node) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a timeout guard to waitForAssetReady.
document.fonts.ready is safely wrapped in try/catch, but the Promise.all over img.decode() has no upper bound. If any <img> (e.g. a remote badge sticker, not the bundled hand PNG) stalls on the network without ever resolving/rejecting, this await never settles — captureShareAsset hangs forever, and every caller (ShareAssetActions.handleShare/handleSave, CardRejectionScreen.handleAppeal) is left stuck in its loading state indefinitely with no recourse for the user.
🛡️ Proposed fix: bound both awaits with a timeout
+async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
+ return Promise.race([promise, new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms))])
+}
+
export async function waitForAssetReady(node: HTMLElement): Promise<void> {
if (typeof document !== 'undefined' && document.fonts?.ready) {
try {
- await document.fonts.ready
+ await withTimeout(document.fonts.ready, 3000)
} catch {
// fonts.ready can reject in odd states — capture anyway.
}
}
- await Promise.all(
- Array.from(node.querySelectorAll('img')).map((img) =>
- typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve()
- )
- )
+ await withTimeout(
+ Promise.all(
+ Array.from(node.querySelectorAll('img')).map((img) =>
+ typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve()
+ )
+ ),
+ 5000
+ )
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function waitForAssetReady(node: HTMLElement): Promise<void> { | |
| if (typeof document !== 'undefined' && document.fonts?.ready) { | |
| try { | |
| await document.fonts.ready | |
| } catch { | |
| // fonts.ready can reject in odd states — capture anyway. | |
| } | |
| } | |
| await Promise.all( | |
| Array.from(node.querySelectorAll('img')).map((img) => | |
| typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve() | |
| ) | |
| ) | |
| } | |
| export async function captureShareAsset(node: HTMLElement): Promise<Blob> { | |
| try { | |
| await waitForAssetReady(node) | |
| async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> { | |
| return Promise.race([promise, new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms))]) | |
| } | |
| export async function waitForAssetReady(node: HTMLElement): Promise<void> { | |
| if (typeof document !== 'undefined' && document.fonts?.ready) { | |
| try { | |
| await withTimeout(document.fonts.ready, 3000) | |
| } catch { | |
| // fonts.ready can reject in odd states — capture anyway. | |
| } | |
| } | |
| await withTimeout( | |
| Promise.all( | |
| Array.from(node.querySelectorAll('img')).map((img) => | |
| typeof img.decode === 'function' ? img.decode().catch(() => undefined) : Promise.resolve() | |
| ) | |
| ), | |
| 5000 | |
| ) | |
| } | |
| export async function captureShareAsset(node: HTMLElement): Promise<Blob> { | |
| try { | |
| await waitForAssetReady(node) |
🤖 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/captureShareAsset.ts` around lines 56 - 73,
Add a timeout guard to waitForAssetReady so asset preparation cannot hang
forever. The current Promise.all over img.decode() in waitForAssetReady can
stall indefinitely on a bad remote image, leaving captureShareAsset unresolved.
Wrap the image decode phase (and ideally the fonts.ready wait as well) with a
bounded timeout and fall back cleanly when the timeout is hit, so
captureShareAsset can continue or fail fast instead of blocking
ShareAssetActions.handleShare, ShareAssetActions.handleSave, and
CardRejectionScreen.handleAppeal.
| const PixelatedHand: FC<{ onReady?: () => void }> = ({ onReady }) => ( | ||
| <img | ||
| src={ASSET_CARD_HAND_PIXEL} | ||
| alt="" | ||
| aria-hidden | ||
| draggable={false} | ||
| onLoad={() => onReady?.()} | ||
| ref={(img) => { | ||
| // A cached image can already be `complete` before React attaches | ||
| // onLoad, so the load event never fires — that would leave the | ||
| // capture gate stuck disabled. Fire onReady directly in that case. | ||
| if (img?.complete) onReady?.() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## File outline\n'
ast-grep outline src/components/Card/share-asset/PixelatedCardFace.tsx --view expanded || true
printf '\n## Relevant lines\n'
nl -ba src/components/Card/share-asset/PixelatedCardFace.tsx | sed -n '250,340p'
printf '\n## Search usages of PixelatedHand and onReady in this area\n'
rg -n "PixelatedHand|onReady" src/components/Card/share-asset src | sed -n '1,200p'Repository: peanutprotocol/peanut-ui
Length of output: 1512
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## Relevant lines from PixelatedCardFace.tsx\n'
sed -n '280,325p' src/components/Card/share-asset/PixelatedCardFace.tsx
printf '\n## Search for onReady call sites in repository\n'
rg -n "\bonReady\b" src | sed -n '1,200p'
printf '\n## Search for setAssetReady or similar consumers\n'
rg -n "setAssetReady|assetReady|capture gate|PixelatedCardFace" src | sed -n '1,240p'Repository: peanutprotocol/peanut-ui
Length of output: 9020
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## shareAsset.types.ts around onReady docs\n'
sed -n '96,125p' src/components/Card/share-asset/shareAsset.types.ts
printf '\n## ShareAssetActions.tsx around readiness comment\n'
sed -n '60,90p' src/components/Card/share-asset/ShareAssetActions.tsx
printf '\n## ShareAssetD3.tsx around onReady wiring\n'
sed -n '210,235p' src/components/Card/share-asset/ShareAssetD3.tsxRepository: peanutprotocol/peanut-ui
Length of output: 3584
🌐 Web query:
HTMLImageElement.complete true broken image naturalWidth 0 MDN
💡 Result:
For an HTMLImageElement, the complete property returns true even if the image is in a "broken" state (i.e., it failed to load due to an error, invalid format, or corruption) [1][2]. The property is intended to indicate that the browser has finished its attempt to fetch the image [3]. Because the browser has completed its attempt to process the image—regardless of whether that attempt was successful—it considers the operation "complete" [3][2]. To determine if an image failed to load, you can check if complete is true and naturalWidth is 0 [2]. Since naturalWidth (and naturalHeight) returns 0 when an image is not available or failed to load [3][4], this combination effectively identifies a broken image [2]. Note that this approach can have limitations, as some valid images (like certain SVGs or intentionally empty images) might also report a naturalWidth of 0 [5].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/complete
- 2: https://lists.w3.org/Archives/Public/public-whatwg-archive/2013Dec/0055.html
- 3: https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement
- 4: https://html.spec.whatwg.org/multipage/embedded-content.html
- 5: Make img.naturalWidth/Height handle dimensionless images better whatwg/html#3510
Guard onReady inside PixelatedHand
onLoad and the callback ref can both invoke onReady, and the inline ref will re-fire on rerenders. That breaks the current “fires once” contract and can also treat a broken cached image as ready; gate the callback and ignore complete images with naturalWidth === 0.
🤖 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 289 -
300, Guard the callback in PixelatedHand so onReady only fires once, since both
the img onLoad handler and the ref callback can invoke it multiple times across
rerenders. Update PixelatedCardFace/PixelatedHand to track whether readiness has
already been reported, and in the ref callback only call onReady for cached
images that are actually valid by checking complete plus naturalWidth > 0 before
signaling ready.
| const completingRef = useRef(false) | ||
|
|
||
| // Keep the modal in sync with the requirement: if the backend clears the | ||
| // advisory (requirement resolved) while the modal is open, auto-close it so | ||
| // the gate doesn't linger over an already-unblocked transfer. | ||
| useEffect(() => { | ||
| if (!advisory) setVisible(false) | ||
| }, [advisory]) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Don’t resurrect the gate after the advisory clears.
If advisory becomes undefined while onCompleteNow() is pending, the effect on Line 34 closes the modal, but a later rejection on Line 60 re-opens the non-dismissible modal with no active requirement. Re-show only when the latest advisory is still present.
Proposed fix
const completingRef = useRef(false)
+ const latestAdvisoryRef = useRef(advisory)
// Keep the modal in sync with the requirement: if the backend clears the
// advisory (requirement resolved) while the modal is open, auto-close it so
// the gate doesn't linger over an already-unblocked transfer.
useEffect(() => {
+ latestAdvisoryRef.current = advisory
if (!advisory) setVisible(false)
}, [advisory])
@@
} catch (error) {
// Launch failed — re-show the gate so the user isn't left with a
// silently dismissed mandatory step and a still-pending requirement.
- setVisible(true)
+ if (latestAdvisoryRef.current) {
+ setVisible(true)
+ }
throw error
} finally {Also applies to: 57-60
🤖 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/hooks/useAdvisoryPreempt.ts` around lines 28 - 35, The advisory modal can
be re-opened after the backend clears the requirement because the rejection path
in useAdvisoryPreempt/onCompleteNow does not verify that the latest advisory
still exists before restoring the non-dismissible gate. Update the async
completion flow to track the current advisory state (or compare against the
latest advisory reference) and only call setVisible(true) when the rejected
completion still corresponds to an active advisory; otherwise leave the modal
closed. Keep the sync effect in useAdvisoryPreempt and the rejection handling
around onCompleteNow aligned so a cleared advisory is never resurrected.
Routine back-merge per release flow. Brings dev up to date with today's main activity: IRL_NOMADS badge (#2320), Manteca outage kill-switch/per-currency/price-shape hotfixes (#2321 #2322 #2323). dev had 0 unique commits — clean merge.