Skip to content

iOS Web Push Device Unregisters Spontaneously #8010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
fred-boink opened this issue Feb 5, 2024 · 67 comments
Open

iOS Web Push Device Unregisters Spontaneously #8010

fred-boink opened this issue Feb 5, 2024 · 67 comments

Comments

@fred-boink
Copy link

fred-boink commented Feb 5, 2024

Operating System

iOS 18.4+

Browser Version

Safari

Firebase SDK Version

10.7.2

Firebase SDK Product:

Messaging

Describe your project's tooling

NextJS 13 PWA

Describe the problem

Push notifications eventually stop being received until device is re-registered. Can take a few hours and lots of messages to occur but eventually stops receiving push.

People mention this can be a cause, Silent Push can cause your device to become unregistered:
https://dev.to/progressier/how-to-fix-ios-push-subscriptions-being-terminated-after-3-notifications-39a7

Safari doesn’t support invisible push notifications. Present push notifications to the user immediately after your service worker receives them. If you don’t, Safari revokes the push notification permission for your site.

https://developer.apple.com/documentation/usernotifications/sending_web_push_notifications_in_web_apps_and_browsers

Possible that Firebase does not waitUntil and WebKit thinks its a invisible push?

Steps and code to reproduce issue

public/firebase-messaging-sw.js

importScripts('https://www.gstatic.com/firebasejs/10.7.2/firebase-app-compat.js');

importScripts('https://www.gstatic.com/firebasejs/10.7.2/firebase-messaging-compat.js');

firebase.initializeApp({
    apiKey: '',
    authDomain: '',
    projectId: '',
    storageBucket: '',
    messagingSenderId: '',
    appId: '',
    measurementId: ''
});

const messaging = firebase.messaging();

messaging.onBackgroundMessage((payload) => {
    const {data} = payload;
    // Customize notification here
    const notificationTitle = data?.title;
    const notificationOptions = {
        data: {
            link: data?.link
        },
        body: data?.body,
        badge: './icons/icon-mono.png',
        icon:
            data?.senderProfileImage
    };

    return self.registration.showNotification(
        notificationTitle,
        notificationOptions
    );
});

self.addEventListener('notificationclick', function (event) {
    event.notification.close();

    event.waitUntil(
        clients
            .matchAll({
                type: 'window'
            })
            .then(function (clientList) {
                for (var i = 0; i < clientList.length; i++) {
                    var client = clientList[i];
                    if (client.url === '/' && 'focus' in client) {
                        return event?.notification?.data?.link
                            ? client.navigate(
                                `${self.origin}/${event?.notification?.data?.link}`
                            )
                            : client.focus();
                    }
                }
                if (clients.openWindow) {
                    return clients.openWindow(
                        event?.notification?.data?.link
                            ? `${self.origin}/${event?.notification?.data?.link}`
                            : '/'
                    );
                }
            })
    );
});
  • install app to homescreen
  • Receive notifications
  • Notice notifications no longer are received
  • Re-register device
  • Receive notifications
@fred-boink fred-boink added new A new issue that hasn't be categoirzed as question, bug or feature request question labels Feb 5, 2024
@fred-boink fred-boink changed the title Title for the bug iOS Web Push Device Unregisters Spontaneously Feb 5, 2024
@jbalidiong jbalidiong added needs-attention and removed new A new issue that hasn't be categoirzed as question, bug or feature request labels Feb 5, 2024
@JVijverberg97
Copy link

Looks like the same issue as #8013!

@fred-boink
Copy link
Author

fred-boink commented Mar 11, 2024

This keeps happening to users. This is the extent of our code. Why would the device stop sending pushes?

messaging.onBackgroundMessage((payload) => {
    const {data} = payload;
    const notificationTitle = data?.title;
    const notificationOptions = {
        data: {
            link: data?.link
        },
        body: data?.body,
        badge: './icons/icon-mono.png',
        icon:
            data?.senderProfileImage
    };

    return self.registration.showNotification(
        notificationTitle,
        notificationOptions
    );
});

@gbaggaley
Copy link

We are also having the same issue, looks like it works about 3 times on iOS and then just stops until the app is then opened and the token refreshed again.

messaging.onBackgroundMessage((payload) => {
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
        body: payload.notification.body,
        icon: payload.notification.image ?? "/icon-256x256.png",
        click_action: payload.data.link,
    };

    return self.registration.showNotification(
        notificationTitle,
        notificationOptions
    );
});

@graphem
Copy link

graphem commented Apr 3, 2024

Could this be related to this? https://dev.to/progressier/how-to-fix-ios-push-subscriptions-being-terminated-after-3-notifications-39a7

Not sure if the onBackgroundMessage message takes care of the waitUntil

@fred-boink
Copy link
Author

Could this be related to this? https://dev.to/progressier/how-to-fix-ios-push-subscriptions-being-terminated-after-3-notifications-39a7

Not sure if the onBackgroundMessage message takes care of the waitUntil

I suspect the issue is iOS thinks this is a invisible push notifications because firebase is doing it async, but until someone actually looks into, I don't know. We are debating moving off of FCM for this reason, it just stops working after some time.

@graphem
Copy link

graphem commented Apr 3, 2024

Alright that is indeed the issue, I just ran a bunch of test and I replace the onBackgroundMessage with this in my service worker:

self.addEventListener('push', function(event) {
    console.log('[Service Worker] Push Received.');
    const payload = event.data.json();  // Assuming the payload is sent as JSON
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
        body: payload.notification.body,
        icon: payload.notification.icon,
        image: payload.notification.image,
        badge: payload.notification.badge,
    };
    event.waitUntil(
        self.registration.showNotification(notificationTitle, notificationOptions)
    );
});

This is working perfectly, so I just remove all trace of firebase in the service worker. All the notifications are coming in now in iOS.

I think this need to be address in the firebase codebase.

@fred-boink
Copy link
Author

Alright that is indeed the issue, I just ran a bunch of test and I replace the onBackgroundMessage with this in my service worker:

``self.addEventListener('push', function(event) { console.log('[Service Worker] Push Received.'); const payload = event.data.json(); // Assuming the payload is sent as JSON const notificationTitle = payload.notification.title; const notificationOptions = { body: payload.notification.body, icon: payload.notification.icon, image: payload.notification.image, badge: payload.notification.badge, }; event.waitUntil( self.registration.showNotification(notificationTitle, notificationOptions) ); });`

This is working perfectly, so I just remove all trace of firebase in the service worker. All the notifications are coming in now in iOS.

I think this need to be address in the firebase codebase.

How do we get their attention!

@graphem
Copy link

graphem commented Apr 3, 2024

Alright that is indeed the issue, I just ran a bunch of test and I replace the onBackgroundMessage with this in my service worker:
``self.addEventListener('push', function(event) { console.log('[Service Worker] Push Received.'); const payload = event.data.json(); // Assuming the payload is sent as JSON const notificationTitle = payload.notification.title; const notificationOptions = { body: payload.notification.body, icon: payload.notification.icon, image: payload.notification.image, badge: payload.notification.badge, }; event.waitUntil( self.registration.showNotification(notificationTitle, notificationOptions) ); });`
This is working perfectly, so I just remove all trace of firebase in the service worker. All the notifications are coming in now in iOS.
I think this need to be address in the firebase codebase.

How do we get their attention!

Yeah, seems like a big deal, since it is not working on iOS

@jonathanyin12
Copy link

Wow this solved my exact problem. Thank you!!

@laubelette
Copy link

Alright that is indeed the issue, I just ran a bunch of test and I replace the onBackgroundMessage with this in my service worker:

self.addEventListener('push', function(event) {
    console.log('[Service Worker] Push Received.');
    const payload = event.data.json();  // Assuming the payload is sent as JSON
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
        body: payload.notification.body,
        icon: payload.notification.icon,
        image: payload.notification.image,
        badge: payload.notification.badge,
    };
    event.waitUntil(
        self.registration.showNotification(notificationTitle, notificationOptions)
    );
});

This is working perfectly, so I just remove all trace of firebase in the service worker. All the notifications are coming in now in iOS.

I think this need to be address in the firebase codebase.

Not working for me. OK for Android but not in IOS.

@laubelette
Copy link

Wow this solved my exact problem. Thank you!!

Hello. Possible to have a piece of code ?

@rchan41
Copy link

rchan41 commented Apr 8, 2024

Update: After fixing some issues with my service worker and nixing my foreground notification handlers, it seems to work more reliably with the event.waitUntil() solution.

Another Update: A notification failed to send after 44 restarts. I was able to reactivate by requesting another token (after an additional restart) but I don't know what causes it as I'm just calling FCM's getToken. I'm thinking of occasionally checking if the token changes against the one stored in local storage and replacing it when needed.

More Findings: When FCM's getToken is called, it appears to update that client's token in Firebase's Topics. It's not reactivating the old token. The old token returns UNREGISTERED. The double notification might be that the token gets subscribed twice to a Topic (one is a new subscription, and the other one is mapped from the old one?).


I also removed Firebase from the service worker. I then tested if notifications would break by restarting the iOS device over and over while also sending a notification between restarts. Eventually, a notification would fail. It is possible it triggered three silent notifications but I also noticed other PWAs on the same device would not be able to confirm the subscription status either.

I don't believe this issue is specific to one PWA's implementation and it's just a bug with Safari. Or somehow another PWA's implementation is causing the others to fail on the same device.

I also noticed that requesting a new messaging token seems to "reactivate" the old one. If you subscribe with this new token, and send a notification to the topic, the iOS device will get two separate notifications.

Edit: I removed the other PWAs, and after a dozen restarts, the notifications still work as expected. I'm still doubtful, so I'll keep trying to reproduce it.

Edit 2: It eventually ended up failing twice afterwards.

@ZackOvando
Copy link

@rchan41 Hi! Sorry just to be clear did you get your PWA to send Push notifications to IOS ? Did it work?

@rchan41
Copy link

rchan41 commented May 2, 2024

@rchan41 Hi! Sorry just to be clear did you get your PWA to send Push notifications to IOS ? Did it work?

Yes, I didn't have issues sending push notifications to iOS. The issue I described with notifications after restarting the iOS device. However, these issues might be unrelated to the topic's issue.

@DarthGigi
Copy link

I can confirm that this is indeed because of silent notifications, when you have an onMessage handler for the foreground to handle the notification yourself (for example showing a toast in your app) and the app gets a push notification, Safari's console logs:
Push event ended without showing any notification may trigger removal of the push subscription.
Screenshot 2024-05-19 at 5  27 52@2x

On the third silent notification, Safari disables push notifications.

Having the app in the background, so that onBackgroundMessage is being triggered, does not cause this issue.

My code
service-worker.js
/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />

const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self));
import { initializeApp, getApps, getApp } from "firebase/app";
import { getMessaging, onBackgroundMessage } from "firebase/messaging/sw";

const firebase =
  getApps().length === 0
    ? initializeApp({
        apiKey: "apiKey",
        authDomain: "authDomain",
        projectId: "projectId",
        storageBucket: "storageBucket",
        messagingSenderId: "messagingSenderId",
        appId: "appId",
        measurementId: "measurementId"
      })
    : getApp();

const messaging = getMessaging(firebase);
onBackgroundMessage(messaging, async (/** @type {import("firebase/messaging").MessagePayload} */ payload) => {
  console.log("Received background message ", payload);
  const notification = /** @type {import("firebase/messaging").NotificationPayload} */ (payload.notification);
  const notificationTitle = notification?.title ?? "Example";
  const notificationOptions = /** @type {NotificationOptions} */ ({
    body: notification?.body ?? "New message from Example",
    icon: notification?.icon ?? "https://example.com/favicon.png",
    image: notification?.image ?? "https://example.com/favicon.png"
  });

  if (navigator.setAppBadge) {
    console.log("setAppBadge is supported");
    if (payload.data.unreadCount && payload.data.unreadCount > 0) {
      console.log("There are unread messages");
      if (!isNaN(Number(payload.data.unreadCount))) {
        console.log("Unread count is a number");
        await navigator.setAppBadge(Number(payload.data.unreadCount));
      } else {
        console.log("Unread count is not a number");
      }
    } else {
      console.log("There are no unread messages");
      await navigator.clearAppBadge();
    }
  }

  await sw.registration.showNotification(notificationTitle, notificationOptions);
});
Layout file
// ...some checks before onMessage
onMessage(messaging, (payload) => {
  toast(payload.notification?.title || "New message", {
    description: MessageToast,
    componentProps: {
      image: payload.notification?.image || "/favicon.png",
      text: payload.notification?.body || "You have a new message",
      username: payload.data?.username || "Unknown"
    },
    action: {
      label: "View",
      onClick: async () => {
        await goto(`/${dynamicUrl}`);
      }
    }
  });
});

@fred-boink
Copy link
Author

fred-boink commented Jun 22, 2024

@DarthGigi This is working properly? What version of firebase are you using? I am still getting unregistering even when using simple code like:

self.addEventListener('push', function(event) {
    console.log('[Service Worker] Push Received.');
    const payload = event.data.json();  // Assuming the payload is sent as JSON
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
        body: payload.notification.body,
        icon: payload.notification.icon,
        image: payload.notification.image,
        badge: payload.notification.badge,
    };
    event.waitUntil(
        self.registration.showNotification(notificationTitle, notificationOptions)
    );
});
`

@garethnic
Copy link

garethnic commented Jun 22, 2024

@fred-boink this is the latest iteration of things on one of the projects that I'm working on. I wasn't the original developer so it's been quite a journey trying to troubleshoot. The current iteration of the firebase service worker is still leading to reports of notifications stopping after a while. Don't know what the secret sauce is.

firebase-sw.js

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  if (event.notification && event.notification.data && event.notification.data.notification) {
    const url = event.notification.data.notification.click_action;
    event.waitUntil(
      self.clients.matchAll({type: 'window'}).then( windowClients => {
        for (var i = 0; i < windowClients.length; i++) {
          var client = windowClients[i];
          if (client.url === url && 'focus' in client) {
            return client.focus();
          }
        }
        if (self.clients.openWindow) {
          console.log("open window")
          return self.clients.openWindow(url);
        }
      })
    )
  }
}, false);

importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js');

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
}

firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

self.addEventListener('push', function (event) {
  messaging.onBackgroundMessage(async (payload) => {
    const notification = JSON.parse(payload.data.notification);

    const notificationOptions = {
      body: notification.body,
      icon: notification.icon,
      data: {
        notification: {
          click_action: notification.click_action
        }
      },
    };

    return event.waitUntil(
      self.registration.showNotification(notification.title, notificationOptions)
    );
  });
});

Then there's also a component with the following:

receiveMessage() {
      try {
          onMessage(this.messaging, (payload) => {
              this.currentMessage = payload;
              let message = payload.data.username + ":\n\n" + payload.data.message;
              this.setNotificationBoxForm(
                  payload.data.a,
                  payload.data.b,
                  payload.data.c
              );
              this.notify = true;
              setTimeout(() => {
                  this.notify = false;
              }, 3000);
          });
      } catch (e) {
          console.log(e);
      }
    },
...
created() {
      this.receiveMessage();
  },

@DarthGigi
Copy link

@fred-boink I'm using Firebase version 10.12.2 which is the latest version at the time of writing. Safari unregistering is still an issue and I don't think we can easily fix it without firebase's help.

@fred-boink
Copy link
Author

@DarthGigi Yes, none of my changes have worked. They need to fix this, its. a major problem. Anyway we can get their attention?

@DarthGigi
Copy link

@fred-boink It is indeed a major problem, I thought this ticket would get their attention, I have no idea why it didn’t. I don’t know any other ways to get their attention other than mail them.

@Dolgovec
Copy link

Greetings. We have faced the same problem for our PWA on iOS. Code works perfect for Android, Windows, Mac and other systems, except iOS. Version of iOS is 17+ for all our devices. Unfortunately, it stopped to show notifications at all. Before we at least got some, but now we can't get even a single notification for iOS (thus we can see in logs that it was successfully send to FCM).
Our implementation in the SW:

importScripts("https://www.gstatic.com/firebasejs/10.0.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.0.0/firebase-messaging-compat.js");
firebase.initializeApp({
 ...
});
const messaging = firebase.messaging();



self.addEventListener('push', (event) => {
    event.stopImmediatePropagation();
    const data = event.data?.json() ?? {};
    console.log('Got push notification', data);

    event.waitUntil(
        processNotificationData(data)
            .then(payload => {
                console.log('Notification:', payload);
                return clients.matchAll({ includeUncontrolled: true, type: 'window' }).then((clientList) => {
                    return self.registration.getNotifications().then((notifications) => {
                        // Concatenate notifications with the same tag and show only one
                        for (let i = 0; i < notifications.length; i++) {
                            const isEqualNotEmptyTag = notifications[i].data?.tag && payload.data?.['tag'] && (notifications[i].data.tag === payload.data?.['tag']);
                            if (isEqualNotEmptyTag) {
                                payload.body = payload.data.text = notifications[i].body + '\n' + payload.body;
                            }
                        }
                        // Show notification
                        return self.registration.showNotification(payload.data?.title, payload);
                    });
                });
            })
            .catch(error => {
                console.error('Error processing push notification', error);
            })
    );
});

async function processNotificationData(payload) {
    const icon = payload.data?.['icon'] || 'assets/logo.svg';
    const text =  payload.data?.['text'];
    const title =  payload.data?.['title'];
    const url = payload.data?.['redirectUrl'] || '';

    const options = {
        tag: payload.data?.['tag'],
        timestamp: Date.now(),
        body: text,
        icon,
        data: {
            ...payload.data,
            icon,
            text,
            title,
            redirectUrl: url,
            onActionClick: {
                default: {
                    operation: 'focusLastFocusedOrOpen',
                    url
                }
            }
        },
        silent: payload?.data?.['silent'] === 'true' ?? false
    };

    if (!options.silent) {
        options.vibrate = [200, 100, 200, 100, 200, 100, 200];
    }

    return options;
}




// handle notification click
self.addEventListener('notificationclick', (event) => {
    console.log('notificationclick received: ', event);
    event.notification.close();

    // Get the URL from the notification
    const url = event.notification.data?.onActionClick?.default?.url || event.notification.data?.click_action;

    event.waitUntil(
        ...
    );
});

self.addEventListener('install', (event) => {
    console.log('Service Worker installing.');
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', (event) => {
    console.log('Service Worker activating.');
    event.waitUntil(clients.claim().then(() => {
        console.log('Service Worker clients claimed.');
    }));
});

self.addEventListener('message', (event) => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting().then(() => {
            console.log('Service Worker skipWaiting called.');
        });
    }
});

self.addEventListener('fetch', (event) => {
    console.log('Fetching:', event.request.url);
});

Can anyone please help? Or any advices how to get them?

@dlarocque
Copy link
Contributor

dlarocque commented Aug 15, 2024

After some reproduction attempts, here's what I've found:

Silent Push
I have not been able to reproduce this issue. My PWA continues to receive notifications (I have sent 50+) even if I don't have event.waitUntil in my onBackgroundMessage handler. Any additional information from anyone experiencing this issue (@fred-boink, @DarthGigi, @gbaggaley, @graphem) would be very helpful. This could be Safari/iOS version, logs, exact reproduction steps, minimal reproduction codebase, or state of browser storage.

Device Restarts
I have been able to reproduce the issue that @rchan41 is facing, where notifications are no longer sent after a device restarts, and then they're all received once the PWA is opened again. If I replace my usage of onBackgroundMessage with
self.addEventListener('push', () => self.registration.showNotification('test notification', {})), the issue goes away, and notifications continue to be received after devices restarts. Discussion on this bug should be moved to #8444.

@efortenberry
Copy link

This is very frustrating for all of our iOS users...

@nathan1658
Copy link

Oh my god same here, I hope this got fixed....

@fred-boink
Copy link
Author

@dlarocque Any luck looking into this? Still not working properly on iOS Safari?

@fred-boink
Copy link
Author

I'll chime in with my experience with this bug. Like many of you, my iOS users would also stop receiving push notifications randomly after some period of time. Using some remote logging, I eventually caught the issue in action. The issue was that iOS was completely resetting the state of my app randomly... indexeddb database, localstorage, everything, including push notification tokens. Our logs showed the app having full state, then the app being closed by the user, then opened 10 minutes later with no local state. There was no rhyme or reason, and the user did nothing out of the ordinary (it was a developer on one of our internal test devices).

Apple explicitly says they will not reset the state of PWAs added to the Home Screen, but in my experience, this is a flat out lie. Our users suffered from it and we saw it with our own eyes. I believe many of you are seeing it too. I suspect Apple will not fix this "bug" because they don't like competition with PWAs, but that's just my personal opinion.

If you are skeptical about this, implement remote logging like I did. Check the local state of the PWA when it loads and log it to your remote server. When the state vanishes, your users will no longer receive push notifications.

@amoffat Have you logged an issue with Safari? I can up vote it and follow.

@AndreyKovanov
Copy link

Looks like the same issue
We stop to receive push-messages after 3 foreground messages because it considered by Safari as silent
Is there a way to fix it?

@ChaeHoonK
Copy link

I’m experiencing an issue with Firebase Cloud Messaging (FCM) push notifications on iOS Safari. Specifically, notifications stop working after the app receives three consecutive push messages while in the foreground. Notifications work as expected when the app is in the background.

  • Foreground Behavior:
    • I’m using onMessage from firebase/messaging to handle push notifications while the app is in the foreground.
    • The messages are received successfully, but they are treated as silent messages (i.e., no visible notification is displayed).
    • After three such messages, iOS Safari stops receiving any further push notifications entirely, even if the app is sent to the background.

  • Background Behavior:
    • I'm using onBackgroundMessage from firebase/messaging in service worker.
    • Notifications handled via onBackgroundMessage work as expected. They are displayed properly without any issues.

My Assumption:

From my understanding, iOS Safari might be unregistering the FCM token after three silent push notifications. It seems that notifications received while the app is in the foreground are treated as silent messages, which might cause the token to become invalidated per Apple’s guidelines.

Goal:

I need a way to display visible push notifications in the foreground using onMessage to ensure notifications are not treated as silent. This may prevent the token from being invalidated.

@andynewman10
Copy link

  • Foreground Behavior:
    • I’m using onMessage from firebase/messaging to handle push notifications while the app is in the foreground.
    • The messages are received successfully, but they are treated as silent messages (i.e., no visible notification is displayed).
    • After three such messages, iOS Safari stops receiving any further push notifications entirely, even if the app is sent to the background.

@ChaeHoonK When you write "The messages are received successfully", are these messages regular Firebase messages, or Firebase notifications?

Goal:

I need a way to display visible push notifications in the foreground using onMessage to ensure notifications are not treated as silent. This may prevent the token from being invalidated.

You need to show a notification by your own means when receiving a Firebase notification message and your app is in the foreground. Just handle onMessage, and display a notification using, eg, flutter_local_notifications.

When your app is in the foreground and you are receiving a Firebase regular message (with no notification payload), my understanding is that you can just process the message in onMessage, you don't have to show a notification. I understand that silent push is permitted in this case.

@ChaeHoonK
Copy link

@andynewman10 "The messages are received successfully" means that I can receive background push notifications more than 3 times (as many as I want) using Firebase messages (no notification payload).

Here is the payload that I used to send data

const payload = {
      data: {
        title: message.title,
        body: message.body,
      },
    }; 

const result = await admin.messaging().send({ ...payload, token });

The code below is how I handled onMessage. But I'm not sure if I'm handling it correctly to be permitted from Safari's silent push restriction.

useEffect(() => {
    const initFCM = async () => {
      if (!isPushNotificationSupported()) return;

      const messaging = await initFirebaseMessaging();

      if (!messaging) return;

      const unsubscribe = onMessage(messaging, (payload) => {
        toast(<ToastComponent payload={payload} />, {
        // react-toastify options
        });
      });
      return () => {
        unsubscribe();
      };
    };

Lastly, I wrote a simple service worker for background push message.

messaging.onBackgroundMessage((payload) => {
  self.registration.showNotification(payload.data.title, {
    body: payload.data.body,
    icon: "/image/icons/icon512_maskable.png",
  });
});

I still have the same issue. When receiving push messages while on foreground, fcm token is automatically unregistered by itself.

@andynewman10
Copy link

const unsubscribe = onMessage(messaging, (payload) => {
toast(ToastComponent payload={payload}, {
// react-toastify options
});
});

What happens if you show a notification using new Notification, instead of creating a ToastComponent?

eg.

Notification.requestPermission().then(function(permission) {
    if (permission === "granted") {
      const notification = new Notification("Either a regular message or notification message was received", {
        body: "You clicked this notification.",
      });
   }
});

If you always use this code for foreground (onMessage) handling, do you still get disconnected? If you don't, what happens if you only show a notification when receiving a message with a notification payload (and just ignore the message if this is a regular message)?

(BTW, sorry about talking about flutter_local_notifications, I was reading a Flutter issue just a few minutes earlier)

@ChaeHoonK
Copy link

ChaeHoonK commented Jan 4, 2025

I've just tried it, but somehow new Notification does not work in onMessage. Maybe I implemented wrong.

I've tried different approach and it work pretty good. I would use this until either Firebase or Safari fix the issue.

useEffect(() => {
    const initFCM = async () => {
      if (!isPushNotificationSupported()) return;

      const messaging = await initFirebaseMessaging();

      if (!messaging) return;

      // Check for iOS token update requirement on mount
      if (isIOS) {
        const requireTokenUpdate = getLocalStorage("ios-require-token-update");
        if (requireTokenUpdate ) {
              // logic to update token              

              setLocalStorage("ios-require-token-update", false);
              setLocalStorage("ios-push-counts", 0);
        }
      }

      const unsubscribe = onMessage(messaging, (payload) => {
        // handling foreground push message.

        // Handle iOS silent push count
        if (isIOS) {
          const currentCount = getLocalStorage("ios-push-counts") || 0;
          const newCount = currentCount + 1;
          setLocalStorage("ios-push-counts", newCount);

          if (newCount >= 3) {
            setLocalStorage("ios-require-token-update", true);
          }
        }
      });
      return () => {
        unsubscribe();
      };
    };

    initFCM();
  }, []);

@smlparry
Copy link

Leaving this here because I've now spent far too long getting iOS Web Push to work reliably.

It's still not perfect (I'm still hitting this: #8444) but it feels much more reliable

When using Firebase's onBackgroundMessage & onMessage I was seeing the error "Push event ended without showing any notification may trigger removal of the push subscription." in the Safari Service Worker dev tools, even though I was showing a Notification (via new Notification) in the onMessage handler

My solution was to update the firebase-messaging-sw.js and just remove all the Firebase related code in favor of handling the "push" event natively

Our use case is pretty simple, Send a notification -> Open the URL so this will likely be applicable to a few people here

This lead me to believe that the flakiness of the push subscription wasn't being helped by Firebase's implementation.

Here is my final firebase-messaging-sw.js file:

self.addEventListener("push", function(event) {
  console.log("[FCM SW] Push Received.", event)

  if (!event.data) return

  const payload = event.data.json()
  console.log("[FCM SW] Push Payload:", payload)

  const notificationOptions = {
    body: payload.notification.body,
    icon: payload.notification.icon,
    data: payload,
    tag: payload.notification.tag || "default",
    actions: payload.notification.actions || []
  }

  // Important Note: The showNotification call MUST be wrapped in event.waitUntil() otherwise
  // iOS may treat these as silent notifications and eventually revoke the subscription.
  event.waitUntil(
    self.registration.showNotification(
      payload.notification.title,
      notificationOptions
    )
  )
})

self.addEventListener("notificationclick", function(event) {
  console.log("[FCM SW] Notification clicked.", event)

  // Wrap all async operations in event.waitUntil()
  event.waitUntil(
    (async () => {
      // Close the notification
      event.notification.close()

      const notificationData = event.notification.data?.notification

      // Try to find an existing window
      const windowClients = await clients.matchAll({
        type: "window",
        includeUncontrolled: true
      })

      // If we have an active window, focus it and send a message
      if (windowClients.length > 0) {
        const client = windowClients[0]
        await client.focus()

        console.log("[FCM SW] Posting message to client:", client)
        client.postMessage({
          type: "PUSH_NOTIFICATION_CLICK",
          payload: notificationData
        })
        return
      }

      const url = notificationData?.click_action

      // If no active window and we have a URL, open it
      if (url && clients.openWindow) {
        console.log("[SW] Opening URL:", url)
        const windowClient = await clients.openWindow(url)
        if (windowClient) {
          await windowClient.focus()
        }
      }
    })()
  )
})

self.addEventListener("install", event => {
  console.log("[FCM SW] Service Worker installing.")
  self.skipWaiting()
})

self.addEventListener("activate", event => {
  console.log("[FCM SW] Service Worker activating.")
  clients.claim()
})

And then in my JS code, I just added:

if ("serviceWorker" in navigator) {
  // Listen for messages from the service worker
  navigator.serviceWorker.addEventListener("message", event => {
    // console.log("[PN] Service worker message", event)

    if (
      event.data.type === "PUSH_NOTIFICATION_CLICK" &&
      event.data.payload?.click_action
    ) {
      handleNotificationUrl(event.data.payload.click_action) // Replace with your handler
    }
  })
}

Again, its not perfect, but what's not working seems to be an issue on Apple's end. This is much more reliable than using Firebase directly

I hope this helps someone!

@DarthGigi
Copy link

I believe the problem is that Safari does not support silent/invisible push notifications, which FCM does support and incorrectly handles.

As stated, after 3 invisible push notifications, Safari will revoke your app's notification permission; the user will have to go through setting up the permissions again.

https://developer.apple.com/videos/play/wwdc2022/10098/?time=814

As mentioned when I showed you the code on how to request a push subscription, you must promise that pushes will be user visible. Handling a push event is not an invitation for your JavaScript to get silent background runtime. Doing so would violate both a user's trust and a user's battery life. When handling a push event, you are in fact required to post a notification to Notification Center. Other browsers all have countermeasures against violating the promise to make pushes user visible, and so does Safari. In the beta build of macOS Ventura, after three push events where you fail to post a notification in a timely manner, your site's push subscription will be revoked. You will need to go through the permission workflow again. That's all.

https://developer.apple.com/documentation/UserNotifications/sending-web-push-notifications-in-web-apps-and-browsers#Enable-push-notifications-for-your-webpage-or-web-app

Safari doesn’t support invisible push notifications. Present push notifications to the user immediately after your service worker receives them. If you don’t, Safari revokes the push notification permission for your site.

@dlarocque
Copy link
Contributor

@DarthGigi I was not able to reproduce the Silent Push notification issue after several attempts when I last tried. Could you share a live website that I can use to reproduce the issue? If concerned about sharing it too publicly feel free to email [email protected]. I want to confirm at least one cause of this issue is Silent Push.

@DarthGigi
Copy link

@dlarocque May I ask what exactly you tried?

I believe the issue lies in the onMessage function imported from firebase/messaging outside the service worker, so in your actual webapp.

onMessage allows you to handle a push notification in your webapp (for example, showing a toast/alert) and doesn't use the OS's native notification system; which Safari doesn't like.

This only happens when the user has the webapp open and active (so not in the background).

If still needed, I could try to make a minimal repro?

@smlparry
Copy link

smlparry commented Feb 20, 2025

What @DarthGigi mentioned is what I experienced too.

I was handling onMessage in my web app, and showing a Notification via new Notification (not a custom toast) in that handler and I was still seeing the issue in the Safari service worker logs (not in the web app logs -- Develop > Service Workers > /firebase-cloud-messaging-scope)

I was only able to replicate when the app was in the foreground

@andynewman10
Copy link

I am working on Safari support for a web app and things appear to be working, but my testing has been very limited (to almost non existent) so far and production code doesn't have Safari enabled.

@DarthGigi @smlparry could it be that Safari just demands us to always display a notification through self.registration.showNotification(), in the push event handler of the service worker, and never in the main page's onMessage handler of Firebase? Basically, Safari would demand that push messages with a notification payload must trigger a call to self.registration.showNotification() in the push handler, even when the app is in the foreground.

Could it be this reason?

@DarthGigi
Copy link

@andynewman10 Yes, as explained in my earlier comment, Safari demands you to show a native push notification when the service worker has been invoked.

@ehtodd
Copy link

ehtodd commented Apr 3, 2025

Anyone tried switching to another service to see if it resolves these issues with IOS? Considering moving to Pusher Beams (https://pusher.com/docs/beams/getting-started/web/sdk-integration/?ref=web). Theoretically if this is a webkit bug then Pusher Beams will have the same bugs as FCM (https://github.com/firebase/firebase-js-sdk/wiki/Known-Issues#fcm-in-ios-progressive-web-apps-pwas) but if it's an FCM bug then using Pusher Beams or another equivalent service should fix it?

@amoffat
Copy link

amoffat commented Apr 3, 2025

Vindicated. Apple tacitly admits a fault in their push notification processing.

https://webkit.org/blog/16535/meet-declarative-web-push/

Allowing websites to remotely wake up a device for silent background work is a privacy violation and expends energy. So if an event handler doesn’t show the user visible notification for any reason we revoke its push subscription

Unfortunately bugs in a service worker script, networking conditions, or local device conditions all might prevent a timely call to showNotification. These scenarios might not always be the fault of the script author and can be difficult to debug.

Emphasis mine.

So a "networking condition" or "local device condition" can prevent or delay a showNotification, and Apple punishes your PWA for it by revoking the subscription.

@amoffat
Copy link

amoffat commented Apr 3, 2025

They go on to explain that Apple's ITP (Intelligent Tracking Prevention) will nuke all of your PWA's data, including push subscriptions:

ITP deletes all website data for websites you haven’t visited in a while. This includes service worker registrations. While this can be frustrating to web developers, it’s key to protecting user privacy. It’s a hard tradeoff we make intentionally given how committed we are to protecting users.

When we implemented Web Push that created a dilemma. Since creating and using a push subscription is inherently tied to having a service worker, ITP removing a service worker registration would render the push subscription useless. Since having strong anti-tracking prevention features seems to be fundamentally at odds with the JavaScript-driven nature of existing Web Push, wouldn’t it be better if Web Push notifications could be delivered without any JavaScript?

So if someone wants push notifications from you, they go through the rigamarole of install the PWA and approving notifications, then if they don't visit your site in "a while", Apple wipes all website data, including the push notification subscription, making it completely useless.

@ehtodd
Copy link

ehtodd commented Apr 3, 2025

Awesome, thanks for sharing that @amoffat. Looks like we just need to implement this new Declarative Web Push standard and then web push will actually work on iOS :) Huge news if true.

@ehtodd
Copy link

ehtodd commented Apr 6, 2025

Okay so I think I got a little ahead of myself with the celebration. We need updates to this SDK and the firebase admin SDK to support Declarative Web Push.

@dlarocque
Copy link
Contributor

@amoffat Thank you for sharing this! Unfortunately for us, it's challenging to know whether issues like these are caused by 1) intentional privacy protecting features 2) a bug in WebKit 3) a bug in the Firebase JS SDK.

@ehtodd, If you believe supporting Declarative Web Push solves issues in WebKit, please open a new feature request in this SDK and in the Admin SDK so that we can track it separately from this bug.

@amoffat
Copy link

amoffat commented Apr 7, 2025

It should be assumed that iOS is unreliable until declarative web push is implemented, given that they have listed 2 cases (ITP and device delays) where the subscription can be revoked outside of the developer's control. Even if Firebase SDK is flawless, developers will still have issues. Focus on declarative web push and I suspect many of the current iOS issues will vanish.

@andynewman10
Copy link

Can anybody please confirm the present issue does not affect macOS Safari, and only affects iOS?

@DarthGigi
Copy link

@andynewman10 it affects all platforms that have Safari and support Web Push

@HaniehMa
Copy link

Hello everyone I am also struggling with getting notifications on my react native app as well and wait until was working well until recently, however I have run into problem with notifications on iOS again.
So we need iOS 18.4 based on the article? And is there a guide of Declarative Web Push implementation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests