1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

チームボード開発記 #4 — PWAプッシュ通知が届かない問題を解決した(フォールバック設計)

1
Posted at

この記事でやること

PWAチャットアプリで「通知が届かない」問題を、フォールバック設計で解決します。

  • PWAプッシュ → ブラウザ通知 → アプリ内通知の段階的フォールバック
  • Service Workerのライフサイクルに起因する2重通知バグの原因と修正
  • 購読失効時の自動クリーンアップ(410 Gone対応)
  • PWAバッジのIndexedDB永続化

前提

  • Next.js 14(App Router)+ Supabase構成
  • Web Push API + Service Worker で通知実装済み
  • web-pushパッケージを使用

通知が届かないケース

ケース 原因
SWが未登録 ブラウザのストレージクリア・PWA未インストール
プッシュ購読の期限切れ ブラウザ/OS側の自動失効
通知権限が未許可 ユーザーがブロック済み

問題は「SWが未登録だがブラウザは開いている」状態。Web Push APIは使えないが、Notification APIは使える。

フォールバック実装

const sendNotification = async (
  title: string,
  body: string,
  userId: string
) => {
  // 1. PWAプッシュ購読があるか確認
  const subscription = await getPushSubscription(userId)

  if (subscription) {
    await fetch("/api/push-send", {
      method: "POST",
      body: JSON.stringify({ subscription, title, body }),
    })
    return
  }

  // 2. ブラウザ通知にフォールバック
  if ("Notification" in window && Notification.permission === "granted") {
    new Notification(title, { body, icon: "/icon-192x192.png" })
    return
  }

  // 3. アプリ内通知は別ルート(Supabase Realtime経由)で常に処理
}

サーバーサイドのプッシュ送信

// /api/push-send/route.ts
import webpush from "web-push"

webpush.setVapidDetails(
  "mailto:your@email.com",
  process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
  process.env.VAPID_PRIVATE_KEY!
)

export async function POST(request: Request) {
  const { subscription, title, body } = await request.json()

  try {
    await webpush.sendNotification(
      subscription,
      JSON.stringify({ title, body })
    )
    return Response.json({ success: true })
  } catch (error: unknown) {
    if (error instanceof webpush.WebPushError && error.statusCode === 410) {
      // 購読失効 → DBから削除して次回フォールバックへ
      await deleteSubscription(subscription.endpoint)
    }
    return Response.json({ success: false }, { status: 500 })
  }
}

410 Goneが返ったらDBの購読情報を削除。次回からブラウザ通知にフォールバックします。

2重通知バグ

最初の実装(NG)

const sendNotification = async (title: string, body: string, userId: string) => {
  if (navigator.serviceWorker?.controller) {
    await sendPushNotification(userId, title, body)
  } else {
    new Notification(title, { body })
  }
}

原因

Service Workerのライフサイクル:

インストール → アクティベーション → 制御開始(controller !== null)

PWAプッシュ購読は持っているがcontrollerがまだnullのタイミングが存在します。

  1. PWAインストール済み → 購読がDBにある
  2. ページ開いた直後、SWアクティベーション中 → controllerはnull
  3. ブラウザ通知を送信
  4. SW起動後、プッシュも届く
  5. 2重通知

修正

controllerではなく購読の有無で判定。

const sendNotification = async (title: string, body: string, userId: string) => {
  const subscription = await getPushSubscription(userId)

  if (subscription) {
    // 購読あり → プッシュのみ(SWの状態に関わらず)
    await sendPushNotification(userId, title, body)
    return
  }

  if ("Notification" in window && Notification.permission === "granted") {
    new Notification(title, { body })
  }
}

購読がDBに存在する = いずれプッシュが届く。controllerがnullでもブラウザ通知は送らない。

PWAバッジのIndexedDB永続化

PWAがバックグラウンドに回るとファビコンバッジが消える問題をIndexedDBで解決。

const saveBadgeCount = async (count: number) => {
  const db = await openDB("team-board", 1, {
    upgrade(db) {
      db.createObjectStore("state")
    },
  })
  await db.put("state", count, "badgeCount")
}

const restoreBadgeCount = async (): Promise<number> => {
  const db = await openDB("team-board", 1)
  const count = await db.get("state", "badgeCount")
  return count ?? 0
}

document.addEventListener("visibilitychange", async () => {
  if (document.visibilityState === "visible") {
    const count = await restoreBadgeCount()
    updateFaviconBadge(count)
    updateTitleBadge(count)
  }
})

Badging API対応環境では併用。

const updateBadge = async (count: number) => {
  await saveBadgeCount(count)
  updateFaviconBadge(count)
  updateTitleBadge(count)

  if ("setAppBadge" in navigator) {
    count > 0
      ? await navigator.setAppBadge(count)
      : await navigator.clearAppBadge()
  }
}

まとめ

問題 解決策
通知が届かない 購読有無で判定 → フォールバック
2重通知 controllerではなく購読で判定
購読の失効 410 Goneで自動削除
バッジが消える IndexedDB永続化 + visibilitychange

PWA通知で一番ハマるのはService Workerのライフサイクルと通知経路の不整合です。購読の有無を唯一の判定基準にすることで「届かない」と「2回来る」の両方を解消できます。


SEOスコアチェックツール: SEO_CHECK — RINIAディレクターツール。
Web制作・SEO関連の技術情報サイト: CodeQuest.work

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?