21

Detect if the app was launched/opened from a push notification describes how to detect whether a native iOS app was opened (that is, either launched or merely made active) via the user tapping a push notification.

How can I do the same thing in React Native? PushNotificationIOS lets me attach a notification listener...

PushNotificationIOS.addEventListener('notification', function (notification) {
    console.log('got a notification', notification);
});

but this fires both when a push notification is received with the application in the foreground, and when I open the app via a push notification.

How can I detect the second case in particular?

Community
  • 1
  • 1
Mark Amery
  • 143,130
  • 81
  • 406
  • 459

3 Answers3

45

There are two cases here that need to be detected in different ways:

  1. The app has been completely terminated (e.g. by restarting the phone, or by double-tapping home and swiping it off the list of apps running in the background) and is being launched by a user's tap on a push notification. This can be detected (and the notification's data acquired) via the React.PushNotificationIOS.getInitialNotification method.
  2. The app had been suspended and is being made active again by a user's tap on a push notification. Just like in a native app, you can tell that this is happening because iOS passes the tapped notification to your app when it is opening (even if it's an old notification) and causes your notification handler to fire while your app is in UIApplicationStateInactive state (or 'background' state, as React Native's AppStateIOS class calls it).

Code to handle both cases (you can put this in your index.ios.js or somewhere else that's run on app launch):

import React from 'react';
import { PushNotificationIOS, AppState } from 'react-native';

function appOpenedByNotificationTap(notification) {
  // This is your handler. The tapped notification gets passed in here.
  // Do whatever you like with it.
  console.log(notification);
}

PushNotificationIOS.getInitialNotification().then(function (notification) {
  if (notification != null) {
    appOpenedByNotificationTap(notification);
  }
});

let backgroundNotification;

PushNotificationIOS.addEventListener('notification', function (notification) {
  if (AppState.currentState === 'background') {
    backgroundNotification = notification;
  }
});

AppState.addEventListener('change', function (new_state) {
  if (new_state === 'active' && backgroundNotification != null) {
    appOpenedByNotificationTap(backgroundNotification);
    backgroundNotification = null;
  }
});
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • 2
    As an addition you might want to store references to your handlers in the state, and call `removeEventListener` in `componentWillUnmount` (if you do this inside a component) – Darius Mar 19 '16 at 15:37
  • @mark-amery In which method should this go to? componentWillMount? – Nimila Hiranya May 12 '16 at 07:03
  • @NimilaHiranya it needn't necessarily go in any method at all; you can put it, for example, in your `index.ios.js` file. But a lifecycle function like `componentWillMount` of your root view might also be a reasonable place. – Mark Amery Jul 04 '16 at 20:05
  • 1
    `PushNotificationIOS.getInitialNotification` returns a promise, not a notification object. The documentation states that it will return null or the notification object. (react-native v0.26.3) – Tom Jul 27 '16 at 19:53
  • @Tom thanks for the correction! I haven't had access to an iOS device or even a Mac with which to test this since my original revision of this answer in December. After `popInitialNotification` was deprecated, I updated this answer based upon the docs without re-testing. I'll put a disclaimer on this answer for now, then check the code to confirm what you've said, update the answer as appropriate, and attempt to fix the official docs through whatever the mechanism for doing that is. – Mark Amery Jul 27 '16 at 20:15
  • 1
    @Tom answer fixed, [Pull Request](https://github.com/facebook/react-native/pull/9052) opened. If possible, I'd appreciate it if you could test that the code currently in my answer works, since I am unable to do so. – Mark Amery Jul 27 '16 at 20:59
  • @MarkAmery Thanks! Unfortunately, I've been trying to make this work for the past 5 hours with no luck -- the notification value passed to the `.then` is _always_ `null`. I've ensured that nowhere else in my app is reading notifications, and still can't get a real value out of here. I may need to rely on racing the notification event against the appstate change event to determine whether the last-received notification was used to open the app. UGH. #raceconditionFTW – Tom Jul 27 '16 at 21:11
  • @Tom just to confirm - you've [followed the instructions on modifying your AppDelegate.m from the docs](https://facebook.github.io/react-native/docs/getting-started.html) and you are indeed completely terminating the app (by double-tapping home and swiping the app off the list of open apps) before reopening it? Assuming both of those things are true, that sounds like a React Native bug and I'm afraid I can't be of much help. All I can suggest is opening issues and waiting for help, or trying to figure out ways to hack around the problem in Objective-C. :( – Mark Amery Jul 27 '16 at 21:17
  • @MarkAmery Doh! I see now that I've been wrongly attempting to test scenario 1 by reproducing scenario 2 -- trying to detect "app opened by APNS" by causing "app foregrounded by APNS". I also see that your code for scenario 2 is exactly the kind of race I was contemplating. So thanks for that! Incidentally, not sure how to get the Xcode debugger to attach to a session that isn't _initiated_ by Xcode (i.e. when launching from death via APNS). Tips? – Tom Jul 27 '16 at 21:25
  • 2
    @Tom afraid not - my best suggestion, sadly entirely serious, is to debug with [alerts](https://facebook.github.io/react-native/docs/alertios.html). I think that's what I did. – Mark Amery Jul 27 '16 at 21:30
  • 3
    @MarkAmery - i dont think this works in a certain situation. LMK if I'm wrong. ` 1) Background the app. 2) Receive a notification. 3) Open the app *normally* without tapping on the notification. ` Expected Result: normal open, nothing special Actual Result: appOpenedByNotificationTap is called – Krishan Gupta Nov 20 '16 at 22:21
  • @KrishanGupta my belief/recollection is that `appOpenedByNotificationTap` is not called in the situation you've described when using my code above, but I'm afraid I'm not in a position to test - I originally wrote and tested the code in my answer whilst at work and I don't personally own either a Mac or an iPhone. Have you tested yourself and observed the behaviour you describe, or are you speculating? – Mark Amery Nov 20 '16 at 23:21
  • @MarkAmery I did test and got the behavior I described. I'm new to react native though, so I could have easily been doing something wrong. I ended up moving all the logic to the server side, and just sending the exact badge count I want with each PN. Thanks! – Krishan Gupta Dec 04 '16 at 18:36
  • Thank you so much for this! It's pretty awful this is the way to do it but it's the only way I've found to get this to work I guess that's #native-development – John Culviner Apr 24 '17 at 16:13
  • I'm experiencing the scenario described by @KrishanGupta. anyone managed to get around that? (1) Background the app. 2) Receive a notification. 3) Open the app normally without tapping on the notification. ` Expected Result: normal open, nothing special Actual Result: appOpenedByNotificationTap is called) – Matan Guttman Dec 04 '18 at 08:08
  • this thing doesn't work anymore as of iOS 13 Xcode 11 – Nicolas Manzini Feb 21 '20 at 08:41
  • `PushNotificationIos` has serious conflicts with Firebase notifications listeners. Isn't there another way to do this distinguish without `PushNotificationIos` ? – Kasra Aug 12 '20 at 09:57
9

For Local Notifications

getInitialNotification does not work with Local Notifications.

Unfortunately, as of 0.28 of React Native, using PushNotificationIOS.getInitialNotification() always returns a null value when being launched by a Local Push Notification.

Because of that, you need to catch the push notification as a launchOption in your AppDelegate.m and pass it into React Native as an appProperty.

Here's all that you need to receive a Local Push Notification from a cold launch or from the background/inactive state.

AppDelegate.m (Native iOS Code)

// Inside of your didFinishLaunchingWithOptions method...

// Create a Mutable Dictionary to hold the appProperties to pass to React Native.
NSMutableDictionary *appProperties = [NSMutableDictionary dictionary];

if (launchOptions != nil) {
  // Get Local Notification used to launch application.
  UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];

  if (notification) {
    // Instead of passing the entire Notification, we'll pass the userInfo,
    // where a Record ID could be stored, for example.
    NSDictionary *notificationUserInfo = [notification userInfo];

    [ appProperties setObject:notificationUserInfo  forKey:@"initialNotificationUserInfo" ];
  }
}

// Your RCTRootView stuff...

rootView.appProperties = appProperties;

index.ios.js (React Native)

componentDidMount() {
  if (this.props.initialNotificationUserInfo) {
    console.log("Launched from Notification from Cold State");
    // This is where you could get a Record ID from this.props.initialNotificationUserInfo
    // and redirect to the appropriate page, for example.
  }

  PushNotificationIOS.addEventListener('localNotification', this._onLocalNotification);
}

componentWillUnmount() {
  PushNotificationIOS.removeEventListener('localNotification', this._onLocalNotification);
}

_onLocalNotification( notification ) {
  if (AppState.currentState != 'active') {
    console.log("Launched from Notification from Background or Inactive state.");
  }
  else {
    console.log("Not Launched from Notification");
  }
}

Make sure to import PushNotificationIOS and AppState from react-native.

I haven't tested this with Remote Push Notifications. Perhaps @MarkAmery's method works just fine with Remote Push Notifications but, unfortunately, as of current state of React Native, this is the only way I was able to get Local Push Notifications from a cold state working.

This is highly undocumented in React Native so I have created an issue on their GitHub repo to draw attention to it and hopefully rectify it. If you're dealing with this, go there and give it a thumbs up so it percolates to the top.

https://github.com/facebook/react-native/issues/8580

Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
  • 1
    Unless something has changed in an update since I posted my answer (I'm afraid I'm unable to check), this is wrong. The notification event handlers will trigger if the app is *suspended* after they're added and then *reactivated* by a tap on a notification, but will not fire if the application is *terminated* and is then *launched* by a tap on a notification - that case needs to be detected by different means. I already covered this in [my answer](http://stackoverflow.com/a/34343226/1709587). – Mark Amery Jul 04 '16 at 19:59
  • @MarkAmery Oh, good catch! You're absolutely correct. Just running some tests now... I'll let you know what I find. – Joshua Pinter Jul 04 '16 at 22:08
  • 1
    @MarkAmery After about a day of debugging I was able to properly handle local push notifications from cold starts. I tried your method but at least for Local Push Notifications, it was not working. I'm opening an Issue with React Native now as well. Thanks for your help. – Joshua Pinter Jul 05 '16 at 17:18
  • FYI, React Native GitHub Issue: https://github.com/facebook/react-native/issues/8580 – Joshua Pinter Jul 05 '16 at 17:29
  • @JoshPinter I'm trying to get my react native app to respond to push notifications from cold starts. I have it working perfectly when tapping on a push notifications opens the app while it's _suspended_ using the `PushNotificationIOS.addEventListener` but I can't figure out a way to get that initial notification from the "cold start" like you're saying. I've tried to use `PushNotificationIOS.getInitialNotification()` once the app starts up when opened from push notification, but all it returns is an empty object: `'launchNotification', { _45: 0, _81: 0, _65: null, _54: null }` – dwilt Jul 22 '16 at 13:46
  • @dwilt I responded to you on the Github issue as well but it's worth responding on here as well. Have you tried the modifications to the `AppDelegate.m`? That's the key part for cold starts. – Joshua Pinter Jul 22 '16 at 15:43
  • Yep, let's chat there: https://github.com/facebook/react-native/issues/8580 – dwilt Jul 25 '16 at 15:08
0

The solution I found for this is a bit hacky and relies on some potentially weird behavior I noticed regarding the order that event handlers fire. The app I have been working on needed the same functionality, to detect specifically when I open the app via a Push Notification.

What I found was that the handler for PushNotificationIOS fires before the handler for AppStateIOS What this meant was that, if I persisted the foreground/background state to memory/AsyncStorage, I could just check it like so in the PushNotification event handler: if (activity === 'background') and if that was true then I knew that I had just opened the app from a Push Notification.

I'm always keeping the AppStateIOS foreground/background activity in memory and on disk (using redux and redux-persist respectively) so I just check that whenever I need to know.

This may potentially be too hacky for your purposes, and that behavior might change in the future or it could be localized to just me. Try looking at that solution and see what you think.

Ryan McDermott
  • 6,127
  • 6
  • 22
  • 23
  • Thanks, this pointed me in the right direction. I think this is as good as it gets, and it's at worst no more hacky than [the equivalent solution for native iOS apps](http://stackoverflow.com/a/16393957/1709587) which does exactly the same thing. However, a shortcoming that you may not be aware of is that this doesn't handle the case where the app has been completely terminated and is actually being *launched* (not merely made active) by the tap on a push notification; `popInitialNotification` is needed for that. I'm gonna self-answer with some code that covers both cases. – Mark Amery Dec 17 '15 at 19:46
  • @MarkAmery Thanks for pointing out the latter case. That's really a big shortcoming of this hack. I'll have to rely on users opening the app from background for now, which I definitely don't like. I'm very excited to see your solution, I'll have to adapt mine to what you come up with! – Ryan McDermott Dec 17 '15 at 20:04