react-native
react-native copied to clipboard
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;
I'm facing the same issue. Any solutions?
same here. Working with react-native 0.65.1 version.
same here
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.
is there any solution? i am also facing same issue.
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 is I am facing issue in android only. do u have any idea how to fix for android?
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 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;
}
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
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?
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 :(
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.
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.
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.
related? #28727
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?
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')"
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 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>
)
}
const linking = { prefixes: ['app://'], config: { // your config // }, async getInitialURL() { return Linking.getInitialURL() }, }
this code worked for me
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)
}
}
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;
};
}, []);
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];
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>
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.
- 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;
- 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);
}
}
- 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!
const linking = { prefixes: ['app://'], config: { // your config // }, async getInitialURL() { return Linking.getInitialURL() }, }this code worked for me
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 ?? '';
},
...
| :warning: | Missing Reproducible Example |
|---|---|
| :information_source: | We could not detect a reproducible example in your issue report. Please provide either:
|
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