DEV Community

David Rickard
David Rickard

Posted on • Edited on

Proper Windows Notifications on Electron

Current state

Electron has a good starter guide to notifications, but it misses a few important points and leaves out a lot of detail.

It's pretty easy to pop a notification, then handle when the user clicks the toast. It fails on some other basic scenarios:

  • If the user clicks a notification in the Action Center when the app is not running, it does nothing.
  • If the user has closed and reopened the app since the notification was sent, then clicks the notification, it doesn't work.
  • If the user has dismissed the notification on a different device, nothing happens. It's frustrating to have to deal with a notification multiple times.

NodeRT

We need something deeper than what Electron gives us out of the box, which means using NodeRT.

There's electron-windows-notifications, though it's wrapping a pretty old version of NodeRT, and it does not expose some of the APIs we'll need. I decided to directly use the generated NodeRT APIs.

@nodert-win10-rs4/windows.data.xml.dom
@nodert-win10-rs4/windows.ui.notifications
Enter fullscreen mode Exit fullscreen mode

These are the bare minimum. You can also target other API levels like @nodert-win10-au, etc. You'll need to set up node-gyp with Python and Windows Build Tools and install the Windows 10 SDK corresponding to the version of the NodeRT package you pulled in.

Note that you need to be using electron-rebuild for this to work. Electron Forge incorporates this automatically and is the easiest path. If you try to add these in a vanilla Electron app, the NODE_MODULE_VERSION of the built native add-on won't match up with the Electron runtime version. Since Electron uses a forked version of Node, these version numbers are staggered and never overlap, thus the need to rebuild the native modules specifically for Electron.

One more thing I noticed was that yarn seemed to work better than npm at handling these native Node modules. I'd always get errors about binding files being missing with npm i but yarn seemed to handle them just fine.

Lastly, NodeRT only works from the main process in electron, so you'll need to call it there and do IPC with the render process.

Protocol activation

In the notification's toastXml, you can specify activationType="protocol":

<toast launch="myapp:navigate?key=value" activationType="protocol">
  <visual>
    <binding template="ToastText01">
      <text id="1">Click to open the app</text>
    </binding>
  </visual>
  <actions>
    <action content="Action 1" activationType="protocol" arguments="myapp:action1" />
    <action content="Action 2" activationType="protocol" arguments="myapp:action2" />
  </actions>
</toast>
Enter fullscreen mode Exit fullscreen mode

Note that myapp:action1 is a complete protocol launch URI; myapp is the scheme and action1 is the path. // is only used when specifying the optional authority section.

You can follow the official guide to setting up your app to handle deep protocol links. But one confusing part of that is it shows under the Windows section a listener for the open-url event. But that event just doesn't get fired on Windows; it's only for Mac OS. For Windows, you need to check the process launch arguments in two places:

1) On app launch (if you got the single instance lock).
2) In the second-instance event handler, using the command line arguments passed to it.

The process launch argument can be in a different position when running in developer mode; I found it best to search all the given command line arguments for something that starts with myapp:.

Finally, I would recommend not adding direct activation callbacks to the notifications you are sending; to avoid double-handling with the protocol activations.

Focus workaround

After implementing the protocol activation, I noticed the window wouldn't come to the foreground on clicking the notification, but instead just blink in the taskbar. Eventually I found that this was caused by a bug in Windows. Normally you can call the native AllowSetForegroundWindow method to permit another application to take foreground focus. But when an app is started from a Windows notification activation, this call fails with ERROR_ACCESS_DENIED. One of a list of conditions has to be met for the call to succeed.

I saw that Chromium had worked around this by sending a key press event that satisfies this check and lets AllowSetForegroundWindow succeed.

I published a Node package to work around this, which you can use like so:

import { sendDummyKeystroke } from 'windows-dummy-keystroke';
sendDummyKeystroke();
Enter fullscreen mode Exit fullscreen mode

Call it before requestSingleInstanceLock() so the AllowSetForegroundWindow call it's making will succeed.

Attribution

Windows needs a way to attribute a notification to a particular app, which it uses to group notifications and show them with the app's name and icon.

It does this through the Application User Model ID or AUMID. You are expected to pass your app's AUMID on the ToastNotificationManager.createToastNotifier() call.

If you read the documentation on the name property for the Windows Squirrel maker config, you might be confused:

Windows Application Model ID (appId).
Defaults to the name field in your app's package.json file.

This is not true! This name turns out to just be a single component of the generated AUMID, which is in the form com.squirrel.<makerConfigName>.<packageName> . <packageName> comes from package.json. So you should end up with something like this:

const notifier = ToastNotificationManager.createToastNotifier('com.squirrel.MyApp.MyApp');
notifier.show(toast);
Enter fullscreen mode Exit fullscreen mode

The mechanism for the attribution is shortcut files. Windows will look for a shortcut in the Start Menu that has the System.AppUserModel.ID property. It uses the name and icon from the matching shortcut. Squirrel automatically adds System.AppUserModel.ID on the shortcut it generates. You can check this with the lnk-parser tool. Your shortcuts can be found in %appdata%\Microsoft\Windows\Start Menu.

Example app

I've got a minimal example app to demonstrate the end-to-end protocol launch setup. I don't have this working for Mac OS yet but I think all it needs is an open-url event listener as per the docs.

Recalling notifications

Recalling a notification that you've already sent but is no longer relevant is a really useful feature. It's so annoying when obsolete notifications just languish and force you to clean them up; this is the major reason I went to all this extra trouble with NodeRT.

To be able to recall your notification, you need to set a "tag" and a "group" on the notification when you send it.

const toast = new ToastNotification(xmlDocument);

toast.tag = 'MyTag';
toast.group = 'default';

notifier.show(toast);
Enter fullscreen mode Exit fullscreen mode

The tag you can use to target a specific notification to recall. The group can be any string; we have to specify it because you need to use this specific override of ToastNotifictionHistory.Remove().

ToastNotificationManager.history.remove(
  'MyTag',
  'default',
  'com.squirrel.MyApp.MyApp');
Enter fullscreen mode Exit fullscreen mode

It's important to use the overload that specifies the AUMID, otherwise the call will be rejected with an "Element not found" error indicating that you don't have permissions to mess with notification history.

Scheduling notifications

The ToastNotifier.AddToSchedule() official docs claim that apps not built entirely on WinRT can't call it. But it actually seems to work just fine:

const date = new Date();
date.setMinutes(date.getMinutes() + 10);
const scheduledToast = new ScheduledToastNotification(xmlDocument, date);

notifier.addToSchedule(scheduledToast);
Enter fullscreen mode Exit fullscreen mode

This was handy in my case where I needed to send reminder notifications at specific times but did not want to have to require the app to be running at all times.

There is the risk that Microsoft considers this a bug and "fixes" it to match the documentation, though they've been aware of it since 2019 and have not done anything.

Limitations / Going further

Interactive notifications via COM server

Windows also lets you type directly into a notification, which is something that a pure protocol approach doesn't handle. electron-windows-interactive-notifications ostensibly handles this but it hasn't been updated in a while and the author is not responding to issues. I dove in here for a bit, implemented two workarounds, invented a third but never successfully got my app to launch. My app doesn't really need the interactive notifications, so I abandoned this approach. If you want to try and tackle this, Chromium's notification_helper.exe might be a helpful reference.

Tracking dismissal

In an ideal world we would be able to track when a user has dismissed a Windows notification then update other devices. But the "dismissed" callback function is invoked when the toast message disappears. It fires whether or not the user interacts with it, and there is no indication of whether it was an activation, an explicit dismissal from the user, or just it going away on its own. I dug in the event but found nothing that would tell me.

On top of that, the user might dismiss the notification in the Action center, which does not invoke the dismiss callback at all. I dug around in the WinRT APIs and apparently the correct way to track this is to register a background task with a ToastNotificationHistoryChanged trigger. But there appeared to be no straightforward way to register this from NodeRT and I couldn't find anyone who's actually done this yet.

Top comments (0)