Repository: customerio/customerio-ios
Pull request #891: chore: Swizzling removal - iOS SDK (w/o FCM deep-linking fix and sample app)
Author: login: uros-mil
Created: 2025-05-02T08:19:55Z | Merged: 2025-05-02T09:54:52Z

Changes:
- Added AppDelegates that should replace swizzling code, and are easy to integrate -  CioAppDelegateAPN & CioAppDelegateFCM
  - They are based on the same parent class, CioAppDelegate, which implements most of the functionality
- Old methods and classes are marked `deprecated`, and an explanation is added to direct the user on how to transition to AppDelegates
- Unit tests are added for new AppDelegates


Diff:
diff --git a/Makefile b/Makefile
index 174ccca26..f7609839c 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ generate:
 	./binny sourcery --sources Sources/DataPipeline --templates Sources/Templates --output Sources/DataPipeline/autogenerated --args imports=CioInternalCommon
 	./binny sourcery --sources Sources/MessagingPush --templates Sources/Templates --output Sources/MessagingPush/autogenerated --args imports=CioInternalCommon
 	./binny sourcery --sources Sources/MessagingPushAPN --templates Sources/Templates --output Sources/MessagingPushAPN/autogenerated --args imports=CioMessagingPush-CioInternalCommon
-	./binny sourcery --sources Sources/MessagingPushFCM --templates Sources/Templates --output Sources/MessagingPushFCM/autogenerated --args imports=CioMessagingPush-CioInternalCommon
+	./binny sourcery --sources Sources/MessagingPushFCM --templates Sources/Templates --output Sources/MessagingPushFCM/autogenerated --args imports=CioMessagingPush-CioInternalCommon-FirebaseMessaging
 	./binny sourcery --sources Sources/MessagingInApp --templates Sources/Templates --output Sources/MessagingInApp/autogenerated --args imports=CioInternalCommon-UIKit
 	./binny sourcery --sources Sources/Migration --templates Sources/Templates --output Sources/Migration/autogenerated --args imports=CioInternalCommon
 
diff --git a/README.md b/README.md
index 8e4fef55d..f3f1256c0 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ This is the official Customer.io SDK for iOS.
 
 You'll find our [complete SDK documentation at https://customer.io/docs/sdk/ios](https://customer.io/docs/sdk/ios/). 
 
-**The SDK has been tested on iOS devices**. It might work on other Apple devices—macOS, tvOS, and watchOS—but we have not officially tested, nor do we officially support, non-iOS devices.
+**The SDK has been tested on iOS devices**. It might work on other Apple devices—tvOS, and watchOS—but we have not officially tested, nor do we officially support, non-iOS devices.
 
 ### Migrating from an older SDK version? 
 
diff --git a/Sources/MessagingPush/Config/MessagingPushConfigBuilder.swift b/Sources/MessagingPush/Config/MessagingPushConfigBuilder.swift
index 32fe747e2..f7aab5d69 100644
--- a/Sources/MessagingPush/Config/MessagingPushConfigBuilder.swift
+++ b/Sources/MessagingPush/Config/MessagingPushConfigBuilder.swift
@@ -96,6 +96,7 @@ public class MessagingPushConfigBuilder {
 
     /// Display push notifications sent by Customer.io while app is in foreground. This value is `true` by default.
     @discardableResult
+    @available(*, deprecated, message: "This shouldn't be set if you're using CioAppDelegate(Wrapper). If you are not using CioAppDelegate(Wrapper), it's recommended you upgrade.")
     public func showPushAppInForeground(_ value: Bool) -> MessagingPushConfigBuilder {
         showPushAppInForeground = value
         return self
diff --git a/Sources/MessagingPush/Integration/CioProviderAgnosticAppDelegate.swift b/Sources/MessagingPush/Integration/CioProviderAgnosticAppDelegate.swift
new file mode 100644
index 000000000..0ecaa7f89
--- /dev/null
+++ b/Sources/MessagingPush/Integration/CioProviderAgnosticAppDelegate.swift
@@ -0,0 +1,171 @@
+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<Selector> = [
+        // UIApplicationDelegate
+        #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)),
+        #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)),
+        #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)),
+        #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)),
+        // 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.appDelegateIntegratedExplicitely = 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
+
+    // Function called when a push notification is clicked or swiped away.
+    open func userNotificationCenter(
+        _ center: UNUserNotificationCenter,
+        didReceive response: UNNotificationResponse,
+        withCompletionHandler completionHandler: @escaping () -> Void
+    ) {
+        _ = messagingPush.userNotificationCenter(center, didReceive: response)
+
+        if let wrappedNoticeCenterDelegate = wrappedNotificationCenterDelegate,
+           wrappedNoticeCenterDelegate.responds(to: #selector(UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:))) {
+            wrappedNoticeCenterDelegate.userNotificationCenter?(center, didReceive: response, withCompletionHandler: completionHandler)
+        } else {
+            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, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
+        wrappedNotificationCenterDelegate?.userNotificationCenter?(center, willPresent: notification, withCompletionHandler: completionHandler)
+    }
+
+    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
+    }
+}
diff --git a/Sources/MessagingPush/MessagingPush.swift b/Sources/MessagingPush/MessagingPush.swift
index 40f38af87..4032eac40 100644
--- a/Sources/MessagingPush/MessagingPush.swift
+++ b/Sources/MessagingPush/MessagingPush.swift
@@ -6,6 +6,8 @@ import Foundation
  So, performing an HTTP request to the API with a device token goes here.
   */
 public class MessagingPush: ModuleTopLevelObject<MessagingPushInstance>, MessagingPushInstance {
+    @_spi(Internal) public static var appDelegateIntegratedExplicitely: Bool = false
+
     @Atomic public private(set) static var shared = MessagingPush()
     @Atomic public private(set) static var moduleConfig: MessagingPushConfigOptions = MessagingPushConfigBuilder().build()
 
@@ -57,7 +59,7 @@ public class MessagingPush: ModuleTopLevelObject<MessagingPushInstance>, Messagi
             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 {
+            if config.autoTrackPushEvents, !Self.appDelegateIntegratedExplicitely {
                 DIGraphShared.shared.automaticPushClickHandling.start()
             }
 
diff --git a/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift b/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift
index 61e6effb9..0972b9acc 100644
--- a/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift
+++ b/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift
@@ -32,11 +32,13 @@ import Foundation
  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
diff --git a/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift b/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift
index 6afe7570c..db9b0568a 100644
--- a/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift
+++ b/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift
@@ -9,6 +9,7 @@ import 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 }
@@ -23,6 +24,7 @@ protocol UserNotificationsFrameworkAdapter {
  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 {
diff --git a/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift b/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift
index 7dc4643cf..c214a34dd 100644
--- a/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift
+++ b/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift
@@ -846,4 +846,62 @@ 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() {
+        Mocks.shared.add(mock: self)
+    }
+
+    /**
+     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
new file mode 100644
index 000000000..03246ee56
--- /dev/null
+++ b/Sources/MessagingPushAPN/Integration/CioAppDelegateAPN.swift
@@ -0,0 +1,60 @@
+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<UserAppDelegate: CioAppDelegateType>: 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/Integration/CioAppDelegateFCM.swift b/Sources/MessagingPushFCM/Integration/CioAppDelegateFCM.swift
new file mode 100644
index 000000000..96a0adedb
--- /dev/null
+++ b/Sources/MessagingPushFCM/Integration/CioAppDelegateFCM.swift
@@ -0,0 +1,95 @@
+import CioInternalCommon
+import UIKit
+@_spi(Internal) import CioMessagingPush
+import FirebaseMessaging
+
+public typealias FirebaseMessagingInstance = () -> FirebaseMessagingIntegration
+
+// sourcery: AutoMockable
+public protocol FirebaseMessagingIntegration {
+    var delegate: MessagingDelegate? { get set }
+    var apnsToken: Data? { get set }
+}
+
+extension Messaging: FirebaseMessagingIntegration {}
+
+@available(iOSApplicationExtension, unavailable)
+open class CioAppDelegate: CioProviderAgnosticAppDelegate, MessagingDelegate {
+    /// Temporary solution, until interfaces MessagingPushInstance/MessagingPushAPNInstance/MessagingPushFCMInstance are fixed
+    private var messagingPushFCM: MessagingPushFCMInstance? {
+        messagingPush as? MessagingPushFCMInstance
+    }
+
+    private var firebaseMessaging: FirebaseMessagingInstance?
+    private var wrappedMessagingDelegate: MessagingDelegate?
+
+    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,
+            firebaseMessaging: nil,
+            appDelegate: nil,
+            logger: DIGraphShared.shared.logger
+        )
+    }
+
+    public init(
+        messagingPush: MessagingPushInstance,
+        userNotificationCenter: UserNotificationCenterInstance?,
+        firebaseMessaging: FirebaseMessagingInstance?,
+        appDelegate: CioAppDelegateType? = nil,
+        config: ConfigInstance? = nil,
+        logger: Logger
+    ) {
+        self.firebaseMessaging = firebaseMessaging
+        super.init(
+            messagingPush: messagingPush,
+            userNotificationCenter: userNotificationCenter,
+            appDelegate: appDelegate,
+            config: config,
+            logger: logger
+        )
+    }
+
+    override public func application(
+        _ application: UIApplication,
+        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+    ) -> Bool {
+        let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
+
+        if config?().autoFetchDeviceToken ?? false,
+           var messaging = firebaseMessaging?() {
+            wrappedMessagingDelegate = messaging.delegate
+            messaging.delegate = self
+        }
+
+        return result
+    }
+
+    // MARK: - MessagingDelegate
+
+    public func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
+        if let wrappedMessagingDelegate,
+           wrappedMessagingDelegate.responds(to: #selector(MessagingDelegate.messaging(_:didReceiveRegistrationToken:))) {
+            wrappedMessagingDelegate.messaging?(messaging, didReceiveRegistrationToken: fcmToken)
+        }
+
+        // Forward the device token to the Customer.io SDK:
+        messagingPushFCM?.registerDeviceToken(fcmToken: fcmToken)
+    }
+}
+
+@available(iOSApplicationExtension, unavailable)
+open class CioAppDelegateWrapper<UserAppDelegate: CioAppDelegateType>: CioAppDelegate {
+    public init() {
+        super.init(
+            messagingPush: MessagingPush.shared,
+            userNotificationCenter: { UNUserNotificationCenter.current() },
+            firebaseMessaging: { Messaging.messaging() },
+            appDelegate: UserAppDelegate(),
+            config: { MessagingPush.moduleConfig },
+            logger: DIGraphShared.shared.logger
+        )
+    }
+}
diff --git a/Sources/MessagingPushFCM/MessagingPushFCM.swift b/Sources/MessagingPushFCM/MessagingPushFCM.swift
index ae5635318..b5541cb5b 100644
--- a/Sources/MessagingPushFCM/MessagingPushFCM.swift
+++ b/Sources/MessagingPushFCM/MessagingPushFCM.swift
@@ -1,5 +1,5 @@
 import CioInternalCommon
-import CioMessagingPush
+@_spi(Internal) import CioMessagingPush
 import Foundation
 #if canImport(UserNotifications)
 import UserNotifications
@@ -92,7 +92,7 @@ public class MessagingPushFCM: MessagingPushFCMInstance {
         let implementation = MessagingPush.initialize(withConfig: config)
 
         let pushConfigOptions = MessagingPush.moduleConfig
-        if pushConfigOptions.autoFetchDeviceToken {
+        if pushConfigOptions.autoFetchDeviceToken, !MessagingPush.appDelegateIntegratedExplicitely {
             shared.setupAutoFetchDeviceToken()
         }
 
diff --git a/Sources/MessagingPushFCM/autogenerated/AutoDependencyInjection.generated.swift b/Sources/MessagingPushFCM/autogenerated/AutoDependencyInjection.generated.swift
index 1b713dd37..396793845 100644
--- a/Sources/MessagingPushFCM/autogenerated/AutoDependencyInjection.generated.swift
+++ b/Sources/MessagingPushFCM/autogenerated/AutoDependencyInjection.generated.swift
@@ -4,6 +4,7 @@
 
 import CioInternalCommon
 import CioMessagingPush
+import FirebaseMessaging
 import Foundation
 
 /**
diff --git a/Sources/MessagingPushFCM/autogenerated/AutoLenses.generated.swift b/Sources/MessagingPushFCM/autogenerated/AutoLenses.generated.swift
index ccc2199fb..16f304dc2 100644
--- a/Sources/MessagingPushFCM/autogenerated/AutoLenses.generated.swift
+++ b/Sources/MessagingPushFCM/autogenerated/AutoLenses.generated.swift
@@ -4,6 +4,7 @@
 
 import CioInternalCommon
 import CioMessagingPush
+import FirebaseMessaging
 import Foundation
 
 /**
diff --git a/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift b/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift
index 158dcff26..cec40c5ac 100644
--- a/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift
+++ b/Sources/MessagingPushFCM/autogenerated/AutoMockable.generated.swift
@@ -11,6 +11,7 @@ import UserNotifications
 #endif
 import CioInternalCommon
 import CioMessagingPush
+import FirebaseMessaging
 
 /**
  ######################################################
@@ -81,6 +82,103 @@ import CioMessagingPush
 
  */
 
+/**
+ Class to easily create a mocked version of the `FirebaseMessagingIntegration` 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 FirebaseMessagingIntegrationMock: FirebaseMessagingIntegration, Mock {
+    /// If *any* interactions done on mock. `true` if any method or property getter/setter called.
+    public var mockCalled: Bool = false //
+
+    public init() {
+        Mocks.shared.add(mock: self)
+    }
+
+    /**
+     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: MessagingDelegate? = 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: MessagingDelegate? {
+        get {
+            mockCalled = true
+            delegateGetCallsCount += 1
+            return underlyingDelegate
+        }
+        set(value) {
+            mockCalled = true
+            delegateSetCallsCount += 1
+            underlyingDelegate = value
+        }
+    }
+
+    /**
+     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 underlyingApnsToken: Data? = nil
+    /// `true` if the getter or setter of property is called at least once.
+    public var apnsTokenCalled: Bool {
+        apnsTokenGetCalled || apnsTokenSetCalled
+    }
+
+    /// `true` if the getter called on the property at least once.
+    public var apnsTokenGetCalled: Bool {
+        apnsTokenGetCallsCount > 0
+    }
+
+    public var apnsTokenGetCallsCount = 0
+    /// `true` if the setter called on the property at least once.
+    public var apnsTokenSetCalled: Bool {
+        apnsTokenSetCallsCount > 0
+    }
+
+    public var apnsTokenSetCallsCount = 0
+    /// The mocked property with a getter and setter.
+    public var apnsToken: Data? {
+        get {
+            mockCalled = true
+            apnsTokenGetCallsCount += 1
+            return underlyingApnsToken
+        }
+        set(value) {
+            mockCalled = true
+            apnsTokenSetCallsCount += 1
+            underlyingApnsToken = value
+        }
+    }
+
+    public func resetMock() {
+        delegate = nil
+        delegateGetCallsCount = 0
+        delegateSetCallsCount = 0
+        apnsToken = nil
+        apnsTokenGetCallsCount = 0
+        apnsTokenSetCallsCount = 0
+    }
+}
+
 /**
  Class to easily create a mocked version of the `MessagingPushFCMInstance` class.
  This class is equipped with functions and properties ready for you to mock!
diff --git a/Tests/MessagingPush/Integration/CioProviderAgnosticAppDelegateTests.swift b/Tests/MessagingPush/Integration/CioProviderAgnosticAppDelegateTests.swift
new file mode 100644
index 000000000..13f8148fe
--- /dev/null
+++ b/Tests/MessagingPush/Integration/CioProviderAgnosticAppDelegateTests.swift
@@ -0,0 +1,272 @@
+@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) -> MessagingPushConfigOptions {
+        MessagingPushConfigOptions(
+            logLevel: .info,
+            cdpApiKey: "test-api-key",
+            region: .US,
+            autoFetchDeviceToken: autoFetchDeviceToken,
+            autoTrackPushEvents: autoTrackPushEvents,
+            showPushAppInForeground: false
+        )
+    }
+
+    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.appDelegateIntegratedExplicitely = 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.appDelegateIntegratedExplicitely)
+        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 testUserNotificationCenterDidReceive_whenNotificationCenterIntegrationIsEnabled_thenWrapperedDelegatesAreCalled() {
+        // Configure mock return value
+        mockMessagingPush.userNotificationCenterReturnValue = nil
+
+        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(mockMessagingPush.userNotificationCenterCalled)
+        XCTAssertTrue(mockNotificationCenterDelegate.didReceiveNotificationResponseCalled)
+        XCTAssertTrue(completionHandlerCalled)
+    }
+
+    func testUserNotificationCenterDidReceive_whenWrappedNotificationCenterDelegateIsNil_thenNotificationCompletitionHandlerIsCalled() {
+        // Create custom app delegate
+        appDelegate = CioProviderAgnosticAppDelegate(
+            messagingPush: mockMessagingPush,
+            userNotificationCenter: { self.mockNotificationCenter },
+            appDelegate: mockAppDelegate,
+            config: { self.createMockConfig(autoTrackPushEvents: false) },
+            logger: mockLogger
+        )
+        mockMessagingPush.userNotificationCenterReturnValue = nil
+        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 testUserNotificationCenterWillPresent_whenCalled_thenWrappedDelegateIsCalled() {
+        // Setup
+        var completonHandlerCalled = false
+        let completionHandler: (UNNotificationPresentationOptions) -> Void = { _ in
+            completonHandlerCalled = 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(completonHandlerCalled)
+    }
+
+    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)
+    }
+}
diff --git a/Tests/MessagingPushAPN/Integration/CioAppDelegateAPNTests.swift b/Tests/MessagingPushAPN/Integration/CioAppDelegateAPNTests.swift
new file mode 100644
index 000000000..95d55063f
--- /dev/null
+++ b/Tests/MessagingPushAPN/Integration/CioAppDelegateAPNTests.swift
@@ -0,0 +1,138 @@
+@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.appDelegateIntegratedExplicitely = 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)
+        mockMessagingPush.userNotificationCenterReturnValue = nil
+
+        var completionHandlerCalled = false
+        let completionHandler = {
+            completionHandlerCalled = true
+        }
+
+        // Call the method
+        appDelegateAPN.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: UNNotificationResponse.testInstance, withCompletionHandler: completionHandler)
+
+        // Verify behavior
+        XCTAssertTrue(mockMessagingPush.userNotificationCenterCalled == true)
+        XCTAssertTrue(mockNotificationCenterDelegate.didReceiveNotificationResponseCalled)
+        XCTAssertTrue(completionHandlerCalled)
+    }
+}
diff --git a/Tests/MessagingPushAPN/Integration/MessagingPushAPNMocks.swift b/Tests/MessagingPushAPN/Integration/MessagingPushAPNMocks.swift
new file mode 100644
index 000000000..9533bd94c
--- /dev/null
+++ b/Tests/MessagingPushAPN/Integration/MessagingPushAPNMocks.swift
@@ -0,0 +1,87 @@
+@testable import CioInternalCommon
+@testable import CioMessagingPush
+@testable import CioMessagingPushAPN
+import Foundation
+
+class MessagingPushAPNMock: MessagingPushInstanceMock, MessagingPushAPNInstance {
+    // MARK: - registerDeviceToken
+
+    /// Number of times the function was called.
+    @Atomic public private(set) var registerDeviceTokenAPNCallsCount = 0
+    /// `true` if the function was ever called.
+    public var registerDeviceTokenAPNCalled: Bool {
+        registerDeviceTokenAPNCallsCount > 0
+    }
+
+    /// The arguments from the *last* time the function was called.
+    @Atomic public private(set) var registerDeviceTokenAPNReceivedArguments: Data?
+    /// Arguments from *all* of the times that the function was called.
+    @Atomic public private(set) var registerDeviceTokenAPNReceivedInvocations: [Data] = []
+    /**
+     Set closure to get called when function gets called. Great way to test logic or return a value for the function.
+     */
+    public var registerDeviceTokenAPNClosure: ((Data) -> Void)?
+
+    /// Mocked function for `registerDeviceToken(apnDeviceToken: Data)`. Your opportunity to return a mocked value and check result of mock in test code.
+    public func registerDeviceToken(apnDeviceToken: Data) {
+        mockCalled = true
+        registerDeviceTokenAPNCallsCount += 1
+        registerDeviceTokenAPNReceivedArguments = apnDeviceToken
+        registerDeviceTokenAPNReceivedInvocations.append(apnDeviceToken)
+        registerDeviceTokenAPNClosure?(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)
+    }
+}
diff --git a/Tests/MessagingPushFCM/Integration/CioAppDelegateFCMTests.swift b/Tests/MessagingPushFCM/Integration/CioAppDelegateFCMTests.swift
new file mode 100644
index 000000000..3532d2532
--- /dev/null
+++ b/Tests/MessagingPushFCM/Integration/CioAppDelegateFCMTests.swift
@@ -0,0 +1,202 @@
+@testable import CioInternalCommon
+@_spi(Internal) @testable import CioMessagingPush
+@testable import CioMessagingPushFCM
+import FirebaseMessaging
+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 mockMessaging: FirebaseMessagingIntegrationMock!
+    var mockMessagingDelegate: MockMessagingDelegate!
+    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()
+        Messaging.swizzleMessaging()
+
+        mockMessagingPush = MessagingPushFCMMock()
+
+        mockAppDelegate = MockAppDelegate()
+
+        mockNotificationCenter = UserNotificationCenterIntegrationMock()
+        mockNotificationCenterDelegate = MockNotificationCenterDelegate()
+        mockNotificationCenter.delegate = mockNotificationCenterDelegate
+
+        mockMessaging = FirebaseMessagingIntegrationMock()
+        mockMessagingDelegate = MockMessagingDelegate()
+        mockMessaging.delegate = mockMessagingDelegate
+
+        mockLogger = LoggerMock()
+
+        appDelegateFCM = CioAppDelegate(
+            messagingPush: mockMessagingPush,
+            userNotificationCenter: { self.mockNotificationCenter },
+            firebaseMessaging: { self.mockMessaging },
+            appDelegate: mockAppDelegate,
+            config: { self.createMockConfig() },
+            logger: mockLogger
+        )
+    }
+
+    override func tearDown() {
+        mockMessagingPush = nil
+        mockAppDelegate = nil
+        mockNotificationCenter = nil
+        mockNotificationCenterDelegate = nil
+        mockMessaging = nil
+        mockMessagingDelegate = nil
+        mockLogger = nil
+        appDelegateFCM = nil
+
+        Messaging.unswizzleMessaging()
+        UNUserNotificationCenter.unswizzleNotificationCenter()
+
+        MessagingPush.appDelegateIntegratedExplicitely = false
+
+        super.tearDown()
+    }
+
+    // MARK: - Tests for FCM-specific functionality
+
+    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_thenMessagingDelegateIsSet() {
+        // Call the method
+        _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
+
+        // Verify behavior
+        XCTAssertTrue(mockMessaging.delegate === appDelegateFCM)
+    }
+
+    func testDidFinishLaunchings_whenAutoFetchDeviceTokenIsDesabled_thenMessagingDelegateIsNotSet() {
+        appDelegateFCM = CioAppDelegate(
+            messagingPush: mockMessagingPush,
+            userNotificationCenter: { self.mockNotificationCenter },
+            firebaseMessaging: { self.mockMessaging },
+            appDelegate: mockAppDelegate,
+            config: { self.createMockConfig(autoFetchDeviceToken: false) },
+            logger: mockLogger
+        )
+        mockMessaging.delegate = nil
+
+        // Call didFinishLaunching
+        let result = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
+
+        // Verify behavior
+        XCTAssertTrue(result)
+        XCTAssertTrue(mockAppDelegate.didFinishLaunchingCalled)
+        XCTAssertNil(mockMessaging.delegate)
+    }
+
+    // MARK: - Test MessagingDelegate
+
+    func testMessagingDidReceiveRegistrationToken_whenCalled_thenWrappedMessagingDelegateIsCalled() {
+        // Setup
+        let fcmToken = "test-fcm-token"
+        _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
+
+        // Call method directly
+        appDelegateFCM.messaging(Messaging.messagingMock(), didReceiveRegistrationToken: fcmToken)
+
+        // Verify behavior
+        XCTAssertTrue(mockMessagingDelegate.didReceiveRegistrationTokenCalled)
+        XCTAssertEqual(mockMessagingDelegate.fcmTokenReceived, fcmToken)
+    }
+
+    func testMessagingDidReceiveRegistrationToken_whenCalled_thenTokenIsForwardedToCIO() {
+        // Setup
+        let fcmToken = "test-fcm-token"
+        _ = appDelegateFCM.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
+
+        // Call method directly
+        appDelegateFCM.messaging(Messaging.messagingMock(), 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
+        mockMessagingPush.userNotificationCenterReturnValue = nil
+        _ = 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(mockMessagingPush.userNotificationCenterCalled == true)
+        XCTAssertTrue(mockNotificationCenterDelegate.didReceiveNotificationResponseCalled)
+        XCTAssertTrue(completionHandlerCalled)
+    }
+}
diff --git a/Tests/MessagingPushFCM/Integration/FirebaseMessagingMocks.swift b/Tests/MessagingPushFCM/Integration/FirebaseMessagingMocks.swift
new file mode 100644
index 000000000..a1e5ca1ea
--- /dev/null
+++ b/Tests/MessagingPushFCM/Integration/FirebaseMessagingMocks.swift
@@ -0,0 +1,29 @@
+import FirebaseMessaging
+
+class MockMessagingDelegate: NSObject, MessagingDelegate {
+    var didReceiveRegistrationTokenCalled = false
+    var fcmTokenReceived: String?
+
+    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
+        didReceiveRegistrationTokenCalled = true
+        fcmTokenReceived = fcmToken
+    }
+}
+
+public extension Messaging {
+    static func swizzleMessaging() {
+        let originalMethod = class_getClassMethod(Messaging.self, #selector(Messaging.messaging))!
+        let swizzledMethod = class_getClassMethod(Messaging.self, #selector(Messaging.messagingMock))!
+        method_exchangeImplementations(originalMethod, swizzledMethod)
+    }
+
+    static func unswizzleMessaging() {
+        swizzleMessaging() // Calling again will swap back
+    }
+
+    @objc class func messagingMock() -> Messaging {
+        let dummyObject = NSObject()
+        let messaging = unsafeBitCast(dummyObject, to: Messaging.self)
+        return messaging
+    }
+}
diff --git a/Tests/MessagingPushFCM/Integration/MessagingPushFCMMocks.swift b/Tests/MessagingPushFCM/Integration/MessagingPushFCMMocks.swift
new file mode 100644
index 000000000..bbac556a1
--- /dev/null
+++ b/Tests/MessagingPushFCM/Integration/MessagingPushFCMMocks.swift
@@ -0,0 +1,114 @@
+@testable import CioInternalCommon
+@testable import CioMessagingPush
+@testable import CioMessagingPushFCM
+import Foundation
+
+class MessagingPushFCMMock: MessagingPushInstanceMock, MessagingPushFCMInstance {
+    // MARK: - registerDeviceToken
+
+    /// Number of times the function was called.
+    @Atomic public private(set) var registerDeviceTokenFCMCallsCount = 0
+    /// `true` if the function was ever called.
+    public var registerDeviceTokenFCMCalled: Bool {
+        registerDeviceTokenFCMCallsCount > 0
+    }
+
+    /// The arguments from the *last* time the function was called.
+    @Atomic public private(set) var registerDeviceTokenFCMReceivedArguments: String??
+    /// Arguments from *all* of the times that the function was called.
+    @Atomic public private(set) var registerDeviceTokenFCMReceivedInvocations: [String?] = []
+    /**
+     Set closure to get called when function gets called. Great way to test logic or return a value for the function.
+     */
+    public var registerDeviceTokenFCMClosure: ((String?) -> Void)?
+
+    /// Mocked function for `registerDeviceToken(fcmToken: String?)`. Your opportunity to return a mocked value and check result of mock in test code.
+    public func registerDeviceToken(fcmToken: String?) {
+        mockCalled = true
+        registerDeviceTokenFCMCallsCount += 1
+        registerDeviceTokenFCMReceivedArguments = fcmToken
+        registerDeviceTokenFCMReceivedInvocations.append(fcmToken)
+        registerDeviceTokenFCMClosure?(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 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)
+    }
+}
diff --git a/Tests/Shared/Mocks/UIKitDelegateMocks.swift b/Tests/Shared/Mocks/UIKitDelegateMocks.swift
new file mode 100644
index 000000000..9650af71a
--- /dev/null
+++ b/Tests/Shared/Mocks/UIKitDelegateMocks.swift
@@ -0,0 +1,78 @@
+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 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
+    }
+}
+
+// Custom UNUserNotificationCenterDelegate mock
+public class MockNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
+    public var didReceiveNotificationResponseCalled = false
+    public var willPresentNotificationCalled = false
+    public var openSettingsForNotificationCalled = false
+
+    public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
+        didReceiveNotificationResponseCalled = true
+        completionHandler()
+    }
+
+    public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
+        willPresentNotificationCalled = true
+        completionHandler([])
+    }
+
+    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/Tests/Shared/NotificationCenter.swift b/Tests/Shared/NotificationCenter.swift
index ec678fa37..6327d4a67 100644
--- a/Tests/Shared/NotificationCenter.swift
+++ b/Tests/Shared/NotificationCenter.swift
@@ -16,7 +16,14 @@ public extension UNNotificationResponse {
     static var testInstance: UNNotificationResponse {
         // OK to use try! as it's utility code used by tests and is rarely edited.
         // swiftlint:disable:next force_try
-        try! XCTUnwrap(UNNotificationResponse(coder: KeyedArchiver()))
+        try! XCTUnwrap(UNNotificationResponse(coder: KeyedArchiver(requiringSecureCoding: false)))
+    }
+}
+
+public extension UNNotification {
+    static var testInstance: UNNotification {
+        // swiftlint:disable:next force_try - Ok in Test code.
+        try! XCTUnwrap(UNNotification(coder: KeyedArchiver(requiringSecureCoding: false)))
     }
 }
 #endif


Reviews:
user: login: mahmoud-elmorabea
body: Looks good! 💯 

_We can prefix the PR with `chore` and it won't kick off a release_
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:46:20Z
user: login: Ahmed-Ali
body: Thanks for splitting out the changes, Uros!

I have few comments. I won't block the PR but let's address the none-`nit` before merging
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:47:23Z
user: login: Ahmed-Ali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:48:28Z
user: login: uros-mil
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:30:25Z
user: login: uros-mil
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:30:37Z
user: login: uros-mil
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:32:28Z
user: login: uros-mil
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:33:11Z
user: login: uros-mil
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:33:24Z
user: login: uros-mil
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:34:55Z
user: login: uros-mil
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:43:33Z

Review comments:
diff_hunk: @@ -15,7 +15,7 @@ This is the official Customer.io SDK for iOS.
 
 You'll find our [complete SDK documentation at https://customer.io/docs/sdk/ios](https://customer.io/docs/sdk/ios/). 
 
-**The SDK has been tested on iOS devices**. It might work on other Apple devices—macOS, tvOS, and watchOS—but we have not officially tested, nor do we officially support, non-iOS devices.
path: README.md
user: login: Ahmed-Ali
body: nit: is this intentional change?
created_at: 2025-05-02T08:30:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: Ahmed-Ali
body: what is the reason for making the class name call out`WithoutTokenRetrieval`?
This seems like a configureable behavior. Which confuses me why it is called out in the type name
created_at: 2025-05-02T08:32:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,7 @@ public class MessagingPushConfigBuilder {
 
     /// Display push notifications sent by Customer.io while app is in foreground. This value is `true` by default.
     @discardableResult
+    @available(*, deprecated, message: "When CioAppDelegate(Wrapper) is used, this method is not needed")
path: Sources/MessagingPush/Config/MessagingPushConfigBuilder.swift
user: login: mahmoud-elmorabea
body: Maybe clarify this as "This shouldn't be set if you're using ioAppDelegate(Wrapper), if you are using ioAppDelegate(Wrapper) yet, it's recommended you upgrade".. somethings along those lines.
created_at: 2025-05-02T08:37:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
+    @_spi(Internal) public let messagingPush: MessagingPushInstance
+    @_spi(Internal) public let logger: Logger
+    @_spi(Internal) public var implementedOptionalMethods: Set<Selector> = [
+        // UIApplicationDelegate
+        #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)),
+        #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)),
+        #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)),
+        #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)),
+        // 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 wrappedNoticeCenterDelegate: 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.appDelegateIntegratedExplicitely = true
+
+        let result = wrappedAppDelegate?.application?(application, didFinishLaunchingWithOptions: launchOptions)
+
+        if config?().autoFetchDeviceToken ?? false {
+            application.cioRegisterForRemoteNotifications(logger: logger)
+        }
+
+        if config?().autoTrackPushEvents ?? false,
+           var center = userNotificationCenter?() {
+            wrappedNoticeCenterDelegate = center.delegate
+            center.delegate = self
+        }
+
+        return result ?? true
+    }
+
+    open func application(
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: Ahmed-Ali
body: did we forget to call messagingPush?
created_at: 2025-05-02T08:40:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,6 +6,8 @@ import Foundation
  So, performing an HTTP request to the API with a device token goes here.
   */
 public class MessagingPush: ModuleTopLevelObject<MessagingPushInstance>, MessagingPushInstance {
+    public static var appDelegateIntegratedExplicitely: Bool = false
path: Sources/MessagingPush/MessagingPush.swift
user: login: mahmoud-elmorabea
body: Guess that has to be public? Maybe we can restrict it to be only a setter?
created_at: 2025-05-02T08:41:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
+    @_spi(Internal) public let messagingPush: MessagingPushInstance
+    @_spi(Internal) public let logger: Logger
+    @_spi(Internal) public var implementedOptionalMethods: Set<Selector> = [
+        // UIApplicationDelegate
+        #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)),
+        #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)),
+        #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)),
+        #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)),
+        // 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 wrappedNoticeCenterDelegate: UNUserNotificationCenterDelegate?
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: Ahmed-Ali
body: nit: the naming `notice` is not easily recognizable and mismatches the actual name
created_at: 2025-05-02T08:41:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: Ahmed-Ali
body: As this one is being subclasses internally, shouldn't we annotate it with `@_spi`?
created_at: 2025-05-02T08:44:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
+    @_spi(Internal) public let messagingPush: MessagingPushInstance
+    @_spi(Internal) public let logger: Logger
+    @_spi(Internal) public var implementedOptionalMethods: Set<Selector> = [
+        // UIApplicationDelegate
+        #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)),
+        #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)),
+        #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)),
+        #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)),
+        // 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 wrappedNoticeCenterDelegate: 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
+        )
+    }
+
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: Ahmed-Ali
body: Line coverage isn't the most indicative of the test coverage. But lack of it indicates potential missed code branches. Let's address before merging
created_at: 2025-05-02T08:48:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,7 @@ public class MessagingPushConfigBuilder {
 
     /// Display push notifications sent by Customer.io while app is in foreground. This value is `true` by default.
     @discardableResult
+    @available(*, deprecated, message: "When CioAppDelegate(Wrapper) is used, this method is not needed")
path: Sources/MessagingPush/Config/MessagingPushConfigBuilder.swift
user: login: uros-mil
body: Makes sense. It's updated.
created_at: 2025-05-02T09:30:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,6 +6,8 @@ import Foundation
  So, performing an HTTP request to the API with a device token goes here.
   */
 public class MessagingPush: ModuleTopLevelObject<MessagingPushInstance>, MessagingPushInstance {
+    public static var appDelegateIntegratedExplicitely: Bool = false
path: Sources/MessagingPush/MessagingPush.swift
user: login: uros-mil
body: Fixed
created_at: 2025-05-02T09:30:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: uros-mil
body: 1. The name is updated to CioProviderAgnosticAppDelegate to better explain the purpose
2. It's not possible to add `@_spi` as open classes are extending it


created_at: 2025-05-02T09:32:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
+    @_spi(Internal) public let messagingPush: MessagingPushInstance
+    @_spi(Internal) public let logger: Logger
+    @_spi(Internal) public var implementedOptionalMethods: Set<Selector> = [
+        // UIApplicationDelegate
+        #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)),
+        #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)),
+        #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)),
+        #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)),
+        // 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 wrappedNoticeCenterDelegate: 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.appDelegateIntegratedExplicitely = true
+
+        let result = wrappedAppDelegate?.application?(application, didFinishLaunchingWithOptions: launchOptions)
+
+        if config?().autoFetchDeviceToken ?? false {
+            application.cioRegisterForRemoteNotifications(logger: logger)
+        }
+
+        if config?().autoTrackPushEvents ?? false,
+           var center = userNotificationCenter?() {
+            wrappedNoticeCenterDelegate = center.delegate
+            center.delegate = self
+        }
+
+        return result ?? true
+    }
+
+    open func application(
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: uros-mil
body: No, since we have separate API for APN and FCM, those calls are in child classes.
created_at: 2025-05-02T09:33:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
+    @_spi(Internal) public let messagingPush: MessagingPushInstance
+    @_spi(Internal) public let logger: Logger
+    @_spi(Internal) public var implementedOptionalMethods: Set<Selector> = [
+        // UIApplicationDelegate
+        #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)),
+        #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)),
+        #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)),
+        #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)),
+        // 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 wrappedNoticeCenterDelegate: UNUserNotificationCenterDelegate?
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: uros-mil
body: Fixed
created_at: 2025-05-02T09:33:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,7 +15,7 @@ This is the official Customer.io SDK for iOS.
 
 You'll find our [complete SDK documentation at https://customer.io/docs/sdk/ios](https://customer.io/docs/sdk/ios/). 
 
-**The SDK has been tested on iOS devices**. It might work on other Apple devices—macOS, tvOS, and watchOS—but we have not officially tested, nor do we officially support, non-iOS devices.
path: README.md
user: login: uros-mil
body: Yes.
Our Package.swift has iOS as the only supported platform, and it's expected that UIKit is available in many places.
In the current code state, it is not enough to just add macOS as a supported platform; we'll need to make the code changes to support it properly.
I intended to remove the incorrect statement.

It would make sense to list necessary changes for extending support to macOS.
created_at: 2025-05-02T09:34:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,171 @@
+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 CioAppDelegateWithoutTokenRetrieval: CioAppDelegateType, UNUserNotificationCenterDelegate {
+    @_spi(Internal) public let messagingPush: MessagingPushInstance
+    @_spi(Internal) public let logger: Logger
+    @_spi(Internal) public var implementedOptionalMethods: Set<Selector> = [
+        // UIApplicationDelegate
+        #selector(UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:)),
+        #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)),
+        #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)),
+        #selector(UIApplicationDelegate.application(_:continue:restorationHandler:)),
+        // 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 wrappedNoticeCenterDelegate: 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
+        )
+    }
+
path: Sources/MessagingPush/Integration/CioAppDelegateWithoutTokenRetrieval.swift
user: login: uros-mil
body: Code coverage is almost 100% in reality.
We have lower numbers because:
- There are `init` methods which are not intended for use, but are necessary to satisfy UIApplication.delegate requirements for a no-arg constructor, and those are not tested
- Tests are not written for Wrappers, as those only forward data to AppDelegates
created_at: 2025-05-02T09:43:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-02T08:20:08Z
body: <!-- sample app builds -->
# Sample app builds 📱

Below you will find the list of the latest versions of the sample apps. 
It's recommended to always download the latest builds to test this PR accurately.
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-02T08:31:23Z
body: ## [Codecov](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio) Report
Attention: Patch coverage is `73.26203%` with `50 lines` in your changes missing coverage. Please review.
> Project coverage is 60.28%. Comparing base [(`8accbdc`)](https://app.codecov.io/gh/customerio/customerio-ios/commit/8accbdc54025ba89b3982e5c6d3d840a8d0771e8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio) to head [(`4aec019`)](https://app.codecov.io/gh/customerio/customerio-ios/commit/4aec0193393291560f763d0a1afcdf8f3b2f9538?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio).
> Report is 2 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio) | Patch % | Lines |
|---|---|---|
| [...ssagingPushFCM/Integration/CioAppDelegateFCM.swift](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&filepath=Sources%2FMessagingPushFCM%2FIntegration%2FCioAppDelegateFCM.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio#diff-U291cmNlcy9NZXNzYWdpbmdQdXNoRkNNL0ludGVncmF0aW9uL0Npb0FwcERlbGVnYXRlRkNNLnN3aWZ0) | 62.26% | [20 Missing :warning: ](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio) |
| [...ssagingPushAPN/Integration/CioAppDelegateAPN.swift](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&filepath=Sources%2FMessagingPushAPN%2FIntegration%2FCioAppDelegateAPN.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio#diff-U291cmNlcy9NZXNzYWdpbmdQdXNoQVBOL0ludGVncmF0aW9uL0Npb0FwcERlbGVnYXRlQVBOLnN3aWZ0) | 47.22% | [19 Missing :warning: ](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio) |
| [...h/Integration/CioProviderAgnosticAppDelegate.swift](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&filepath=Sources%2FMessagingPush%2FIntegration%2FCioProviderAgnosticAppDelegate.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio#diff-U291cmNlcy9NZXNzYWdpbmdQdXNoL0ludGVncmF0aW9uL0Npb1Byb3ZpZGVyQWdub3N0aWNBcHBEZWxlZ2F0ZS5zd2lmdA==) | 89.58% | [10 Missing :warning: ](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio) |
| [Sources/MessagingPushFCM/MessagingPushFCM.swift](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&filepath=Sources%2FMessagingPushFCM%2FMessagingPushFCM.swift&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio#diff-U291cmNlcy9NZXNzYWdpbmdQdXNoRkNNL01lc3NhZ2luZ1B1c2hGQ00uc3dpZnQ=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #891      +/-   ##
==========================================
+ Coverage   59.69%   60.28%   +0.58%     
==========================================
  Files         160      163       +3     
  Lines        7009     7193     +184     
==========================================
+ Hits         4184     4336     +152     
- Misses       2825     2857      +32     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/customerio/customerio-ios/pull/891?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=customerio).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-02T08:35:53Z
body: <!-- sdk size reports -->
# SDK binary size reports 📊

<details>
<summary>SDK binary size of this PR</summary>

```
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  16.0%   152Ki  16.0%   152Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/autogenerated/AutoMockable.generated.swift
   4.0%  38.2Ki   4.0%  38.2Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Communication/EventBusHandler.swift
   3.8%  36.0Ki   3.8%  36.0Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift
   3.7%  35.4Ki   3.7%  35.4Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/autogenerated/AutoMockable.generated.swift
   2.7%  25.8Ki   2.7%  25.8Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/InAppMessageManager.swift
   2.4%  22.9Ki   2.4%  22.9Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/DataPipelineImplementation.swift
   2.4%  22.8Ki   2.4%  22.8Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Migration/autogenerated/AutoMockable.generated.swift
   2.3%  21.9Ki   2.3%  21.9Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/QueueStorage.swift
   2.0%  18.9Ki   2.0%  18.9Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPushAPN/autogenerated/AutoMockable.generated.swift
   1.9%  18.6Ki   1.9%  18.6Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Communication/Event.swift
   1.9%  18.0Ki   1.9%  18.0Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/EngineWeb/AnyEncodable.swift
   1.8%  16.9Ki   1.8%  16.9Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/InAppMessageMiddleware.swift
   1.6%  15.6Ki   1.6%  15.6Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/Type/PushNotification.swift
   1.6%  15.2Ki   1.6%  15.2Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/autogenerated/AutoDependencyInjection.generated.swift
   1.6%  15.1Ki   1.6%  15.1Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Communication/EventMemoryStorage.swift
   1.5%  14.3Ki   1.5%  14.3Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/InAppMessageState.swift
   1.4%  13.5Ki   1.4%  13.5Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Communication/EventStorage.swift
   1.2%  11.8Ki   1.2%  11.8Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/DataPipeline.swift
   1.2%  11.3Ki   1.2%  11.3Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Managers/MessageManager.swift
   1.1%  10.2Ki   1.1%  10.2Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/PushHandling/PushEventHandlerProxy.swift
   1.0%  9.81Ki   1.0%  9.81Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Views/GistInlineInAppMessageView.swift
   1.0%  9.61Ki   1.0%  9.61Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/JsonAdapter.swift
   1.0%  9.50Ki   1.0%  9.50Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/CustomerIOInstance.swift
   1.0%  9.41Ki   1.0%  9.41Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/EngineWeb/EngineWeb.swift
   1.0%  9.30Ki   1.0%  9.30Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/InAppMessageReducer.swift
   0.9%  8.83Ki   0.9%  8.83Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/Request/TrackDeliveryEventRequestBody.swift
   0.9%  8.71Ki   0.9%  8.71Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Plugins/AutoTrackingScreenViews.swift
   0.9%  8.64Ki   0.9%  8.64Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/UserNotificationsFramework/Wrappers.swift
   0.9%  8.28Ki   0.9%  8.28Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/Log.swift
   0.9%  8.22Ki   0.9%  8.22Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Migration/DataPipelineMigrationAssistant.swift
   0.9%  8.18Ki   0.9%  8.18Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Views/UIKitInline.swift
   0.9%  8.12Ki   0.9%  8.12Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/Integration/CioProviderAgnosticAppDelegate.swift
   0.8%  8.04Ki   0.8%  8.04Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Queue.swift
   0.8%  7.44Ki   0.8%  7.44Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/RichPush/RichPushHttpClient.swift
   0.8%  7.41Ki   0.8%  7.41Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Type/QueueTaskMetadata.swift
   0.8%  7.39Ki   0.8%  7.39Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Utilities/URLComponents.swift
   0.7%  6.97Ki   0.7%  6.97Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Managers/ModalViewManager.swift
   0.7%  6.86Ki   0.7%  6.86Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/Config/MessagingPushConfigBuilder.swift
   0.7%  6.84Ki   0.7%  6.84Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/Store/PushHistory.swift
   0.7%  6.39Ki   0.7%  6.39Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Gist.swift
   0.7%  6.21Ki   0.7%  6.21Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/KeyValueStorage.swift
   0.6%  5.79Ki   0.6%  5.79Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/autogenerated/AutoDependencyInjection.generated.swift
   0.6%  5.70Ki   0.6%  5.70Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Migration/TrackEventMigrationType.swift
   0.6%  5.64Ki   0.6%  5.64Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Type/QueueTask.swift
   0.6%  5.63Ki   0.6%  5.63Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Views/SwiftUIInline.swift
   0.6%  5.50Ki   0.6%  5.50Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/autogenerated/AutoDependencyInjection.generated.swift
   0.5%  5.16Ki   0.5%  5.16Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/Response/ErrorMessageResponse.swift
   0.5%  5.07Ki   0.5%  5.07Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/InAppMessageAction.swift
   0.5%  5.06Ki   0.5%  5.06Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/GistQueueNetwork.swift
   0.5%  5.03Ki   0.5%  5.03Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Managers/Models/Message.swift
   0.5%  4.57Ki   0.5%  4.57Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Plugins/DataPipelinePublishedEvents.swift
   0.5%  4.45Ki   0.5%  4.45Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Plugins/DeviceContexualAttributes.swift
   0.5%  4.44Ki   0.5%  4.44Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/Request/MetricRequest.swift
   0.5%  4.43Ki   0.5%  4.43Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Communication/EventBus.swift
   0.4%  4.21Ki   0.4%  4.21Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/FileStorage.swift
   0.4%  4.21Ki   0.4%  4.21Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/PushHandling/iOSPushEventListener.swift
   0.4%  4.08Ki   0.4%  4.08Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/GistDelegate.swift
   0.4%  3.86Ki   0.4%  3.86Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/MessagingPush.swift
   0.4%  3.79Ki   0.4%  3.79Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/EngineWeb/EngineWebConfiguration.swift
   0.4%  3.73Ki   0.4%  3.73Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Module/ModuleTopLevelObject.swift
   0.4%  3.70Ki   0.4%  3.70Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/HttpRequestRunner.swift
   0.4%  3.59Ki   0.4%  3.59Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Views/GistModalViewController.swift
   0.4%  3.45Ki   0.4%  3.45Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Config/MessagingInAppConfigBuilder.swift
   0.4%  3.37Ki   0.4%  3.37Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/StringExtensions.swift
   0.3%  3.32Ki   0.3%  3.32Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/SystemLogger.swift
   0.3%  3.32Ki   0.3%  3.32Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/Atomic.swift
   0.3%  3.25Ki   0.3%  3.25Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/RichPush/MessagingPush+RichPush.swift
   0.3%  3.15Ki   0.3%  3.15Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Managers/ModalMessageManager.swift
   0.3%  3.10Ki   0.3%  3.10Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/StringAnyEncodable.swift
   0.3%  3.06Ki   0.3%  3.06Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/DIManager.swift
   0.3%  3.02Ki   0.3%  3.02Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/EngineWeb/EngineEventHandler.swift
   0.3%  2.98Ki   0.3%  2.98Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Plugins/Context.swift
   0.3%  2.96Ki   0.3%  2.96Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Managers/QueueManager.swift
   0.3%  2.91Ki   0.3%  2.91Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/BaseNetwork.swift
   0.3%  2.88Ki   0.3%  2.88Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Task Data/DeletePushNotificationQueueTaskData.swift
   0.3%  2.84Ki   0.3%  2.84Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/DeviceInfo.swift
   0.3%  2.84Ki   0.3%  2.84Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Task Data/IdentifyProfileQueueTaskData.swift
   0.3%  2.84Ki   0.3%  2.84Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Task Data/TrackEventQueueTaskData.swift
   0.3%  2.65Ki   0.3%  2.65Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/HttpRequestParams.swift
   0.3%  2.62Ki   0.3%  2.62Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPushAPN/Integration/CioAppDelegateAPN.swift
   0.3%  2.59Ki   0.3%  2.59Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/PushHandling/ManualPushHandling+UserNotifications.swift
   0.3%  2.58Ki   0.3%  2.58Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Task Data/RegisterPushNotificationQueueTaskData.swift
   0.3%  2.55Ki   0.3%  2.55Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/HttpRequestError.swift
   0.3%  2.54Ki   0.3%  2.54Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Util/DeviceAttributesProvider.swift
   0.2%  2.37Ki   0.2%  2.37Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/Core/Subscription.swift
   0.2%  2.34Ki   0.2%  2.34Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/RichPush/RichPushRequestHandler.swift
   0.2%  2.32Ki   0.2%  2.32Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Config/SDKConfigBuilder.swift
   0.2%  2.29Ki   0.2%  2.29Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPushAPN/MessagingPushAPN.swift
   0.2%  2.20Ki   0.2%  2.20Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/UserNotificationsFramework/UserNotificationsFrameworkAdapter.swift
   0.2%  2.09Ki   0.2%  2.09Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Type/QueueTaskRunResults.swift
   0.2%  2.07Ki   0.2%  2.07Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/MessagingInAppImplementation.swift
   0.2%  1.98Ki   0.2%  1.98Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/MessagingPushImplementation.swift
   0.2%  1.95Ki   0.2%  1.95Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/LockManager.swift
   0.2%  1.89Ki   0.2%  1.89Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Plugins/ScreenFilterPlugin.swift
   0.2%  1.75Ki   0.2%  1.75Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/RichPush/RichPushDeliveryTracker.swift
   0.2%  1.73Ki   0.2%  1.73Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/autogenerated/AutoDependencyInjection.generated.swift
   0.2%  1.67Ki   0.2%  1.67Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPushAPN/MessagingPush+APN.swift
   0.2%  1.64Ki   0.2%  1.64Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Views/GistView.swift
   0.2%  1.62Ki   0.2%  1.62Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/DateExtensions.swift
   0.2%  1.59Ki   0.2%  1.59Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/Core/Synchronized.swift
   0.2%  1.52Ki   0.2%  1.52Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/Models/UserQueueResponse.swift
   0.2%  1.49Ki   0.2%  1.49Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/Request/PushMetric.swift
   0.2%  1.48Ki   0.2%  1.48Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/SdkClient.swift
   0.2%  1.46Ki   0.2%  1.46Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/MessagingInApp.swift
   0.1%  1.42Ki   0.1%  1.42Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/CIOApiEndpoint.swift
   0.1%  1.42Ki   0.1%  1.42Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/autogenerated/AutoMockable.generated.swift
   0.1%  1.37Ki   0.1%  1.37Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Extensions/GistExtensions.swift
   0.1%  1.37Ki   0.1%  1.37Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/PushHandling/PushClickHandler.swift
   0.1%  1.36Ki   0.1%  1.36Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Utilities/UIColor+Hex.swift
   0.1%  1.34Ki   0.1%  1.34Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Store/SdkConfig.swift
   0.1%  1.34Ki   0.1%  1.34Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/RichPush/RichPushRequest.swift
   0.1%  1.32Ki   0.1%  1.32Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/Request/InAppMetric.swift
   0.1%  1.30Ki   0.1%  1.30Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/Mocks.swift
   0.1%  1.24Ki   0.1%  1.24Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/UIKitWrapper.swift
   0.1%  1.24Ki   0.1%  1.24Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/ThreadUtil.swift
   0.1%  1.22Ki   0.1%  1.22Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/DictionaryExtension.swift
   0.1%  1.21Ki   0.1%  1.21Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Store/GlobalDataStore.swift
   0.1%  1.19Ki   0.1%  1.19Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Type/ScreenView.swift
   0.1%  1.18Ki   0.1%  1.18Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Config/DataPipelineConfigOptions.swift
   0.1%  1.12Ki   0.1%  1.12Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/DeviceExtension.swift
   0.1%  1.12Ki   0.1%  1.12Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Type/Region.swift
   0.1%  1.02Ki   0.1%  1.02Ki    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/DIGraphShared.swift
   0.1%    1024   0.1%    1024    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/AnyEncodable.swift
   0.1%     980   0.1%     980    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Type/InAppMessage.swift
   0.1%     968   0.1%     968    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/KeyValueStorageKey.swift
   0.1%     896   0.1%     896    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/Utilities/HTTPMethod.swift
   0.1%     880   0.1%     880    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Type/QueueTaskType.swift
   0.1%     880   0.1%     880    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/UserAgentUtil.swift
   0.1%     872   0.1%     872    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/Request/EmptyRequestBody.swift
   0.1%     868   0.1%     868    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPushAPN/MessagingPushAPN+PushConfigs.swift
   0.1%     832   0.1%     832    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/CustomerIO+Events.swift
   0.1%     832   0.1%     832    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Managers/InlineMessageManager.swift
   0.1%     820   0.1%     820    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/ArrayExtensions.swift
   0.1%     792   0.1%     792    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/Endpoints/Utilities/GistNetworkRequest.swift
   0.1%     788   0.1%     788    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/DeepLinkUtil.swift
   0.1%     768   0.1%     768    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Plugins/Logger.swift
   0.1%     752   0.1%     752    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Utilities/Environment.swift
   0.1%     652   0.1%     652    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/Type/CustomerIOParsedPushPayload.swift
   0.1%     612   0.1%     612    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/CustomerIO.swift
   0.1%     600   0.1%     600    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Service/DownloadFileType.swift
   0.1%     600   0.1%     600    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/Config/MessagingPushConfigOptions.swift
   0.1%     576   0.1%     576    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/UIKitExtensions.swift
   0.1%     572   0.1%     572    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/Endpoints/QueueEndpoint.swift
   0.1%     532   0.1%     532    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Managers/LogManager.swift
   0.1%     508   0.1%     508    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/Endpoints/Utilities/GistNetworkRequestError.swift
   0.1%     504   0.1%     504    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Store/ProfileStore.swift
   0.1%     496   0.1%     496    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Background Queue/Type/QueueStatus.swift
   0.0%     484   0.0%     484    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/CustomerIO+Segment.swift
   0.0%     480   0.0%     480    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/RingBuffer.swift
   0.0%     480   0.0%     480    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Utilities/Encodable.swift
   0.0%     468   0.0%     468    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/Endpoints/LogEndpoint.swift
   0.0%     468   0.0%     468    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/Core/Store.swift
   0.0%     416   0.0%     416    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/Utilities/ElapsedTimer.swift
   0.0%     396   0.0%     396    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/Core/StoreSubscriber.swift
   0.0%     380   0.0%     380    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Config/MessagingInAppConfigOptions.swift
   0.0%     236   0.0%     236    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/Util/NotificationCenterWrapper.swift
   0.0%     228   0.0%     228    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/Network/NetworkSettings.swift
   0.0%     204   0.0%     204    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/Lock.swift
   0.0%     200   0.0%     200    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Extensions/LoggerExtensions.swift
   0.0%     196   0.0%     196    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/DeviceMetricsGrabber.swift
   0.0%     192   0.0%     192    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingPush/PushHandling/AutomaticPushClickHandling.swift
   0.0%     164   0.0%     164    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Store/QueueInventoryMemoryStore.swift
   0.0%     144   0.0%     144    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/Aliases.swift
   0.0%     128   0.0%     128    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/Timer.swift
   0.0%     120   0.0%     120    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/DataExtensions.swift
   0.0%     116   0.0%     116    /Users/runner/work/customerio-ios/customerio-ios/Sources/DataPipeline/Plugins/CustomerIODestination.swift
   0.0%      84   0.0%      84    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/State/InAppMessageStore.swift
   0.0%      68   0.0%      68    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/SingleScheduleTimer.swift
   0.0%      64   0.0%      64    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/DateUtil.swift
   0.0%      52   0.0%      52    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Version.swift
   0.0%      48   0.0%      48    /Users/runner/work/customerio-ios/customerio-ios/Sources/MessagingInApp/Gist/EngineWeb/EngineWebProvider.swift
   0.0%      36   0.0%      36    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Util/Time.swift
   0.0%      20   0.0%      20    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Deprecated/CustomerIO+Tracking.swift
   0.0%       8   0.0%       8    /Users/runner/work/customerio-ios/customerio-ios/Sources/Common/Extensions/ErrorExtension.swift
 100.0%   951Ki 100.0%   951Ki    TOTAL
Filtering enabled (source_filter); omitted file = 2.00Mi, vm = 2.09Mi of entries

```

</details>

<details>
<summary>SDK binary size diff report vs. main branch</summary>

```
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  [ = ]       0  [ = ]       0    TOTAL
Filtering enabled (source_filter); omitted file = 1.99Mi, vm = 2.05Mi of entries

```

</details>
author_association: NONE