Repository: electron
Issue: [Feature Request]: Update Notifications Tutorial Notes for Windows
Labels: enhancement :sparkles:,documentation :notebook:

### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a feature request that matches the one I want to file, without success.

### Problem Description

The current guide in Notifications under Windows [`Use advanced notifications`](https://www.electronjs.org/docs/latest/tutorial/notifications#use-advanced-notifications) recommend native node libraries that are over 3+ years old.

The guide should be updated to make use of the `Notification` constructor property [`toastXml`](https://www.electronjs.org/docs/latest/api/notification#notificationtoastxml-windows) instead for customizing notifications on Windows, with instructions on how to work with action buttons.

### Proposed Solution

The documentation should reference [`toastXml`](https://www.electronjs.org/docs/latest/api/notification#notificationtoastxml-windows) as the proper way to define a Windows notification with the following notes:

- Users can find the XML schema on the [Microsoft Toast Schema](https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root) site to help them build out the XML
- `app.setAppUserModelId()` must be set to the correct value (if using `electron-builder`, this would be the `appId` value).
  * The id used is what will be used as the notification title in Windows which is not modifiable using the Toast XML
  * If correctly set, the app icon should also appear next to the notification title
- Image references need to start with `file:///` when using the `<image src="<path>" />` tag for resources pulled from the app
  * You can generate the path to append to `file:///` using something like `join(app.getAppPath(), 'resources', 'icon.png').replace('app.asar', 'app.asar.unpacked')` for Windows
- Since this is XML, attribute data and textual content needs to be escaped. I used [`escape-html`](https://www.npmjs.com/package/escape-html) for this.
  * This also means converting `&` to `&amp;` in the query string of your protocol definition
- If the notification doesn't show up, there's probably something wrong with the XML (eg unescaped values, missing required attributes). Not clear how to debug this rather than trial and error at the moment
- Unlike MacOS notifications, the ones in Windows are not tightly-bound to the `Notification` event handlers. Instead, you'll have to use [protocol handling](https://www.electronjs.org/docs/latest/api/protocol) to pass notification data to be handled by the application
  * Be aware of [protocol length limits under Windows](https://github.com/electron/electron/issues/40776)
  * Because Windows will open another app when protocol handling, the user will need the following code to ensure only a single instance runs

```typescript
// main/index.ts
import { app } from 'electron';
import { platform } from '@electron-toolkit/utils';

if (!platform.isMacOS) {
  const gotTheLock = app.requestSingleInstanceLock();

  // Ensure only an instance is running at a time when not on macOS
  if (!gotTheLock) {
    console.log('Another instance is already running, exiting.');
    app.quit();
    return;
  }
}

app.whenReady().then(async () => {
  app.setAppUserModelId(<the app id>);
})
```

Example code to handle notifications:

```typescript
import escape from 'escape-html';
import { Notification } from 'electron';

interface NotificationData {
    id: string;
    title: string;
    body: string;
    actions: Array<string>;
}

const notificationData: NotificationData = {
  // Generate a random value for this
  id: 'abcd-efgh'
  title: 'test title',
  body: 'test body',
  actions: ['Confirm', 'Deny'],
}

// Define an Electron notification for Windows that contains two action buttons
// <toast launch=""> proto handler is for when the user just clicks on the notification
// then we have <action arguments=""> to specify the proto handler for when an action is clicked
// <text> elements with empty content is ignored and not rendered
const notification = new Notification({
{
    toastXml: `
    <toast launch="your-app://notify/?id=${notificationData.id}" activationType="protocol">
        <visual>
          <binding template="ToastGeneric">
            <text>${notificationData.title ? escape(notificationData.title) : ''}</text>
            <text>${escape(notificationData.body)}</text>
          </binding>
        </visual>
        <actions>
          ${notification.actions
            ?.map(
              (actionText, index) =>
                `<action content="${escape(actionText)}" arguments="your-app://notify/?id=${
                  notificationData.id
                }&amp;actionIdx=${index}" activationType="protocol" />`,
            )
            .join('\n')}
        </actions>
    </toast>`,
})

// register your standard event handlers
// This will auto-work in MacOS, but must be triggered manually in windows
// User clicks on an action button
notification.on('action', (_, actionIndex) => {
  if (actionIndex === 0) {
     // Handle Confirm
  } else {
    // Handle Deny
  }
})

// User clicks on the notification itself
notification.on('click', () => {});

// You'll want to save the notification reference somewhere, like a Map<string, Notification>, where the key is the notification id, and the value is the notification you created

const notifications = new Map()

notifications.set(n.id, n);

// In your protocol handling, you'll extract out the id and actionIdx value and read from the map and manually trigger the 
// handlers

export function handleWindowsNotificationAction(id: string, actionIndex: string) {
  const idx = parseInt(actionIndex, 10) ?? -1
  const notification = notifications.get(id)

  if (notification) {
     // We have an action triggered
    if (idx !== -1) {
      notification.emit('action', null, idx);
      return
    }

    // The notification was clicked
    notification.emit('click');

   // we're done with it, remove it from the map
   notifications.delete(id)
  }
}
```

The above example is a bit crude, but shows the general idea of how to implement action buttons in Windows Electron for notifications.

Additional references:

- https://stackoverflow.com/questions/68282036/electron-toast-actions-on-windows
- https://www.sipgate.de/blog/how-to-create-native-notifications-with-action-buttons-on-windows-for-your-electron-app

keywords: Windows / Notification / XML / Toast / toastXml / electron-windows-notifications / electron-windows-interactive-notifications / node-notifier / actions / buttons

### Alternatives Considered

N/A

### Additional Information

_No response_