Skip to content

Commit 22dd4ce

Browse files
committed
feat: simplify auth-gated deep linking with React Navigation
1 parent d98fe5e commit 22dd4ce

13 files changed

Lines changed: 71 additions & 170 deletions

File tree

README.md

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,28 @@ This API requires `DetourProvider` to be mounted above your `NavigationContainer
136136
See React Navigation docs:
137137
https://reactnavigation.org/docs/deep-linking?config=static#integrating-with-other-tools
138138

139-
For auth-gated apps, use the helper hook to avoid custom queueing boilerplate:
140-
141-
```ts
142-
import { useDetourReactNavigationLinking } from "@swmansion/react-native-detour";
143-
144-
const linking = useDetourReactNavigationLinking({
145-
config: linkingConfig,
146-
canHandleUrl: isSignedIn && isOnboardingCompleted,
147-
});
139+
For auth-gated apps, let React Navigation hold the deep link until the right screen is reachable.
140+
Render screens conditionally on auth/onboarding state and opt in to React Navigation's pending-link
141+
behavior on the navigator:
142+
143+
```tsx
144+
<Stack.Navigator UNSTABLE_routeNamesChangeBehavior="lastUnhandled">
145+
{isSignedIn
146+
? isOnboardingCompleted
147+
? <>
148+
<Stack.Screen name="Tabs" component={TabNavigator} />
149+
<Stack.Screen name="Details" component={Details} />
150+
</>
151+
: <Stack.Screen name="Onboarding" component={Onboarding} />
152+
: <Stack.Screen name="SignIn" component={SignIn} />}
153+
</Stack.Navigator>
148154
```
149155

156+
A deep link that arrives while the user is signed-out is parsed, found unreachable (the target
157+
screen isn't currently rendered), and remembered. When the rendered screen set changes — after
158+
sign-in, then again after onboarding — React Navigation retries and lands the user on the target.
159+
See `examples/react-navigation-advanced` for a working setup.
160+
150161
### Controlling which links Detour processes
151162

152163
Use `linkProcessingMode` to control which link sources the SDK listens to:
@@ -180,7 +191,7 @@ All example apps with Detour SDK integrated live in `examples/`:
180191
| `examples/expo-router-advanced` | Expo Router with auth flow and custom native-intent |
181192
| `examples/expo-bare` | Expo without file-based routing (plain `index.js` entry point) |
182193
| `examples/react-navigation` | Minimal React Navigation example |
183-
| `examples/react-navigation-advanced` | React Navigation with auth flow and dedicated helper API |
194+
| `examples/react-navigation-advanced` | React Navigation with auth + onboarding gated deep linking |
184195

185196
The monorepo uses **pnpm workspaces**. Start by installing all dependencies from the repo root:
186197

@@ -328,18 +339,11 @@ export type DetourUrlEvent = {
328339
export type DetourUrlSubscription = {
329340
remove: () => void;
330341
};
331-
332-
export type UseDetourReactNavigationLinkingOptions<Config = unknown> = {
333-
config: Config;
334-
canHandleUrl?: boolean;
335-
prefixes?: string[];
336-
};
337342
```
338343

339344
```js
340345
Detour.getInitialURL(): Promise<string | undefined>
341346
Detour.addEventListener("url", (event: DetourUrlEvent) => void): DetourUrlSubscription
342-
useDetourReactNavigationLinking(options): linking
343347
```
344348

345349
---

examples/react-navigation-advanced/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@ This example demonstrates an auth-gated React Navigation app with Detour integra
66

77
- Auth flow with conditional screen rendering in a single stack (React Navigation standard pattern).
88
- Screens: `SignIn``Onboarding` (once per install) → `Tabs` (Home, Explore, Settings) + `Details`.
9-
- `useDetourGate` exposes auth/onboarding gate state and `useDetourReactNavigationLinking` handles queued URL delivery.
10-
- React Navigation linking uses the SDK adapter API:
9+
- React Navigation linking uses the SDK adapter API as the URL source:
1110
- `Detour.getInitialURL()`
1211
- `Detour.addEventListener("url", ({ url }) => ...)`
13-
- The helper hook (`useDetourReactNavigationLinking`) wraps these APIs and removes custom bridge boilerplate.
12+
- The navigator opts into `UNSTABLE_routeNamesChangeBehavior="lastUnhandled"` so React Navigation remembers a deep link that hits a screen which is not currently rendered and replays it once that screen becomes part of the navigator.
1413
- Detour processes all link types (universal / app links, custom scheme, deferred) and the app maps routes via React Navigation linking config.
1514

1615
## Auth-gated deferred link behavior
1716

18-
- If a deferred link arrives and the user is not signed in, the splash hides and `SignIn` is shown. The link is queued.
19-
- After sign-in, `useDetourGate` re-fires. If onboarding has not been completed yet, `Onboarding` is shown first — the link is still kept alive.
20-
- After onboarding, `useDetourGate` re-fires again and the queued URL is delivered to React Navigation linking, which resolves `details` (or falls through to `NotFound`).
17+
- If a deferred link arrives and the user is not signed in, the splash hides and `SignIn` is shown. React Navigation parses the URL, finds `Details` is not currently rendered, and marks the action as the last unhandled one.
18+
- After sign-in, the rendered screen set changes. If onboarding has not been completed yet, `Onboarding` is shown `Details` is still not rendered, so the pending link stays remembered.
19+
- After onboarding, `Details` becomes part of the rendered stack. React Navigation retries the unhandled action and navigates to `Details` (or falls through to `NotFound`).
2120

2221
Reference docs:
23-
https://reactnavigation.org/docs/deep-linking?config=static#integrating-with-other-tools
22+
- https://reactnavigation.org/docs/deep-linking?config=static#integrating-with-other-tools
23+
- https://reactnavigation.org/docs/auth-flow (see `UNSTABLE_routeNamesChangeBehavior`)
2424

2525
## Test flow
2626

examples/react-navigation-advanced/src/App.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useMemo } from "react";
22

33
import { Text, View } from "react-native";
44

55
import * as SplashScreen from "expo-splash-screen";
66
import * as SystemUI from "expo-system-ui";
77

8-
import { NavigationContainer } from "@react-navigation/native";
8+
import { type LinkingOptions, NavigationContainer } from "@react-navigation/native";
99

1010
import {
1111
type Config,
12+
DETOUR_LINKING_PREFIX,
13+
Detour,
1214
DetourProvider,
13-
useDetourReactNavigationLinking,
1415
} from "@swmansion/react-native-detour";
1516

1617
import { AuthProvider } from "./auth";
17-
import { Navigation, linkingConfig } from "./navigation";
18+
import { Navigation, type RootStackParamList, linkingConfig } from "./navigation";
1819
import { colors, styles } from "./styles";
19-
import { useDetourGate } from "./useDetourGate";
2020

2121
const hasCredentials =
2222
!!process.env.EXPO_PUBLIC_DETOUR_API_KEY && !!process.env.EXPO_PUBLIC_DETOUR_APP_ID;
@@ -53,17 +53,28 @@ SplashScreen.preventAutoHideAsync();
5353
SystemUI.setBackgroundColorAsync(colors.background);
5454

5555
const AppRoot = () => {
56-
const [isNavigationReady, setNavigationReady] = useState(false);
57-
const { canHandleDetourLink } = useDetourGate(isNavigationReady);
58-
const linking = useDetourReactNavigationLinking({
59-
config: linkingConfig,
60-
canHandleUrl: canHandleDetourLink,
61-
});
56+
const linking = useMemo<LinkingOptions<RootStackParamList>>(
57+
() => ({
58+
prefixes: [DETOUR_LINKING_PREFIX],
59+
config: linkingConfig,
60+
async getInitialURL() {
61+
return await Detour.getInitialURL();
62+
},
63+
subscribe(listener) {
64+
const subscription = Detour.addEventListener("url", ({ url }) => {
65+
listener(url);
66+
});
67+
68+
return () => subscription.remove();
69+
},
70+
}),
71+
[],
72+
);
6273

6374
return (
6475
<NavigationContainer
6576
linking={linking}
66-
onReady={() => setNavigationReady(true)}
77+
onReady={() => SplashScreen.hideAsync()}
6778
theme={{
6879
dark: true,
6980
colors: {

examples/react-navigation-advanced/src/navigation/index.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,20 @@ function renderAuthScreens({
6868
}
6969

7070
// Auth flow using conditional screen rendering — equivalent of Stack.Protected in expo-router.
71-
// When isSignedIn or isOnboardingCompleted changes the navigator resets to the first valid screen.
71+
// `UNSTABLE_routeNamesChangeBehavior="lastUnhandled"` makes React Navigation remember a deep link
72+
// that hits a screen which isn't currently rendered (e.g. Details while signed-out) and replay it
73+
// once the navigator's screen set changes (after sign-in, then again after onboarding).
7274
// Returns null until auth is loaded from AsyncStorage so the splash covers the empty state.
7375
export function Navigation() {
7476
const { isLoaded, isSignedIn, isOnboardingCompleted } = useAuth();
7577

7678
if (!isLoaded) return null;
7779

7880
return (
79-
<Stack.Navigator screenOptions={screenOptions}>
81+
<Stack.Navigator
82+
screenOptions={screenOptions}
83+
UNSTABLE_routeNamesChangeBehavior="lastUnhandled"
84+
>
8085
{renderAuthScreens({ isSignedIn, isOnboardingCompleted })}
8186
<Stack.Screen name="NotFound" component={NotFound} />
8287
</Stack.Navigator>

examples/react-navigation-advanced/src/navigation/screens/Explore.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export function Explore() {
4343

4444
<Text style={styles.sectionHeader}>Deferred Link + Auth Gate</Text>
4545
<Text style={styles.bullet}>
46-
Copy a Detour link, sign out, then relaunch. The link survives sign-in {" "}
47-
<Text style={styles.accent}>useDetourGate</Text> picks it up once authenticated.
46+
Copy a Detour link, sign out, then relaunch. The link survives the sign-in flow — you'll
47+
land on the Details screen once you complete sign-in and onboarding.
4848
</Text>
4949
<Text style={styles.bullet}>
5050
Make sure <Text style={styles.accent}>Copy link feature enabled</Text> is turned on in

examples/react-navigation-advanced/src/navigation/screens/Home.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export function Home() {
4343

4444
<Text style={styles.sectionHeader}>Deferred Link + Auth Gate</Text>
4545
<Text style={styles.bullet}>
46-
Copy a Detour link, sign out, then relaunch. The link survives sign-in {" "}
47-
<Text style={styles.accent}>useDetourGate</Text> picks it up once authenticated.
46+
Copy a Detour link, sign out, then relaunch. The link survives the sign-in flow — you'll
47+
land on the Details screen once you complete sign-in and onboarding.
4848
</Text>
4949
<Text style={styles.bullet}>
5050
Make sure <Text style={styles.accent}>Copy link feature enabled</Text> is turned on in

examples/react-navigation-advanced/src/navigation/screens/Onboarding.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ export function Onboarding() {
4444
<Text style={styles.sectionHeader}>Deferred Link + Auth Gate</Text>
4545
<Text style={styles.bullet}>
4646
Copy a Detour link to your clipboard, sign out, then relaunch. The deferred link will
47-
survive the sign-in flow — once you authenticate,{" "}
48-
<Text style={styles.accent}>useDetourGate</Text> picks it back up and navigates
49-
automatically.
47+
survive the sign-in flow — once you finish sign-in and onboarding, React Navigation
48+
replays the link and lands you on Details automatically.
5049
</Text>
5150
<Text style={styles.bullet}>
5251
Make sure <Text style={styles.accent}>Copy link feature enabled</Text> is turned on in App

examples/react-navigation-advanced/src/navigation/screens/Settings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export function Settings() {
4343

4444
<Text style={styles.sectionHeader}>Deferred Link + Auth Gate</Text>
4545
<Text style={styles.bullet}>
46-
Copy a Detour link, sign out, then relaunch. The link survives sign-in {" "}
47-
<Text style={styles.accent}>useDetourGate</Text> picks it up once authenticated.
46+
Copy a Detour link, sign out, then relaunch. The link survives the sign-in flow — you'll
47+
land on the Details screen once you complete sign-in and onboarding.
4848
</Text>
4949
<Text style={styles.bullet}>
5050
Make sure <Text style={styles.accent}>Copy link feature enabled</Text> is turned on in

examples/react-navigation-advanced/src/useDetourGate.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

examples/react-navigation/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ This example demonstrates the minimal integration of `@swmansion/react-native-de
1515
- For React Navigation linking details, see:
1616
https://reactnavigation.org/docs/deep-linking?config=static#integrating-with-other-tools
1717

18-
For the dedicated helper API (`useDetourReactNavigationLinking`), see `examples/react-navigation-advanced`.
18+
For an auth-gated setup that uses React Navigation's pending-link behavior to survive sign-in and
19+
onboarding, see `examples/react-navigation-advanced`.
1920

2021
## Test flow
2122

0 commit comments

Comments
 (0)