react-native icon indicating copy to clipboard operation
react-native copied to clipboard

Deep linking is not working when app is closed/killed

Open Md-Mudassir47 opened this issue 4 years ago • 32 comments
trafficstars

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;

Md-Mudassir47 avatar Oct 07 '21 14:10 Md-Mudassir47

I'm facing the same issue. Any solutions?

jinghongchan avatar Nov 02 '21 09:11 jinghongchan

same here. Working with react-native 0.65.1 version.

kale1d avatar Nov 12 '21 17:11 kale1d

same here

harshiths97 avatar Nov 12 '21 17:11 harshiths97

I found a work around to handle the situation, & assuming that your app has Auth flow as well, if not here's a solution to support Auth flow for deep-linking.

What i had noticed was when the app was in background the user's app state was in sync and deep link was working as expected and was showing the correct info but when I close/kill the app and open it from a deep-link then the user's data isn't found and it was failing because user's state is checked during app launch from splash screen but during an app launch through deep link it doesn't open from splash screen it directly opens the deep linked screen.

So to resolve it and show the correct screen with proper info i'm adding a check on each launch when its from deep link.

Profile.js

import React from 'react'
import {CommonActions} from '@react-navigation/native';
import DeepLinkLoader from '../../widgets/deeplinkloader; //A loader component to show only when deep link is opened

const Profile = () => {
 const [deepLoading, setDeepLoading] = React.useState(false);
 let isDeepLinked = React.useRef(false);

  //Handling back functionality of deep link
 // If this is not handled then on press of back the app exits completely to handle that i'm setting it to route to home screen.
  const onDeviceBackPress = () => {
    if (isDeepLinked.current) {
      navigation.dispatch(
        CommonActions.reset({
          index: 0,
          routes: [{name: 'HomeScreen'}],
        }),
      );
      return true;
    }
  };

  React.useEffect(() => {
    if (user.id === undefined) {
      setDeepLoading(true);
      isDeepLinked.current = true;
     //Storage has the `userId` which i'm fetching & making an api call to fetch the 
     //user details and set it to state and then load the screen
      Storage.instance.getData('userId').then((id) => {
        API.instance.get(`/users/${id}/all`).then((res) => {
          // Adding the user to the state & setting the deep link to false to show the screen.
          dispatch(addUser(res));
          setDeepLoading(false);
        });
      });
    }
    const backHandler = BackHandler.addEventListener(
      'hardwareBackPress',
      onDeviceBackPress,
    );

    return () => backHandler.remove();
  }, []);

// If the screen is opened from a deep link then the below loader will show 
// & once the user info is set then it will show the actual screen
if (deepLoading) return <DeepLinkLoader />;

  return (
       <View>
          //Profile info
        </View>
  )
}

export default Profile

When a closed app is launched from deep link it checks if user's data is found or not, 
if not found then it makes an api call  and sets it to the state through redux & show the screen, 
incase the `userId` is also not found  then it will fallback to the login screen.

Md-Mudassir47 avatar Nov 16 '21 08:11 Md-Mudassir47

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

saurabh874 avatar Jan 01 '22 08:01 saurabh874

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"
            }
}

blueprin4 avatar Jan 26 '22 19:01 blueprin4

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

saurabh874 avatar Jan 27 '22 05:01 saurabh874

Any solutions for opening normal deep URLs on a killed iOS app? I'm not trying to fix push-notification deep links, so can't use the content_availablefix.

Seems like it is getting sent in my Appdelegate, all the way to RCTDeviceEventEmitter. Not sure where the url falls off, but I'm guessing inside react-native iOS native code. Works on android!

nixolas1 avatar Feb 11 '22 15:02 nixolas1

@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;
    }

vitorbetani avatar Mar 08 '22 13:03 vitorbetani

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

nixolas1 avatar Mar 14 '22 13:03 nixolas1

I met the same issue (android and ios). I made a check for permissions and auth context for every open link - it works correctly. Any chance to solve it?

SalischevArtem avatar Mar 15 '22 14:03 SalischevArtem

hello @vitorbetani what is inside the handleDeepLinkingUrl? I would like to use your code but i cant because it said handleDeepLinkingUrl is not defined please help :(

Fernando555 avatar Mar 28 '22 12:03 Fernando555

hello @vitorbetani what is inside the handleDeepLinkingUrl? I would like to use your code but i cant because it said handleDeepLinkingUrl is not defined please help :(

Hey Fernando, the handleDeepLinkingUrl is a function of your own, you can create and define what to do with the income URL. For example, you can have a switch/case to send the user to a specific screen of your app.

vitorbetani avatar Mar 31 '22 11:03 vitorbetani

I'm facing the same issue in iOS only. I handle url for deep link after getting it by calling Linking.getInitialURL() in onReady handler for NavigationContainer. It works in Android, but not in iOS.

mmkhmk avatar Apr 18 '22 11:04 mmkhmk

I'm facing the issue in iOS. when clicking on the Deeplink URL, If the app is not in the background or closed it lands on the initial page itself. I am unable to get the value of the clicked URL in my "ViewController". I am using SceneDelegate "openURLContexts" and "continue userActivity" delegate methods.

bhargavSrao92 avatar Apr 19 '22 09:04 bhargavSrao92

related? #28727

keech avatar Apr 22 '22 04:04 keech

I had the same problem, but only on ios. I've read the documentation of react native, react navigation, github issues, stack overflow questions over and over again.

Nothing worked.

But after all of that i implemented this: await Linking.canOpenURL(initialUrl) it works on ios with all type of deep linking variations (custom schema, https and dynamic link).

Can somebody confirm this behaviour?

megacherry avatar Apr 24 '22 14:04 megacherry

Hello @vitorbetani , Sorry to ask you again! My only doubt is what navigationref we need to use to navigate to a particular screen? Can you give me an example? Please help!!! I tried using - isReadyRef.navigate("screennameABC") but getting error as "Cannot read properties of undefined (reading 'apply')"

vijay14887 avatar May 30 '22 07:05 vijay14887

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);
    };
  }, []);

felippewick avatar Jul 21 '22 08:07 felippewick

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>
	)
}

rafaelmaeuer avatar Jul 28 '22 10:07 rafaelmaeuer

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

this code worked for me

Kaung-Htet-Hein avatar Aug 16 '22 09:08 Kaung-Htet-Hein

One solution is to wait a little before load url

        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: 
UIScene.ConnectionOptions) {

...Your code

// Load the link, but set a timeout of X seconds to fix app crashing when loading deep link while app is NOT already running in the background.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
    self.handleUniversalLink(url: url)
}
   }

koutantos avatar Sep 13 '22 08:09 koutantos

Here a little write-up how I was able to solve the issue in my application. Perhaps it is helpful.

Delaying the opening of the deep link with setTimeout or similar is not a reliable solution as proposed by me earlier.

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

If the deep link is referring to a stack navigator (here: AppNavigator with isAuth = false) that is not yet rendered, the linking does not work.

return  isAuth ? <AppNavigator /> : <AuthNavigator />

So I'm waiting until the AppNavigator is rendered, and then open the deep link which I stored in a useRef:

/*** navigation/index.jsx ***/

  const [appNavigatorReady, setAppNavigatorReady] = useState(false);

useEffect(() => {
    responseListener.current =
      Notifications.addNotificationResponseReceivedListener((response) => {
        let url =
          response.notification?.request?.content?.data?.url ??
          Constants.manifest.scheme + '://';

deepLinkRef.current = url;
      
// Open link when notification is received and AppNavigator is rendered - app is in foreground OR background
if (appNavigatorReady) {
          Linking.openURL(url);
        }

// Open link after waiting until AppNavigator is rendered - app was killed
if (appNavigatorReady && deepLinkRef.current) {
      Linking.openURL(deepLinkRef.current);
    }

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

return  isAuth ? <AppNavigator onLayout={setAppNavigatorReady} /> : <AuthNavigator />

/*** AppNavigator.jsx ***/

useEffect(() => {

let ignore = false;
if (!ignore) {
      onLayout(true);
    }
    return () => {
      ignore = true;
    };
}, []);

felippewick avatar Nov 28 '22 09:11 felippewick

here what i have done in my side i modified didFinishLaunchWithOption and intercepted the payload of notification and get the deeplink url then created new launch options and start app with these options and it works like a charm no need for intervals neither timeouts


  if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) {
     NSDictionary *remoteNotif = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
     // here goes specific selection according to the payload structure we receive from cleverTap
     // example here: { wzrk_dl : 'partoo://reviews' }
     // see https://docs.clevertap.com/docs/faq#q-what-is-the-format-of-the-payload-of-the-push-notification-for-ios
     if (remoteNotif[@"wzrk_dl"]) {
         NSString *initialURL = remoteNotif[@"wzrk_dl"];
         if (!launchOptions[UIApplicationLaunchOptionsURLKey]) {
             newLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL];
         }
     }
  }
  
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:newLaunchOptions];

idrissakhi avatar Jan 17 '23 15:01 idrissakhi

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>

farhoudshapouran avatar Jan 25 '23 06:01 farhoudshapouran

For me, my goal is to redirect the user to the specific pages when the notification is clicked. The below code works on iOS and Android devices.

  1. Deep linking : can redirect user to specific page when App is in Foreground, but not killed / quitted. If your notification not isn't used for deep linking. You can place onNotificationOpenedApp in index.js.
import * as Linking from "expo-linking";
import messaging from "@react-native-firebase/messaging";

const prefix = Linking.createURL("/");

const config = {
  screens: {
    ConnectFitness: {
      path: "fitness/:id",
      parse: {
        id: (id) => `${id}`,
      },
    },
    EventDetail: {
      path: "eventDetails/:id",
      parse: {
        id: (id) => `${id}`,
      },
    },
  },
};

const linking = {
  prefixes: [prefix],
  config: config,
  async getInitialURL() {},
  subscribe(listener) {
    const onReceiveURL = ({ url }) => listener(url);

    Linking.addEventListener("url", onReceiveURL);

    const unsubscribeNotification = messaging().onNotificationOpenedApp((message) => {
      const url = message?.data?.link;

      // for deep linking
      if (url) {
        listener(url);
      }
    });

    return () => {
      Linking.removeEventListener("url", onReceiveURL);
      unsubscribeNotification();
    };
  },
};

export default linking;

  1. For handling the killed state notification, I just placed this is the Splash Screen.
async function getNotification() {
  try {
    const initialNotification = await messaging().getInitialNotification();

    if (initialNotification) {
      // this is only for deep linking
      Linking.openURL(initialNotification?.data?.link);
    }
  } catch (error) {
    console.log(error);
  }
}
  1. This is the body of my notification:
    let message = {
      tokens: ["TOKEN1", "TOKEN2"],
      data: {
        notifee: JSON.stringify({
          title: title,
          body: body,
          android: {
            channelId: "sound",
            smallIcon: "ic_launcher",
            pressAction: {
              id: "default",
              launchActivity: "default",
            },
          },
        }),
        link: "myApp://eventDetails/500",
      },
      notification: {
        title: title,
        body: body,
      },
      apns: {
        payload: {
          aps: {
            // Important, to receive `onMessage` event in the foreground when message is incoming
            contentAvailable: 1,
            // Important, without this the extension won't fire
            mutableContent: 1,
          },
          headers: {
            "apns-push-type": "background",
            "apns-priority": "5",
            "apns-topic": "org.test.myApp", // your app bundle identifier
          },
        },
      },
      android: {
        priority: "high",
      },
    };

    const result = await notification.push(message);

Thanks guys!

pkyipab avatar Feb 16 '23 19:02 pkyipab

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

this code worked for me

dulquerkawsarkhan avatar Mar 01 '23 15:03 dulquerkawsarkhan

Implement getInitialURL inside linking properly.

...,
config: deepLinkConfigs,
async getInitialURL() {
    const url = decodeURI((await Linking.getInitialURL()) ?? '');

    if (url) return url;

    // Check if there is an initial firebase notification
    const message = await messaging().getInitialNotification();

    // Get deep link from data
    // if this is undefined, the app will open the default/home page

    return message?.data?.link ?? '';
},
...

vemarav avatar Jul 24 '23 12:07 vemarav

:warning: Missing Reproducible Example
:information_source: We could not detect a reproducible example in your issue report. Please provide either:
  • If your bug is UI related: a Snack
  • If your bug is build/update related: use our Reproducer Template. A reproducer needs to be in a GitHub repository under your username.

github-actions[bot] avatar Jul 24 '23 12:07 github-actions[bot]

I was facing same issue i used some other function and it started working fine as expected here is my code

dynamicLinks().onLink((item) => { this.eeplinkingMethod(item); });

as i am assuming it was happening due to listener

hope this will help

Thanks

sanketappsimity avatar Jan 09 '24 15:01 sanketappsimity