ExpoとCloud Functions for Firebaseで通知機能の実装

#はじめに
expoでメッセージ機能がついたアプリを作る際、通知機能は必須ということで、[Cloud Functions for Firebase](https://firebase.google.com/docs/functions?hl=ja)を使って通知機能を実装します。

#プッシュトークンを取得する
https://docs.expo.dev/push-notifications/push-notifications-setup/

↑を参考にユーザから通知の許可を取り、プッシュトークンを取得します。

```index.tsx
const registerForPushNotificationsAsync = async () => {
    if (Constants.isDevice) {
      const {
        status: existingStatus
      } = await Notifications.getPermissionsAsync();
      let finalStatus = existingStatus;
      if (existingStatus !== "granted") {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }
      if (finalStatus !== "granted") {
        //alert("プッシュ通知のプッシュトークンを取得できませんでした.");
        return;
      }
      //const token = (await Notifications.getExpoPushTokenAsync()).data;
      //console.log(token);
    } else {
      //alert("プッシュ通知には物理デバイスを使用する必要があります");
      return;
    }

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

#プッシュトークンを保存する
今回は複数端末に通知を送ることができるように、以下のようにプッシュトークンをCloud Firestoreに保存します。
ログイン時やログアウト時にプッシュトークンを追加・削除するようにしています。

```db.ts
export const db = firebase.firestore();

export const addPushToken = async (uid: string) => {
  if (Constants.isDevice) {
    const pushToken = await Notifications.getExpoPushTokenAsync();
    await db.collection("users").doc(uid).update({
      pushToken: firebase.firestore.FieldValue.arrayUnion(pushToken.data)
    });
  }
};

export const removePushToken = async (uid: string) => {
  if (Constants.isDevice) {
    const pushToken = await Notifications.getExpoPushTokenAsync();
    await db.collection("users").doc(uid).update({
      pushToken: firebase.firestore.FieldValue.arrayRemove(pushToken.data)
    });
  }
};
```

#メッセージ機能の実装
https://github.com/FaridSafi/react-native-gifted-chat

↑を使い、メッセージページを作りました。
```onSend```にて以下をFirestoreに保存します。

```
{
  id,
  uid,        // 送信者のuid
  friendId,   // 受信者のuid
  createdAt,
  text 　　　　// メッセージ内容
}
```

#Cloud Functionsの実装
新しいメッセージが保存されることをトリガーにしています。

https://firebase.google.com/docs/functions/firestore-events?hl=ja

https://github.com/expo/expo-server-sdk-node

↑を参考にしています。

メッセージ送信にて保存した受信者のidからpush tokenを取得し、送信者のidから送信者名を取得し、通知を送信します。

```index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const db = admin.firestore();

const { Expo } = require("expo-server-sdk");
const expo = new Expo();

exports.sendNotification = functions
  .region("asia-northeast1")
  .firestore.document("messages/{messageId}")
  .onCreate(async (snap, context) => {
    const messageData = snap.data();

    // 受信者のpush tokenの取得
    const recipientId = messageData.friendId;
    const recipientSnapshot = await db
      .collection("users")
      .doc(recipientId)
      .get();
    const recipientData = recipientSnapshot.data();
    if (!userData.pushToken) {
      functions.logger.log("push tokenを取得できませんでした。");
    } else {
      const pushTokenList = userData.pushToken;

      // 送信者の名前の取得
      const senderId = messageData.uid;
      const senderSnapshot = await db
        .collection("users")
        .doc(senderId)
        .get();
      const senderData = senderSnapshot.data();
      const senderName = senderData.name;

      // プッシュ通知用のメッセージオブジェクトを作成
      let messages = [];
      for (let pushToken of pushTokenList) {
        if (!Expo.isExpoPushToken(pushToken)) {
          functions.logger.error(
            `Push token ${pushToken} is not a valid Expo push token`
          );
        }
        messages.push({
          to: pushToken,
          title: senderName,
          body: messageData.text
        });
      }

      const chunks = expo.chunkPushNotifications(messages);
      const tickets = [];
      (async () => {
        // Expo Push API をリクエスト
        for (let chunk of chunks) {
          try {
            const ticketChunk = await expo.sendPushNotificationsAsync(chunk);
            tickets.push(...ticketChunk);
          } catch (error) {
            functions.logger.error(error);
          }
        }
      })();
    }
  });
```