diff --git a/PushHelper.swift b/PushHelper.swift new file mode 100644 index 0000000..8d2597b --- /dev/null +++ b/PushHelper.swift @@ -0,0 +1,344 @@ +import Foundation + +private let completionHandlerIdKey = "completionHandlerId" + +extension UNAuthorizationStatus { + var description : String { + switch self { + case .notDetermined: + return "NotDetermined" + case .denied: + return "Denied" + case .authorized: + return "Authorized" + case .provisional: + return "Provisional" + case .ephemeral: + return "Ephemeral" + @unknown default: + return "NotDetermined" + } + } +} + +enum NativeEvent { + case tokenReceived + case notificationOpened + case launchNotificationOpened + case backgroundMessageReceived + case foregroundMessageReceived + + var key: String { + switch(self) { + case .tokenReceived: + return "TOKEN_RECEIVED" + case .notificationOpened: + return "NOTIFICATION_OPENED" + case .launchNotificationOpened: + return "LAUNCH_NOTIFICATION_OPENED" + case .backgroundMessageReceived: + return "BACKGROUND_MESSAGE_RECEIVED" + case .foregroundMessageReceived: + return "FOREGROUND_MESSAGE_RECEIVED" + } + } + + var name: String { + switch(self) { + case .tokenReceived: + return "TokenReceived" + case .notificationOpened: + return "NotificationOpened" + case .launchNotificationOpened: + return "LaunchNotificationOpened" + case .foregroundMessageReceived: + return "ForegroundMessageReceived" + case .backgroundMessageReceived: + return "BackgroundMessageReceived" + } + } +} + +struct PushEvent { + var type: NativeEvent + var payload: Any +} + +class PushEventManager { + static let shared = PushEventManager() + + private var eventQueue: [PushEvent] = [] + private var sendEvent: ((PushEvent) -> Void)? + + func setSendEvent(sendEvent: @escaping (PushEvent) -> Void) { + self.sendEvent = sendEvent + flushQueuedEvents() + } + + func sendEventToJS(_ event: PushEvent) { + if let sendEvent = self.sendEvent { + sendEvent(event) + } else { + eventQueue.append(event) + } + } + + private func flushQueuedEvents() { + while (!eventQueue.isEmpty) { + sendEventToJS(eventQueue.removeFirst()) + } + } +} + +final class PushNotificationManager { + static let shared = PushNotificationManager() + + private var cachedDeviceToken: String? + private var launchNotification: [AnyHashable: Any]? + private var remoteNotificationCompletionHandlers: [String: (UIBackgroundFetchResult) -> Void] = [:] + private let sharedEventManager: PushEventManager + + init() { + sharedEventManager = PushEventManager.shared + setUpObservers() + } + + deinit { + removeObservers() + } + + func handleLaunchOptions(launchOptions: [AnyHashable: Any]) { + // 1. The host App launch is caused by a notification + if let remoteNotification = launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any], + let application = RCTSharedApplication() { + // 2. The host App is launched from terminated state to the foreground + // (including transitioning to foregound), i.e. .active .inactive. + // This happens under one of below conditions: + // a. Remote notifications are not able to launch the host App (without `content-available: 1`) + // b. Remote notifications background mode was not enabled on the host App + // c. The end user disabled background refresh of the host App + // 3. This notification must be tapped by an end user which is recorded as the launch notification + if application.applicationState != .background { + launchNotification = remoteNotification + + // NOTE: the notification payload will also be passed into didReceiveRemoteNotification below after + // this delegate method, didFinishLaunchingWithOptions completes. + // As this notification will already be recorded as the launch notification, it should not be sent as + // notificationOpened event, this check is handled in didReceiveRemoteNotification. + } + + // Otherwise the host App is launched in the background, this notification will be sent to react-native + // as backgroundMessageReceived event in didReceiveRemoteNotification below. + // After the host App launched in the background, didFinishLaunchingWithOptions will no longer + // be fired when an end user taps a notification. + // After the host App launched in the background, it runs developers' react-native code as well. + } + } + + func requestPermissions( + _ permissions: [AnyHashable: Any], + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + if RCTRunningInAppExtension() { + reject("ERROR", "requestPermissions can not be called in App Extensions", nil) + return + } + + Task { + var options: UNAuthorizationOptions = [] + + if permissions["alert"] as? Bool == true { + options.insert(.alert) + } + + if permissions["badge"] as? Bool == true { + options.insert(.badge) + } + + if permissions["sound"] as? Bool == true { + options.insert(.sound) + } + + if permissions["criticalAlert"] as? Bool == true { + options.insert(.criticalAlert) + } + + if permissions["provisional"] as? Bool == true { + options.insert(.provisional) + } + + do { + let granted = try await AUNotificationPermissions.request(options) + resolve(granted) + } catch { + reject("ERROR", error.localizedDescription, error) + } + } + } + + func getPermissionStatus( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + Task { + let status = await AUNotificationPermissions.status + resolve(status.description) + } + } + + func getLaunchNotification( + _ resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let launchNotification = self.launchNotification + self.launchNotification = nil + resolve(launchNotification == nil ? NSNull() : launchNotification) + } + + func setBadgeCount(_ count: Int) { + DispatchQueue.main.async { + RCTSharedApplication()?.applicationIconBadgeNumber = count + } + } + + func getBadgeCount( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + DispatchQueue.main.async { + resolve(RCTSharedApplication()?.applicationIconBadgeNumber ?? 0) + } + } + + func didRegisterForRemoteNotificationsWithDeviceToken(deviceToken: Data) { + let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() + + // Reduce frequency of tokenReceived event emitting to RN + if (cachedDeviceToken != token) { + cachedDeviceToken = token + sharedEventManager.sendEventToJS( + PushEvent(type: NativeEvent.tokenReceived, payload: ["token": cachedDeviceToken]) + ) + } + } + + func didFailToRegisterForRemoteNotificationsWithError(error: Error) { + print("Register for remote notifications failed due to \(error).") + } + + func didReceiveRemoteNotification( + userInfo: [AnyHashable: Any], + completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + if let application = RCTSharedApplication() { + switch application.applicationState { + case .background: + let completionHandlerId = UUID().uuidString + var userInfoCopy = userInfo + + remoteNotificationCompletionHandlers[completionHandlerIdKey] = completionHandler + userInfoCopy[completionHandlerIdKey] = completionHandlerId + + sharedEventManager.sendEventToJS( + PushEvent(type: NativeEvent.backgroundMessageReceived, payload: userInfoCopy) + ) + + // Expecting remoteNotificationCompletionHandlers[completionHandlerIdKey] to be called from JS to complete + // the background notification + case .inactive: + if let launchNotification = launchNotification { + if NSDictionary(dictionary: launchNotification).isEqual(to: userInfo) { + // When the last tapped notification is the same as the launch notification, + // it's sent as launchNotificationOpened event, and retrievable via getLaunchNotification. + PushEventManager.shared.sendEventToJS( + PushEvent(type: NativeEvent.launchNotificationOpened, payload: launchNotification) + ) + } else { + // When a launch notification is recorded in handleLaunchOptions above, + // but the last tapped notification is not the recorded launch notification, the last + // tapped notification will be sent to react-native as notificationOpened event. + // This may happen when an end user rapidly tapped on multiple notifications. + self.launchNotification = nil + sharedEventManager.sendEventToJS( + PushEvent(type: NativeEvent.notificationOpened, payload: userInfo) + ) + } + } else { + // When there is no launch notification recorded, the last tapped notification + // will be sent to react-native as notificationOpened event. + sharedEventManager.sendEventToJS( + PushEvent(type: NativeEvent.notificationOpened, payload: userInfo) + ) + } + completionHandler(.noData) + case .active: + sharedEventManager.sendEventToJS( + PushEvent(type: NativeEvent.foregroundMessageReceived, payload: userInfo) + ) + completionHandler(.noData) + @unknown default: break // we don't handle any possible new state added in the future for now + } + } + } + + func completeNotification(_ completionHandlerId: String) { + if let completionHandler = remoteNotificationCompletionHandlers[completionHandlerId] { + completionHandler(.noData) + remoteNotificationCompletionHandlers.removeValue(forKey: completionHandlerId) + } + } + + private func setUpObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + } + + private func removeObservers() { + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + } + + @objc + private func applicationDidBecomeActive() { + registerForRemoteNotifications() + } + + @objc + private func applicationDidEnterBackground() { + // When App enters background we remove the cached launchNotification + // as when the App reopens after this point, there won't be a notification + // that launched the App. + launchNotification = nil + } + + private func registerForRemoteNotifications() { + if RCTRunningInAppExtension() { + return + } + + DispatchQueue.main.async { + RCTSharedApplication()?.registerForRemoteNotifications() + } + } +} diff --git a/PushNotification.h b/PushNotification.h new file mode 100644 index 0000000..fad899e --- /dev/null +++ b/PushNotification.h @@ -0,0 +1,20 @@ +// +// PushNotification.h +// candlefinance-push +// +// Created by Gary Tokman on 4/16/24. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PushNotification : NSObject + ++ (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; ++ (void) didFailToRegisterForRemoteNotificationsWithError:(NSError*)error; ++ (void) didReceiveRemoteNotification:(NSDictionary *)userInfo withCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/PushNotification.m b/PushNotification.m new file mode 100644 index 0000000..f3910ea --- /dev/null +++ b/PushNotification.m @@ -0,0 +1,25 @@ +// +// PushNotification.m +// candlefinance-push +// +// Created by Gary Tokman on 4/16/24. +// + +#import "PushNotification.h" +#import "candlefinance_push-Swift.h" + +@implementation PushNotification + ++ (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { + [PushNotificationAppDelegateHelper didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} + ++ (void) didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { + [PushNotificationAppDelegateHelper didFailToRegisterForRemoteNotificationsWithError:error]; +} + ++ (void) didReceiveRemoteNotification:(NSDictionary*)userInfo withCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + [PushNotificationAppDelegateHelper didReceiveRemoteNotificationWithUserInfo:userInfo completionHandler:completionHandler]; +} + +@end diff --git a/PushNotificationAppDelegateHelper.swift b/PushNotificationAppDelegateHelper.swift new file mode 100644 index 0000000..29bb7af --- /dev/null +++ b/PushNotificationAppDelegateHelper.swift @@ -0,0 +1,28 @@ +import Foundation + +@objc(PushNotificationAppDelegateHelper) +public class PushNotificationAppDelegateHelper: NSObject { + @objc + static public func didRegisterForRemoteNotificationsWithDeviceToken(_ deviceToken: Data) { + PushNotificationManager + .shared + .didRegisterForRemoteNotificationsWithDeviceToken(deviceToken: deviceToken) + } + + @objc + static public func didFailToRegisterForRemoteNotificationsWithError(_ error: Error) { + PushNotificationManager + .shared + .didFailToRegisterForRemoteNotificationsWithError(error: error) + } + + @objc + static public func didReceiveRemoteNotification( + userInfo: [AnyHashable: Any], + completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + PushNotificationManager + .shared + .didReceiveRemoteNotification(userInfo: userInfo, completionHandler: completionHandler) + } +} diff --git a/Status.swift b/Status.swift new file mode 100644 index 0000000..0cfa58e --- /dev/null +++ b/Status.swift @@ -0,0 +1,62 @@ +import Foundation +import UserNotifications + +#if canImport(WatchKit) +import WatchKit +#elseif canImport(UIKit) +import UIKit +typealias Application = UIApplication +#elseif canImport(AppKit) +import AppKit +typealias Application = NSApplication +#endif + +@available(iOSApplicationExtension, unavailable) +@available(watchOSApplicationExtension, unavailable) +@available(tvOSApplicationExtension, unavailable) +@available(macCatalystApplicationExtension, unavailable) +@available(OSXApplicationExtension, unavailable) +/// Provides convenience methods for requesting and checking notifications permissions. +public class AUNotificationPermissions { + + /// Check if notifications are allowed + public static var allowed: Bool { + get async { + await status == .authorized ? true : false + } + } + + /// Check the notification permission status + public static var status: UNAuthorizationStatus { + get async { + await withCheckedContinuation { continuation in + UNUserNotificationCenter.current().getNotificationSettings { settings in + continuation.resume(returning: settings.authorizationStatus) + } + } + } + } + + /// Request notification permissions + /// - Parameter options: Requested notification options + @discardableResult + public static func request(_ options: UNAuthorizationOptions? = nil) async throws -> Bool { + let options = options ?? [.badge, .alert, .sound] + let notificationsAllowed = try await UNUserNotificationCenter.current().requestAuthorization( + options: options + ) + + return notificationsAllowed + } + + /// Register device with APNs + public static func registerForRemoteNotifications() async { + await MainActor.run { + #if canImport(WatchKit) + WKExtension.shared().registerForRemoteNotifications() + #else + Application.shared.registerForRemoteNotifications() + #endif + } + } +} diff --git a/example/ios/AppDelegate.swift b/example/ios/AppDelegate.swift index eebb231..72d8756 100644 --- a/example/ios/AppDelegate.swift +++ b/example/ios/AppDelegate.swift @@ -13,10 +13,6 @@ import NotificationCenter // 1 @UIApplicationMain class AppDelegate: RCTAppDelegate { - - // 2 - let push = Push() - var isDarkMode: Bool { return UITraitCollection.current.userInterfaceStyle == .dark } @@ -45,23 +41,15 @@ class AppDelegate: RCTAppDelegate { extension AppDelegate: UNUserNotificationCenterDelegate { override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - push.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - } - - override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - push.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - } - - public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - push.userNotificationCenter(center, willPresent: notification, withCompletionHandler: completionHandler) + PushNotificationAppDelegateHelper.didRegisterForRemoteNotificationsWithDeviceToken(deviceToken) } - public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { - await push.userNotificationCenter(center, didReceive: response) + override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) { + PushNotificationAppDelegateHelper.didFailToRegisterForRemoteNotificationsWithError(error) } override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - push.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) + PushNotificationAppDelegateHelper.didReceiveRemoteNotification(userInfo: userInfo, completionHandler: completionHandler) } } diff --git a/ios/Push.mm b/ios/Push.mm index 297c08e..3975478 100644 --- a/ios/Push.mm +++ b/ios/Push.mm @@ -5,10 +5,7 @@ @interface RCT_EXTERN_MODULE(Push, RCTEventEmitter) RCT_EXTERN_METHOD(supportedEvents) -RCT_EXTERN_METHOD(requestPermissions:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - -RCT_EXTERN_METHOD(onFinish:(NSString *)uuid withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(onFinish:(NSString *)uuid) RCT_EXTERN_METHOD(isRegisteredForRemoteNotifications:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) @@ -16,12 +13,20 @@ @interface RCT_EXTERN_MODULE(Push, RCTEventEmitter) RCT_EXTERN_METHOD(registerForToken:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(requestPermissions:(NSDictionary*)permissions + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) + RCT_EXTERN_METHOD(getAuthorizationStatus:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) + reject:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(getLaunchNotification:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(setBadgeCount:(int)count) + +RCT_EXTERN_METHOD(getBadgeCount:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) -+ (BOOL)requiresMainQueueSetup -{ - return NO; -} @end diff --git a/ios/Push.swift b/ios/Push.swift index 7cc7a40..e74eee1 100644 --- a/ios/Push.swift +++ b/ios/Push.swift @@ -1,232 +1,128 @@ import UIKit import React -enum NotificationType: String, CaseIterable { - case notificationReceived, deviceTokenReceived, errorReceived -} - -struct Action { - let type: NotificationType - let payload: Any! -} - -public protocol Logger { - func track(event: String) -} - -struct NotificationPayload { - let kind: NotificationKind - let userInfo: [AnyHashable: Any] -} - -public class SharedPush: NSObject { - static var sharedInstance: SharedPush = .init() - var isObserving: Bool = false - var notificationCallbackDictionary: [String: () -> Void] = [:] - public var emitter: RCTEventEmitter? - public var logger: Logger? - var queue: [NotificationPayload] = [] -} +private let expectedEventNames: Set = [ + NativeEvent.tokenReceived.name, + NativeEvent.backgroundMessageReceived.name, + NativeEvent.foregroundMessageReceived.name, + NativeEvent.notificationOpened.name, + NativeEvent.launchNotificationOpened.name +] @objc(Push) final public class Push: RCTEventEmitter { - public lazy var shared = SharedPush.sharedInstance - - override public init() { - super.init() - shared.emitter = self - } - - @objc public override func startObserving() { - super.startObserving() - shared.isObserving = true - if !shared.queue.isEmpty { - shared.queue.forEach(handleRemoteNotificationReceived(payload:)) - shared.queue.removeAll() + var registeredEventNames: Set = [] + var hasListeners = false + private let sharedNotificationManager: PushNotificationManager + + // Override the bridge setter and getter to capture the launch options + public override var bridge: RCTBridge! { + set(bridge) { + super.bridge = bridge + + if let launchOptions = bridge.launchOptions { + sharedNotificationManager.handleLaunchOptions(launchOptions: launchOptions) + } + } + get { + return super.bridge } - shared.logger?.track(event: #function) - } - - @objc public override func stopObserving() { - super.stopObserving() - shared.isObserving = false - shared.logger?.track(event: #function) } - public override func supportedEvents() -> [String]! { - return NotificationType.allCases.map { $0.rawValue } + override init() { + sharedNotificationManager = PushNotificationManager.shared + super.init() } - @objc(onFinish:withResolver:withRejecter:) - public func onFinish(uuid: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - if let callback = shared.notificationCallbackDictionary[uuid] { - callback() - shared.notificationCallbackDictionary.removeValue(forKey: uuid) - } - shared.logger?.track(event: #function) + func notifyFlushQueuedEvents() { + PushEventManager.shared.setSendEvent(sendEvent: self.sendEventToJS) } - @objc(requestPermissions:withRejecter:) - func requestPermissions(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in - if let error = error { - reject("permission_error", error.localizedDescription, error) - } else if granted { - resolve(true) - } else { - reject("permission_error", "Permission denied", nil) - } - } - } - shared.logger?.track(event: #function) + func sendEventToJS(event: PushEvent) { + sendEvent(withName: event.type.name, body: event.payload) } - @objc(getAuthorizationStatus:withRejecter:) - func getAuthorizationStatus(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - shared.logger?.track(event: #function) - DispatchQueue.main.async { - UNUserNotificationCenter.current().getNotificationSettings { settings in - resolve(settings.authorizationStatus.rawValue) - } + public override func addListener(_ eventName: String!) { + super.addListener(eventName) + registeredEventNames.insert(eventName) + + if (registeredEventNames == expectedEventNames) { + notifyFlushQueuedEvents() } } - @objc(registerForToken:withRejecter:) - func registerForToken(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - resolve(true) - } - shared.logger?.track(event: #function) + public override func supportedEvents() -> [String]! { + return Array(expectedEventNames) } - @objc(isRegisteredForRemoteNotifications:withRejecter:) - func isRegisteredForRemoteNotifications(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - DispatchQueue.main.async { - let value = UIApplication.shared.isRegisteredForRemoteNotifications - resolve(value) - } - shared.logger?.track(event: #function) + @objc + func requestPermissions( + _ permissions: [AnyHashable: Any], + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + sharedNotificationManager.requestPermissions(permissions, resolve: resolve, reject: reject) } -} - -enum NotificationKind { - case opened, foreground - case background((UIBackgroundFetchResult) -> Void) - - var rawValue: String { - switch self { - case .foreground: "foreground" - case .opened: "opened" - case .background: "background" - } + @objc + func getPermissionStatus( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + sharedNotificationManager.getPermissionStatus(resolve, reject: reject) } -} - -extension Push { - func handleRemoteNotificationReceived( - payload: NotificationPayload + @objc + func getLaunchNotification( + _ resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock ) { - guard shared.isObserving else { - let message = "Warning: Not observing for kind: \(payload.kind.rawValue). \(#function). Adding to the queue." - shared.queue.append(payload) - shared.logger?.track(event: message) - return - } - let kind = payload.kind - let userInfo = payload.userInfo - - switch kind { - case .foreground: - if let emitter = shared.emitter { - emitter.sendEvent(withName: NotificationType.notificationReceived.rawValue, body: ["payload": userInfo, "kind": kind.rawValue]) - } else { - shared.logger?.track(event: "Fatal: Emitter not found for kind: \(kind). \(#function)") - } - case .background(let completion): - let uuid = UUID().uuidString - shared.notificationCallbackDictionary[uuid] = { - completion(.newData) - } - DispatchQueue.main.asyncAfter(deadline: .now() + 29) { [weak self] in - if let callback = self?.shared.notificationCallbackDictionary[uuid] { - callback() - self?.shared.notificationCallbackDictionary.removeValue(forKey: uuid) - self?.shared.logger?.track(event: "Fatal: Completion handler called w/o user doing so. \(#function)") - } - } - if let emitter = shared.emitter { - emitter.sendEvent(withName: NotificationType.notificationReceived.rawValue, body: ["payload": userInfo, "uuid": uuid, "kind": kind.rawValue]) - } else { - shared.logger?.track(event: "Fatal: Emitter not found for kind: \(kind.rawValue). \(#function)") - } - case .opened: - if let emitter = shared.emitter { - emitter.sendEvent(withName: NotificationType.notificationReceived.rawValue, body: ["payload": userInfo, "kind": kind.rawValue]) - } else { - shared.logger?.track(event: "Fatal: Emitter not found for kind: \(kind.rawValue). \(#function)") - } - } - + sharedNotificationManager.getLaunchNotification(resolve, reject: reject) } - func handleRemoteNotificationsRegistered(deviceToken: String) { - if let emitter = shared.emitter { - emitter.sendEvent(withName: NotificationType.deviceTokenReceived.rawValue, body: deviceToken) - } else { - shared.logger?.track(event: "Fatal: Emitter not found. \(#function)") - } + @objc + func setBadgeCount(_ count: Int) { + sharedNotificationManager.setBadgeCount(count) } - func handleRemoteNotificationRegistrationError(error: String) { - if let emitter = shared.emitter { - emitter.sendEvent(withName: NotificationType.errorReceived.rawValue, body: error) - } else { - shared.logger?.track(event: "Fatal: Emitter not found. \(#function)") - } + @objc + func getBadgeCount( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + sharedNotificationManager.getBadgeCount(resolve, reject: reject) } -} - -extension Push: UNUserNotificationCenterDelegate { - - public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - let tokenParts = deviceToken.map { data -> String in - return String(format: "%02x", data) - } - let token = tokenParts.joined() - shared.logger?.track(event: "Device token received \(#function).") - handleRemoteNotificationsRegistered(deviceToken: token) + @objc + func onFinish(uuid: String) { + sharedNotificationManager.completeNotification(uuid) } - public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - shared.logger?.track(event: "Error registering for remote notifications: \(error.localizedDescription)") - handleRemoteNotificationRegistrationError(error: error.localizedDescription) + @objc(registerForToken:withRejecter:) + func registerForToken(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + resolve(true) + } } - public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - handleRemoteNotificationReceived(payload: .init(kind: .foreground, userInfo: notification.request.content.userInfo)) - completionHandler([.banner, .sound, .badge, .list]) - } - public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { - handleRemoteNotificationReceived( - payload: .init(kind: .opened, - userInfo: response.notification.request.content.userInfo) - ) + @objc + public override static func requiresMainQueueSetup() -> Bool { + return true } - public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - handleRemoteNotificationReceived(payload: - .init( - kind: .background(completionHandler), - userInfo: userInfo - ) - ) + @objc + public override func constantsToExport() -> [AnyHashable : Any]! { + return [ + "NativeEvent": [ + NativeEvent.backgroundMessageReceived.key: NativeEvent.backgroundMessageReceived.name, + NativeEvent.foregroundMessageReceived.key: NativeEvent.foregroundMessageReceived.name, + NativeEvent.notificationOpened.key: NativeEvent.notificationOpened.name, + NativeEvent.launchNotificationOpened.key: NativeEvent.launchNotificationOpened.name, + NativeEvent.tokenReceived.key: NativeEvent.tokenReceived.name, + ], + ] } }