React-native-onesignal: How to get initial notification?

Created on 2 Oct 2017  ·  22Comments  ·  Source: OneSignal/react-native-onesignal

The React Native PushNotificationIOS module has a getInitialNotification() method that allows fetching the notification that was tapped on to open the app (if there was one). Is there some similar functionality for this library?

When I sent a test notification to my device while the app was closed I received the Device info: log in my console when it opened, but I didn't see any of the other event logs for receiving a notification.

Edit 3:
For some reason I'm no longer able to see the onOpened event for initial notifications, though nothing changed in my code.

Edit 2:
By moving around the listeners I was able to see the log for the onOpened event, but that being a passive listener means that I can't actively check to see on launch if my app was opened with a notification or not, I just have to wait for some arbitrary period of time to see if that event is fired? Is there no way to actively check whether the app was opened with a push notification or not?

Edit:
I found an article that references using OneSignal.configure() and onNotificationOpened, but looking at the source code it doesn't appear that OneSignal.configure() accepts parameters anymore?

Documentation

Most helpful comment

I have the same problem, and it seems like many others are also having this problem. Some related/duplicate issues I've found include https://github.com/geektimecoil/react-native-onesignal/issues/206, https://github.com/geektimecoil/react-native-onesignal/issues/195, https://github.com/geektimecoil/react-native-onesignal/issues/191, https://github.com/geektimecoil/react-native-onesignal/issues/336, https://github.com/geektimecoil/react-native-onesignal/issues/334, https://github.com/geektimecoil/react-native-onesignal/issues/264, https://github.com/geektimecoil/react-native-onesignal/issues/279.

These issues all seem to be about this problem of the opened event listener not being called when you tap a push notification to open the app and the app is not running at the time. From what I've read in those issues and the testing I've done, it seems like the cause of this issue is the registration of the event listener being delayed in any way (i.e. OneSignal.addEventListener('opened', this.onOpened); is called slightly after the app itself is loaded). This could be because you have this code within a React component (e.g. in the constructor or componentWillMount or componentDidMount functions), or because it's not called until after you configure/rehydrate your Redux store, etc.. And due to this delay, the event itself (that a notification was opened) is fired by react-native-onesignal before the event listener is registered, and at this point you've missed the event and have no way to know that it happened. As a side note, this same issue affects other event listeners, e.g. the ids event listener.

If you move this code so that it happens as early as possible, it appears that the event listener will be registered before the event is fired and as such your event handler function will be called. For example, you should move the event listener registration code to your index.ios.js file, or at the beginning of your App.js file in the case where your index.ios.js file is simply just importing the App component and registering it (via AppRegistry.registerComponent).

Once you've got this set up, you're now able to capture the event that a notification was opened and its up to you how you handle it, which will depend on how your app is set up (e.g. whether you're using Redux, or whether you're using redux-persist, etc.).

My current solution is to save the notification to a variable defined at the top level of my App.js file itself (before I even declare my App component), and then once I've completed initialisation of my Redux store (which includes waiting for rehydration of my store by redux-persist), I dispatch actions with the notification as a payload (if there was a notification opened), and then I also remove my top level event listeners and add new event listeners on my App component itself (these event listeners also just simply dispatch actions to my Redux store with the notification, device IDs etc. as the action payload).

This solution is obviously very hacky, and there are probably some edge cases where this would not work as it's supposed to. But until a proper solution is implemented in react-native-onesignal, this seems to be a decent workaround.

I think what needs to happen is for react-native-onesignal to be updated so that it waits until you've set up your event handlers before it fires off the events. One possible solution could be that when events occur (such as the opened event), the events are put into a queue, and react-native-onesignal then checks if there is a corresponding event listener currently registered. If there isn't, it leaves the event in the queue. Then, as soon as the event listener is registered, the events which would be received by this event listener are fired and removed from the queue. That way, you could add the event listeners in the app at any point (e.g. after waiting for your Redux store to be configured and rehydrated, which could potentially be 1-2 seconds after the app itself has loaded), and then as soon as the event listeners are registered, your event handler functions would be called and receive the events that occurred (such as the event that a notification was opened).

Another possible solution, at least for specifically detecting that the app was opened because the user tapped on a push notification, would be to do something similar to getInitialNotification() from PushNotificationIOS like you suggested, where you can call that function at any time and it will return the push notification that the user tapped to open the app (or null if this did not occur). But even if this was implemented in react-native-onesignal, it wouldn't do anything to resolve this problem which affects the other event listeners (e.g. the ids event listener).

So I think there really needs to be a solution that addresses this core problem of events being fired before any corresponding event listener is registered, leading to the event being missed by the app.

All 22 comments

I have the same problem, and it seems like many others are also having this problem. Some related/duplicate issues I've found include https://github.com/geektimecoil/react-native-onesignal/issues/206, https://github.com/geektimecoil/react-native-onesignal/issues/195, https://github.com/geektimecoil/react-native-onesignal/issues/191, https://github.com/geektimecoil/react-native-onesignal/issues/336, https://github.com/geektimecoil/react-native-onesignal/issues/334, https://github.com/geektimecoil/react-native-onesignal/issues/264, https://github.com/geektimecoil/react-native-onesignal/issues/279.

These issues all seem to be about this problem of the opened event listener not being called when you tap a push notification to open the app and the app is not running at the time. From what I've read in those issues and the testing I've done, it seems like the cause of this issue is the registration of the event listener being delayed in any way (i.e. OneSignal.addEventListener('opened', this.onOpened); is called slightly after the app itself is loaded). This could be because you have this code within a React component (e.g. in the constructor or componentWillMount or componentDidMount functions), or because it's not called until after you configure/rehydrate your Redux store, etc.. And due to this delay, the event itself (that a notification was opened) is fired by react-native-onesignal before the event listener is registered, and at this point you've missed the event and have no way to know that it happened. As a side note, this same issue affects other event listeners, e.g. the ids event listener.

If you move this code so that it happens as early as possible, it appears that the event listener will be registered before the event is fired and as such your event handler function will be called. For example, you should move the event listener registration code to your index.ios.js file, or at the beginning of your App.js file in the case where your index.ios.js file is simply just importing the App component and registering it (via AppRegistry.registerComponent).

Once you've got this set up, you're now able to capture the event that a notification was opened and its up to you how you handle it, which will depend on how your app is set up (e.g. whether you're using Redux, or whether you're using redux-persist, etc.).

My current solution is to save the notification to a variable defined at the top level of my App.js file itself (before I even declare my App component), and then once I've completed initialisation of my Redux store (which includes waiting for rehydration of my store by redux-persist), I dispatch actions with the notification as a payload (if there was a notification opened), and then I also remove my top level event listeners and add new event listeners on my App component itself (these event listeners also just simply dispatch actions to my Redux store with the notification, device IDs etc. as the action payload).

This solution is obviously very hacky, and there are probably some edge cases where this would not work as it's supposed to. But until a proper solution is implemented in react-native-onesignal, this seems to be a decent workaround.

I think what needs to happen is for react-native-onesignal to be updated so that it waits until you've set up your event handlers before it fires off the events. One possible solution could be that when events occur (such as the opened event), the events are put into a queue, and react-native-onesignal then checks if there is a corresponding event listener currently registered. If there isn't, it leaves the event in the queue. Then, as soon as the event listener is registered, the events which would be received by this event listener are fired and removed from the queue. That way, you could add the event listeners in the app at any point (e.g. after waiting for your Redux store to be configured and rehydrated, which could potentially be 1-2 seconds after the app itself has loaded), and then as soon as the event listeners are registered, your event handler functions would be called and receive the events that occurred (such as the event that a notification was opened).

Another possible solution, at least for specifically detecting that the app was opened because the user tapped on a push notification, would be to do something similar to getInitialNotification() from PushNotificationIOS like you suggested, where you can call that function at any time and it will return the push notification that the user tapped to open the app (or null if this did not occur). But even if this was implemented in react-native-onesignal, it wouldn't do anything to resolve this problem which affects the other event listeners (e.g. the ids event listener).

So I think there really needs to be a solution that addresses this core problem of events being fired before any corresponding event listener is registered, leading to the event being missed by the app.

Hi @jordanmkoncz , can you share the code of the file to which you dispatched the actions? I have the same react-native setup as you (redux-persist etc..)

@ccoeder sure, here's my current App.js file.

import React, { Component } from 'react';
import { Provider } from 'react-redux';
import OneSignal from 'react-native-onesignal';
import isNil from 'lodash/isNil';
import configureStore from '../redux/configureStore';
import {
  pushNotificationIdsReceived,
  pushNotificationOpened,
  pushNotificationReceived,
  pushNotificationRegistered,
} from '../redux/actionCreators';
import AppNavigator from './AppNavigator';
import Blank from './Blank';

let openedPushNotificationResult = null;
let receivedPushNotification = null;
let receivedNotificationUserInfo = null;
let receivedIds = null;

const onOpened = openResult => {
  openedPushNotificationResult = openResult;
};

const onReceived = notification => {
  receivedPushNotification = notification;
};

const onRegistered = notificationUserInfo => {
  receivedNotificationUserInfo = notificationUserInfo;
};

const onIds = ids => {
  receivedIds = ids;
};

OneSignal.addEventListener('opened', onOpened);
OneSignal.addEventListener('received', onReceived);
OneSignal.addEventListener('registered', onRegistered);
OneSignal.addEventListener('ids', onIds);

class App extends Component {
  constructor() {
    super();

    this.state = {
      isStoreInitialised: false,
    };

    this.store = null;

    this.onOpened = this.onOpened.bind(this);
    this.onReceived = this.onReceived.bind(this);
    this.onRegistered = this.onRegistered.bind(this);
    this.onIds = this.onIds.bind(this);
  }

  componentDidMount() {
    this.store = configureStore(store => {
      if (!isNil(openedPushNotificationResult)) {
        store.dispatch(pushNotificationOpened(openedPushNotificationResult));
      }

      if (!isNil(receivedPushNotification)) {
        store.dispatch(pushNotificationReceived(receivedPushNotification));
      }

      if (!isNil(receivedNotificationUserInfo)) {
        store.dispatch(pushNotificationRegistered(receivedNotificationUserInfo));
      }

      if (!isNil(receivedIds)) {
        store.dispatch(pushNotificationIdsReceived(receivedIds));
      }

      OneSignal.addEventListener('opened', this.onOpened);
      OneSignal.addEventListener('received', this.onReceived);
      OneSignal.addEventListener('registered', this.onRegistered);
      OneSignal.addEventListener('ids', this.onIds);

      OneSignal.removeEventListener('opened', onOpened);
      OneSignal.removeEventListener('received', onReceived);
      OneSignal.removeEventListener('registered', onRegistered);
      OneSignal.removeEventListener('ids', onIds);

      this.setState({ isStoreInitialised: true });
    });
  }

  componentWillUnmount() {
    OneSignal.removeEventListener('opened', this.onOpened);
    OneSignal.removeEventListener('received', this.onReceived);
    OneSignal.removeEventListener('registered', this.onRegistered);
    OneSignal.removeEventListener('ids', this.onIds);
    OneSignal.removeEventListener('opened', onOpened);
    OneSignal.removeEventListener('received', onReceived);
    OneSignal.removeEventListener('registered', onRegistered);
    OneSignal.removeEventListener('ids', onIds);
  }

  onOpened(openResult) {
    this.store.dispatch(pushNotificationOpened(openResult));
  }

  onReceived(notification) {
    this.store.dispatch(pushNotificationReceived(notification));
  }

  onRegistered(notificationUserInfo) {
    this.store.dispatch(pushNotificationRegistered(notificationUserInfo));
  }

  onIds(ids) {
    this.store.dispatch(pushNotificationIdsReceived(ids));
  }

  render() {
    if (!this.state.isStoreInitialised) {
      return <Blank />;
    }

    return (
      <Provider store={this.store}>
        <AppNavigator />
      </Provider>
    );
  }
}

export default App;

Note: configureStore is a function that creates my Redux store (i.e. it calls createStore()), then sets up redux-persist (i.e. it calls persistStore()). configureStore takes a callback function as a parameter, and in the onComplete callback of the persistStore function I call the callback function passed to configureStore and pass the store object as a parameter to this callback. Finally, configureStore returns the store object.

So basically, I wait until my store has been set up and rehydrated, and then I dispatch actions based on events received by my initial event listeners, then add the new event listeners and remove the initial event listeners.

@jordanmkoncz Thank you for sharing your code. At first I thought I had the same error as you, but I realized that my problem is related to the silent notification. I need to take action if the notification comes while the application is closed. I guess your codes do not work for me.

No worries @ccoeder. Yeah the issue of actually taking action on a notification while the app is closed is a different one. This issue is specifically relating to detecting in the app that the user tapped a notification to open the app, at the time that the app is launched (when it was previously not running at all).

Take a look how react-native-fcm does, there is an method called getInitialNotification

@jordanmkoncz Thanks for your posting.

I had the similar issue. when App received the push notification, screen navigates to the desired screen.
but when open the notification, it's not working for navigation push function.
example:
componentWillMount() {
OneSignal.addEventListener('received', this._onReceived);
OneSignal.addEventListener('opened', this._onOpened);
}
componentWillUnmount() {
OneSignal.removeEventListener('received', this.__onReceived);
OneSignal.removeEventListener('opened', this._onOpened);
}
_onRecevied(notification) {
console.log('Notification received: ', notification); // working well
this.props.navigator.push({ pushSreen, passProps : { params } }); // working well
}
_onOpened(openResult) {
console.log('Notification body: ', openResult.notification.payload.body); // working well
this.props.navigator.push({ pushSreen, passProps : { params } }); // not working
}

What do you think about this? If you can help me, hope to please reply here. Thanks

Hmm.. I have myPushNotificationHelper component as a child of my Redux Provider, which is rendered only after the store has been rehydrated.

componentWillMount() {
    AppState.addEventListener("change", this.handleAppStateChange);
    OneSignal.configure({
      onNotificationOpened: this.handleOpenNotification
    });
    OneSignal.addEventListener("received", this.onReceived);
    OneSignal.addEventListener("opened", this.onOpened);
    OneSignal.addEventListener("registered", this.onRegistered);
    OneSignal.addEventListener("ids", this.onIds);

    if (!IOS) {
      OneSignal.inFocusDisplaying(0);
    }
  }

  componentWillUnmount() {
    AppState.removeEventListener("change", this.handleAppStateChange);
    OneSignal.removeEventListener("received", this.onReceived);
    OneSignal.removeEventListener("opened", this.onOpened);
    OneSignal.removeEventListener("registered", this.onRegistered);
    OneSignal.removeEventListener("ids", this.onIds);
  }

handleOpenNotification = (message, data, isActive) => {
    console.log("Notification", message, data, isActive);

    if (isActive) {
      // touchable banner displaying info from push notification
    } else {
      // act on data received from push notification
    }
  };

onReceived = notification => {
    console.log("Notification received: ", notification);
  };

onOpened = openResult => {
    console.log("Message: ", openResult.notification.payload.body);
    console.log("Data: ", openResult.notification.payload.additionalData);
    console.log("isActive: ", openResult.notification.isAppInFocus);
    console.log("openResult: ", openResult);
  };

Even if my app if closed -- not in a background AppState, but completely closed -- if I tap on a push notification, the onOpened method is called with the openResult object and associated data, and I'm free to dispatch redux actions appropriately.

@wkoutre that's interesting, could you post what versions of react-native, react-native-onesignal etc. you're currently using? Have you tested this on both iOS and Android? Have you done extensive testing of opening the app by tapping a push notification, with the app being completely closed at the time the push notification was tapped, and found that your onOpened method is called 100% of the time?

Could you do some testing to see if it still works 100% of the time if you artificially delay the loading of your PushNotificationHelper even further, e.g. by having a setTimeout with a delay of something like 5 seconds, after which you change something in state to allow PushNotificationHelper to be mounted and register its event listeners with OneSignal?

@jordanmkoncz

"react-native": "0.50.4"
"react-native-onesignal": "^3.0.7"

I've tested on both iOS and Android, yes. On both, 100% of the time:

  • app is completely closed
  • push notification is received
  • push notification is tapped
  • app loads
  • onOpened is called with data from the push notification, and openResult.notification.isAppInFocus === false

I'll do the setTimeout and get back to you.


EDIT1: I changed absolutely nothing, and now it fails on Android 100% of the time.


EDIT2: Doing the setTimeout delay caused onOpened not to fire on app load from tapping a push notification. However, I moved everything into the lifecycle methods of App.js -- my root component being registered in index.ios.js and index.android.js -- and onOpened is called 100% of the time iOS, and ~50% of the time on Android.

Not sure why Android is inconsistent, but it's obviously an issue. On Android, when the app is completely closed and a notification comes in, the debugger clears and everything in my App.js loads up until I create the class, itself.

const IOSX = HEIGHT === 812 && PLATFORM === "ios";

const wrapperStyle = [Styles.flex1];
if (IOSX) wrapperStyle.push(Styles.backgroundDarkGray);

let SPLASH_TIME;

console.log(`Change to TRUE below to reset store state`);

const PURGE = DEV ? false : false;

if (DEV) SPLASH_TIME = 4000;
else SPLASH_TIME = 4000;

// const composeEnhancers = composeWithDevTools({
//   realtime: true,
//   port: 8000
// });

const composeEnhancers =
  typeof window === "object" &&
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
  DEV
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
        // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
      })
    : compose;

const middlewares = [ReduxThunk];

// debugging tooling
// if (DEV) {
//   // add logger
// middlewares.push(logger)

//   // middlewares = [...middlewares, logger];
//   middlewares = [...middlewares];

// replace these transient reducers when they've been created

export const store = createStore(
  Reducers,
  {},
  composeEnhancers(
    applyAppStateListener(),
    applyMiddleware(...middlewares),
    autoRehydrate()
  )
);

// if (module.hot) {
//   // Enable hot module replacement for reducers
//   module.hot.accept(() => {
//     const nextRootReducer = require("./reducers/index").default;
//     store.replaceReducer(nextRootReducer);
//   });
// }

console.log(`right before APP`);

// Debugger logs everything before this line.

export default class App extends Component {
    ...
}

@jordanmkoncz Curious if you've tried this library: https://github.com/wix/react-native-notifications

Alright I have a workaround that works 100% of the time on both iOS and Android.

In my App.js but before I declare my App component, I have:

const handleOnOpened = openResult => {
  store.dispatch(setInitialNotification(openResult));
};

OneSignal.configure({});
OneSignal.addEventListener("opened", handleOnOpened);

... where setInitialNotification is an action creator.

Then, when my (now mounted) App finishes rehydrating, I call:

this.setState({ rehydrated: true }, () => {
          OneSignal.removeEventListener("opened", handleOnOpened);
        });

My App -- wrapped with a redux Provider -- is then returned in the render method, looking something like...

<Provider store={store}>
        <IphoneXAwareView style={wrapperStyle}>
          <StatusBar hidden={false} barStyle="light-content" />
          <MyAlert />
          <Loading />
          <ConnectedAppWithNavigationState />
          <PushNotificationHelper />
        </IphoneXAwareView>
      </Provider>

From there, the PushNotificationHelper component handles listening for and handling all push notifications.

@bhoop It's been a couple months, so not sure if you're still looking for a fix but... if so, give this a try.

@wkoutre Could you set up a PR with updates to the Readme file that includes this workaround for redux?

@avishayil Sure thing. I’ll get to it by this evening

@avishayil PR made.

I'm facing a problem that sounds like this. When app is opened, it's running ok all callbacks, when the app is in background as well. But, if I receive a push, my app icon increment +1, and the system show it's banner with notification content BUT if I ignore the banner and open the app by the icon (not by notification banner) callback is ignored =[ how to handle this ?

  • expo detach app
  • react 16.3.1
  • onesignal 3.2.5
  • expo 27.0.1

@brunoandradebr this is the expected behavior. Opening the app from the app icon never takes in account any notification you may have received. It just opens the app.

If you want to change the behavior of your app when it starts based on whether the user received a notification, you'll need to check that yourself on launch.

It's not true. When I open the app by icon when the app is running in background, it's handles push notification as desired. Only when I remove it from memory (swipe), then I open by icon, nothing happens. I was investigating and I think it's XCode stop the app and closes oonesignal lib link.

@brunoandradebr I might be misunderstanding the issue you're having, and which callback(s) are you referring to.
If the app runs in the background, it is capable of handling received events. But if the app is killed, it cannot listen for any event. The only way a notification can interact with an app (for iOS at least), is by interacting with the notification in any way.

When I kill the app and has a notification, if I open the app by icon, the app can't handle callback events, BUT if the app is running on background, and I open by icon, callbacks fires as expected. Got it ?

@brunoandradebr I think I got it, and if I'm not wrong that's the behavior I described in my previous comment. In any case, I feel this is off topic and if you think you have an issue with this, you should open a separate issue on the repo.

@jordanmkoncz You seem to have a really good understanding of the react native onesignal issues. The versions I am using react-native-onesignal: ^3.2.8 and react-native: 0.56.0. I was able to implement handling notifications in the background and foreground but I am running into a problem now when the app is closed and a notification is tapped. The app only shows a blank white screen that does not go away unless you close and restart it. I am not sure how to handle this situation since I cannot find any documentation on OneSignal's website. It is frustrating because I spent time learning and implementing this library and now I am not sure if it will be able to handle this extremely important use case. I read a possible workaround for this above which implementing the event listeners in index.js but I am using react navigation and need to get a reference from my main navigator which is declared in App.js in order to navigate to a nested stack screen. I would really appreciate any advice that one has in dealing with this problem and how they solved it. Thanks in advance.

Was this page helpful?
0 / 5 - 0 ratings