Skip to content
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

IOS pwa shows notification even when the app is in the foreground #8002

Open
pablor21 opened this issue Feb 1, 2024 · 11 comments
Open

IOS pwa shows notification even when the app is in the foreground #8002

pablor21 opened this issue Feb 1, 2024 · 11 comments

Comments

@pablor21
Copy link

pablor21 commented Feb 1, 2024

Operating System

IOS 17.2.1

Browser Version

Safari/10.7.2

Firebase SDK Version

10.7.2

Firebase SDK Product:

Messaging

Describe your project's tooling

Vue, Vite

Describe the problem

I'm trying to receive push messages on Iphone pwa. The notifications are coming just fine, the problem is that the notification is showing even if the app is in the foreground.

On desktop (mac/windows) and android it's working fine, but on IOS the notification always shows up regardless if the app is open or not.

Steps and code to reproduce issue

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')

const firebaseConfig = {} // my config

firebase.initializeApp(firebaseConfig)
const messaging = firebase.messaging()


// If you would like to customize notifications that are received in the
// background (Web app is closed or not in browser focus) then you should
// implement this optional method.
// Keep in mind that FCM will still show notification messages automatically 
// and you should use data messages for custom notifications.
// For more info see: 
// https://firebase.google.com/docs/cloud-messaging/concept-options
messaging.onBackgroundMessage(function(payload) {
    console.log('[firebase-messaging-sw.js] Received background message ', payload);
    // Customize notification here
    const notificationTitle = 'Background Message Title';
    const notificationOptions = {
      body: 'Background Message body.',
      icon: '/img/logo.svg'
    };
  
    self.registration.showNotification(notificationTitle,
      notificationOptions);
  });

@pablor21 pablor21 added new A new issue that hasn't be categoirzed as question, bug or feature request question labels Feb 1, 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 2, 2024
@juyeongnoh
Copy link

Any updates? I'm facing the same issue.

@dlarocque
Copy link
Contributor

dlarocque commented Aug 16, 2024

@pablor21 @juyeongnoh

Thanks for reporting this! Unfortunately, I haven't been able to reproduce it.

I have a PWA set up, and onBackgroundMessage running in a firebase-messaging-sw.js only gets triggered when the app is running in the background.

Could you share any additional information that may help me reproduce the issue? A minimal app or demo where this issue occurs would be really helpful

@google-oss-bot
Copy link
Contributor

Hey @pablor21. We need more information to resolve this issue but there hasn't been an update in 5 weekdays. I'm marking the issue as stale and if there are no new updates in the next 5 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

@yergom
Copy link

yergom commented Aug 27, 2024

Hi, I do have some new insights. The problem is that, under some circumstances that I have not been able to figure out, the service worker self.client.matchAll call returns always an empty list. Reproducing the issue is simple:

self.addEventListener("push", (event) => {
    event.stopImmediatePropagation(); //Disables firebase push handler
    event.waitUntil((async ()=>{
      const clients = await (self.clients.matchAll({
        type: "window",
        includeUncontrolled: true,
      }) as Promise<WindowClient[]>);
      return await self.registration.showNotification(
        `Clients:${clients.length}`,
      );
    }
  )());
getMessaging(firebaseApp);

But my tests are inconclusive. On iOS 16.7.5 clients are reported, on iOS 16.6.1 they are not. To get things worse, I have two separate iphones on ios 17.5, and one reports clients, the other one does not. This makes me think that there must be some OS level setting preventing the service worker from getting the correct client list.

This seems to be connected to this webkit bug: https://bugs.webkit.org/show_bug.cgi?id=268797. My feeling is that all the current iOS related bugs can be explained by it. Being able to get the client list is the base for sending foreground , reacting on notification clicks and changing the url of the background app, etc, etc

@dlarocque
Copy link
Contributor

Hi, I do have some new insights. The problem is that, under some circumstances that I have not been able to figure out, the service worker self.client.matchAll call returns always an empty list. Reproducing the issue is simple:

self.addEventListener("push", (event) => {
    event.stopImmediatePropagation(); //Disables firebase push handler
    event.waitUntil((async ()=>{
      const clients = await (self.clients.matchAll({
        type: "window",
        includeUncontrolled: true,
      }) as Promise<WindowClient[]>);
      return await self.registration.showNotification(
        `Clients:${clients.length}`,
      );
    }
  )());
getMessaging(firebaseApp);

But my tests are inconclusive. On iOS 16.7.5 clients are reported, on iOS 16.6.1 they are not. To get things worse, I have two separate iphones on ios 17.5, and one reports clients, the other one does not. This makes me think that there must be some OS level setting preventing the service worker from getting the correct client list.

This seems to be connected to this webkit bug: https://bugs.webkit.org/show_bug.cgi?id=268797. My feeling is that all the current iOS related bugs can be explained by it. Being able to get the client list is the base for sending foreground , reacting on notification clicks and changing the url of the background app, etc, etc

How does this relate to the original issue? The reported issue is that onBackgroundMessage is being triggered when the app is in the foreground.

@yergom
Copy link

yergom commented Aug 27, 2024

Hi, I do have some new insights. The problem is that, under some circumstances that I have not been able to figure out, the service worker self.client.matchAll call returns always an empty list. Reproducing the issue is simple:

self.addEventListener("push", (event) => {
    event.stopImmediatePropagation(); //Disables firebase push handler
    event.waitUntil((async ()=>{
      const clients = await (self.clients.matchAll({
        type: "window",
        includeUncontrolled: true,
      }) as Promise<WindowClient[]>);
      return await self.registration.showNotification(
        `Clients:${clients.length}`,
      );
    }
  )());
getMessaging(firebaseApp);

But my tests are inconclusive. On iOS 16.7.5 clients are reported, on iOS 16.6.1 they are not. To get things worse, I have two separate iphones on ios 17.5, and one reports clients, the other one does not. This makes me think that there must be some OS level setting preventing the service worker from getting the correct client list.
This seems to be connected to this webkit bug: https://bugs.webkit.org/show_bug.cgi?id=268797. My feeling is that all the current iOS related bugs can be explained by it. Being able to get the client list is the base for sending foreground , reacting on notification clicks and changing the url of the background app, etc, etc

How does this relate to the original issue? The reported issue is that onBackgroundMessage is being triggered when the app is in the foreground.

Let me elaborate a bit more if it was not clear. Have a look at the onPush listener:

export async function onPush(
  event: PushEvent,
  messaging: MessagingService
): Promise<void> {
  const internalPayload = getMessagePayloadInternal(event);
  if (!internalPayload) {
    return;
  }

  const clientList = await getClientList(); //<--  clientList is always empty due to webkit's bug


  if (hasVisibleClients(clientList)) {
   //This part implements foreground notifications, but is never called
    return sendMessagePayloadInternalToWindows(clientList, internalPayload);
  }

  //The method continues, and the payload is forwarded to the onBackgroundMessage handler
  if (!!internalPayload.notification) {
    await showNotification(wrapInternalPayload(internalPayload));
  }

  if (!messaging) {
    return;
  }

  if (!!messaging.onBackgroundMessageHandler) {
    const payload = externalizePayload(internalPayload);

    if (typeof messaging.onBackgroundMessageHandler === 'function') {
      await messaging.onBackgroundMessageHandler(payload);
    } else {
      messaging.onBackgroundMessageHandler.next(payload);
    }
  }
}

Notice that onNotificationClicked has a similar issue

export async function onNotificationClick(

  let client = await getWindowClient(url);
  //<--client will be null, so only the default behavior is executed and the notification's url is ignored

  if (!client) {
    client = await self.clients.openWindow(link);

    // Wait three seconds for the client to initialize and set up the message handler so that it
    // can receive the message.
    await sleep(3000);
  } else {
    client = await client.focus();
  }

  if (!client) {
    // Window Client will not be returned if it's for a third party origin.
    return;
  }

  internalPayload.messageType = MessageType.NOTIFICATION_CLICKED;
  internalPayload.isFirebaseMessaging = true;
  return client.postMessage(internalPayload);

@dlarocque
Copy link
Contributor

Hi, I do have some new insights. The problem is that, under some circumstances that I have not been able to figure out, the service worker self.client.matchAll call returns always an empty list. Reproducing the issue is simple:

self.addEventListener("push", (event) => {
    event.stopImmediatePropagation(); //Disables firebase push handler
    event.waitUntil((async ()=>{
      const clients = await (self.clients.matchAll({
        type: "window",
        includeUncontrolled: true,
      }) as Promise<WindowClient[]>);
      return await self.registration.showNotification(
        `Clients:${clients.length}`,
      );
    }
  )());
getMessaging(firebaseApp);

But my tests are inconclusive. On iOS 16.7.5 clients are reported, on iOS 16.6.1 they are not. To get things worse, I have two separate iphones on ios 17.5, and one reports clients, the other one does not. This makes me think that there must be some OS level setting preventing the service worker from getting the correct client list.
This seems to be connected to this webkit bug: https://bugs.webkit.org/show_bug.cgi?id=268797. My feeling is that all the current iOS related bugs can be explained by it. Being able to get the client list is the base for sending foreground , reacting on notification clicks and changing the url of the background app, etc, etc

How does this relate to the original issue? The reported issue is that onBackgroundMessage is being triggered when the app is in the foreground.

Let me elaborate a bit more if it was not clear. Have a look at the onPush listener:

export async function onPush(
  event: PushEvent,
  messaging: MessagingService
): Promise<void> {
  const internalPayload = getMessagePayloadInternal(event);
  if (!internalPayload) {
    return;
  }

  const clientList = await getClientList(); //<--  clientList is always empty due to webkit's bug


  if (hasVisibleClients(clientList)) {
   //This part implements foreground notifications, but is never called
    return sendMessagePayloadInternalToWindows(clientList, internalPayload);
  }

  //The method continues, and the payload is forwarded to the onBackgroundMessage handler
  if (!!internalPayload.notification) {
    await showNotification(wrapInternalPayload(internalPayload));
  }

  if (!messaging) {
    return;
  }

  if (!!messaging.onBackgroundMessageHandler) {
    const payload = externalizePayload(internalPayload);

    if (typeof messaging.onBackgroundMessageHandler === 'function') {
      await messaging.onBackgroundMessageHandler(payload);
    } else {
      messaging.onBackgroundMessageHandler.next(payload);
    }
  }
}

Notice that onNotificationClicked has a similar issue

export async function onNotificationClick(

  let client = await getWindowClient(url);
  //<--client will be null, so only the default behavior is executed and the notification's url is ignored

  if (!client) {
    client = await self.clients.openWindow(link);

    // Wait three seconds for the client to initialize and set up the message handler so that it
    // can receive the message.
    await sleep(3000);
  } else {
    client = await client.focus();
  }

  if (!client) {
    // Window Client will not be returned if it's for a third party origin.
    return;
  }

  internalPayload.messageType = MessageType.NOTIFICATION_CLICKED;
  internalPayload.isFirebaseMessaging = true;
  return client.postMessage(internalPayload);

Ah, I see- thanks for elaborating.

My understanding so far is that there is a WebKit bug that causes self.clients to be empty, which causes unexpected behaviour downstream when propagating the events through onPush and onNotificationClick.

I still don't quite understand how this is related to the WebKit bug you linked, where notificationclick events don't get fired in service workers. Could you elaborate on that?

@yergom
Copy link

yergom commented Aug 27, 2024

@dlarocque If you read the comments on https://bugs.webkit.org/show_bug.cgi?id=268797 carefully, my conclusion is that the service worker used by the push notification system (APNs) is different than the one spawned by the page itself. Apparently, if the app is completely closed and is opened due to clicking on a push notification, then the APN's service worker will be linked to the WindowClient, and then the issue will not be present. I have not been able to reproduce this. However, I've tried the following:

  • call self.client.claim() on 'push' event, and try to get the client list. No clients are found.
  • Try to communicate between the service worker and the pages using a BroadcastChannel. It also does not work, which, in my opinion, means that both service workers are isolated from each other at the VM level.

On top of all of this, also be aware that currently webkit is also not implementing silent notifications correctly. This means that if you are lucky enough to get foreground notifications, the push subscription will be automatically unsubscribed after 3 foreground messages, as reported in #8010

My current solution is to overwrite firebase behavior's on ios, and make sure that I always create a notification, to avoid the sudden unsubscription, something along these lines:

export function initializeFirebase() {
  if (isIOS()) {
    /**
     * We always create a push notification
     */
    self.addEventListener("push", (event) => {
      event.waitUntil(onIosPush(event));
    });
    /**
     * register other firebase event listeners
     */
    getMessaging(firebaseApp);
  } else {
    const messaging = getMessaging(firebaseApp);
    onBackgroundMessage(messaging, onMessage);
  }
}

async function onIosPush(event: PushEvent) {
  const internalPayload = getMessagePayloadInternal(event);
  event.stopImmediatePropagation(); //So that firebase's listener is not called
  if (!internalPayload) {
    return;
  }
  const payload = externalizePayload(internalPayload);
  return await onMessage(payload);
}

@bp-oleg
Copy link

bp-oleg commented Sep 2, 2024

I'm trying to achieve this behavior. PWA on iOS 17.6.1 does receive pushes via Firebase, shows notification while app is in background. But when app is in foreground, it receives messages in onMessage, then I need to show the notification, and new Notification(title, options) does not work. No idea how you get Firebase to show notifications in foreground

@dlarocque
Copy link
Contributor

This has been added to a list of known issues with FCM in iOS PWAs caused by WebKit bugs in our Wiki: https://github.com/firebase/firebase-js-sdk/wiki/Known-Issues

@pablor21
Copy link
Author

pablor21 commented Sep 8, 2024

I'm sorry! I was on vacation and I missed the whole discussion. It has been some time since I posted this issue and I don't remember how did I got the app working. It's good to know that people will be aware of this behavior and can plan for it accordingly.

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

7 participants