1// Forked from react-navigation with a custom `extractPathFromURL` that automatically
2// allows any prefix and parses Expo Go URLs.
3// For simplicity the following are disabled: enabled, prefixes, independent
4// https://github.com/react-navigation/react-navigation/blob/main/packages/native/src/useLinking.native.tsx
5import {
6  getActionFromState as getActionFromStateDefault,
7  getStateFromPath as getStateFromPathDefault,
8  NavigationContainerRef,
9  ParamListBase,
10} from '@react-navigation/core';
11import type { LinkingOptions } from '@react-navigation/native';
12import * as React from 'react';
13import { Linking, Platform } from 'react-native';
14
15import { extractExpoPathFromURL } from './extractPathFromURL';
16
17type ResultState = ReturnType<typeof getStateFromPathDefault>;
18
19type Options = LinkingOptions<ParamListBase>;
20
21const linkingHandlers: symbol[] = [];
22
23export default function useLinking(
24  ref: React.RefObject<NavigationContainerRef<ParamListBase>>,
25  {
26    // enabled = true,
27    // prefixes,
28    filter,
29    config,
30    getInitialURL = () =>
31      Promise.race([
32        Linking.getInitialURL(),
33        new Promise<undefined>((resolve) =>
34          // Timeout in 150ms if `getInitialState` doesn't resolve
35          // Workaround for https://github.com/facebook/react-native/issues/25675
36          setTimeout(resolve, 150)
37        ),
38      ]),
39    subscribe = (listener) => {
40      const callback = ({ url }: { url: string }) => listener(url);
41
42      const subscription = Linking.addEventListener('url', callback) as
43        | { remove(): void }
44        | undefined;
45
46      return () => {
47        subscription?.remove();
48      };
49    },
50    getStateFromPath = getStateFromPathDefault,
51    getActionFromState = getActionFromStateDefault,
52  }: Options
53) {
54  //   const independent = useNavigationIndependentTree();
55
56  React.useEffect(
57    () => {
58      if (process.env.NODE_ENV === 'production') {
59        return undefined;
60      }
61
62      // if (independent) {
63      //   return undefined;
64      // }
65
66      if (
67        // enabled !== false &&
68        linkingHandlers.length
69      ) {
70        console.error(
71          [
72            '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:',
73            "- You don't have multiple NavigationContainers in the app each with 'linking' enabled",
74            '- Only a single instance of the root component is rendered',
75            Platform.OS === 'android'
76              ? "- You have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple instances"
77              : '',
78          ]
79            .join('\n')
80            .trim()
81        );
82      }
83
84      const handler = Symbol();
85
86      // if (enabled !== false) {
87      linkingHandlers.push(handler);
88      // }
89
90      return () => {
91        const index = linkingHandlers.indexOf(handler);
92
93        if (index > -1) {
94          linkingHandlers.splice(index, 1);
95        }
96      };
97    },
98    [
99      // enabled,
100      // independent
101    ]
102  );
103
104  // We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
105  // This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
106  // Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
107  //   const enabledRef = React.useRef(enabled);
108  //   const prefixesRef = React.useRef(prefixes);
109  const filterRef = React.useRef(filter);
110  const configRef = React.useRef(config);
111  const getInitialURLRef = React.useRef(getInitialURL);
112  const getStateFromPathRef = React.useRef(getStateFromPath);
113  const getActionFromStateRef = React.useRef(getActionFromState);
114
115  React.useEffect(() => {
116    // enabledRef.current = enabled;
117    // prefixesRef.current = prefixes;
118    filterRef.current = filter;
119    configRef.current = config;
120    getInitialURLRef.current = getInitialURL;
121    getStateFromPathRef.current = getStateFromPath;
122    getActionFromStateRef.current = getActionFromState;
123  });
124
125  const getStateFromURL = React.useCallback((url: string | null | undefined) => {
126    if (!url || (filterRef.current && !filterRef.current(url))) {
127      return undefined;
128    }
129
130    // NOTE(EvanBacon): This is the important part.
131    const path = extractExpoPathFromURL(url);
132
133    return path !== undefined ? getStateFromPathRef.current(path, configRef.current) : undefined;
134  }, []);
135
136  const getInitialState = React.useCallback(() => {
137    // let state: ResultState | undefined;
138    // if (enabledRef.current) {
139    const url = getInitialURLRef.current();
140
141    if (url != null && typeof url !== 'string') {
142      return url.then((url) => {
143        const state = getStateFromURL(url);
144
145        return state;
146      });
147    }
148
149    const state = getStateFromURL(url);
150    // }
151
152    const thenable = {
153      then(onfulfilled?: (state: ResultState | undefined) => void) {
154        onfulfilled?.(state);
155        return thenable;
156      },
157      catch() {
158        return thenable;
159      },
160    };
161
162    return thenable as PromiseLike<ResultState | undefined>;
163  }, [getStateFromURL]);
164
165  React.useEffect(() => {
166    const listener = (url: string) => {
167      //   if (!enabled) {
168      //     return;
169      //   }
170
171      const navigation = ref.current;
172      const state = navigation ? getStateFromURL(url) : undefined;
173
174      if (navigation && state) {
175        // Make sure that the routes in the state exist in the root navigator
176        // Otherwise there's an error in the linking configuration
177        const rootState = navigation.getRootState();
178
179        if (state.routes.some((r) => !rootState?.routeNames.includes(r.name))) {
180          console.warn(
181            "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."
182          );
183          return;
184        }
185
186        const action = getActionFromStateRef.current(state, configRef.current);
187
188        if (action !== undefined) {
189          try {
190            navigation.dispatch(action);
191          } catch (e) {
192            // Ignore any errors from deep linking.
193            // This could happen in case of malformed links, navigation object not being initialized etc.
194            console.warn(
195              `An error occurred when trying to handle the link '${url}': ${
196                typeof e === 'object' && e != null && 'message' in e ? e.message : e
197              }`
198            );
199          }
200        } else {
201          navigation.resetRoot(state);
202        }
203      }
204    };
205
206    return subscribe(listener);
207  }, [
208    // enabled,
209    getStateFromURL,
210    ref,
211    subscribe,
212  ]);
213
214  return {
215    getInitialState,
216  };
217}
218