はじめに
今回は Firebase Cloud Messaging(FCM)によるプッシュ通知を本番運用するうえで考えるべき課題の中から「許可取得」にフォーカスして、具体的な設計と実装を深掘りしていきます。
せっかく自分のサイトにプッシュ通知を導入するなら、できるだけ多くのユーザーに許可してもらいたいですよね。
自分の送った通知がたくさんのユーザーに届いて、実際にサイトに来てもらえたら嬉しいですよね。
ただ、許可フローの設計を雑にやると、許可してもらえるはずだったユーザーを取りこぼします。
それどころか、一度拒否されたら二度と通知を届けられなくなる可能性すらあります。
この記事では、ブラウザごとの許可の仕組みの違いを整理したうえで、拒否リスクを抑える「2段階の許可フロー」の設計と実装を見ていきます。
対象読者: 自分のサイトにFCMでプッシュ通知を導入しようとしている、またはチュートリアルは動かしたが本番運用に向けて許可フローを見直したいエンジニア
1. なぜ許可フローの設計が重要なのか
プッシュ通知を受け取るかどうかは、ブラウザが表示する「許可ダイアログ」でユーザーが判断します。
これは Chrome の例です。見た目はブラウザによって異なりますが、いずれも OS やブラウザが提供するもので、「ネイティブダイアログ」とも呼ばれます。
このダイアログで「拒否」されると、サイト側から再表示する手段がありません。
つまり、ダイアログを出すタイミングを間違えると、そのユーザーとの接点を失うことになります。
この記事では、そうならないための具体的な設計と実装を見ていきます。
2. ブラウザやデバイスごとの許可の仕組み
「FCMのチュートリアル通りに Notification.requestPermission() を呼んだのに、Firefoxだと許可ダイアログが出ない」
――こんな問題にぶつかるのは、許可ダイアログを出すための条件が端末やブラウザによってかなり違うからです。
以下は筆者が各ブラウザで検証した結果です。バージョンによって動作が異なる場合があります。
PC
| ブラウザ | バージョン | ユーザー操作なしでの requestPermission()
|
|---|---|---|
| Chrome | 147.0.7727.103 | ダイアログが表示される(※1) |
| Edge | 147.0.3912.72 | Quieter UI のみ表示される |
| Firefox | 150.0 | Quieter UI のみ表示される |
| macOS Safari | 18.6 | ユーザー操作が必要である旨のコンソールエラーが出力される |
スマホ
| ブラウザ | バージョン | ユーザー操作なしでの requestPermission()
|
|---|---|---|
| Chrome | 147.0.7727.101 | ダイアログが表示される(※1) |
| Edge | 146.0.3856.97 | 「通知がブロックされました」と表示される(※2) |
| Firefox | 152.0a1 | ユーザー操作が必要である旨のコンソールエラーが出力される |
| iOS PWA | 26.2.1 | ユーザー操作が必要である旨のコンソールエラーが出力される |
※1 複数回無視されると自動ブロックや Quieter UI に切り替わる
※2 複数回無視されると表示されなくなる
Quieter UI とは?
環境によってはユーザー操作なしで requestPermission() を呼ぶと、通常のダイアログの代わりに小さなUIで許可を促す「Quieter UI」が表示されます。
FirefoxのQuieter UI

EdgeのQuieter UI

さりげなく表示されるので、ユーザーはスルーしてしまうことが多く、実質的に許可を取れないのと同じなんですよね。
Chrome では、許可ダイアログが複数回無視されると自動でブロックされてしまう可能性があります。
こうなると denied と同じ扱いなので、サイト側からはもう許可を求められません。
さらに、多くのユーザーから通知がブロックされているサイトでは、初めて訪問したユーザーに対しても Chrome が自動的に通知をブロックすることがあります。
Firefox や Edge、macOS Safari はユーザー操作がないとダイアログが出ないので、ボタンクリックなどを起点に requestPermission() を呼ぶ必要があります。
また、macOS Safari には許可状態の検知にも罠があるので、4. 許可状態の検知 ― Safari の罠で詳しく触れます。
iOS はさらに特殊で、通常の Safari ではプッシュ通知自体が使えません。
ホーム画面に追加した PWA でのみサポートされているので、iOS ユーザーに届けたい場合はまず PWA 化とホーム画面追加への誘導から考える必要があります。
3. 2段階の許可フロー
ここまで見てきたように、ブラウザごとに制約はバラバラで、しかもネイティブダイアログで拒否されたらやり直しがほぼきかない。なかなか厳しい状況です。
これらの問題をまとめて解決できるのが、ネイティブダイアログの前に独自の確認UIを挟むというアプローチです。
こういったポップアップを見かけたことがあるのではないでしょうか。これが独自の確認UIの一例です。
なぜ2段階にするのか
- 拒否のリスクを下げる: 独自ダイアログの段階で興味のないユーザーが離脱するので、ネイティブダイアログに到達するのは「受け取りたい」と思っているユーザーだけになる
- ユーザー操作を起点にできる: 独自ダイアログのボタンクリックをトリガーにすれば、Firefox や Edge、Safari でもネイティブダイアログを表示できる
- 通知の価値を伝えられる: 「新着記事をお知らせします」など、ユーザーにとってのメリットを説明してから許可を求められる
- 「後で」という選択肢を作れる: ネイティブダイアログには「許可」と「ブロック」しかないが、独自ダイアログに「後で」ボタンを置けば、今は興味がないユーザーにも次回以降に再度アプローチできる
基本的なフローの例
フローを見る前に、Notification.permission が返す3つの状態を押さえておきましょう。
| 値 | 意味 |
|---|---|
"default" |
まだ許可も拒否もしていない(ダイアログを出せる) |
"granted" |
許可済み(通知を送れる) |
"denied" |
ブロック済み(サイト側から再度許可を求められない) |
ユーザーがサイトに訪問
│
▼
許可状態を確認
├─ granted → 通知の配信準備完了(※)
│
▼ (default)
独自の確認ダイアログを表示
│ 「新着記事の通知を受け取りますか?」
│
├─ ユーザーが「受け取る」をクリック
│ │
│ ▼
│ Notification.requestPermission() を呼ぶ
│ └─ granted → 通知の配信準備完了(※)
│
└─ ユーザーが「後で」をクリック
│
▼
次回表示までの間隔を記録して終了
※ 許可が取れたあとは、FCM トークンを取得してサーバーに送信する処理が必要です。詳しくは次回の記事で解説します。
「後で」を選んだユーザーに毎回ダイアログを出すのはよくないので、「次回表示可能日時」を localStorage に記録して、それまではダイアログを出さないようにします。
間隔は24時間くらいから始めて、サイトの特性に合わせて調整するとよいです。
実装例
ここでは HTML の <dialog> 要素を使う例を示します。
※サンプルコードのため、エラーハンドリング等は省略しています。
<dialog id="dialog-confirm">
<p>新着記事の通知を受け取りますか?</p>
<button id="dialog-confirm-accept">受け取る</button>
<button id="dialog-confirm-dismiss">後で</button>
</dialog>
この HTML に対応する TypeScript です。上から読んでいけばフローがわかるように並べています。
// --- 定数 ---
const CONFIRM_COOLDOWN_KEY = "confirm_next_show_at";
const CONFIRM_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24時間
// --- エントリーポイント ---
// ページ読み込み時やユーザーの特定のアクション後に呼び出す
document.addEventListener("DOMContentLoaded", runConfirmFlow);
async function runConfirmFlow(): Promise<void> {
if (!shouldShowConfirm()) {
return;
}
const userAccepted = await showConfirmDialog();
if (!userAccepted) {
// 「後で」が押されたらクールダウンを記録して終了
const nextShowAt = Date.now() + CONFIRM_COOLDOWN_MS;
localStorage.setItem(CONFIRM_COOLDOWN_KEY, String(nextShowAt));
return;
}
// 「受け取る」が押されたらネイティブダイアログを表示
// ボタンクリック起点なので Firefox / Edge / Safari でも動作する
const browserPermission = await Notification.requestPermission();
if (browserPermission === "granted") {
// FCM トークンを取得してサーバーに送信する(次回の記事で解説)
await registerPushToken();
}
}
// --- 表示判定 ---
function shouldShowConfirm(): boolean {
// すでに許可済み or ブロック済みなら表示しない
if (Notification.permission !== "default") {
return false;
}
const nextShowAt = localStorage.getItem(CONFIRM_COOLDOWN_KEY);
if (nextShowAt && Date.now() < Number(nextShowAt)) {
return false; // クールダウン中
}
return true;
}
// --- ダイアログ制御 ---
function showConfirmDialog(): Promise<boolean> {
return new Promise((resolve) => {
const dialog = document.getElementById(
"dialog-confirm",
) as HTMLDialogElement;
dialog.showModal();
dialog.querySelector("#dialog-confirm-accept")!.addEventListener(
"click",
() => {
dialog.close();
resolve(true);
},
{ once: true },
);
dialog.querySelector("#dialog-confirm-dismiss")!.addEventListener(
"click",
() => {
dialog.close();
resolve(false);
},
{ once: true },
);
});
}
これで、ユーザーが独自ダイアログで「受け取る」を選んだときだけネイティブダイアログが表示され、「後で」を選んだ場合は一定期間ダイアログを出さない、という2段階フローが動くようになりました。
なお、サイト訪問時点ですでに通知がブロックされている(Notification.permission === "denied")場合は、サイト側から許可を再リクエストする方法がありません。
ブラウザの設定画面から手動で解除してもらう必要があるため、必要に応じて解除手順を案内するUIを別途用意するとよいでしょう。
4. 許可状態の検知 ― Safari の罠
3. 2段階の許可フローの実装例では、Notification.permission を使って default / granted / denied を判定しました。これ自体はどのブラウザでも問題なく動きます。
ただし、許可状態が「いつ変わったか」をリアルタイムに知りたいこともありますよね。たとえば、ユーザーが設定画面から通知を許可した瞬間に画面を切り替えたい、といったケースです。
Chrome / Firefox / Edge では navigator.permissions.query() の onchange イベントでこれが実現できます。
const status = await navigator.permissions.query({ name: "notifications" });
// ユーザーが設定画面で許可状態を変えた瞬間に反応する
status.addEventListener("change", () => {
console.log("許可状態が変わりました:", status.state);
});
ところが macOS Safari では navigator.permissions.query({ name: "notifications" })のonchangeイベントがバグにより発火しません。 iOS の PWA でも同様です。
Notification.permission の変化を定期的にチェックするポーリングで代わりに対応できます。
両方をまとめて扱うとこんな感じになります。
// --- 許可状態の変化を監視する ---
type PermissionCallback = (state: NotificationPermission) => void;
function watchPermission(onChange: PermissionCallback): void {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (!isSafari) {
// Chrome / Firefox / Edge: Permissions API + onchange
navigator.permissions.query({ name: "notifications" }).then((status) => {
status.addEventListener("change", () => {
onChange(status.state as NotificationPermission);
});
});
return;
}
// Safari / iOS PWA: Notification.permission を2秒ごとにポーリング
let prevPermission = Notification.permission;
const intervalId = setInterval(() => {
const currentPermission = Notification.permission;
if (currentPermission !== prevPermission) {
prevPermission = currentPermission;
onChange(currentPermission);
clearInterval(intervalId);
}
}, 2000);
}
// --- 使い方 ---
watchPermission((state) => {
if (state === "granted") {
registerPushToken(); // FCM トークンを取得してサーバーに送信する(次回の記事で解説)
}
});
これで、ブラウザの違いを意識せずに許可状態の変化を監視できるようになりました。
まとめ
この記事のポイントをざっとおさらいします。
- ブラウザごとの許可の違い ― Chrome 以外はダイアログ表示にユーザー操作が必須。Chrome も複数回無視されると自動ブロックされる。iOS は PWA でのみ対応
- 2段階の許可フロー ― 独自の確認UIを挟むことで、拒否リスクを下げつつ各ブラウザで動かせる
-
許可状態の検知 ― macOS Safari / iOS PWA では
onchangeが発火しないため、Notification.permissionのポーリングで対応する
許可フローは、一度拒否されるとリカバリーがほぼ効きません。
2段階フローを挟むことで、通知に関心のあるユーザーにだけ許可を求められるうえ、ユーザー操作が必須なブラウザにも対応できます。
プッシュ通知を導入するならぜひやっておきたい対応なので、試してみてください。

