Push Notifications in React Native with Google FCM

This article intends to be a complete guide in implementing push notifications in React Native in february 2017. There are many packages that provide push notifications, and I’ll present a setup that works the best both for remote and local notifications, and compiles fine without much hacking. Let’s get started!

Let’s see the packages to consider:

The first two are both intended to focus on just one thing: push notifications. It’s tempting to start our journey with them.

Problems with react-native-fcm

Unfortunately, react-native-fcm does not compile with Gradle 3.1, even though it seems to be the most promising approach. We are left with the other two packages.

Problems with react-native-push-notification

For push notifications what seems to be a better idea than a package containing these exact words in its title?! Actually, react-native-push-notification is a great package, I have found only two single major problem with it: it does not support the new FCM features, like channels, etc, and I did not succeed to get it work with remote notifications at all. One might think at this point then we should go with react-native-firebase. Your mileage may vary, and the response depends on your requirements!

If you need local notifications then react-native-push-notification is the best option you can have. If you would like to use remote notifications, especially with channels, you are left with react-native firebase.

Problems with react-native-firebase

First of all, let’s nail down that react-native-firebase is a great package, and I’m sure that invertase would be happy to accept pull requests if someone has the time and expertise to develop the code.

React-native-firebase is great for remote notifications, it’s easy to set up, comes with all the firebase features you can imaging, and you are still not forced to link them all. This is a great approach as you can control your final app size much better this way, and adding new modules is rather easy.

The only problem I could find with react-native-firebase is the following line of code in its Android bridge:

getAlarmManager().setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent);
This line is called if you set up repeating local notifications. In order to add repeating notification the following comment has been added to the react-native-push-notification package:
<div>
  <pre>// Can't use setRepeating for recurring notifications because setRepeating

// is inexact by default starting API 19 and the notifications are not fired // at the exact time. During testing, it was found that notifications could // late by many minutes.

 Now, as you can see, react-native-firebase does exactly this. Sad story!

Mix & Match

So, how can one get just the best parts of all the above features? My solution is to use both react-native-firebase and react-native-push-notification. The former is used for remote notification delivery and overall notification handling, the latter is used just to schedule and show local notiications.

 The easy part

In order to do this we will need the following additions to AndoidManifest.xml:
<manifest ...>

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application
      ...
      android:launchMode="singleTop">

      <service android:name="io.invertase.firebase.messaging.MessagingService" android:enabled="true" android:exported="true">
        <intent-filter>
          <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
      </service>
      <service android:name="io.invertase.firebase.messaging.InstanceIdService" android:exported="false">
        <intent-filter>
          <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
        </intent-filter>
      </service>

      <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
      <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
        <intent-filter>
          <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
      </receiver>
    </application>
</manifest>
The above services/receivers make react-native-firebase to handle remote notifications, and react-native-push-notification to handle local notifications.

The messy part

On the js side I prefer using Sagas to connect to services like push notification handling.
export function * addLocalNotification (data: PushDataTypes) : Iterable<mixed> {
  yield call(addLocalNotificationAction, data});
}

export function * removeLocalNotification ({ date }: PushDataTypes): Iterable<any> {
  yield call(removeLocalNotificationAction, date);
}

export function createNotificationChannel (): any {
  return eventChannel(emitter => {
    const handler = type => data => {
      emitter([type, data])
    }

    const unsubscribers = configurePushService(handler);

    return function closeNotificationChannel () {
      for(let unsubscriber of unsubscribers) {
        unsubscriber();
      }
    }
  })
}

export function * setupNotifications (channel) {
  try {
    while (true) {
      const [type, data] = yield take(channel);
      yield call(handleNotification, type, data)
      if (data.finish) {
        yield call(data.finish)
      }
    }
  } catch (err) {
    yield call([Sentry, Sentry.captureException], err);
  } finally {
    if (yield cancelled()) {
      yield call([channel, channel.close]);
    }
  }
}

export function * Setup () {
  yield call(setupFCM)

  const notificationChannel = yield call(createNotificationChannel);
  yield all([
    call(setupNotifications, notificationChannel),
    takeEvery(NotificationTypes.ADD_NOTIFICATION, addLocalNotification),
    takeEvery(NotificationTypes.DROP_NOTIFICATION, removeLocalNotification)
    takeEvery(NotificationTypes.JOIN_CHANNEL, ...)
    takeEvery(NotificationTypes.LEAVE_CHANNEL, ...)
  ]);
}

export function * setupFCM () {
  const initialNotification = yield call([firebase.messaging(), firebase.messaging().getInitialNotification]);
  yield call(handleNotification, 'initial', initialNotification)
  const token = yield call([firebase.messaging(), firebase.messaging().getToken]);
  // do something with the device token if you want here
}
Here an interesting bits might be notifcationChannel in the setup method. I’ll explain that later. Otherwise what’s happening is really simple, first setupFCM is called.
  1. I check for any initial notifications
  2. and make it handle by handleNotification
  3. I get the device FCM token
  4. (I store it normally on  my server so I can send messages to single devices)

In Setup I do some more steps.

  1. I create a channel to receive notifications
  2. I set up firebase notifications
  3. I register some Redux event listeners to handle setting up and removing local notifications or subscribing, unsubscribing from channels

The notification-related code for the above is the following:

Finishing it up

So, what are those methods called from the sagas?! Finally, here they come.

export function configure (eventChannel) {
  let unsubscribers = []
  unsubscribers.push(firebase.messaging().onMessage(eventChannel('message')))
  return unsubscribers;
}

export function handleNotification (notificationType, notificationData) {
  console.log({ name: notificationType, value: notificationData });
}

function _getId (date: number) {
  return parseInt(date / 1000, 10).toString()
}

export function addLocalNotification ({ type, date, bigText = 'Big Text', title = 'Title', message = 'Message' }: PushDataTypes): Iterable<mixed> {
  PushNotification.localNotificationSchedule({
    /* Android Only Properties */
    id: _getId(date), // (optional) Valid unique 32 bit integer specified as string. default: Autogenerated Unique ID
    // ticker: 'My Notification Ticker', // (optional)
    date: new Date(date),
    bigText, // (optional) default: "message" prop
    title, // (optional, for iOS this is only used in apple watch, the title will be the app name on other iOS devices)
    message, // (required)
    playSound: true, // (optional) default: true
    soundName: 'default', // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played)
    number: '1', // (optional) Valid 32 bit integer specified as string. default: none (Cannot be zero)
    repeatType: 'day', // (Android only) Repeating interval. Could be one of `week`, `day`, `hour`, `minute, `time`. If specified as time, it should be accompanied by one more parameter 'repeatTime` which should the number of milliseconds between each interval
    // actions: '["Yes", "No"]',  // (Android only) See the doc for notification actions to know more

    autoCancel: true, // (optional) default: true
    largeIcon: 'icon', // (optional) default: "ic_launcher"
    smallIcon: 'icon', // (optional) default: "ic_notification" with fallback for "ic_launcher"
    // subText: '', // (optional) default: none
    color: 'purple', // (optional) default: system default
    vibrate: true, // (optional) default: true
    vibration: 300, // vibration length in milliseconds, ignored if vibrate=false, default: 1000
    tag: 'some_tag', // (optional) add tag to message
    group: 'appapp', // (optional) add group to message
    ongoing: false // (optional) set whether this is an "ongoing" notification
  })
}

export function removeLocalNotification (date: number): Iterable<any> {
  PushNotification.cancelLocalNotifications({ id: _getId(date) })
}

The configure method manages all the message subscribers. Actually, we’ve only one, react-native-firebase. Thankfully, its onMessage method returns an unsubscribe function.

When a new message arrives, the handler passed into configure will be called. This handler was created from Setup, and it loops endlessly in setupNotifications. Once a notification arrives, it yields, and handleNotification is called that currently just writes to the console.

Why do we need all these criss-crossing-function-passing-mess? First, we might get rid of some of it, like to message type argument, and it would make the code a bit simpler. But I’ve left it there as it allows me to add new message handlers easily if I would ever need. (And it came really handly when I tried to set everything up properly.). Second, and more importantly, because I want to handle the closing of the channel properly. As I’ve already mentioned, react-native-firebase‘s onMessage method returns an unsubscribe funtion. This function gets called if we ever react the exception handling part of setupNotifications.

What messages might come in?

What messages shall we expect to arrive in the end? Obviously, having different handlers for local and remote messages won’t make our life very easy.  We have 2×3 possible scenarios.

A message can be

  • local
  • remote

When the push notification is tapped on the app might be

  • in the foreground
  • in the background, but running
  • not running

All these cases will show up differently in our handleNotification method. When the app was not running, then the type will be initial, otherwise it will be message. When the message is local, then notificationData contains

{
  fcm: {
    action: null
  },
  notification: { ... }
}

When the message is remote, then there is no notification key, moreover _fcm _has many other keys. A typical remote message might be the following:

{
  "key1": "value1",
  "fcm": {
    "action": null,
    "tag": null,
    "icon": null,
    "color": null,
    "body": "előtér",
    "title": "címem"
  },
  "finish": " finish() "
}

I found it weird, but the key-value pairs come outside of the fcm part.

Possible Caveats

Until now I did not manage to get remote notifications show up as push-notification when the app was running, but in the background. There is quite some discussion about this topic on stackoverflow though, and to me it seemed that the answer can be constructed from the top 3 replies.