iOSに対応したWebプッシュ通知をFirebase Cloud Messagingを使って実装

2023-04-19

はじめに

iOS 端末でも iOS16.4 より、とうとう Web プッシュ通知に対応しました。これで、主要デバイスのプッシュ通知対応が完了しました。これまでは、あまり(多分)はやってなかった Web プッシュ通知の実装も加速しそうです。

そんな、Web プッシュ通知時代到来に備えるために、React と TypeScript を使ってFirebase Cloud Messaging(FCM)経由で受信する Web サイトを作成します。

今回実装したコードはこちらのリポジトリで公開しています。

実装

React と TypeScript を使って実装するためのプロジェクトの雛形を作成します。

雛形を作るコマンド
$ npx create-react-app web-push-mobile --template typescript

Firebase のライブラリをインストールします。

firebaseライブラリをインストール
$ npm install firebase

受信編

iOS ではプッシュ通知を受信するために PWA に対応しなければいけないので必要に応じてmanifest.jsonに設定を加えます(自分はプロジェクト作成時にあったものをそのまま使っています)。iOS 対応で特に重要なのは、displayの設定みたいです(参考)。

manifest.json
{

  "short_name": "React App",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

次に Service Worker の作成をします。Service Worker とは、ざっくり言うとプロキシサーバーのように動作するもののことみたいです(参考)。Service Worker 経由でプッシュ通知やバックブランド同期の API へアクセスすることができるようでした。

firebase-messaging-sw.jsという名前で、Web 上で公開するディレクトリの直下に置きます。今回は、/publicに設置しました。公式ドキュメントを参考に設定します。initializeAppは Firebase 上でプロジェクト作成時に設定を作ってくれるのでそのままコピペしました。

firebase-messaging-sw.js
importScripts("https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js");
importScripts(
  "https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js"
);

firebase.initializeApp({
  apiKey: "AIzaSyDewXQrloq4dzwUq4HzhAL3gVsmRHoCY80",
  authDomain: "web-push-mobile.firebaseapp.com",
  projectId: "web-push-mobile",
  storageBucket: "web-push-mobile.appspot.com",
  messagingSenderId: "516158597738",
  appId: "1:516158597738:web:712c3ee4fc38fd5c8bf813",
});

self.addEventListener("push", (event) => {
  const json = event.data.json();
  const title = json.notification.title;
  const body = json.notification.body;
  self.registration.showNotification(title, {
    body: body,
  });
});

Firebase のサンプルから一部変更しています。addEventListenerの箇所です。Firebase のライブラリが提供している実装を試してもいいのですが、バックブランドとフォアグラウンド両方に対応しようとするとそれぞれの処理を書かないといけません。今回はバックブランドでもフォアグラウンドでも同じように動作してほしかったので、一つにまとめて書けるこちらを採用しました。詳しい解説はMDN の PushEventにあります。

最初はプッシュ通知を表示するために、Notificationを使っていたのですがどうやらモバイルのブラウザだとServiceWorkerRegistration.showNotification()で表示しないとだめみたいでした(ブラウザ版だと正しく動作しているように見受けられました)。実際に Android の Chrome でNotificationを呼び出していると以下のようなエラーが出ていました(iOS の Safari で動かしたときのログは見れてないのですが、プッシュ通知のポップアップが表示されなかったのでおそらく同様のエラーがでていそうでした)。

エラー内容
caught (in promise) TypeError: Failed to construct 'Notification': Illegal constructor. Use ServiceWorkerRegistration.showNotification() instead.
    at e.onMessageHandler (A...

Firebase のコンソールから一括送信もできるのですが、特定のデバイスに対して送信できるようにデバイストークンの発行もします。

a.ts
import { FirebaseApp, initializeApp } from "firebase/app";
import { getMessaging, getToken } from "firebase/messaging";

const app: FirebaseApp = initializeApp({
  apiKey: "AIzaSyDewXQrloq4dzwUq4HzhAL3gVsmRHoCY80",
  authDomain: "web-push-mobile.firebaseapp.com",
  projectId: "web-push-mobile",
  storageBucket: "web-push-mobile.appspot.com",
  messagingSenderId: "516158597738",
  appId: "1:516158597738:web:712c3ee4fc38fd5c8bf813",
});

export const RequestNotificationPermission = async () => {
  const messaging = getMessaging(app);
  try {
    const token: string = await getToken(messaging, {
      vapidKey:
        "BByJOdMjroC5aEwmWOCNxHkr6ftTYp6xR9nUYIMl1wAhOjy-ChZsKavLBWVfhYO777LvQZN3Gh0kj9Ch-rH6Qx4",
    });
    if (token != null) {
      console.log(`Notification token: ${token}`);
      return token;
    } else {
      console.log(`トークンがNullです。`);
      throw new Error("トークンがNullです。");
    }
  } catch (error) {
    console.error(`Errorです。 ${error}`);
    throw error;
  }
};

getTokenでユーザに対してプッシュ通知の許諾を取るポップアップを表示します。返り値としてデバイストークンが発行されるのでこれを使って送信します。

送信編

いちいち curl で送るのも面倒なのでブラウザ上でデバイストークンを指定して送るようにもしてみました。(API キーが載っているので検証で試した後はすぐにプロジェクトを消すなどした方が良いです)

axios をインストールします。

axiosライブラリをインストール
$ npm install axios

Firebase にプッシュ通知を送るための共通設定を axios で設定します。今回は旧 API(HTTP プロトコル)経由でプッシュ通知を送信します。

axios.ts
import axios from "axios";

// 旧APIの方を使います。(HTTP v1ではない)
const instance = axios.create({
  baseURL: "https://fcm.googleapis.com",
  headers: {
    "Content-Type": "application/json",
    Authorization:
      "key=AAAAeC1zKmo:APA91bEvvh-dBAOVACqGnKiasNgkToVOPGz9iAcylWukKW4FGqK5wLB4xxUEOp-AJaowJtfcRq0TOCMECdyQHMchMoz4rQT0UvqzUo77fHW4V2Tkd2KGyG1EF29Df2srElDEWLAhaLfC",
  },
});

export default instance;

実際に送信をするコードです。

SendPushNotification.ts
import axios from "./axios/axios";

const requestBody = (title: string, body: string, token: string) => {
    return  {
      to: `${token}`,
        notification: {
          title: `${title}`,
          body: `${body}`,
        },
    };
  };

export const SendPushNotification = async (
  title: string,
  body: string,
  token: string
) => {
    try {
        const response = await axios.post("/fcm/send", requestBody(title, body, token))
        console.log(response)
    } catch(error) {
        console.log(error)
    }
};

requestBodyにプッシュ通知を送るためのパラメータを設定します。詳しくはこちらに載っています。 送信する際のエンドポイントがhttps://fcm.googleapis.com/fcm/sendになるので.post("/fcm/send")を指定します。

ボタンをクリックしたら送信するようにします。


export const RequestNotificationPermissionLayout: React.FC = () => {
  const [pushNotificationToken, setToken] = useState("");
  const [title, setTitle] = useState("");
  const [body, setBody] = useState("");

  const sendPush = async () => {
    try {
      await SendPushNotification(title, body, pushNotificationToken);
    } catch (error) {
      console.log(error);
    }
  };
  return (
    <>
     ...
      <Button onClick={sendPush}>プッシュ通知を送信</Button>
     ...
    </>
  );
};
Tatsumi0000

Written by Tatsumi0000 モバイル開発が好きなエンジニアのブログです. GitHub

Copyright © 2023, Tatsumi0000 All Rights Reserved.