Skip to content

Commit c8ac939

Browse files
authored
refactor: Process link (#38)
* refactor: rename API_KEY to apiKey * refactor: update detour context type and usage * refactor: improve examples and clean up types * docs: improve deep link testing instructions and logic * refactor: simplify type annotation in useDetour hook
1 parent 06b2b45 commit c8ac939

48 files changed

Lines changed: 757 additions & 742 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import { Stack, usePathname, useRouter } from 'expo-router';
6666
SplashScreen.preventAutoHideAsync();
6767

6868
export function RootNavigator() {
69-
const { isLinkProcessed, linkRoute, clearLink } = useDetourContext();
69+
const { isLinkProcessed, link, clearLink } = useDetourContext();
7070
const pathname = usePathname();
7171
const router = useRouter();
7272

@@ -77,13 +77,13 @@ export function RootNavigator() {
7777
}, [isLinkProcessed]);
7878

7979
useEffect(() => {
80-
if (!isLinkProcessed || !linkRoute) return;
81-
if (pathname !== linkRoute) {
82-
router.replace(linkRoute);
80+
if (!isLinkProcessed || !link) return;
81+
if (pathname !== link.pathname) {
82+
router.replace({ pathname: link.pathname, params: link.params });
8383
return;
8484
}
8585
clearLink(); // avoid redirecting again when returning to this screen
86-
}, [clearLink, isLinkProcessed, linkRoute, pathname, router]);
86+
}, [clearLink, isLinkProcessed, link, pathname, router]);
8787

8888
if (!isLinkProcessed) {
8989
return null;
@@ -143,7 +143,7 @@ If you want to know more details about a given example and how to configure it,
143143

144144
## Clearing handled links
145145

146-
If your app redirects based on `linkRoute` (especially in entry screens), call `clearLink()` after handling the route. This prevents repeated redirects when the user returns to the same screen.
146+
If your app redirects based on `link` (especially in entry screens), call `clearLink()` after handling the route. This prevents repeated redirects when the user returns to the same screen.
147147

148148
## Types
149149

@@ -191,7 +191,7 @@ export type Config = {
191191

192192
### DetourContextType
193193

194-
This type represents the object returned by the useDetourContext hook, containing the deferred link and its processing status.
194+
This type represents the object returned by the `useDetourContext` hook, containing the resolved link and its processing status.
195195

196196
```js
197197
export type DetourContextType = {
@@ -202,27 +202,43 @@ export type DetourContextType = {
202202
isLinkProcessed: boolean;
203203

204204
/**
205-
* The raw link URL (string or URL object), or null if no link was found.
205+
* The resolved link object, or null if no link was found.
206206
*/
207-
linkUrl: string | URL | null;
207+
link: DetourLink;
208208

209209
/**
210-
* The parsed route path derived from the link (e.g. '/details/42'), or null if no link was found.
210+
* Resets the link to null. Call this after you handle a link.
211211
*/
212-
linkRoute: string | null;
212+
clearLink: () => void;
213+
};
214+
```
213215

214-
/**
215-
* The type of the detected link: 'deferred', 'verified' (Universal/App link), or 'scheme'.
216-
* 'scheme' is only emitted when linkProcessingMode is 'all' (default).
217-
* Null if no link was found.
218-
*/
219-
linkType: LinkType | null;
216+
### DetourLink
217+
218+
The resolved link object, or null if no link was found.
219+
220+
```js
221+
export type DetourLink = {
222+
/** The original link URL as received by the SDK. */
223+
url: string | URL;
224+
225+
/** Full route path including query string (e.g. '/details/42?campaign=summer'). */
226+
route: string;
227+
228+
/** Route path without query string (e.g. '/details/42'). */
229+
pathname: string;
230+
231+
/** Parsed query parameters (e.g. { campaign: 'summer' }). */
232+
params: Record<string, string>;
220233

221234
/**
222-
* Clears the current link context (route/url/type). Call this after you handle a link.
235+
* The type of the detected link:
236+
* - 'deferred': resolved from the Detour API on first app install
237+
* - 'verified': Universal Link (iOS) or App Link (Android)
238+
* - 'scheme': custom scheme deep link (only when linkProcessingMode is 'all')
223239
*/
224-
clearLink: () => void;
225-
};
240+
type: LinkType;
241+
} | null;
226242
```
227243

228244
---

examples/expo-bare/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ If you need more complex routing flows with a specific navigation library, check
3333
- Install dependencies: `yarn install`
3434
- Configure this app in Detour Dashboard: `https://godetour.dev` using identifiers from `app.json` (for example `ios.bundleIdentifier`, `android.package`).
3535
- Use values from Dashboard from "API configuration" section to fill `.env` and update `app.json` with generated integration code.
36-
- Run prebuild for this example: `cd examples/expo-bare && npx expo prebuild`
37-
- Start the example from repo root: `yarn examples:expo-bare start`
38-
- Run on device/simulator: `yarn workspace @swmansion/react-native-detour-expo-bare ios` or `yarn workspace @swmansion/react-native-detour-expo-bare android`
36+
- Run prebuild for this example: `yarn prebuild`
37+
- Start the example: `yarn start`
38+
- Run on device/simulator: `yarn ios` or `yarn android`
39+
- Trigger test links: **deferred** — copy the link from Detour Dashboard before a fresh install, then install and launch (link resolves on first open). **Universal/App link** — open the link from Dashboard while the app is running. See **Test flow** for more detail.

examples/expo-bare/app.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,24 @@
77
"icon": "./assets/icon.png",
88
"userInterfaceStyle": "light",
99
"newArchEnabled": true,
10+
"scheme": "detour-expo-bare",
1011
"splash": {
1112
"image": "./assets/splash-icon.png",
1213
"resizeMode": "contain",
1314
"backgroundColor": "#ffffff"
1415
},
1516
"ios": {
16-
"supportsTablet": true,
17-
"bundleIdentifier": "detourreactnative.expobare"
17+
"bundleIdentifier": "detourreactnative.expobare",
18+
"appleTeamId": "ABCDE12345",
19+
"supportsTablet": true
1820
},
1921
"android": {
22+
"package": "detourreactnative.expobare",
2023
"adaptiveIcon": {
2124
"foregroundImage": "./assets/adaptive-icon.png",
2225
"backgroundColor": "#ffffff"
2326
},
24-
"edgeToEdgeEnabled": true,
25-
"package": "detourreactnative.expobare"
27+
"edgeToEdgeEnabled": true
2628
},
2729
"web": {
2830
"favicon": "./assets/favicon.png"

examples/expo-bare/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"start": "expo start",
77
"android": "expo run:android",
88
"ios": "expo run:ios",
9-
"web": "expo start --web"
9+
"web": "expo start --web",
10+
"prebuild": "expo prebuild --clean"
1011
},
1112
"dependencies": {
1213
"@expo/metro-runtime": "~5.0.4",

examples/expo-bare/src/Screen.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useDetourContext } from '@swmansion/react-native-detour';
22
import { StyleSheet, Text, View } from 'react-native';
33

44
export const Screen = () => {
5-
const { isLinkProcessed, linkRoute, linkType, linkUrl } = useDetourContext();
5+
const { isLinkProcessed, link } = useDetourContext();
66

77
return (
88
<View style={styles.container}>
@@ -23,16 +23,21 @@ export const Screen = () => {
2323
</Text>
2424

2525
<Text style={styles.status}>
26-
<Text style={styles.bold}>linkType:</Text> {linkType ?? 'none'}
26+
<Text style={styles.bold}>type:</Text> {link?.type ?? 'none'}
2727
</Text>
2828

2929
<Text style={styles.status}>
30-
<Text style={styles.bold}>linkUrl:</Text> {String(linkUrl ?? 'none')}
30+
<Text style={styles.bold}>url:</Text> {String(link?.url ?? 'none')}
31+
</Text>
32+
33+
<Text style={styles.status}>
34+
<Text style={styles.bold}>query params:</Text>{' '}
35+
{link?.params ? JSON.stringify(link.params, null, 2) : 'none'}
3136
</Text>
3237

3338
<Text style={styles.status}>
3439
<Text style={styles.resolvedLink}>Resolved route:</Text>{' '}
35-
{linkRoute ? linkRoute : 'none'}
40+
{link?.route ?? 'none'}
3641
</Text>
3742
</View>
3843
</View>

examples/expo-router-advanced/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Optional custom scheme test:
4040
- Install dependencies: `yarn install`
4141
- Configure this app in Detour Dashboard: `https://godetour.dev` using identifiers from `app.json` (for example `ios.bundleIdentifier`, `android.package`).
4242
- Use values from Dashboard from "API configuration" section to fill `.env` and update `app.json` with generated integration code.
43-
- Run prebuild for this example: `cd examples/expo-router-advanced && npx expo prebuild`
44-
- Start the example from repo root: `yarn examples:expo-router-advanced start`
45-
- Run on device/simulator: `yarn workspace @swmansion/react-native-detour-expo-router-advanced ios` or `yarn workspace @swmansion/react-native-detour-expo-router-advanced android`
43+
- Run prebuild for this example: `yarn prebuild`
44+
- Start the example: `yarn start`
45+
- Run on device/simulator: `yarn ios` or `yarn android`
46+
- Trigger test links: **deferred** — copy the link from Detour Dashboard before a fresh install, then install and launch (link resolves on first open). **Universal/App link** — open the link from Dashboard while the app is running. **Custom scheme** — open `detour-expo-router-advanced://app/anything` directly. See **Test flow** for more detail.

examples/expo-router-advanced/app.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@
1414
"backgroundColor": "#ffffff"
1515
},
1616
"ios": {
17-
"supportsTablet": true,
18-
"bundleIdentifier": "detourreactnative.exporouteradvanced"
17+
"bundleIdentifier": "detourreactnative.exporouteradvanced",
18+
"appleTeamId": "ABCDE12345",
19+
"supportsTablet": true
1920
},
2021
"android": {
22+
"package": "detourreactnative.exporouteradvanced",
2123
"adaptiveIcon": {
2224
"foregroundImage": "./assets/adaptive-icon.png",
2325
"backgroundColor": "#ffffff"
2426
},
25-
"edgeToEdgeEnabled": true,
26-
"package": "detourreactnative.exporouteradvanced"
27+
"edgeToEdgeEnabled": true
2728
},
2829
"web": {
2930
"favicon": "./assets/favicon.png"

examples/expo-router-advanced/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"start": "expo start",
77
"android": "expo run:android",
88
"ios": "expo run:ios",
9-
"web": "expo start --web"
9+
"web": "expo start --web",
10+
"prebuild": "expo prebuild --clean"
1011
},
1112
"dependencies": {
1213
"@expo/metro-runtime": "~5.0.4",

examples/expo-router-advanced/src/DetourGate.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,56 +11,55 @@ const getPathname = (route: string) => route.split('?')[0] || '/';
1111
// This component acts as a gate for incoming Detour links, coordinating with the auth state and routing logic of the app.
1212
// It should be placed inside the DetourProvider and rendered on all screens (e.g. in the root layout) to ensure that incoming links are handled correctly regardless of the current screen.
1313
export const DetourGate = () => {
14-
const { isLinkProcessed, linkRoute, linkType, clearLink } =
15-
useDetourContext();
14+
const { isLinkProcessed, link, clearLink } = useDetourContext();
1615
const { isSignedIn, setPendingLink, setPendingLinkType } = useAuth();
1716
const router = useRouter();
1817

1918
useEffect(() => {
2019
if (!isLinkProcessed) return;
2120

2221
// No link to process, hide the splash screen and continue as normal.
23-
if (!linkRoute) {
22+
if (!link) {
2423
SplashScreen.hideAsync();
2524
return;
2625
}
2726

2827
if (!isSignedIn) {
2928
// For signed-out users, keep the link as pending and continue with auth flow first.
30-
setPendingLink(linkRoute);
31-
setPendingLinkType(linkType);
29+
setPendingLink(link.route);
30+
setPendingLinkType(link.type);
3231
clearLink();
3332
SplashScreen.hideAsync();
3433
router.replace('/sign-in');
3534
return;
3635
}
3736

38-
const path = getPathname(linkRoute);
37+
const path = getPathname(link.route);
3938

4039
if (path !== ALLOWED_ROUTE) {
4140
// Example gate: only `/details` is accepted in this demo.
4241
clearLink();
4342
SplashScreen.hideAsync();
44-
router.replace('/+not-found');
43+
router.replace({
44+
pathname: '/+not-found',
45+
params: { path: link.route },
46+
});
4547
return;
4648
}
4749

48-
const nextParams = linkType
49-
? { fromDeepLink: 'true', linkType }
50-
: { fromDeepLink: 'true' };
51-
5250
clearLink();
5351
SplashScreen.hideAsync();
5452
router.replace({
5553
pathname: '/(app)/details',
56-
params: nextParams,
54+
// Except of link query params the debuging params are passed here to show how link data was processed.
55+
// You can remove them in production.
56+
params: { fromDeepLink: 'true', linkType: link.type, ...link.params },
5757
});
5858
}, [
5959
clearLink,
6060
isLinkProcessed,
6161
isSignedIn,
62-
linkRoute,
63-
linkType,
62+
link,
6463
router,
6564
setPendingLink,
6665
setPendingLinkType,
Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Link, useLocalSearchParams } from 'expo-router';
22
import { Platform } from 'react-native';
3-
import { Pressable, StyleSheet, Text, View } from 'react-native';
3+
import { Pressable, Text, View } from 'react-native';
4+
import { styles } from '../../styles';
45

56
// This screen is the expected destination for Detour links resolving to /details.
67
export default function DetailsScreen() {
@@ -15,6 +16,12 @@ export default function DetailsScreen() {
1516
default: linkType,
1617
});
1718
}
19+
// Remove the debug params from the params object to show only the original link params.
20+
const linkParams = Object.fromEntries(
21+
Object.entries(params).filter(
22+
([key]) => key !== 'fromDeepLink' && key !== 'linkType'
23+
)
24+
);
1825

1926
return (
2027
<View style={styles.screen}>
@@ -27,6 +34,14 @@ export default function DetailsScreen() {
2734
? `Opened via deep link (${linkType} link)`
2835
: 'Opened via button navigation'}
2936
</Text>
37+
{Object.keys(linkParams).length > 0 && (
38+
<>
39+
<Text style={styles.label}>Link parameters: </Text>
40+
<Text style={styles.label}>
41+
{JSON.stringify(linkParams, null, 2)}
42+
</Text>
43+
</>
44+
)}
3045
<Text style={styles.instructions}>
3146
Trigger a Detour link that resolves to{' '}
3247
<Text style={styles.bold}>/details</Text> to validate this flow.
@@ -40,48 +55,3 @@ export default function DetailsScreen() {
4055
</View>
4156
);
4257
}
43-
44-
const styles = StyleSheet.create({
45-
screen: {
46-
flex: 1,
47-
justifyContent: 'center',
48-
alignItems: 'center',
49-
padding: 20,
50-
},
51-
card: {
52-
padding: 24,
53-
borderRadius: 12,
54-
backgroundColor: '#f9fafb',
55-
gap: 12,
56-
alignItems: 'center',
57-
},
58-
title: {
59-
fontSize: 20,
60-
fontWeight: '600',
61-
},
62-
label: {
63-
fontSize: 14,
64-
color: '#6b7280',
65-
},
66-
instructions: {
67-
fontSize: 13,
68-
color: '#475569',
69-
},
70-
bold: {
71-
fontWeight: '600',
72-
},
73-
value: {
74-
fontWeight: '600',
75-
},
76-
button: {
77-
marginTop: 8,
78-
paddingHorizontal: 20,
79-
paddingVertical: 10,
80-
borderRadius: 8,
81-
backgroundColor: '#111827',
82-
},
83-
buttonText: {
84-
color: '#ffffff',
85-
fontWeight: '600',
86-
},
87-
});

0 commit comments

Comments
 (0)