Advanced KaiOS Development: Push Notifications

Posted by Tom Barrasso on

Sending Notifications with the Web Push Protocol on KaiOS.

The Web Push Protocol

Push Notifications on KaiOS
Push Notifications on KaiOS

Push Notifications on KaiOS work more or less the same as they do in a web browser like Firefox, with some caveats. They are a great way to provide utility and drive engagement. For instance:

  • Chat app with notifications for new messages
  • Email app with notifications for new emails
  • Podcast app with notifications for new episodes
  • Sports app with notifications for game scores
  • Bank app with notifications for transactionals

Push Notifications require two components: a client with a registered ServiceWorker to receive notifications, and a web server to store subscriptions and send notifications. This article focuses on KaiOS-specific client-side interactions, as there are many great back-end libraries like web-push. There are also alternatives such as WebSockets and Server-Sent Events (SSE), although these are limited to interactions while the app is active in the foreground, while Web Push allows notifications to be received even if the app isn’t open.

Permissions

In KaiOS 2.5, applications must explicitly request permission to subscribe and receive push notifications by declaring the "push" permission in their manifest.webapp file. In KaiOS 3.0, the "push" permission is available by default to all applications.

Similarly, on KaiOS 2.5 (but not KaiOS 3.0), the "serviceworker" permission is needed to use register a ServiceWorker to receive push notifications. This permission was removed in KaiOS 3.0 to align with standard Progressive Web App (PWA) functionality.

Note: it’s impossible to forget the "serviceworker" permission because without it, on KaiOS 2.5 navigator.serviceWorker will be undefined.

Finally, the "desktop-notification" is needed on both KaiOS 2.5 and 3.0 to display notifications using the Notification constructor or ServiceWorkerRegistration.showNotification(). In KaiOS 2.5, by default the permission is granted for all app types, while in KaiOS 3.0 "pwa" type apps will display a prompt for the user to confirm. While user interaction is not required to display notifications or request permission in KaiOS 2.5, it is for "pwa" apps in KaiOS 3.0.

Note: it’s important to check for permission before using the above APIs. Depending on the KaiOS version and app type, users might be able revoke default permissions in Settings > Privacy & Security > App Permissions.

Notification Types and ServiceWorkers

KaiOS Notification Types
KaiOS Notification Types. Left requireInteraction = false, right requireInteraction = true

Depending on whether Notification.requireInteraction is set, KaiOS will display notifications differently. If requireInteraction is enabled, notifications will display in the foreground as a modal with actions (by default, “Dismiss”). The user must take an action to proceed. If requireInteraction is disabled, which it is by default, then the notification will display for several seconds before becoming available in the Notification Center.

There are two ways to display web notifications, using the Notification constructor and by calling ServiceWorkerRegistration.showNotification(). Using the Notification constructor creates non-persistent notifications that users can only respond to while your application is open. In contrast, actions are only supported for persistent notifications. In contrast, a notification associated with a ServiceWorker is a persistent notification that will trigger the the notificationclick and notificationclose events.

ServiceWorkers on KaiOS work similarly to Firefox, but on KaioS 2.5 there’s one special function worth mentioning: the Clients.openApp function which is part of the Clients interface. openApp launches a web app with the same origin of its service worker scope.

 1self.addEventListener('notificationclick', (event) => {
 2  let found = false;
 3  clients.matchAll().then((clients) => {
 4    // Check if the app is already opened
 5    for (i = 0; i < clients.length; i++) {
 6      if (clients[i].url === event.data.url) {
 7        found = true;
 8        break;
 9      }
10    }
11
12    // If not, launch the app
13    if (!found) {
14      clients.openApp({ msg: 'Data' });
15    }
16  });
17});

The optional msg property is a String that gets passed to the app via the serviceworker-notification system message. Apps need to register for this event in manifest.webapp (KaiOS 2.5).

1{
2    "messages": [
3        { "serviceworker-notification": "/" }
4    ]
5}

These messages can then be received by the application using system message handlers. In KaiOS 2.5, it’s as simple as calling navigator.mozSetMessageHandler. In KaiOS 3.0 this isn’t necessary because Clients.openApp was removed in favor of the standard Clients.openWindow, WindowClient.focus, and Client.postMessage, but there is an analagous SystemMessage API.

1navigator.mozSetMessageHandler("serviceworker-notification", (event) => {
2    console.log(event.msg)
3});

By combining a ServiceWorker, the push event, Clients.openApp, and a message handler for the serviceworker-notification event, it’s now possible to build a KaiOS web app that receives push messages, displays notifications, and opens the appropriate page.

VAPID Keys

Although applicationServerKey is required in browsers like Chrome and Edge, KaiOS and Firefox allow developers to send push notifications without setting applicationServerKey during subscription.

1serviceWorkerRegistration.pushManager.subscribe({
2    userVisibleOnly: true,
3    applicationServerKey: null,
4})

However, if Voluntary Application Server Identification for Web Push (VAPID) keys are not configured, only empty push notifications will be allowed. This significantly limits the utility of push notifications. For that reason, it’s best to generate and use VAPID keys. It’s simple with the web-push library:

1web-push generate-vapid-keys --json

This generates a public & private key pair. The public key is that’s provided to applicationServerKey in the form of a Uint8Array, while the private key is to send notifications server-side and should not be shared!

1{
2"publicKey":"BF7_KAxbQWoYtHwB7YnL0BlSQ-tvfmWrbp6Z9pUC_8kAdBUDv2QAZ4QScnQjwS982cpV5mqtT6QebWQLP5GpGwM",
3"privateKey":"4cjKnRevSuxLh6KHBEQzG9I3pzM_LFZwyxkqBsW1Kdg"
4}

Converting a Base64-encoded String into a Uint8Array is simple with a function like urlB64ToUint8Array, for instance base64-to-uint8array:

 1function urlB64ToUint8Array(base64String) {
 2  let padding = '='.repeat((4 - base64String.length % 4) % 4);
 3  let base64 = (base64String + padding)
 4    .replace(/\-/g, '+')
 5    .replace(/_/g, '/');
 6
 7  let rawData = window.atob(base64);
 8  let outputArray = new Uint8Array(rawData.length);
 9
10  for (let i = 0, e = rawData.length; i < e; ++i) {
11    outputArray[i] = rawData.charCodeAt(i);
12  }
13  return outputArray;
14};

PushManager.subscribe() returns a Promise, with the important details in the response object. Putting it all together:

 1const PUBLIC_KEY = "BF7_KAxbQWoYtHwB7YnL0BlSQ-tvfmWrbp6Z9pUC_8kAdBUDv2QAZ4QScnQjwS982cpV5mqtT6QebWQLP5GpGwM";
 2
 3serviceWorkerRegistration.pushManager.subscribe({
 4    userVisibleOnly: true,
 5    applicationServerKey: urlB64ToUint8Array(PUBLIC_KEY),
 6}).then(
 7    (pushSubscriptionObj) => {
 8        // Serialized endpoint & keys property
 9        const pushSubscription = pushSubscriptionObj.toJSON();
10        saveServerSide(pushSubscription); // TODO
11});

The pushSubscription object will contain two important properties, endpoint and keys. Here’s an example of what that looks like on KaiOS:

1{
2    "endpoint": "https://push.kaiostech.com:8443/wpush/v2/gAAAAABf4QzE-pX31ttCqVfnQQH90dCU9QvwXmWJgcdcHR6BZWMMQ1S_uRfi217k4FAoivLjhJviXJDWF2s7ya47OnfcSjZt2J98HIHFK2UQzZgG5VA7Jagvh-R0SrggsMpSWugCe90Sk9_mqCILmJPe1BN8NF5_jbaDO0U3VwTiF7lMGo9eccI",
3    "keys": {
4        "auth": "SIQaHqu_J-jZfskndcqeYw",
5        "p256dh": "ABykpssKmfXKskWYi6tVwvCZUthXodHLBMJnxUtTym3PCcNse5WFeRbepfXDFhn21jIVxEc_HrFdgKuURbJFh74"
6    }
7}

On the JioPhone, push subscriptions will look similar but resolve to a different domain.

1{
2    "endpoint": "https://update.kai.jiophone.net:8443/wpush/v1/gAAAAABj6Fj8YbiwPc4ydWXSl0lPT_D8lFpddlATEBrtdZ58fPVd7HDKGqo3nZhkuYtu-_-Hx-COEGk-R5NEbX_J8xLJKAt0Z5uqesPeBv5LaqmZmYBNhp8YuojDqAioJD2BN-wM5mZs",
3    "keys": {
4        "auth": "uhvorcFRq-pTnoHZwiN7sw",
5        "p256dh": "BPcvklqnsLZ8n8T_dK71XZQHCp0hZnrNYsY9HJbzNuwRRs0Pyz_Bc48n21whq6Lq8s9_E41pZCVFfZ5xiFII1nU"
6    }
7}

Note: as of the time of writing, the JioStore does not allow third-party applications access to the permissions needed to send push notifications.

Sending Push Notifications

Once you have the endpoint and keys for a user, sending push notifications is simple. Push notifications can be send from the command line using web-push:

 1web-push send-notification \
 2  --endpoint=$ENDPOINT \
 3  --key=$p256dh \
 4  --auth="SIQaHqu_J-jZfskndcqeYw" \
 5  --vapid-pubkey="BF7_KAxbQWoYtHwB7YnL0BlSQ-tvfmWrbp6Z9pUC_8kAdBUDv2QAZ4QScnQjwS982cpV5mqtT6QebWQLP5GpGwM" \
 6  --vapid-pvtkey="4cjKnRevSuxLh6KHBEQzG9I3pzM_LFZwyxkqBsW1Kdg" \
 7  --vapid-subject="https://myapp.com" \
 8  --ttl=10 \
 9  --encoding="aesgcm" \
10  --payload="Hello, World"

Push notifications can also be sent using NodeJS.

 1const pushSubscription = { endpoint, keys };
 2const message = 'Hello, World';
 3const options = {
 4  timeout: 1000, // milliseconds
 5  TTL: 10,
 6  contentEncoding: 'aesgcm',
 7};
 8
 9webpush.setVapidDetails(
10    SUBJECT,
11    PUBLIC_KEY,
12    PRIVATE_KEY,
13);
14
15webpush.sendNotification(
16      pushSubscription,
17      message,
18      options,
19)

Note: the default contentEncoding for web-push is aes128gcm, however, for KaiOS only aesgcm is supported.

Tags, Timestamps & mozbehavior

KaiOS 2.5 & 3.0 both support the tag property, allowing developers to replace existing notifications, rather than sending duplicates. Tags are a simple string that identifies the notification during construction. For instance, in PodLP a tag is set to the Podcast ID to avoid duplicate new episode notifications.

Unfortunately, neither Firefox nor KaiOS support the timestamp. As a result, all notifications will display a timestamp from the moment they are displayed to the user, even if the event happened in the past.

KaiOS also has a custom property, mozbehavior, with the following properties defined in Notification.webidl.

 1dictionary NotificationLoopControl {
 2  boolean sound;
 3  unsigned long soundMaxDuration;
 4  boolean vibration;
 5  unsigned long vibrationMaxDuration;
 6};
 7
 8dictionary NotificationBehavior {
 9  boolean noscreen = false;
10  boolean noclear = false;
11  boolean showOnlyOnce = false;
12  DOMString soundFile = "";
13  sequence<unsigned long> vibrationPattern;
14  NotificationLoopControl loopControl;
15};

Note: NotificationLoopControl is only available on KaiOS 3.0.

The two most useful properties are soundFile, while allows for custom notification sounds including via a remote file, and vibrationPattern which allows for custom vibration patterns.

Good Vibrations

Since Notification.vibrate isn’t available on Firefox or KaiOS, the main use for mozbehavior is to set the vibrationPattern to a custom pattern.

1let options = {
2    body: "Body",
3    mozbehavior: {
4        vibrationPattern: [ 30, 200, 30 ]
5    },
6};
7let notification = new Notification("Title", options);

With that, the notification will trigger a vibration when it’s received similar to calling navigator.vibrate.

Conclusion

Push notifications in KaiOS work similar to other modern browsers, but with a few special features, properties and quirks. Notifications are a great way to drive user engagement and keep users informed. If you’re looking for a partner to ensure the best possible user experience on KaiOS, you can find the author’s contact info on the About page.