angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
94.71k stars 24.68k forks source link

Angulars Service Worker - "beforePushNotification" #55641

Open Flo0806 opened 2 weeks ago

Flo0806 commented 2 weeks ago

Which @angular/* package(s) are relevant/related to the feature request?

Angular 17.3.x and @angular/pwa

Description

Angulars ngsw-worker.js shows notification at any time a "push" event occurs. However, receiving push notifications when the app is open and in the foreground is not a good user experience. Angular's service worker does not offer a direct way to determine whether a notification should finally be displayed or not. The "onPush" method cannot be overwritten, at most it can be extended - which does not lead to the desired result.

I tried things with a custom service worker like this:

self.addEventListener("push", (event) => {
  console.log("Push event detected, but notifications are suppressed.");
  event.preventDefault();
  event.stopPropagation();

  event.waitUntil(
    (async function () {

      console.log("Received push", event.data.json());
      event.data.json().notification.title = null;

      return;
    })()
  );
});

importScripts("./ngsw-worker.js");

but without success (It doesn't matter whether importScripts is at the top or at the bottom)

Proposed solution

A option to set, or a optional function which will return a boolean, like this:

this.swPush.beforePushNotification = () => { return false; }

Alternatives considered

A way to override functions like "onPush" in the Service Worker itself.

Flo0806 commented 2 weeks ago

In the end we can always add our own service worker. But that was not my goal. I have found a solution - it's not the best way to go, but it works:

self.addEventListener("push", (event) => {
  event.preventDefault();
  event.stopPropagation();
  event.stopImmediatePropagation();
  if (!event.data) {
    return;
  }
  event.waitUntil(
    (async (mySelf) => {
      const windowClients = await mySelf.clients.matchAll({ type: "window" });
      if (!windowClients || !windowClients[0]) return;

      const client = windowClients[0];

      if (client && client .visibilityState !== "visible") {
        await handlePush(event.data.json());
      }
    })(self)
  );
});

async function broadcast(msg) {
  const clients = await self.clients.matchAll();
  clients.forEach((client) => {
    client.postMessage(msg);
  });
}

async function handlePush(data) {
  var NOTIFICATION_OPTION_NAMES = [
    "actions",
    "badge",
    "body",
    "data",
    "dir",
    "icon",
    "image",
    "lang",
    "renotify",
    "requireInteraction",
    "silent",
    "tag",
    "timestamp",
    "title",
    "vibrate",
  ];

  await broadcast({
    type: "PUSH",
    data,
  });
  if (!data.notification || !data.notification.title) {
    return;
  }
  const desc = data.notification;
  let options = {};
  NOTIFICATION_OPTION_NAMES.filter((name) => desc.hasOwnProperty(name)).forEach(
    (name) => (options[name] = desc[name])
  );
  await self.registration.showNotification(desc["title"], options);
}

importScripts("./ngsw-worker.js");