react-native: Deep linking is not working when app is closed/killed

Description

If app in background

  • specific screen will open as expected. (through deeplink)

If app is not in background or closed

  • it will show first screen only. (through deeplink)

I tried some of the work arounds mentioned on stackoverflow but they dont seems to work

References: Deep linking - doesn’t work if app is closed , React Native - Deep linking is not working when app is not in background (Android, iOS), Deep linking not working when app is in background state React native

React Native version:

System: OS: macOS 10.15.7 Binaries: Node: 16.8.0 - /usr/local/bin/node npm: 7.22.0 - /usr/local/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman Managers: CocoaPods: 1.10.1 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: iOS 14.4, DriverKit 20.2, macOS 11.1, tvOS 14.3, watchOS 7.2 npmPackages: @react-native-community/cli: Not Found react: 17.0.2 => 17.0.2 react-native: ^0.64.0 => 0.64.2

Expected Results

It should open the specific screen through deep link when the app is in closed/killed state also.

Snack, code example :

linking.js

 const config = {
   screens: {
      Home:'home',
      Profile:'profile,
     },
  };
    
 const linking = {
  prefixes: ['demo://app'],
  config,
};
     
 export default linking;

App.js

import React, {useState, useEffect} from 'react';
import {Linking} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {createStackNavigator} from '@react-navigation/stack';
import {NavigationContainer} from '@react-navigation/native';
import linking from './utils/linking';
import {Home, Profile, SplashScreen} from './components';

const Stack = createStackNavigator();

const App = () => {

function _handleOpenUrl(event) {
  console.log('handleOpenUrl', event.url);
}

  // this handles the case where a deep link launches the application
  Linking.getInitialURL()
    .then((url) => {
      if (url) {
        console.log('launch url', url);
        _handleOpenUrl({url});
      }
    })
    .catch((err) => console.error('launch url error', err));

  useEffect(() => {
    Linking.addEventListener('url', _handleOpenUrl);
    return () => {
      Linking.removeEventListener('url', _handleOpenUrl);
    };
  }, []);

  return (
    <NavigationContainer linking={linking}>
      <Stack.Navigator
        initialRouteName="SplashScreen"
        screenOptions={{...TransitionPresets.SlideFromRightIOS}}>
        <Stack.Screen
          name="SplashScreen"
          component={SplashScreen}
          options={{headerShown: false}}
        />
        <Stack.Screen
          name="Home"
          component={Home}
          options={{headerShown: false, gestureEnabled: false}}
        />
        <Stack.Screen
          name="Profile"
          component={Profile}
          options={{headerShown: false, gestureEnabled: false}}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default App;

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 13
  • Comments: 31

Most upvoted comments

I’m facing the same issue. Any solutions?

@nixolas1 I’m facing exactly the same issue, putting a setTimeout inside the getInitialUrl make it works , but is not a cool solution, does anyone knows how to solve this in better way?

async getInitialURL() {
    const url = await Linking.getInitialURL();
    if (url) {
        setTimeout(() => {
            handleDeepLinkingUrl(url);
        }, 5000);
    }

    return url;
}

EDIT After a while I’ve created a Ref in my navigation object:

<NavigationContainer linking={linking} onReady={() => { isReadyRef.current = true }}>

The problem is that the navigation routes aren’t ready when the app is closed, so I’ve put a setInterval in the linking object, to check when the Navigation Container is ready to decide when to redirect:

async getInitialURL() {
        const url = await Linking.getInitialURL();
        if (url) {
            let interval: NodeJS.Timeout | null = null;

            interval = setInterval(() => {
                if (isReadyRef.current) {
                    handleDeepLinkingUrl(url);

                    if (interval) {
                        clearInterval(interval);
                    }
                }
            }, 100);
        }

        return url;
    }

I had the same problem and for me, the following is working on iOS and Android:

import * as Linking from 'expo-linking'

// prefix for deep-links
const prefix = Linking.createURL('/')

// config for deep-links
const config = {
	screens: {
		...
		},
	},
}

// variable for url
let deepLink

// linking config for navigator
const linking = {
	prefixes: [prefix],
	config,
	async getInitialURL() {
		// Check if app was opened from a deep link
		deepLink = await Linking.getInitialURL()
		// Don't handle it now - wait until Navigation is ready
		console.log('DeepLink URL:', deepLink)
	},
}

// open url if deepLink is defined
const openDeepLink = async () => {
	if (deepLink) Linking.openURL(deepLink)
}

// check when react navigation is ready
const onNavigationReady = async () => {
	// if deep link exists, open when navigation is ready
	await openDeepLink()
}

function AppNavigator() {
	return (
		<NavigationContainer onReady={onNavigationReady} linking={linking}>
			<Stack.Navigator>
				<Stack.Group>
					...
				</Stack.Group>
			</Stack.Navigator>
		</NavigationContainer>
	)
}

same here. Working with react-native 0.65.1 version.

@blueprin4 is I am facing issue in android only. do u have any idea how to fix for android?

is there any solution? i am also facing same issue.

Nice, seems like a possible solution @vitorbetani. Could I ask where you call your getInitialUrl function? I run mine in a useEffect inside a child component, and Linking.getInitialUrl just returns nothing. EDIT: Nevermind, I’m guessing you use the ReactNavigation example

Based on https://documentation.onesignal.com/v7.0/docs/react-native-sdk#handlers Deep linking in iOS from an app closed state You must be Modify the application:didFinishLaunchingWithOptions in your AppDelegate.m file to use the following:

NSMutableDictionary *newLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions];
    if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) {
        NSDictionary *remoteNotif = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
        if (remoteNotif[@"custom"] && remoteNotif[@"custom"][@"u"]) {
            NSString *initialURL = remoteNotif[@"custom"][@"u"];
            if (!launchOptions[UIApplicationLaunchOptionsURLKey]) {
                newLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL];
            }
        }
    }

RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:newLaunchOptions];

also in reactnavigation: https://reactnavigation.org/docs/deep-linking/

const linking = {
    prefixes: ["https://example.com", "example://"],
    config,
    async getInitialURL() {
      const url = await Linking.getInitialURL();
      if (url != null) {
        return url;
      }
    },
  };

<NavigationContainer linking={linking}>
   ...
</NavigationContainer>

const linking = { prefixes: ['app://'], config: { // your config // }, async getInitialURL() { return Linking.getInitialURL() }, }

this code worked for me

I tried @vitorbetani’s approach and it didn’t work for me on ios. Fyi, I’m currently using a less pretty approach with settimeout instead but it’s working in my case

useEffect(() => {
    notificationListener.current =
      Notifications.addNotificationReceivedListener((notification) => {});

    responseListener.current =
      Notifications.addNotificationResponseReceivedListener((response) => {
        let url = response.notification.request.content.data.url;

        setTimeout(() => {
          Linking.openURL(url);
        }, 100);
      });

    return () => {
      Notifications.removeNotificationSubscription(
        notificationListener.current
      );
      Notifications.removeNotificationSubscription(responseListener.current);
    };
  }, []);

I was having same problem with I fixed it by adding “content available” on iOS . Here is link to SO I posted https://stackoverflow.com/a/70869289/4724718 The complete Onesignal postman code is:

{
  "app_id": "1234",
  "included_segments": ["Test"],
  "content_available" : true,
  "contents": {
                "en": "Hi"
            },
            "data": {
                "dynamic_link": "https://google.com"
            },
            "headings": {
                "en": "Testing"
            }
}