iOS notifications can lose permission to push if the notification is suppressed. Discourse’s code is configured to optionally suppress notifications, and will thereby lose permission to push when this happens three times.
Here’s the code for the push-notification service worker.
This code has a critical bug in it, at line 178. There, the service worker checks to see if the user is active (i.e. not idle). In that case, the push event returns false without showing a notification.
(It also checks payload.hide_when_active
, but it turns out that hide_when_active
is always true, and so this code always returns false when the user is active.)
Apple forbids silent pushes, revoking push permission after three events that don’t show notifications
This is not acceptable under Apple’s rules for push notifications.
https://webkit.org/blog/12945/meet-web-push/
Power and privacy
Both the WebKit open source project and Apple treat privacy as a fundamental human right. As with other privileged features of the web platform, requesting a push subscription requires an explicit user gesture. It also requires you set the
userVisibleOnly
flag to true, and fulfill that promise by always showing a notification in response to a push message.The Web Push API is not an invitation for silent background runtime, as that would both violate a user’s trust and impact a user’s battery life.
Violations of the
userVisibleOnly
promise will result in a push subscription being revoked.
(emphasis mine)
This is explained in further detail in Apple’s WWDC video on Web Pushes, at 9:57
https://developer.apple.com/videos/play/wwdc2022/10098/?time=596
Notice we are explicitly stating that we promise to always make pushes user visible. While the standard for the JavaScript Push API optionally accommodates silent JavaScript runtime in response to a push, most browsers do not support that. Safari does not support that.
… and then at 13:35:
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.
(emphasis mine)
Apple recommends showing notifications immediately, not after closing notifications
Apple’s recommended code looks like this, at 11:39:
https://developer.apple.com/videos/play/wwdc2022/10098/?time=699
self.addEventListener('push', (event) => {
let pushMessageJSON = event.data.json();
// Our server puts everything needed to show the notification
// in our JSON data.
event.waitUntil(self.registration.showNotification(pushMessageJSON.title, {
body: pushMessageJSON.body,
tag: pushMessageJSON.tag,
actions: [{
action: pushMessageJSON.actionURL,
title: pushMessageJSON.actionTitle,
}]
}));
}
Remember how when we subscribed for push, our JavaScript promised they would always be user visible? That means we must always show a platform native notification in response to each push. It is best to do this as early as possible in your push event handler.
Discourse’s code is not following the recommended best practice. Discourse’s code is first closing all notifications, and only then showing a notification.
Discourse should always call showNotification
in response to a push event, and it should always do it as soon as possible.