Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enable APNS registration + notification delivery in macOS apps #33574

Merged
merged 34 commits into from Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d7b5d07
feat: enable APNS registration
joanx Mar 30, 2022
bf05601
fix: add APNS register/unregister methods to app
joanx Mar 30, 2022
48f2573
minor wording edit (kick off CircleCI)
joanx May 2, 2022
21b0770
fix: lint
joanx May 7, 2022
d619a96
fix: build errors
joanx May 9, 2022
4debddf
fix: undo semantic override
joanx May 9, 2022
a2f0258
fix: clean up push_notifications header file
joanx May 9, 2022
b692619
fix: enable pushNotifications module visibility
joanx May 18, 2022
7e5f839
fix: WIP resolve build errors
joanx May 18, 2022
e842356
fix: add types, misc. cleanup
joanx May 18, 2022
9af65b5
fix: type reference
joanx May 19, 2022
1d60214
fix: cleanup
joanx May 19, 2022
a1d6309
fix: remove delegate-setting logic
joanx May 25, 2022
20fd646
fix: lint
joanx May 26, 2022
2fdb31d
fix: build failure
joanx May 26, 2022
66cde9e
fix: attempt fix build 2
joanx May 26, 2022
5dac57e
force builds
joanx Jun 7, 2022
948fd59
force builds
joanx Jun 8, 2022
563e323
Merge branch 'main' into enable-apns
joanx Jun 8, 2022
662bf3b
fix: rm LICENSES.chromium.html
joanx Jun 8, 2022
30186f7
fix: gen cleanup
joanx Jun 8, 2022
c3ea89e
Merge branch 'main' into enable-apns
joanx Jun 8, 2022
5e78aae
fix: decouple implementation from Browser object
joanx Jun 16, 2022
7443a87
fix: cleanup
joanx Jun 16, 2022
d590b5a
fix: add general push_notifications module
joanx Jun 16, 2022
220e590
fix: cleanup
joanx Jun 16, 2022
312ba2d
fix: rm unneeded imports
joanx Jun 17, 2022
055fafc
fix: variable renaming + update get handler
joanx Jun 24, 2022
11055ce
fix: cleanup
joanx Jun 24, 2022
8bc184a
fix: lint + update docs
joanx Jun 29, 2022
0508d7b
fix: add example to docs
joanx Jun 29, 2022
90443e5
fix: naming + avoid list mutation
joanx Jun 29, 2022
1d00714
fix: apns_promise_set as class variable
joanx Jun 29, 2022
9802079
fix: lint
joanx Jun 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/api/push-notifications.md
@@ -0,0 +1,48 @@
# pushNotifications

Process: [Main](../glossary.md#main-process)
joanx marked this conversation as resolved.
Show resolved Hide resolved

> 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 @@ -124,6 +124,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 @@ -289,6 +290,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 |
joanx marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -60,6 +60,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_system_preferences) \
Expand Down
1 change: 1 addition & 0 deletions typings/internal-ambient.d.ts
Expand Up @@ -229,6 +229,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_system_preferences'): { systemPreferences: Electron.SystemPreferences };
Expand Down