Skip to content

Commit db3d901

Browse files
committed
fix(notifications): never suppress push when clients.matchAll() returns empty
The TTL-gated appIsVisibleFresh fallback was designed to handle the iOS Safari PWA quirk where clients.matchAll() returns an empty list even when the app is visually open. In practice, the TTL window (30 s) meant any push that arrived within 30 seconds of the app being last visible was silently dropped — including the common case of backgrounding the app and immediately receiving a reply. pagehide + visibilitychange together are now much more reliable at delivering setAppVisible:false before the SW processes the push. When both events DO fail (iOS kills the JS context mid-event), clients.matchAll() ALSO tends to return the backgrounded client as visibilityState:'hidden', which is already handled correctly by the first branch. The clients.length === 0 path is therefore a triple-failure edge case where we simply cannot determine visibility. Erring toward showing the notification (a possible duplicate, handled gracefully by the in-app banner) is always better than silently dropping it. Removes appVisibleSetAt, APP_VISIBLE_TTL_MS, and the stale-flag fallback.
1 parent a9e8ab9 commit db3d901

File tree

1 file changed

+11
-20
lines changed

1 file changed

+11
-20
lines changed

src/sw.ts

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,6 @@ let notificationSoundEnabled = true;
1212
// The clients.matchAll() visibilityState is unreliable on iOS Safari PWA,
1313
// so we use this explicit flag as a fallback.
1414
let appIsVisible = false;
15-
// Timestamp (Date.now()) of the last time appIsVisible was set to true.
16-
// Used to expire the flag if the app backgrounded before the SW received the
17-
// hidden message (e.g. iOS suspended the JS context mid-visibilitychange).
18-
let appVisibleSetAt = 0;
19-
// If no visible heartbeat has been received in this window, treat the flag as
20-
// stale and do not suppress push notifications.
21-
const APP_VISIBLE_TTL_MS = 30_000;
2215
let showMessageContent = false;
2316
let showEncryptedMessageContent = false;
2417
let clearNotificationsOnRead = false;
@@ -578,7 +571,6 @@ self.addEventListener('message', (event: ExtendableMessageEvent) => {
578571
if (type === 'setAppVisible') {
579572
if (typeof (data as { visible?: unknown }).visible === 'boolean') {
580573
appIsVisible = (data as { visible: boolean }).visible;
581-
if (appIsVisible) appVisibleSetAt = Date.now();
582574
}
583575
}
584576
if (type === 'setNotificationSettings') {
@@ -758,21 +750,20 @@ const onPushNotification = async (event: PushEvent) => {
758750
// If the app is open and visible, skip the OS push notification — the in-app
759751
// pill notification handles the alert instead.
760752
//
761-
// Two-tier visibility check:
762-
// 1. When clients.matchAll() returns ≥1 client, trust its visibilityState
763-
// directly. iOS can suspend the JS thread before postMessage({ visible:
764-
// false }) is processed, leaving appIsVisible stuck at true. matchAll()
765-
// still reports the backgrounded client as 'hidden', so it is the
766-
// authoritative signal when available.
767-
// 2. When matchAll() returns zero clients (a separate iOS Safari PWA quirk
768-
// where the page is invisible to the SW even while visible), fall back to
769-
// the TTL-gated flag: the flag expires after APP_VISIBLE_TTL_MS so a stale
770-
// true from a quick background doesn't permanently suppress notifications.
771-
const appIsVisibleFresh = appIsVisible && Date.now() - appVisibleSetAt < APP_VISIBLE_TTL_MS;
753+
// When clients.matchAll() returns ≥1 client, trust its visibilityState
754+
// directly. iOS can suspend the JS thread before postMessage({ visible:
755+
// false }) is processed, leaving appIsVisible stuck at true. matchAll()
756+
// still reports the backgrounded client as 'hidden', so it is the
757+
// authoritative and most reliable signal.
758+
//
759+
// When matchAll() returns zero clients (a separate iOS Safari PWA quirk),
760+
// visibility is unknowable — do NOT suppress. Better to show a duplicate
761+
// (handled gracefully by the in-app banner) than to silently drop a
762+
// notification while the app is backgrounded.
772763
const hasVisibleClient =
773764
clients.length > 0
774765
? clients.some((client) => client.visibilityState === 'visible')
775-
: appIsVisibleFresh;
766+
: false;
776767
console.debug(
777768
'[SW push] appIsVisible:',
778769
appIsVisible,

0 commit comments

Comments
 (0)