Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { OverlayPortalHost } from "./components/OverlayPortal";
import { useUniwindTheme } from "./lib/useUniwindTheme";
import { appBlurTargetRef } from "./lib/appBlurTarget";
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
import { shouldHandleAppLink } from "./lib/appLinking";

import "../global.css";

Expand All @@ -41,14 +42,7 @@ const appLinking = {
// schemes `app.config.ts` registers, because iOS never delivers an
// unregistered scheme to the app.
prefixes: [Linking.createURL("/"), "pylon-code://", "pylon-code-dev://", "pylon-code-preview://"],
// The Expo dev client launches the app via
// <scheme>://expo-development-client/?url=<packager> — 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);
Expand Down
34 changes: 34 additions & 0 deletions apps/mobile/src/lib/appLinking.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vite-plus/test";

import { shouldHandleAppLink } from "./appLinking";

describe("shouldHandleAppLink", () => {
it.each([
"pylon-code://",
"pylon-code:///",
"pylon-code-dev://",
"pylon-code-dev:///",
"pylon-code-preview://",
"pylon-code-preview:///",
"PYLON-CODE://",
])("keeps the current route for wake-only link %s", (url) => {
expect(shouldHandleAppLink(url)).toBe(false);
});

it.each([
"pylon-code://threads/env-1/thread-1",
"pylon-code-dev://threads/env-1/thread-1",
"pylon-code-preview://settings/environments",
"pylon-code://pair?pairingUrl=https%3A%2F%2Fexample.test",
"pylon-code://?pairingUrl=https%3A%2F%2Fexample.test",
])("preserves navigable and pairing link %s", (url) => {
expect(shouldHandleAppLink(url)).toBe(true);
});

it.each([
"pylon-code-dev://expo-development-client/?url=packager",
"pylon-code://expo-sharing/anything",
])("ignores native lifecycle link %s", (url) => {
expect(shouldHandleAppLink(url)).toBe(false);
});
});
10 changes: 10 additions & 0 deletions apps/mobile/src/lib/appLinking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** Ignore lifecycle and wake-only URLs before they reach React Navigation. */
export function shouldHandleAppLink(url: string): boolean {
if (url.includes("expo-development-client") || url.includes("://expo-sharing")) {
return false;
}

// iOS dictation keyboards can return to the app with only its scheme. Those
// URLs wake the app but have no route, so navigation must keep its stack.
return !/^pylon-code(?:-dev|-preview)?:\/*$/i.test(url);
}
Loading