react-native-firebase(v6↑) + react-native-push-notificationを使ったプッシュ通知実装の注意点

# まえがき

私が開発しているReact Nativeアプリについて、元々react-native-firebase(v5)を使ってプッシュ通知を実装していましたが、そろそろバージョンを上げることにしました。

ところが、v6からローカル通知の機能がNotifeeという有料の別ライブラリに分離されており、ローカル通知の機能のみ他のライブラリを採用せざるを得なくなりました。

そこで使えそうなライブラリを調べてみたところ、

- [**Notifee**](https://github.com/notifee/react-native-notifee): ローカル通知専用ライブラリ。react-native-firebaseのチームが作っており当然相性も良く、品質も高そうだが、**有料**。
- [**react-native-push-notification**](https://github.com/zo0r/react-native-push-notification): リモート＆ローカル通知ライブラリ。iOS部分はreact-native-notification-iosを使用している。一番スターが多い。
- [**react-native-notifications**](https://github.com/wix/react-native-notifications): リモート＆ローカル通知ライブラリ。Wix製。Androidでchannelを指定してローカル通知を送れなさそう。(今のところ)

という感じのようだったので、react-native-push-notificationを採用することにしました。

本記事では、**react-native-firebase(v6↑)**と**react-native-push-notification**を使ってプッシュ通知を実装するにあたっての注意点を紹介します。

**追記 (2023/05/29)**

今さら気が付いたのですが、Notifeeが無料になったようですのでNotifeeを使うのが一番良いと思われます！

https://invertase.io/blog/open-sourcing-notifee

## 実現したいこと

- FCMで受信したメッセージ(※)を、アプリの状態(Foreground/Background/Quit)にかかわらずプッシュ通知で表示したい
- プッシュ通知をタップしたとき、アプリの状態にかかわらず処理したい

(※) 本記事で扱うFCMメッセージは `notification` と `data` の両方を含む、データペイロード付きの通知メッセージとします。

**参照** https://firebase.google.com/docs/cloud-messaging/concept-options?hl=ja#notification-messages-with-optional-data-payload

## 本記事で触れないこと

- react-native-firebaseの導入方法
- FCMの導入方法

# 環境

- React Native (0.63.2)
- @react-native-firebase/app (8.4.2)
- @react-native-firebase/messaging (7.8.6)
- react-native-push-notification (5.1.1)
- @react-native-community/push-notification-ios (1.5.0)

# react-native-push-notificationを導入する

※ react-native-firebaseについては導入が済んでいるものとします。

まずは、[react-native-push-notification](https://github.com/zo0r/react-native-push-notification)を導入します。このライブラリは、iOS部分については[@react-native-community/push-notification-ios](https://github.com/react-native-community/push-notification-ios)を使用しているため、併せてインストールを行います。

```sh
npm install --save react-native-push-notification @react-native-community/push-notification
npx pod-install
```

続けて、iOSについて下記コードの追記を行います。

```objc:AppDelegate.h
#import <UserNotifications/UNUserNotificationCenter.h>
...
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate, UNUserNotificationCenterDelegate>
```

```objc:AppDelegate.m
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>

...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
  center.delegate = self;

  return YES;
}

...

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
  [RNCPushNotificationIOS didReceiveNotificationResponse:response];
  completionHandler();
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}
```

push-notification-iosの[READMEの導入手順](https://github.com/react-native-community/push-notification-ios#update-appdelegatem)には、他にもリモート通知用のデリゲートメソッドがいくつか記載してありますが、今回はローカル通知用として使用するため、上記のデリゲートメソッドがあればOKです。

# 実装前に抑えておくポイント

## アプリの状態

アプリには以下の3つの状態があり、状態によってプッシュ通知受信時およびタップ時の挙動が異なります。

- **Foreground**: アプリが前面にあり動作している状態
- **Background**: ホーム画面や他のアプリが前面にあり、アプリが背面で動作している状態
- **Quit**: タスクキルなどでアプリが死んでいる状態

Quit時にプッシュ通知をタップした場合は、アプリが起動します。その起動したタイミングでタップしたプッシュ通知を処理することになります。

## リモート通知とローカル通知

アプリがBackgroundまたはQuit状態のときにFCMを受信すると、FCM側(react-native-firebase)がプッシュ通知を表示してくれます。しかし、Foreground状態の場合には表示してくれず、代わりに`onMessage`コールバックが呼ばれます。そのため、`onMessage`の中で、react-native-push-notificationを使ってプッシュ通知を表示する処理を実施する必要があります。本記事では、前者のことを**リモート通知**、後者のことを**ローカル通知**と呼んでいます。

# 実装する

関係するコードの抜粋です。

```js:app.js
import PushNotification from 'react-native-push-notification';
import PushNotificationIOS from '@react-native-community/push-notification-ios';

class App extends Component {
  constructor(props) {
    ...

    PushNotification.configure({
      requestPermissions: false,
      onNotification: notification => {
        // プッシュ通知をタップしたときに呼ばれる
        ...  // URLを開くなどの処理
        notification.finish(PushNotificationIOS.FetchResult.NoData);
      } 
    });
  }

  componentDidMount() {
    this.unsubscribeMethods = [
      firebaseMessaging().onTokenRefresh((token: string) => {
        // トークンリフレッシュ時に呼ばれる
        ...  // トークンリフレッシュ時の処理
      }),
      firebaseMessaging().onMessage(message => {
        // Foreground時にリモートプッシュ通知を受信した際に呼ばれる
        this._localNotification(message);        
      })
    ];
  }

  componentDidUnmount() {
    this.unsubscribeMethods();
  }

  // ローカル通知
  _localNotification(message) {
    PushNotification.localNotification({
      title: message.notification.title,
      message: message.notification.body,
      userInfo: message.data,
      ...
    });
  }
}



```

ForegroundでFCMを受信したときには`onMessage`が呼ばれるので、`localNotification()`でローカル通知を行っています。

プッシュ通知がタップされたときはreact-native-push-notificationの`onNotification`が呼ばれます。ローカル通知だけでなく、react-native-firebaseが表示したリモート通知がタップされたときも呼ばれます。また、Quit時にタップされてアプリが起動する場合も、起動時に呼ばれます[^1]。すなわち、プッシュ通知がタップされた際の処理は、あらゆる場合に`onNotification`でカバーできることになります。

[^1]: この挙動は `configure` で `popInitialNotification` を指定すると変更できるようです。本記事ではデフォルト(true)の場合について言及しています。

なお、@react-native-firebase/messagingには、リモート通知をタップした際に呼ばれる`onNotificationOpenedApp`や、起動時に呼ぶことでQuit時にタップした通知を取得する`getInitialNotification()`が用意されていますが、リモート通知に限って処理を行いたいケースを除いては、特に使う必要はなさそうです。

## 注意点

**Androidの場合かつリモート通知をタップした場合**、`onNotification`で通知されるnotificationの構造が異なります。

例えば、下記の形のFCMメッセージが送信された場合、

```json
{
  "message": {
    "token": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification": {
      "title":"お知らせ",
      "body":"タップしてURLを開きます"
    },
    "data": {
      "url": "https://natade-coco.co.jp/"
    }
  }
}
```

`onNotification` で受け取ったnotificationから `url` へは `notification.data.url` でアクセスできます。

しかし、Androidのかつリモート通知の場合には`data`で囲まれておらず、 `notifiation.url` で直接アクセスする形になります。そのため、あらゆるケースで正しくデータを拾うためには一工夫が必要になりそうです。

# タップしたときの動き(メモ)

各OS、各アプリ状態、ローカル通知/リモート通知について、プッシュ通知をタップしたときにどのコールバックが呼ばれるかを整理した際のメモを載せておきます。`onNotification`では、Foregroundでタップされたかどうかの値`foreground`、ユーザーがタップしたかどうかの値`userInteraction`が引数で渡されますが、一部のケースで誤った値になっていそうです(該当箇所は太字で記載しています)。

## iOS

### ローカル通知

- **Foreground**: `onNotification`(foreground=true, userInteraction=**false**)が呼ばれる
- **Background**: `onNotification`(foreground=false, userInteraction=true)が呼ばれる。
- **Quit**: `onNotification`(foreground=false, userInteration=true)が呼ばれる。(`getInitialNotification()`では取れない。)

### リモート通知

- **Foreground**: `onNotificationOpenedApp`が呼ばれる。`onNotification`(foreground=true, userInteraction=**false**)も呼ばれる。
- **Background**: `onNotificationOpenedApp`が呼ばれる。`onNotification`(foreground=false, userInteraction=true)も呼ばれる。
- **Quit**: `getInitialNotification()`で取れる。`onNotification`(foreground=false, userInteraction=true)も呼ばれる。

## Android

### ローカル通知

- **Foreground**: `onNotification`(foreground=true, userInteraction=true)が呼ばれる。
- **Background**: `onNotification`(foreground=**true**, userInteraction=true)が呼ばれる。
- **Quit**: `onNotification`(foreground=false, userInteration=true)が呼ばれる。(`getInitialNotification()`では取れない。)

### リモート通知

- **Foreground**: `onNotification`(foreground=**false**, userInteraction=true)が呼ばれる。`onNotificationOpenedApp`も呼ばれる。
- **Background**: `onNotification`(foreground=false, userInteraction=true)が呼ばれる。`onNotificationOpenedApp`も呼ばれる。
- **Quit**: `getInitialNotification()`で取れる。`onNotification`(foreground=false, userInteraction=true)も呼ばれる。

# まとめ

- ForegroundでFCMを受信した際にはプッシュ通知が表示されず`onMessage`が呼ばれるので、そのタイミングでreact-native-push-notificationの`localNotification()`を使ってローカル通知を表示する。
- OS、アプリの状態、リモート通知/ローカル通知にかかわらず、通知タップ時の処理は`onNotification`で行えばOK。
- `onNotification`のforegroundやuserInteractionの値は怪しいので、場合分けには使わない方が無難。

以上です。