From 08eadd8149670d2196abbf8a4ac1e1eb6508ed24 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 17:06:04 +0000 Subject: [PATCH] Split typecheck into per-platform passes (ios, android, web) The single native pass resolved [".ios", ".android", ".native", ""], which models a platform that does not exist - on iOS Metro never falls back to .android files and vice versa. Replace it with tsconfig.check.ios.json and tsconfig.check.android.json, each mirroring Metro's real resolution order for its platform, alongside the existing web pass. pnpm typecheck now runs all three, and the lint workflow matrix runs them as separate jobs. The ambient check shims in src/platform are now excluded from passes they aren't written for (previously the web shims were silently included in the native pass via the "src" include glob), and a new Android-pass shim pins react-native-device-attest to its platform-neutral base class, whose Android variant omits the iOS-only getDeviceCheckToken static. The Android pass surfaced a real bug: Referrer.getReferrerInfo() is async on Android (the Play referrer API only exposes a promise) but sync on iOS/web, and the shared callers used it synchronously - so on Android the deepLink:referrerReceived metric fired with a Promise instead of data. The API is now uniformly promise-returning and the callers await it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BCuMKXWHuyGoNhTVAHBMyk --- .github/workflows/lint.yml | 3 ++- .../src/Referrer/index.ios.ts | 10 +++++++++- .../src/Referrer/index.ts | 2 +- .../src/Referrer/index.web.ts | 10 +++++++++- package.json | 4 +++- src/Navigation.tsx | 17 +++++++++-------- src/lib/hooks/useIntentHandler.ts | 2 +- src/platform/misc.android-check.d.ts | 13 +++++++++++++ src/platform/misc.web-check.d.ts | 4 ++-- src/platform/react-native-svg.web-check.d.ts | 6 +++--- tsconfig.check.android.json | 14 ++++++++++++++ tsconfig.check.ios.json | 9 +++++++++ tsconfig.check.json | 10 +++++++++- 13 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 src/platform/misc.android-check.d.ts create mode 100644 tsconfig.check.android.json create mode 100644 tsconfig.check.ios.json diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 630f3ebef8..49bc67ada9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,8 @@ jobs: strategy: fail-fast: false matrix: - job: [lint, prettier, typecheck, 'typecheck:web'] + job: + [lint, prettier, 'typecheck:ios', 'typecheck:android', 'typecheck:web'] steps: - name: Check out Git repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts index 1bef0f74e9..5b0e566abd 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts @@ -6,7 +6,15 @@ export function getGooglePlayReferrerInfoAsync(): Promise { + return Promise.resolve(getReferrerInfoSync()) +} + +function getReferrerInfoSync(): ReferrerInfo | null { const referrer = SharedPrefs.getString('referrer') if (referrer) { SharedPrefs.removeValue('referrer') diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts index 89ed4f58a3..3771e071de 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts @@ -5,6 +5,6 @@ export function getGooglePlayReferrerInfoAsync(): Promise { throw new NotImplementedError() } diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts index 3f8671ac31..d0510ef89f 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts @@ -7,7 +7,15 @@ export function getGooglePlayReferrerInfoAsync(): Promise { + return Promise.resolve(getReferrerInfoSync()) +} + +function getReferrerInfoSync(): ReferrerInfo | null { if ( Platform.OS === 'web' && // for ssr diff --git a/package.json b/package.json index 3484bc643a..198e0e1c49 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,9 @@ "lint": "oxlint --quiet src modules", "lint-native": "swiftlint ./modules && ktlint ./modules", "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", - "typecheck": "tsgo --project ./tsconfig.check.json", + "typecheck": "pnpm run typecheck:ios && pnpm run typecheck:android && pnpm run typecheck:web", + "typecheck:ios": "tsgo --project ./tsconfig.check.ios.json", + "typecheck:android": "tsgo --project ./tsconfig.check.android.json", "typecheck:web": "tsgo --project ./tsconfig.check.web.json", "e2e:mock-server": "cd dev-env && pnpm start", "e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 76fa7187b2..5207a3a123 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1003,14 +1003,15 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { }) if (IS_WEB) { - const referrerInfo = Referrer.getReferrerInfo() - if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { - ax.metric('deepLink:referrerReceived', { - to: window.location.href, - referrer: referrerInfo?.referrer, - hostname: referrerInfo?.hostname, - }) - } + void Referrer.getReferrerInfo().then(referrerInfo => { + if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { + ax.metric('deepLink:referrerReceived', { + to: window.location.href, + referrer: referrerInfo?.referrer, + hostname: referrerInfo?.hostname, + }) + } + }) } // temp, just testing diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 5a9d0b39fe..eaa6482830 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -40,7 +40,7 @@ export function useIntentHandler() { await WebBrowser.dismissBrowser().catch(() => {}) } - const referrerInfo = Referrer.getReferrerInfo() + const referrerInfo = await Referrer.getReferrerInfo() if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { ax.metric('deepLink:referrerReceived', { to: url, diff --git a/src/platform/misc.android-check.d.ts b/src/platform/misc.android-check.d.ts new file mode 100644 index 0000000000..9c6361b5fb --- /dev/null +++ b/src/platform/misc.android-check.d.ts @@ -0,0 +1,13 @@ +/* + * Used ONLY by the Android typecheck pass (tsconfig.check.android.json) - it + * is excluded from the other passes and has no runtime effect. + * + * react-native-device-attest's Android variant implements only the Play + * Integrity statics, so the iOS-only getDeviceCheckToken (whose app call + * site is IS_IOS-guarded) fails to resolve under .android module suffixes. + * Pin the module to its platform-neutral base class, which declares the + * full API surface. + */ +declare module 'react-native-device-attest' { + export {DeviceAttestBase as default} from 'react-native-device-attest/build/DeviceAttestBase' +} diff --git a/src/platform/misc.web-check.d.ts b/src/platform/misc.web-check.d.ts index 81e0161353..d21fb93603 100644 --- a/src/platform/misc.web-check.d.ts +++ b/src/platform/misc.web-check.d.ts @@ -1,6 +1,6 @@ /* - * Used ONLY by the web typecheck pass (tsconfig.check.web.json) - it is not - * included by the main tsconfig, and it has no runtime effect. See + * Used ONLY by the web typecheck pass (tsconfig.check.web.json) - it is + * excluded from the other passes and has no runtime effect. See * react-native-svg.web-check.d.ts for the full background on why some * packages need to be pinned to their native declarations under * `moduleSuffixes: [".web", ""]`. diff --git a/src/platform/react-native-svg.web-check.d.ts b/src/platform/react-native-svg.web-check.d.ts index c8886ee362..c969d0fe7a 100644 --- a/src/platform/react-native-svg.web-check.d.ts +++ b/src/platform/react-native-svg.web-check.d.ts @@ -1,13 +1,13 @@ /* - * Used ONLY by the web typecheck pass (tsconfig.check.web.json) - it is not - * included by the main tsconfig, and it has no runtime effect. + * Used ONLY by the web typecheck pass (tsconfig.check.web.json) - it is + * excluded from the other passes and has no runtime effect. * * Under `moduleSuffixes: [".web", ""]`, react-native-svg's type entry * resolves to its DOM-flavored `ReactNativeSVG.web.d.ts`, a different API * surface (no SvgProps/PathProps, react-native-web style types) than the * native one the app is written against. At runtime the web build accepts * the same props, so this ambient declaration pins the package to its - * native declarations for one coherent type surface across both passes. + * native declarations for one coherent type surface across all passes. * * moduleSuffixes remaps even explicit `.d.ts` specifiers, so this mirrors * the package's ReactNativeSVG.d.ts + elements.d.ts via deep module paths diff --git a/tsconfig.check.android.json b/tsconfig.check.android.json new file mode 100644 index 0000000000..3aa14b5615 --- /dev/null +++ b/tsconfig.check.android.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.check.json", + "compilerOptions": { + /* + * Mirrors Metro's resolution order for platform=android. + */ + "moduleSuffixes": [".android", ".native", ""] + }, + /* + * Narrows the base exclude so the *.android-check.d.ts ambient shims + * apply to this pass. + */ + "exclude": ["src/platform/*.web-check.d.ts"] +} diff --git a/tsconfig.check.ios.json b/tsconfig.check.ios.json new file mode 100644 index 0000000000..c91d6f804e --- /dev/null +++ b/tsconfig.check.ios.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.check.json", + "compilerOptions": { + /* + * Mirrors Metro's resolution order for platform=ios. + */ + "moduleSuffixes": [".ios", ".native", ""] + } +} diff --git a/tsconfig.check.json b/tsconfig.check.json index f9f1370f6d..6a3a0ed03d 100644 --- a/tsconfig.check.json +++ b/tsconfig.check.json @@ -1,4 +1,12 @@ { "extends": "./tsconfig.json", - "include": ["src", "modules", "app.config.js"] + "include": ["src", "modules", "app.config.js"], + /* + * Ambient check shims apply only to the platform pass that includes them + * back (tsconfig.check.android.json, tsconfig.check.web.json). + */ + "exclude": [ + "src/platform/*.android-check.d.ts", + "src/platform/*.web-check.d.ts" + ] }