From 85483d56f68ad67ddcc0833971b95ac941c46a6b Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Thu, 11 Jun 2026 12:47:56 +0100 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20release-audit=20follow-ups=20?= =?UTF-8?q?=E2=80=94=20string=20badge-icon=20fallback=20+=20reachable=20re?= =?UTF-8?q?start-identity=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getBadgeIcon fell back to PEANUTMAN_LOGO (StaticImageData, typed `any` by the svg shim) while shareAssetLayout types iconUrl as string and ShareAssetD3 feeds it to a raw — an unknown badge code (the recurring FE-BADGES-drop incident) would render a broken stamp. Unwrap .src and pin the contract with a test. UnlockedRegions' provider-rejection override only promoted fixable/blocked, so the restart-identity copy + handleRestartIdentity CTA already wired in the modal were unreachable from this predicate — those users fell back to the generic start flow. Include restart-identity. Plus two lint nits from the #2218 review: unused useRouter in the payment error boundary, missing alt on the careers mascot. --- src/app/[...recipient]/error.tsx | 2 -- .../Badges/__tests__/badge.utils.test.ts | 20 +++++++++++++++++++ src/components/Badges/badge.utils.ts | 6 ++++-- .../__tests__/shareAssetLayout.test.ts | 7 +++++++ src/components/Jobs/index.tsx | 2 +- .../Profile/views/UnlockedRegions.view.tsx | 4 +++- 6 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 src/components/Badges/__tests__/badge.utils.test.ts diff --git a/src/app/[...recipient]/error.tsx b/src/app/[...recipient]/error.tsx index b9e261f5f..e2157ebba 100644 --- a/src/app/[...recipient]/error.tsx +++ b/src/app/[...recipient]/error.tsx @@ -1,14 +1,12 @@ 'use client' import { useEffect } from 'react' -import { useRouter } from 'next/navigation' import { useModalsContext } from '@/context/ModalsContext' import { Button } from '@/components/0_Bruddle/Button' import { Card } from '@/components/0_Bruddle/Card' import { recoverFromChunkError } from '@/utils/chunk-error-recovery' export default function PaymentError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { - const router = useRouter() const { setIsSupportModalOpen } = useModalsContext() useEffect(() => { diff --git a/src/components/Badges/__tests__/badge.utils.test.ts b/src/components/Badges/__tests__/badge.utils.test.ts new file mode 100644 index 000000000..c01290aa3 --- /dev/null +++ b/src/components/Badges/__tests__/badge.utils.test.ts @@ -0,0 +1,20 @@ +import { BADGES, getBadgeIcon } from '../badge.utils' + +// jest-transform-stub turns svg imports into a bare string, so mock the mascot +// module to mirror the real StaticImageData shape — the fallback must unwrap .src. +jest.mock('@/assets/mascot', () => ({ + PEANUTMAN_LOGO: { src: '/peanut-logo-stub.svg' }, +})) + +describe('getBadgeIcon', () => { + it('returns the badge path for known codes', () => { + expect(getBadgeIcon('WAITLIST_SKIP')).toBe(BADGES.WAITLIST_SKIP.path) + }) + + it('falls back to a string URL for unknown codes (raw consumers)', () => { + // Unknown codes happen in prod when the FE BADGES map drops a code the BE + // still awards (the recurring badge-registry silent-drop incident). + expect(getBadgeIcon('NOT_A_REAL_BADGE')).toBe('/peanut-logo-stub.svg') + expect(getBadgeIcon(undefined)).toBe('/peanut-logo-stub.svg') + }) +}) diff --git a/src/components/Badges/badge.utils.ts b/src/components/Badges/badge.utils.ts index e8fcaa698..ac830689a 100644 --- a/src/components/Badges/badge.utils.ts +++ b/src/components/Badges/badge.utils.ts @@ -195,8 +195,10 @@ export const BADGES: Record = { * list. Used by /dev/share-builder + /dev/debug for iteration. */ export const BADGE_CODES: readonly string[] = Object.keys(BADGES) -export function getBadgeIcon(code?: string) { - return (code && BADGES[code]?.path) || PEANUTMAN_LOGO +export function getBadgeIcon(code?: string): string { + // .src: the svg import is StaticImageData (typed `any` by the module shim, so the + // annotation alone can't enforce this) — raw consumers need a string URL. + return (code && BADGES[code]?.path) || PEANUTMAN_LOGO.src } // returns the public-facing description for a badge code (third-person perspective) diff --git a/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts b/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts index 6d49d3628..a5eef3215 100644 --- a/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts +++ b/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts @@ -8,6 +8,13 @@ * regress here. */ +// jest-transform-stub flattens svg imports to a bare string; getBadgeIcon unwraps +// PEANUTMAN_LOGO.src, so mirror the real StaticImageData shape for the fallback path. +jest.mock('@/assets/mascot', () => ({ + ...jest.requireActual('@/assets/mascot'), + PEANUTMAN_LOGO: { src: '/peanut-logo-stub.svg' }, +})) + import { SeededRandom } from '../seededRandom' import { CANVAS_W, diff --git a/src/components/Jobs/index.tsx b/src/components/Jobs/index.tsx index 6407a3f8d..6282063ae 100644 --- a/src/components/Jobs/index.tsx +++ b/src/components/Jobs/index.tsx @@ -4,7 +4,7 @@ export function Careers() { return (
- +
{'<'} Hey there! Want to work at Peanut?
diff --git a/src/components/Profile/views/UnlockedRegions.view.tsx b/src/components/Profile/views/UnlockedRegions.view.tsx index f0e99017d..0b20eeb02 100644 --- a/src/components/Profile/views/UnlockedRegions.view.tsx +++ b/src/components/Profile/views/UnlockedRegions.view.tsx @@ -117,7 +117,9 @@ const UnlockedRegions = () => { !!selectedRegion && clickedRegionProvider !== null && isSumsubApproved && - (providerRejectionForRegion.state === 'fixable' || providerRejectionForRegion.state === 'blocked') + (providerRejectionForRegion.state === 'fixable' || + providerRejectionForRegion.state === 'blocked' || + providerRejectionForRegion.state === 'restart-identity') const modalVariant = hasProviderRejectionForRegion ? ('provider_rejection' as const) : baseModalVariant const handleFinalKycSuccess = useCallback(() => { From 7474430533ba9a794e71b511631964a8d8fd49d5 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Thu, 11 Jun 2026 13:09:04 +0100 Subject: [PATCH 2/2] review: self-updating rejection predicate + StaticImageData-shaped jest asset stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /code-review pass on this PR converged on two structural fixes: The predicate now reads state !== 'happy' instead of enumerating the three non-happy members — the enumeration pattern is how restart-identity got missed in the first place, and a future fourth state would have repeated it. The jest asset stub (jest-transform-stub) flattened image imports to a bare string while Next yields StaticImageData — tests of .src-reading code ran against fiction, which is exactly how the original object-into-string fallback shipped unnoticed. Point the mapper at a {src,width,height} stub and drop both per-file mascot mocks this PR had added. jest-transform-stub stays in devDependencies for now: removing it forces a pnpm lockfile re-resolve that drags Sentry onto different otel peers — that cleanup belongs in a deps PR. --- package.json | 2 +- .../Badges/__tests__/badge.utils.test.ts | 14 +++++--------- .../share-asset/__tests__/shareAssetLayout.test.ts | 7 ------- .../Profile/views/UnlockedRegions.view.tsx | 7 ++++--- src/utils/__mocks__/static-image.ts | 13 +++++++++++++ 5 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 src/utils/__mocks__/static-image.ts diff --git a/package.json b/package.json index fe83fa310..95c502729 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "node_modules/(?!(@wagmi|wagmi|viem|@viem|@walletconnect|@justaname\\.id|@zerodev|permissionless)/)" ], "moduleNameMapper": { - "\\.(svg|png|jpg|jpeg|gif|webp)$": "jest-transform-stub", + "\\.(svg|png|jpg|jpeg|gif|webp)$": "/src/utils/__mocks__/static-image.ts", "^@/config/wagmi\\.config$": "/src/utils/__mocks__/wagmi-config.ts", "^wagmi/chains$": "/src/utils/__mocks__/wagmi.ts", "^@justaname\\.id/react$": "/src/utils/__mocks__/justaname.ts", diff --git a/src/components/Badges/__tests__/badge.utils.test.ts b/src/components/Badges/__tests__/badge.utils.test.ts index c01290aa3..a0e6c3f35 100644 --- a/src/components/Badges/__tests__/badge.utils.test.ts +++ b/src/components/Badges/__tests__/badge.utils.test.ts @@ -1,11 +1,5 @@ import { BADGES, getBadgeIcon } from '../badge.utils' -// jest-transform-stub turns svg imports into a bare string, so mock the mascot -// module to mirror the real StaticImageData shape — the fallback must unwrap .src. -jest.mock('@/assets/mascot', () => ({ - PEANUTMAN_LOGO: { src: '/peanut-logo-stub.svg' }, -})) - describe('getBadgeIcon', () => { it('returns the badge path for known codes', () => { expect(getBadgeIcon('WAITLIST_SKIP')).toBe(BADGES.WAITLIST_SKIP.path) @@ -13,8 +7,10 @@ describe('getBadgeIcon', () => { it('falls back to a string URL for unknown codes (raw consumers)', () => { // Unknown codes happen in prod when the FE BADGES map drops a code the BE - // still awards (the recurring badge-registry silent-drop incident). - expect(getBadgeIcon('NOT_A_REAL_BADGE')).toBe('/peanut-logo-stub.svg') - expect(getBadgeIcon(undefined)).toBe('/peanut-logo-stub.svg') + // still awards (the recurring badge-registry silent-drop incident). The + // fallback must unwrap StaticImageData.src — never leak the object. + expect(typeof getBadgeIcon('NOT_A_REAL_BADGE')).toBe('string') + expect(getBadgeIcon('NOT_A_REAL_BADGE')).toBeTruthy() + expect(getBadgeIcon(undefined)).toBe(getBadgeIcon('NOT_A_REAL_BADGE')) }) }) diff --git a/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts b/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts index a5eef3215..6d49d3628 100644 --- a/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts +++ b/src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts @@ -8,13 +8,6 @@ * regress here. */ -// jest-transform-stub flattens svg imports to a bare string; getBadgeIcon unwraps -// PEANUTMAN_LOGO.src, so mirror the real StaticImageData shape for the fallback path. -jest.mock('@/assets/mascot', () => ({ - ...jest.requireActual('@/assets/mascot'), - PEANUTMAN_LOGO: { src: '/peanut-logo-stub.svg' }, -})) - import { SeededRandom } from '../seededRandom' import { CANVAS_W, diff --git a/src/components/Profile/views/UnlockedRegions.view.tsx b/src/components/Profile/views/UnlockedRegions.view.tsx index 0b20eeb02..99e12708f 100644 --- a/src/components/Profile/views/UnlockedRegions.view.tsx +++ b/src/components/Profile/views/UnlockedRegions.view.tsx @@ -117,9 +117,10 @@ const UnlockedRegions = () => { !!selectedRegion && clickedRegionProvider !== null && isSumsubApproved && - (providerRejectionForRegion.state === 'fixable' || - providerRejectionForRegion.state === 'blocked' || - providerRejectionForRegion.state === 'restart-identity') + // Any non-happy state has a dedicated rendering in the ActionModal below. + // Derive from !== 'happy' so a new ProviderRejectionState member can't + // silently miss this gate again (restart-identity did exactly that). + providerRejectionForRegion.state !== 'happy' const modalVariant = hasProviderRejectionForRegion ? ('provider_rejection' as const) : baseModalVariant const handleFinalKycSuccess = useCallback(() => { diff --git a/src/utils/__mocks__/static-image.ts b/src/utils/__mocks__/static-image.ts new file mode 100644 index 000000000..364252b7e --- /dev/null +++ b/src/utils/__mocks__/static-image.ts @@ -0,0 +1,13 @@ +// Jest stand-in for Next.js static asset imports (svg/png/jpg/gif/webp). +// Next yields StaticImageData ({ src, width, height, ... }), but the previous +// jest-transform-stub flattened imports to a bare string — so any code reading +// `.src` (the prod pattern, e.g. getBadgeIcon's fallback) tested against +// fiction. Mirror the real shape so tests and prod agree. +const staticImageStub = { + src: '/test-file-stub', + height: 1, + width: 1, + blurDataURL: '/test-file-stub', +} + +export default staticImageStub