feat: improve Windows Toast actions support (#48132)

* feat: improve Windows Toast actions support

* fix: ensure MSIX compatibility

* test: add bad clsid format test
This commit is contained in:
Shelley Vohr
2026-02-12 23:25:20 +01:00
committed by GitHub
parent a65cfed500
commit 74fd10450f
20 changed files with 1004 additions and 60 deletions

View File

@@ -1122,6 +1122,19 @@ Updates the current activity if its type matches `type`, merging the entries fro
Changes the [Application User Model ID][app-user-model-id] to `id`.
### `app.setToastActivatorCLSID(id)` _Windows_
* `id` string
Changes the [Toast Activator CLSID][toast-activator-clsid] to `id`. If one is not set via this method, it will be randomly generated for the app.
* The value must be a valid GUID/CLSID in one of the following forms:
* Canonical brace-wrapped: `{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}` (preferred)
* Canonical without braces: `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` (braces will be added automatically)
* Hex digits are case-insensitive.
This method should be called early (before showing notifications) so the value is baked into the registration/shortcut. Supplying an empty string or an unparsable value throws and leaves the existing (or generated) CLSID unchanged. If this method is never called, a random CLSID is generated once per run and exposed via `app.toastActivatorCLSID`.
### `app.setActivationPolicy(policy)` _macOS_
* `policy` string - Can be 'regular', 'accessory', or 'prohibited'.
@@ -1704,8 +1717,13 @@ platforms) that allows you to perform actions on your app icon in the user's doc
A `boolean` property that returns `true` if the app is packaged, `false` otherwise. For many apps, this property can be used to distinguish development and production environments.
### `app.toastActivatorCLSID` _Windows_ _Readonly_
A `string` property that returns the app's [Toast Activator CLSID][toast-activator-clsid].
[tasks]:https://learn.microsoft.com/en-us/windows/win32/shell/taskbar-extensions#tasks
[app-user-model-id]: https://learn.microsoft.com/en-us/windows/win32/shell/appids
[toast-activator-clsid]: https://learn.microsoft.com/en-us/windows/win32/properties/props-system-appusermodel-toastactivatorclsid
[electron-forge]: https://www.electronforge.io/
[electron-packager]: https://github.com/electron/packager
[CFBundleURLTypes]: https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/TP40009249-102207-TPXREF115

View File

@@ -67,6 +67,22 @@ Emitted when the notification is shown to the user. Note that this event can be
multiple times as a notification can be shown multiple times through the
`show()` method.
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const n = new Notification({
title: 'Title!',
subtitle: 'Subtitle!',
body: 'Body!'
})
n.on('show', () => console.log('Notification shown!'))
n.show()
})
```
#### Event: 'click'
Returns:
@@ -75,6 +91,22 @@ Returns:
Emitted when the notification is clicked by the user.
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const n = new Notification({
title: 'Title!',
subtitle: 'Subtitle!',
body: 'Body!'
})
n.on('click', () => console.log('Notification clicked!'))
n.show()
})
```
#### Event: 'close'
Returns:
@@ -88,21 +120,85 @@ is closed.
On Windows, the `close` event can be emitted in one of three ways: programmatic dismissal with `notification.close()`, by the user closing the notification, or via system timeout. If a notification is in the Action Center after the initial `close` event is emitted, a call to `notification.close()` will remove the notification from the action center but the `close` event will not be emitted again.
#### Event: 'reply' _macOS_
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const n = new Notification({
title: 'Title!',
subtitle: 'Subtitle!',
body: 'Body!'
})
n.on('close', () => console.log('Notification closed!'))
n.show()
})
```
#### Event: 'reply' _macOS_ _Windows_
Returns:
* `event` Event
* `reply` string - The string the user entered into the inline reply field.
* `details` Event\<\>
* `reply` string - The string the user entered into the inline reply field.
* `reply` string _Deprecated_
Emitted when the user clicks the "Reply" button on a notification with `hasReply: true`.
#### Event: 'action' _macOS_
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const n = new Notification({
title: 'Send a Message',
body: 'Body Text',
hasReply: true,
replyPlaceholder: 'Message text...'
})
n.on('reply', (e, reply) => console.log(`User replied: ${reply}`))
n.on('click', () => console.log('Notification clicked'))
n.show()
})
```
#### Event: 'action' _macOS_ _Windows_
Returns:
* `event` Event
* `index` number - The index of the action that was activated.
* `details` Event\<\>
* `actionIndex` number - The index of the action that was activated.
* `selectionIndex` number _Windows_ - The index of the selected item, if one was chosen. -1 if none was chosen.
* `actionIndex` number _Deprecated_
* `selectionIndex` number _Windows_ _Deprecated_
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const items = ['One', 'Two', 'Three']
const n = new Notification({
title: 'Choose an Action!',
actions: [
{ type: 'button', text: 'Action 1' },
{ type: 'button', text: 'Action 2' },
{ type: 'selection', text: 'Apply', items }
]
})
n.on('click', () => console.log('Notification clicked'))
n.on('action', (e) => {
console.log(`User triggered action at index: ${e.actionIndex}`)
if (e.selectionIndex > -1) {
console.log(`User chose selection item '${items[e.selectionIndex]}'`)
}
})
n.show()
})
```
#### Event: 'failed' _Windows_
@@ -113,6 +209,22 @@ Returns:
Emitted when an error is encountered while creating and showing the native notification.
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const n = new Notification({
title: 'Bad Action'
})
n.on('failed', (e, err) => {
console.log('Notification failed: ', err)
})
n.show()
})
```
### Instance Methods
Objects created with the `new Notification()` constructor have the following instance methods:
@@ -126,12 +238,42 @@ call this method before the OS will display it.
If the notification has been shown before, this method will dismiss the previously
shown notification and create a new one with identical properties.
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const n = new Notification({
title: 'Title!',
subtitle: 'Subtitle!',
body: 'Body!'
})
n.show()
})
```
#### `notification.close()`
Dismisses the notification.
On Windows, calling `notification.close()` while the notification is visible on screen will dismiss the notification and remove it from the Action Center. If `notification.close()` is called after the notification is no longer visible on screen, calling `notification.close()` will try remove it from the Action Center.
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const n = new Notification({
title: 'Title!',
subtitle: 'Subtitle!',
body: 'Body!'
})
n.show()
setTimeout(() => n.close(), 5000)
})
```
### Instance Properties
#### `notification.title`

View File

@@ -1,13 +1,15 @@
# NotificationAction Object
* `type` string - The type of action, can be `button`.
* `type` string - The type of action, can be `button` or `selection`. `selection` is only supported on Windows.
* `text` string (optional) - The label for the given action.
* `items` string[] (optional) _Windows_ - The list of items for the `selection` action `type`.
## Platform / Action Support
| Action Type | Platform Support | Usage of `text` | Default `text` | Limitations |
|-------------|------------------|-----------------|----------------|-------------|
| `button` | macOS | Used as the label for the button | "Show" (or a localized string by system default if first of such `button`, otherwise empty) | Only the first one is used. If multiple are provided, those beyond the first will be listed as additional actions (displayed when mouse active over the action button). Any such action also is incompatible with `hasReply` and will be ignored if `hasReply` is `true`. |
| `button` | macOS, Windows | Used as the label for the button | "Show" on macOS (localized) if first `button`, otherwise empty; Windows uses provided `text` | macOS: Only the first one is used as primary; others shown as additional actions (hover). Incompatible with `hasReply` (beyond first ignored). |
| `selection` | Windows | Used as the label for the submit button for the selection menu | "Select" | Requires an `items` array property specifying option labels. Emits the `action` event with `(index, selectedIndex)` where `selectedIndex` is the chosen option (>= 0). Ignored on platforms that do not support selection actions. |
### Button support on macOS
@@ -18,3 +20,34 @@ following criteria.
* App has its `NSUserNotificationAlertStyle` set to `alert` in the `Info.plist`.
If either of these requirements are not met the button won't appear.
### Selection support on Windows
To add a selection (combo box) style action, include an action with `type: 'selection'`, a `text` label for the submit button, and an `items` array of strings:
```js
const { Notification, app } = require('electron')
app.whenReady().then(() => {
const items = ['One', 'Two', 'Three']
const n = new Notification({
title: 'Choose an option',
actions: [{
type: 'selection',
text: 'Apply',
items
}]
})
n.on('action', (e) => {
console.log(`User triggered action at index: ${e.actionIndex}`)
if (e.selectionIndex > 0) {
console.log(`User chose selection item '${items[e.selectionIndex]}'`)
}
})
n.show()
})
```
When the user activates the selection action, the notification's `action` event will be emitted with two parameters: `actionIndex` (the action's index in the `actions` array) and `selectedIndex` (the zero-based index of the chosen item, or `-1` if unavailable). On non-Windows platforms selection actions are ignored.

View File

@@ -79,6 +79,8 @@ filenames = {
"shell/browser/notifications/win/notification_presenter_win.h",
"shell/browser/notifications/win/windows_toast_notification.cc",
"shell/browser/notifications/win/windows_toast_notification.h",
"shell/browser/notifications/win/windows_toast_activator.cc",
"shell/browser/notifications/win/windows_toast_activator.h",
"shell/browser/relauncher_win.cc",
"shell/browser/ui/certificate_trust_win.cc",
"shell/browser/ui/file_dialog_win.cc",

View File

@@ -88,6 +88,7 @@
#if BUILDFLAG(IS_WIN)
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include "shell/browser/ui/win/jump_list.h"
#endif
@@ -1840,6 +1841,10 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
#if BUILDFLAG(IS_WIN)
.SetMethod("setAppUserModelId",
base::BindRepeating(&Browser::SetAppUserModelID, browser))
.SetMethod("setToastActivatorCLSID",
base::BindRepeating(&App::SetToastActivatorCLSID,
base::Unretained(this)))
.SetProperty("toastActivatorCLSID", &App::GetToastActivatorCLSID)
#endif
.SetMethod(
"isDefaultProtocolClient",
@@ -1967,6 +1972,34 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
.SetMethod("resolveProxy", &App::ResolveProxy);
}
#if BUILDFLAG(IS_WIN)
void App::SetToastActivatorCLSID(gin_helper::ErrorThrower thrower,
const std::string& id) {
std::wstring wide = base::UTF8ToWide(id);
CLSID parsed;
if (FAILED(::CLSIDFromString(wide.c_str(), &parsed))) {
if (!wide.empty() && wide.front() != L'{') {
std::wstring with_braces = L"{" + wide + L"}";
if (FAILED(::CLSIDFromString(with_braces.c_str(), &parsed))) {
thrower.ThrowError("Invalid CLSID format");
return;
}
wide = std::move(with_braces);
} else {
thrower.ThrowError("Invalid CLSID format");
return;
}
}
SetAppToastActivatorCLSID(wide);
}
v8::Local<v8::Value> App::GetToastActivatorCLSID(v8::Isolate* isolate) {
return gin::ConvertToV8(isolate,
base::WideToUTF8(GetAppToastActivatorCLSID()));
}
#endif
const char* App::GetHumanReadableName() const {
return "Electron / App";
}

View File

@@ -265,6 +265,12 @@ class App final : public gin::Wrappable<App>,
// Set or remove a custom Jump List for the application.
JumpListResult SetJumpList(v8::Isolate* isolate, v8::Local<v8::Value> val);
// Set the toast activator CLSID.
void SetToastActivatorCLSID(gin_helper::ErrorThrower thrower,
const std::string& id);
// Get the toast activator CLSID.
v8::Local<v8::Value> GetToastActivatorCLSID(v8::Isolate* isolate);
#endif // BUILDFLAG(IS_WIN)
std::unique_ptr<ProcessSingleton> process_singleton_;

View File

@@ -31,6 +31,9 @@ struct Converter<electron::NotificationAction> {
return false;
}
dict.Get("text", &(out->text));
std::vector<std::u16string> items;
if (dict.Get("items", &items))
out->items = std::move(items);
return true;
}
@@ -39,6 +42,9 @@ struct Converter<electron::NotificationAction> {
auto dict = gin::Dictionary::CreateEmpty(isolate);
dict.Set("text", val.text);
dict.Set("type", val.type);
if (!val.items.empty()) {
dict.Set("items", val.items);
}
return ConvertToV8(isolate, dict);
}
};
@@ -138,8 +144,20 @@ void Notification::SetToastXml(const std::u16string& new_toast_xml) {
toast_xml_ = new_toast_xml;
}
void Notification::NotificationAction(int index) {
Emit("action", index);
void Notification::NotificationAction(int action_index, int selection_index) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin_helper::internal::Event* event =
gin_helper::internal::Event::New(isolate);
v8::Local<v8::Object> event_object =
event->GetWrapper(isolate).ToLocalChecked();
gin_helper::Dictionary dict(isolate, event_object);
dict.Set("selectionIndex", selection_index);
dict.Set("actionIndex", action_index);
EmitWithoutEvent("action", event_object, action_index, selection_index);
}
void Notification::NotificationClick() {
@@ -147,7 +165,18 @@ void Notification::NotificationClick() {
}
void Notification::NotificationReplied(const std::string& reply) {
Emit("reply", reply);
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
gin_helper::internal::Event* event =
gin_helper::internal::Event::New(isolate);
v8::Local<v8::Object> event_object =
event->GetWrapper(isolate).ToLocalChecked();
gin_helper::Dictionary dict(isolate, event_object);
dict.Set("reply", reply);
EmitWithoutEvent("reply", event_object, reply);
}
void Notification::NotificationDisplayed() {

View File

@@ -45,7 +45,7 @@ class Notification final : public gin_helper::DeprecatedWrappable<Notification>,
static const char* GetClassName() { return "Notification"; }
// NotificationDelegate:
void NotificationAction(int index) override;
void NotificationAction(int action_index, int selection_index) override;
void NotificationClick() override;
void NotificationReplied(const std::string& reply) override;
void NotificationDisplayed() override;

View File

@@ -139,7 +139,7 @@ void CocoaNotification::NotificationReplied(const std::string& reply) {
void CocoaNotification::NotificationActivated() {
if (delegate())
delegate()->NotificationAction(action_index_);
delegate()->NotificationAction(action_index_, -1);
this->LogAction("button clicked");
}
@@ -156,7 +156,7 @@ void CocoaNotification::NotificationActivated(
}
}
delegate()->NotificationAction(index);
delegate()->NotificationAction(index, -1);
}
this->LogAction("button clicked");

View File

@@ -22,6 +22,15 @@ NotificationOptions& NotificationOptions::operator=(NotificationOptions&&) =
default;
NotificationOptions::~NotificationOptions() = default;
NotificationAction::NotificationAction() = default;
NotificationAction::~NotificationAction() = default;
NotificationAction::NotificationAction(const NotificationAction&) = default;
NotificationAction& NotificationAction::operator=(const NotificationAction&) =
default;
NotificationAction::NotificationAction(NotificationAction&&) noexcept = default;
NotificationAction& NotificationAction::operator=(
NotificationAction&&) noexcept = default;
Notification::Notification(NotificationDelegate* delegate,
NotificationPresenter* presenter)
: delegate_(delegate), presenter_(presenter) {}

View File

@@ -23,6 +23,14 @@ class NotificationPresenter;
struct NotificationAction {
std::u16string type;
std::u16string text;
std::vector<std::u16string> items;
NotificationAction();
~NotificationAction();
NotificationAction(const NotificationAction&);
NotificationAction& operator=(const NotificationAction&);
NotificationAction(NotificationAction&&) noexcept;
NotificationAction& operator=(NotificationAction&&) noexcept;
};
struct NotificationOptions {

View File

@@ -19,7 +19,9 @@ class NotificationDelegate {
// Notification was replied to
virtual void NotificationReplied(const std::string& reply) {}
virtual void NotificationAction(int index) {}
// |selection_index| is >= 0 only for selection actions (Windows), otherwise
// -1.
virtual void NotificationAction(int action_index, int selection_index = -1) {}
virtual void NotificationClick() {}
virtual void NotificationClosed() {}

View File

@@ -16,6 +16,7 @@
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include "shell/browser/notifications/win/windows_toast_notification.h"
#include "shell/common/thread_restrictions.h"
#include "third_party/skia/include/core/SkBitmap.h"
@@ -45,6 +46,9 @@ std::unique_ptr<NotificationPresenter> NotificationPresenter::Create() {
if (!presenter->Init())
return {};
// Ensure COM toast activator is registered once the presenter is ready.
NotificationActivator::RegisterActivator();
if (electron::debug_notifications)
LOG(INFO) << "Successfully created Windows notifications presenter";

View File

@@ -0,0 +1,429 @@
// Copyright (c) 2025 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include <propkey.h>
#include <propvarutil.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <wrl/module.h>
#ifdef StrCat
// Undefine Windows shlwapi.h StrCat macro to avoid conflict with
// base/strings/strcat.h which deliberately defines a StrCat macro sentinel.
#undef StrCat
#endif
#include <string>
#include <vector>
#include <algorithm>
#include <atomic>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions_win.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/registry.h"
#include "base/win/scoped_propvariant.h"
#include "base/win/win_util.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "shell/browser/api/electron_api_notification.h" // nogncheck - for delegate events
#include "shell/browser/electron_browser_client.h"
#include "shell/browser/notifications/notification.h"
#include "shell/browser/notifications/notification_delegate.h"
#include "shell/browser/notifications/notification_presenter.h"
#include "shell/browser/notifications/win/notification_presenter_win.h"
#include "shell/common/application_info.h"
namespace electron {
namespace {
class NotificationActivatorFactory final : public IClassFactory {
public:
NotificationActivatorFactory() : ref_count_(1) {}
~NotificationActivatorFactory() = default;
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) override {
if (!ppv)
return E_POINTER;
if (riid == IID_IUnknown || riid == IID_IClassFactory) {
*ppv = static_cast<IClassFactory*>(this);
} else {
*ppv = nullptr;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
IFACEMETHODIMP_(ULONG) AddRef() override { return ++ref_count_; }
IFACEMETHODIMP_(ULONG) Release() override {
ULONG res = --ref_count_;
if (res == 0)
delete this;
return res;
}
IFACEMETHODIMP CreateInstance(IUnknown* outer,
REFIID riid,
void** ppv) override {
if (outer)
return CLASS_E_NOAGGREGATION;
auto activator = Microsoft::WRL::Make<NotificationActivator>();
return activator.CopyTo(riid, ppv);
}
IFACEMETHODIMP LockServer(BOOL) override { return S_OK; }
private:
std::atomic<ULONG> ref_count_;
};
NotificationActivatorFactory* g_factory_instance = nullptr;
bool g_registration_in_progress = false;
std::wstring GetExecutablePath() {
wchar_t path[MAX_PATH];
DWORD len = ::GetModuleFileNameW(nullptr, path, std::size(path));
if (len == 0 || len == std::size(path))
return L"";
return std::wstring(path, len);
}
void EnsureCLSIDRegistry() {
std::wstring exe = GetExecutablePath();
if (exe.empty())
return;
std::wstring clsid = GetAppToastActivatorCLSID();
std::wstring key_path = L"Software\\Classes\\CLSID\\" + clsid;
base::win::RegKey key(HKEY_CURRENT_USER, key_path.c_str(), KEY_SET_VALUE);
if (!key.Valid())
return;
key.WriteValue(nullptr, L"Electron Notification Activator");
key.WriteValue(L"CustomActivator", 1U);
std::wstring local_server = key_path + L"\\LocalServer32";
base::win::RegKey server_key(HKEY_CURRENT_USER, local_server.c_str(),
KEY_SET_VALUE);
if (!server_key.Valid())
return;
server_key.WriteValue(nullptr, exe.c_str());
}
bool ExistingShortcutValid(const base::FilePath& lnk_path, PCWSTR aumid) {
if (!base::PathExists(lnk_path))
return false;
Microsoft::WRL::ComPtr<IShellLink> existing;
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&existing))))
return false;
Microsoft::WRL::ComPtr<IPersistFile> pf;
if (FAILED(existing.As(&pf)) ||
FAILED(pf->Load(lnk_path.value().c_str(), STGM_READ))) {
return false;
}
Microsoft::WRL::ComPtr<IPropertyStore> store;
if (FAILED(existing.As(&store)))
return false;
base::win::ScopedPropVariant pv_id;
base::win::ScopedPropVariant pv_clsid;
if (FAILED(store->GetValue(PKEY_AppUserModel_ID, pv_id.Receive())) ||
FAILED(store->GetValue(PKEY_AppUserModel_ToastActivatorCLSID,
pv_clsid.Receive()))) {
return false;
}
if (pv_id.get().vt == VT_LPWSTR && pv_clsid.get().vt == VT_CLSID &&
pv_id.get().pwszVal && pv_clsid.get().puuid) {
CLSID desired;
if (SUCCEEDED(::CLSIDFromString(GetAppToastActivatorCLSID(), &desired))) {
return _wcsicmp(pv_id.get().pwszVal, aumid) == 0 &&
IsEqualGUID(*pv_clsid.get().puuid, desired);
}
}
return false;
}
void EnsureShortcut() {
PCWSTR aumid = GetRawAppUserModelID();
if (!aumid || !*aumid)
return;
std::wstring exe = GetExecutablePath();
if (exe.empty())
return;
PWSTR programs_path = nullptr;
if (FAILED(
SHGetKnownFolderPath(FOLDERID_Programs, 0, nullptr, &programs_path)))
return;
base::FilePath programs(programs_path);
CoTaskMemFree(programs_path);
std::wstring product_name = base::UTF8ToWide(GetApplicationName());
if (product_name.empty())
product_name = L"ElectronApp";
base::CreateDirectory(programs);
base::FilePath lnk_path = programs.Append(product_name + L".lnk");
if (base::PathExists(lnk_path)) {
Microsoft::WRL::ComPtr<IShellLink> existing;
if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&existing)))) {
Microsoft::WRL::ComPtr<IPersistFile> pf;
if (SUCCEEDED(existing.As(&pf)) &&
SUCCEEDED(pf->Load(lnk_path.value().c_str(), STGM_READ))) {
Microsoft::WRL::ComPtr<IPropertyStore> store;
if (SUCCEEDED(existing.As(&store))) {
base::win::ScopedPropVariant pv_clsid;
if (SUCCEEDED(store->GetValue(PKEY_AppUserModel_ToastActivatorCLSID,
pv_clsid.Receive())) &&
pv_clsid.get().vt == VT_CLSID && pv_clsid.get().puuid) {
wchar_t buf[64] = {0};
if (StringFromGUID2(*pv_clsid.get().puuid, buf, std::size(buf)) >
0) {
SetAppToastActivatorCLSID(buf);
}
}
}
}
}
}
if (ExistingShortcutValid(lnk_path, aumid))
return;
Microsoft::WRL::ComPtr<IShellLink> shell_link;
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&shell_link))))
return;
shell_link->SetPath(exe.c_str());
shell_link->SetArguments(L"");
shell_link->SetDescription(product_name.c_str());
shell_link->SetWorkingDirectory(
base::FilePath(exe).DirName().value().c_str());
Microsoft::WRL::ComPtr<IPropertyStore> prop_store;
if (SUCCEEDED(shell_link.As(&prop_store))) {
PROPVARIANT pv_id;
PropVariantInit(&pv_id);
if (SUCCEEDED(InitPropVariantFromString(aumid, &pv_id))) {
prop_store->SetValue(PKEY_AppUserModel_ID, pv_id);
PropVariantClear(&pv_id);
}
PROPVARIANT pv_clsid;
PropVariantInit(&pv_clsid);
GUID clsid;
if (SUCCEEDED(::CLSIDFromString(GetAppToastActivatorCLSID(), &clsid)) &&
SUCCEEDED(InitPropVariantFromCLSID(clsid, &pv_clsid))) {
prop_store->SetValue(PKEY_AppUserModel_ToastActivatorCLSID, pv_clsid);
PropVariantClear(&pv_clsid);
}
prop_store->Commit();
}
Microsoft::WRL::ComPtr<IPersistFile> persist_file;
if (SUCCEEDED(shell_link.As(&persist_file))) {
persist_file->Save(lnk_path.value().c_str(), TRUE);
}
}
} // namespace
DWORD NotificationActivator::g_cookie_ = 0;
bool NotificationActivator::g_registered_ = false;
NotificationActivator::NotificationActivator() = default;
NotificationActivator::~NotificationActivator() = default;
// static
// static
void NotificationActivator::RegisterActivator() {
if (g_registered_ || g_registration_in_progress)
return;
g_registration_in_progress = true;
// For packaged (MSIX) apps, COM server registration is handled via the app
// manifest (com:Extension and desktop:ToastNotificationActivation), so we
// skip the registry and shortcut creation. We still need to call
// CoRegisterClassObject to handle activations at runtime.
//
// For unpackaged apps, we need to create the Start Menu shortcut with
// AUMID/CLSID properties and register the COM server in the registry.
bool is_packaged = IsRunningInDesktopBridge();
// Perform all blocking filesystem / registry work off the UI thread.
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(
[](bool is_packaged) {
// Skip shortcut and registry setup for packaged apps - these are
// declared in the app manifest instead.
if (!is_packaged) {
EnsureShortcut();
EnsureCLSIDRegistry();
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce([]() {
g_registration_in_progress = false;
if (g_registered_)
return;
CLSID clsid;
if (FAILED(::CLSIDFromString(GetAppToastActivatorCLSID(),
&clsid))) {
LOG(ERROR) << "Invalid toast activator CLSID";
return;
}
if (!g_factory_instance)
g_factory_instance =
new (std::nothrow) NotificationActivatorFactory();
if (!g_factory_instance)
return;
DWORD cookie = 0;
HRESULT hr = ::CoRegisterClassObject(
clsid, static_cast<IClassFactory*>(g_factory_instance),
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED, &cookie);
if (FAILED(hr))
return;
hr = ::CoResumeClassObjects();
if (FAILED(hr)) {
::CoRevokeClassObject(cookie);
return;
}
g_cookie_ = cookie;
g_registered_ = true;
}));
},
is_packaged));
}
// static
void NotificationActivator::UnregisterActivator() {
if (!g_registered_)
return;
::CoRevokeClassObject(g_cookie_);
g_cookie_ = 0;
g_registered_ = false;
// Factory instance lifetime intentionally leaked after revoke to avoid
// race; could be deleted here if ensured no pending COM calls.
}
IFACEMETHODIMP NotificationActivator::Activate(
LPCWSTR app_user_model_id,
LPCWSTR invoked_args,
const NOTIFICATION_USER_INPUT_DATA* data,
ULONG data_count) {
std::wstring args = invoked_args ? invoked_args : L"";
std::wstring aumid = app_user_model_id ? app_user_model_id : L"";
std::vector<ActivationUserInput> copied_inputs;
if (data && data_count) {
std::vector<NOTIFICATION_USER_INPUT_DATA> temp;
temp.resize(static_cast<size_t>(data_count));
std::copy_n(data, static_cast<size_t>(data_count), temp.begin());
copied_inputs.reserve(temp.size());
for (const auto& entry : temp) {
ActivationUserInput ui;
if (entry.Key)
ui.key = entry.Key;
if (entry.Value)
ui.value = entry.Value;
copied_inputs.push_back(std::move(ui));
}
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&HandleToastActivation, std::move(args),
std::move(copied_inputs)));
return S_OK;
}
void HandleToastActivation(const std::wstring& invoked_args,
std::vector<ActivationUserInput> inputs) {
// Expected invoked_args format:
// type=<click|action|reply>&action=<index>&tag=<hash> Parse simple key=value
// pairs separated by '&'.
std::wstring args = invoked_args;
std::wstring type;
std::wstring action_index_str;
std::wstring tag_str;
for (const auto& token : base::SplitString(args, L"&", base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
auto kv = base::SplitString(token, L"=", base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
if (kv.size() != 2)
continue;
if (kv[0] == L"type")
type = kv[1];
else if (kv[0] == L"action")
action_index_str = kv[1];
else if (kv[0] == L"tag")
tag_str = kv[1];
}
int action_index = -1;
if (!action_index_str.empty()) {
action_index = std::stoi(action_index_str);
}
std::string reply_text;
for (const auto& entry : inputs) {
std::wstring_view key_view(entry.key);
if (key_view == L"reply") {
reply_text = base::WideToUTF8(entry.value);
}
}
auto* browser_client =
static_cast<ElectronBrowserClient*>(ElectronBrowserClient::Get());
if (!browser_client)
return;
NotificationPresenter* presenter = browser_client->GetNotificationPresenter();
if (!presenter)
return;
Notification* target = nullptr;
for (auto* n : presenter->notifications()) {
std::wstring tag_hash =
base::NumberToWString(base::FastHash(n->notification_id()));
if (tag_hash == tag_str) {
target = n;
break;
}
}
if (!target)
return;
if (type == L"action" && target->delegate()) {
int selection_index = -1;
for (const auto& entry : inputs) {
std::wstring_view key_view(entry.key);
if (base::StartsWith(key_view, L"selection",
base::CompareCase::SENSITIVE)) {
int parsed = -1;
if (base::StringToInt(entry.value, &parsed) && parsed >= 0)
selection_index = parsed;
break;
}
}
if (action_index >= 0)
target->delegate()->NotificationAction(action_index, selection_index);
} else if ((type == L"reply" || (!reply_text.empty() && type.empty())) &&
target->delegate()) {
target->delegate()->NotificationReplied(reply_text);
} else if ((type == L"click" || type.empty()) && target->delegate()) {
target->NotificationClicked();
}
}
} // namespace electron

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2025 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_NOTIFICATIONS_WIN_WINDOWS_TOAST_ACTIVATOR_H_
#define ELECTRON_SHELL_BROWSER_NOTIFICATIONS_WIN_WINDOWS_TOAST_ACTIVATOR_H_
#include <NotificationActivationCallback.h>
#include <windows.h>
#include <wrl/implements.h>
#include <string>
#include <vector>
namespace electron {
class NotificationPresenterWin;
class NotificationActivator
: public Microsoft::WRL::RuntimeClass<
Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
INotificationActivationCallback> {
public:
NotificationActivator();
~NotificationActivator() override;
IFACEMETHODIMP Activate(LPCWSTR app_user_model_id,
LPCWSTR invoked_args,
const NOTIFICATION_USER_INPUT_DATA* data,
ULONG data_count) override;
static void RegisterActivator();
static void UnregisterActivator();
private:
static DWORD g_cookie_;
static bool g_registered_;
};
struct ActivationUserInput {
std::wstring key;
std::wstring value;
};
void HandleToastActivation(const std::wstring& invoked_args,
std::vector<ActivationUserInput> inputs);
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_NOTIFICATIONS_WIN_WINDOWS_TOAST_ACTIVATOR_H_

View File

@@ -111,43 +111,41 @@ const std::string FailureResultToString(HRESULT failure_reason) {
return hresult_str;
}
constexpr char kPlaceholderContent[] = "placeHolderContent";
constexpr char kContent[] = "content";
constexpr char kToast[] = "toast";
constexpr char kVisual[] = "visual";
constexpr char kBinding[] = "binding";
constexpr char kTemplate[] = "template";
constexpr char kToastText01[] = "ToastText01";
constexpr char kToastText02[] = "ToastText02";
constexpr char kToastImageAndText01[] = "ToastImageAndText01";
constexpr char kToastImageAndText02[] = "ToastImageAndText02";
constexpr char kToastTemplate[] = "ToastGeneric";
constexpr char kText[] = "text";
constexpr char kImage[] = "image";
constexpr char kPlacement[] = "placement";
constexpr char kAppLogoOverride[] = "appLogoOverride";
constexpr char kHintCrop[] = "hint-crop";
constexpr char kHintInputId[] = "hint-inputId";
constexpr char kHintCropNone[] = "none";
constexpr char kSrc[] = "src";
constexpr char kAudio[] = "audio";
constexpr char kSilent[] = "silent";
constexpr char kReply[] = "reply";
constexpr char kTrue[] = "true";
constexpr char kID[] = "id";
constexpr char kInput[] = "input";
constexpr char kType[] = "type";
constexpr char kSelection[] = "selection";
constexpr char kScenario[] = "scenario";
constexpr char kReminder[] = "reminder";
constexpr char kActions[] = "actions";
constexpr char kAction[] = "action";
constexpr char kActivationType[] = "activationType";
constexpr char kSystem[] = "system";
constexpr char kActivationTypeForeground[] = "foreground";
constexpr char kActivationTypeSystem[] = "system";
constexpr char kArguments[] = "arguments";
constexpr char kDismiss[] = "dismiss";
// The XML version header that has to be stripped from the output.
constexpr char kXmlVersionHeader[] = "<?xml version=\"1.0\"?>\n";
const char* GetTemplateType(bool two_lines, bool has_icon) {
if (has_icon) {
return two_lines ? kToastImageAndText02 : kToastImageAndText01;
}
return two_lines ? kToastText02 : kToastText01;
}
} // namespace
// static
@@ -171,8 +169,9 @@ WindowsToastNotification::GetToastTaskRunner() {
}
bool WindowsToastNotification::Initialize() {
// Just initialize, don't care if it fails or already initialized.
Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
HRESULT hr = Windows::Foundation::Initialize(RO_INIT_SINGLETHREADED);
if (FAILED(hr))
Windows::Foundation::Initialize(RO_INIT_MULTITHREADED);
ScopedHString toast_manager_str(
RuntimeClass_Windows_UI_Notifications_ToastNotificationManager);
@@ -273,8 +272,8 @@ void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
DebugLog("CreateToastXmlOnBackgroundThread called");
ComPtr<IXmlDocument> toast_xml;
if (!CreateToastXmlDocument(options, presenter, weak_notification,
ui_task_runner, &toast_xml)) {
if (!CreateToastXmlDocument(options, presenter, notification_id,
weak_notification, ui_task_runner, &toast_xml)) {
return; // Error already posted to UI thread
}
@@ -300,13 +299,14 @@ void WindowsToastNotification::CreateToastNotificationOnBackgroundThread(
bool WindowsToastNotification::CreateToastXmlDocument(
const NotificationOptions& options,
NotificationPresenter* presenter,
const std::string& notification_id,
base::WeakPtr<Notification> weak_notification,
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
ComPtr<IXmlDocument>* toast_xml) {
// The custom xml takes priority over the preset template.
if (!options.toast_xml.empty()) {
DebugLog(base::StrCat({"Processing custom toast_xml, length: ",
base::NumberToString(options.toast_xml.length())}));
DebugLog("Toast XML (custom) id=" + notification_id + ": " +
base::UTF16ToUTF8(options.toast_xml));
HRESULT hr = XmlDocumentFromString(base::as_wcstr(options.toast_xml),
toast_xml->GetAddressOf());
DebugLog(base::StrCat({"XmlDocumentFromString returned HRESULT: ",
@@ -316,7 +316,6 @@ bool WindowsToastNotification::CreateToastXmlDocument(
base::StrCat({"XML: Invalid XML, ERROR ", FailureResultToString(hr)});
DebugLog(base::StrCat({"XML parsing failed, posting error: ", err}));
PostNotificationFailedToUIThread(weak_notification, err, ui_task_runner);
DebugLog("PostNotificationFailedToUIThread called");
return false;
}
DebugLog("XML parsing succeeded");
@@ -325,8 +324,13 @@ bool WindowsToastNotification::CreateToastXmlDocument(
std::wstring icon_path =
presenter_win->SaveIconToFilesystem(options.icon, options.icon_url);
std::u16string toast_xml_str =
GetToastXml(options.title, options.msg, icon_path, options.timeout_type,
options.silent);
GetToastXml(notification_id, options.title, options.msg, icon_path,
options.timeout_type, options.silent, options.actions,
options.has_reply, options.reply_placeholder);
DebugLog("Toast XML (generated) id=" + notification_id + ": " +
base::UTF16ToUTF8(toast_xml_str));
HRESULT hr = XmlDocumentFromString(base::as_wcstr(toast_xml_str),
toast_xml->GetAddressOf());
if (FAILED(hr)) {
@@ -523,11 +527,15 @@ void WindowsToastNotification::Dismiss() {
}
std::u16string WindowsToastNotification::GetToastXml(
const std::string& notification_id,
const std::u16string& title,
const std::u16string& msg,
const std::wstring& icon_path,
const std::u16string& timeout_type,
bool silent) {
bool silent,
const std::vector<NotificationAction>& actions,
bool has_reply,
const std::u16string& reply_placeholder) {
XmlWriter xml_writer;
xml_writer.StartWriting();
@@ -543,36 +551,33 @@ std::u16string WindowsToastNotification::GetToastXml(
xml_writer.StartElement(kVisual);
// <binding template="<template>">
xml_writer.StartElement(kBinding);
const bool two_lines = (!title.empty() && !msg.empty());
xml_writer.AddAttribute(kTemplate,
GetTemplateType(two_lines, !icon_path.empty()));
xml_writer.AddAttribute(kTemplate, kToastTemplate);
// Add text nodes.
std::u16string line1;
std::u16string line2;
if (title.empty() || msg.empty()) {
line1 = title.empty() ? msg : title;
if (line1.empty())
line1 = u"[no message]";
// <text>
xml_writer.StartElement(kText);
xml_writer.AddAttribute(kID, "1");
xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
xml_writer.EndElement(); // </text>
} else {
line1 = title;
line2 = msg;
// <text>
xml_writer.StartElement(kText);
xml_writer.AddAttribute(kID, "1");
xml_writer.AppendElementContent(base::UTF16ToUTF8(line1));
xml_writer.EndElement();
xml_writer.EndElement(); // </text>
// <text>
xml_writer.StartElement(kText);
xml_writer.AddAttribute(kID, "2");
xml_writer.AppendElementContent(base::UTF16ToUTF8(line2));
xml_writer.EndElement();
xml_writer.EndElement(); // </text>
}
// Optional icon as app logo override (small icon).
if (!icon_path.empty()) {
// <image>
xml_writer.StartElement(kImage);
xml_writer.AddAttribute(kID, "1");
xml_writer.AddAttribute(kPlacement, kAppLogoOverride);
@@ -584,15 +589,85 @@ std::u16string WindowsToastNotification::GetToastXml(
xml_writer.EndElement(); // </binding>
xml_writer.EndElement(); // </visual>
// <actions> (only to ensure reminder has a dismiss button).
if (is_reminder) {
if (is_reminder || has_reply || !actions.empty()) {
// <actions>
xml_writer.StartElement(kActions);
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kSystem);
xml_writer.AddAttribute(kArguments, kDismiss);
xml_writer.AddAttribute(
"content", base::WideToUTF8(l10n_util::GetWideString(IDS_APP_CLOSE)));
xml_writer.EndElement(); // </action>
if (is_reminder) {
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeSystem);
xml_writer.AddAttribute(kArguments, kDismiss);
xml_writer.AddAttribute(
kContent, base::WideToUTF8(l10n_util::GetWideString(IDS_APP_CLOSE)));
xml_writer.EndElement(); // </action>
}
if (has_reply) {
// <input>
xml_writer.StartElement(kInput);
xml_writer.AddAttribute(kID, kReply);
xml_writer.AddAttribute(kType, kText);
if (!reply_placeholder.empty()) {
xml_writer.AddAttribute(kPlaceholderContent,
base::UTF16ToUTF8(reply_placeholder));
}
xml_writer.EndElement(); // </input>
}
for (size_t i = 0; i < actions.size(); ++i) {
const auto& act = actions[i];
if (act.type == u"button" || act.type.empty()) {
// <action>
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args = base::StrCat(
{"type=action&action=", base::NumberToString(i),
"&tag=", base::NumberToString(base::FastHash(notification_id))});
xml_writer.AddAttribute(kArguments, args.c_str());
xml_writer.AddAttribute(kContent, base::UTF16ToUTF8(act.text));
xml_writer.EndElement(); // <action>
} else if (act.type == u"selection") {
std::string input_id =
base::StrCat({kSelection, base::NumberToString(i)});
xml_writer.StartElement(kInput); // <input>
xml_writer.AddAttribute(kID, input_id.c_str());
xml_writer.AddAttribute(kType, kSelection);
for (size_t opt_i = 0; opt_i < act.items.size(); ++opt_i) {
xml_writer.StartElement(kSelection); // <selection>
xml_writer.AddAttribute(kID, base::NumberToString(opt_i).c_str());
xml_writer.AddAttribute(kContent,
base::UTF16ToUTF8(act.items[opt_i]));
xml_writer.EndElement(); // </selection>
}
xml_writer.EndElement(); // </input>
// The button that submits the selection.
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args = base::StrCat(
{"type=action&action=", base::NumberToString(i),
"&tag=", base::NumberToString(base::FastHash(notification_id))});
xml_writer.AddAttribute(kArguments, args.c_str());
xml_writer.AddAttribute(
kContent,
base::UTF16ToUTF8(act.text.empty() ? u"Select" : act.text));
xml_writer.AddAttribute(kHintInputId, input_id.c_str());
xml_writer.EndElement(); // </action>
}
}
if (has_reply) {
// <action>
xml_writer.StartElement(kAction);
xml_writer.AddAttribute(kActivationType, kActivationTypeForeground);
std::string args =
base::StrCat({"type=reply&tag=",
base::NumberToString(base::FastHash(notification_id))});
xml_writer.AddAttribute(kArguments, args.c_str());
// TODO(codebytere): we should localize this.
xml_writer.AddAttribute(kContent, "Reply");
xml_writer.AddAttribute(kHintInputId, kReply);
xml_writer.EndElement(); // <action>
}
xml_writer.EndElement(); // </actions>
}
@@ -663,6 +738,57 @@ ToastEventHandler::~ToastEventHandler() = default;
IFACEMETHODIMP ToastEventHandler::Invoke(
winui::Notifications::IToastNotification* sender,
IInspectable* args) {
std::wstring arguments_w;
std::wstring tag_w;
std::wstring group_w;
if (args) {
Microsoft::WRL::ComPtr<winui::Notifications::IToastActivatedEventArgs>
activated_args;
if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&activated_args)))) {
HSTRING args_hs = nullptr;
if (SUCCEEDED(activated_args->get_Arguments(&args_hs)) && args_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(args_hs, &len);
if (raw && len)
arguments_w.assign(raw, len);
}
}
}
if (sender) {
Microsoft::WRL::ComPtr<winui::Notifications::IToastNotification2> toast2;
if (SUCCEEDED(sender->QueryInterface(IID_PPV_ARGS(&toast2)))) {
HSTRING tag_hs = nullptr;
if (SUCCEEDED(toast2->get_Tag(&tag_hs)) && tag_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(tag_hs, &len);
if (raw && len)
tag_w.assign(raw, len);
}
HSTRING group_hs = nullptr;
if (SUCCEEDED(toast2->get_Group(&group_hs)) && group_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(group_hs, &len);
if (raw && len)
group_w.assign(raw, len);
}
}
}
std::string notif_id;
std::string notif_hash;
if (notification_) {
notif_id = notification_->notification_id();
notif_hash = base::NumberToString(base::FastHash(notif_id));
}
bool structured = arguments_w.find(L"&tag=") != std::wstring::npos ||
arguments_w.find(L"type=action") != std::wstring::npos ||
arguments_w.find(L"type=reply") != std::wstring::npos;
if (structured)
return S_OK;
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&Notification::NotificationClicked, notification_));

View File

@@ -60,11 +60,16 @@ class WindowsToastNotification : public Notification {
friend class ToastEventHandler;
HRESULT ShowInternal(const NotificationOptions& options);
static std::u16string GetToastXml(const std::u16string& title,
const std::u16string& msg,
const std::wstring& icon_path,
const std::u16string& timeout_type,
const bool silent);
static std::u16string GetToastXml(
const std::string& notification_id,
const std::u16string& title,
const std::u16string& msg,
const std::wstring& icon_path,
const std::u16string& timeout_type,
const bool silent,
const std::vector<NotificationAction>& actions,
bool has_reply,
const std::u16string& reply_placeholder);
static HRESULT XmlDocumentFromString(
const wchar_t* xmlString,
ABI::Windows::Data::Xml::Dom::IXmlDocument** doc);
@@ -77,6 +82,7 @@ class WindowsToastNotification : public Notification {
static bool CreateToastXmlDocument(
const NotificationOptions& options,
NotificationPresenter* presenter,
const std::string& notification_id,
base::WeakPtr<Notification> weak_notification,
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner,
ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument>* toast_xml);

View File

@@ -32,6 +32,8 @@ PCWSTR GetRawAppUserModelID();
bool GetAppUserModelID(ScopedHString* app_id);
void SetAppUserModelID(const std::wstring& name);
bool IsRunningInDesktopBridge();
PCWSTR GetAppToastActivatorCLSID();
void SetAppToastActivatorCLSID(const std::wstring& clsid);
#endif
} // namespace electron

View File

@@ -28,6 +28,11 @@ std::wstring& GetAppUserModelId() {
return *g_app_user_model_id;
}
std::wstring& GetToastActivatorCLSID() {
static base::NoDestructor<std::wstring> g_toast_activator_clsid;
return *g_toast_activator_clsid;
}
std::string GetApplicationName() {
auto* module = GetModuleHandle(nullptr);
std::unique_ptr<FileVersionInfo> info(
@@ -82,4 +87,37 @@ bool IsRunningInDesktopBridge() {
return result;
}
PCWSTR GetAppToastActivatorCLSID() {
if (GetToastActivatorCLSID().empty()) {
GUID guid;
if (SUCCEEDED(::CoCreateGuid(&guid))) {
wchar_t buf[64] = {0};
if (StringFromGUID2(guid, buf, std::size(buf)) > 0)
GetToastActivatorCLSID() = buf;
}
}
return GetToastActivatorCLSID().c_str();
}
void SetAppToastActivatorCLSID(const std::wstring& clsid) {
CLSID parsed;
if (SUCCEEDED(::CLSIDFromString(clsid.c_str(), &parsed))) {
// Normalize formatting.
wchar_t buf[64] = {0};
if (StringFromGUID2(parsed, buf, std::size(buf)) > 0)
GetToastActivatorCLSID() = buf;
} else {
// Try adding braces if user omitted them.
if (!clsid.empty() && clsid.front() != L'{') {
std::wstring with_braces = L"{" + clsid + L"}";
if (SUCCEEDED(::CLSIDFromString(with_braces.c_str(), &parsed))) {
wchar_t buf[64] = {0};
if (StringFromGUID2(parsed, buf, std::size(buf)) > 0)
GetToastActivatorCLSID() = buf;
}
}
}
}
} // namespace electron

View File

@@ -155,6 +155,14 @@ describe('app module', () => {
});
});
ifdescribe(process.platform === 'win32')('app.setToastActivatorCLSID()', () => {
it('throws on invalid format', () => {
expect(() => {
app.setToastActivatorCLSID('1234567890');
}).to.throw(/Invalid CLSID format/);
});
});
describe('app.isPackaged', () => {
it('should be false during tests', () => {
expect(app.isPackaged).to.equal(false);