Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deep linking is not working when app is closed/killed #32350

Open
Md-Mudassir47 opened this issue Oct 7, 2021 · 33 comments
Open

Deep linking is not working when app is closed/killed #32350

Md-Mudassir47 opened this issue Oct 7, 2021 · 33 comments
Labels
API: Linking Needs: Repro This issue could be improved with a clear list of steps to reproduce the issue. Needs: Triage 🔍

Comments

@Md-Mudassir47
Copy link

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;
@jinghongchan
Copy link

I'm facing the same issue. Any solutions?

@kale1d
Copy link

kale1d commented Nov 12, 2021

same here. Working with react-native 0.65.1 version.

@harshiths97
Copy link

harshiths97 commented Nov 12, 2021

same here

@Md-Mudassir47
Copy link
Author

Md-Mudassir47 commented Nov 16, 2021

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.

@saurabh874
Copy link

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

@blueprin4
Copy link

blueprin4 commented Jan 26, 2022

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

@saurabh874
Copy link

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

@nixolas1
Copy link

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!

@vitorbetani
Copy link

vitorbetani commented Mar 8, 2022

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

@nixolas1
Copy link

nixolas1 commented Mar 14, 2022

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

@SalischevArtem
Copy link

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?

@Fernando555
Copy link

Fernando555 commented Mar 28, 2022

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 :(

@vitorbetani
Copy link

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.

@mmkhmk
Copy link

mmkhmk commented Apr 18, 2022

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.

@bhargavSrao92
Copy link

bhargavSrao92 commented Apr 19, 2022

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.

@keech
Copy link

keech commented Apr 22, 2022

related?
#28727

@megacherry
Copy link

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?

@vijay14887
Copy link

vijay14887 commented May 30, 2022

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')"

@felippewick
Copy link

felippewick commented Jul 21, 2022

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

@rafaelmaeuer
Copy link

rafaelmaeuer commented Jul 28, 2022

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

@Kaung-Htet-Hein
Copy link

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

this code worked for me

@koutantos
Copy link

koutantos commented Sep 13, 2022

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

@felippewick
Copy link

felippewick commented Nov 28, 2022

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

@idrissakhi
Copy link

idrissakhi commented Jan 17, 2023

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];

@farhoudshapouran
Copy link

farhoudshapouran commented Jan 25, 2023

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>

@pkyipab
Copy link

pkyipab commented Feb 16, 2023

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!

@swadexpress
Copy link

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

this code worked for me

@vemarav
Copy link

vemarav commented Jul 24, 2023

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 ?? '';
},
...

@github-actions
Copy link

⚠️ Missing Reproducible Example
ℹ️ 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 github-actions bot added the Needs: Repro This issue could be improved with a clear list of steps to reproduce the issue. label Jul 24, 2023
@sanketappsimity
Copy link

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

@yogendrajs
Copy link

it's been a long time since this issue was opened, is anyone working on the fix? @react-native-bot

@ghillahill
Copy link

Any news about it?

@cgtorniado
Copy link

Any updates on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API: Linking Needs: Repro This issue could be improved with a clear list of steps to reproduce the issue. Needs: Triage 🔍
Projects
None yet
Development

No branches or pull requests