iOSのPWAでプッシュ通知がうまくいかない場合の対処法

ReactのブロジェクトでFirebase Cloud Messagingを使用してプッシュ通知を実装しました。その際、iOSのPWA（Progressive Web App）で、プッシュ通知が来ない・iPhoneの「設定」＞「通知」＞の一覧にインストールしたPWAが表示されないという事象に直面しました。本記事では、デバッグしていく中でぶつかった、いくつかのエラーとその解決策について説明します。

## エラー1：NotSupportedError: Subscribing for push requires an applicationServerKey
**エラーメッセージ**: `NotSupportedError: Subscribing for push requires an applicationServerKey`

**原因**: このエラーは、pushManager.subscribe メソッドに applicationServerKey が不足している場合に発生します。applicationServerKey はオプションとして扱われていますが、実際には設定が必要です。

**解決策**: applicationServerKey を含むように pushManager.subscribe を修正します。このとき、applicationServerKey はUint8Array形式で渡す必要があります。そのため、VAPIDキー（公開鍵）をURL-safe base64形式からUint8Array形式に変換する関数 urlBase64ToUint8Array を使用します。以下は修正後のコード例です。

```ts
const applicationServerKey = urlBase64ToUint8Array(import.meta.env.FIREBASE_PUBLIC_VAPID_KEY as string);

const registration = await navigator.serviceWorker.register(
  '/firebase-messaging-sw.js'
);
await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey,
});
```

urlBase64ToUint8Array 関数は、VAPIDキーを正しい形式に変換するためのもので、Push APIが要求するフォーマットに合致させるために必要です。この関数は、URL-safe base64エンコードされた文字列をUint8Arrayに変換し、Push APIが正しく処理できるようにします。

```ts
const urlBase64ToUint8Array = (base64String: string): Uint8Array => {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');
  const rawData = window.atob(base64);
  return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
};
```

## エラー2：Notification prompting can only be done from a user gesture.
**エラーメッセージ**: `Notification prompting can only be done from a user gesture.
`

**原因**: Androidデバイスでは、PWAをインストールする際に通知設定のポップアップが自動的に表示されますが、iOSではそのようなポップアップが表示されません。そのため、iOSではクリックなどのユーザーアクションを起点として、パーミッションのポップアップを表示する必要があります。

**解決策**: ユーザーアクションをトリガーとして通知のパーミッションをリクエストし、その後でプッシュ通知のサブスクリプションを行います。以下のコード例がその実装です。

```ts
export const showPushNotificationPermissionPopup = async () => {
  if (Notification.permission === 'default' && 'Notification' in window) {
    try {
      const permission = await Notification.requestPermission();
      if (permission === 'granted') {
        await subscribeToPushNotifications();
      }
    } catch (error) {
      console.error('Error requesting notification permission:', error);
    }
  }
};

export const subscribeToPushNotifications = async () => {
  const applicationServerKey = urlBase64ToUint8Array(
    import.meta.env.VITE_FIREBASE_VAPID_KEY as string
  );
  try {
    const registration = await navigator.serviceWorker.register(
      '/firebase-messaging-sw.js'
    );
    await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey,
    });
  } catch (error) {
    console.error('Error during push subscription:', error);
  }
};
```

Reactでの簡単な例
以下は、Reactコンポーネント内でボタンをクリックして showPushNotificationPermissionPopup 関数を実行する簡単な例です。ユーザーがボタンをクリックすることで、通知許可のポップアップが表示され、許可が得られればプッシュ通知のサブスクリプションが行われます。

```react
import React from 'react';
import { showPushNotificationPermissionPopup } from './notificationService'; // 先ほどの関数をインポート

const NotificationButton = () => {
  const handleButtonClick = () => {
    showPushNotificationPermissionPopup();
  };

  return (
    <button onClick={handleButtonClick}>
      通知を有効にする
    </button>
  );
};

export default NotificationButton;
```

この例では、ユーザーが「通知を有効にする」ボタンをクリックすることで、iOSのPWAでも通知の許可とプッシュ通知のサブスクリプションが適切に処理されるようになります。

## まとめ
iOSのPWAでFirebase Cloud Messagingを使用してプッシュ通知を実装する際には、applicationServerKey の指定や、ユーザーアクションをトリガーとする通知許可のリクエストが重要です。これらのポイントを押さえて、スムーズにプッシュ通知を設定しましょう。