0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

続:ExpoでNotificationを送る

Posted at

前にも記事を書いたのですが、古くなったり、内容が完結してなかったりしたので再度まとめます。
expoと取得したtokenの保存先としてfirestoreを利用してみます。

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

  • Notificationの機能はエミュレータじゃ動きません(テストできません)。

やりたいこと

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

テストアプリの仕様

アプリ(Expo)側:許可+受け取り

以下のようにフォームとボタンを配置。
ボタンを押すとNotificationのための許可を求め、OKなら識別子と一緒にfirestoreに保存。
token取得方法の詳細は本家サイトを見る。

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

スクリーンショット 2019-12-13 8.00.28.png

サーバ側(nodeスクリプト):送信処理

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

実装:アプリ側

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

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

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

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

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

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は下記のような感じです。各自の環境に合わせて設定してください。

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で試すこともできます。デバッグのお供にどうぞ。

実装:サーバ側

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

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

npm install --save expo-server-sdk

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

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

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

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の返り値には送信ステータスが入るみたいです。

node sendNotification.js

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

ひとまず以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?