@research-ag/ic-web-push
v0.1.2
Published
Browser SDK for Internet Computer web push notifications with a companion service worker.
Maintainers
Readme
IC Web Push (browser SDK)
IC Web Push is a tiny browser-side SDK that wires your web app to the Internet Computer (IC) notification canister to enable standards-based Web Push notifications.
- Works with the standard Push API in Safari, Chromium, Firefox, Edge, and Android browsers.
- Coexists with your existing service worker by using a separate scope.
- Handles subscription, unsubscription, and application registration against the IC notification canister.
Default notification canister ID: zjwxf-jyaaa-aaaao-a43ca-cai (configurable).
High-level architecture
- Your app registers a dedicated service worker (SW) under a separate scope (e.g.,
/ic-web-push/). This SW only handles displaying push notifications and reacting to clicks. - The SDK lists available relayers from the IC notification canister, picks one (random by default), fetches its VAPID public key, and subscribes the browser via
PushManagerusing that key. - The resulting
PushSubscriptionplus the chosen relayer principal is sent to the notification canister and associated with your application canister principal. - Your backend canister sends notifications to the notification canister; each subscription is routed to its chosen relayer's queue for delivery via Web Push.
Files in this module
index.ts— public API surface for initializing and controlling subscriptions.sw.js— the service worker that displays notifications and handles clicks.
Coexisting with your app's service worker
Service workers are scoped by path. To avoid conflicts with your main app SW, IC Web Push uses its own scope, by default /ic-web-push/, and assumes its file is hosted at /ic-web-push-sw.js in your web root.
You may keep your app's own sw.js for app caching, offline, etc. This SDK's SW will not interfere because it is registered under a separate scope and only handles push and notificationclick events within that scope.
Hosting the service worker file
Place the service worker file at the web root so it can be served at /ic-web-push-sw.js. There are multiple ways to do this depending on your bundler:
- Vite: copy
src/ic-web-push/sw.jsintopublic/ic-web-push-sw.js(thepublicfolder is served at web root). Example:- Copy once manually, or
- Add a small build step/plugin to copy the file on build
- CRA/Next.js/others: add a copy step to move
sw.jsto the output root asic-web-push-sw.js.
You can also customize the path/scope via init() if you prefer a different location.
Usage
- Initialize the SDK early in app startup:
import icWebPush from 'ic-web-push';
import { HttpAgent } from '@icp-sdk/core/agent';
const agent = new HttpAgent({ host: 'https://ic0.app' });
// If developing locally against a replica, you may need:
// await agent.fetchRootKey();
// Ensure the user is authenticated and the identity is set to the agent reference before proceeding
icWebPush.init({
agent,
// Notification canister ID (defaults to mainnet id):
// notificationCanisterId: 'zjwxf-jyaaa-aaaao-a43ca-cai',
// Your application canister principal (REQUIRED to subscribe):
applicationCanisterId: '<your app canister id>',
// Optional: customize where the service worker is served from
serviceWorkerPath: '/ic-web-push-sw.js',
serviceWorkerScope: '/ic-web-push/',
});
icWebPush.setDebug(true); // optional- Register the service worker:
await icWebPush.registerServiceWorker();- Request permission (if needed) and subscribe:
// One-shot convenience that registers SW, ensures permission, and subscribes
await icWebPush.ensureSubscribed({ requestPermissionIfNeeded: true });
// Or do it step-by-step and pick a relayer explicitly
if (await icWebPush.getPermissionStatus() !== 'granted') {
await icWebPush.requestPermission();
}
// Option A: pick a random relayer
const relayer = await icWebPush.chooseRandomRelayer();
if (!relayer) throw new Error('No relayers available');
await icWebPush.subscribe({ relayer });
// Option B: list relayers and choose one by your own policy
const relayers = await icWebPush.listRelayers();
// e.g., pick the first or prefer by description
await icWebPush.subscribe({ relayer: relayers[0].relayer });- Unsubscribe later (optional):
await icWebPush.unsubscribe();
// Or to remove all subscriptions for your app principal on-chain:
await icWebPush.unsubscribeAll();API reference
init(config)— initializes SDK. Options:agent(HttpAgent): an agent for interaction with IC. Should use user's identity.notificationCanisterId(string): notification canister ID, default mainnet ID.applicationCanisterId(string): your app canister principal. Required forsubscribe/unsubscribeAll.serviceWorkerPath(string): where the SW file is served from. Default/ic-web-push-sw.js.serviceWorkerScope(string): SW scope. Default/ic-web-push/.
setDebug(enabled)— toggles debug logs.registerServiceWorker()— registers the SW at the configured path/scope.getPermissionStatus()— returns the currentNotification.permission.requestPermission()— prompts the browser permission dialog.subscribe({ requestPermissionIfNeeded? })— creates a push subscription and registers it on-chain.ensureSubscribed({ requestPermissionIfNeeded? })— convenience wrapper to do SW, permission, and subscription together.getSubscription()— resolves the currentPushSubscriptionornull.isSubscribed()— boolean indicating whether a subscription exists locally.unsubscribe()— removes the local subscription and tries to unregister it on the canister.unsubscribeAll()— unregisters all subscriptions for the configured application principal on the canister.
Service worker behavior
This SDK ships a dedicated service worker (sw.js) intended to live under its own scope (recommended: /ic-web-push/) and be served from your web root as /ic-web-push-sw.js. It is designed to coexist with your app’s main service worker without conflict. The worker implements the following:
install- Calls
self.skipWaiting()so that updates to the worker take effect immediately after install.
- Calls
activate- Calls
self.clients.claim()to take control of pages under its scope without a manual reload.
- Calls
push- Parses a JSON payload (if present). Robust to missing/invalid payloads and falls back to a generic notification.
- Supported payload fields (top-level preferred;
data.*also accepted for backward compatibility):title(string): Notification title. Default:"New notification".content(string): Notification text; mapped to Notification APIbody. Default:"You have a new message".url(string): A URL to open/focus when the notification is clicked. May also be provided asdata.url.actions(array): Standard Notification API actions.requireInteraction(boolean): Iftrue, the notification stays until user interaction.tag(string): Notification tag for collapsing/updating and for later clearing.data(object): Arbitrary extra fields; merged into the notificationdataobject. The worker ensuresdata.urlis set to the resolved URL.
- Display options applied by default:
iconandbadgedefault to/favicon.ico(override by editingsw.js).- If
tagis provided, it is set on the notification so later notifications with the same tag replace or group, per browser behavior.
- Example payload sent by your canister (matches
NotificationBody):{ "title": "New message", "content": "You received a message", "url": "/inbox/123", "tag": "chat-123" }
message- Listens for
{ type: 'CLEAR_NOTIFICATIONS_BY_TAG', tag: string }to programmatically close notifications with the given tag. - Uses
registration.getNotifications({ includeTriggered: true })when supported, with a safe fallback togetNotifications(). - Example from a page context (any controlled client under the SW scope):
navigator.serviceWorker.controller?.postMessage({ type: 'CLEAR_NOTIFICATIONS_BY_TAG', tag: 'chat-123', });
- Listens for
notificationclick- Closes the clicked notification.
- Resolves a target URL from
notification.data.url(defaults to/). - Looks for an existing same-origin window client and, if found:
- Focuses it.
- Tries to
postMessage({ type: 'OPEN_URL', url })so the app can handle in-app routing/session flags. - As a safe fallback, if the client is not at the origin root and the href differs, calls
client.navigate(url).
- If no suitable client exists, opens a new window via
clients.openWindow(url).
You can customize icons, default texts, or add behavior by editing src/ic-web-push/sw.js before copying it into your build’s web root as ic-web-push-sw.js.
Page <-> SW messaging: handling OPEN_URL
When a notification is clicked, the SW first tries to notify an existing tab using a message. In your application code, you may listen to this message to perform client-side routing instead of a hard navigation:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
const msg = event.data;
if (msg?.type === 'OPEN_URL' && typeof msg.url === 'string') {
// App-specific navigation, e.g., using your router
// router.push(new URL(msg.url, location.origin).pathname);
// Or simply: location.href = msg.url;
}
});
}Integration example (Vite + the provided chat_frontend)
Copy the service worker to the example's public root:
- Copy
src/ic-web-push/sw.jstoexample/chat_frontend/public/ic-web-push-sw.js.
- Copy
Initialize and subscribe in your app code, e.g.,
example/chat_frontend/src/App.jsx:
import { useEffect } from 'react';
import icWebPush from 'ic-web-push';
import { HttpAgent } from '@dfinity/agent';
export default function App() {
useEffect(() => {
icWebPush.init({
agent: new HttpAgent(),
applicationCanisterId: '<chat_backend_canister_id>',
// notificationCanisterId: '<override if not using default>'
});
icWebPush.ensureSubscribed({ requestPermissionIfNeeded: true }).catch(console.error);
}, []);
return <div>Chat app with IC Web Push</div>;
}- Start the dev server. Verify you see the permission prompt and a registered service worker under Application > Service Workers in DevTools.
Sending notifications
From your canister or backend, call sendNotifications with a vector of (principal, NotificationBody) pairs on the notification canister. The principal should be your user's principal.
NotificationBody schema:
record {
title: text;
content: text;
url: opt text;
tag: opt text;
}Refer to the canister Candid (src/notification-canister/notification_canister.did) for the full interface. On-chain sending uses sendNotifications(principal, NotificationBody).
Troubleshooting
- Ensure HTTPS and a secure origin. Service workers and Push require secure contexts.
- Make sure the SW file is actually served at the path you configured in
init(). - If subscription fails, check that the VAPID key is being fetched successfully from the notification canister.
- If notifications do not appear, ensure the notification payload fields match what your SW expects, and that the browser has permission granted.
- Some desktop browsers block notifications when the window is focused; try sending while unfocused or check OS notification settings.
