Skip to content

fix(app): use service worker notifications on Android - #58399

Open
Zijian-Wang wants to merge 2 commits into
anomalyco:devfrom
Zijian-Wang:android-pwa-notifications
Open

Zijian-Wang wants to merge 2 commits into
anomalyco:devfrom
Zijian-Wang:android-pwa-notifications

Conversation

@Zijian-Wang

@Zijian-Wang Zijian-Wang commented Oct 23, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #49961

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Chrome on Android rejects new Notification(...). It requires ServiceWorkerRegistration.showNotification().

The web platform now uses an active service worker registration when one exists and keeps the constructor only as a fallback. The generated worker imports public/notification.js, which handles notificationclick for same-origin session URLs: focus an existing tab, navigate an app window, or open a new one.

The session notification callers now pass sessionHref(server, sessionID), so clicking a notification opens the session that finished instead of whatever page happened to be open. The tag is the same href, so a newer event replaces the stale notification.

How did you verify your code works?

  • bun typecheck in packages/app
  • bun test src/runtime/platform/notification.test.ts src/runtime/platform/web.test.ts — 6 passed
  • The service-worker path is tested with an injected registration. I could not test on a physical Android device in this environment; real-device delivery still needs a manual check.

Screenshots / recordings

Not included: the behavior needs a physical Android device. The notification appears through the Android system UI, not inside the app.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Chrome on Android rejects the Notification constructor. Prefer ServiceWorkerRegistration.showNotification when the offline worker is active, add a notification click handler that reopens the matching session, and keep the constructor only as a fallback.
@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Oct 23, 2026

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't verify this on a real device: it needs Chrome on Android with an active service worker. The two new unit test files pass and tsc -b passes in packages/app and packages/cli. The worker's same-origin and session-route checks match the format sessionHref produces.

Public API: unchanged. The only signature change is an optional href parameter on the app-internal Platform.notify.

The main concern: nothing in this pull request passes the new href. The callers in shell/notifications/notification.tsx still use the three-argument form; the change that passes sessionHref(...) is only in the stacked #58400. On its own, the service-worker path sets no tag and uses the current page as the click target. The in-page onClick (openNotificationSession) is also dropped on that path. Because the worker path is now preferred wherever one is active, desktop users of the installed web app lose the current "click opens the session" behaviour too, not only Android users. Please include the caller change here so this pull request works on its own (details inline).

const registration = "serviceWorker" in navigator ? await activeRegistration() : undefined

if (registration?.active) {
await registration.showNotification(title, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in this PR passes href yet: both callers in shell/notifications/notification.tsx still call notify(title, description, onClick). So on this path:

  • tag is undefined, so notifications stack instead of replacing each other.
  • data.url falls back to location.href, the page that was open when the notification fired. If that is a different session, the click goes back there instead of to the session that finished. If it is any non-session route, destination() in notification.js rejects it and the click does nothing, not even focusing the window.
  • onClick is never used here. Desktop browsers with an active worker previously ran openNotificationSession on click and now lose it.

Could you move the sessionHref(input.key, sessionID) caller change from #58400 into this PR, so the fix works without the push stack?

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit fixes my earlier point: both session notification calls now pass sessionHref(...), so service-worker notifications are tagged per session and a click opens the session that finished. Unit tests (6) and test-browser/server-notification-state.test.ts (4) pass, and tsc -b passes in packages/app. Real Android delivery is still unverified because it needs a physical device. Public API unchanged. No new concerns.

@dennyschwender

Copy link
Copy Markdown

From #62750 (closed as a duplicate of this PR — agreed, this is the right place). Two pieces from it that this PR does not have yet, both small and both fix real failures I reproduced with a raw-CDP loop (Chromium with --disable-blink-features=NotificationConstructor, i.e. real Blink Android behavior, against the built app):

1. The plugin attention.notify wrapper must settle on failure (packages/app/src/runtime/plugin/host-apis.tsx). The promise chain resolves only on success, so when showing a notification rejects, the promise never settles and the Web-Locks coordinator (opencode:notification-delivery / -focus) stays held — the first failure deadlocks every later notification until reload. Loop evidence before:

T2-android-shape -> displayed:FAIL chain-settled:FAIL no-unhandled:FAIL locks-free:FAIL
T5-post-failure-cascade -> delivered-after-prior-failure:FAIL settled:FAIL  (Web-Lock deadlock)

One-line fix: void value.notify(plugin, options).then(resolve, skip) (resolve the "not shown" shape on rejection). After that, T3-cascade-after-failure -> failure-settled:PASS delivered-after-failure:PASS locks-free:PASS.

2. Request notification permission from a user gesture (packages/app/src/settings/notifications/notifications.tsx). Notification.requestPermission() is only ever called from inside notify, i.e. never from a gesture, so the permission prompt cannot be answered in-app on Android. Enabling any notification channel in Settings › Notifications is the natural gesture: while Notification.permission === "default", call requestPermission() from the toggle's change handler (web platform only). No new UI, no new copy.

Implementation reference (both included, plus unit tests at the createWebNotify seam): #62750 and branch android-pwa-notifications on dennyschwender/opencode@d0aa5a3. Happy to rebase those two onto this PR's branch or review here instead — and yes, the remaining gap is exactly real-Android device testing.

This branch has not been deployed

No deployments
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.

web: notifications fail in Android PWA

2 participants