diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 897dae47d57f..7b2c0318f9fb 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -22,6 +22,7 @@ import { RootStack } from "./Stack"; import { appAtomRegistry } from "./state/atom-registry"; import { OverlayPortalHost } from "./components/OverlayPortal"; import { appBlurTargetRef } from "./lib/appBlurTarget"; +import { shouldHandleAppLink } from "./lib/appLinking"; import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme"; import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator"; @@ -40,14 +41,7 @@ const appLinking = { prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], // Keep the compact thread list available beneath a directly opened thread. config: { initialRouteName: "Home" }, - // The Expo dev client launches the app via - // ://expo-development-client/?url= — that URL addresses - // the launcher, not app navigation. Without this filter it falls through - // to the NotFound wildcard route on every dev launch. - // expo-sharing uses a private lifecycle URL only to wake the app. The - // persisted share inbox below owns navigation once the payload is durable. - filter: (url: string) => - !url.includes("expo-development-client") && !url.includes("://expo-sharing"), + filter: shouldHandleAppLink, }; const Navigation = createStaticNavigation(RootStack); diff --git a/apps/mobile/src/lib/appLinking.test.ts b/apps/mobile/src/lib/appLinking.test.ts new file mode 100644 index 000000000000..62781ce5d4b6 --- /dev/null +++ b/apps/mobile/src/lib/appLinking.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { shouldHandleAppLink } from "./appLinking"; + +describe("shouldHandleAppLink", () => { + it.each(["t3code://", "t3code:///", "t3code-dev://", "t3code-preview://"])( + "ignores scheme-only URL %s", + (url) => { + expect(shouldHandleAppLink(url)).toBe(false); + }, + ); + + it.each([ + "t3code://threads/env-1/thread-1", + "t3code://pair?pairingUrl=x", + "t3code-dev://settings/usage?tab=limits", + ])("handles path-bearing URL %s", (url) => { + expect(shouldHandleAppLink(url)).toBe(true); + }); + + it.each(["t3code://expo-development-client/?url=x", "t3code://expo-sharing/anything"])( + "ignores lifecycle URL %s", + (url) => { + expect(shouldHandleAppLink(url)).toBe(false); + }, + ); +}); diff --git a/apps/mobile/src/lib/appLinking.ts b/apps/mobile/src/lib/appLinking.ts new file mode 100644 index 000000000000..16c746868802 --- /dev/null +++ b/apps/mobile/src/lib/appLinking.ts @@ -0,0 +1,17 @@ +/** + * The Expo dev client launches the app via + * ://expo-development-client/?url= — that URL addresses + * the launcher, not app navigation. Without this filter it falls through + * to the NotFound wildcard route on every dev launch. + * expo-sharing uses a private lifecycle URL only to wake the app. The + * persisted share inbox in App.tsx owns navigation once the payload is durable. + * A scheme-only URL, as sent by iOS dictation keyboards returning to the app, + * only wakes the app and must not reset navigation to Home. + */ +export function shouldHandleAppLink(url: string): boolean { + return ( + !url.includes("expo-development-client") && + !url.includes("://expo-sharing") && + !/^t3code(-dev|-preview)?:\/*$/.test(url) + ); +}