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 27 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
50 changes: 50 additions & 0 deletions docs/api/push-notifications.md
@@ -0,0 +1,50 @@
# pushNotifications

> Register for remote push notification services

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

## Events

The `pushNotification` module emits the following events:

#### Event: 'registered-for-apns-notifications' _macOS_

Returns:

* `token` String - A token that identifies the device to Apple Push Notification Service (APNS)

Emitted when the app successfully registers to receive remote notifications from APNS.
See: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428766-application?language=objc

#### Event: 'failed-to-register-for-apns-notifications' _macOS_

Returns:

* `error` String

Emitted when the app fails to register to receive remote notifications from APNS.
See: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428554-application?language=objc

#### 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_

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 BrowserWindow event `registered-for-apns-notifications` will be emitted with the APNS device token. Otherwise, the event `failed-to-register-for-apns-notifications` will be emitted.
See: https://developer.apple.com/documentation/appkit/nsapplication/1428476-registerforremotenotificationtyp?language=objc
joanx marked this conversation as resolved.
Show resolved Hide resolved

### `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;
79 changes: 79 additions & 0 deletions shell/browser/api/electron_api_push_notifications.cc
@@ -0,0 +1,79 @@
// 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* push_notifications = nullptr;
joanx marked this conversation as resolved.
Show resolved Hide resolved

gin::WrapperInfo PushNotifications::kWrapperInfo = {gin::kEmbedderNativeGin};

PushNotifications::PushNotifications() {
DCHECK(!push_notifications);
push_notifications = this;
}

PushNotifications::~PushNotifications() {
DCHECK(push_notifications);
push_notifications = nullptr;
}

// static
PushNotifications* PushNotifications::Get() {
return push_notifications;
}
joanx marked this conversation as resolved.
Show resolved Hide resolved

// static
gin::Handle<PushNotifications> PushNotifications::Create(v8::Isolate* isolate) {
return gin::CreateHandle(isolate, new PushNotifications());
joanx marked this conversation as resolved.
Show resolved Hide resolved
}

// 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)
61 changes: 61 additions & 0 deletions shell/browser/api/electron_api_push_notifications.h
@@ -0,0 +1,61 @@
// 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 "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"

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 OnDidRegisterForAPNSNotificationsWithDeviceToken(
const std::string& token);
void OnDidFailToRegisterForAPNSNotificationsWithError(
const std::string& error);
void OnDidReceiveAPNSNotification(const base::DictionaryValue& user_info);
#endif

private:
PushNotifications();
~PushNotifications() override;

#if BUILDFLAG(IS_MAC)
void RegisterForAPNSNotifications();
void UnregisterForAPNSNotifications();
#endif
};

} // namespace api

} // namespace electron

#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_PUSH_NOTIFICATIONS_H_
44 changes: 44 additions & 0 deletions shell/browser/api/electron_api_push_notifications_mac.mm
@@ -0,0 +1,44 @@
// 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>

#import "shell/browser/mac/electron_application.h"
#include "shell/common/gin_converters/value_converter.h"

namespace electron {

namespace api {

void PushNotifications::RegisterForAPNSNotifications() {
[[AtomApplication sharedApplication]
registerForRemoteNotificationTypes:NSRemoteNotificationTypeBadge |
joanx marked this conversation as resolved.
Show resolved Hide resolved
NSRemoteNotificationTypeAlert |
NSRemoteNotificationTypeSound];
}

void PushNotifications::UnregisterForAPNSNotifications() {
[[AtomApplication sharedApplication] unregisterForRemoteNotifications];
}

void PushNotifications::OnDidRegisterForAPNSNotificationsWithDeviceToken(
const std::string& token) {
Emit("registered-for-apns-notifications", token);
}

void PushNotifications::OnDidFailToRegisterForAPNSNotificationsWithError(
const std::string& error) {
Emit("failed-to-register-for-apns-notifications", error);
}

void PushNotifications::OnDidReceiveAPNSNotification(
const base::DictionaryValue& user_info) {
Emit("received-apns-notification", user_info);
}

} // namespace api

} // namespace electron
41 changes: 41 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,44 @@ - (IBAction)newWindowForTab:(id)sender {
electron::Browser::Get()->NewWindowForTab();
}

- (void)application:(NSApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
// https://stackoverflow.com/a/16411517
const char* tokenData = static_cast<const char*>([deviceToken bytes]);
joanx marked this conversation as resolved.
Show resolved Hide resolved
NSMutableString* tokenString = [NSMutableString string];
for (NSUInteger i = 0; i < [deviceToken length]; i++) {
[tokenString appendFormat:@"%02.2hhX", tokenData[i]];
}
electron::api::PushNotifications* pushNotifications =
electron::api::PushNotifications::Get();
if (pushNotifications) {
pushNotifications->OnDidRegisterForAPNSNotificationsWithDeviceToken(
base::SysNSStringToUTF8(tokenString));
}
}

- (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* pushNotifications =
electron::api::PushNotifications::Get();
if (pushNotifications) {
electron::api::PushNotifications::Get()
->OnDidFailToRegisterForAPNSNotificationsWithError(error_message);
}
}

- (void)application:(NSApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo {
electron::api::PushNotifications* pushNotifications =
electron::api::PushNotifications::Get();
if (pushNotifications) {
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