Skip to content

Commit

Permalink
feat: Enable APNS registration + notification delivery in macOS apps (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
joanx committed Jul 12, 2022
1 parent 5314ae5 commit afd08c9
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 0 deletions.
48 changes: 48 additions & 0 deletions docs/api/push-notifications.md
@@ -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
2 changes: 2 additions & 0 deletions filenames.auto.gni
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions filenames.gni
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions lib/browser/api/module-list.ts
Expand Up @@ -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') },
Expand Down
3 changes: 3 additions & 0 deletions lib/browser/api/push-notifications.ts
@@ -0,0 +1,3 @@
const { pushNotifications } = process._linkedBinding('electron_browser_push_notifications');

export default pushNotifications;
77 changes: 77 additions & 0 deletions shell/browser/api/electron_api_push_notifications.cc
@@ -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 changes: 64 additions & 0 deletions shell/browser/api/electron_api_push_notifications.h
@@ -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 changes: 62 additions & 0 deletions shell/browser/api/electron_api_push_notifications_mac.mm
@@ -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
40 changes: 40 additions & 0 deletions shell/browser/mac/electron_application_delegate.mm
Expand Up @@ -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"
Expand Down Expand Up @@ -157,4 +158,43 @@ - (IBAction)newWindowForTab:(id)sender {
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
1 change: 1 addition & 0 deletions shell/common/node_bindings.cc
Expand Up @@ -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) \
Expand Down
1 change: 1 addition & 0 deletions typings/internal-ambient.d.ts
Expand Up @@ -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 };
Expand Down

0 comments on commit afd08c9

Please sign in to comment.