From 748e92f120b9ff73c6b1e14515f60c76701081db Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Sat, 24 Oct 2020 16:09:30 +0200 Subject: [PATCH] feat: add `getInitialURL` and `subscribe` options to linking config For apps with push notifications linking to screens inside the app, currently we need to handle them separately (e.g. [instructions for firebase](https://rnfirebase.io/messaging/notifications#handling-interaction), [instructions for expo notifications](https://docs.expo.io/push-notifications/receiving-notifications/)). But if we add a link in the notification to use for deep linking, we can instead reuse the same deep linking logic instead. This commit adds the `getInitialURL` and `subscribe` options which internally used `Linking` API to allow more advanced implementations by combining it with other sources such as push notifications. Example usage with Firebase notifications could look like this: ```js const linking = { prefixes: ['myapp://', 'https://myapp.com'], async getInitialURL() { // Check if app was opened from a deep link const url = await Linking.getInitialURL(); if (url != null) { return url; } // Check if there is an initial firebase notification const message = await messaging().getInitialNotification(); // Get the `url` property from the notification which corresponds to a screen // This property needs to be set on the notification payload when sending it return message?.notification.url; }, subscribe(listener) { const onReceiveURL = ({ url }: { url: string }) => listener(url); // Listen to incoming links from deep linking Linking.addEventListener('url', onReceiveURL); // Listen to firebase push notifications const unsubscribeNotification = messaging().onNotificationOpenedApp( (message) => { const url = message.notification.url; if (url) { // If the notification has a `url` property, use it for linking listener(url); } } ); return () => { // Clean up the event listeners Linking.removeEventListener('url', onReceiveURL); unsubscribeNotification(); }; }, config, }; ``` --- packages/native/src/types.tsx | 14 +++++++++ packages/native/src/useLinking.native.tsx | 38 ++++++++++++++--------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/native/src/types.tsx b/packages/native/src/types.tsx index 43ac1bdc..b6515bad 100644 --- a/packages/native/src/types.tsx +++ b/packages/native/src/types.tsx @@ -49,6 +49,20 @@ export type LinkingOptions = { * ``` */ config?: { initialRouteName?: string; screens: PathConfigMap }; + /** + * Custom function to get the initial URL used for linking. + * Uses `Linking.getInitialURL()` by default. + * Not supported on the web. + */ + getInitialURL?: () => Promise; + /** + * Custom function to get subscribe to URL updates. + * Uses `Linking.addEventListener('url', callback)` by default. + * Not supported on the web. + */ + subscribe?: ( + listener: (url: string) => void + ) => undefined | void | (() => void); /** * Custom function to parse the URL to a valid navigation state (advanced). * Only applicable on Web. diff --git a/packages/native/src/useLinking.native.tsx b/packages/native/src/useLinking.native.tsx index 5bc8039e..f225f04d 100644 --- a/packages/native/src/useLinking.native.tsx +++ b/packages/native/src/useLinking.native.tsx @@ -16,6 +16,22 @@ export default function useLinking( enabled = true, prefixes, config, + getInitialURL = () => + Promise.race([ + Linking.getInitialURL(), + new Promise((resolve) => + // Timeout in 150ms if `getInitialState` doesn't resolve + // Workaround for https://github.com/facebook/react-native/issues/25675 + setTimeout(resolve, 150) + ), + ]), + subscribe = (listener) => { + const callback = ({ url }: { url: string }) => listener(url); + + Linking.addEventListener('url', callback); + + return () => Linking.removeEventListener('url', callback); + }, getStateFromPath = getStateFromPathDefault, }: LinkingOptions ) { @@ -48,14 +64,16 @@ export default function useLinking( const enabledRef = React.useRef(enabled); const prefixesRef = React.useRef(prefixes); const configRef = React.useRef(config); + const getInitialURLRef = React.useRef(getInitialURL); const getStateFromPathRef = React.useRef(getStateFromPath); React.useEffect(() => { enabledRef.current = enabled; prefixesRef.current = prefixes; configRef.current = config; + getInitialURLRef.current = getInitialURL; getStateFromPathRef.current = getStateFromPath; - }, [config, enabled, getStateFromPath, prefixes]); + }, [config, enabled, prefixes, getInitialURL, getStateFromPath]); const extractPathFromURL = React.useCallback((url: string) => { for (const prefix of prefixesRef.current) { @@ -80,15 +98,7 @@ export default function useLinking( return undefined; } - const url = await (Promise.race([ - Linking.getInitialURL(), - new Promise((resolve) => - // Timeout in 150ms if `getInitialState` doesn't resolve - // Workaround for https://github.com/facebook/react-native/issues/25675 - setTimeout(resolve, 150) - ), - ]) as Promise); - + const url = await getInitialURLRef.current(); const path = url ? extractPathFromURL(url) : null; if (path) { @@ -99,7 +109,7 @@ export default function useLinking( }, [extractPathFromURL]); React.useEffect(() => { - const listener = ({ url }: { url: string }) => { + const listener = (url: string) => { if (!enabled) { return; } @@ -122,10 +132,8 @@ export default function useLinking( } }; - Linking.addEventListener('url', listener); - - return () => Linking.removeEventListener('url', listener); - }, [enabled, extractPathFromURL, ref]); + return subscribe(listener); + }, [enabled, ref, subscribe, extractPathFromURL]); return { getInitialState,