この記事はDeNA 25 新卒 Advent Calendar 2024の7日目の記事です!他の記事もぜひチェックしてみてください!
はじめに
初めまして、立命館大学4回生の藤堂ゆうかです。
サークルの後輩との開発で Web アプリケーションを作っていたのですが、通知に苦戦したため、Web アプリの通知についてまとめようと思いこの記事を書きました。
通知実装奮闘記として、ローカルネットワーク内の端末で通知テストが受信できるようになるまでの実装を紹介したいと思います。
実装環境
- Next.js
v15.1.3
(App Router)- React:
v19.0.0
- TypeScript:
v15.1.3
- React:
- Firebase Cloud Messaging(FCM)
- PWA
- iPhone16
- iOS
18.0.1
- iOS
この記事のゴール
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のプロジェクトを作成する。
プロジェクトの作成方法
※Googleアナリティクスの設定は任意です。必要に応じてしてください。
2. プロジェクトに Web アプリを追加
3. FCM の鍵ペアを生成
- 設定(⚙️)から「プロジェクトの設定」を開く
- 「Cloud Messaging」タブを押下
- 「Generate key pair」で鍵ペアを生成
生成できると、生成した鍵ペアが確認できます。これを、次の繋ぎ込みで使用します。
4. FCM の秘匿情報を env ファイルに記入
2.プロジェクトにWebアプリを追加で生成した apiKey
や、3.FCMの鍵ペアを生成で生成した鍵ペアを、env
ファイルに書き込みます。
# 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
は、後の通知設定で使用します。
"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つのカスタムフック、useFCM
・useFCMToken
・useNotificationPermissionStatus
を定義し、page.tsx
で呼び出します。
page.tsx
は、テンプレートの部分を削除し、useFCM
を呼び出します。
"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
ステートが更新されます。
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 トークンが取得されます。
"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 トークンの取得などにもこの許可が必要になるので、必ずユーザに通知の許可を求めるようにしましょう。
"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
を作成します。
// 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",
}
参考にしたのは下記サイトです。
動作確認
テストの送信方法
-
入力した FCM トークンの端末に通知が届いたら動作確認完了
PC ( Arc ) による動作確認
npm run dev
した状態で https://localhost:3000/ にアクセスすると、画面に FCM トークンが表示されます。それを Firebase の通知作成画面で打ち込んでテストを送信すると、通知が届いたことが確認できました。
テストボタン押下から実際の受信まではラグもなく一瞬でした。
iPhone16 による動作確認
同じ Wi-Fi に接続した PC でアプリケーションを起動し、起動時にターミナルに表示されるリンクに iPhone でアクセスします。「ホーム画面に追加」してからアプリとして起動します。
通知の許可が自動でうまくできなかったので、「通知の許可をとるボタン」を追加してそれを押下することで通知の許可に成功しました。
<button
onClick={() => {
Notification.requestPermission().then((permission) => {
alert(permission);
});
}}
>
allow notification
</button>
あとは PC と同様にトークンを取得して Firebase のダッシュボードで打ち込めば、テストが届きました。
Safari 内だと、常に「ReferenceError: Can't find variable: Notification」というエラーが出ますが、PWA の方を確認したら動作確認できたので今回は対応していません。
おわりに
通知のテストを飛ばしたはずなのにずっと届かなくて苦労したのですが、最終的な原因が PC 自体の設定でバナー表示させないようになっていただけということがありました。
様々なエラーも対処できていないので、デプロイ先の確認等も終わらせて [完全版]としてまた記事を出せたらいいなと思います。
また、「通知を許可することで、より効果的にアプリを活用できる」ことをユーザーに認識してもらうために、まず通知許可のモーダルを表示し、その後に本来の通知許可ポップアップに繋げることで、設定をよりスムーズに行ってもらいやすくなる、という工夫も見かけました。
サービスで扱う場合にはこちらにも対応したいです。