react-native
react-native copied to clipboard
[iOS] PushNotification with DeepLinks doesn't work when the app is close
Description
when my iOS app receive push notification and when the app is close only opens in the home screen but never goes to the screen with the deeplink route, i always get url = null
//this way is to detect universal deep link
Linking.getInitialURL()
.then((url) => {
console.log('DEEPLINK', url);
if (url) {
Linking.canOpenURL(url).then((supported) =>
supported ? handleDeepLink(url) : null,
);
}
})
.catch((err) => {
console.warn('An error occurred deepLink', err);
});
but the curious thing is when the app is open or in the background it works perfectly, I have implemented documentation's setup with universal links
I am currently working with react native 0.73.11 and i updated to 0.74.6 and 0.75.1 because in your CHANGELOG mentions changes with Push Notification on iOS but the behavior is the same NO FIX
also looking on internet I haven't found any solution
Steps to reproduce
- close the iOS app
- send a push notification with deep links
- the app should open in the correct screen but only open at home screen
React Native Version
0.73.11
Affected Platforms
Runtime - iOS, Other (please specify)
Output of npx react-native info
System:
OS: macOS 15.1.1
CPU: (8) arm64 Apple M2
Memory: 132.95 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 20.17.0
path: /usr/local/bin/node
Yarn:
version: 1.22.22
path: /usr/local/bin/yarn
npm:
version: 10.8.2
path: /usr/local/bin/npm
Watchman:
version: 2024.11.04.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 24.1
- iOS 18.1
- macOS 15.1
- tvOS 18.1
- visionOS 2.1
- watchOS 11.1
Android SDK:
API Levels:
- "31"
- "34"
- "35"
Build Tools:
- 30.0.3
- 31.0.0
- 33.0.1
- 34.0.0
System Images:
- android-29 | Google Play ARM 64 v8a
- android-32 | Google APIs ARM 64 v8a
- android-32 | Google Play ARM 64 v8a
- android-TiramisuPrivacySandbox | Google Play ARM 64 v8a
- android-VanillaIceCream | Google Play ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2024.1 AI-241.18034.62.2411.12071903
Xcode:
version: 16.1/16B40
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.10
path: /usr/bin/javac
Ruby:
version: 3.3.6
path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.3.1
wanted: 18.3.1
react-native:
installed: 0.75.1
wanted: 0.75.1
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: false
Stacktrace or Logs
not apply
Reproducer
none
Screenshots and Videos
none
[!WARNING] Unsupported version: It looks like your issue or the example you provided uses an unsupported version of React Native.
Due to the number of issues we receive, we're currently only accepting new issues against one of the supported versions. Please upgrade to latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If you cannot upgrade, please open your issue on StackOverflow to get further community support.
[!WARNING] Missing reproducer: 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/upgrade related: a project using our Reproducer Template
- Otherwise send us a Pull Request with the RNTesterPlayground.js edited to reproduce your bug.
[!WARNING] Unsupported version: It looks like your issue or the example you provided uses an unsupported version of React Native.
Due to the number of issues we receive, we're currently only accepting new issues against one of the supported versions. Please upgrade to latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If you cannot upgrade, please open your issue on StackOverflow to get further community support.
@Javs-21 It would be great if you could provide a sample reproducer. You can use this template.
A few things to clarify:
- Is this issue specific to iOS only?
- Can you try this on the new architecture (NewArch)?
- What happens if you use the deeplink without a push notification (i.e., paste the deeplink in a messaging app and then open it)? Does it work or not?
@shubhamguptadream11 thanks for you replayed for security purposes i can't share or create information about the project (is for a big company).
- yes only happens on iOS, Android works perfectly.
- yes, test in the new arch.
- with simulator iphone 16 pro and this command
xcrun simctl openurl booted myapp://SCREENworks also with urls, but when it receives a push notification with a deeplink never works.
you can find more cases like mine one [Deep linking - doesn't work if app is closed](https://stackoverflow.com/questions/67789779/deep-linking-doesnt-work-if-app-is-closed) Deep linking is not working when app is closed/killed Universal links callback doesn't work if launching closed app [iOS Universal Links are not opening in-app](https://stackoverflow.com/questions/32751225/ios-universal-links-are-not-opening-in-app/76254119)
I can confirm that we also experience the same issues in our app, in the same scenarios.
someone found a solution to this? continueUserActivity is triggered in AppDelegate, calling LinkingManager continueUserActivity which holds the correct url, but it looks like react-navigation gets the call, but routes aren't ready yet. Only happens on iOS when the app is closed and a push notification is opened
Found a solution:
AppDelegate.swift
func application(
_: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Regular setup...
var initialProps: [String: Any] = [:]
// Get the deeplink url from the push notification (this is vendor specific)
if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any],
let appExtra = remoteNotification["app_extra"] as? [String: Any],
let deeplink = appExtra["deeplink"] as? String {
initialProps["iOSPushNotificationDeepLink"] = deeplink
}
factory.startReactNative(
withModuleName: "Nzz",
in: window,
initialProperties: initialProps, // ! pass as initial prop
launchOptions: launchOptions
)
JS
function App({iOSPushNotificationDeepLink}) {
return <NavigationContainer linking={linking} initialState={isIos && iOSPushNotificationDeepLink
? linking?.getStateFromPath?.(iOSPushNotificationDeepLink, linking?.config)
: undefined}>
}
This works well to solve push notifications not opening the deeplink when the app is closed on iOS. Regular deeplinks are working when the app is closed because launchOptions keeps state of the deeplink and it's retrieved correctly with getInitialUrl.
With the RN 0.72 -> 0.79 upgrade, JS will only start executing after application continueUserActivity and LinkingManager continueUserActivity have already triggered (I think they trigger a Linking.addEventListener('url'), but JS hasn't even started executing)
Hello, I had the same problem, I tried the solution from @Gregoirevda , but some library that I use was overwriting the project's initalProps, I was frustrated at not being able to solve this problem the way that Gregoirevda indicated, but I found a way via JS to solve it. In the code below I used Push notification properties from the react-native-push-notification library to capture the data from the notification that forced the application to open and that has an associated deeplink. This way I saved the data in a local variable and used it at a later time, managing to execute the navigation the way I wanted through push notification. In my case, the ab_uri variable is provided by the tool I'm using to create the pushes.
if(Platform.OS === 'ios') {
PushNotification.configure({
onNotification: notification => {
const { ab_uri } = notification?.data || {}
const setLinkToStorage = async () => {
await AsyncStorage.setItem(
'IosDeepLink', ab_uri
)
}
setLinkToStorage()
notification.finish(PushNotificationIOS.FetchResult.NoData)
},
})
}
I can confirm that we also experience the same issues in our app, in the same scenarios. Using Expo SDK 53 & React Native 0.79.5, in 52 version was working as expected. We are using customerIO React Native SDK to manage push notifications and React Navigation to manage deep links.
EDIT: I resolved it using the @Gregoirevda solution, thanks man by the way , I own you a beer! Im using another approach and expo with expo prebuild so I had to create a plugin to insert the code in the AppDelegate.swift =>
const { withAppDelegate } = require("expo/config-plugins")
const CUSTOM_CODE_SNIPPET = `
var initialProps: [String: Any] = [:]
// Get the deeplink url from the push notification (this is vendor specific)
if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] {
// Handle app_extra deeplink (generic push notifications)
if let appExtra = remoteNotification["app_extra"] as? [String: Any],
let deeplink = appExtra["deeplink"] as? String {
initialProps["iOSPushNotificationDeepLink"] = deeplink
}
// Handle CustomerIO push notifications
if let cio = remoteNotification["CIO"] as? [String: Any],
let push = cio["push"] as? [String: Any],
let link = push["link"] as? String {
initialProps["iOSPushNotificationDeepLink"] = link
}
}
`
const withCustomAppDelegate = config => {
return withAppDelegate(config, config => {
let contents = config.modResults.contents
contents = contents.replace(
`let factory = ExpoReactNativeFactory(delegate: delegate)`,
`let factory = ExpoReactNativeFactory(delegate: delegate)\n${CUSTOM_CODE_SNIPPET}`,
)
contents = contents.replace(
`launchOptions: launchOptions)`,
`initialProperties: initialProps,\n launchOptions: launchOptions)`,
)
// eslint-disable-next-line no-param-reassign
config.modResults.contents = contents
return config
})
}
module.exports = withCustomAppDelegate
And accessing the initialProps object using a Wrapper in the entry point of the app.
import React from "react"
import { registerRootComponent } from "expo"
import { Text } from "react-native"
import App from "./src/app/App"
Text.defaultProps = Text.defaultProps || {}
Text.defaultProps.maxFontSizeMultiplier = 1.2
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
// Create a wrapper component that receives initialProps and passes them to App
// This is needed for the iOS native side to pass initialProps to the app
const AppWrapper = props => {
return React.createElement(App, props)
}
registerRootComponent(AppWrapper)
Usage in App.tsx =>
`type AppProps = {
iOSPushNotificationDeepLink?: string
}
//Get initial props from the iOS native side for push notification deep link issue
const App = (props: AppProps) => {
const navigationRef = useNavigationContainerRef<RootStackParamList>()
const routeNameRef = useRef<string | undefined>(undefined)
const setDeeplinkUrl = useSetAtom(deeplinkUrlAtom)
const handleUrl = useCallback(
(url: string | null) => {
if (url && url.includes("expo")) return //ignore expo links (created on the expo server bundle)
if (url && !url.includes(CLERK_SSO_URL)) {
setDeeplinkUrl(url)
}
},
[setDeeplinkUrl],
)
useEffect(() => {
// Handle iOS push notification deep link from props or initialProps
const deepLink = props?.iOSPushNotificationDeepLink
if (isIOS && deepLink) {
handleUrl(deepLink)
}
}, [props?.iOSPushNotificationDeepLink, handleUrl])`