diff --git a/docs/api/push-notifications.md b/docs/api/push-notifications.md new file mode 100644 index 0000000000000..cad396bc03d56 --- /dev/null +++ b/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 + +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` + +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 diff --git a/filenames.auto.gni b/filenames.auto.gni index 82a36d7a46360..2b865962a3885 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -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", diff --git a/filenames.gni b/filenames.gni index 9cdb0637abb60..5b9d3cc721dbb 100644 --- a/filenames.gni +++ b/filenames.gni @@ -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", diff --git a/lib/browser/api/module-list.ts b/lib/browser/api/module-list.ts index b9e3340cd2955..be516aec2885a 100644 --- a/lib/browser/api/module-list.ts +++ b/lib/browser/api/module-list.ts @@ -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') }, diff --git a/lib/browser/api/push-notifications.ts b/lib/browser/api/push-notifications.ts new file mode 100644 index 0000000000000..346ae93c6b0da --- /dev/null +++ b/lib/browser/api/push-notifications.ts @@ -0,0 +1,3 @@ +const { pushNotifications } = process._linkedBinding('electron_browser_push_notifications'); + +export default pushNotifications; diff --git a/shell/browser/api/electron_api_push_notifications.cc b/shell/browser/api/electron_api_push_notifications.cc new file mode 100644 index 0000000000000..bb0cb13d0f1d7 --- /dev/null +++ b/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 + +#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::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 exports, + v8::Local unused, + v8::Local 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) diff --git a/shell/browser/api/electron_api_push_notifications.h b/shell/browser/api/electron_api_push_notifications.h new file mode 100644 index 0000000000000..f818afdd109d4 --- /dev/null +++ b/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 + +#include +#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, + public gin_helper::EventEmitterMixin, + public BrowserObserver { + public: + static PushNotifications* Get(); + static gin::Handle 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> apns_promise_set_; + +#if BUILDFLAG(IS_MAC) + v8::Local RegisterForAPNSNotifications(v8::Isolate* isolate); + void UnregisterForAPNSNotifications(); +#endif +}; + +} // namespace api + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_PUSH_NOTIFICATIONS_H_ diff --git a/shell/browser/api/electron_api_push_notifications_mac.mm b/shell/browser/api/electron_api_push_notifications_mac.mm new file mode 100644 index 0000000000000..c52511625a704 --- /dev/null +++ b/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 + +#include +#include +#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 PushNotifications::RegisterForAPNSNotifications( + v8::Isolate* isolate) { + gin_helper::Promise promise(isolate); + v8::Local 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> 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> 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 diff --git a/shell/browser/mac/electron_application_delegate.mm b/shell/browser/mac/electron_application_delegate.mm index d065eb75e4213..013f51137e4bf 100644 --- a/shell/browser/mac/electron_application_delegate.mm +++ b/shell/browser/mac/electron_application_delegate.mm @@ -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 @@ - (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([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 diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc index b0c6558e20556..71a2f54eb5c65 100644 --- a/shell/common/node_bindings.cc +++ b/shell/common/node_bindings.cc @@ -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) \ diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 207c4162789ab..950d848f50cb2 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -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 };