feat: Enable APNS registration + notification delivery in macOS apps (#33574)
This commit is contained in:
parent
5314ae5342
commit
afd08c9450
11 changed files with 302 additions and 0 deletions
48
docs/api/push-notifications.md
Normal file
48
docs/api/push-notifications.md
Normal file
|
@ -0,0 +1,48 @@
|
|||
# pushNotifications
|
||||
|
||||
Process: [Main](../glossary.md#main-process)
|
||||
|
||||
> Register for and receive notifications from remote push notification services
|
||||
|
||||
For example, when registering for push notifications via Apple push notification services (APNS):
|
||||
|
||||
```javascript
|
||||
const { pushNotifications, Notification } = require('electron')
|
||||
|
||||
pushNotifications.registerForAPNSNotifications().then((token) => {
|
||||
// forward token to your remote notification server
|
||||
})
|
||||
|
||||
pushNotifications.on('received-apns-notification', (event, userInfo) => {
|
||||
// generate a new Notification object with the relevant userInfo fields
|
||||
})
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
The `pushNotification` module emits the following events:
|
||||
|
||||
#### Event: 'received-apns-notification' _macOS_
|
||||
|
||||
Returns:
|
||||
|
||||
* `userInfo` Record<String, any>
|
||||
|
||||
Emitted when the app receives a remote notification while running.
|
||||
See: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428430-application?language=objc
|
||||
|
||||
## Methods
|
||||
|
||||
The `pushNotification` module has the following methods:
|
||||
|
||||
### `pushNotifications.registerForAPNSNotifications()` _macOS_
|
||||
|
||||
Returns `Promise<string>`
|
||||
|
||||
Registers the app with Apple Push Notification service (APNS) to receive [Badge, Sound, and Alert](https://developer.apple.com/documentation/appkit/sremotenotificationtype?language=objc) notifications. If registration is successful, the promise will be resolved with the APNS device token. Otherwise, the promise will be rejected with an error message.
|
||||
See: https://developer.apple.com/documentation/appkit/nsapplication/1428476-registerforremotenotificationtyp?language=objc
|
||||
|
||||
### `pushNotifications.unregisterForAPNSNotifications()` _macOS_
|
||||
|
||||
Unregisters the app from notifications received from APNS.
|
||||
See: https://developer.apple.com/documentation/appkit/nsapplication/1428747-unregisterforremotenotifications?language=objc
|
|
@ -40,6 +40,7 @@ auto_filenames = {
|
|||
"docs/api/power-save-blocker.md",
|
||||
"docs/api/process.md",
|
||||
"docs/api/protocol.md",
|
||||
"docs/api/push-notifications.md",
|
||||
"docs/api/safe-storage.md",
|
||||
"docs/api/screen.md",
|
||||
"docs/api/service-workers.md",
|
||||
|
@ -212,6 +213,7 @@ auto_filenames = {
|
|||
"lib/browser/api/power-monitor.ts",
|
||||
"lib/browser/api/power-save-blocker.ts",
|
||||
"lib/browser/api/protocol.ts",
|
||||
"lib/browser/api/push-notifications.ts",
|
||||
"lib/browser/api/safe-storage.ts",
|
||||
"lib/browser/api/screen.ts",
|
||||
"lib/browser/api/session.ts",
|
||||
|
|
|
@ -128,6 +128,7 @@ filenames = {
|
|||
"shell/browser/api/electron_api_menu_mac.mm",
|
||||
"shell/browser/api/electron_api_native_theme_mac.mm",
|
||||
"shell/browser/api/electron_api_power_monitor_mac.mm",
|
||||
"shell/browser/api/electron_api_push_notifications_mac.mm",
|
||||
"shell/browser/api/electron_api_system_preferences_mac.mm",
|
||||
"shell/browser/api/electron_api_web_contents_mac.mm",
|
||||
"shell/browser/auto_updater_mac.mm",
|
||||
|
@ -295,6 +296,8 @@ filenames = {
|
|||
"shell/browser/api/electron_api_printing.cc",
|
||||
"shell/browser/api/electron_api_protocol.cc",
|
||||
"shell/browser/api/electron_api_protocol.h",
|
||||
"shell/browser/api/electron_api_push_notifications.cc",
|
||||
"shell/browser/api/electron_api_push_notifications.h",
|
||||
"shell/browser/api/electron_api_safe_storage.cc",
|
||||
"shell/browser/api/electron_api_safe_storage.h",
|
||||
"shell/browser/api/electron_api_screen.cc",
|
||||
|
|
|
@ -22,6 +22,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
|
|||
{ name: 'Notification', loader: () => require('./notification') },
|
||||
{ name: 'powerMonitor', loader: () => require('./power-monitor') },
|
||||
{ name: 'powerSaveBlocker', loader: () => require('./power-save-blocker') },
|
||||
{ name: 'pushNotifications', loader: () => require('./push-notifications') },
|
||||
{ name: 'protocol', loader: () => require('./protocol') },
|
||||
{ name: 'safeStorage', loader: () => require('./safe-storage') },
|
||||
{ name: 'screen', loader: () => require('./screen') },
|
||||
|
|
3
lib/browser/api/push-notifications.ts
Normal file
3
lib/browser/api/push-notifications.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
const { pushNotifications } = process._linkedBinding('electron_browser_push_notifications');
|
||||
|
||||
export default pushNotifications;
|
77
shell/browser/api/electron_api_push_notifications.cc
Normal file
77
shell/browser/api/electron_api_push_notifications.cc
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright (c) 2022 Asana, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/api/electron_api_push_notifications.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace api {
|
||||
|
||||
PushNotifications* g_push_notifications = nullptr;
|
||||
|
||||
gin::WrapperInfo PushNotifications::kWrapperInfo = {gin::kEmbedderNativeGin};
|
||||
|
||||
PushNotifications::PushNotifications() = default;
|
||||
|
||||
PushNotifications::~PushNotifications() {
|
||||
g_push_notifications = nullptr;
|
||||
}
|
||||
|
||||
// static
|
||||
PushNotifications* PushNotifications::Get() {
|
||||
if (!g_push_notifications)
|
||||
g_push_notifications = new PushNotifications();
|
||||
return g_push_notifications;
|
||||
}
|
||||
|
||||
// static
|
||||
gin::Handle<PushNotifications> PushNotifications::Create(v8::Isolate* isolate) {
|
||||
return gin::CreateHandle(isolate, PushNotifications::Get());
|
||||
}
|
||||
|
||||
// static
|
||||
gin::ObjectTemplateBuilder PushNotifications::GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) {
|
||||
auto builder = gin_helper::EventEmitterMixin<
|
||||
PushNotifications>::GetObjectTemplateBuilder(isolate);
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
builder
|
||||
.SetMethod("registerForAPNSNotifications",
|
||||
&PushNotifications::RegisterForAPNSNotifications)
|
||||
.SetMethod("unregisterForAPNSNotifications",
|
||||
&PushNotifications::UnregisterForAPNSNotifications);
|
||||
#endif
|
||||
return builder;
|
||||
}
|
||||
|
||||
const char* PushNotifications::GetTypeName() {
|
||||
return "PushNotifications";
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace electron
|
||||
|
||||
namespace {
|
||||
|
||||
void Initialize(v8::Local<v8::Object> exports,
|
||||
v8::Local<v8::Value> unused,
|
||||
v8::Local<v8::Context> context,
|
||||
void* priv) {
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
gin::Dictionary dict(isolate, exports);
|
||||
dict.Set("pushNotifications",
|
||||
electron::api::PushNotifications::Create(isolate));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NODE_LINKED_MODULE_CONTEXT_AWARE(electron_browser_push_notifications,
|
||||
Initialize)
|
64
shell/browser/api/electron_api_push_notifications.h
Normal file
64
shell/browser/api/electron_api_push_notifications.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) 2016 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_PUSH_NOTIFICATIONS_H_
|
||||
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_PUSH_NOTIFICATIONS_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <vector>
|
||||
#include "gin/handle.h"
|
||||
#include "gin/wrappable.h"
|
||||
#include "shell/browser/browser_observer.h"
|
||||
#include "shell/browser/electron_browser_client.h"
|
||||
#include "shell/browser/event_emitter_mixin.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace api {
|
||||
|
||||
class PushNotifications
|
||||
: public ElectronBrowserClient::Delegate,
|
||||
public gin::Wrappable<PushNotifications>,
|
||||
public gin_helper::EventEmitterMixin<PushNotifications>,
|
||||
public BrowserObserver {
|
||||
public:
|
||||
static PushNotifications* Get();
|
||||
static gin::Handle<PushNotifications> Create(v8::Isolate* isolate);
|
||||
|
||||
// gin::Wrappable
|
||||
static gin::WrapperInfo kWrapperInfo;
|
||||
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) override;
|
||||
const char* GetTypeName() override;
|
||||
|
||||
// disable copy
|
||||
PushNotifications(const PushNotifications&) = delete;
|
||||
PushNotifications& operator=(const PushNotifications&) = delete;
|
||||
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
void OnDidReceiveAPNSNotification(const base::DictionaryValue& user_info);
|
||||
void ResolveAPNSPromiseSetWithToken(const std::string& token_string);
|
||||
void RejectAPNSPromiseSetWithError(const std::string& error_message);
|
||||
#endif
|
||||
|
||||
private:
|
||||
PushNotifications();
|
||||
~PushNotifications() override;
|
||||
// This set maintains all the promises that should be fulfilled
|
||||
// once macOS registers, or fails to register, for APNS
|
||||
std::vector<gin_helper::Promise<std::string>> apns_promise_set_;
|
||||
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
v8::Local<v8::Promise> RegisterForAPNSNotifications(v8::Isolate* isolate);
|
||||
void UnregisterForAPNSNotifications();
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_PUSH_NOTIFICATIONS_H_
|
62
shell/browser/api/electron_api_push_notifications_mac.mm
Normal file
62
shell/browser/api/electron_api_push_notifications_mac.mm
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2022 Asana, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/api/electron_api_push_notifications.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#import "shell/browser/mac/electron_application.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace api {
|
||||
|
||||
v8::Local<v8::Promise> PushNotifications::RegisterForAPNSNotifications(
|
||||
v8::Isolate* isolate) {
|
||||
gin_helper::Promise<std::string> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
[[AtomApplication sharedApplication]
|
||||
registerForRemoteNotificationTypes:NSRemoteNotificationTypeBadge |
|
||||
NSRemoteNotificationTypeAlert |
|
||||
NSRemoteNotificationTypeSound];
|
||||
|
||||
PushNotifications::apns_promise_set_.emplace_back(std::move(promise));
|
||||
return handle;
|
||||
}
|
||||
|
||||
void PushNotifications::ResolveAPNSPromiseSetWithToken(
|
||||
const std::string& token_string) {
|
||||
std::vector<gin_helper::Promise<std::string>> promises =
|
||||
std::move(PushNotifications::apns_promise_set_);
|
||||
for (auto& promise : promises) {
|
||||
promise.Resolve(token_string);
|
||||
}
|
||||
}
|
||||
|
||||
void PushNotifications::RejectAPNSPromiseSetWithError(
|
||||
const std::string& error_message) {
|
||||
std::vector<gin_helper::Promise<std::string>> promises =
|
||||
std::move(PushNotifications::apns_promise_set_);
|
||||
for (auto& promise : promises) {
|
||||
promise.RejectWithErrorMessage(error_message);
|
||||
}
|
||||
}
|
||||
|
||||
void PushNotifications::UnregisterForAPNSNotifications() {
|
||||
[[AtomApplication sharedApplication] unregisterForRemoteNotifications];
|
||||
}
|
||||
|
||||
void PushNotifications::OnDidReceiveAPNSNotification(
|
||||
const base::DictionaryValue& user_info) {
|
||||
Emit("received-apns-notification", user_info);
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace electron
|
|
@ -13,6 +13,7 @@
|
|||
#include "base/mac/scoped_objc_class_swizzler.h"
|
||||
#include "base/strings/sys_string_conversions.h"
|
||||
#include "base/values.h"
|
||||
#include "shell/browser/api/electron_api_push_notifications.h"
|
||||
#include "shell/browser/browser.h"
|
||||
#include "shell/browser/mac/dict_util.h"
|
||||
#import "shell/browser/mac/electron_application.h"
|
||||
|
@ -157,4 +158,43 @@ static NSDictionary* UNNotificationResponseToNSDictionary(
|
|||
electron::Browser::Get()->NewWindowForTab();
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication*)application
|
||||
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
|
||||
// https://stackoverflow.com/a/16411517
|
||||
const char* token_data = static_cast<const char*>([deviceToken bytes]);
|
||||
NSMutableString* token_string = [NSMutableString string];
|
||||
for (NSUInteger i = 0; i < [deviceToken length]; i++) {
|
||||
[token_string appendFormat:@"%02.2hhX", token_data[i]];
|
||||
}
|
||||
// Resolve outstanding APNS promises created during registration attempts
|
||||
electron::api::PushNotifications* push_notifications =
|
||||
electron::api::PushNotifications::Get();
|
||||
if (push_notifications) {
|
||||
push_notifications->ResolveAPNSPromiseSetWithToken(
|
||||
base::SysNSStringToUTF8(token_string));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication*)application
|
||||
didFailToRegisterForRemoteNotificationsWithError:(NSError*)error {
|
||||
std::string error_message(base::SysNSStringToUTF8(
|
||||
[NSString stringWithFormat:@"%ld %@ %@", [error code], [error domain],
|
||||
[error userInfo]]));
|
||||
electron::api::PushNotifications* push_notifications =
|
||||
electron::api::PushNotifications::Get();
|
||||
if (push_notifications) {
|
||||
push_notifications->RejectAPNSPromiseSetWithError(error_message);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication*)application
|
||||
didReceiveRemoteNotification:(NSDictionary*)userInfo {
|
||||
electron::api::PushNotifications* push_notifications =
|
||||
electron::api::PushNotifications::Get();
|
||||
if (push_notifications) {
|
||||
electron::api::PushNotifications::Get()->OnDidReceiveAPNSNotification(
|
||||
electron::NSDictionaryToDictionaryValue(userInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
V(electron_browser_power_save_blocker) \
|
||||
V(electron_browser_protocol) \
|
||||
V(electron_browser_printing) \
|
||||
V(electron_browser_push_notifications) \
|
||||
V(electron_browser_safe_storage) \
|
||||
V(electron_browser_session) \
|
||||
V(electron_browser_screen) \
|
||||
|
|
1
typings/internal-ambient.d.ts
vendored
1
typings/internal-ambient.d.ts
vendored
|
@ -225,6 +225,7 @@ declare namespace NodeJS {
|
|||
}
|
||||
_linkedBinding(name: 'electron_browser_power_monitor'): PowerMonitorBinding;
|
||||
_linkedBinding(name: 'electron_browser_power_save_blocker'): { powerSaveBlocker: Electron.PowerSaveBlocker };
|
||||
_linkedBinding(name: 'electron_browser_push_notifications'): { pushNotifications: Electron.PushNotifications };
|
||||
_linkedBinding(name: 'electron_browser_safe_storage'): { safeStorage: Electron.SafeStorage };
|
||||
_linkedBinding(name: 'electron_browser_session'): typeof Electron.Session;
|
||||
_linkedBinding(name: 'electron_browser_screen'): { createScreen(): Electron.Screen };
|
||||
|
|
Loading…
Reference in a new issue