0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DeNA 25 新卒Advent Calendar 2024

Day 7

Next.js(PWA)とFCMによるWeb通知の実装ー通知テストを受け取るまでー

Last updated at Posted at 2025-01-26

この記事はDeNA 25 新卒 Advent Calendar 2024の7日目の記事です!他の記事もぜひチェックしてみてください!

はじめに

初めまして、立命館大学4回生の藤堂ゆうかです。

サークルの後輩との開発で Web アプリケーションを作っていたのですが、通知に苦戦したため、Web アプリの通知についてまとめようと思いこの記事を書きました。

通知実装奮闘記として、ローカルネットワーク内の端末で通知テストが受信できるようになるまでの実装を紹介したいと思います。

実装環境

  • Next.js v15.1.3 (App Router)
    • React: v19.0.0
    • TypeScript: v15.1.3
  • Firebase Cloud Messaging(FCM)
  • PWA
  • iPhone16
    • iOS 18.0.1

この記事のゴール

Mac のローカル環境でアプリケーションを起動し、以下を確認できる状態を目指します。

  • localhost での通知テストの受信
  • iPhone16 (外部デバイス)での PWA を使用した通知テストの受信

実装

実装をするにあたって、全般的な流れは次の公式サイトを、

Next.js におけるフォルダ構成やコードの記述方法は次のリポジトリを参考にしました。

1. Next.js のアプリケーションを立ち上げる

下記コードをターミナルで実行する。

npx create-next-app@latest
各オプションは下記の通り
✔ What is your project named? … notification-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
Creating a new Next.js app in /Users/yuuka1120/Documents/notification-app.

2. Firebaseのプロジェクトを作成し、設定を行う

1. Firebaseのプロジェクトを作成

FirebaseのコンソールからFirebaseのプロジェクトを作成する。

プロジェクトの作成方法
  1. 「プロジェクトを作成する」ボタンを押す
    プロジェクトを作成ボタン

  2. プロジェクト名を記入して「続行」
    プロジェクト作成画面

  3. 「続行」※
    Googleアナリティクス設定画面

  4. Default Account for Firebase を選択して「プロジェクトを作成」
    スクリーンショット 2025-01-06 15.58.57.png

※Googleアナリティクスの設定は任意です。必要に応じてしてください。

2. プロジェクトに Web アプリを追加

Web アプリの追加方法
  1. Web アプリを選択する
    プロジェクトのダッシュボード画面

  2. アプリ名を記入する
    アプリ名を記入画面

  3. apiKey などを記録しておく
    firebaseの初期化コード

3. FCM の鍵ペアを生成

公式サイトの生成方法

鍵ペアの生成方法

  1. 設定(⚙️)から「プロジェクトの設定」を開く
  2. 「Cloud Messaging」タブを押下
  3. 「Generate key pair」で鍵ペアを生成

生成された秘密鍵の確認

生成できると、生成した鍵ペアが確認できます。これを、次の繋ぎ込みで使用します。

4. FCM の秘匿情報を env ファイルに記入

2.プロジェクトにWebアプリを追加で生成した apiKey や、3.FCMの鍵ペアを生成で生成した鍵ペアを、env ファイルに書き込みます。

.env.local
# Webアプリ作成時にアプリ
NEXT_PUBLIC_FIREBASE_APP_ID=*****
NEXT_PUBLIC_FIREBASE_API_KEY=*****
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=*****
NEXT_PUBLIC_FIREBASE_PROJECT_ID=*****
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=*****
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=*****
# 作成した鍵ペア
NEXT_PUBLIC_FIREBASE_VAPID_KEY=*****

5. Firebase のインストール

ターミナルで、npm を使用して Firebase をインストールします。

npm install firebase

6. firebase.ts の作成

Firebase の初期化を行います。加えて、firebase/messaging から getMessaging をインポートし、初期化した firebaseApp を引数に渡して messaging としてエクスポートします。この messaging は、後の通知設定で使用します。

app/utils/firebase/index.ts
"use client";
import { type FirebaseOptions, initializeApp } from "firebase/app";
import { getMessaging } from "firebase/messaging";

const firebaseConfig: FirebaseOptions = {
  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,
};

const firebaseApp = initializeApp(firebaseConfig);
export const messaging = () => getMessaging(firebaseApp);
export default firebaseApp;

7. 通知関連のカスタムフック実装

3つのカスタムフック、useFCMuseFCMTokenuseNotificationPermissionStatus を定義し、page.tsx で呼び出します。

page.tsx は、テンプレートの部分を削除し、useFCM を呼び出します。

app/page.tsx
"use client";
import useFCM from "@/app/utils/hooks/useFCM";

export default function Home() {
  const { messages, fcmToken } = useFCM();
  console.log(`messages:`, messages);
  return (
    <div>
      <p>fcmToken: {fcmToken}</p>
      <p>messages: {JSON.stringify(messages)}</p>
    </div>
  );
}

useFCM では、FCM メッセージの受信を管理します。メッセージが受信されると、messages ステートが更新されます。

app/utils/hooks/useFCM.tsx
import { useEffect, useState } from "react";
import useFCMToken from "@/app/utils/hooks/useFCMToken";
import { messaging } from "@/app/utils/firebase";
import { MessagePayload, onMessage } from "firebase/messaging";

const useFCM = () => {
  const fcmToken = useFCMToken();
  const [messages, setMessages] = useState<MessagePayload[]>([]);
  useEffect(() => {
    if ("serviceWorker" in navigator) {
      const fcmMessaging = messaging();
      const unsubscribe = onMessage(fcmMessaging, (payload) => {
        setMessages((messages) => [...messages, payload]);
      });
      return () => unsubscribe();
    }
  }, [fcmToken]);
  return { fcmToken, messages };
};

export default useFCM;

useFCMToken では、FCM トークンを取得し、管理します。通知の許可状態が granted (付与)のときのみ、FCM トークンが取得されます。

app/utils/hooks/useFCMToken.tsx
"use client";
import { useEffect, useState } from "react";
import { getToken, isSupported } from "firebase/messaging";
import { messaging } from "@/app/utils/firebase";
import useNotificationPermission from "@/app/utils/hooks/useNotificationPermission";

const useFCMToken = () => {
  const permission = useNotificationPermission();
  const [fcmToken, setFcmToken] = useState<string | null>(null);
  useEffect(() => {
    const retrieveToken = async () => {
      if (typeof window !== "undefined" && "serviceWorker" in navigator) {
        if (permission === "granted") {
          const isFCMSupported = await isSupported();
          if (!isFCMSupported) return;
          const fcmToken = await getToken(messaging(), {
            vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
          });
          setFcmToken(fcmToken);
        }
      }
    };
    retrieveToken();
  }, [permission]);
  return fcmToken;
};

export default useFCMToken;

useNotificationPermissionStatusでは、通知の許可状態を管理します。Notification.requestPermission() を使用してユーザーに通知の許可を求め、許可された場合に通知を送れるようになります。FCM トークンの取得などにもこの許可が必要になるので、必ずユーザに通知の許可を求めるようにしましょう。

app/utils/hooks/useNotificationPermissionStatus.tsx
"use client";
import { useEffect, useState } from "react";

const useNotificationPermissionStatus = () => {
  const [permission, setPermission] =
    useState<NotificationPermission>("default");
  useEffect(() => {
    const handler = () => setPermission(Notification.permission);
    handler();
    Notification.requestPermission().then(handler);
    navigator.permissions
      .query({ name: "notifications" })
      .then((notificationPerm) => {
        notificationPerm.onchange = handler;
      });
  }, []);
  return permission;
};

export default useNotificationPermissionStatus;

8. firebase-messaging-sw.js の作成

publicフォルダ下に firebase-messaging-sw.js を作成します。

public/firebase-messaging-sw.js
// eslint-disable-next-line no-undef
importScripts("https://www.gstatic.com/firebasejs/8.8.0/firebase-app.js");
// eslint-disable-next-line no-undef
importScripts("https://www.gstatic.com/firebasejs/8.8.0/firebase-messaging.js");
const firebaseConfig = {
  apiKey: "*****",
  authDomain: "*****",
  projectId: "*****",
  storageBucket: "*****",
  messagingSenderId: "*****",
  appId: "*****",
  measurementId: "*****",
};
// eslint-disable-next-line no-undef
firebase.initializeApp(firebaseConfig);
// eslint-disable-next-line no-undef
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
  console.log(
    "[firebase-messaging-sw.js] Received background message ",
    payload
  );
  const notificationTitle = payload.notification.title;
  const notificationOptions = {
    body: payload.notification.body,
    icon: "./logo.png",
  };
  self.registration.showNotification(notificationTitle, notificationOptions);
});

ここには apiKey などを直接書き込みます。 GitHubに上げてはいけない部分なので、そのままプッシュすべきではないのですが、今のところ最適解が見つかっていないので、今後見つけて [完全版] として記事にする予定です。

9. PWA の設定

作成中のアプリケーションを PWA 化します。
下記記事を参考に実装しました。

manifestファイルの作成だけは記事内で紹介されているジェネレーターではなく、下記サイトで作成しました。

9. ローカル環境のアプリ起動時に HTTPS を指定

ここまでの設定で localhost の動作確認は取れたのですが、iPhone からでは FCM トークンがうまく取得できませんでした。( permission が強制的に "denied" になっていた)

その原因は、「アプリが HTTP で起動していたから」というものでした。よって、HTTPS で起動するように npm run dev コマンドにオプションを追加して、ローカル環境で HTTPS を使用できるように設定しました。

  "scripts": {
-    "dev": "next dev",
+    "dev": "next dev --experimental-https",
  }

参考にしたのは下記サイトです。

動作確認

テストの送信方法

  1. ダッシュボードの Messaging から「最初のキャンペーンを作成」
    プロジェクトのダッシュボード画面(Messagingタブ選択時)

  2. 「Firebase Notification メッセージ」を選択して「作成」
    通知の種類選択画面

  3. 通知のタイトル・通知テキストを入力して「テスト メッセージを送信」
    通知の作成画面

  4. 取得した FCM トークンを入力(または選択)し「テスト」
    FCMトークン選択画面

  5. 入力した FCM トークンの端末に通知が届いたら動作確認完了

PC ( Arc ) による動作確認

npm run dev した状態で https://localhost:3000/ にアクセスすると、画面に FCM トークンが表示されます。それを Firebase の通知作成画面で打ち込んでテストを送信すると、通知が届いたことが確認できました。
テストボタン押下から実際の受信まではラグもなく一瞬でした。

Arcに届いた通知

iPhone16 による動作確認

同じ Wi-Fi に接続した PC でアプリケーションを起動し、起動時にターミナルに表示されるリンクに iPhone でアクセスします。「ホーム画面に追加」してからアプリとして起動します。

通知の許可が自動でうまくできなかったので、「通知の許可をとるボタン」を追加してそれを押下することで通知の許可に成功しました。

app/page.tsx
      <button
        onClick={() => {
          Notification.requestPermission().then((permission) => {
            alert(permission);
          });
        }}
      >
        allow notification
      </button>

あとは PC と同様にトークンを取得して Firebase のダッシュボードで打ち込めば、テストが届きました。
スマホに届いた通知

警告「接続はプライベートではありません」について 接続はプライベートではありませんという警告が出ますが、「詳細を表示」→「このWebサイトを閲覧」で確認できました。
警告画面 詳細画面
接続はプライベートではありませんという警告画面 接続はプライベートではありませんのエラー詳細画面

Safari 内だと、常に「ReferenceError: Can't find variable: Notification」というエラーが出ますが、PWA の方を確認したら動作確認できたので今回は対応していません。

おわりに

通知のテストを飛ばしたはずなのにずっと届かなくて苦労したのですが、最終的な原因が PC 自体の設定でバナー表示させないようになっていただけということがありました。
様々なエラーも対処できていないので、デプロイ先の確認等も終わらせて [完全版]としてまた記事を出せたらいいなと思います。

また、「通知を許可することで、より効果的にアプリを活用できる」ことをユーザーに認識してもらうために、まず通知許可のモーダルを表示し、その後に本来の通知許可ポップアップに繋げることで、設定をよりスムーズに行ってもらいやすくなる、という工夫も見かけました。
サービスで扱う場合にはこちらにも対応したいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?