feat(fe): service worker pulls and renders notifications - #4258
Draft
MRmarioruci wants to merge 7 commits into
Draft
feat(fe): service worker pulls and renders notifications#4258MRmarioruci wants to merge 7 commits into
MRmarioruci wants to merge 7 commits into
Conversation
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 20, 2026 09:55
80189bf to
487c460
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
2 times, most recently
from
August 20, 2026 11:56
c401021 to
03c729f
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 20, 2026 15:25
03c729f to
71af199
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 07:35
71af199 to
d7eb98d
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 07:48
d7eb98d to
7def791
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 08:05
7def791 to
8c872de
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 08:13
8c872de to
153a490
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 08:27
153a490 to
9642e24
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 09:21
9642e24 to
a7e46d5
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 09:29
a7e46d5 to
8b42a54
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 09:33
8b42a54 to
39623c4
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 09:42
39623c4 to
6420e58
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 21, 2026 09:55
6420e58 to
8d50d90
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 09:19
de92f20 to
c00311c
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 09:59
c00311c to
2bcdaea
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 10:26
2bcdaea to
8cbc1f5
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 10:52
8cbc1f5 to
6419466
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 11:01
6419466 to
e240527
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 11:34
e240527 to
ac9246a
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 12:12
ac9246a to
d0bd2dd
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 24, 2026 12:24
d0bd2dd to
3ac5903
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 25, 2026 07:56
3ac5903 to
7fa646d
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 25, 2026 08:51
7fa646d to
43318f4
Compare
MRmarioruci
force-pushed
the
feat/push-notifications-sw
branch
from
August 25, 2026 09:17
43318f4 to
d465de9
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_NOTIFICATIONSflag.Changes
The placeholder
static/push-sw.jsis replaced by a bundled SvelteKit service worker (src/service-worker.ts), registered on opt-in rather than every load (kit.serviceWorker.registeris off) and with nofetchhandler, so it stays off the auth origin's request path. Onpush, 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; anotificationclickfocuses an open tab for the app or opens one. To let the worker build an agent without the browser-onlyglobals, the pull credential now also stores the IC host it was minted against.For review — provisional bits
ii_pending_notifications : () -> (vec record { title; body }) queryis a placeholder shape the worker expects; the client crate PR finalizes it.<origin>/.well-known/ii-notification-sendersat 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.npm run buildemitsdist/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.{ 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).