Expo/Notificationの各機能を使ってみる

ExpoでのNotificationの各機能について検証してみたのをまとめます。
Notificationの流れがある程度わかっている方向けの記事になりますので、悪しからずご容赦ください。

### 環境
Expo v35.0
iOS 13/iPhone8
Android9/AQUOS sense2 SHV43

# Expoプッシュトークンを取得
ひとまずトークンを取得して、コンソールに表示してみます。（ほぼ公式のソースのまま）

```javascript
import { Notifications } from "expo";
import * as Permissions from "expo-permissions";

(async () => {
  const { status: existingStatus } = await Permissions.getAsync(
    Permissions.NOTIFICATIONS
  );
  let finalStatus = existingStatus;

  // only ask if permissions have not already been determined, because
  // iOS won't necessarily prompt the user a second time.
  if (existingStatus !== "granted") {
    // Android remote notification permissions are granted during the app
    // install, so this will only ask on iOS
    const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
    finalStatus = status;
  }

  // Stop here if the user did not grant permissions
  if (finalStatus !== "granted") {
    return;
  }

  // Get the token that uniquely identifies this device
  const token = await Notifications.getExpoPushTokenAsync();

  console.log("token!!!", token);
})();
```

`ExponentPushToken`から始まるトークンがコンソールに表示されたらOKです。

```
token!!! ExponentPushToken[XXXXXXXXXXXXXXXXXXXXXX]
```

このトークンは端末ごとに固有のもので、アプリをインストールし直したりした場合には新しく生成されます。
ですので、実際にアプリで使用する場合はユーザーごとに複数のトークンを保存するような仕組みが必要になります。

# 送信してみる

では、試しにNotificationを送信してみましょう。
公式の`Push Notification Tool`やサーバー用のSDKも各種揃っていますが、要はExpoのAPIにトークンやNotificationのデータをPOSTしているだけなので、以下のように簡単にターミナルからテストすることができます。

```
$ curl -H "Content-Type: application/json" -X POST "https://expo.io/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[XXXXXXXXXXXXXXXXXXXXXX]",
  "title":"hello",
  "body": "world",
  "data": {},
}'
```

`to`のところに先ほど取得したExpoプッシュトークンをいれます。APIを叩いてエラーがなければ端末に通知が届くと思います。

Expo Clientで開発している場合、アプリ↔︎OS間のやりとりはExpo Clientを介すことになるので、特にNotificationに関してはスタンドアロンアプリと違う挙動をすることが多いです。
iOS/Androidで試しにそれぞれ送ってみましょう。

iOS
上：ExpoClient上のアプリで取得したトークンに送ったもの
下：Expoでスタンドアロンビルドされたアプリに送ったもの
<img width="375" alt="PNGイメージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/9c0d7c49-98ab-763a-b584-2fa8d8f1b02c.png">
こちらはアイコン以外に変化ないですね。ExpoClientの場合はExpoClientのアイコンが表示され、スタンドアロンアプリの場合はapp.jsonで指定されたアプリ自体のアイコンが表示されます。

一方、

Android
上：ExpoClient上のアプリで取得したトークンに送ったもの
下：Expoでスタンドアロンビルドされたアプリに送ったもの
<img width="375" alt="Android" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/f537b2b8-44cc-ab48-2a10-e68a6f1338f9.jpeg">
ExpoClientの場合はタイトルが`[アプリ名] - [Notificationタイトル]`というように表示されました。これはExpoClient側が勝手にやってるものと思われます。また、スタンドアロンアプリの場合はアプリアイコンが表示されていません。これはフォーラムにもissueが上がっているようで、Expo側の問題かもしれません。
https://forums.expo.io/t/push-notification-dynamic-icon/1684/5
小さいアイコンはどちらもExpoのデフォルトのもの（上向きの矢印のような）が表示されていますね。

### AndroidのNotificationアイコンをカスタマイズする
小さいアイコンに関しては`app.json`での設定が必要なようです。
[ドキュメント](https://docs.expo.io/versions/latest/workflow/configuration/#notification)を参考にしてみます。

*assets/notificationicon.png*
![notificationicon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/efdb975f-d4b7-2c4e-44e5-8f644367c4ea.png)
説明通り、96px x 96pxで透過PNGの画像を作成しました。このページでは白く見える部分が透過されています。
これを`app.json`で指定します。

```json:app.json
    "notification": {
      "icon": "./assets/notificationicon.png",
      "color": "#00c2ad"
    },
```

アプリをビルドし、Notificationを送ってみると...
<img width="375" alt="Android" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/c2d80086-def0-98d7-a4f7-544431293549.jpeg">
小さいアイコンが設定したものに変わりました。もっとシンプルなアイコンにした方が良さそうです。
説明だと`color`で指定した色がオーバーレイされるはずなのですが、色は他のUIと共通のものになっていますね。これは端末のソフトウェアによるのかもしれません。
ひとまずExpoのアイコンではなくなったので良しとしましょう。

#### 追記
端末によっては、ここで指定したアイコンがそのままの色で表示されることがあるようです。
<img width="768" alt="Screenshot_2019-11-19-06-34-10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/9af916bd-798f-9c95-334d-8358921e5574.png">
白い透過画像に差し替えると
<img width="768" alt="Screenshot_2019-11-19-06-46-16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/58b08d48-7ab0-ff39-09f7-4394f3a26719.png">
こんな感じに馴染みました。白色・透過PNGがベストかもしれません。
Androidでは通知の表示のされ方は端末ごとのソフトウェアによって結構ばらつきがあるようです。

# バッジを制御してみる

アプリアイコンに表示されるバッジの数字を動的に制御してみます。
APIに`badge`パラメータを渡すと...

```
$ curl -H "Content-Type: application/json" -X POST "https://expo.io/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[XXXXXXXXXXXXXXXXXXXXXX]",
  "title":"hello",
  "body": "world",
  "data": {},
  "badge": 100
}'
```
<img width="375" alt="iOS" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/f8f9cdfd-ef78-c304-4695-4afb40c410b4.png">
指定した数字がつきました。
これはiOSのみで、Androidでは実現できず、単純に溜まっている通知の数がバッジとして表示されるのみです。
[ネイティブの仕様](https://developer.android.com/training/notify-user/badges#set_custom_notification_count)ではAndroidもできるはずですが、今のところExpoでは対応していないということかと思います。

iOSではアプリ側でもバッジの制御ができます。
数字を0に戻す処理を足してみます。

```javascript
import { Platform } from 'react-native';

if (Platform.OS === 'ios') {
  Notifications.setBadgeNumberAsync(0);
}
```

<img width="375" alt="PNGイメージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/fc545bad-7902-9ad7-3a1c-56564e476a73.png">

バッジが消えました。
ユーザーが通知に関わるコンテンツを閲覧したらバッジの数を変更するといったことがこれで可能です。（現在のバッジの数は`Notifications.getBadgeNumberAsync()`で取得できます。）

# アプリ側で通知に反応する

通知が届いたときにアプリ側で処理するために、リスナーを登録してみます。

```javascript
import { Notifications } from "expo";

Notifications.addListener(notification => {
  console.log('received notification', notification);
});
```

リスナーに渡されたNotificationの内容をコンソールに出してみると、こんな感じです。
<img width="650" alt="React_Native_Debugger.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/ad4421ee-08d2-c00e-ecfa-b20fbce502c6.png">
`remote`はPushNotificationかLocalNotificationかどうかが、
`actionId`と`userText`はあとで紹介するNotificationCategoryのときに値が入ります。

`origin`はアプリへの導線を表し、`selected`の場合は通知をタップしたとき、`received`の場合はアプリ起動中に通知が来たときです。
これをみることで、例えば通知をタップしたら詳細画面へ直接遷移し、アプリ起動中の場合はアラートダイアログを出したあと詳細画面へ遷移する、といった制御が可能です。

```javascript:例
import { Alert } from "react-native";

...
if (notification.origin === "selected") {
  // 詳細画面へ
} else if (notification.origin === "received") {
  Alert.alert(
    `コメントが届きました`,
    "詳細画面へ移動しますか？",
    [
      {
        text: "キャンセル",
        style: "cancel"
      },
      {
        text: "移動する",
        onPress: () => {
          // 詳細画面へ
        }
      }
    ]
  );
}
```

スタンドアロンアプリとしてビルドして動作検証済みです。
ExpoClientでの開発中は通常、通知をタップしたらExpoClientのホーム画面に移動してしまうので、`selected`のパターンは確認できません。

また、アラートを表示するなどしなくても、APIに`_displayInForeground`パラメータをつけることでアプリ起動中にも上に通知が表示されるようになります。

```
$ curl -H "Content-Type: application/json" -X POST "https://expo.io/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[XXXXXXXXXXXXXXXXXXXXXX]",
  "title":"hello",
  "body": "world",
  "data": {},
  "_displayInForeground": true
}'
```
iOSでは、
<img width="375" alt="PNGイメージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/0618e902-48d9-7e0a-a9f0-595caa9d4817.png">
アプリの上に重なります。この状態で通知をタップしたら`received`に。

通知が上に引っ込んでから、通知センターを引き出して、通知をタップしたら`selected`に。
<img width="375" alt="PNGイメージ 2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/d9e08beb-89c9-43a4-9dcb-a14dcecbe56e.png">
あるいは
<img width="375" alt="PNGイメージ 3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/a1b818f8-5c2b-5542-25a3-8cd125d70d52.png">
アプリに重なっている通知を深くタップしたり下にスワイプしてフォーカスしてからタップすると`selected`になります。

Androidでは残念ながら`_displayInForeground`は動作しませんでした。
また、iOSでは`_displayInForeground`ではない通知がアプリ起動時に届いた場合通知センターに通知が表示されないのに対して、
Androidでは`_displayInForeground`やアプリの起動状態に関係なく通知センターに表示されます。アプリが起動中ならその瞬間`received`でリスナーが呼ばれ、その通知をタップした場合にも`selected`でリスナーが呼ばれるので、Androidでは一つの通知に関して最大2回リスナーが呼ばれることになります。

# NotificationCategoryを使ってみる

iOSではInteractiveNotifications、Androidでは通知アクションといわれる機能を使って、ユーザーが通知に対するアクションを選択できるようにしてみます。

まずはアプリ側で通知カテゴリ（Notification Category）を定義します。

```javascript
import { Notifications } from "expo";

Notifications.createCategoryAsync('myCategory', [
  {
    actionId: "foo",
    buttonTitle: "foo", // ボタンに表示されるタイトル
    textInput: { // テキスト入力をさせる場合
      submitButtonTitle: 'OK', // 送信ボタンのtaitoru
      placeholder: 'placeholder' // 未入力の時に表示されるplaceholder
    },
    isDestructive: true, // trueだとボタンが赤文字になる（iOSのみ）
    isAuthenticationRequired: true // trueだとロック時にアクションできない（iOSのみ）
  },
  {
    actionId: "bar",
    buttonTitle: "bar",
    isDestructive: false,
    isAuthenticationRequired: false
  },
  {
    actionId: "baz",
    buttonTitle: "baz",
    isDestructive: false,
    isAuthenticationRequired: false,
    doNotOpenInForeground: true // trueにするとタップしてもアプリを開かない（iOSのみ）
  }
]);
```
`Notifications.createCategoryAsync`の第一引数には任意のカテゴリーIDを、第二引数にアクションの配列を渡します。
アクションの`actionId`は先ほどのリスナーに渡されるNotificationオブジェクトに入るので、これによってアプリ側からはどのアクションが呼ばれたか判別できるというわけです。

アプリ側で一度このコードを実行しておいてから、APIを叩いてPush通知を送信してみましょう。

```
$ curl -H "Content-Type: application/json" -X POST "https://expo.io/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[XXXXXXXXXXXXXXXXXXXXXX]",
  "title":"hello",
  "body": "world",
  "data": {
    "some": "data"
  },
  "_category": "@user/experienceId:myCategory"
}'
```
`_category`パラメータに先ほどアプリ側で定義したカテゴリーIDをいれます。
ExpoClient上で確認する場合は、`@ユーザー名/プロジェクト名:カテゴリーID`というようにprefixをいれて送ります。
スタンドアロンアプリではカテゴリーIDのみで大丈夫です。

通知が届き、iOSでは通知を深くタップするとこのようにボタンが表示されました。
<img width="375" alt="PNGイメージ 4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/0d03fdfb-92ee-1ca8-c934-0332abf31c82.png">

一つ目の`foo`はテキストを入力させるように定義しているので、タップすると入力UIが表示されます。
<img width="375" alt="PNGイメージ 5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/6856ecb7-c827-03a8-fea4-b5a83c1c9b76.png">
これで送信ボタンをタップしたらアプリ側のリスナーが呼ばれます。
入力された内容は先ほどの`userText`から参照できます。

Androidでは...
<img width="375" alt="Android" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/b6a6ec11-1d16-b821-4400-ed8759262077.jpeg">
通知センターにそのまま表示されました。
テキスト入力もできます。送信ボタンのタイトル（`submitButtonTitle`）は効かず、アイコンになっていますね。
<img width="375" alt="Android" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/a5203711-fd07-2c98-aef5-d897d01985da.jpeg">

あまり使わないでしょうが、カテゴリーを削除するためのメソッドも用意されています。

```
Notifications.deleteCategoryAsync(categoryId)
```

# 終わり

以上、ExpoのNotificationについて、使いそうな機能を検証してみました。
引き続きほかの機能も調べていこうと思います。

#### 追記

各機能をExpoClient/スタンドアロンビルド/ストアからインストールのそれぞれで検証してみた状況を[スプレッドシート](https://docs.google.com/spreadsheets/d/1i2r4IiAqTzToPwCXuwfNY2sHEU7oi_EtQC-XFCyIgMo/edit?usp=sharing)にまとめています。