Expo Push API でReact-nativeアプリに通知処理を実装する



# 目的
- 通知処理の実現のために必要な実装を整理・把握する
- Expoのマネージドワークフローを利用し、出来るだけ簡易に通知処理を実現する

# 環境
- react 16.11.0
- react-native 0.62.2
- Expo 3.21.13
- Node 6.13.4

# ゴール
- ミニマルに通知処理を実現しているExpo公式サンプルコードの内容を理解する


# Expo Push APIでの　Push通知の概要
通知処理を実現する方法には以下がある。
・Googleが提供している Firebase JS SDK
　一部機能制限がある
・React Native Firebase
　Expoのマネージドワークフローをデタッチする必要がある
・Expo Push API 


今回はExpo Push API を利用する
> 公式ドキュメント
> https://docs.expo.io/push-notifications/overview/#usage

 
・Expoのサーバを利用し通知処理を行う。自前でサーバ環境を用意することを省略できる（構築も可能。SDKは公式サイトから提供されている）
・ネイティブデバイス情報やAPN（Apple Push Notificationサービス）やFCM（Firebase Cloud Messaging）との通信をExpo側で処理してくれる

# 実装する必要のある内容
プッシュ通知の設定には大きく分けて3つのステップがある
1)セットアップ：ユーザーのExpoプッシュトークンの取得
2)送信: 通知を送信したいときにトークンを使ってExpoのPush APIを呼び出す
3)受信：アプリ内の通知に反応する

以下、各ステップを実現するために必要な内容を列記する

## １）セットアップ
以下２つの設定を行う必要がある
・パーミッションの取得
・ExpoPushTokenの把握（プッシュ先がメールならメアド）

### 1.1コード例
- 以下の処理で、通知の許可をユーザから取得すること、端末のデバイストークンを取得することを行う

```
registerForPushNotificationsAsync = async () => {

 // 実機であるかどうかをチェック
  if (Constants.isDevice) {
    const { status: existingStatus } = await Permissions.getAsync(Permissions.NOTIFICATIONS);
    let finalStatus = existingStatus;
    if (existingStatus !== 'granted') {
      const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
      finalStatus = status;
    }

  // 通知が許可されているかどうかをチェック
    if (finalStatus !== 'granted') {
      alert('Failed to get push token for push notification!');
      return;
    }

    // デバイストークンを取得
    const token = (await Notifications.getExpoPushTokenAsync()).data;
    console.log(token);
    this.setState({ expoPushToken: token });
  } else {
    alert('Must use physical device for Push Notifications');
  }

// 端末がAndroidである場合は次の設定を行う
  if (Platform.OS === 'android') {
    Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }
  };
```

> 参考：[React Native] ExpoのPush Notificationsでプッシュ通知する
> https://www.aizulab.com/blog/expo-push-notifications-example


### 1.2 FCMのセットアップ
Androidに対応をする必要がある場合は設定が必要となる
（FirebaseアプリとExpoアプリは連携済みの前提）

Firebase consoleにて以下を実行し、`google-services.json` を取得する
- 概要からアンドロイドマーク、または追加を行う
- パッケージ名`com.xxx.任意のパッケージ名`
- google-services.jsonをダウンロード
- ダウンロードしたファイルを、プロジェクトのルートディレクトリに配置する
- app.jsonにgoogle-services.jsonの相対パスを追加

```
"android": {
      "googleServicesFile": "./google-services.json"
    },

```

- また、Androidでビルドする時はコマンドラインで以下を実行する

```
expo build:android
```

### 1.3 サーバクレデンシャルの設定
Firebase consoleにて以下を実行する
- 設定>cloud messagingタブのサーバーキーを取得する
- サーバキーをExpoにpushする

```
expo push:android:upload --api-key <your-token-here>
```

> 参考：ExpoのPush APIのガイド
> https://docs.expo.io/push-notifications/sending-notifications/


- 以上でFCMセットアップ完了となる

## 2)送信: 通知を送信したいときにトークンを使ってExpoのPush APIを呼び出します。
### 2.1 とりあえず通知を飛ばしたい
- 送信を受信テストのために実施したい場合は、以下のツールで通知をテスト送信することができる

> テスト用の通知送信ツール
> https://expo.io/notifications


### 2.2 アプリから通知を送信する方法

- `Expo-notifications` モジュールを利用しクライアントを実装する
> https://docs.expo.io/versions/latest/sdk/notifications/
(Expoサーバを中継せず、自分で構築したサーバからAPNやFCMへの通知送信も可能)

- ExpoPushTokenを使用してExpoサーバーにリクエストを送信する
- リクエストは、次の内容のPostリクエストを行う。


```
https://exp.host/--/api/v2/push/send


host: exp.host
accept: application/json
accept-encoding: gzip, deflate
content-type: application/json
```

curl で実行する場合の例

```
curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[ここには送り先の端末通知トークンを設定する]",
  "title":"hello", 通知のタイトル情報をここに設定する
  "body": "world"
}'
```

- リクエストボディは、toのみが必須となっている。宛先の通知トークンをここに設定する
- title、bodyに通知する内容を設定する

### 2.3 ログの取得方法　送信が正しく行われたかどうかを確認する
- 送信結果はリクエストidをidsに渡してレシートを発行して確認する
- あくまでも通知サービスの実行結果であるため、実端末の電源オフ等で受け取れていない場合でもOKが帰る

```
curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/getReceipts" -d '{
  "ids": [
    "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY",
    "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ",
  ]
}'
```



## 3)受信：アプリ内の通知に反応する。

- 通じに応じて画面を制御する場合は Notifications API を利用する


### 3.1 通知を受信した時のリスナー

- アプリがフォアグラウンドの時、通知を受けた時の振る舞いをこのオプションで設定する
- `Notifications.setNotificationHandler`


```
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

```


### 3.2 通知に反応した時のリスナー

- 起動中に通知イベントを取得した時と、通知にインタラクトしたイベントを取得した時のハンドラは以下
・addNotificationReceivedListener
  -> 通知を取得したことを検知して処理を行う
・addNotificationResponseReceivedListener 
  -> 通知に対してのユーザのアクションを検知して処理を行う

### 3.3 以上のリスナーをコンポーネント読み込み時に設定する
- コード例

```
componentDidMount() {
    registerForPushNotificationsAsync();

    Notifications.addNotificationReceivedListener(this._handleNotification);
    
    Notifications.addNotificationResponseReceivedListener(this._handleNotificationResponse);
  }

  _handleNotification = notification => {
    this.setState({ notification: notification });
  };

  _handleNotificationResponse = response => {
    console.log(response);
  };
```


# サンプルコードを読む

- これまでの知識をもとに、実際のコード読み、実装に必要な処理を把握する
- サンプルコード内に、その処理の意味を記載

> Push Notifications Overview
> https://docs.expo.io/push-notifications/overview/#usage


```
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';  // 追加するコンポーネント
import * as Permissions from 'expo-permissions'; // 追加するコンポーネント
import React, { useState, useEffect, useRef } from 'react';  // 追加するコンポーネント
import { Text, View, Button, Platform } from 'react-native'; // 追加するコンポーネント

// 用途: 受信
// アプリがフォアグラウンドの時に通知を受信した時の振る舞いを設定
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

export default function App() {
// 用途: 　ステートフックの設定（useStateフックの利用）
// 関数コンポーネントでstateを使うため。
// やっていること：[stateの名前, セッタメソッド名] を設定、以後ここに値を保持して引き回せる
// ドキュメント　https://ja.reactjs.org/docs/hooks-state.html
  const [expoPushToken, setExpoPushToken] = useState('');
  const [notification, setNotification] = useState(false);
  const notificationListener = useRef();
  const responseListener = useRef();

// 用途：（useEffect副作用フック）
// コンポーネントのレンダー後（読み込み、更新時）に実行する処理を定義
// やっていること：Reactクラスの機能であるcomponentDidMount/componentDidUpdate/componentWillUnmount の動作を関数コンポーネントで実現する
// ドキュメント　https://ja.reactjs.org/docs/hooks-effect.html
  useEffect(() => {
    registerForPushNotificationsAsync().then(token => setExpoPushToken(token));

    // 用途：アプリがフォアグラウンドの時の振る舞いを定義
    // やっていること：起動中に通知を受け取った時の動作として、通知をstateに設定
    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      setNotification(notification);
    });

    // 用途：ユーザの反応に応じて動作するリスナー
    // やっていること：ユーザが通知をタップしたときの動作を定義
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      console.log(response);
    });

// 用途：クリーンアップ処理の定義
// userEffectのreturnに登録する関数は、コンポーネントがunmountされるときに実行される。ここで主にcleanup処理を定義する
// やっていること：クリーンアップ処理、第２引数の値が変わっているときだけ実行する、という絞り込みでパフォーマンス改善できる
// クリーンアップ処理として、購読処理を停止している
    return () => {
      Notifications.removeNotificationSubscription(notificationListener);
      Notifications.removeNotificationSubscription(responseListener);
    };
  }, []);
// ===ここまでが、通知リスナーの購読開始設定と、購読終了時のクリーンアップの設定

  return (
    <View
      style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'space-around',
      }}>
      <Text>Your expo push token: {expoPushToken}</Text>
      <View style={{ alignItems: 'center', justifyContent: 'center' }}>
        <Text>Title: {notification && notification.request.content.title} </Text>
        <Text>Body: {notification && notification.request.content.body}</Text>
        <Text>Data: {notification && JSON.stringify(notification.request.content.data)}</Text>
      </View>
      <Button
        title="Press to Send Notification"
        onPress={async () => {
	// 用途: プッシュの実行呼び出し
	// やっていること：stateに設定されている端末の通知トークンを、通知発行メソッドに渡している
          await sendPushNotification(expoPushToken);
        }}
      />
    </View>
  );
}

// 用途：通知の発行
// やっていること：受け取った通知トークンでExpoサーバに通知を発行
async function sendPushNotification(expoPushToken) {
  const message = {
    to: expoPushToken, // 通知宛先としてトークンを設定
    sound: 'default',
    title: 'Original Title',
    body: 'And here is the body!',
    data: { data: 'goes here' },
  };

  await fetch('https://exp.host/--/api/v2/push/send', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Accept-encoding': 'gzip, deflate',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(message),
  });
}

// 用途：通知トークンの発行
// やっていること：デバイス・認可のチェックを行い、expo-notificationsコンポーネントを利用して端末トークンを発行する
// このトークンが通知の宛先となる
async function registerForPushNotificationsAsync() {
  let token;
  if (Constants.isDevice) {
    const { status: existingStatus } = await Permissions.getAsync(Permissions.NOTIFICATIONS);
    let finalStatus = existingStatus;
    if (existingStatus !== 'granted') {
      const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      alert('Failed to get push token for push notification!');
      return;
    }
    token = (await Notifications.getExpoPushTokenAsync()).data;
    console.log(token);
  } else {
    alert('Must use physical device for Push Notifications');
  }

  if (Platform.OS === 'android') {
    Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }

  return token;
}

```

# まとめ
## 通知実現のための段取り
1. 通知を受ける側は、自分の通知トークンを`expo-notifications`を発行し相手に渡す
2. 送り主は、ボタンイベント等をきっかけに、Expoのサーバ `https://exp.host/` に宛先としてもらった通知トークンを設定しリクエストをする
3. `useEffect`で設定した各リスナーで通知を取得する