@amazon-devices/react-navigation__native 2.0.1758683737
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +5 -0
- package/SystemBundles/6132afb62bda3afb78ba1a1aca51924df73e9dbcc77106aa0f97a0ef68b04ebb.bundle +15196 -0
- package/SystemBundles/6132afb62bda3afb78ba1a1aca51924df73e9dbcc77106aa0f97a0ef68b04ebb.map +1 -0
- package/SystemModules/amzn__react-navigation__native-2.modules.txt +76 -0
- package/SystemModules/amzn__react-navigation__native-2.private.modules.txt +101 -0
- package/SystemModules/keplerscript-system-bundle-config.json +7 -0
- package/THIRD_PARTY_NOTICES.txt +24 -0
- package/kepler-compatibility.json +8 -0
- package/kepler-transformed-package-info.json +3 -0
- package/lib/commonjs/Link.js +50 -0
- package/lib/commonjs/Link.js.map +1 -0
- package/lib/commonjs/LinkingContext.js +15 -0
- package/lib/commonjs/LinkingContext.js.map +1 -0
- package/lib/commonjs/NavigationContainer.js +109 -0
- package/lib/commonjs/NavigationContainer.js.map +1 -0
- package/lib/commonjs/ServerContainer.js +54 -0
- package/lib/commonjs/ServerContainer.js.map +1 -0
- package/lib/commonjs/ServerContext.js +12 -0
- package/lib/commonjs/ServerContext.js.map +1 -0
- package/lib/commonjs/createMemoryHistory.js +220 -0
- package/lib/commonjs/createMemoryHistory.js.map +1 -0
- package/lib/commonjs/extractPathFromURL.js +25 -0
- package/lib/commonjs/extractPathFromURL.js.map +1 -0
- package/lib/commonjs/index.js +129 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/theming/DarkTheme.js +19 -0
- package/lib/commonjs/theming/DarkTheme.js.map +1 -0
- package/lib/commonjs/theming/DefaultTheme.js +19 -0
- package/lib/commonjs/theming/DefaultTheme.js.map +1 -0
- package/lib/commonjs/theming/ThemeContext.js +15 -0
- package/lib/commonjs/theming/ThemeContext.js.map +1 -0
- package/lib/commonjs/theming/ThemeProvider.js +20 -0
- package/lib/commonjs/theming/ThemeProvider.js.map +1 -0
- package/lib/commonjs/theming/useTheme.js +16 -0
- package/lib/commonjs/theming/useTheme.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useBackButton.js +11 -0
- package/lib/commonjs/useBackButton.js.map +1 -0
- package/lib/commonjs/useBackButton.native.js +27 -0
- package/lib/commonjs/useBackButton.native.js.map +1 -0
- package/lib/commonjs/useDocumentTitle.js +32 -0
- package/lib/commonjs/useDocumentTitle.js.map +1 -0
- package/lib/commonjs/useDocumentTitle.native.js +10 -0
- package/lib/commonjs/useDocumentTitle.native.js.map +1 -0
- package/lib/commonjs/useLinkBuilder.js +63 -0
- package/lib/commonjs/useLinkBuilder.js.map +1 -0
- package/lib/commonjs/useLinkProps.js +97 -0
- package/lib/commonjs/useLinkProps.js.map +1 -0
- package/lib/commonjs/useLinkTo.js +45 -0
- package/lib/commonjs/useLinkTo.js.map +1 -0
- package/lib/commonjs/useLinking.js +303 -0
- package/lib/commonjs/useLinking.js.map +1 -0
- package/lib/commonjs/useLinking.native.js +152 -0
- package/lib/commonjs/useLinking.native.js.map +1 -0
- package/lib/commonjs/useScrollToTop.js +95 -0
- package/lib/commonjs/useScrollToTop.js.map +1 -0
- package/lib/commonjs/useThenable.js +41 -0
- package/lib/commonjs/useThenable.js.map +1 -0
- package/lib/module/Link.js +41 -0
- package/lib/module/Link.js.map +1 -0
- package/lib/module/LinkingContext.js +7 -0
- package/lib/module/LinkingContext.js.map +1 -0
- package/lib/module/NavigationContainer.js +100 -0
- package/lib/module/NavigationContainer.js.map +1 -0
- package/lib/module/ServerContainer.js +45 -0
- package/lib/module/ServerContainer.js.map +1 -0
- package/lib/module/ServerContext.js +4 -0
- package/lib/module/ServerContext.js.map +1 -0
- package/lib/module/createMemoryHistory.js +214 -0
- package/lib/module/createMemoryHistory.js.map +1 -0
- package/lib/module/extractPathFromURL.js +18 -0
- package/lib/module/extractPathFromURL.js.map +1 -0
- package/lib/module/index.js +15 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/theming/DarkTheme.js +13 -0
- package/lib/module/theming/DarkTheme.js.map +1 -0
- package/lib/module/theming/DefaultTheme.js +13 -0
- package/lib/module/theming/DefaultTheme.js.map +1 -0
- package/lib/module/theming/ThemeContext.js +6 -0
- package/lib/module/theming/ThemeContext.js.map +1 -0
- package/lib/module/theming/ThemeProvider.js +11 -0
- package/lib/module/theming/ThemeProvider.js.map +1 -0
- package/lib/module/theming/useTheme.js +7 -0
- package/lib/module/theming/useTheme.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useBackButton.js +5 -0
- package/lib/module/useBackButton.js.map +1 -0
- package/lib/module/useBackButton.native.js +19 -0
- package/lib/module/useBackButton.native.js.map +1 -0
- package/lib/module/useDocumentTitle.js +24 -0
- package/lib/module/useDocumentTitle.js.map +1 -0
- package/lib/module/useDocumentTitle.native.js +4 -0
- package/lib/module/useDocumentTitle.native.js.map +1 -0
- package/lib/module/useLinkBuilder.js +54 -0
- package/lib/module/useLinkBuilder.js.map +1 -0
- package/lib/module/useLinkProps.js +88 -0
- package/lib/module/useLinkProps.js.map +1 -0
- package/lib/module/useLinkTo.js +36 -0
- package/lib/module/useLinkTo.js.map +1 -0
- package/lib/module/useLinking.js +292 -0
- package/lib/module/useLinking.js.map +1 -0
- package/lib/module/useLinking.native.js +143 -0
- package/lib/module/useLinking.native.js.map +1 -0
- package/lib/module/useScrollToTop.js +87 -0
- package/lib/module/useScrollToTop.js.map +1 -0
- package/lib/module/useThenable.js +33 -0
- package/lib/module/useThenable.js.map +1 -0
- package/lib/typescript/src/Link.d.ts +24 -0
- package/lib/typescript/src/Link.d.ts.map +1 -0
- package/lib/typescript/src/LinkingContext.d.ts +8 -0
- package/lib/typescript/src/LinkingContext.d.ts.map +1 -0
- package/lib/typescript/src/NavigationContainer.d.ts +26 -0
- package/lib/typescript/src/NavigationContainer.d.ts.map +1 -0
- package/lib/typescript/src/ServerContainer.d.ts +15 -0
- package/lib/typescript/src/ServerContainer.d.ts.map +1 -0
- package/lib/typescript/src/ServerContext.d.ts +10 -0
- package/lib/typescript/src/ServerContext.d.ts.map +1 -0
- package/lib/typescript/src/__mocks__/window.d.ts +19 -0
- package/lib/typescript/src/__mocks__/window.d.ts.map +1 -0
- package/lib/typescript/src/createMemoryHistory.d.ts +25 -0
- package/lib/typescript/src/createMemoryHistory.d.ts.map +1 -0
- package/lib/typescript/src/extractPathFromURL.d.ts +2 -0
- package/lib/typescript/src/extractPathFromURL.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/theming/DarkTheme.d.ts +4 -0
- package/lib/typescript/src/theming/DarkTheme.d.ts.map +1 -0
- package/lib/typescript/src/theming/DefaultTheme.d.ts +4 -0
- package/lib/typescript/src/theming/DefaultTheme.d.ts.map +1 -0
- package/lib/typescript/src/theming/ThemeContext.d.ts +5 -0
- package/lib/typescript/src/theming/ThemeContext.d.ts.map +1 -0
- package/lib/typescript/src/theming/ThemeProvider.d.ts +9 -0
- package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -0
- package/lib/typescript/src/theming/useTheme.d.ts +2 -0
- package/lib/typescript/src/theming/useTheme.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +127 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useBackButton.d.ts +4 -0
- package/lib/typescript/src/useBackButton.d.ts.map +1 -0
- package/lib/typescript/src/useBackButton.native.d.ts +4 -0
- package/lib/typescript/src/useBackButton.native.d.ts.map +1 -0
- package/lib/typescript/src/useDocumentTitle.d.ts +8 -0
- package/lib/typescript/src/useDocumentTitle.d.ts.map +1 -0
- package/lib/typescript/src/useDocumentTitle.native.d.ts +2 -0
- package/lib/typescript/src/useDocumentTitle.native.d.ts.map +1 -0
- package/lib/typescript/src/useLinkBuilder.d.ts +6 -0
- package/lib/typescript/src/useLinkBuilder.d.ts.map +1 -0
- package/lib/typescript/src/useLinkProps.d.ts +21 -0
- package/lib/typescript/src/useLinkProps.d.ts.map +1 -0
- package/lib/typescript/src/useLinkTo.d.ts +9 -0
- package/lib/typescript/src/useLinkTo.d.ts.map +1 -0
- package/lib/typescript/src/useLinking.d.ts +55 -0
- package/lib/typescript/src/useLinking.d.ts.map +1 -0
- package/lib/typescript/src/useLinking.native.d.ts +51 -0
- package/lib/typescript/src/useLinking.native.d.ts.map +1 -0
- package/lib/typescript/src/useScrollToTop.d.ts +27 -0
- package/lib/typescript/src/useScrollToTop.d.ts.map +1 -0
- package/lib/typescript/src/useThenable.d.ts +2 -0
- package/lib/typescript/src/useThenable.d.ts.map +1 -0
- package/package.json +1 -0
- package/src/Link.tsx +53 -0
- package/src/LinkingContext.tsx +12 -0
- package/src/NavigationContainer.tsx +152 -0
- package/src/ServerContainer.tsx +56 -0
- package/src/ServerContext.tsx +14 -0
- package/src/__mocks__/window.tsx +77 -0
- package/src/createMemoryHistory.tsx +224 -0
- package/src/extractPathFromURL.tsx +29 -0
- package/src/index.tsx +14 -0
- package/src/theming/DarkTheme.tsx +15 -0
- package/src/theming/DefaultTheme.tsx +15 -0
- package/src/theming/ThemeContext.tsx +10 -0
- package/src/theming/ThemeProvider.tsx +15 -0
- package/src/theming/useTheme.tsx +9 -0
- package/src/types.tsx +145 -0
- package/src/useBackButton.native.tsx +33 -0
- package/src/useBackButton.tsx +11 -0
- package/src/useDocumentTitle.native.tsx +3 -0
- package/src/useDocumentTitle.tsx +41 -0
- package/src/useLinkBuilder.tsx +82 -0
- package/src/useLinkProps.tsx +126 -0
- package/src/useLinkTo.tsx +71 -0
- package/src/useLinking.native.tsx +215 -0
- package/src/useLinking.tsx +395 -0
- package/src/useScrollToTop.tsx +116 -0
- package/src/useThenable.tsx +41 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getActionFromState as getActionFromStateDefault,
|
|
3
|
+
getStateFromPath as getStateFromPathDefault,
|
|
4
|
+
NavigationContainerRef,
|
|
5
|
+
ParamListBase,
|
|
6
|
+
} from '@amazon-devices/react-navigation__core';
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import { Linking, Platform } from 'react-native';
|
|
9
|
+
|
|
10
|
+
import extractPathFromURL from './extractPathFromURL';
|
|
11
|
+
import type { LinkingOptions } from './types';
|
|
12
|
+
|
|
13
|
+
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
|
14
|
+
|
|
15
|
+
type Options = LinkingOptions<ParamListBase> & {
|
|
16
|
+
independent?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let linkingHandlers: Symbol[] = [];
|
|
20
|
+
|
|
21
|
+
export default function useLinking(
|
|
22
|
+
ref: React.RefObject<NavigationContainerRef<ParamListBase>>,
|
|
23
|
+
{
|
|
24
|
+
independent,
|
|
25
|
+
enabled = true,
|
|
26
|
+
prefixes,
|
|
27
|
+
filter,
|
|
28
|
+
config,
|
|
29
|
+
getInitialURL = () =>
|
|
30
|
+
Promise.race([
|
|
31
|
+
Linking.getInitialURL(),
|
|
32
|
+
new Promise<undefined>((resolve) =>
|
|
33
|
+
// Timeout in 150ms if `getInitialState` doesn't resolve
|
|
34
|
+
// Workaround for https://github.com/facebook/react-native/issues/25675
|
|
35
|
+
setTimeout(resolve, 150)
|
|
36
|
+
),
|
|
37
|
+
]),
|
|
38
|
+
subscribe = (listener) => {
|
|
39
|
+
const callback = ({ url }: { url: string }) => listener(url);
|
|
40
|
+
|
|
41
|
+
const subscription = Linking.addEventListener('url', callback) as
|
|
42
|
+
| { remove(): void }
|
|
43
|
+
| undefined;
|
|
44
|
+
|
|
45
|
+
// Storing this in a local variable stops Jest from complaining about import after teardown
|
|
46
|
+
// @ts-expect-error: removeEventListener is not present in newer RN versions
|
|
47
|
+
const removeEventListener = Linking.removeEventListener?.bind(Linking);
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
// https://github.com/facebook/react-native/commit/6d1aca806cee86ad76de771ed3a1cc62982ebcd7
|
|
51
|
+
if (subscription?.remove) {
|
|
52
|
+
subscription.remove();
|
|
53
|
+
} else {
|
|
54
|
+
removeEventListener?.('url', callback);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
getStateFromPath = getStateFromPathDefault,
|
|
59
|
+
getActionFromState = getActionFromStateDefault,
|
|
60
|
+
}: Options
|
|
61
|
+
) {
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (process.env.NODE_ENV === 'production') {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (independent) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (enabled !== false && linkingHandlers.length) {
|
|
72
|
+
console.error(
|
|
73
|
+
[
|
|
74
|
+
'Looks like you have configured linking in multiple places. This is likely an error since deep links should only be handled in one place to avoid conflicts. Make sure that:',
|
|
75
|
+
"- You don't have multiple NavigationContainers in the app each with 'linking' enabled",
|
|
76
|
+
'- Only a single instance of the root component is rendered',
|
|
77
|
+
Platform.OS === 'android'
|
|
78
|
+
? "- You have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple instances"
|
|
79
|
+
: '',
|
|
80
|
+
]
|
|
81
|
+
.join('\n')
|
|
82
|
+
.trim()
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handler = Symbol();
|
|
87
|
+
|
|
88
|
+
if (enabled !== false) {
|
|
89
|
+
linkingHandlers.push(handler);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
const index = linkingHandlers.indexOf(handler);
|
|
94
|
+
|
|
95
|
+
if (index > -1) {
|
|
96
|
+
linkingHandlers.splice(index, 1);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, [enabled, independent]);
|
|
100
|
+
|
|
101
|
+
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
|
|
102
|
+
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
|
|
103
|
+
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
|
|
104
|
+
const enabledRef = React.useRef(enabled);
|
|
105
|
+
const prefixesRef = React.useRef(prefixes);
|
|
106
|
+
const filterRef = React.useRef(filter);
|
|
107
|
+
const configRef = React.useRef(config);
|
|
108
|
+
const getInitialURLRef = React.useRef(getInitialURL);
|
|
109
|
+
const getStateFromPathRef = React.useRef(getStateFromPath);
|
|
110
|
+
const getActionFromStateRef = React.useRef(getActionFromState);
|
|
111
|
+
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
enabledRef.current = enabled;
|
|
114
|
+
prefixesRef.current = prefixes;
|
|
115
|
+
filterRef.current = filter;
|
|
116
|
+
configRef.current = config;
|
|
117
|
+
getInitialURLRef.current = getInitialURL;
|
|
118
|
+
getStateFromPathRef.current = getStateFromPath;
|
|
119
|
+
getActionFromStateRef.current = getActionFromState;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const getStateFromURL = React.useCallback(
|
|
123
|
+
(url: string | null | undefined) => {
|
|
124
|
+
if (!url || (filterRef.current && !filterRef.current(url))) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const path = extractPathFromURL(prefixesRef.current, url);
|
|
129
|
+
|
|
130
|
+
return path !== undefined
|
|
131
|
+
? getStateFromPathRef.current(path, configRef.current)
|
|
132
|
+
: undefined;
|
|
133
|
+
},
|
|
134
|
+
[]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const getInitialState = React.useCallback(() => {
|
|
138
|
+
let state: ResultState | undefined;
|
|
139
|
+
|
|
140
|
+
if (enabledRef.current) {
|
|
141
|
+
const url = getInitialURLRef.current();
|
|
142
|
+
|
|
143
|
+
if (url != null && typeof url !== 'string') {
|
|
144
|
+
return url.then((url) => {
|
|
145
|
+
const state = getStateFromURL(url);
|
|
146
|
+
|
|
147
|
+
return state;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
state = getStateFromURL(url);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const thenable = {
|
|
155
|
+
then(onfulfilled?: (state: ResultState | undefined) => void) {
|
|
156
|
+
return Promise.resolve(onfulfilled ? onfulfilled(state) : state);
|
|
157
|
+
},
|
|
158
|
+
catch() {
|
|
159
|
+
return thenable;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return thenable as PromiseLike<ResultState | undefined>;
|
|
164
|
+
}, [getStateFromURL]);
|
|
165
|
+
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
const listener = (url: string) => {
|
|
168
|
+
if (!enabled) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const navigation = ref.current;
|
|
173
|
+
const state = navigation ? getStateFromURL(url) : undefined;
|
|
174
|
+
|
|
175
|
+
if (navigation && state) {
|
|
176
|
+
// Make sure that the routes in the state exist in the root navigator
|
|
177
|
+
// Otherwise there's an error in the linking configuration
|
|
178
|
+
const rootState = navigation.getRootState();
|
|
179
|
+
|
|
180
|
+
if (state.routes.some((r) => !rootState?.routeNames.includes(r.name))) {
|
|
181
|
+
console.warn(
|
|
182
|
+
"The navigation state parsed from the URL contains routes not present in the root navigator. This usually means that the linking configuration doesn't match the navigation structure. See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration."
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const action = getActionFromStateRef.current(state, configRef.current);
|
|
188
|
+
|
|
189
|
+
if (action !== undefined) {
|
|
190
|
+
try {
|
|
191
|
+
navigation.dispatch(action);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
// Ignore any errors from deep linking.
|
|
194
|
+
// This could happen in case of malformed links, navigation object not being initialized etc.
|
|
195
|
+
console.warn(
|
|
196
|
+
`An error occurred when trying to handle the link '${url}': ${
|
|
197
|
+
typeof e === 'object' && e != null && 'message' in e
|
|
198
|
+
? e.message
|
|
199
|
+
: e
|
|
200
|
+
}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
navigation.resetRoot(state);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return subscribe(listener);
|
|
210
|
+
}, [enabled, getStateFromURL, ref, subscribe]);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
getInitialState,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findFocusedRoute,
|
|
3
|
+
getActionFromState as getActionFromStateDefault,
|
|
4
|
+
getPathFromState as getPathFromStateDefault,
|
|
5
|
+
getStateFromPath as getStateFromPathDefault,
|
|
6
|
+
NavigationContainerRef,
|
|
7
|
+
NavigationState,
|
|
8
|
+
ParamListBase,
|
|
9
|
+
} from '@amazon-devices/react-navigation__core';
|
|
10
|
+
import isEqual from 'fast-deep-equal';
|
|
11
|
+
import * as React from 'react';
|
|
12
|
+
|
|
13
|
+
import createMemoryHistory from './createMemoryHistory';
|
|
14
|
+
import ServerContext from './ServerContext';
|
|
15
|
+
import type { LinkingOptions } from './types';
|
|
16
|
+
|
|
17
|
+
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find the matching navigation state that changed between 2 navigation states
|
|
21
|
+
* e.g.: a -> b -> c -> d and a -> b -> c -> e -> f, if history in b changed, b is the matching state
|
|
22
|
+
*/
|
|
23
|
+
const findMatchingState = <T extends NavigationState>(
|
|
24
|
+
a: T | undefined,
|
|
25
|
+
b: T | undefined
|
|
26
|
+
): [T | undefined, T | undefined] => {
|
|
27
|
+
if (a === undefined || b === undefined || a.key !== b.key) {
|
|
28
|
+
return [undefined, undefined];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Tab and drawer will have `history` property, but stack will have history in `routes`
|
|
32
|
+
const aHistoryLength = a.history ? a.history.length : a.routes.length;
|
|
33
|
+
const bHistoryLength = b.history ? b.history.length : b.routes.length;
|
|
34
|
+
|
|
35
|
+
const aRoute = a.routes[a.index];
|
|
36
|
+
const bRoute = b.routes[b.index];
|
|
37
|
+
|
|
38
|
+
const aChildState = aRoute.state as T | undefined;
|
|
39
|
+
const bChildState = bRoute.state as T | undefined;
|
|
40
|
+
|
|
41
|
+
// Stop here if this is the state object that changed:
|
|
42
|
+
// - history length is different
|
|
43
|
+
// - focused routes are different
|
|
44
|
+
// - one of them doesn't have child state
|
|
45
|
+
// - child state keys are different
|
|
46
|
+
if (
|
|
47
|
+
aHistoryLength !== bHistoryLength ||
|
|
48
|
+
aRoute.key !== bRoute.key ||
|
|
49
|
+
aChildState === undefined ||
|
|
50
|
+
bChildState === undefined ||
|
|
51
|
+
aChildState.key !== bChildState.key
|
|
52
|
+
) {
|
|
53
|
+
return [a, b];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return findMatchingState(aChildState, bChildState);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run async function in series as it's called.
|
|
61
|
+
*/
|
|
62
|
+
export const series = (cb: () => Promise<void>) => {
|
|
63
|
+
let queue = Promise.resolve();
|
|
64
|
+
const callback = () => {
|
|
65
|
+
queue = queue.then(cb);
|
|
66
|
+
};
|
|
67
|
+
return callback;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let linkingHandlers: Symbol[] = [];
|
|
71
|
+
|
|
72
|
+
type Options = LinkingOptions<ParamListBase> & {
|
|
73
|
+
independent?: boolean;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default function useLinking(
|
|
77
|
+
ref: React.RefObject<NavigationContainerRef<ParamListBase>>,
|
|
78
|
+
{
|
|
79
|
+
independent,
|
|
80
|
+
enabled = true,
|
|
81
|
+
config,
|
|
82
|
+
getStateFromPath = getStateFromPathDefault,
|
|
83
|
+
getPathFromState = getPathFromStateDefault,
|
|
84
|
+
getActionFromState = getActionFromStateDefault,
|
|
85
|
+
}: Options
|
|
86
|
+
) {
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
if (process.env.NODE_ENV === 'production') {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (independent) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (enabled !== false && linkingHandlers.length) {
|
|
97
|
+
console.error(
|
|
98
|
+
[
|
|
99
|
+
'Looks like you have configured linking in multiple places. This is likely an error since deep links should only be handled in one place to avoid conflicts. Make sure that:',
|
|
100
|
+
"- You don't have multiple NavigationContainers in the app each with 'linking' enabled",
|
|
101
|
+
'- Only a single instance of the root component is rendered',
|
|
102
|
+
]
|
|
103
|
+
.join('\n')
|
|
104
|
+
.trim()
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const handler = Symbol();
|
|
109
|
+
|
|
110
|
+
if (enabled !== false) {
|
|
111
|
+
linkingHandlers.push(handler);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
const index = linkingHandlers.indexOf(handler);
|
|
116
|
+
|
|
117
|
+
if (index > -1) {
|
|
118
|
+
linkingHandlers.splice(index, 1);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}, [enabled, independent]);
|
|
122
|
+
|
|
123
|
+
const [history] = React.useState(createMemoryHistory);
|
|
124
|
+
|
|
125
|
+
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
|
|
126
|
+
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
|
|
127
|
+
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
|
|
128
|
+
const enabledRef = React.useRef(enabled);
|
|
129
|
+
const configRef = React.useRef(config);
|
|
130
|
+
const getStateFromPathRef = React.useRef(getStateFromPath);
|
|
131
|
+
const getPathFromStateRef = React.useRef(getPathFromState);
|
|
132
|
+
const getActionFromStateRef = React.useRef(getActionFromState);
|
|
133
|
+
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
enabledRef.current = enabled;
|
|
136
|
+
configRef.current = config;
|
|
137
|
+
getStateFromPathRef.current = getStateFromPath;
|
|
138
|
+
getPathFromStateRef.current = getPathFromState;
|
|
139
|
+
getActionFromStateRef.current = getActionFromState;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const server = React.useContext(ServerContext);
|
|
143
|
+
|
|
144
|
+
const getInitialState = React.useCallback(() => {
|
|
145
|
+
let value: ResultState | undefined;
|
|
146
|
+
|
|
147
|
+
if (enabledRef.current) {
|
|
148
|
+
const location =
|
|
149
|
+
server?.location ??
|
|
150
|
+
(typeof window !== 'undefined' ? window.location : undefined);
|
|
151
|
+
|
|
152
|
+
const path = location ? location.pathname + location.search : undefined;
|
|
153
|
+
|
|
154
|
+
if (path) {
|
|
155
|
+
value = getStateFromPathRef.current(path, configRef.current);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const thenable = {
|
|
160
|
+
then(onfulfilled?: (state: ResultState | undefined) => void) {
|
|
161
|
+
return Promise.resolve(onfulfilled ? onfulfilled(value) : value);
|
|
162
|
+
},
|
|
163
|
+
catch() {
|
|
164
|
+
return thenable;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return thenable as PromiseLike<ResultState | undefined>;
|
|
169
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const previousIndexRef = React.useRef<number | undefined>(undefined);
|
|
173
|
+
const previousStateRef = React.useRef<NavigationState | undefined>(undefined);
|
|
174
|
+
const pendingPopStatePathRef = React.useRef<string | undefined>(undefined);
|
|
175
|
+
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
previousIndexRef.current = history.index;
|
|
178
|
+
|
|
179
|
+
return history.listen(() => {
|
|
180
|
+
const navigation = ref.current;
|
|
181
|
+
|
|
182
|
+
if (!navigation || !enabled) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const path = location.pathname + location.search;
|
|
187
|
+
const index = history.index;
|
|
188
|
+
|
|
189
|
+
const previousIndex = previousIndexRef.current ?? 0;
|
|
190
|
+
|
|
191
|
+
previousIndexRef.current = index;
|
|
192
|
+
pendingPopStatePathRef.current = path;
|
|
193
|
+
|
|
194
|
+
// When browser back/forward is clicked, we first need to check if state object for this index exists
|
|
195
|
+
// If it does we'll reset to that state object
|
|
196
|
+
// Otherwise, we'll handle it like a regular deep link
|
|
197
|
+
const record = history.get(index);
|
|
198
|
+
|
|
199
|
+
if (record?.path === path && record?.state) {
|
|
200
|
+
navigation.resetRoot(record.state);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const state = getStateFromPathRef.current(path, configRef.current);
|
|
205
|
+
|
|
206
|
+
// We should only dispatch an action when going forward
|
|
207
|
+
// Otherwise the action will likely add items to history, which would mess things up
|
|
208
|
+
if (state) {
|
|
209
|
+
// Make sure that the routes in the state exist in the root navigator
|
|
210
|
+
// Otherwise there's an error in the linking configuration
|
|
211
|
+
const rootState = navigation.getRootState();
|
|
212
|
+
|
|
213
|
+
if (state.routes.some((r) => !rootState?.routeNames.includes(r.name))) {
|
|
214
|
+
console.warn(
|
|
215
|
+
"The navigation state parsed from the URL contains routes not present in the root navigator. This usually means that the linking configuration doesn't match the navigation structure. See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration."
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (index > previousIndex) {
|
|
221
|
+
const action = getActionFromStateRef.current(
|
|
222
|
+
state,
|
|
223
|
+
configRef.current
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (action !== undefined) {
|
|
227
|
+
try {
|
|
228
|
+
navigation.dispatch(action);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// Ignore any errors from deep linking.
|
|
231
|
+
// This could happen in case of malformed links, navigation object not being initialized etc.
|
|
232
|
+
console.warn(
|
|
233
|
+
`An error occurred when trying to handle the link '${path}': ${
|
|
234
|
+
typeof e === 'object' && e != null && 'message' in e
|
|
235
|
+
? e.message
|
|
236
|
+
: e
|
|
237
|
+
}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
navigation.resetRoot(state);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
navigation.resetRoot(state);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// if current path didn't return any state, we should revert to initial state
|
|
248
|
+
navigation.resetRoot(state);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}, [enabled, history, ref]);
|
|
252
|
+
|
|
253
|
+
React.useEffect(() => {
|
|
254
|
+
if (!enabled) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const getPathForRoute = (
|
|
259
|
+
route: ReturnType<typeof findFocusedRoute>,
|
|
260
|
+
state: NavigationState
|
|
261
|
+
): string => {
|
|
262
|
+
// If the `route` object contains a `path`, use that path as long as `route.name` and `params` still match
|
|
263
|
+
// This makes sure that we preserve the original URL for wildcard routes
|
|
264
|
+
if (route?.path) {
|
|
265
|
+
const stateForPath = getStateFromPathRef.current(
|
|
266
|
+
route.path,
|
|
267
|
+
configRef.current
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (stateForPath) {
|
|
271
|
+
const focusedRoute = findFocusedRoute(stateForPath);
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
focusedRoute &&
|
|
275
|
+
focusedRoute.name === route.name &&
|
|
276
|
+
isEqual(focusedRoute.params, route.params)
|
|
277
|
+
) {
|
|
278
|
+
return route.path;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return getPathFromStateRef.current(state, configRef.current);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (ref.current) {
|
|
287
|
+
// We need to record the current metadata on the first render if they aren't set
|
|
288
|
+
// This will allow the initial state to be in the history entry
|
|
289
|
+
const state = ref.current.getRootState();
|
|
290
|
+
|
|
291
|
+
if (state) {
|
|
292
|
+
const route = findFocusedRoute(state);
|
|
293
|
+
const path = getPathForRoute(route, state);
|
|
294
|
+
|
|
295
|
+
if (previousStateRef.current === undefined) {
|
|
296
|
+
previousStateRef.current = state;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
history.replace({ path, state });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const onStateChange = async () => {
|
|
304
|
+
const navigation = ref.current;
|
|
305
|
+
|
|
306
|
+
if (!navigation || !enabled) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const previousState = previousStateRef.current;
|
|
311
|
+
const state = navigation.getRootState();
|
|
312
|
+
|
|
313
|
+
// root state may not available, for example when root navigators switch inside the container
|
|
314
|
+
if (!state) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const pendingPath = pendingPopStatePathRef.current;
|
|
319
|
+
const route = findFocusedRoute(state);
|
|
320
|
+
const path = getPathForRoute(route, state);
|
|
321
|
+
|
|
322
|
+
previousStateRef.current = state;
|
|
323
|
+
pendingPopStatePathRef.current = undefined;
|
|
324
|
+
|
|
325
|
+
// To detect the kind of state change, we need to:
|
|
326
|
+
// - Find the common focused navigation state in previous and current state
|
|
327
|
+
// - If only the route keys changed, compare history/routes.length to check if we go back/forward/replace
|
|
328
|
+
// - If no common focused navigation state found, it's a replace
|
|
329
|
+
const [previousFocusedState, focusedState] = findMatchingState(
|
|
330
|
+
previousState,
|
|
331
|
+
state
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
previousFocusedState &&
|
|
336
|
+
focusedState &&
|
|
337
|
+
// We should only handle push/pop if path changed from what was in last `popstate`
|
|
338
|
+
// Otherwise it's likely a change triggered by `popstate`
|
|
339
|
+
path !== pendingPath
|
|
340
|
+
) {
|
|
341
|
+
const historyDelta =
|
|
342
|
+
(focusedState.history
|
|
343
|
+
? focusedState.history.length
|
|
344
|
+
: focusedState.routes.length) -
|
|
345
|
+
(previousFocusedState.history
|
|
346
|
+
? previousFocusedState.history.length
|
|
347
|
+
: previousFocusedState.routes.length);
|
|
348
|
+
|
|
349
|
+
if (historyDelta > 0) {
|
|
350
|
+
// If history length is increased, we should pushState
|
|
351
|
+
// Note that path might not actually change here, for example, drawer open should pushState
|
|
352
|
+
history.push({ path, state });
|
|
353
|
+
} else if (historyDelta < 0) {
|
|
354
|
+
// If history length is decreased, i.e. entries were removed, we want to go back
|
|
355
|
+
|
|
356
|
+
const nextIndex = history.backIndex({ path });
|
|
357
|
+
const currentIndex = history.index;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
if (nextIndex !== -1 && nextIndex < currentIndex) {
|
|
361
|
+
// An existing entry for this path exists and it's less than current index, go back to that
|
|
362
|
+
await history.go(nextIndex - currentIndex);
|
|
363
|
+
} else {
|
|
364
|
+
// We couldn't find an existing entry to go back to, so we'll go back by the delta
|
|
365
|
+
// This won't be correct if multiple routes were pushed in one go before
|
|
366
|
+
// Usually this shouldn't happen and this is a fallback for that
|
|
367
|
+
await history.go(historyDelta);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Store the updated state as well as fix the path if incorrect
|
|
371
|
+
history.replace({ path, state });
|
|
372
|
+
} catch (e) {
|
|
373
|
+
// The navigation was interrupted
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
// If history length is unchanged, we want to replaceState
|
|
377
|
+
history.replace({ path, state });
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
// If no common navigation state was found, assume it's a replace
|
|
381
|
+
// This would happen if the user did a reset/conditionally changed navigators
|
|
382
|
+
history.replace({ path, state });
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// We debounce onStateChange coz we don't want multiple state changes to be handled at one time
|
|
387
|
+
// This could happen since `history.go(n)` is asynchronous
|
|
388
|
+
// If `pushState` or `replaceState` were called before `history.go(n)` completes, it'll mess stuff up
|
|
389
|
+
return ref.current?.addListener('state', series(onStateChange));
|
|
390
|
+
}, [enabled, history, ref]);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
getInitialState,
|
|
394
|
+
};
|
|
395
|
+
}
|