Skip to content

Commit

Permalink
feat: add getInitialURL and subscribe options to linking config
Browse files Browse the repository at this point in the history
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,
};
```
  • Loading branch information
satya164 committed Oct 24, 2020
1 parent 7f3b27a commit 748e92f
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 15 deletions.
14 changes: 14 additions & 0 deletions packages/native/src/types.tsx
Expand Up @@ -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<string | null | undefined>;
/**
* 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.
Expand Down
38 changes: 23 additions & 15 deletions packages/native/src/useLinking.native.tsx
Expand Up @@ -16,6 +16,22 @@ export default function useLinking(
enabled = true,
prefixes,
config,
getInitialURL = () =>
Promise.race([
Linking.getInitialURL(),
new Promise<undefined>((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
) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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<string | null | undefined>);

const url = await getInitialURLRef.current();
const path = url ? extractPathFromURL(url) : null;

if (path) {
Expand All @@ -99,7 +109,7 @@ export default function useLinking(
}, [extractPathFromURL]);

React.useEffect(() => {
const listener = ({ url }: { url: string }) => {
const listener = (url: string) => {
if (!enabled) {
return;
}
Expand All @@ -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,
Expand Down

0 comments on commit 748e92f

Please sign in to comment.