diff --git a/.gitignore b/.gitignore index 5b01b0492..230f9a91c 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,4 @@ fastlane/test_output iOSInjectionProject/ Env.swift +.vscode/settings.json diff --git a/Apps/APN-UIKit/APN UIKit/AppDelegate.swift b/Apps/APN-UIKit/APN UIKit/AppDelegate.swift index 2f4d0f4ca..7aa15b3fc 100644 --- a/Apps/APN-UIKit/APN UIKit/AppDelegate.swift +++ b/Apps/APN-UIKit/APN UIKit/AppDelegate.swift @@ -7,8 +7,6 @@ import CioMessagingPushAPN import UIKit @main -class AppDelegateWithCioIntegration: CioAppDelegateWrapper {} - class AppDelegate: UIResponder, UIApplicationDelegate { var storage = DIGraphShared.shared.storage var deepLinkHandler = DIGraphShared.shared.deepLinksHandlerUtil @@ -28,6 +26,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + // Forward the APN device token to Customer.io SDK. + // Required: iOS delivers tokens only via UIApplicationDelegate — there is no SwiftUI or async API alternative. + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + MessagingPushAPN.shared.registerDeviceToken(apnDeviceToken: deviceToken) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + MessagingPushAPN.shared.deleteDeviceToken() + } + func initializeCioAndInAppListeners() { // Set default setting if those don't exist DIGraphShared.shared.settingsService.setDefaultSettings() @@ -116,7 +124,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // // Function called when a push notification is clicked or swiped away. // func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { // // Track custom event with Customer.io. -// // NOT required for basic PN tap tracking - that is done automatically with `CioAppDelegateWrapper`. // CustomerIO.shared.track( // name: "custom push-clicked event", // properties: ["push": response.notification.request.content.userInfo] diff --git a/Apps/CocoaPods-FCM/src/App.swift b/Apps/CocoaPods-FCM/src/App.swift index d482fccc7..5c2500811 100644 --- a/Apps/CocoaPods-FCM/src/App.swift +++ b/Apps/CocoaPods-FCM/src/App.swift @@ -3,14 +3,7 @@ import SwiftUI @main struct MainApp: App { - // Default option, without CIO integration -// @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - - // Use this option if you don't have a need to extend `CioAppDelegateWrapper` - @UIApplicationDelegateAdaptor(CioAppDelegateWrapper.self) private var appDelegate - - // Use this option if you need to extend `CioAppDelegateWrapper`: class AppDelegateWithCioIntegration: CioAppDelegateWrapper {} -// @UIApplicationDelegateAdaptor(AppDelegateWithCioIntegration.self) private var appDelegate + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject var userManager: UserManager = .init() diff --git a/Apps/CocoaPods-FCM/src/AppDelegate.swift b/Apps/CocoaPods-FCM/src/AppDelegate.swift index 34153c178..d279be328 100644 --- a/Apps/CocoaPods-FCM/src/AppDelegate.swift +++ b/Apps/CocoaPods-FCM/src/AppDelegate.swift @@ -15,9 +15,12 @@ class AppDelegate: NSObject, UIApplicationDelegate { Next line of code is used for testing how Firebase behaves when another object is set as the delegate for `UNUserNotificationCenter`. This is not necessary for the Customer.io SDK to work. */ -// let anotherPushEventHandler = AnotherPushEventHandler() + // let anotherPushEventHandler = AnotherPushEventHandler() - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { // Follow setup guide for setting up FCM push: https://firebase.google.com/docs/cloud-messaging/ios/client // The FCM SDK provides a device token to the app that you then send to the Customer.io SDK. @@ -38,9 +41,12 @@ class AppDelegate: NSObject, UIApplicationDelegate { .deepLinkCallback { (url: URL) in // You can call any method to process this furhter, // or redirect it to `application(_:continue:restorationHandler:)` for consistency, if you use deep-linking in Firebase - let openLinkInHostAppActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + let openLinkInHostAppActivity = NSUserActivity( + activityType: NSUserActivityTypeBrowsingWeb) openLinkInHostAppActivity.webpageURL = url - return self.application(UIApplication.shared, continue: openLinkInHostAppActivity, restorationHandler: { _ in }) + return self.application( + UIApplication.shared, continue: openLinkInHostAppActivity, + restorationHandler: { _ in }) } let logLevel = appSetSettings?.debugSdkMode if logLevel == nil || logLevel == true { @@ -57,7 +63,9 @@ class AppDelegate: NSObject, UIApplicationDelegate { // Initialize messaging features after initializing Customer.io SDK MessagingInApp - .initialize(withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build()) + .initialize( + withConfig: MessagingInAppConfigBuilder(siteId: siteId, region: .US).build() + ) .setEventListener(self) MessagingPushFCM.initialize( withConfig: MessagingPushConfigBuilder() @@ -71,7 +79,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { Next line of code is used for testing how Firebase behaves when another object is set as the delegate for `UNUserNotificationCenter`. This is not necessary for the Customer.io SDK to work. */ -// UNUserNotificationCenter.current().delegate = anotherPushEventHandler + // UNUserNotificationCenter.current().delegate = anotherPushEventHandler /* Registers the `AppDelegate` class to handle when a push notification gets clicked. @@ -79,14 +87,15 @@ class AppDelegate: NSObject, UIApplicationDelegate { Push notifications sent by Customer.io will be handled by the Customer.io SDK automatically, unless you disabled that feature. Therefore, this line of code is not required if you only want to handle push notifications sent by Customer.io. */ -// UNUserNotificationCenter.current().delegate = self + // UNUserNotificationCenter.current().delegate = self return true } - // IMPORTANT: If FCM is used with enabled swizzling (default state) it will not call this method in SwiftUI based apps. - // Use `deepLinkCallback` on SDKConfigBuilder, as that works in all scenarios. - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { + func application( + _ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void + ) -> Bool { guard let universalLinkUrl = userActivity.webpageURL else { return false } @@ -108,7 +117,6 @@ class AppDelegate: NSObject, UIApplicationDelegate { // // Function called when a push notification is clicked or swiped away. // func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { // // Track custom event with Customer.io. -// // NOT required for basic PN tap tracking - that is done automatically with `CioAppDelegateWrapper`. // CustomerIO.shared.track( // name: "custom push-clicked event", // properties: ["push": response.notification.request.content.userInfo] @@ -127,30 +135,40 @@ extension AppDelegate: InAppEventListener { nonisolated func messageShown(message: InAppMessage) { CustomerIO.shared.track( name: "inapp shown", - properties: ["delivery-id": message.deliveryId ?? "(none)", "message-id": message.messageId] + properties: [ + "delivery-id": message.deliveryId ?? "(none)", "message-id": message.messageId, + ] ) } nonisolated func messageDismissed(message: InAppMessage) { CustomerIO.shared.track( name: "inapp dismissed", - properties: ["delivery-id": message.deliveryId ?? "(none)", "message-id": message.messageId] + properties: [ + "delivery-id": message.deliveryId ?? "(none)", "message-id": message.messageId, + ] ) } nonisolated func errorWithMessage(message: InAppMessage) { CustomerIO.shared.track( name: "inapp error", - properties: ["delivery-id": message.deliveryId ?? "(none)", "message-id": message.messageId] + properties: [ + "delivery-id": message.deliveryId ?? "(none)", "message-id": message.messageId, + ] ) } - nonisolated func messageActionTaken(message: InAppMessage, actionValue: String, actionName: String) { - CustomerIO.shared.track(name: "inapp action", properties: [ - "delivery-id": message.deliveryId ?? "(none)", - "message-id": message.messageId, - "action-value": actionValue, - "action-name": actionName - ]) + nonisolated func messageActionTaken( + message: InAppMessage, actionValue: String, actionName: String + ) { + CustomerIO.shared.track( + name: "inapp action", + properties: [ + "delivery-id": message.deliveryId ?? "(none)", + "message-id": message.messageId, + "action-value": actionValue, + "action-name": actionName, + ]) } } diff --git a/Apps/CocoaPods-FCM/test cocoapods.xcodeproj/project.pbxproj b/Apps/CocoaPods-FCM/test cocoapods.xcodeproj/project.pbxproj index ee6a74c6a..fd2ee8bea 100644 --- a/Apps/CocoaPods-FCM/test cocoapods.xcodeproj/project.pbxproj +++ b/Apps/CocoaPods-FCM/test cocoapods.xcodeproj/project.pbxproj @@ -434,14 +434,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-test cocoapods/Pods-test cocoapods-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-test cocoapods/Pods-test cocoapods-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-test cocoapods/Pods-test cocoapods-frameworks.sh\"\n"; diff --git a/Sources/MessagingPush/Integration/CioProviderAgnosticAppDelegate.swift b/Sources/MessagingPush/Integration/CioProviderAgnosticAppDelegate.swift deleted file mode 100644 index 44f3c187c..000000000 --- a/Sources/MessagingPush/Integration/CioProviderAgnosticAppDelegate.swift +++ /dev/null @@ -1,229 +0,0 @@ -import CioInternalCommon -import UIKit - -public typealias CioAppDelegateType = NSObject & UIApplicationDelegate - -public typealias ConfigInstance = () -> MessagingPushConfigOptions - -public typealias UserNotificationCenterInstance = () -> UserNotificationCenterIntegration - -// sourcery: AutoMockable -public protocol UserNotificationCenterIntegration { - var delegate: UNUserNotificationCenterDelegate? { get set } -} - -extension UNUserNotificationCenter: UserNotificationCenterIntegration {} - -private extension UIApplication { - func cioRegisterForRemoteNotifications(logger: Logger) { - logger.debug("CIO: Registering for remote notifications") - registerForRemoteNotifications() - } -} - -@available(iOSApplicationExtension, unavailable) -open class CioProviderAgnosticAppDelegate: CioAppDelegateType, UNUserNotificationCenterDelegate { - @_spi(Internal) public let messagingPush: MessagingPushInstance - @_spi(Internal) public let logger: Logger - @_spi(Internal) public var implementedOptionalMethods: Set = [ - // UIApplicationDelegate - #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)), - #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)), - #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)), - #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)), - #selector(UIApplicationDelegate.application(_:open:options:)), - // UNUserNotificationCenterDelegate - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)), - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)), - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:openSettingsFor:)) - ] - - @_spi(Internal) public var config: ConfigInstance? - - private var userNotificationCenter: UserNotificationCenterInstance? - private let wrappedAppDelegate: UIApplicationDelegate? - private var wrappedNotificationCenterDelegate: UNUserNotificationCenterDelegate? - - override public convenience init() { - DIGraphShared.shared.logger.error("CIO: This no-argument initializer should not to be used. Added since UIKit's AppDelegate initialization process crashes if for no-arg init is missing.") - self.init( - messagingPush: MessagingPush.shared, - userNotificationCenter: { UNUserNotificationCenter.current() }, - appDelegate: nil, - config: nil, - logger: DIGraphShared.shared.logger - ) - } - - public init( - messagingPush: MessagingPushInstance, - userNotificationCenter: UserNotificationCenterInstance?, - appDelegate: CioAppDelegateType? = nil, - config: ConfigInstance? = nil, - logger: Logger - ) { - self.messagingPush = messagingPush - self.userNotificationCenter = userNotificationCenter - self.logger = logger - self.config = config - self.wrappedAppDelegate = appDelegate - super.init() - } - - open func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - MessagingPush.appDelegateIntegratedExplicitly = true - - let result = wrappedAppDelegate?.application?(application, didFinishLaunchingWithOptions: launchOptions) - - if config?().autoFetchDeviceToken ?? false { - application.cioRegisterForRemoteNotifications(logger: logger) - } - - if config?().autoTrackPushEvents ?? false, - var center = userNotificationCenter?() { - wrappedNotificationCenterDelegate = center.delegate - center.delegate = self - } - - return result ?? true - } - - open func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - wrappedAppDelegate?.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - } - - open func application( - _ application: UIApplication, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) { - wrappedAppDelegate?.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) - - logger.error("CIO: Device token is deleted for current user. Failed to register for remote notifications: \(error.localizedDescription)") - if config?().autoFetchDeviceToken ?? false { - messagingPush.deleteDeviceToken() - } - } - - // MARK: - UNUserNotificationCenterDelegate - - open func userNotificationCenter( - _ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void - ) { - // `completionHandlerCalled` is used to overcome limitation of `responds(to:)` when working with optional methods from protocol. - // Component may implement the protocol, but not specific optional method. In this case `responds(to:)` will return true. - // Explicit flag, `completionHandlerCalled` in this case, allows us to detect if method is implemented, since it's required by Apple to - // call `completionHandler` before returning. - var completionHandlerCalled = false - if let wrappedNotificationCenterDelegate = wrappedNotificationCenterDelegate, - wrappedNotificationCenterDelegate.responds(to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:))) { - wrappedNotificationCenterDelegate.userNotificationCenter?( - center, - willPresent: notification, - withCompletionHandler: { options in - completionHandler(options) - completionHandlerCalled = true - } - ) - } - - guard !completionHandlerCalled else { return } - - if config?().showPushAppInForeground ?? false { - if #available(iOS 14.0, *) { - completionHandler([.list, .banner, .badge, .sound]) - } else { - completionHandler([.alert, .badge, .sound]) - } - } else { - // Don't show the notification in the foreground - completionHandler([]) - } - } - - // Function called when a push notification is clicked or swiped away. - open func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) { - // Cast to concrete type since method was removed from protocol - if let implementation = messagingPush as? MessagingPush { - _ = implementation.userNotificationCenter(center, didReceive: response) - } - - // `completionHandlerCalled` is used to overcome limitation of `responds(to:)` when working with optional methods from protocol. - // Component may implement the protocol, but not specific optional method. In this case `responds(to:)` will return true. - // Explicit flag, `completionHandlerCalled` in this case, allows us to detect if method is implemented, since it's required by Apple to - // call `completionHandler` before returning. - var completionHandlerCalled = false - if let wrappedNotificationCenterDelegate = wrappedNotificationCenterDelegate, - wrappedNotificationCenterDelegate.responds(to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:))) { - wrappedNotificationCenterDelegate.userNotificationCenter?( - center, - didReceive: response, - withCompletionHandler: { - completionHandler() - completionHandlerCalled = true - } - ) - } - - guard !completionHandlerCalled else { return } - - completionHandler() - } - - // MARK: - method forwarding - - @objc - override public func responds(to aSelector: Selector!) -> Bool { - if implementedOptionalMethods.contains(aSelector), super.responds(to: aSelector) { - return true - } - return wrappedAppDelegate?.responds(to: aSelector) ?? false - } - - @objc - override public func forwardingTarget(for aSelector: Selector!) -> Any? { - if implementedOptionalMethods.contains(aSelector), super.responds(to: aSelector) { - return self - } - if let wrappedAppDelegate = wrappedAppDelegate, - wrappedAppDelegate.responds(to: aSelector) { - return wrappedAppDelegate - } - return nil - } -} - -/// Prevent issues caused by swizzling in various SDKs: -/// - those are not using `responds(to:)` and `forwardingTarget(for:)`, but only check does original implementation exist -/// - this is the case with FirebaseMassaging -/// - for this reason, empty methods are added and forwarding to wrapper is possible -@available(iOSApplicationExtension, unavailable) -extension CioProviderAgnosticAppDelegate { - open func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { - wrappedNotificationCenterDelegate?.userNotificationCenter?(center, openSettingsFor: notification) - } - - @objc - open func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { - wrappedAppDelegate?.application?(application, continue: userActivity, restorationHandler: restorationHandler) ?? false - } - - open func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey: Any] = [:] - ) -> Bool { - wrappedAppDelegate?.application?(app, open: url, options: options) ?? false - } -} diff --git a/Sources/MessagingPush/Integration/PushNotificationCenterRegistrar.swift b/Sources/MessagingPush/Integration/PushNotificationCenterRegistrar.swift new file mode 100644 index 000000000..fd6276e2f --- /dev/null +++ b/Sources/MessagingPush/Integration/PushNotificationCenterRegistrar.swift @@ -0,0 +1,79 @@ +import CioInternalCommon +import Foundation +import UserNotifications + +/// Manages UNUserNotificationCenter delegate registration, replacing the AppDelegate-based integration. +@available(iOSApplicationExtension, unavailable) +// sourcery: AutoMockable +protocol PushNotificationCenterRegistrar { + /// Captures any existing UNUserNotificationCenter delegate into the push handler proxy, + /// then registers the SDK as the sole delegate. + /// Call this during SDK initialization when `autoTrackPushEvents` is enabled. + func activate() +} + +/// Sets the SDK as the sole UNUserNotificationCenter delegate on SDK initialization, +/// capturing any previously registered delegate into the push event handler proxy so it +/// continues to receive forwarded events. This replaces the AppDelegate subclassing +/// and swizzling-based integration approaches. +@available(iOSApplicationExtension, unavailable) +// sourcery: InjectRegisterShared = "PushNotificationCenterRegistrar" +// sourcery: InjectSingleton +class PushNotificationCenterRegistrarImpl: NSObject, UNUserNotificationCenterDelegate, + PushNotificationCenterRegistrar +{ + private let pushEventHandler: PushEventHandler + private let pushEventHandlerProxy: PushEventHandlerProxy + private var userNotificationCenter: UserNotificationCenter + + init( + pushEventHandler: PushEventHandler, + pushEventHandlerProxy: PushEventHandlerProxy, + userNotificationCenter: UserNotificationCenter + ) { + self.pushEventHandler = pushEventHandler + self.pushEventHandlerProxy = pushEventHandlerProxy + self.userNotificationCenter = userNotificationCenter + } + + func activate() { + if let existingDelegate = userNotificationCenter.currentDelegate { + pushEventHandlerProxy.addPushEventHandler( + UNUserNotificationCenterDelegateWrapper(delegate: existingDelegate) + ) + } + userNotificationCenter.currentDelegate = self + } + + // MARK: - UNUserNotificationCenterDelegate + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: + @escaping (UNNotificationPresentationOptions) -> Void + ) { + pushEventHandler.shouldDisplayPushAppInForeground( + UNNotificationWrapper(notification: notification) + ) { shouldShowPush in + if shouldShowPush { + if #available(iOS 14.0, *) { + completionHandler([.list, .banner, .badge, .sound]) + } else { + completionHandler([.alert, .badge, .sound]) + } + } else { + completionHandler([]) + } + } + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + pushEventHandler.onPushAction( + UNNotificationResponseWrapper(response: response), completionHandler: completionHandler) + } +} diff --git a/Sources/MessagingPush/MessagingPush.swift b/Sources/MessagingPush/MessagingPush.swift index 6e6448bc5..4e2f6ab8f 100644 --- a/Sources/MessagingPush/MessagingPush.swift +++ b/Sources/MessagingPush/MessagingPush.swift @@ -1,18 +1,16 @@ import CioInternalCommon import Foundation + #if canImport(UserNotifications) -import UserNotifications + import UserNotifications #endif -/** - Swift code goes into this module that are common to *all* of the Messaging Push modules (APN, FCM, etc). - So, performing an HTTP request to the API with a device token goes here. - */ +/// Swift code goes into this module that are common to *all* of the Messaging Push modules (APN, FCM, etc). +/// So, performing an HTTP request to the API with a device token goes here. public class MessagingPush: ModuleTopLevelObject, MessagingPushInstance { - @_spi(Internal) public static var appDelegateIntegratedExplicitly: Bool = false - @Atomic public private(set) static var shared = MessagingPush() - @Atomic public private(set) static var moduleConfig: MessagingPushConfigOptions = MessagingPushConfigBuilder().build() + @Atomic public private(set) static var moduleConfig: MessagingPushConfigOptions = + MessagingPushConfigBuilder().build() private static let moduleName = "MessagingPush" @@ -25,29 +23,36 @@ public class MessagingPush: ModuleTopLevelObject, Messagi } #if DEBUG - // Methods to set up the test environment. - // In unit tests, any implementation of the interface works, while integration tests use the actual implementation. - - @discardableResult - static func setUpSharedInstanceForUnitTest(implementation: MessagingPushInstance, diGraphShared: DIGraphShared, config: MessagingPushConfigOptions) -> MessagingPushInstance { - // initialize static properties before implementation creation, as they may be directly used by other classes - moduleConfig = config - shared.globalDataStore = diGraphShared.globalDataStore - shared._implementation = implementation - return implementation - } + // Methods to set up the test environment. + // In unit tests, any implementation of the interface works, while integration tests use the actual implementation. + + @discardableResult + static func setUpSharedInstanceForUnitTest( + implementation: MessagingPushInstance, diGraphShared: DIGraphShared, + config: MessagingPushConfigOptions + ) -> MessagingPushInstance { + // initialize static properties before implementation creation, as they may be directly used by other classes + moduleConfig = config + shared.globalDataStore = diGraphShared.globalDataStore + shared._implementation = implementation + return implementation + } - @discardableResult - static func setUpSharedInstanceForIntegrationTest(diGraphShared: DIGraphShared, config: MessagingPushConfigOptions) -> MessagingPushInstance { - moduleConfig = config - let implementation = MessagingPushImplementation(diGraph: diGraphShared, moduleConfig: config) - return setUpSharedInstanceForUnitTest(implementation: implementation, diGraphShared: diGraphShared, config: config) - } + @discardableResult + static func setUpSharedInstanceForIntegrationTest( + diGraphShared: DIGraphShared, config: MessagingPushConfigOptions + ) -> MessagingPushInstance { + moduleConfig = config + let implementation = MessagingPushImplementation( + diGraph: diGraphShared, moduleConfig: config) + return setUpSharedInstanceForUnitTest( + implementation: implementation, diGraphShared: diGraphShared, config: config) + } - static func resetTestEnvironment() { - moduleConfig = MessagingPushConfigBuilder().build() - shared = MessagingPush() - } + static func resetTestEnvironment() { + moduleConfig = MessagingPushConfigBuilder().build() + shared = MessagingPush() + } #endif /** @@ -56,14 +61,16 @@ public class MessagingPush: ModuleTopLevelObject, Messagi */ @discardableResult @available(iOSApplicationExtension, unavailable) - public static func initialize(withConfig config: MessagingPushConfigOptions = MessagingPushConfigBuilder().build()) -> MessagingPushInstance { + public static func initialize( + withConfig config: MessagingPushConfigOptions = MessagingPushConfigBuilder().build() + ) -> MessagingPushInstance { shared.initializeModuleIfNotAlready { // set moduleConfig before creating implementation instance as dependencies inside instance may directly use moduleConfig from MessagingPush. Self.moduleConfig = config // Some part of the initialize is specific only to non-NSE targets. // Put those parts in this non-NSE initialize method. - if config.autoTrackPushEvents, !Self.appDelegateIntegratedExplicitly { - DIGraphShared.shared.automaticPushClickHandling.start() + if config.autoTrackPushEvents { + DIGraphShared.shared.pushNotificationCenterRegistrar.activate() } return shared.getImplementation(config: config) @@ -78,7 +85,9 @@ public class MessagingPush: ModuleTopLevelObject, Messagi @available(iOSApplicationExtension, introduced: 13.0) @available(visionOSApplicationExtension, introduced: 1.0) @discardableResult - public static func initializeForExtension(withConfig config: MessagingPushConfigOptions) -> MessagingPushInstance { + public static func initializeForExtension(withConfig config: MessagingPushConfigOptions) + -> MessagingPushInstance + { shared.initializeModuleIfNotAlready { // set moduleConfig before creating implementation instance as dependencies inside instance may directly use moduleConfig from MessagingPush. Self.moduleConfig = config @@ -134,37 +143,37 @@ public class MessagingPush: ModuleTopLevelObject, Messagi } #if canImport(UserNotifications) - /** - - returns: - Bool indicating if this push notification is one handled by Customer.io SDK or not. - If function returns `false`, `contentHandler` will *not* be called by the SDK. - */ - @discardableResult - public func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) -> Bool { - guard let implementation = implementation else { - contentHandler(request.content) - return false - } + /** + - returns: + Bool indicating if this push notification is one handled by Customer.io SDK or not. + If function returns `false`, `contentHandler` will *not* be called by the SDK. + */ + @discardableResult + public func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) -> Bool { + guard let implementation = implementation else { + contentHandler(request.content) + return false + } - return implementation.didReceive(request, withContentHandler: contentHandler) - } + return implementation.didReceive(request, withContentHandler: contentHandler) + } - /** - iOS telling the notification service to hurry up and stop modifying the push notifications. - Stop all network requests and modifying and show the push for what it looks like now. - */ - public func serviceExtensionTimeWillExpire() { - implementation?.serviceExtensionTimeWillExpire() - } + /** + iOS telling the notification service to hurry up and stop modifying the push notifications. + Stop all network requests and modifying and show the push for what it looks like now. + */ + public func serviceExtensionTimeWillExpire() { + implementation?.serviceExtensionTimeWillExpire() + } #endif } // Convenient way for other modules to access instance as well as being able to mock instance in tests. -public extension DIGraphShared { - var messagingPushInstance: MessagingPushInstance { +extension DIGraphShared { + public var messagingPushInstance: MessagingPushInstance { if let override: MessagingPushInstance = getOverriddenInstance() { return override } diff --git a/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift b/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift deleted file mode 100644 index 0972b9acc..000000000 --- a/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift +++ /dev/null @@ -1,57 +0,0 @@ -import CioInternalCommon -import Foundation - -/** - Sets up the CIO SDK to automatically handle events related to push notifications such as when a push is clicked or deciding if a push should be shown while app is in foreground. - - This feature is complex and these docs are meant to explain how it works. - - When a push notification is clicked on an iOS device, iOS will notify the iOS app for that notification. iOS does this by: - 1. Getting an instance of `UNUserNotificationCenterDelegate` from `UNUserNotificationCenter.current().delegate`. - 2. Calling `userNotificationCenter(_:didReceive:withCompletionHandler:)` on the delegate. - - This is simple when the CIO SDK is the only SDK in an app that needs to get notified when a push is clicked on for a host app. When a customer has installed multiple SDKs into their app that all want to handle when a push is clicked, that's when this gets complex. That's because iOS has a restriction that only 1 object can be set as the `UNUserNotificationCenter.current().delegate`. The CIO SDK when it's initialized can set itself as this delegate instance, but then another SDK can set itself as the delegate instead so the CIO SDK is then no longer called when a push is clicked. - - To solve this problem, the CIO SDK performs this logic: - 1. The CIO SDK forces itself to always be the only `UNUserNotificationCenter.current().delegate` instance of the host iOS app. - - The SDK does this via swizzling. When `UNUserNotificationCenter.current().delegate` setter gets called, the CIO SDK gets notified that this event happened. When a new delegate gets set, the CIO SDK reverses this action by resetting the CIO SDK `UNUserNotificationCenterDelegate` instance as the delegate. - - Related code for this logic: - * `iOSPushEventListener` - where swizzling is. - * `iOSPushEventListener` - is the CIO SDK instance of `UNUserNotificationCenterDelegate`. - - 2. When the CIO SDK's instance of `UNUserNotificationCenterDelegate` is called, the SDK forwards that push click event to all other `UNUserNotificationCenterDelegate` instances registered to the app. This allows other SDKs the ability to handle a push click event for pushes not sent by CIO. - - Related code for this logic: - * `NotificationCenterDelegateProxy` - stores all other `UNUserNotificationCenterDelegate` instances that have been registered with the host app. - * `iOSPushEventListener` - is what calls the proxy class when a push is clicked that was not sent by CIO. - - The goal of this feature is: - 1. The customer does not need to interact with `UNUserNotificationCenter` themselves to get the CIO SDK to process when a push is clicked. The CIO SDK should be able to set this up itself. - 2. The CIO SDK should be able to stay compatible with other SDKs that also want to handle push click events. A customer should be able to install 2+ push notification SDKs in an app and all of them are able to work, even though iOS only allows 1 `UNUserNotificationCenterDelegate` instance to be set in the app. - */ -@available(iOSApplicationExtension, unavailable) -@available(*, deprecated, message: "This swizzling based system is replaced with CioAppDelegate(Wrapper)") -protocol AutomaticPushClickHandling: AutoMockable { - func start() -} - -@available(iOSApplicationExtension, unavailable) -@available(*, deprecated, message: "This swizzling based system is replaced with CioAppDelegate(Wrapper)") -// sourcery: InjectRegisterShared = "AutomaticPushClickHandling" -class AutomaticPushClickHandlingImpl: AutomaticPushClickHandling { - private let notificationCenterAdapter: UserNotificationsFrameworkAdapter - private let logger: Logger - - init(notificationCenterAdapter: UserNotificationsFrameworkAdapter, logger: Logger) { - self.notificationCenterAdapter = notificationCenterAdapter - self.logger = logger - } - - func start() { - logger.debug("Starting automatic push click handling.") - - notificationCenterAdapter.beginListeningNewNotificationCenterDelegateSet() - } -} diff --git a/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift b/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift deleted file mode 100644 index db9b0568a..000000000 --- a/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift +++ /dev/null @@ -1,129 +0,0 @@ -import CioInternalCommon -import Foundation -import UserNotifications - -/** - The iOS framework, `UserNotifications`, is abstracted away from the SDK codebase. - - This file is the connection between our SDK and `UserNotifications`. - In production, iOS will call functions in this file. Those requests are then forwarded onto the abstracted code in the SDK to perform all of the logic. - */ -@available(iOSApplicationExtension, unavailable) -@available(*, deprecated, message: "This swizzling based system is replaced with CioAppDelegate(Wrapper)") -protocol UserNotificationsFrameworkAdapter { - // A reference to an instance of UNUserNotificationCenterDelegate that we can provide to iOS in production. - var delegate: UNUserNotificationCenterDelegate { get } - - func beginListeningNewNotificationCenterDelegateSet() - - // Called when a new `UNUserNotificationCenterDelegate` is set on the host app. Our Swizzling is what calls this function. - func newNotificationCenterDelegateSet(_ newDelegate: UNUserNotificationCenterDelegate?) -} - -/** - Keep this class small and simple because it is only able to be tested in QA testing. All logic for handling push events should be in the rest of the code base that has automated tests around it. - */ -@available(iOSApplicationExtension, unavailable) -@available(*, deprecated, message: "This swizzling based system is replaced with CioAppDelegate(Wrapper)") -// sourcery: InjectRegisterShared = "UserNotificationsFrameworkAdapter" -// sourcery: InjectSingleton -class UserNotificationsFrameworkAdapterImpl: NSObject, UNUserNotificationCenterDelegate, UserNotificationsFrameworkAdapter { - private var pushEventHandler: PushEventHandler - private var userNotificationCenter: UserNotificationCenter - private var notificationCenterDelegateProxy: PushEventHandlerProxy - - init( - pushEventHandler: PushEventHandler, - userNotificationCenter: UserNotificationCenter, - notificationCenterDelegateProxy: PushEventHandlerProxy - ) { - self.pushEventHandler = pushEventHandler - self.userNotificationCenter = userNotificationCenter - self.notificationCenterDelegateProxy = notificationCenterDelegateProxy - } - - var delegate: UNUserNotificationCenterDelegate { - self - } - - // MARK: Swizzling to get notified when a new delegate is set on host app. - - // The swizzling is tightly coupled to the UserNotitications framework. So, the swizzling is housed in this file. - - func beginListeningNewNotificationCenterDelegateSet() { - // Sets up swizzling of `UNUserNotificationCenter.current().delegate` setter to get notified when a new delegate is set on host app. - swizzle( - forClass: UNUserNotificationCenter.self, - original: #selector(setter: UNUserNotificationCenter.delegate), - new: #selector(UNUserNotificationCenter.cio_swizzled_setDelegate(delegate:)) - ) - - // This re-assignment triggers NotifiationCenter.delegate setter that we swizzled. We want to run this logic now in case a delegate is already set before the CIO SDK is initialized. - userNotificationCenter.currentDelegate = userNotificationCenter.currentDelegate - } - - func newNotificationCenterDelegateSet(_ newDelegate: UNUserNotificationCenterDelegate?) { - guard let newDelegate = newDelegate else { - return - } - - notificationCenterDelegateProxy.addPushEventHandler(UNUserNotificationCenterDelegateWrapper(delegate: newDelegate)) - } - - // MARK: UNUserNotificationCenterDelegate functions. - - // Functions called by iOS framework, `UserNotifications`. This adapter class simply passes these requests to other code in our SDK where the logic exists. - // Convert UserNotifications files into abstracted data types that our SDK understands. - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - pushEventHandler.onPushAction(UNNotificationResponseWrapper(response: response), completionHandler: completionHandler) - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - pushEventHandler.shouldDisplayPushAppInForeground(UNNotificationWrapper(notification: notification)) { shouldShowPush in - if shouldShowPush { - if #available(iOS 14.0, *) { - completionHandler([.list, .banner, .badge, .sound]) - } else { - completionHandler([.badge, .sound]) - } - } else { - completionHandler([]) - } - } - } - - // Swizzle method convenient when original and swizzled methods both belong to same class. - func swizzle(forClass: AnyClass, original: Selector, new: Selector) { - guard let originalMethod = class_getInstanceMethod(forClass, original) else { return } - guard let swizzledMethod = class_getInstanceMethod(forClass, new) else { return } - - let didAddMethod = class_addMethod(forClass, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) - - if didAddMethod { - class_replaceMethod(forClass, new, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) - } else { - method_exchangeImplementations(originalMethod, swizzledMethod) - } - } -} - -// This is a bit confusing and makes the code a little more complex. However, it's the most reliable way found to get UNUserNotificationCenter.delegate swizzling to work by using an extension. -@available(iOSApplicationExtension, unavailable) -extension UNUserNotificationCenter { - // Swizzled method that gets called when `UNUserNotificationCenter.current().delegate` setter called. - @objc dynamic func cio_swizzled_setDelegate(delegate: UNUserNotificationCenterDelegate?) { - let logger = DIGraphShared.shared.logger - let userNotificationsFrameworkAdapter = DIGraphShared.shared.userNotificationsFrameworkAdapter - - logger.debug("New UNUserNotificationCenter.delegate set. Delegate class: \(String(describing: delegate))") - - userNotificationsFrameworkAdapter.newNotificationCenterDelegateSet(delegate) - - // Forward request to the original implementation that we swizzled. So that the app finishes setting UNUserNotificationCenter.delegate. - // - // Instead of providing the given 'delegate', provide CIO SDK's click handler. - // This will force our SDK to be the 1 push click handler of the app instead of the given 'delegate'. - cio_swizzled_setDelegate(delegate: userNotificationsFrameworkAdapter.delegate) - } -} diff --git a/Sources/MessagingPush/autogenerated/AutoDependencyInjection.generated.swift b/Sources/MessagingPush/autogenerated/AutoDependencyInjection.generated.swift index ce961032c..4547c083c 100644 --- a/Sources/MessagingPush/autogenerated/AutoDependencyInjection.generated.swift +++ b/Sources/MessagingPush/autogenerated/AutoDependencyInjection.generated.swift @@ -54,9 +54,6 @@ extension DIGraphShared { func testDependenciesAbleToResolve() -> Int { var countDependenciesResolved = 0 - _ = automaticPushClickHandling - countDependenciesResolved += 1 - _ = pushEventHandler countDependenciesResolved += 1 @@ -69,6 +66,9 @@ extension DIGraphShared { _ = pushHistory countDependenciesResolved += 1 + _ = pushNotificationCenterRegistrar + countDependenciesResolved += 1 + _ = pushNotificationLogger countDependenciesResolved += 1 @@ -81,25 +81,10 @@ extension DIGraphShared { _ = userNotificationCenter countDependenciesResolved += 1 - _ = userNotificationsFrameworkAdapter - countDependenciesResolved += 1 - return countDependenciesResolved } // Handle classes annotated with InjectRegisterShared - // AutomaticPushClickHandling - @available(iOSApplicationExtension, unavailable) - var automaticPushClickHandling: AutomaticPushClickHandling { - getOverriddenInstance() ?? - newAutomaticPushClickHandling - } - - @available(iOSApplicationExtension, unavailable) - private var newAutomaticPushClickHandling: AutomaticPushClickHandling { - AutomaticPushClickHandlingImpl(notificationCenterAdapter: userNotificationsFrameworkAdapter, logger: logger) - } - // PushEventHandler @available(iOSApplicationExtension, unavailable) var pushEventHandler: PushEventHandler { @@ -150,6 +135,20 @@ extension DIGraphShared { PushHistoryImpl(lockManager: lockManager) } + // PushNotificationCenterRegistrar (singleton) + @available(iOSApplicationExtension, unavailable) + var pushNotificationCenterRegistrar: PushNotificationCenterRegistrar { + getOverriddenInstance() ?? + getSingletonOrCreate { + _get_pushNotificationCenterRegistrar() + } + } + + @available(iOSApplicationExtension, unavailable) + private func _get_pushNotificationCenterRegistrar() -> PushNotificationCenterRegistrar { + PushNotificationCenterRegistrarImpl(pushEventHandler: pushEventHandler, pushEventHandlerProxy: pushEventHandlerProxy, userNotificationCenter: userNotificationCenter) + } + // PushNotificationLogger var pushNotificationLogger: PushNotificationLogger { getOverriddenInstance() ?? @@ -189,20 +188,6 @@ extension DIGraphShared { private var newUserNotificationCenter: UserNotificationCenter { UserNotificationCenterImpl() } - - // UserNotificationsFrameworkAdapter (singleton) - @available(iOSApplicationExtension, unavailable) - var userNotificationsFrameworkAdapter: UserNotificationsFrameworkAdapter { - getOverriddenInstance() ?? - getSingletonOrCreate { - _get_userNotificationsFrameworkAdapter() - } - } - - @available(iOSApplicationExtension, unavailable) - private func _get_userNotificationsFrameworkAdapter() -> UserNotificationsFrameworkAdapter { - UserNotificationsFrameworkAdapterImpl(pushEventHandler: pushEventHandler, userNotificationCenter: userNotificationCenter, notificationCenterDelegateProxy: pushEventHandlerProxy) - } } // swiftlint:enable all diff --git a/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift b/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift index c20a83ebe..5044e1787 100644 --- a/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift +++ b/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift @@ -80,48 +80,6 @@ import CioInternalCommon */ -/** - Class to easily create a mocked version of the `AutomaticPushClickHandling` class. - This class is equipped with functions and properties ready for you to mock! - - Note: This file is automatically generated. This means the mocks should always be up-to-date and has a consistent API. - See the SDK documentation to learn the basics behind using the mock classes in the SDK. - */ -@available(iOSApplicationExtension, unavailable) -class AutomaticPushClickHandlingMock: AutomaticPushClickHandling, Mock { - /// If *any* interactions done on mock. `true` if any method or property getter/setter called. - var mockCalled: Bool = false // - - init() {} - - public func resetMock() { - startCallsCount = 0 - - mockCalled = false // do last as resetting properties above can make this true - } - - // MARK: - start - - /// Number of times the function was called. - @Atomic private(set) var startCallsCount = 0 - /// `true` if the function was ever called. - var startCalled: Bool { - startCallsCount > 0 - } - - /** - Set closure to get called when function gets called. Great way to test logic or return a value for the function. - */ - var startClosure: (() -> Void)? - - /// Mocked function for `start()`. Your opportunity to return a mocked value and check result of mock in test code. - func start() { - mockCalled = true - startCallsCount += 1 - startClosure?() - } -} - /** Class to easily create a mocked version of the `MessagingPushInstance` class. This class is equipped with functions and properties ready for you to mock! @@ -696,6 +654,48 @@ class PushHistoryMock: PushHistory, Mock { } } +/** + Class to easily create a mocked version of the `PushNotificationCenterRegistrar` class. + This class is equipped with functions and properties ready for you to mock! + + Note: This file is automatically generated. This means the mocks should always be up-to-date and has a consistent API. + See the SDK documentation to learn the basics behind using the mock classes in the SDK. + */ +@available(iOSApplicationExtension, unavailable) +class PushNotificationCenterRegistrarMock: PushNotificationCenterRegistrar, Mock { + /// If *any* interactions done on mock. `true` if any method or property getter/setter called. + var mockCalled: Bool = false // + + init() {} + + public func resetMock() { + activateCallsCount = 0 + + mockCalled = false // do last as resetting properties above can make this true + } + + // MARK: - activate + + /// Number of times the function was called. + @Atomic private(set) var activateCallsCount = 0 + /// `true` if the function was ever called. + var activateCalled: Bool { + activateCallsCount > 0 + } + + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + */ + var activateClosure: (() -> Void)? + + /// Mocked function for `activate()`. Your opportunity to return a mocked value and check result of mock in test code. + func activate() { + mockCalled = true + activateCallsCount += 1 + activateClosure?() + } +} + /** Class to easily create a mocked version of the `PushNotificationLogger` class. This class is equipped with functions and properties ready for you to mock! @@ -1178,60 +1178,4 @@ class UserNotificationCenterMock: UserNotificationCenter, Mock { } } -/** - Class to easily create a mocked version of the `UserNotificationCenterIntegration` class. - This class is equipped with functions and properties ready for you to mock! - - Note: This file is automatically generated. This means the mocks should always be up-to-date and has a consistent API. - See the SDK documentation to learn the basics behind using the mock classes in the SDK. - */ -public class UserNotificationCenterIntegrationMock: UserNotificationCenterIntegration, Mock { - /// If *any* interactions done on mock. `true` if any method or property getter/setter called. - public var mockCalled: Bool = false // - - public init() {} - - /** - When setter of the property called, the value given to setter is set here. - When the getter of the property called, the value set here will be returned. Your chance to mock the property. - */ - public var underlyingDelegate: UNUserNotificationCenterDelegate? = nil - /// `true` if the getter or setter of property is called at least once. - public var delegateCalled: Bool { - delegateGetCalled || delegateSetCalled - } - - /// `true` if the getter called on the property at least once. - public var delegateGetCalled: Bool { - delegateGetCallsCount > 0 - } - - public var delegateGetCallsCount = 0 - /// `true` if the setter called on the property at least once. - public var delegateSetCalled: Bool { - delegateSetCallsCount > 0 - } - - public var delegateSetCallsCount = 0 - /// The mocked property with a getter and setter. - public var delegate: UNUserNotificationCenterDelegate? { - get { - mockCalled = true - delegateGetCallsCount += 1 - return underlyingDelegate - } - set(value) { - mockCalled = true - delegateSetCallsCount += 1 - underlyingDelegate = value - } - } - - public func resetMock() { - delegate = nil - delegateGetCallsCount = 0 - delegateSetCallsCount = 0 - } -} - // swiftlint:enable all diff --git a/Sources/MessagingPushAPN/Integration/CioAppDelegateAPN.swift b/Sources/MessagingPushAPN/Integration/CioAppDelegateAPN.swift deleted file mode 100644 index 03246ee56..000000000 --- a/Sources/MessagingPushAPN/Integration/CioAppDelegateAPN.swift +++ /dev/null @@ -1,60 +0,0 @@ -import CioInternalCommon -import UIKit -@_spi(Internal) import CioMessagingPush - -@available(iOSApplicationExtension, unavailable) -open class CioAppDelegate: CioProviderAgnosticAppDelegate { - /// Temporary solution, until interfaces MessagingPushInstance/MessagingPushAPNInstance/MessagingPushFCMInstance are fixed - private var messagingPushAPN: MessagingPushAPNInstance? { - messagingPush as? MessagingPushAPNInstance - } - - public convenience init() { - DIGraphShared.shared.logger.error("CIO: This no-argument initializer should not to be used. Added since UIKit's AppDelegate initialization process crashes if for no-arg init is missing.") - self.init( - messagingPush: MessagingPush.shared, - userNotificationCenter: nil, - appDelegate: nil, - config: nil, - logger: DIGraphShared.shared.logger - ) - } - - override public init( - messagingPush: MessagingPushInstance, - userNotificationCenter: UserNotificationCenterInstance?, - appDelegate: CioAppDelegateType? = nil, - config: ConfigInstance? = nil, - logger: Logger - ) { - super.init( - messagingPush: messagingPush, - userNotificationCenter: userNotificationCenter, - appDelegate: appDelegate, - config: config, - logger: logger - ) - } - - override public func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - - messagingPushAPN?.registerDeviceToken(apnDeviceToken: deviceToken) - } -} - -@available(iOSApplicationExtension, unavailable) -open class CioAppDelegateWrapper: CioAppDelegate { - public init() { - super.init( - messagingPush: MessagingPush.shared, - userNotificationCenter: { UNUserNotificationCenter.current() }, - appDelegate: UserAppDelegate(), - config: { MessagingPush.moduleConfig }, - logger: DIGraphShared.shared.logger - ) - } -} diff --git a/Sources/MessagingPushAPN/MessagingPush+APN.swift b/Sources/MessagingPushAPN/MessagingPush+APN.swift index e50136176..301e95859 100644 --- a/Sources/MessagingPushAPN/MessagingPush+APN.swift +++ b/Sources/MessagingPushAPN/MessagingPush+APN.swift @@ -1,28 +1,13 @@ import CioMessagingPush import Foundation + #if canImport(UserNotifications) -import UserNotifications + import UserNotifications #endif -/** - Convenient extensions so singleton instances of `MessagingPush` can access functions from `MessagingPushAPN`. - */ +/// Convenient extensions so singleton instances of `MessagingPush` can access functions from `MessagingPushAPN`. extension MessagingPush: MessagingPushAPNInstance { public func registerDeviceToken(apnDeviceToken: Data) { MessagingPushAPN.shared.registerDeviceToken(apnDeviceToken: apnDeviceToken) } - - public func application( - _ application: Any, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) { - MessagingPushAPN.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - } - - public func application( - _ application: Any, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) { - MessagingPushAPN.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - } } diff --git a/Sources/MessagingPushAPN/MessagingPushAPN+PushConfigs.swift b/Sources/MessagingPushAPN/MessagingPushAPN+PushConfigs.swift deleted file mode 100644 index 4972f0422..000000000 --- a/Sources/MessagingPushAPN/MessagingPushAPN+PushConfigs.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -import UIKit - -extension MessagingPushAPN { - @available(iOSApplicationExtension, unavailable) - func setupAutoFetchDeviceToken() { - // Swizzle method `didRegisterForRemoteNotificationsWithDeviceToken` - swizzleDidRegisterForRemoteNotifications() - // Register for push notifications to invoke`didRegisterForRemoteNotificationsWithDeviceToken` method - UIApplication.shared.registerForRemoteNotifications() - } - - @available(iOSApplicationExtension, unavailable) - private func swizzleDidRegisterForRemoteNotifications() { - let appDelegate = UIApplication.shared.delegate - let appDelegateClass: AnyClass? = object_getClass(appDelegate) - - // Swizzle `didRegisterForRemoteNotificationsWithDeviceToken` - let originalSelector = #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)) - let swizzledSelector = #selector(application(_:didRegisterForRemoteNotificationsWithDeviceToken:)) - swizzle(forOriginalClass: appDelegateClass, forSwizzledClass: MessagingPushAPN.self, original: originalSelector, new: swizzledSelector) - - // Swizzle `didFailToRegisterForRemoteNotificationsWithError` - let originalSelectorForDidFail = #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)) - let swizzledSelectorForDidFail = #selector(application(_:didFailToRegisterForRemoteNotificationsWithError:)) - swizzle(forOriginalClass: appDelegateClass, forSwizzledClass: MessagingPushAPN.self, original: originalSelectorForDidFail, new: swizzledSelectorForDidFail) - } - - private func swizzle(forOriginalClass: AnyClass?, forSwizzledClass: AnyClass?, original: Selector, new: Selector) { - guard let swizzledMethod = class_getInstanceMethod(forSwizzledClass, new) else { return } - guard let originalMethod = class_getInstanceMethod(forOriginalClass, original) else { - // Add method if it doesn't exist - class_addMethod(forOriginalClass, new, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) - return - } - method_exchangeImplementations(originalMethod, swizzledMethod) - } - - // Swizzled method for APN device token. - @objc - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - Self.shared.registerDeviceToken(apnDeviceToken: deviceToken) - } - - // Swizzled method for `didFailToRegisterForRemoteNotificationsWithError' - @objc - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - MessagingPushAPN.shared.deleteDeviceToken() - } -} diff --git a/Sources/MessagingPushAPN/MessagingPushAPN.swift b/Sources/MessagingPushAPN/MessagingPushAPN.swift index 3225a59b4..fda37f437 100644 --- a/Sources/MessagingPushAPN/MessagingPushAPN.swift +++ b/Sources/MessagingPushAPN/MessagingPushAPN.swift @@ -1,8 +1,12 @@ import CioInternalCommon @_spi(Internal) import CioMessagingPush import Foundation + +#if canImport(UIKit) + import UIKit +#endif #if canImport(UserNotifications) -import UserNotifications + import UserNotifications #endif // Some functions are copied from MessagingPush because @@ -11,18 +15,6 @@ import UserNotifications public protocol MessagingPushAPNInstance: AutoMockable { func registerDeviceToken(apnDeviceToken: Data) - // sourcery:Name=didRegisterForRemoteNotifications - func application( - _ application: Any, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data - ) - - // sourcery:Name=didFailToRegisterForRemoteNotifications - func application( - _ application: Any, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) - func deleteDeviceToken() func trackMetric( @@ -32,23 +24,23 @@ public protocol MessagingPushAPNInstance: AutoMockable { ) #if canImport(UserNotifications) - // Used for rich push - @discardableResult - // sourcery:Name=didReceiveNotificationRequest - // sourcery:IfCanImport=UserNotifications - func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) -> Bool - - // Used for rich push - // sourcery:IfCanImport=UserNotifications - func serviceExtensionTimeWillExpire() + // Used for rich push + @discardableResult + // sourcery:Name=didReceiveNotificationRequest + // sourcery:IfCanImport=UserNotifications + func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) -> Bool + + // Used for rich push + // sourcery:IfCanImport=UserNotifications + func serviceExtensionTimeWillExpire() #endif } public class MessagingPushAPN: MessagingPushAPNInstance { - static let shared = MessagingPushAPN() + public static let shared = MessagingPushAPN() var messagingPush: MessagingPushInstance { MessagingPush.shared @@ -59,14 +51,6 @@ public class MessagingPushAPN: MessagingPushAPNInstance { messagingPush.registerDeviceToken(deviceToken) } - public func application(_ application: Any, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - registerDeviceToken(apnDeviceToken: deviceToken) - } - - public func application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error) { - messagingPush.deleteDeviceToken() - } - public func deleteDeviceToken() { messagingPush.deleteDeviceToken() } @@ -88,8 +72,8 @@ public class MessagingPushAPN: MessagingPushAPNInstance { let implementation = MessagingPush.initialize(withConfig: config) let pushConfigOptions = MessagingPush.moduleConfig - if pushConfigOptions.autoFetchDeviceToken, !MessagingPush.appDelegateIntegratedExplicitly { - shared.setupAutoFetchDeviceToken() + if pushConfigOptions.autoFetchDeviceToken { + UIApplication.shared.registerForRemoteNotifications() } return implementation @@ -101,50 +85,53 @@ public class MessagingPushAPN: MessagingPushAPNInstance { @available(iOSApplicationExtension, introduced: 13.0) @available(visionOSApplicationExtension, introduced: 1.0) @discardableResult - public static func initializeForExtension(withConfig config: MessagingPushConfigOptions) -> MessagingPushInstance { + public static func initializeForExtension(withConfig config: MessagingPushConfigOptions) + -> MessagingPushInstance + { let implementation = MessagingPush.initializeForExtension(withConfig: config) return implementation } #if canImport(UserNotifications) - /** - - returns: - Bool indicating if this push notification is one handled by Customer.io SDK or not. - If function returns `false`, `contentHandler` will *not* be called by the SDK. - */ - @discardableResult - public func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) -> Bool { - messagingPush.didReceive(request, withContentHandler: contentHandler) - } + /** + - returns: + Bool indicating if this push notification is one handled by Customer.io SDK or not. + If function returns `false`, `contentHandler` will *not* be called by the SDK. + */ + @discardableResult + public func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) -> Bool { + messagingPush.didReceive(request, withContentHandler: contentHandler) + } - /** - iOS OS telling the notification service to hurry up and stop modifying the push notifications. - Stop all network requests and modifying and show the push for what it looks like now. - */ - public func serviceExtensionTimeWillExpire() { - messagingPush.serviceExtensionTimeWillExpire() - } + /** + iOS OS telling the notification service to hurry up and stop modifying the push notifications. + Stop all network requests and modifying and show the push for what it looks like now. + */ + public func serviceExtensionTimeWillExpire() { + messagingPush.serviceExtensionTimeWillExpire() + } - @available(iOSApplicationExtension, unavailable) - public func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse - ) -> CustomerIOParsedPushPayload? { - // Use concrete MessagingPush instance since method was removed from protocol - MessagingPush.shared.userNotificationCenter(center, didReceive: response) - } + @available(iOSApplicationExtension, unavailable) + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) -> CustomerIOParsedPushPayload? { + // Use concrete MessagingPush instance since method was removed from protocol + MessagingPush.shared.userNotificationCenter(center, didReceive: response) + } - @available(iOSApplicationExtension, unavailable) - public func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) -> Bool { - // Use concrete MessagingPush instance since method was removed from protocol - MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) - } + @available(iOSApplicationExtension, unavailable) + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) -> Bool { + // Use concrete MessagingPush instance since method was removed from protocol + MessagingPush.shared.userNotificationCenter( + center, didReceive: response, withCompletionHandler: completionHandler) + } #endif } diff --git a/Sources/MessagingPushAPN/autogenerated/AutoMockable.generated.swift b/Sources/MessagingPushAPN/autogenerated/AutoMockable.generated.swift index 79ff947f5..6a7c777e2 100644 --- a/Sources/MessagingPushAPN/autogenerated/AutoMockable.generated.swift +++ b/Sources/MessagingPushAPN/autogenerated/AutoMockable.generated.swift @@ -99,16 +99,6 @@ public class MessagingPushAPNInstanceMock: MessagingPushAPNInstance, Mock { registerDeviceTokenReceivedArguments = nil registerDeviceTokenReceivedInvocations = [] - mockCalled = false // do last as resetting properties above can make this true - didRegisterForRemoteNotificationsCallsCount = 0 - didRegisterForRemoteNotificationsReceivedArguments = nil - didRegisterForRemoteNotificationsReceivedInvocations = [] - - mockCalled = false // do last as resetting properties above can make this true - didFailToRegisterForRemoteNotificationsCallsCount = 0 - didFailToRegisterForRemoteNotificationsReceivedArguments = nil - didFailToRegisterForRemoteNotificationsReceivedInvocations = [] - mockCalled = false // do last as resetting properties above can make this true deleteDeviceTokenCallsCount = 0 @@ -159,60 +149,6 @@ public class MessagingPushAPNInstanceMock: MessagingPushAPNInstance, Mock { registerDeviceTokenClosure?(apnDeviceToken) } - // MARK: - application - - /// Number of times the function was called. - @Atomic public private(set) var didRegisterForRemoteNotificationsCallsCount = 0 - /// `true` if the function was ever called. - public var didRegisterForRemoteNotificationsCalled: Bool { - didRegisterForRemoteNotificationsCallsCount > 0 - } - - /// The arguments from the *last* time the function was called. - @Atomic public private(set) var didRegisterForRemoteNotificationsReceivedArguments: (application: Any, deviceToken: Data)? - /// Arguments from *all* of the times that the function was called. - @Atomic public private(set) var didRegisterForRemoteNotificationsReceivedInvocations: [(application: Any, deviceToken: Data)] = [] - /** - Set closure to get called when function gets called. Great way to test logic or return a value for the function. - */ - public var didRegisterForRemoteNotificationsClosure: ((Any, Data) -> Void)? - - /// Mocked function for `application(_ application: Any, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)`. Your opportunity to return a mocked value and check result of mock in test code. - public func application(_ application: Any, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - mockCalled = true - didRegisterForRemoteNotificationsCallsCount += 1 - didRegisterForRemoteNotificationsReceivedArguments = (application: application, deviceToken: deviceToken) - didRegisterForRemoteNotificationsReceivedInvocations.append((application: application, deviceToken: deviceToken)) - didRegisterForRemoteNotificationsClosure?(application, deviceToken) - } - - // MARK: - application - - /// Number of times the function was called. - @Atomic public private(set) var didFailToRegisterForRemoteNotificationsCallsCount = 0 - /// `true` if the function was ever called. - public var didFailToRegisterForRemoteNotificationsCalled: Bool { - didFailToRegisterForRemoteNotificationsCallsCount > 0 - } - - /// The arguments from the *last* time the function was called. - @Atomic public private(set) var didFailToRegisterForRemoteNotificationsReceivedArguments: (application: Any, error: Error)? - /// Arguments from *all* of the times that the function was called. - @Atomic public private(set) var didFailToRegisterForRemoteNotificationsReceivedInvocations: [(application: Any, error: Error)] = [] - /** - Set closure to get called when function gets called. Great way to test logic or return a value for the function. - */ - public var didFailToRegisterForRemoteNotificationsClosure: ((Any, Error) -> Void)? - - /// Mocked function for `application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error)`. Your opportunity to return a mocked value and check result of mock in test code. - public func application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error) { - mockCalled = true - didFailToRegisterForRemoteNotificationsCallsCount += 1 - didFailToRegisterForRemoteNotificationsReceivedArguments = (application: application, error: error) - didFailToRegisterForRemoteNotificationsReceivedInvocations.append((application: application, error: error)) - didFailToRegisterForRemoteNotificationsClosure?(application, error) - } - // MARK: - deleteDeviceToken /// Number of times the function was called. diff --git a/Sources/MessagingPushFCM/Integration/CioAppDelegateFCM.swift b/Sources/MessagingPushFCM/Integration/CioAppDelegateFCM.swift deleted file mode 100644 index 4160800ea..000000000 --- a/Sources/MessagingPushFCM/Integration/CioAppDelegateFCM.swift +++ /dev/null @@ -1,66 +0,0 @@ -import CioInternalCommon -import UIKit -@_spi(Internal) import CioMessagingPush - -@available(iOSApplicationExtension, unavailable) -open class CioAppDelegate: CioProviderAgnosticAppDelegate, FirebaseServiceDelegate { - /// Temporary solution, until interfaces MessagingPushInstance/MessagingPushAPNInstance/MessagingPushFCMInstance are fixed - private var messagingPushFCM: MessagingPushFCMInstance? { - messagingPush as? MessagingPushFCMInstance - } - - private var firebaseService: FirebaseService? - private var wrappedFirebaseDelegate: FirebaseServiceDelegate? - - public convenience init() { - DIGraphShared.shared.logger.error("CIO: This no-argument initializer should not to be used. Added since UIKit's AppDelegate initialization process crashes if for no-arg init is missing.") - self.init( - messagingPush: MessagingPush.shared, - userNotificationCenter: nil, - appDelegate: nil, - logger: DIGraphShared.shared.logger - ) - } - - override public func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - let result = super.application(application, didFinishLaunchingWithOptions: launchOptions) - - if config?().autoFetchDeviceToken ?? false { - if var service = MessagingPushFCM.shared.firebaseMessaging() { - wrappedFirebaseDelegate = service.delegate - service.delegate = self - } else { - DIGraphShared.shared.logger.error("CIO: firebaseService is nil. Make sure to initialize the MessagingPushFCM SDK before use.") - } - } - - return result - } - - // MARK: - FirebaseServiceDelegate - - public func didReceiveRegistrationToken(_ token: String?) { - if let wrappedFirebaseDelegate { - wrappedFirebaseDelegate.didReceiveRegistrationToken(token) - } - - // Forward the device token to the Customer.io SDK: - messagingPushFCM?.registerDeviceToken(fcmToken: token) - } -} - -@available(iOSApplicationExtension, unavailable) -open class CioAppDelegateWrapper: CioAppDelegate { - public init() { - super.init( - messagingPush: MessagingPush.shared, - userNotificationCenter: { UNUserNotificationCenter.current() }, - appDelegate: UserAppDelegate(), - config: { MessagingPush.moduleConfig }, - logger: DIGraphShared.shared.logger - ) - } -} diff --git a/Sources/MessagingPushFCM/MessagingPush+FCM.swift b/Sources/MessagingPushFCM/MessagingPush+FCM.swift index 584763ff4..df196bcf9 100644 --- a/Sources/MessagingPushFCM/MessagingPush+FCM.swift +++ b/Sources/MessagingPushFCM/MessagingPush+FCM.swift @@ -1,28 +1,13 @@ import CioMessagingPush import Foundation + #if canImport(UserNotifications) -import UserNotifications + import UserNotifications #endif -/** - Convenient extensions so singleton instances of `MessagingPush` can access functions from `MessagingPushFCM`. - */ +/// Convenient extensions so singleton instances of `MessagingPush` can access functions from `MessagingPushFCM`. extension MessagingPush: MessagingPushFCMInstance { public func registerDeviceToken(fcmToken: String?) { MessagingPushFCM.shared.registerDeviceToken(fcmToken: fcmToken) } - - public func messaging( - _ messaging: Any, - didReceiveRegistrationToken fcmToken: String? - ) { - MessagingPushFCM.shared.messaging(messaging, didReceiveRegistrationToken: fcmToken) - } - - public func application( - _ application: Any, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) { - MessagingPushFCM.shared.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - } } diff --git a/Sources/MessagingPushFCM/MessagingPushFCM+PushConfigs.swift b/Sources/MessagingPushFCM/MessagingPushFCM+PushConfigs.swift deleted file mode 100644 index 5a2500691..000000000 --- a/Sources/MessagingPushFCM/MessagingPushFCM+PushConfigs.swift +++ /dev/null @@ -1,53 +0,0 @@ -import CioInternalCommon -import Foundation -#if canImport(UIKit) -import UIKit -#endif - -extension MessagingPushFCM { - @available(iOSApplicationExtension, unavailable) - func setupAutoFetchDeviceToken() { - swizzleDidRegisterForRemoteNotifications() - UIApplication.shared.registerForRemoteNotifications() - } - - @available(iOSApplicationExtension, unavailable) - private func swizzleDidRegisterForRemoteNotifications() { - let appDelegate = UIApplication.shared.delegate - let appDelegateClass: AnyClass? = object_getClass(appDelegate) - let originalSelector = #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)) - let swizzledSelector = #selector(application(_:didRegisterForRemoteNotificationsWithDeviceToken:)) - swizzle(forOriginalClass: appDelegateClass, forSwizzledClass: MessagingPushFCM.self, original: originalSelector, new: swizzledSelector) - } - - private func swizzle(forOriginalClass: AnyClass?, forSwizzledClass: AnyClass?, original: Selector, new: Selector) { - guard let swizzledMethod = class_getInstanceMethod(forSwizzledClass, new) else { return } - guard let originalMethod = class_getInstanceMethod(forOriginalClass, original) else { - // Add method if it doesn't exist - class_addMethod(forOriginalClass, new, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) - return - } - method_exchangeImplementations(originalMethod, swizzledMethod) - } - - // Swizzled method for APN device token. - // Fetch the FCM token using the Firebase delegate method when the APN token is set. - @objc - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - // Use Self.shared because after swizzling, `self` is the AppDelegate, not MessagingPushFCM - guard let firebaseService = Self.shared.firebaseMessaging() else { - DIGraphShared.shared.logger.error("CIO: firebaseService is nil. Make sure to initialize the MessagingPushFCM SDK before use.") - return - } - - firebaseService.apnsToken = deviceToken - // Registers listener with FCM SDK to always have the latest FCM token. - // Used to automatically register it with the SDK. - firebaseService.fetchToken(completion: { token, _ in - guard let token = token else { - return - } - Self.shared.registerDeviceToken(fcmToken: token) - }) - } -} diff --git a/Sources/MessagingPushFCM/MessagingPushFCM.swift b/Sources/MessagingPushFCM/MessagingPushFCM.swift index 4cd575089..16be5b3dc 100644 --- a/Sources/MessagingPushFCM/MessagingPushFCM.swift +++ b/Sources/MessagingPushFCM/MessagingPushFCM.swift @@ -1,8 +1,9 @@ import CioInternalCommon @_spi(Internal) import CioMessagingPush import Foundation + #if canImport(UserNotifications) -import UserNotifications + import UserNotifications #endif // Some functions are copied from MessagingPush because @@ -11,18 +12,6 @@ import UserNotifications public protocol MessagingPushFCMInstance: AutoMockable { func registerDeviceToken(fcmToken: String?) - // sourcery:Name=didReceiveRegistrationToken - func messaging( - _ messaging: Any, - didReceiveRegistrationToken fcmToken: String? - ) - - // sourcery:Name=didFailToRegisterForRemoteNotifications - func application( - _ application: Any, - didFailToRegisterForRemoteNotificationsWithError error: Error - ) - func deleteDeviceToken() func trackMetric( @@ -32,27 +21,28 @@ public protocol MessagingPushFCMInstance: AutoMockable { ) #if canImport(UserNotifications) - @discardableResult - // sourcery:Name=didReceiveNotificationRequest - // sourcery:IfCanImport=UserNotifications - func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) -> Bool - - // sourcery:IfCanImport=UserNotifications - func serviceExtensionTimeWillExpire() + @discardableResult + // sourcery:Name=didReceiveNotificationRequest + // sourcery:IfCanImport=UserNotifications + func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) -> Bool + + // sourcery:IfCanImport=UserNotifications + func serviceExtensionTimeWillExpire() #endif } public class MessagingPushFCM: MessagingPushFCMInstance { - static let shared = MessagingPushFCM() + public static let shared = MessagingPushFCM() var messagingPush: MessagingPushInstance { MessagingPush.shared } var firebaseService: FirebaseService? + private var wrappedFirebaseDelegate: FirebaseServiceDelegate? func firebaseMessaging() -> FirebaseService? { firebaseService @@ -65,17 +55,6 @@ public class MessagingPushFCM: MessagingPushFCMInstance { messagingPush.registerDeviceToken(deviceToken) } - public func messaging(_ messaging: Any, didReceiveRegistrationToken fcmToken: String?) { - guard let deviceToken = fcmToken else { - return - } - registerDeviceToken(fcmToken: deviceToken) - } - - public func application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error) { - messagingPush.deleteDeviceToken() - } - public func deleteDeviceToken() { messagingPush.deleteDeviceToken() } @@ -101,8 +80,15 @@ public class MessagingPushFCM: MessagingPushFCMInstance { shared.firebaseService = firebaseService let pushConfigOptions = MessagingPush.moduleConfig - if pushConfigOptions.autoFetchDeviceToken, !MessagingPush.appDelegateIntegratedExplicitly { - shared.setupAutoFetchDeviceToken() + if pushConfigOptions.autoFetchDeviceToken { + if var service = shared.firebaseMessaging() { + shared.wrappedFirebaseDelegate = service.delegate + service.delegate = shared + } else { + DIGraphShared.shared.logger.error( + "CIO: firebaseService is nil. Make sure to initialize the MessagingPushFCM SDK before use." + ) + } } return implementation @@ -114,50 +100,65 @@ public class MessagingPushFCM: MessagingPushFCMInstance { @available(iOSApplicationExtension, introduced: 13.0) @available(visionOSApplicationExtension, introduced: 1.0) @discardableResult - public static func initializeForExtension(withConfig config: MessagingPushConfigOptions) -> MessagingPushInstance { + public static func initializeForExtension(withConfig config: MessagingPushConfigOptions) + -> MessagingPushInstance + { let implementation = MessagingPush.initializeForExtension(withConfig: config) return implementation } #if canImport(UserNotifications) - /** - - returns: - Bool indicating if this push notification is one handled by Customer.io SDK or not. - If function returns `false`, `contentHandler` will *not* be called by the SDK. - */ - @discardableResult - public func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) -> Bool { - messagingPush.didReceive(request, withContentHandler: contentHandler) - } + /** + - returns: + Bool indicating if this push notification is one handled by Customer.io SDK or not. + If function returns `false`, `contentHandler` will *not* be called by the SDK. + */ + @discardableResult + public func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) -> Bool { + messagingPush.didReceive(request, withContentHandler: contentHandler) + } - /** - iOS OS telling the notification service to hurry up and stop modifying the push notifications. - Stop all network requests and modifying and show the push for what it looks like now. - */ - public func serviceExtensionTimeWillExpire() { - messagingPush.serviceExtensionTimeWillExpire() - } + /** + iOS OS telling the notification service to hurry up and stop modifying the push notifications. + Stop all network requests and modifying and show the push for what it looks like now. + */ + public func serviceExtensionTimeWillExpire() { + messagingPush.serviceExtensionTimeWillExpire() + } - @available(iOSApplicationExtension, unavailable) - public func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse - ) -> CustomerIOParsedPushPayload? { - // Use concrete MessagingPush instance since method was removed from protocol - MessagingPush.shared.userNotificationCenter(center, didReceive: response) - } + @available(iOSApplicationExtension, unavailable) + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) -> CustomerIOParsedPushPayload? { + // Use concrete MessagingPush instance since method was removed from protocol + MessagingPush.shared.userNotificationCenter(center, didReceive: response) + } - @available(iOSApplicationExtension, unavailable) - public func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) -> Bool { - // Use concrete MessagingPush instance since method was removed from protocol - MessagingPush.shared.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler) - } + @available(iOSApplicationExtension, unavailable) + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) -> Bool { + // Use concrete MessagingPush instance since method was removed from protocol + MessagingPush.shared.userNotificationCenter( + center, didReceive: response, withCompletionHandler: completionHandler) + } #endif } + +// MARK: - FirebaseServiceDelegate + +extension MessagingPushFCM: FirebaseServiceDelegate { + /// Called by Firebase when a new FCM registration token is available. + public func didReceiveRegistrationToken(_ token: String?) { + if let wrappedFirebaseDelegate { + wrappedFirebaseDelegate.didReceiveRegistrationToken(token) + } + registerDeviceToken(fcmToken: token) + } +} diff --git a/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift b/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift index dd5e6f4dc..85503f37e 100644 --- a/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift +++ b/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift @@ -99,16 +99,6 @@ public class MessagingPushFCMInstanceMock: MessagingPushFCMInstance, Mock { registerDeviceTokenReceivedArguments = nil registerDeviceTokenReceivedInvocations = [] - mockCalled = false // do last as resetting properties above can make this true - didReceiveRegistrationTokenCallsCount = 0 - didReceiveRegistrationTokenReceivedArguments = nil - didReceiveRegistrationTokenReceivedInvocations = [] - - mockCalled = false // do last as resetting properties above can make this true - didFailToRegisterForRemoteNotificationsCallsCount = 0 - didFailToRegisterForRemoteNotificationsReceivedArguments = nil - didFailToRegisterForRemoteNotificationsReceivedInvocations = [] - mockCalled = false // do last as resetting properties above can make this true deleteDeviceTokenCallsCount = 0 @@ -159,60 +149,6 @@ public class MessagingPushFCMInstanceMock: MessagingPushFCMInstance, Mock { registerDeviceTokenClosure?(fcmToken) } - // MARK: - messaging - - /// Number of times the function was called. - @Atomic public private(set) var didReceiveRegistrationTokenCallsCount = 0 - /// `true` if the function was ever called. - public var didReceiveRegistrationTokenCalled: Bool { - didReceiveRegistrationTokenCallsCount > 0 - } - - /// The arguments from the *last* time the function was called. - @Atomic public private(set) var didReceiveRegistrationTokenReceivedArguments: (messaging: Any, fcmToken: String?)? - /// Arguments from *all* of the times that the function was called. - @Atomic public private(set) var didReceiveRegistrationTokenReceivedInvocations: [(messaging: Any, fcmToken: String?)] = [] - /** - Set closure to get called when function gets called. Great way to test logic or return a value for the function. - */ - public var didReceiveRegistrationTokenClosure: ((Any, String?) -> Void)? - - /// Mocked function for `messaging(_ messaging: Any, didReceiveRegistrationToken fcmToken: String?)`. Your opportunity to return a mocked value and check result of mock in test code. - public func messaging(_ messaging: Any, didReceiveRegistrationToken fcmToken: String?) { - mockCalled = true - didReceiveRegistrationTokenCallsCount += 1 - didReceiveRegistrationTokenReceivedArguments = (messaging: messaging, fcmToken: fcmToken) - didReceiveRegistrationTokenReceivedInvocations.append((messaging: messaging, fcmToken: fcmToken)) - didReceiveRegistrationTokenClosure?(messaging, fcmToken) - } - - // MARK: - application - - /// Number of times the function was called. - @Atomic public private(set) var didFailToRegisterForRemoteNotificationsCallsCount = 0 - /// `true` if the function was ever called. - public var didFailToRegisterForRemoteNotificationsCalled: Bool { - didFailToRegisterForRemoteNotificationsCallsCount > 0 - } - - /// The arguments from the *last* time the function was called. - @Atomic public private(set) var didFailToRegisterForRemoteNotificationsReceivedArguments: (application: Any, error: Error)? - /// Arguments from *all* of the times that the function was called. - @Atomic public private(set) var didFailToRegisterForRemoteNotificationsReceivedInvocations: [(application: Any, error: Error)] = [] - /** - Set closure to get called when function gets called. Great way to test logic or return a value for the function. - */ - public var didFailToRegisterForRemoteNotificationsClosure: ((Any, Error) -> Void)? - - /// Mocked function for `application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error)`. Your opportunity to return a mocked value and check result of mock in test code. - public func application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error) { - mockCalled = true - didFailToRegisterForRemoteNotificationsCallsCount += 1 - didFailToRegisterForRemoteNotificationsReceivedArguments = (application: application, error: error) - didFailToRegisterForRemoteNotificationsReceivedInvocations.append((application: application, error: error)) - didFailToRegisterForRemoteNotificationsClosure?(application, error) - } - // MARK: - deleteDeviceToken /// Number of times the function was called. diff --git a/Tests/MessagingInApp/Inbox/NotificationInboxTest.swift b/Tests/MessagingInApp/Inbox/NotificationInboxTest.swift index 62cbbdafd..6a93ffb1e 100644 --- a/Tests/MessagingInApp/Inbox/NotificationInboxTest.swift +++ b/Tests/MessagingInApp/Inbox/NotificationInboxTest.swift @@ -1,6 +1,7 @@ +import XCTest + @testable import CioInternalCommon @testable import CioMessagingInApp -import XCTest class NotificationInboxTest: UnitTest { private var notificationInbox: DefaultNotificationInbox! @@ -66,14 +67,16 @@ class NotificationInboxTest: UnitTest { properties: [:] ) - let stateWithMessages = InAppMessageState().copy(inboxMessages: [olderMessage, newerMessage]) + let stateWithMessages = InAppMessageState().copy(inboxMessages: [ + olderMessage, newerMessage, + ]) inAppMessageManagerMock.underlyingState = stateWithMessages let messages = await notificationInbox.getMessages() XCTAssertEqual(messages.count, 2) - XCTAssertEqual(messages[0].queueId, "queue-2") // Newer date - XCTAssertEqual(messages[1].queueId, "queue-1") // Older date + XCTAssertEqual(messages[0].queueId, "queue-2") // Newer date + XCTAssertEqual(messages[1].queueId, "queue-1") // Older date } func test_getMessages_whenTopicProvided_expectFilteredAndSortedByNewestFirst() async { @@ -115,7 +118,9 @@ class NotificationInboxTest: UnitTest { properties: [:] ) - let stateWithMessages = InAppMessageState().copy(inboxMessages: [oldPromoMessage, updateMessage, newPromoMessage]) + let stateWithMessages = InAppMessageState().copy(inboxMessages: [ + oldPromoMessage, updateMessage, newPromoMessage, + ]) inAppMessageManagerMock.underlyingState = stateWithMessages let messages = await notificationInbox.getMessages(topic: "promo") @@ -127,8 +132,8 @@ class NotificationInboxTest: UnitTest { XCTAssertTrue(queueIds.contains("queue-3")) // Verify sorting: newest first - XCTAssertEqual(messages[0].deliveryId, "msg3") // middleDate - XCTAssertEqual(messages[1].deliveryId, "msg1") // olderDate + XCTAssertEqual(messages[0].deliveryId, "msg3") // middleDate + XCTAssertEqual(messages[1].deliveryId, "msg1") // olderDate } func test_getMessages_whenTopicMatchingIsCaseInsensitive_expectCorrectFiltering() async { @@ -204,7 +209,10 @@ class NotificationInboxTest: UnitTest { notificationInbox.markMessageOpened(message: message) XCTAssertEqual(inAppMessageManagerMock.dispatchCallsCount, 1) - guard case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments?.action else { + guard + case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments? + .action + else { XCTFail("Expected inboxAction, got different action") return } @@ -238,7 +246,10 @@ class NotificationInboxTest: UnitTest { notificationInbox.markMessageUnopened(message: message) XCTAssertEqual(inAppMessageManagerMock.dispatchCallsCount, 1) - guard case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments?.action else { + guard + case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments? + .action + else { XCTFail("Expected inboxAction, got different action") return } @@ -272,7 +283,10 @@ class NotificationInboxTest: UnitTest { notificationInbox.markMessageDeleted(message: message) XCTAssertEqual(inAppMessageManagerMock.dispatchCallsCount, 1) - guard case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments?.action else { + guard + case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments? + .action + else { XCTFail("Expected inboxAction, got different action") return } @@ -305,7 +319,10 @@ class NotificationInboxTest: UnitTest { notificationInbox.trackMessageClicked(message: message, actionName: "view_details") XCTAssertEqual(inAppMessageManagerMock.dispatchCallsCount, 1) - guard case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments?.action else { + guard + case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments? + .action + else { XCTFail("Expected inboxAction, got different action") return } @@ -337,7 +354,10 @@ class NotificationInboxTest: UnitTest { notificationInbox.trackMessageClicked(message: message, actionName: nil) XCTAssertEqual(inAppMessageManagerMock.dispatchCallsCount, 1) - guard case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments?.action else { + guard + case .inboxAction(let inboxAction) = inAppMessageManagerMock.dispatchReceivedArguments? + .action + else { XCTFail("Expected inboxAction, got different action") return } @@ -413,7 +433,9 @@ class NotificationInboxTest: UnitTest { properties: [:] ) - let stateWithMessages = InAppMessageState().copy(inboxMessages: [promoMessage, updateMessage]) + let stateWithMessages = InAppMessageState().copy(inboxMessages: [ + promoMessage, updateMessage, + ]) inAppMessageManagerMock.underlyingState = stateWithMessages let listener = await MainActor.run { @@ -500,7 +522,8 @@ class NotificationInboxTest: UnitTest { _ = (listener1, listener2) } - func test_addChangeListener_multipleListenersWithDifferentTopics_expectCorrectFiltering() async { + func test_addChangeListener_multipleListenersWithDifferentTopics_expectCorrectFiltering() async + { let expectation1 = expectation(description: "Promo listener receives promo messages") let expectation2 = expectation(description: "Updates listener receives update messages") @@ -527,7 +550,9 @@ class NotificationInboxTest: UnitTest { properties: [:] ) - let stateWithMessages = InAppMessageState().copy(inboxMessages: [promoMessage, updateMessage]) + let stateWithMessages = InAppMessageState().copy(inboxMessages: [ + promoMessage, updateMessage, + ]) inAppMessageManagerMock.underlyingState = stateWithMessages let (listener1, listener2) = await MainActor.run { @@ -588,7 +613,7 @@ class NotificationInboxTest: UnitTest { } // Wait for initial callback - try? await Task.sleep(nanoseconds: 100000000) // 100ms + try? await Task.sleep(nanoseconds: 100_000_000) // 100ms let initialCallbackCount = callbackCount XCTAssertGreaterThan(initialCallbackCount, 0, "Should have received initial callback") @@ -597,9 +622,10 @@ class NotificationInboxTest: UnitTest { notificationInbox.removeChangeListener(listener) // Wait to ensure no more callbacks - try? await Task.sleep(nanoseconds: 100000000) // 100ms + try? await Task.sleep(nanoseconds: 100_000_000) // 100ms - XCTAssertEqual(callbackCount, initialCallbackCount, "Should not receive more callbacks after removal") + XCTAssertEqual( + callbackCount, initialCallbackCount, "Should not receive more callbacks after removal") } func test_removeChangeListener_withMultipleListeners_expectOnlyTargetListenerRemoved() async { @@ -649,7 +675,7 @@ class NotificationInboxTest: UnitTest { notificationInbox.removeChangeListener(listener1) // Wait to ensure listener1 doesn't receive more callbacks - try? await Task.sleep(nanoseconds: 100000000) // 100ms + try? await Task.sleep(nanoseconds: 100_000_000) // 100ms // Both should have been called once (initial callback) XCTAssertEqual(listener1CallCount, 1, "Listener 1 should only receive initial callback") @@ -705,7 +731,7 @@ class NotificationInboxTest: UnitTest { } // Wait for initial callbacks - try? await Task.sleep(nanoseconds: 100000000) // 100ms + try? await Task.sleep(nanoseconds: 100_000_000) // 100ms XCTAssertGreaterThan(callbackCount, 0, "Should have received initial callbacks") @@ -715,9 +741,10 @@ class NotificationInboxTest: UnitTest { notificationInbox.removeChangeListener(listener) // Wait to ensure no more callbacks - try? await Task.sleep(nanoseconds: 100000000) // 100ms + try? await Task.sleep(nanoseconds: 100_000_000) // 100ms - XCTAssertEqual(callbackCount, initialCallbackCount, "Should not receive more callbacks after removal") + XCTAssertEqual( + callbackCount, initialCallbackCount, "Should not receive more callbacks after removal") } func test_addChangeListener_receivesOngoingCallbacksWhenStateChanges() async { @@ -757,12 +784,14 @@ class NotificationInboxTest: UnitTest { // Simulate state change: Delete a message let stateWithOneMessage = InAppMessageState().copy(inboxMessages: [message1]) - inAppMessageManagerMock.subscribeReceivedArguments?.subscriber.newState(state: stateWithOneMessage) + inAppMessageManagerMock.subscribeReceivedArguments?.subscriber.newState( + state: stateWithOneMessage) // Wait for update callback await fulfillment(of: [updateExpectation], timeout: 1.0) XCTAssertEqual(callbackCount, 2, "Should receive update callback") - XCTAssertEqual(receivedMessageCounts[1], 1, "Update callback should have 1 message after delete") + XCTAssertEqual( + receivedMessageCounts[1], 1, "Update callback should have 1 message after delete") // Keep listener alive _ = listener @@ -789,7 +818,7 @@ class NotificationInboxTest: UnitTest { } // Wait for initial emission - try? await Task.sleep(nanoseconds: 50000000) // 50ms + try? await Task.sleep(nanoseconds: 50_000_000) // 50ms // Then: should receive initial messages immediately XCTAssertEqual(receivedMessages.count, 1) @@ -816,7 +845,7 @@ class NotificationInboxTest: UnitTest { } } - try? await Task.sleep(nanoseconds: 50000000) + try? await Task.sleep(nanoseconds: 50_000_000) // Then: should receive only filtered messages XCTAssertEqual(receivedMessages.count, 1) @@ -847,18 +876,19 @@ class NotificationInboxTest: UnitTest { } // Wait for initial emission - try? await Task.sleep(nanoseconds: 50000000) + try? await Task.sleep(nanoseconds: 50_000_000) // Then: add message and verify stream emits update let message = createTestMessage(queueId: "msg1") let stateWithMessage = InAppMessageState().copy(inboxMessages: [message]) - inAppMessageManagerMock.subscribeReceivedArguments?.subscriber.newState(state: stateWithMessage) + inAppMessageManagerMock.subscribeReceivedArguments?.subscriber.newState( + state: stateWithMessage) await fulfillment(of: [expectation], timeout: 1.0) XCTAssertEqual(receivedMessages.count, 2) - XCTAssertEqual(receivedMessages[0].count, 0) // Initial empty - XCTAssertEqual(receivedMessages[1].count, 1) // After update + XCTAssertEqual(receivedMessages[0].count, 0) // Initial empty + XCTAssertEqual(receivedMessages[1].count, 1) // After update task.cancel() } @@ -875,19 +905,20 @@ class NotificationInboxTest: UnitTest { } } - try? await Task.sleep(nanoseconds: 50000000) + try? await Task.sleep(nanoseconds: 50_000_000) // When: canceling the task task.cancel() - try? await Task.sleep(nanoseconds: 50000000) + try? await Task.sleep(nanoseconds: 50_000_000) let countAfterCancel = receivedCount // Then: no more updates after cancellation let message = createTestMessage(queueId: "msg1") let stateWithMessage = InAppMessageState().copy(inboxMessages: [message]) - inAppMessageManagerMock.subscribeReceivedArguments?.subscriber.newState(state: stateWithMessage) - try? await Task.sleep(nanoseconds: 50000000) + inAppMessageManagerMock.subscribeReceivedArguments?.subscriber.newState( + state: stateWithMessage) + try? await Task.sleep(nanoseconds: 50_000_000) XCTAssertEqual(receivedCount, countAfterCancel) } diff --git a/Tests/MessagingPush/Integration/CioProviderAgnosticAppDelegateTests.swift b/Tests/MessagingPush/Integration/CioProviderAgnosticAppDelegateTests.swift deleted file mode 100644 index 583d2b12c..000000000 --- a/Tests/MessagingPush/Integration/CioProviderAgnosticAppDelegateTests.swift +++ /dev/null @@ -1,357 +0,0 @@ -@testable import CioInternalCommon -@_spi(Internal) @testable import CioMessagingPush -import SharedTests -import UIKit -import UserNotifications -import XCTest - -class CioProviderAgnosticAppDelegateTests: XCTestCase { - // Mock Classes - var mockMessagingPush: MessagingPushInstanceMock! - var mockAppDelegate: MockAppDelegate! - var mockNotificationCenter: UserNotificationCenterIntegrationMock! - var mockNotificationCenterDelegate: MockNotificationCenterDelegate! - var mockLogger: LoggerMock! - var appDelegate: CioProviderAgnosticAppDelegate! - - func createMockConfig( - autoFetchDeviceToken: Bool = true, - autoTrackPushEvents: Bool = true, - showPushAppInForeground: Bool = true - ) -> MessagingPushConfigOptions { - MessagingPushConfigOptions( - logLevel: .info, - cdpApiKey: "test-api-key", - region: .US, - autoFetchDeviceToken: autoFetchDeviceToken, - autoTrackPushEvents: autoTrackPushEvents, - showPushAppInForeground: showPushAppInForeground - ) - } - - override func setUp() { - super.setUp() - - UNUserNotificationCenter.swizzleNotificationCenter() - - mockMessagingPush = MessagingPushInstanceMock() - mockAppDelegate = MockAppDelegate() - mockNotificationCenter = UserNotificationCenterIntegrationMock() - mockNotificationCenterDelegate = MockNotificationCenterDelegate() - mockLogger = LoggerMock() - - // Configure mock notification center with a delegate - mockNotificationCenter.delegate = mockNotificationCenterDelegate - - // Create AppDelegate with mocks - appDelegate = CioProviderAgnosticAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig() }, - logger: mockLogger - ) - } - - override func tearDown() { - mockMessagingPush = nil - mockAppDelegate = nil - mockNotificationCenter = nil - mockNotificationCenterDelegate = nil - mockLogger = nil - appDelegate = nil - - UNUserNotificationCenter.unswizzleNotificationCenter() - - MessagingPush.appDelegateIntegratedExplicitly = false - - super.tearDown() - } - - // MARK: - Tests for application(_:didFinishLaunchingWithOptions:) - - func testDidFinishLaunchingWithOptions_whenValidConfigIsUsed_thenTokenIsRequestedAndDelegateIsSet() { - // Call the method - let result = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Verify behavior - XCTAssertTrue(MessagingPush.appDelegateIntegratedExplicitly) - XCTAssertTrue(result) - XCTAssertTrue(mockAppDelegate.didFinishLaunchingCalled) - XCTAssertTrue(mockLogger.debugCallsCount == 1) - XCTAssertTrue(mockLogger.debugReceivedInvocations.contains { - $0.message.contains("CIO: Registering for remote notifications") - }) - XCTAssertTrue(mockNotificationCenter.delegate === appDelegate) - } - - func testDidFinishLaunchingWithOptions_whenValidConfigIsUsed_thenTokenIsNotRequested() { - appDelegate = CioProviderAgnosticAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig(autoFetchDeviceToken: false) }, - logger: mockLogger - ) - - // Call the method - let result = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Verify behavior - XCTAssertTrue(result) - XCTAssertTrue(mockAppDelegate.didFinishLaunchingCalled) - XCTAssertFalse(mockLogger.debugReceivedInvocations.contains { - $0.message.contains("CIO: Registering for remote notifications") - }) - XCTAssertTrue(mockNotificationCenter.delegate === appDelegate) - } - - func testDidFinishLaunchingWithOptions_whenAutoTrackPushEventsIsDisabled_thenDelegateIsNotSet() { - appDelegate = CioProviderAgnosticAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig(autoTrackPushEvents: false) }, - logger: mockLogger - ) - - // This should not cause a conflict now since shouldIntegrateWithNotificationCenter is false - let result = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Verify behavior - XCTAssertTrue(result) - XCTAssertTrue(mockAppDelegate.didFinishLaunchingCalled) - XCTAssertTrue(mockLogger.debugReceivedInvocations.contains { - $0.message.contains("CIO: Registering for remote notifications") - }) - // Delegate should not be set on notification center - XCTAssertNotEqual(mockNotificationCenter.delegate as? CioProviderAgnosticAppDelegate, appDelegate) - } - - // MARK: - Tests for remote notification registration - - func testDidRegisterForRemoteNotifications_whenCalled_thenWrappedDelegateIsCalled() { - // Setup - let application = UIApplication.shared - let deviceToken = "device_token".data(using: .utf8)! - - // Call the method - appDelegate.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.didRegisterForRemoteNotificationsCalled) - XCTAssertEqual(mockAppDelegate.deviceTokenReceived, deviceToken) - } - - func testDidFailToRegisterForRemoteNotifications_whenCalled_thenWrappedDelegateAndMessagingPushAreCalled() { - // Setup - let application = UIApplication.shared - let error = NSError(domain: "test", code: 123, userInfo: nil) - - // Call the method - appDelegate.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.didFailToRegisterForRemoteNotificationsCalled) - XCTAssertEqual((mockAppDelegate.errorReceived as NSError?)?.domain, "test") - XCTAssertTrue(mockMessagingPush.deleteDeviceTokenCalled) - } - - // MARK: - Tests for UNUserNotificationCenterDelegate methods - - func testUserNotificationCenterWillPresent_whenCalled_thenWrappedDelegateIsCalled() { - // Setup - var completionHandlerCalled = false - let completionHandler: (UNNotificationPresentationOptions) -> Void = { _ in - completionHandlerCalled = true - } - - _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: UNNotification.testInstance, withCompletionHandler: completionHandler) - - // Verify behavior - XCTAssertTrue(mockNotificationCenterDelegate.willPresentNotificationCalled) - XCTAssertTrue(completionHandlerCalled) - } - - func testUserNotificationCenterWillPresent_whenWrappedDelegateDoesntImplementMethod_thenDefaultHandlingIsUsed() { - // Setup - var completionHandlerCalled = false - var presentationOptions: UNNotificationPresentationOptions? - let completionHandler: (UNNotificationPresentationOptions) -> Void = { options in - completionHandlerCalled = true - presentationOptions = options - } - - // Make sure the delegate doesn't respond to willPresent method - mockNotificationCenterDelegate.respondsToSelectors = [ - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)): false - ] - - _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: UNNotification.testInstance, withCompletionHandler: completionHandler) - - // Verify behavior - XCTAssertFalse(mockNotificationCenterDelegate.willPresentNotificationCalled) - XCTAssertTrue(completionHandlerCalled) - - // Verify default presentation options based on iOS version - if #available(iOS 14.0, *) { - XCTAssertEqual(presentationOptions, [.list, .banner, .badge, .sound]) - } else { - XCTAssertEqual(presentationOptions, [.alert, .badge, .sound]) - } - } - - func testUserNotificationCenterWillPresent_whenWrappedDelegateDoesntImplementMethodAndShowPushAppInForegroundIsFalse_thenNotificationIsNotShown() { - // Setup - var presentationOptions: UNNotificationPresentationOptions? - let completionHandler: (UNNotificationPresentationOptions) -> Void = { options in - presentationOptions = options - } - - // Create app delegate with showPushAppInForeground: false - appDelegate = CioProviderAgnosticAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig(showPushAppInForeground: false) }, - logger: mockLogger - ) - - // Make sure the delegate doesn't respond to willPresent method - mockNotificationCenterDelegate.respondsToSelectors = [ - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)): false - ] - - _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: UNNotification.testInstance, withCompletionHandler: completionHandler) - - // Verify behavior - should not show notification - XCTAssertEqual(presentationOptions, []) - } - - func testUserNotificationCenterDidReceive_whenNotificationCenterIntegrationIsEnabled_thenWrappedDelegateAndMessagingPushAreCalled() { - var completionHandlerCalled = false - let completionHandler = { - completionHandlerCalled = true - } - _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: UNNotificationResponse.testInstance, withCompletionHandler: completionHandler) - - // Verify behavior - XCTAssertTrue(mockNotificationCenterDelegate.didReceiveNotificationResponseCalled) - XCTAssertTrue(completionHandlerCalled) - } - - func testUserNotificationCenterDidReceive_whenWrappedNotificationCenterDelegateIsNil_thenNotificationCompletionHandlerIsCalled() { - // Create custom app delegate - appDelegate = CioProviderAgnosticAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig(autoTrackPushEvents: false) }, - logger: mockLogger - ) - var completionHandlerCalled = false - let completionHandler = { - completionHandlerCalled = true - } - _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: UNNotificationResponse.testInstance, withCompletionHandler: completionHandler) - - // Verify behavior - XCTAssertTrue(completionHandlerCalled) - } - - // MARK: - Tests for method forwarding - - func testResponds_whenSelectorIsProvided_thenItShouldCorrectlyDetectResponse() { - // Test all implemented optional methods - for selector in appDelegate.implementedOptionalMethods { - XCTAssertTrue(appDelegate.responds(to: selector), "Should respond to \(selector)") - } - - // Test implementation for methodimplemented by the wrapped app delegate - let wrappedSelector = #selector(UIApplicationDelegate.applicationDidBecomeActive(_:)) - XCTAssertTrue(appDelegate.responds(to: wrappedSelector), "Should respond to wrapped delegate's selector \(wrappedSelector)") - - // Test a non-implemented method - let nonImplementedSelector = #selector(UIApplicationDelegate.applicationDidEnterBackground(_:)) - XCTAssertFalse(appDelegate.responds(to: nonImplementedSelector), "Should not respond to \(nonImplementedSelector)") - } - - func testForwardingTarget_whenSelectorIsProvided_thenItShouldCorrectlyDetectTarget() { - // Test forwarding for an implemented method - let implementedSelector = #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)) - XCTAssertEqual(appDelegate.forwardingTarget(for: implementedSelector) as? CioProviderAgnosticAppDelegate, appDelegate) - - // Test forwarding for a method implemented by the wrapped app delegate - let wrappedSelector = #selector(UIApplicationDelegate.applicationDidBecomeActive(_:)) - XCTAssertEqual(appDelegate.forwardingTarget(for: wrappedSelector) as? MockAppDelegate, mockAppDelegate) - - // Test forwarding for a method not implemented by any delegate - let nonImplementedSelector = #selector(UIApplicationDelegate.applicationDidEnterBackground(_:)) - XCTAssertNil(appDelegate.forwardingTarget(for: nonImplementedSelector)) - } - - // MARK: - Tests for extension methods - - func testUserNotificationCenterOpenSettingsFor_whenCalled_thenWrappedDelegateIsCalled() { - // Setup - _ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegate.userNotificationCenter(UNUserNotificationCenter.current(), openSettingsFor: UNNotification.testInstance) - - // Verify behavior - XCTAssertTrue(mockNotificationCenterDelegate.openSettingsForNotificationCalled) - } - - func testApplicationContinueUserActivity_whenCalled_thenWrappedDelegateIsCalled() { - // Setup - let userActivity = NSUserActivity(activityType: "test") - let restorationHandler: ([UIUserActivityRestoring]?) -> Void = { _ in } - - // Call the method - let result = appDelegate.application(UIApplication.shared, continue: userActivity, restorationHandler: restorationHandler) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.continueUserActivityCalled) - XCTAssertTrue(result) - } - - func testApplicationOpenUrl_whenCalled_thenWrappedDelegateIsCalled() { - // Setup - let testUrl = URL(string: "myapp://deeplink")! - let testOptions: [UIApplication.OpenURLOptionsKey: Any] = [.sourceApplication: "com.test.app"] - - // Call the method - let result = appDelegate.application(UIApplication.shared, open: testUrl, options: testOptions) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.openUrlCalled) - XCTAssertEqual(mockAppDelegate.urlReceived, testUrl) - XCTAssertEqual(mockAppDelegate.optionsReceived?[.sourceApplication] as? String, "com.test.app") - XCTAssertTrue(result) - } - - func testRespondsToApplicationOpenUrl_whenSelectorIsInImplementedMethods_thenReturnsTrue() { - // Test that the app delegate responds to the URL opening selector - let urlSelector = #selector(UIApplicationDelegate.application(_:open:options:)) - XCTAssertTrue(appDelegate.responds(to: urlSelector), "AppDelegate should respond to application(_:open:options:) selector") - - // More importantly, test that it's handled by CioAppDelegate itself, not just forwarded - XCTAssertEqual(appDelegate.forwardingTarget(for: urlSelector) as? CioProviderAgnosticAppDelegate, appDelegate, "URL handling should be handled by CioAppDelegate, not forwarded to wrapped delegate") - } -} diff --git a/Tests/MessagingPush/Integration/PushNotificationCenterRegistrarTests.swift b/Tests/MessagingPush/Integration/PushNotificationCenterRegistrarTests.swift new file mode 100644 index 000000000..3339b6fd0 --- /dev/null +++ b/Tests/MessagingPush/Integration/PushNotificationCenterRegistrarTests.swift @@ -0,0 +1,136 @@ +import Foundation +import SharedTests +import UserNotifications +import XCTest + +@testable import CioInternalCommon +@testable import CioMessagingPush + +class PushNotificationCenterRegistrarTests: UnitTest { + private var registrar: PushNotificationCenterRegistrarImpl! + private var pushEventHandlerMock: PushEventHandlerMock! + private var pushEventHandlerProxyMock: PushEventHandlerProxyMock! + private var userNotificationCenterMock: UserNotificationCenterMock! + + override func setUp() { + super.setUp() + + pushEventHandlerMock = PushEventHandlerMock() + pushEventHandlerProxyMock = PushEventHandlerProxyMock() + userNotificationCenterMock = UserNotificationCenterMock() + + registrar = PushNotificationCenterRegistrarImpl( + pushEventHandler: pushEventHandlerMock, + pushEventHandlerProxy: pushEventHandlerProxyMock, + userNotificationCenter: userNotificationCenterMock + ) + } + + // MARK: activate + + func test_activate_expectSetsSelfAsNotificationCenterDelegate() { + registrar.activate() + + XCTAssertTrue(userNotificationCenterMock.currentDelegate === registrar) + } + + func test_activate_givenExistingDelegate_expectExistingDelegateAddedToProxy() { + let existingDelegate = MockNotificationCenterDelegate() + userNotificationCenterMock.currentDelegate = existingDelegate + + registrar.activate() + + XCTAssertEqual(pushEventHandlerProxyMock.addPushEventHandlerCallsCount, 1) + } + + func test_activate_givenNoExistingDelegate_expectNoHandlerAddedToProxy() { + userNotificationCenterMock.currentDelegate = nil + + registrar.activate() + + XCTAssertFalse(pushEventHandlerProxyMock.addPushEventHandlerCalled) + } + + // MARK: willPresent + + func test_willPresent_expectCallsPushEventHandler() { + let expectation = expectation(description: "shouldDisplayPushAppInForeground called") + pushEventHandlerMock.shouldDisplayPushAppInForegroundClosure = { _, completionHandler in + expectation.fulfill() + completionHandler(true) + } + + registrar.userNotificationCenter( + UNUserNotificationCenter.mockCenter(), + willPresent: UNNotification.testInstance, + withCompletionHandler: { _ in } + ) + + waitForExpectations(timeout: 2) + } + + func test_willPresent_givenShouldShowTrue_expectBannerOptions() { + pushEventHandlerMock.shouldDisplayPushAppInForegroundClosure = { _, completionHandler in + completionHandler(true) + } + var receivedOptions: UNNotificationPresentationOptions? + + registrar.userNotificationCenter( + UNUserNotificationCenter.mockCenter(), + willPresent: UNNotification.testInstance, + withCompletionHandler: { options in receivedOptions = options } + ) + + XCTAssertNotNil(receivedOptions) + XCTAssertFalse(receivedOptions!.isEmpty) + } + + func test_willPresent_givenShouldShowFalse_expectEmptyOptions() { + pushEventHandlerMock.shouldDisplayPushAppInForegroundClosure = { _, completionHandler in + completionHandler(false) + } + var receivedOptions: UNNotificationPresentationOptions? + + registrar.userNotificationCenter( + UNUserNotificationCenter.mockCenter(), + willPresent: UNNotification.testInstance, + withCompletionHandler: { options in receivedOptions = options } + ) + + XCTAssertEqual(receivedOptions, []) + } + + // MARK: didReceive + + func test_didReceive_expectCallsOnPushAction() { + let expectation = expectation(description: "onPushAction called") + pushEventHandlerMock.onPushActionClosure = { _, completionHandler in + expectation.fulfill() + completionHandler() + } + + registrar.userNotificationCenter( + UNUserNotificationCenter.mockCenter(), + didReceive: UNNotificationResponse.mockResponse(), + withCompletionHandler: {} + ) + + waitForExpectations(timeout: 2) + } +} + +// MARK: - Test helpers + +extension UNNotificationResponse { + fileprivate static func mockResponse() -> UNNotificationResponse { + unsafeBitCast(NSObject(), to: UNNotificationResponse.self) + } +} + +extension UNUserNotificationCenter { + /// Creates a UNUserNotificationCenter instance without calling `.current()`, + /// which crashes in unit tests due to missing bundle context. + fileprivate static func mockCenter() -> UNUserNotificationCenter { + unsafeBitCast(NSObject(), to: UNUserNotificationCenter.self) + } +} diff --git a/Tests/MessagingPush/MessagingPushTest.swift b/Tests/MessagingPush/MessagingPushTest.swift index 04165faef..0cd12fcc8 100644 --- a/Tests/MessagingPush/MessagingPushTest.swift +++ b/Tests/MessagingPush/MessagingPushTest.swift @@ -11,33 +11,36 @@ class MessagingPushTest: IntegrationTest { nil } - private let automaticPushClickHandlingMock = AutomaticPushClickHandlingMock() + private let pushNotificationCenterRegistrarMock = PushNotificationCenterRegistrarMock() override func setUp() { super.setUp() - mockCollection.add(mock: automaticPushClickHandlingMock) + mockCollection.add(mock: pushNotificationCenterRegistrarMock) DIGraphShared.shared.override( - value: automaticPushClickHandlingMock, forType: AutomaticPushClickHandling.self + value: pushNotificationCenterRegistrarMock, + forType: PushNotificationCenterRegistrar.self ) } // MARK: initialize - func test_initialize_givenDefaultModuleConfigOptions_expectStartAutoPushClickHandling() { + func + test_initialize_givenDefaultModuleConfigOptions_expectActivatePushNotificationCenterRegistrar() + { MessagingPush.initialize() - XCTAssertEqual(automaticPushClickHandlingMock.startCallsCount, 1) + XCTAssertEqual(pushNotificationCenterRegistrarMock.activateCallsCount, 1) } - func test_initialize_givenCustomerDisabledAutoPushClickHandling_expectDoNotEnableFeature() { + func test_initialize_givenCustomerDisabledAutoTrackPushEvents_expectDoNotActivateRegistrar() { MessagingPush.initialize( withConfig: MessagingPushConfigBuilder() .autoTrackPushEvents(false) .build() ) - XCTAssertFalse(automaticPushClickHandlingMock.startCalled) + XCTAssertFalse(pushNotificationCenterRegistrarMock.activateCalled) } } diff --git a/Tests/MessagingPushAPN/APITest.swift b/Tests/MessagingPushAPN/APITest.swift index cd5a73f8e..47e0a2b76 100644 --- a/Tests/MessagingPushAPN/APITest.swift +++ b/Tests/MessagingPushAPN/APITest.swift @@ -1,16 +1,14 @@ // import CioMessagingPush // do not import. We want to test that customers only need to import 'CioMessagingPushAPN' -import CioMessagingPushAPN // do not use `@testable` so we can test functions are made public and not `internal`. +import CioMessagingPushAPN // do not use `@testable` so we can test functions are made public and not `internal`. import Foundation import SharedTests import XCTest -/** - Contains an example of every public facing SDK function call. This file helps - us prevent introducing breaking changes to the SDK by accident. If a public function - of the SDK is modified, this test class will not successfully compile. By not compiling, - that is a reminder to either fix the compilation and introduce the breaking change or - fix the mistake and not introduce the breaking change in the code base. - */ +/// Contains an example of every public facing SDK function call. This file helps +/// us prevent introducing breaking changes to the SDK by accident. If a public function +/// of the SDK is modified, this test class will not successfully compile. By not compiling, +/// that is a reminder to either fix the compilation and introduce the breaking change or +/// fix the mistake and not introduce the breaking change in the code base. class MessagingPushAPNAPITest: UnitTest { // Test that public functions are accessible by mocked instances let mock = MessagingPushAPNInstanceMock() @@ -41,18 +39,6 @@ class MessagingPushAPNAPITest: UnitTest { MessagingPush.shared.registerDeviceToken(apnDeviceToken: Data()) mock.registerDeviceToken(apnDeviceToken: Data()) - MessagingPush.shared.application("", didRegisterForRemoteNotificationsWithDeviceToken: Data()) - mock.application("", didRegisterForRemoteNotificationsWithDeviceToken: Data()) - - MessagingPush.shared.application( - "", - didFailToRegisterForRemoteNotificationsWithError: GenericError.registrationFailed - ) - mock.application( - "", - didFailToRegisterForRemoteNotificationsWithError: GenericError.registrationFailed - ) - MessagingPush.shared.deleteDeviceToken() mock.deleteDeviceToken() @@ -83,20 +69,24 @@ class MessagingPushAPNAPITest: UnitTest { try skipRunningTest() #if canImport(UserNotifications) - MessagingPush.shared - .didReceive(UNNotificationRequest( - identifier: "", - content: UNNotificationContent(), - trigger: nil - )) { _ in } - mock.didReceive(UNNotificationRequest( - identifier: "", - content: UNNotificationContent(), - trigger: nil - )) { _ in } - - MessagingPush.shared.serviceExtensionTimeWillExpire() - mock.serviceExtensionTimeWillExpire() + MessagingPush.shared + .didReceive( + UNNotificationRequest( + identifier: "", + content: UNNotificationContent(), + trigger: nil + ) + ) { _ in } + mock.didReceive( + UNNotificationRequest( + identifier: "", + content: UNNotificationContent(), + trigger: nil + ) + ) { _ in } + + MessagingPush.shared.serviceExtensionTimeWillExpire() + mock.serviceExtensionTimeWillExpire() #endif } @@ -104,26 +94,27 @@ class MessagingPushAPNAPITest: UnitTest { try skipRunningTest() #if canImport(UserNotifications) - // Cannot guarantee instance or mock will have userNotificationCenter() function as that function is not - // available to app extensions. - _ = MessagingPush.shared.userNotificationCenter( - .current(), - didReceive: UNNotificationResponse.testInstance, - withCompletionHandler: {} - ) - // custom handler - let pushContent: CustomerIOParsedPushPayload? = MessagingPush.shared.userNotificationCenter( - .current(), - didReceive: UNNotificationResponse - .testInstance - ) - - // make sure all properties that a customer might care about are all public - _ = pushContent?.notificationContent - _ = pushContent?.title - _ = pushContent?.body - _ = pushContent?.deepLink - _ = pushContent?.image + // Cannot guarantee instance or mock will have userNotificationCenter() function as that function is not + // available to app extensions. + _ = MessagingPush.shared.userNotificationCenter( + .current(), + didReceive: UNNotificationResponse.testInstance, + withCompletionHandler: {} + ) + // custom handler + let pushContent: CustomerIOParsedPushPayload? = MessagingPush.shared + .userNotificationCenter( + .current(), + didReceive: UNNotificationResponse + .testInstance + ) + + // make sure all properties that a customer might care about are all public + _ = pushContent?.notificationContent + _ = pushContent?.title + _ = pushContent?.body + _ = pushContent?.deepLink + _ = pushContent?.image #endif } } diff --git a/Tests/MessagingPushAPN/Integration/CioAppDelegateAPNTests.swift b/Tests/MessagingPushAPN/Integration/CioAppDelegateAPNTests.swift deleted file mode 100644 index 384a91fee..000000000 --- a/Tests/MessagingPushAPN/Integration/CioAppDelegateAPNTests.swift +++ /dev/null @@ -1,136 +0,0 @@ -@testable import CioInternalCommon -@_spi(Internal) @testable import CioMessagingPush -@testable import CioMessagingPushAPN -import SharedTests -import UIKit -import UserNotifications -import XCTest - -class CioAppDelegateAPNTests: XCTestCase { - var appDelegateAPN: CioAppDelegate! - - // Mock Classes - var mockMessagingPush: MessagingPushAPNMock! - var mockAppDelegate: MockAppDelegate! - var mockNotificationCenter: UserNotificationCenterIntegrationMock! - var mockNotificationCenterDelegate: MockNotificationCenterDelegate! - var mockLogger: LoggerMock! - - func createMockConfig(autoFetchDeviceToken: Bool = true, autoTrackPushEvents: Bool = true) -> MessagingPushConfigOptions { - MessagingPushConfigOptions( - logLevel: .info, - cdpApiKey: "test-api-key", - region: .US, - autoFetchDeviceToken: autoFetchDeviceToken, - autoTrackPushEvents: autoTrackPushEvents, - showPushAppInForeground: false - ) - } - - override func setUp() { - super.setUp() - - UNUserNotificationCenter.swizzleNotificationCenter() - - mockMessagingPush = MessagingPushAPNMock() - mockAppDelegate = MockAppDelegate() - mockNotificationCenter = UserNotificationCenterIntegrationMock() - mockNotificationCenterDelegate = MockNotificationCenterDelegate() - mockLogger = LoggerMock() - - // Configure mock notification center with a delegate - mockNotificationCenter.delegate = mockNotificationCenterDelegate - - // Create CioAppDelegate with mocks - appDelegateAPN = CioAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig() }, - logger: mockLogger - ) - } - - override func tearDown() { - mockMessagingPush = nil - mockAppDelegate = nil - mockNotificationCenter = nil - mockNotificationCenterDelegate = nil - mockLogger = nil - appDelegateAPN = nil - - UNUserNotificationCenter.unswizzleNotificationCenter() - - MessagingPush.appDelegateIntegratedExplicitly = false - - super.tearDown() - } - - // MARK: - Tests for APN-specific functionality - - func testDidRegisterForRemoteNotifications_whenCalled_thenSuperIsCalledANdDeviceTokenIsRegistered() { - // Setup - let deviceToken = "device_token".data(using: .utf8)! - _ = appDelegateAPN.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegateAPN.application(UIApplication.shared, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.didRegisterForRemoteNotificationsCalled) - XCTAssertEqual(mockAppDelegate.deviceTokenReceived, deviceToken) - - // Verify APN-specific behavior: registerDeviceToken is called with the device token - XCTAssertTrue(mockMessagingPush.registerDeviceTokenAPNCalled) - XCTAssertEqual(mockMessagingPush.registerDeviceTokenAPNReceivedArguments, deviceToken) - } - - // MARK: - Tests for inherited AppDelegate functionality - - func testDidFinishLaunchingWithOption_whenCalled_thenSuperIsCalled() { - // Call the method - let result = appDelegateAPN.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Verify behavior - XCTAssertTrue(result) - XCTAssertTrue(mockAppDelegate.didFinishLaunchingCalled) - XCTAssertTrue(mockLogger.debugCallsCount == 1) - XCTAssertTrue(mockLogger.debugReceivedInvocations.contains { - $0.message.contains("CIO: Registering for remote notifications") - }) - XCTAssertTrue(mockNotificationCenter.delegate === appDelegateAPN) - } - - func testDidFailToRegisterForRemoteNotifications_whenCalled_thenSuperIsCalled() { - // Setup - let application = UIApplication.shared - let error = NSError(domain: "test", code: 123, userInfo: nil) - - // Call the method - appDelegateAPN.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.didFailToRegisterForRemoteNotificationsCalled) - XCTAssertEqual((mockAppDelegate.errorReceived as NSError?)?.domain, "test") - XCTAssertTrue(mockMessagingPush?.deleteDeviceTokenCalled == true) - } - - // MARK: - Tests for UNUserNotificationCenterDelegate methods - - func testUserNotificationCenterDidReceive_whenCalled_thenSuperIsCalled() { - // Setup - _ = appDelegateAPN.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - var completionHandlerCalled = false - let completionHandler = { - completionHandlerCalled = true - } - - // Call the method - appDelegateAPN.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: UNNotificationResponse.testInstance, withCompletionHandler: completionHandler) - - // Verify behavior - XCTAssertTrue(mockNotificationCenterDelegate.didReceiveNotificationResponseCalled) - XCTAssertTrue(completionHandlerCalled) - } -} diff --git a/Tests/MessagingPushFCM/APITest.swift b/Tests/MessagingPushFCM/APITest.swift index 36bfb41e9..55763ff07 100644 --- a/Tests/MessagingPushFCM/APITest.swift +++ b/Tests/MessagingPushFCM/APITest.swift @@ -42,18 +42,6 @@ class MessagingPushFCMAPITest: UnitTest { MessagingPush.shared.registerDeviceToken(fcmToken: "") mock.registerDeviceToken(fcmToken: "") - MessagingPush.shared.messaging("", didReceiveRegistrationToken: "token") - mock.messaging("", didReceiveRegistrationToken: "token") - - MessagingPush.shared.messaging("", didReceiveRegistrationToken: nil) - mock.messaging("", didReceiveRegistrationToken: nil) - - MessagingPush.shared.application( - "", - didFailToRegisterForRemoteNotificationsWithError: GenericError.registrationFailed - ) - mock.application("", didFailToRegisterForRemoteNotificationsWithError: GenericError.registrationFailed) - MessagingPush.shared.deleteDeviceToken() mock.deleteDeviceToken() diff --git a/Tests/MessagingPushFCM/Integration/CioAppDelegateFCMTests.swift b/Tests/MessagingPushFCM/Integration/CioAppDelegateFCMTests.swift deleted file mode 100644 index 75effb383..000000000 --- a/Tests/MessagingPushFCM/Integration/CioAppDelegateFCMTests.swift +++ /dev/null @@ -1,199 +0,0 @@ -@testable import CioInternalCommon -@_spi(Internal) @testable import CioMessagingPush -@testable import CioMessagingPushFCM -import SharedTests -import UIKit -import UserNotifications -import XCTest - -class CioAppDelegateFCMTests: XCTestCase { - var appDelegateFCM: CioAppDelegate! - - // Mock Classes - var mockMessagingPush: MessagingPushFCMMock! - var mockAppDelegate: MockAppDelegate! - var mockNotificationCenter: UserNotificationCenterIntegrationMock! - var mockNotificationCenterDelegate: MockNotificationCenterDelegate! - var mockFirebaseService: MockFirebaseService! - var mockFirebaseServiceDelegate: MockFirebaseServiceDelegate! - var mockLogger: LoggerMock! - - // Mock config for testing - func createMockConfig(autoFetchDeviceToken: Bool = true, autoTrackPushEvents: Bool = true) -> MessagingPushConfigOptions { - MessagingPushConfigOptions( - logLevel: .info, - cdpApiKey: "test-api-key", - region: .US, - autoFetchDeviceToken: autoFetchDeviceToken, - autoTrackPushEvents: autoTrackPushEvents, - showPushAppInForeground: false - ) - } - - override func setUp() { - super.setUp() - - UNUserNotificationCenter.swizzleNotificationCenter() - - mockMessagingPush = MessagingPushFCMMock() - - mockAppDelegate = MockAppDelegate() - - mockNotificationCenter = UserNotificationCenterIntegrationMock() - mockNotificationCenterDelegate = MockNotificationCenterDelegate() - mockNotificationCenter.delegate = mockNotificationCenterDelegate - - mockFirebaseService = MockFirebaseService() - mockFirebaseServiceDelegate = MockFirebaseServiceDelegate() - mockFirebaseService.delegate = mockFirebaseServiceDelegate - - mockLogger = LoggerMock() - - // Set up the FirebaseService on MessagingPushFCM.shared - MessagingPushFCM.shared.firebaseService = mockFirebaseService - - appDelegateFCM = CioAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig() }, - logger: mockLogger - ) - } - - override func tearDown() { - mockMessagingPush = nil - mockAppDelegate = nil - mockNotificationCenter = nil - mockNotificationCenterDelegate = nil - mockFirebaseService = nil - mockFirebaseServiceDelegate = nil - mockLogger = nil - appDelegateFCM = nil - - // Clean up MessagingPushFCM.shared.firebaseService - MessagingPushFCM.shared.firebaseService = nil - - UNUserNotificationCenter.unswizzleNotificationCenter() - - MessagingPush.appDelegateIntegratedExplicitly = false - - super.tearDown() - } - - func testDidFinishLaunching_whenCalled_thenSuperIsCalled() { - // Call the method - let result = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Verify behavior - XCTAssertTrue(result) - XCTAssertTrue(mockAppDelegate.didFinishLaunchingCalled) - // -- `registerForRemoteNotifications` is called - XCTAssertTrue(mockLogger.debugReceivedInvocations.contains { - $0.message.contains("CIO: Registering for remote notifications") - }) - } - - func testDidFinishLaunching_whenCalled_thenFirebaseServiceDelegateIsSet() { - // Call the method - _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Verify behavior - the CioAppDelegate should be set as the delegate on the FirebaseService - XCTAssertTrue(mockFirebaseService.delegate === appDelegateFCM) - } - - func testDidFinishLaunchings_whenAutoFetchDeviceTokenIsDisabled_thenFirebaseServiceDelegateIsNotSet() { - appDelegateFCM = CioAppDelegate( - messagingPush: mockMessagingPush, - userNotificationCenter: { self.mockNotificationCenter }, - appDelegate: mockAppDelegate, - config: { self.createMockConfig(autoFetchDeviceToken: false) }, - logger: mockLogger - ) - mockFirebaseService.delegate = nil - - // Call didFinishLaunching - let result = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Verify behavior - XCTAssertTrue(result) - XCTAssertTrue(mockAppDelegate.didFinishLaunchingCalled) - XCTAssertNil(mockFirebaseService.delegate) - } - - // MARK: - Test FirebaseServiceDelegate - - func testDidReceiveRegistrationToken_whenCalled_thenWrappedFirebaseServiceDelegateIsCalled() { - // Setup - let fcmToken = "test-fcm-token" - _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call method directly - appDelegateFCM.didReceiveRegistrationToken(fcmToken) - - // Verify behavior - the wrapped delegate should be called - XCTAssertTrue(mockFirebaseServiceDelegate.didReceiveRegistrationTokenCalled) - XCTAssertEqual(mockFirebaseServiceDelegate.receivedToken, fcmToken) - } - - func testDidReceiveRegistrationToken_whenCalled_thenTokenIsForwardedToCIO() { - // Setup - let fcmToken = "test-fcm-token" - _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call method directly - appDelegateFCM.didReceiveRegistrationToken(fcmToken) - - // Verify behavior - XCTAssertTrue(mockMessagingPush.registerDeviceTokenFCMCalled) - XCTAssertEqual(mockMessagingPush.registerDeviceTokenFCMReceivedArguments, fcmToken) - } - - // MARK: - Tests for inherited AppDelegate functionality - - func testDidFailToRegisterForRemoteNotifications_whenCalled_thenSuperIsCalled() { - // Setup - let application = UIApplication.shared - let error = NSError(domain: "test", code: 123, userInfo: nil) - - // Call the method - appDelegateFCM.application(application, didFailToRegisterForRemoteNotificationsWithError: error) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.didFailToRegisterForRemoteNotificationsCalled) - XCTAssertEqual((mockAppDelegate.errorReceived as NSError?)?.domain, "test") - XCTAssertTrue(mockMessagingPush.deleteDeviceTokenCalled == true) - } - - // MARK: - Tests for UNUserNotificationCenterDelegate methods - - func testDidRegisterForRemoteNotifications_whenCalled_thenSuperIsCalled() { - // Setup - let deviceToken = "device_token".data(using: .utf8)! - _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - // Call the method - appDelegateFCM.application(UIApplication.shared, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) - - // Verify behavior - XCTAssertTrue(mockAppDelegate.didRegisterForRemoteNotificationsCalled) - XCTAssertEqual(mockAppDelegate.deviceTokenReceived, deviceToken) - } - - func testUserNotificationCenterDidReceive_whenCalled_thenSuperIsCalled() { - // Setup - _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) - - var completionHandlerCalled = false - let completionHandler = { - completionHandlerCalled = true - } - - // Call the method - appDelegateFCM.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: UNNotificationResponse.testInstance, withCompletionHandler: completionHandler) - - // Verify behavior - XCTAssertTrue(mockNotificationCenterDelegate.didReceiveNotificationResponseCalled) - XCTAssertTrue(completionHandlerCalled) - } -} diff --git a/Tests/Shared/Mocks/UIKitDelegateMocks.swift b/Tests/Shared/Mocks/UIKitDelegateMocks.swift index a5d121a94..557e8c66a 100644 --- a/Tests/Shared/Mocks/UIKitDelegateMocks.swift +++ b/Tests/Shared/Mocks/UIKitDelegateMocks.swift @@ -1,62 +1,20 @@ import ObjectiveC import UIKit -// Custom UIApplicationDelegate mock -public class MockAppDelegate: NSObject, UIApplicationDelegate { - public var didFinishLaunchingCalled = false - public var didRegisterForRemoteNotificationsCalled = false - public var didFailToRegisterForRemoteNotificationsCalled = false - public var didBecomeActiveCalled = false - public var continueUserActivityCalled = false - public var launchOptionsReceived: [UIApplication.LaunchOptionsKey: Any]? - public var deviceTokenReceived: Data? - public var errorReceived: Error? - public var openUrlCalled = false - public var urlReceived: URL? - public var optionsReceived: [UIApplication.OpenURLOptionsKey: Any]? - - public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - didFinishLaunchingCalled = true - launchOptionsReceived = launchOptions - return true - } - - public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - didRegisterForRemoteNotificationsCalled = true - deviceTokenReceived = deviceToken - } - - public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - didFailToRegisterForRemoteNotificationsCalled = true - errorReceived = error - } - - public func applicationDidBecomeActive(_ application: UIApplication) { - didBecomeActiveCalled = true - } - - public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - continueUserActivityCalled = true - return true - } - - public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - openUrlCalled = true - urlReceived = url - optionsReceived = options - return true - } -} - // Custom UNUserNotificationCenterDelegate mock public class MockNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { public var didReceiveNotificationResponseCalled = false public var willPresentNotificationCalled = false public var openSettingsForNotificationCalled = false public var respondsToSelectors: [Selector: Bool] = [ - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:withCompletionHandler:)): true, - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:)): true, - #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:openSettingsFor:)): true + #selector( + UNUserNotificationCenterDelegate.userNotificationCenter( + _:willPresent:withCompletionHandler:)): true, + #selector( + UNUserNotificationCenterDelegate.userNotificationCenter( + _:didReceive:withCompletionHandler:)): true, + #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:openSettingsFor:)): + true, ] override public func responds(to aSelector: Selector!) -> Bool { @@ -66,35 +24,26 @@ public class MockNotificationCenterDelegate: NSObject, UNUserNotificationCenterD return super.responds(to: aSelector) } - public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + public func userNotificationCenter( + _ center: UNUserNotificationCenter, willPresent notification: UNNotification, + withCompletionHandler completionHandler: + @escaping (UNNotificationPresentationOptions) -> Void + ) { willPresentNotificationCalled = true completionHandler([]) } - public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + public func userNotificationCenter( + _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { didReceiveNotificationResponseCalled = true completionHandler() } - public func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { + public func userNotificationCenter( + _ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification? + ) { openSettingsForNotificationCalled = true } } - -public extension UNUserNotificationCenter { - static func swizzleNotificationCenter() { - let originalMethod = class_getClassMethod(UNUserNotificationCenter.self, #selector(UNUserNotificationCenter.current))! - let swizzledMethod = class_getClassMethod(UNUserNotificationCenter.self, #selector(UNUserNotificationCenter.currentMock))! - method_exchangeImplementations(originalMethod, swizzledMethod) - } - - static func unswizzleNotificationCenter() { - swizzleNotificationCenter() // Calling again will swap back - } - - @objc class func currentMock() -> UNUserNotificationCenter { - let dummyObject = NSObject() - let notificationCenter = unsafeBitCast(dummyObject, to: UNUserNotificationCenter.self) - return notificationCenter - } -} diff --git a/api-docs/CioMessagingPushAPN.api b/api-docs/CioMessagingPushAPN.api index 8dccff489..c485a8d43 100644 --- a/api-docs/CioMessagingPushAPN.api +++ b/api-docs/CioMessagingPushAPN.api @@ -1,7 +1,6 @@ public final class CioMessagingPushAPN.MessagingPushAPN { + public final val shared: MessagingPushAPN; public fun func registerDeviceToken(apnDeviceToken: Data); - public fun func application(_ application: Any, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data); - public fun func application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error); public fun func deleteDeviceToken(); public fun func trackMetric(deliveryID: String, event: Metric, deviceToken: String); public fun func initialize( @@ -24,14 +23,6 @@ public final class CioMessagingPushAPN.MessagingPushAPN { } public interface CioMessagingPushAPN.MessagingPushAPNInstance { public fun func registerDeviceToken(apnDeviceToken: Data); - public fun func application( - _ application: Any, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data -); - public fun func application( - _ application: Any, - didFailToRegisterForRemoteNotificationsWithError error: Error -); public fun func deleteDeviceToken(); public fun func trackMetric( deliveryID: String, diff --git a/api-docs/CioMessagingPushFCM.api b/api-docs/CioMessagingPushFCM.api index 784eb8aa5..0e464db10 100644 --- a/api-docs/CioMessagingPushFCM.api +++ b/api-docs/CioMessagingPushFCM.api @@ -7,9 +7,8 @@ public interface CioMessagingPushFCM.FirebaseServiceDelegate { public fun func didReceiveRegistrationToken(_ token: String?); } public final class CioMessagingPushFCM.MessagingPushFCM { + public final val shared: MessagingPushFCM; public fun func registerDeviceToken(fcmToken: String?); - public fun func messaging(_ messaging: Any, didReceiveRegistrationToken fcmToken: String?); - public fun func application(_ application: Any, didFailToRegisterForRemoteNotificationsWithError error: Error); public fun func deleteDeviceToken(); public fun func trackMetric(deliveryID: String, event: Metric, deviceToken: String); public fun func internalSetup( @@ -33,14 +32,6 @@ public final class CioMessagingPushFCM.MessagingPushFCM { } public interface CioMessagingPushFCM.MessagingPushFCMInstance { public fun func registerDeviceToken(fcmToken: String?); - public fun func messaging( - _ messaging: Any, - didReceiveRegistrationToken fcmToken: String? -); - public fun func application( - _ application: Any, - didFailToRegisterForRemoteNotificationsWithError error: Error -); public fun func deleteDeviceToken(); public fun func trackMetric( deliveryID: String,