はじめに
最近、Firebase Cloud Messaging(以下FCM)によるWebプッシュ通知の機能開発を試みた。のだが、色々あってあまりうまい具合に動かず、結局OneSignalを使う形で舵を切りなおした。が、Firebase Cloud Messagingを使った機能開発で培った試行錯誤のノウハウをそのまま失ってしまうのは惜しいと考え、ここに備忘録という形で供養する。誰かのためになればいいなと思います。
必要なPermission
Google CloudのAPI Keyの画面で、API Keyに付与するPermissionを選択することになると思うが、FCMでデバイストークンを発行する・そのデバイストークン向けにWebプッシュ通知を送信するために必要なPermissionは以下4点。これ以外は必要ない。ただ、「少なくともこのPermissionがあれば最低限動いた」という意味であって、この中にも不要なものがあるかもしれない。
- Firebase Management API
- FCM Registration API
- Firebase Cloud Messaging API
- Firebase Installations API
デバイストークンの発行、管理、その他
- PWA化は以下の記事が参考になりました。ありがとうございます。
https://qiita.com/Coa3/items/c81c8c7984bd5fc4ec69- なお、Next.jsのWebアプリをiOS向けにPWA化するだけなら、
manifest.json
やアイコン類をつくって/public
配下に格納しておくだけでよく、next-pwa
のインストールは必要ない。これは公式ドキュメントでもそんなようなことが書かれている(公式ドキュメントではapp/manifest.ts
を作るサンプルを提示しているが要するにmanifest.json
つくって配置するのと同じなのでまあ好みの問題だと思う)
https://nextjs.org/docs/app/building-your-application/configuring/progressive-web-apps
- なお、Next.jsのWebアプリをiOS向けにPWA化するだけなら、
- Webプッシュ通知に関しては以下の記事が参考になりました。ありがとうございます。
https://qiita.com/nanin/items/f3382a871eb04bf6828e- ただし必要なのは
firebase-messaging-sw.js
のみで、service-worker.js
はなくてもデバイストークン発行可能だった。
- ただし必要なのは
- デバイストークン発行するためにフロント側(Next.jsではClient-Components)でFirebaseのオブジェクトを生成することになるが、これにあたり指定が必要なフィールドは
apiKey
、projectId
、messagingSenderId
、appId
の4つだけで、他のプロパティ(authDomain
とstorageBucket
)は指定不要だった。それとgetToken
の引数で渡すvapidKey
が必要。つまり、部分的には省くが以下の実装でtoken
の変数にデバイストークンが発行されて格納される。const firebaseConfig = { apiKey: process.env['NEXT_PUBLIC_FIREBASE_API_KEY'], //authDomain: process.env['NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN'], projectId: process.env['NEXT_PUBLIC_FIREBASE_PROJECT_ID'], //storageBucket: process.env['NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET'], messagingSenderId: process.env['NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID'], appId: process.env['NEXT_PUBLIC_FIREBASE_APP_ID'], }; if ('serviceWorker' in navigator) { await navigator.serviceWorker.register('/firebase-messaging-sw.js'); } ... const app = initializeApp(firebaseConfig); const messaging = getMessaging(app); ... const token = await getToken(messaging, { vapidKey: process.env["NEXT_PUBLIC_FIREBASE_VAPID_KEY"] , serviceWorkerRegistration: await navigator.serviceWorker.ready, });
- 「FCMに登録されているデバイストークンの一覧」を取り出すためのAPIはないらしい。
https://stackoverflow.com/questions/43060846/
デバイストークンの保管は実装者の責任でやれということだそうだ。それは別にいいんだがFCMって際限なくデバイストークン発行可能ってわけでもないのではないのだろうか。調べられてない。- 使われてないトークンは270日で期限切れにするという記述はある。
https://firebase.google.com/docs/cloud-messaging/manage-tokens?hl=ja
- 使われてないトークンは270日で期限切れにするという記述はある。
- インストールしたPWAをアプリとして削除するとメッセージは届かなくなるがFCM SDKでの送信結果はsuccessになる。試してみた感じ、後述する送信データ形式不正の件も含め、「実際には届かないがSDKの実行結果は成功扱いになる」ケースはちょこちょこ起きる。多分FCM自身も最終的に「実機に届ける」部分までは把握しきれてないんじゃないか(SDKの範疇で実行に成功していればsuccessと返す仕様なんじゃないか)と予想する。個人の意見です。
FCMによるWebプッシュ通知
- デバイス単体に送信するなら
https://fcm.googleapis.com/v1/projects/[プロジェクトID]/messages:send
にcurl
なりfetch
なりすればよい。ここに書いてあります。→https://firebase.google.com/docs/reference/fcm/rest?hl=ja- ただ、上記の通り、これは「単体」にのみしか送信できない。昔の記事とか読むと
tokens
フィールドにstring
の配列でデバイストークン並べれば送信できるような記述もチラホラ見かけるが、少なくとも試した限りではそうしたことはできなかった。後述するSDK使うと内部的にはどうやらAPI呼んでるっぽいのでAPIのリソースにも「複数端末への送信」に相当するものがありそうな気がするが、とりあえず公式には公開されていない。複数端末への送信も、for
でグルグル回しながらその分このAPIコールすれば出来なくはない、が…
- ただ、上記の通り、これは「単体」にのみしか送信できない。昔の記事とか読むと
- 単発で複数端末に送信するなら
firebase-admin/messaging
インストールしてmessaging.sendEachForMulticast
を使う。ただしこれも最大500人のデバイストークンが対象となる。501以上渡すどうなるのかはわからない。こういう場合は500で配列区切ってループをネストするしかない、のかな?なお、昔の記事だとsendAll
とかsendMulticast
とか使う例が出てくるがこれは2024年12月現在どうやら廃止されている→https://firebase.google.com/docs/reference/admin/node/firebase-admin.messaging.messaging.md - メッセージのペイロードにあたる
BaseMessage
クラスは全部のフィールドが必須ではないからか、意味不明なオブジェクト渡してもSDKでの実行自体はエラーなく動いてしまう(「送信できた」としてレスポンスが返ってくる)。例えば以下のオブジェクトは実際には端末にはWebプッシュ通知としては届かないが、レスポンスは"success"
で返ってくる:これを実行するとconst admin = require('firebase-admin'); const {initializeApp, } = require('firebase-admin/app'); const registrationTokens = [ 'eLKZxsvF3jkwGqVn691234:AP...', ]; (async()=>{ try { const app = initializeApp({ credential: admin.credential.cert({ projectId: process.env['FIREBASE_PROJECT_ID'], clientEmail: process.env['FIREBASE_CLIENT_EMAIL'], privateKey: process.env['FIREBASE_PRIVATE_KEY'].replace(/\\n/g, '\n'), }), }); const messaging = getMessaging(app); // notificationとdataオブジェクトはbodyで囲う必要がない(そんな仕様はない)のでこれは送信データとしては形式不正 const messageForMultipleDevices = { body: { notification: { title: 'test title by sendMulticast', body: 'test body by sendMulticast:' + new Date().toUTCString(), }, data: { test: 'data.test by sendMulticast', }, }, tokens: registrationTokens, }; const r = await messaging.sendEachForMulticast(messageForMultipleDevices); console.log(`result:`); console.log(JSON.stringify(r)); } catch(error) { throw error; } })();
となって、正常に送信できたようなレスポンスを返してくるが、データ形式が不正なので(端末に届けるべき送信メッセージのペイロードが存在しないので)当然実機には何も届かない。ちなみにデータ形式としては以下が正解。(これで実機にWebプッシュ通知のメッセージが届いた実績があります。)result: {"responses":[{"success":true,"messageId":"projects/[project id]/messages/[uuid]"}],"successCount":1,"failureCount":0}
const messageForMultipleDevices = { notification: { title: 'test title by sendMulticast', body: 'test body by sendMulticast:' + new Date().toUTCString(), }, data: { test: 'data.test by sendMulticast', }, tokens: registrationTokens, };
- ドキュメントには特に明記されていないが、実際に何度か試してみたところでは、上記のように送信データに不正なオブジェクトを渡して送信すると(実際には届かないがSDKのレスポンスはsuccessになるような実行をすると)、どうもそのときに送信先に指定したデバイストークンがFCMの中でしれっと「無効」な扱いにされてる(以後、そのデバイストークンには正当なメッセージオブジェクトを渡したとしても通知が届かなくなる)気がする。実際、そのあとデバイストークン再発行したら別の値になった。ここは実体験によるものであり、仕様を言及するものではないですが、気になったので書いておきます。