From a0ed2da0060328dadfed360c9ce22c6a285f0e90 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 26 Aug 2026 20:13:51 +0300 Subject: [PATCH] Fix starter pack navigation from search and explore (#11563) --- src/screens/StarterPack/StarterPackScreen.tsx | 5 +- src/types/bsky/__tests__/starterPack.test.ts | 51 ++++++++++++++++--- src/types/bsky/starterPack.ts | 13 +++++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index 2043ae2e7d..e73bab09ce 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -147,10 +147,7 @@ export function StarterPackScreenInner({ const isValid = starterPack && (starterPack.list || starterPack?.creator?.did === currentAccount?.did) && - // Cards may precache a synthetic full view while navigating. Its list CID - // is intentionally empty until the server response replaces it, so use - // the trusted app-view discriminator here instead of strict validation. - bsky.isType(app.bsky.graph.defs.starterPackView, starterPack) && + bsky.starterPack.isTrustedView(starterPack) && bsky.matches(app.bsky.graph.starterpack, starterPack.record) if (!did || !starterPack || !isValid || !moderationOpts) { diff --git a/src/types/bsky/__tests__/starterPack.test.ts b/src/types/bsky/__tests__/starterPack.test.ts index 161de2a867..c679abe600 100644 --- a/src/types/bsky/__tests__/starterPack.test.ts +++ b/src/types/bsky/__tests__/starterPack.test.ts @@ -1,7 +1,11 @@ -import {type app} from '#/lexicons' +/* Full schema matching exercises CID validation, so use the real CID parser. */ +jest.unmock('multiformats/cid') + +import {app} from '#/lexicons' import { type AnyStarterPackView, isBasicView, + isTrustedView, isView, } from '#/types/bsky/starterPack' @@ -9,15 +13,21 @@ const now = () => new Date().toISOString() const creator = { $type: 'app.bsky.actor.defs#profileViewBasic', - did: 'did:plc:abc', - handle: 'alice.test', + did: 'did:plc:qrllvid7s54k4hnwtqxwetrf', + handle: 'joshuajfriedman.com', } const basicView = { $type: 'app.bsky.graph.defs#starterPackViewBasic', - uri: 'at://did:plc:abc/app.bsky.graph.starterpack/123', - cid: 'bafypack', - record: {}, + uri: 'at://did:plc:qrllvid7s54k4hnwtqxwetrf/app.bsky.graph.starterpack/3l4poszxde32k', + cid: 'bafyreiaxduxpwdpjgvve3klfs4flkwjwfqiurszw4o6jvjarpqqmeqwiza', + record: { + $type: 'app.bsky.graph.starterpack', + createdAt: '2024-09-22T03:52:03.686Z', + feeds: [], + list: 'at://did:plc:qrllvid7s54k4hnwtqxwetrf/app.bsky.graph.list/3l4posztwzy2e', + name: 'Bluesky for Art History', + }, creator, indexedAt: now(), } @@ -27,6 +37,18 @@ const fullView = { $type: 'app.bsky.graph.defs#starterPackView', } +const {$type: _, ...directFullView} = fullView + +const syntheticFullView = { + ...fullView, + list: { + uri: 'at://did:plc:abc/app.bsky.graph.list/123', + cid: '', + name: 'Starter pack', + purpose: 'app.bsky.graph.defs#referencelist', + }, +} + /* * Type-level assertions for the view alias: it must accept both the basic and * the full starter pack view. Compile-time only - a failure surfaces as a @@ -76,6 +98,23 @@ describe('types/bsky/starterPack guards', () => { }) }) + describe('isTrustedView', () => { + it('accepts a direct full view with an omitted $type', () => { + expect(isTrustedView(directFullView)).toBe(true) + }) + + it('accepts a typed synthetic view with placeholder fields', () => { + expect( + app.bsky.graph.defs.starterPackView.matches(syntheticFullView), + ).toBe(false) + expect(isTrustedView(syntheticFullView)).toBe(true) + }) + + it('rejects the basic view', () => { + expect(isTrustedView(basicView)).toBe(false) + }) + }) + it('narrows a view from either world to a readable shape', () => { /* * The `$type` string is world-independent, so one guard narrows values from diff --git a/src/types/bsky/starterPack.ts b/src/types/bsky/starterPack.ts index 594fd40c89..b6894287e5 100644 --- a/src/types/bsky/starterPack.ts +++ b/src/types/bsky/starterPack.ts @@ -25,6 +25,19 @@ export function isView(v: unknown): v is app.bsky.graph.defs.StarterPackView { ) } +/** + * Accepts both forms of a full starter pack view used by the app: + * + * - direct lexicon refs returned by the app view, where `$type` may be omitted + * - trusted synthetic cache entries, which carry `$type` but may contain + * placeholder fields that do not yet pass full schema validation + */ +export function isTrustedView( + v: unknown, +): v is app.bsky.graph.defs.StarterPackView { + return isView(v) || app.bsky.graph.defs.starterPackView.matches(v) +} + /** * Matches any starter pack view exported by our SDK. */