RELATIONS Developers Blog

RELATIONS株式会社の開発ブログです。

ExpoとFirebaseを使って、サーバーレスで手軽にできるプッシュ通知配信を試しました

こんにちは、RELATIONS株式会社の大川です。
普段は Wistant の開発・運用をしているエンジニアです。

最近 Expo(React Nativeによるアプリ開発を支援するツール)とFirebase(DBやユーザー認証機能などアプリ開発でよくある機能を提供しているクラウドサービス)
を使い始めて、 Expoで作成したReact Nativeアプリのプッシュ通知を配信する仕組みを検証しています。

検証している仕組みの概要

検証用に、シンプルな掲示板アプリを作成しました。
ソースコードはこちらにあります。)
機能は以下の2つです。

  • メッセージを投稿できる
  • メッセージが投稿されると全員にプッシュ通知が届く

プッシュ通知を配信する仕組みは、Expoが提供している Expo Push API を利用しています。

Expo Push APIの特長は、iOS用の証明書設定や、Android用のサーバーキー設定を行わなくても、実際の端末でプッシュ通知の確認ができることです。

とても手軽ですが、デバイストークン(端末ごとにユニークなトークン)を管理するためのバックエンド開発が必要になります。

そこでメッセージとプッシュ通知を配信するためのデバイストークンの保存にCloud Firestore(Firebaseが提供しているデータベース。以下、Firestore)を利用し、メッセージが投稿されたタイミングで実行される処理を Cloud Functions for Firebase(クラウド上に保存したバックエンドのコードを実行できるサービス。以下、Cloud Functions)に実装しました。

メッセージとデバイストークンをCloud Firestoreに保存し、それをトリガーに起動されたCloud FunctionsでExpo Push APIをリクエストします
プッシュ通知配信のフロー

アプリの実装について

アプリでは、デバイストークンを端末から取得して Firestoreに保存するようにしています。

端末からデバイストークンを取得する部分は、ExpoのHPに公開されているドキュメントを参考に実装すると簡単です。

// デバイストークンを端末から取得して Firebase に保存する処理
const registerForPushNotificationsAsync = (navigation) => {
  return async () => {
    // プッシュ通知のパーミッションを取得
    const { status: existingStatus } = await Permissions.getAsync(
      Permissions.NOTIFICATIONS
    );
    let finalStatus = existingStatus;

    // すでにプッシュ通知が許可されていればなにもしない
    if (existingStatus !== "granted") {
      // (iOS向け) プッシュ通知の許可をユーザーに求める
      const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
      finalStatus = status;
    }

    // プッシュ通知が許可されなかった場合なにもしない
    if (finalStatus !== "granted") {
      return;
    }

    // Expo 用のデバイストークンを取得
    const token = await Notifications.getExpoPushTokenAsync();

    // Firebase にデバイストークンを保存するメソッドを呼び出す
    saveDeviceToken(token);

    // React Navigation で画面遷移
    navigation.navigate("Thread");
  };
};

途中で呼び出しているsaveDeviceTokenメソッドで、Firestoreのtokensコレクションにデバイストークンを保存しています。 (今回は検証目的なので、このメソッドが呼ばれる度にデバイストークンが追加されていく実装になっていますが、実際に運用するときは端末固有のIDやユーザー情報と紐づけた方が効率的です)

const saveDeviceToken = async (token) => {
  try {
    // Firestore にデバイストークンを保存
    const docRef = await db.collection("tokens").add({ token })
    console.log(`document written with ID:${docRef}`); // デバッグ用
  } catch (err) {
    console.error(`error: ${err}`);
  }
};

シミュレータで動作確認を行うと、プッシュ通知が許可されず処理されないので スマートフォンにExpoクライアントアプリをインストールしてから動作確認をしています。

うまく動作すれば、Firebase側にデバイストークンが保存されていることが確認できます。

FirebaseのWebコンソールでデバイストークンを確認
FirebaseのWebコンソールでデバイストークンを確認

Cloud Functionsの実装について

Cloud Functionsでは、データの保存をきっかけにして関数を実行できるので、メッセージが保存されたときにプッシュ通知を配信するようにしています。
(アプリとは別プロジェクトで、ソースコードはこちらにあります。)

const { Expo } = require('expo-server-sdk');
const functions = require('firebase-functions');
const admin = require('firebase-admin');

// Expo のインスタンスを用意
const expo = new Expo();

// Firestore のインスタンスを用意
admin.initializeApp();
const settings = { timestampsInSnapshots: true };
const db = admin.firestore();
db.settings(settings);

exports.sendNotification = functions.firestore
  .document('messages/{messageId}')
  .onCreate(async () => {
    // デバイストークンを Firestore から全検索
    const query = db.collection('tokens').where('token', '>', '');
    const snapshot = await query.get();
    const tokens = snapshot.docs.map(doc => doc.get('token'));

    // プッシュ通知用のメッセージオブジェクトを作成
    const messages = [];
    tokens.forEach(pushToken => {
      if (!Expo.isExpoPushToken(pushToken)) {
        console.error(`Push token ${pushToken} is not a valid Expo push token`);
      }

      messages.push({
        to: pushToken,
        sound: 'default',
        body: 'This is a test notification',
        data: { withSome: 'data' },
      });
    });
    const chunks = expo.chunkPushNotifications(messages);
    const tickets = [];
    (async () => {
      // Expo Push API をリクエスト
      for (let chunk of chunks) {
        try {
          const ticketChunk = await expo.sendPushNotificationsAsync(chunk);
          console.log(ticketChunk);
          tickets.push(...ticketChunk);
        } catch (error) {
          console.error(error);
        }
      }
    })();
  });

Expo Push APIの呼び出し部分は、前述のドキュメント を参考にしています。

今回は async / awaitをCloud Functions内で利用しているので、
functions/package.json で実行環境の nodeバージョンを8に指定しています。

"engines": {
  "node": "8"
},

また、 Cloud Functionsで外部APIをリクエストするためには有料プランに申し込む必要があるので、従量制プランにしています。
(ローカルで動作確認すると実行するとうまくいくのに、デプロイして動かすとエラーになったので少しつまずきました。)

端末を2台用意すれば、片方でメッセージを投稿し、もう片方でプッシュ通知が受信されるか確認できます。

プッシュ通知の受信を確認
プッシュ通知の受信を確認

まとめ

Expo Push APIとFirebaseを利用してプッシュ通知配信の仕組みをつくると、 アプリとCloud Functionsのロジック実装のみに集中できて、とても手軽にプッシュ通知の配信ができるようになりました。

引き続き以下の実装を行い、検証を進めていく予定です。

  • デバイストークンをユーザー情報に紐付ける
  • 配信できなかったデバイストークンの削除処理を追加する
  • 特定のユーザーにプッシュ通知を配信できるようにする

今後もよろしくお願いいたします。


(追記 2019/1/23 12:00) 続きをこちらに投稿しました。