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

[iOS] PushNotification with DeepLinks doesn't work when the app is close

Open Javs-21 opened this issue 11 months ago • 8 comments

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

  1. close the iOS app
  2. send a push notification with deep links
  3. 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

Javs-21 avatar Dec 17 '24 00:12 Javs-21

[!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.

react-native-bot avatar Dec 17 '24 00:12 react-native-bot

[!WARNING] Missing reproducer: We could not detect a reproducible example in your issue report. Please provide either:

react-native-bot avatar Dec 17 '24 00:12 react-native-bot

[!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.

react-native-bot avatar Dec 17 '24 00:12 react-native-bot

@Javs-21 It would be great if you could provide a sample reproducer. You can use this template.

A few things to clarify:

  1. Is this issue specific to iOS only?
  2. Can you try this on the new architecture (NewArch)?
  3. 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?

sarthak-d11 avatar Dec 17 '24 06:12 sarthak-d11

@shubhamguptadream11 thanks for you replayed for security purposes i can't share or create information about the project (is for a big company).

  1. yes only happens on iOS, Android works perfectly.
  2. yes, test in the new arch.
  3. with simulator iphone 16 pro and this command xcrun simctl openurl booted myapp://SCREEN works 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)

Javs-21 avatar Dec 17 '24 15:12 Javs-21

I can confirm that we also experience the same issues in our app, in the same scenarios.

vvslepkan avatar May 16 '25 08:05 vvslepkan

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

Gregoirevda avatar May 27 '25 08:05 Gregoirevda

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)

Gregoirevda avatar May 27 '25 11:05 Gregoirevda

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

Washhh avatar Jun 23 '25 18:06 Washhh

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])`

pinomartin avatar Aug 06 '25 19:08 pinomartin