Skip to content

fix(sw): focus the client before navigating on notification click - #914

Open
bitfactory-dk wants to merge 1 commit into
bulwarkmail:mainfrom
bitfactory-dk:fix/notification-click-focus-before-navigate
Open

fix(sw): focus the client before navigating on notification click#914
bitfactory-dk wants to merge 1 commit into
bulwarkmail:mainfrom
bitfactory-dk:fix/notification-click-focus-before-navigate

Conversation

@bitfactory-dk

Copy link
Copy Markdown

Clicking a push notification on Android closes the notification and does nothing — the app never comes to the front.

Cause

handleNotificationClick navigates an existing client and then focuses it:

if ("navigate" in client && targetUrl) {
  await client.navigate(targetUrl);
}
return client.focus();

A notification click grants the service worker a transient user activation, and both focus() and openWindow() refuse to run without one. navigate() spends that activation — and it also replaces the client's document, which invalidates the WindowClient handle the very next line uses. Navigating first therefore breaks the click two ways over.

Instrumented on a device, against a backgrounded installed PWA:

client 1: navigate ok -> focus() threw NotFoundError       (handle stale after navigation)
client 2: navigate ok -> focus() threw InvalidAccessError  (activation already spent)
openWindow()          -> threw InvalidAccessError          (same)

Every rejection was swallowed by the surrounding catch, so the notification closed and nothing opened, with nothing in any log to say why.

Fix

Focus first, while the activation is still live, then navigate the focused client. navigate() does not need an activation of its own once the client is focused, and it is wrapped so that a failed navigation still leaves the app raised rather than falling through to opening a second window.

Testing

  • Verified on a physical Android device against an installed PWA: a notification click now raises the app and opens the message. Running in production since 2026-08-24.
  • node --check public/sw.js passes.
  • Confined to public/sw.js; no build, dependency or configuration surface touched.

Not in this PR

There is a second, unrelated defect in the same file: badge points at /api/pwa-icon/192, and Android builds the status-bar badge from the alpha channel alone, so an opaque app icon masks to a featureless blob. I have kept it out of this PR because the fix involves choosing which of your own brand assets to use, which is your call rather than mine. I will open it as a separate issue with the measurements.

Clicking a push notification on Android closed the notification and did
nothing: the app never came to the front.

handleNotificationClick navigates an existing client and then focuses it:

    if ("navigate" in client && targetUrl) {
      await client.navigate(targetUrl);
    }
    return client.focus();

A notification click grants the service worker a transient user
activation, and both focus() and openWindow() refuse to run without one.
navigate() spends that activation, and it also replaces the client's
document, which invalidates the WindowClient handle the next line uses.
So navigating first breaks the click two ways over.

Instrumented on a device, against a backgrounded installed PWA:

  client 1: navigate ok -> focus() threw NotFoundError      (stale handle)
  client 2: navigate ok -> focus() threw InvalidAccessError (activation spent)
  openWindow()          -> threw InvalidAccessError         (same)

Every rejection was swallowed by the surrounding catch, so the toast
closed and nothing opened, with nothing in any log to say why.

Focusing first uses the activation while it is still live. navigate()
needs none of its own once the client is focused, and it is wrapped so a
failed navigation still leaves the app raised rather than falling through
to opening a second window.

Running in production since 2026-08-24.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant