続：ExpoでNotificationを送る


[前にも記事を書いた](https://qiita.com/zaburo/items/e04c1698694c6018a731)のですが、古くなったり、内容が完結してなかったりしたので再度まとめます。
expoと取得したtokenの保存先としてfirestoreを利用してみます。

## 最初に知っておくべきこと

* Notificationの機能はエミュレータじゃ動きません（テストできません）。

## やりたいこと

* expoで作ったアプリにNotificationを送りたい
* token情報はfirebase(firestore)に保存しておく
* firestoreの情報を使ってNotificationを送る（とりあえずnodeスクリプト書く）

## テストアプリの仕様

### アプリ(Expo)側：許可＋受け取り

以下のようにフォームとボタンを配置。
ボタンを押すとNotificationのための許可を求め、OKなら識別子と一緒にfirestoreに保存。
token取得方法の詳細は[本家サイト](https://docs.expo.io/versions/latest/guides/push-notifications/)を見る。

>識別子は通常はFirebase Authenticationのuidとかになると思いますが、Auth実装がめんどいのでとりあえずユーザーを特定するための一意なキーとして利用。


![スクリーンショット 2019-12-13 8.00.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/09659e20-180a-84da-0c31-17116001a9b1.png "スクリーンショット 2019-12-13 8.00.28.png")

### サーバ側（nodeスクリプト）：送信処理

expo-server-sdkを利用することでnodeからexpo-serverにNotification送信を指示できるようなので、やってみる。
使い方は[本家サイト](https://github.com/expo/expo-server-sdk-node)を参考にする。

## 実装：アプリ側

必要なモジュールをインストールします。
token取得に必須なのはexpo-permissionsだけです。

```
npm install --save react-native-element formik yup firebase expo-permissions
```

実装します。大きく2つの機能を実装しています。
iOSはフォアグラウンドでNotificationを受け取ったとき何も反応しないので、フォアグラウンド時の処理も追加しています。

* tokenの取得
* Notificationをフォアグラウンドで受け取ったときの処理

コードは以下の通り。
Firebase.jsというファイルを作り、そこでFirebaseの定義をしています（サンプル後述）。

```jsx:App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

import { Card, Input, Button } from 'react-native-elements';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { Notifications } from 'expo';
import * as Permissions from 'expo-permissions';

import firebase, { db } from './Firebase';

export default class App extends React.Component {

    handleOnSubmit = async (values) => {
        // alert(JSON.stringify(values));
        try {
            //現在のパーミッション状況を取得
            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') {
                //何もしない
                return;
            }

            //tokenを取得
            const token = await Notifications.getExpoPushTokenAsync();

            //Firebaseに保存
            db.collection("members").add({
                identifier: values.identifier,
                token: token,
                createdAt: firebase.firestore.FieldValue.serverTimestamp(),
            })

            alert("登録しました。");

        } catch (e) {
            console.log(e);
        }

    }

    componentDidMount = () => {

        //Notification監視リスナー追加
        this.notificationUnSubscription = Notifications.addListener(this.handleNotification);

    }

    handleNotification  = (notification) => {
        
        //バックグラウンド時
        if(notification.origin === 'selected'){
            //何もしない
        }
        //フォアグラウンド時
        if(notification.origin === 'received'){
            alert('通知がきました。' + JSON.stringify(notification));
        }
    }

    render() {
        return (
            <View style={{ flex: 1, justifyContent: 'center' }}>
                <Formik
                    initialValues={{ identifier: '' }}
                    onSubmit={this.handleOnSubmit}
                    validationSchema={Yup.object().shape({
                        identifier: Yup.string().required(),
                    })}
                >
                    {
                        ({ handleSubmit, handleChange, handleBlur, values, errors, touched }) => (
                            <Card
                                title="Notification Test"
                            >
                                <Input
                                    label="識別子"
                                    name="identifier"
                                    autoCapitalize="none"
                                    value={values.identifier}
                                    onChangeText={handleChange('identifier')}
                                    onBlur={handleBlur('identifier')}
                                    errorMessage={errors.identifier && touched.identifier ? errors.identifier : null}
                                />
                                <Button
                                    title="Notification登録"
                                    buttonStyle={{ marginVertical: 30 }}
                                    onPress={handleSubmit}
                                />
                            </Card>
                        )
                    }
                </Formik>
            </View>
        )
    }
}
```

上記コードでは重複登録等の処理はしていません。必要に応じて実装してください。

>重複登録したら、その数だけ、その端末にNotificationが届きます。


### Firebase.js

いちおうFirebase.jsは下記のような感じです。各自の環境に合わせて設定してください。

```js:Firebase.js
import firebase from 'firebase/app';
import 'firebase/firestore';

const firebaseConfig = {
    apiKey: "xxxxxxxxxx",
    authDomain: "xxxxxxxxxx",
    databaseURL: "xxxxxxxxxx",
    projectId: "xxxxxxxxxx",
    storageBucket: "xxxxxxxxxx",
    messagingSenderId: "xxxxxxxxxx",
    appId: "xxxxxxxxxx",
    measurementId: "xxxxxxxxxx"
};

firebase.initializeApp(firebaseConfig);
export default firebase;
export const db = firebase.firestore();
```

### その他

なお、Tokenが取得できたら、[Expoのテスト用UI](https://expo.io/notifications)で試すこともできます。デバッグのお供にどうぞ。

## 実装：サーバ側

expo-server-sdkを利用すれば簡単です。ここではnodeスクリプトにしていますが、実務ではFirebase Functionsとかで送信関数を作ったりすることになるのでしょう。

```bash
mkdir notification-server
cd notification-server
npm init -f
touch sendNotification.js

npm install --save expo-server-sdk
```

難しいことは特に無いのですが「ループしながら送ればいいのかな」と思っていたところ、

* 一度、送信先、送信メッセージを配列に入れる
* chunkする
* chunkしたものをループしながらNoficationをsendする

というような流れを取るみたいです。

```js:sendNotification.js
const { Expo } = require('expo-server-sdk');
const admin = require('firebase-admin');

const expo = new Expo();

//firebaseの初期化
admin.initializeApp({
    credential: admin.credential.cert('/path/to/key.json'),
    databaseURL: 'https://[project_id].firebase.com'
});

const db = admin.firestore();


//async/awaitを使いたいので即時関数の中で使ってます
(async () => {
	
	//データ取得
    const snapshots = await db.collection("members").get();
    const docs = snapshots.docs.map(doc => doc.data());
	
	//ループ
    docs.map(doc => {

        //いちおうtokenの形式チェック
        if (!Expo.isExpoPushToken(doc.token)) {
            console.log(doc.token + " is no a valid token.");
        }

        //送るメッセージ群を入れる配列を作る（こうするのが定石らしい）
        let message = [];

        //送るメッセージを配列に追加（メッセージを分けたい場合はここで条件分岐 if doc.identifier == 'xxx'）
        message.push({
            to: doc.token,
            sound: 'default',
            title: 'test',
            body: 'this is a test notification for 111',
            data: { 'name': 'foo' }
        });

        //chunkする
        const chunks = expo.chunkPushNotifications(message);
        //ループを回しながら非同期で送る
        let tickets = []
        chunks.map(async chunk => {
            try {
            	  //送信処理
                let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
                console.log(ticketChunk); //結果が返る
            } catch (e) {
                console.log(e);
            }
        })
        console.log(tickets);
    })

})();
```

送信する。sendの返り値には送信ステータスが入るみたいです。

```bash
node sendNotification.js

[ { status: 'ok', id: '4643b1f8-be99-47f0-9ccd-56758df80716' } ]
[ { status: 'ok', id: 'd71913ca-8612-4b41-bb26-486d097db6c8' } ]
```

ひとまず以上です。