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