前にも記事を書いたのですが、古くなったり、内容が完結してなかったりしたので再度まとめます。
expoと取得したtokenの保存先としてfirestoreを利用してみます。
最初に知っておくべきこと
- Notificationの機能はエミュレータじゃ動きません(テストできません)。
やりたいこと
- expoで作ったアプリにNotificationを送りたい
- token情報はfirebase(firestore)に保存しておく
- firestoreの情報を使ってNotificationを送る(とりあえずnodeスクリプト書く)
テストアプリの仕様
アプリ(Expo)側:許可+受け取り
以下のようにフォームとボタンを配置。
ボタンを押すとNotificationのための許可を求め、OKなら識別子と一緒にfirestoreに保存。
token取得方法の詳細は本家サイトを見る。
識別子は通常はFirebase Authenticationのuidとかになると思いますが、Auth実装がめんどいのでとりあえずユーザーを特定するための一意なキーとして利用。
サーバ側(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の定義をしています(サンプル後述)。
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は下記のような感じです。各自の環境に合わせて設定してください。
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する
というような流れを取るみたいです。
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' } ]
ひとまず以上です。