FirebaseからブラウザにWEB Pushを送る

# はじめに

こんにちは、エンジニアのkeitaMaxです。

今回はFirebaseからブラウザにWEB Pushを送れるようにしようと思います。

https://firebase.google.com/docs/cloud-messaging/js/client?authuser=0&hl=ja&_gl=1*1yzmu92*_up*MQ..*_ga*OTEzNjcyOS4xNzE0MTgxNDE0*_ga_CW55HF8NVT*MTcxNDIwMTg0OS4yLjEuMTcxNDIwMTkyNS40Ny4wLjA.

これ記事を参考に進めていこうと思います。



# Firebaseの登録と設定

https://console.firebase.google.com/

Firebaseに登録し、プロジェクトの作成を行います。

![スクリーンショット 2024-04-27 10.37.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/9021be36-0aee-faf7-5ea8-bb973ca66ea3.png)

今回は`code-golf`という名前にしました。

画面の指示にある通りに入力をすると無事プロジェクトを作成することができました。

![スクリーンショット 2024-04-27 10.40.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/8eeb33a7-d216-51a6-50e0-ee9a1cd6f927.png)

### アプリの登録

WEBを選択し、ウェブアプリにFireFirebaseを追加します。

名前は`code-golf-web`にしました。

![スクリーンショット 2024-04-27 10.41.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/d1e1e5bd-57f7-e412-b109-498332d92dad.png)

![スクリーンショット 2024-04-27 10.46.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/b0720cf7-999b-a094-16c9-f3ce8e3c7edb.png)

すると以下のように、npm installしてコードを追加してくださいというような画面が出るので、WEB Pushを送りたいアプリでfirebaseのライブラリを以下コマンドでinstallします。

![スクリーンショット 2024-04-27 10.47.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/05918b2c-208c-9710-2ff6-7c10fe06f490.png)

```
npm install firebase
```

インストールが終わったら、画面に出てきたようにNext.js側のコードを修正します。

※firebase configは必ず保存しておいてください。

### Cloud Messagingの有効

歯車→プロジェクトの設定→Cloud Messaging→Cloud Messaging　API(レガシー）の３点→Google Cloud コンソールで管理
を順に押していきます。

![スクリーンショット 2024-04-27 10.58.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/b41b1b85-7671-744a-527a-762bdffa13ce.png)


そこでCloud Messaging を有効にします。

![スクリーンショット 2024-04-27 11.02.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/11b5d3f1-d59f-10ad-c731-b480a867813c.png)

Firebaseのプロジェクト設定の画面に戻ってくるとトークンが出ているかと思いますので、それも保存しておきます。


### ウェブプッシュ証明書の作成

最後に同じページの下にあるウェブプッシュ証明書のところで`Generate key pair`を押してウェブプッシュ証明書を作成します。

![スクリーンショット 2024-04-27 11.05.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/b75c2ab2-620b-2a06-e01e-16f18d871f5c.png)

これも重要なので保存しておいてください。


# 実装

next.jsで書くのは以下の記事がとてもわかりやすく書いてくれていました。

https://medium.com/simform-engineering/firebase-cloud-messaging-in-react-a-comprehensive-guide-b5e325452f97

.envファイルを用意して以下のように環境変数を設定しました。

```
NEXT_PUBLIC_APP_API_KEY=xxxxxxxxx
NEXT_PUBLIC_AUTH_DOMAIN=xxxxxxxxx
NEXT_PUBLIC_PROJECT_ID=xxxxxxxxx
NEXT_PUBLIC_STORAGE_BUCKET=xxxxxxxxx
NEXT_PUBLIC_MESSAGING_SENDER_ID=xxxxxxxxx
NEXT_PUBLIC_APP_ID=xxxxxxxxx
NEXT_PUBLIC_MEASUREMENT_ID=G-xxxxxxxxx
NEXT_PUBLIC_FCM_TOKEN=xxxxxxxxx
NEXT_PUBLIC_VAPID_KEY=xxxxxxxxx
```
変数名は`NEXT_PUBLIC_`から始めないといけないので注意してください。

そして記事にある通りに`index.tsx`と`hooks/firebase.ts`を以下のように実装しました。

```react:index.tsx
"use client"

import React, { useEffect } from "react"
import {  getToken, onMessage } from "firebase/messaging";
import { messaging } from "./hooks/firebase";
import { toast, ToastContainer } from "react-toastify";
const vapidKey: string | undefined =process.env.VITE_APP_VAPID_KEY ?? ''

export const RankingView = React.memo(function RankingView() {
  async function requestPermission() {
    const permission = await Notification.requestPermission();
    if (permission === "granted") {
      const token = await getToken(messaging, {
        vapidKey: vapidKey,
      });

      console.log("Token generated : ", token);
    } else if (permission === "denied") {
      alert("You denied for the notification");
    }
  }
  useEffect(() => {
    requestPermission();
  }, []);

  onMessage(messaging, (payload) => {
    toast(<Message notification={payload.notification} />);
  });
  
  return (
    <div className="w-fit rounded bg-red-500 px-4 py-2 text-white hover:bg-red-300">
      RankingView
      <ToastContainer />
    </div>
  )
})
const Message = ({ notification }:any) => {
  return (
    <>
      <div id="notificationHeader">
        {/* image is optional */}
        {notification.image && (
          <div id="imageContainer">
            <img src={notification.image} width={100} />
          </div>
        )}
        <span>{notification.title}</span>
      </div>
      <div id="notificationBody">{notification.body}</div>
    </>
  );
};

```

```ts:hooks/firebase.ts
import { initializeApp } from "firebase/app";

import { getMessaging } from "firebase/messaging";
const apiKey: string | undefined = process.env.NEXT_PUBLIC_APP_API_KEY ?? ''
const authDomain: string | undefined =process.env.NEXT_PUBLIC_AUTH_DOMAIN ?? ''
const projectId: string | undefined =process.env.NEXT_PUBLIC_PROJECT_ID ?? ''
const storageBucket: string | undefined =process.env.NEXT_PUBLIC_STORAGE_BUCKET ?? ''
const messagingSenderId: string | undefined =process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID ?? ''
const appId: string | undefined =process.env.NEXT_PUBLIC_APP_ID ?? ''
const measurementId: string | undefined =process.env.NEXT_PUBLIC_MEASUREMENT_ID ?? ''

const firebaseConfig = {
  apiKey: apiKey,
  authDomain: authDomain,
  projectId: projectId,
  storageBucket: storageBucket,
  messagingSenderId: messagingSenderId,
  appId: appId,
  measurementId: measurementId,
};

const app = initializeApp(firebaseConfig);

export const messaging = getMessaging(app);
```

### 困ってやったこと１

`npm run dev`で見た時の以下のエラーが発生してしまいました。

```
FirebaseError: Messaging: We are unable to register the default service worker. Failed to register a ServiceWorker for scope ('http://localhost:3000/firebase-cloud-messaging-push-scope') with script ('http://localhost:3000/firebase-messaging-sw.js'): A bad HTTP response code (404) was received when fetching the script. (messaging/failed-service-worker-registration).
```

原因はどうやらHTTPSで立ち上げていないことが原因のよ一つうでしたので、HTTPSで立ち上げました。

Next.jsをHTTPSで立ち上げる方法は以下の記事で紹介しているので参考にしてください。

https://qiita.com/keitaMax/items/5d393cbdf68392606544


### 困ってやったこと２

これをしてもエラーは消えてくれなくて、調べてみるとpublicに`firebase-messaging-sw.js`というファイルをおかなくてはいけないということでした。

中身は公式のものそのままでいいということだったので以下を置きました。

https://github.com/firebase/quickstart-js/tree/master/messaging

上のURLが公式なので参考にしてください。

https://qiita.com/stake15/items/8938dd1ee5fa8f15225c

この記事を参考にしました。

```js:firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/5.7.3/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.7.3/firebase-messaging.js');

firebase.initializeApp({
  'messagingSenderId': 'xxxxxxxx' //自分のmessagingSenderIdにしてください
});

const messaging = firebase.messaging();

messaging.setBackgroundMessageHandler((payload) => {
  console.log('[firebase-messaging-sw.js] Received background message ', payload);
  const notificationTitle = 'Background Message Title';
  const notificationOptions = {
    title: 'push-test-web',
    body: 'push-test-web: Background Message body',
    icon: '/favicon.ico',
  };

  return self.registration.showNotification(notificationTitle,
    notificationOptions);
});
```

### 困ってやったこと３

それでもまだ以下のエラーがコ出てしまっていました。

```
FirebaseError: Messaging: This browser doesn't support the API's required to use the Firebase SDK. (messaging/unsupported-browser).
    at eval (webpack-internal:///(ssr)/./node_modules/@firebase/messaging/dist/esm/index.esm2017.js:1069:33) {
  code: 'messaging/unsupported-browser',
  customData: {}
}
```

そのため、処理が書いてあるところを`Notification`というコンポーネントにして、ダイナミックインポートをしました。以下を参考に修正しました。

https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading

`index.tsx`と`Notification/index.tsx`を以下のようにしました。

```react:index.tsx
"use client"

import dynamic from "next/dynamic"
import React from "react"
const Notification = dynamic(() => import( './components/Notification'))

export const RankingView = React.memo(function RankingView() {
  return (
    <>
      <div>
        <div>
          <p className="text-black">
            らんきんぐ画面
          </p>
        </div>
        <Notification />
      </div>
    </>
  )
})

```
```react:Notification/index.tsx
"use client"

import React, { useState, useEffect } from "react";
import { onMessageListener, requestForToken } from "../../hooks/firebase";

const Notification = () => {
  const [notification, setNotification] = useState({ title: "", body: "" });
  useEffect(() => {
    if (notification?.title) {
      alert("title: " + notification?.title + "\nbody: " + notification?.body);
    }
  }, [notification]);

  requestForToken()

  onMessageListener()
  .then((payload:any) => {
      setNotification({ title: payload?.notification?.title, body: payload?.notification?.body });
    })
    .catch((err:any) => console.log("failed: ", err));

  return <div />;
};

export default Notification;
```

これでもまだ色々えらー出ているので困りました。


# 困ってまだ解決できていないこと

まだ以下のエラーが出てしまって困っています。
```
Error: Cannot read properties of undefined (reading 'addEventListener')
```

まだ修正しきれていないので、これの解決は次の記事にしようと思います。


・バージョン
Chrome : 124.0.6367.93
Next.js : 14.2.3
react : 18.3.1
npm : 10.5.0

# おわりに

思っていたより難しく詰まってしまいました。
まだ解決できていない問題もあるので、しっかり調べて解決したいと思います。
もし解決策わかる方がいらっしゃいましたら教えていただけると幸いです。

この記事での質問や、間違っている、もっといい方法があるといったご意見などありましたらご指摘していただけると幸いです。

最後まで読んでいただきありがとうございました！


# 参考

https://next-code.jp/blog/tech/firebase/fcm%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6php%E3%81%8B%E3%82%89web%E3%83%97%E3%83%83%E3%82%B7%E3%83%A5%E3%82%92%E9%80%81%E3%82%8B/


https://medium.com/simform-engineering/firebase-cloud-messaging-in-react-a-comprehensive-guide-b5e325452f97


https://qiita.com/stake15/items/8938dd1ee5fa8f15225c

https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading