Skip to content

feat(fe): service worker pulls and renders notifications - #4258

Draft
MRmarioruci wants to merge 7 commits into
feat/push-notifications-managefrom
feat/push-notifications-sw
Draft

feat(fe): service worker pulls and renders notifications#4258
MRmarioruci wants to merge 7 commits into
feat/push-notifications-managefrom
feat/push-notifications-sw

Conversation

@MRmarioruci

@MRmarioruci MRmarioruci commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The stack can now consent, subscribe, seal, deliver a ping, and manage apps — but the ping still shows a placeholder. This makes the service worker real: it pulls the notification's content from the dApp and renders it, so a delivered ping becomes an actual notification. Behind the off-by-default PUSH_NOTIFICATIONS flag.

Changes

The placeholder static/push-sw.js is replaced by a bundled SvelteKit service worker (src/service-worker.ts), registered on opt-in rather than every load (kit.serviceWorker.register is off) and with no fetch handler, so it stays off the auth origin's request path. On push, the browser has already decrypted II's sealed payload, so the worker reads the routing origin ({"o":"<origin>"}), loads that app's credential from IndexedDB, resolves the dApp canister from the origin's own discovery document, and pulls the pending notifications as the user's per-app identity — rendering each. With no usable credential, an expired one, or an unreachable canister it shows a generic, app-attributed notification; a notificationclick focuses an open tab for the app or opens one. To let the worker build an agent without the browser-only globals, the pull credential now also stores the IC host it was minted against.

For review — provisional bits

  • Pull interface. ii_pending_notifications : () -> (vec record { title; body }) query is a placeholder shape the worker expects; the client crate PR finalizes it.
  • Canister resolution. The worker fetches <origin>/.well-known/ii-notification-senders at push time (cross-origin, so it needs the dApp to serve CORS). D-30.1 would prefer recording the canister at consent — a cleaner follow-up once the shape settles.
  • agent-js in a worker. It bundles cleanly (npm run build emits dist/service-worker.js), but the authenticated pull is only exercisable against a live relay + dApp, so it is CI/manual-verified, not unit-tested. The generic fallback is the guaranteed path.
  • Module worker. Registered with { type: "module" } (Chrome 91+, Safari 16.4+, Firefox 111+).

The worker resolves the sending canister from the origin stored with the pull credential, which is canonicalized to the legacy gateway for principal stability and so is not necessarily served there. It looks for the well-known on each gateway, the same way the canister-side fetch does (#4251); without that the lookup fails on those origins and every notification falls back to the generic one.

No candid change; no locale changes (the extractor owns en.po).

Previous: #4257

@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 80189bf to 487c460 Compare August 20, 2026 09:55
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch 2 times, most recently from c401021 to 03c729f Compare August 20, 2026 11:56
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 03c729f to 71af199 Compare August 20, 2026 15:25
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 71af199 to d7eb98d Compare August 21, 2026 07:35
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from d7eb98d to 7def791 Compare August 21, 2026 07:48
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 7def791 to 8c872de Compare August 21, 2026 08:05
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 8c872de to 153a490 Compare August 21, 2026 08:13
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 153a490 to 9642e24 Compare August 21, 2026 08:27
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 9642e24 to a7e46d5 Compare August 21, 2026 09:21
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from a7e46d5 to 8b42a54 Compare August 21, 2026 09:29
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 8b42a54 to 39623c4 Compare August 21, 2026 09:33
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 39623c4 to 6420e58 Compare August 21, 2026 09:42
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 6420e58 to 8d50d90 Compare August 21, 2026 09:55
@MRmarioruci MRmarioruci reopened this Aug 21, 2026
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from de92f20 to c00311c Compare August 24, 2026 09:19
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from c00311c to 2bcdaea Compare August 24, 2026 09:59
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 2bcdaea to 8cbc1f5 Compare August 24, 2026 10:26
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 8cbc1f5 to 6419466 Compare August 24, 2026 10:52
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 6419466 to e240527 Compare August 24, 2026 11:01
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from e240527 to ac9246a Compare August 24, 2026 11:34
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from ac9246a to d0bd2dd Compare August 24, 2026 12:12
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from d0bd2dd to 3ac5903 Compare August 24, 2026 12:24
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 3ac5903 to 7fa646d Compare August 25, 2026 07:56
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 7fa646d to 43318f4 Compare August 25, 2026 08:51
@MRmarioruci
MRmarioruci force-pushed the feat/push-notifications-sw branch from 43318f4 to d465de9 Compare August 25, 2026 09:17
Replaces the placeholder push worker with a bundled SvelteKit service
worker (registered on opt-in, no fetch handler). On a ping it reads the
sealed routing origin, loads that app's stored credential, resolves the
dApp canister from the origin's discovery document, and pulls the pending
notifications as the user's per-app identity, rendering each. With no
usable credential or an unreachable canister it shows a generic,
app-attributed notification; a click focuses or opens the app. The
credential now carries the IC host so the worker can build an agent
without the browser-only globals.
…ption

When the browser rotates or expires the push endpoint, the old one is dead and the new one is unregistered, so delivery to this device stops silently. The worker can't re-register — that needs the user's II identity, which only an authenticated page holds — so it clears the stale subscription; the next authenticated page reconcile registers the new endpoint.
…ation

`require-await` fails the lint job: the stub returns a value, so `async`
without an `await` is reported. The sibling stub escapes it only by having
an empty body. Return the promise directly.
The opt-in failed screen printed the raw error (a full replica rejection /
canister trap) in a mono box, and the settings toggle put the same string in
its error toast. Drop both; the generic "Couldn't turn on notifications" /
"Couldn't change notifications on this device" copy stays. The failure is
still recorded to local diagnostics for the blocked/failed guidance.
The worker resolves the sending canister from the origin stored with the
pull credential, which is canonicalized to the legacy gateway for principal
stability and so is not necessarily served there — the same reason the
canister-side fetch tries each gateway. Without this the lookup fails on
those origins and every notification falls back to the generic one.
The origin's well-known can authorize several sender canisters, and any of
them may own notification content. The worker read only the first principal
and treated it as the content canister, which baked in an undocumented "first
principal owns content" role and lost content from the rest.

Parse every valid principal in the senders document, within the same cap the
backend binds (MAX_SENDERS). Cache the whole list per origin. Pull each
canister concurrently under its own timeout, so one slow or unreachable sender
does not block the others or the push handler. Reconcile per origin and
canister: the tag namespaces a notification by its owning canister so two
canisters can reuse an id, an empty response closes that canister's
notifications, and an unreachable canister is treated as unknown and left
untouched. Refresh the sender list once when a cached canister fails, and close
anything a now-absent sender still shows.
…ications

Align the service worker's pull with the dApp contract the notification client
serves, and stop re-raising notifications the user has already dismissed.

The pull now decodes ii_pending_notifications as { id: blob; title; body: text;
url: opt text; created_at: nat64 }, the shape the dApp side declares, instead of
the earlier { id: text; title; body: opt text } that could not read it. The blob
id becomes hex at the boundary, so it still keys the tag, the shown set, and the
dismissed set as a string; a click opens the notification's url when it stays
within the consented origin.

Because the dApp evicts pending content on its own cycle rather than being told
what a device displayed, a pull can still find a dismissed notification pending.
The worker now records each dismissal per origin in IndexedDB and reconcile
skips those tags, so a dismissed notification is not raised again; once its
canister stops listing the id, reconcile hands the tag back to forget, so a
re-issued id can raise a fresh notification.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:notifications Web Push notifications

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant