diff --git a/UnitTests/MParticle+PrivateMethods.h b/UnitTests/MParticle+PrivateMethods.h index 08467987e..a292c5d9a 100644 --- a/UnitTests/MParticle+PrivateMethods.h +++ b/UnitTests/MParticle+PrivateMethods.h @@ -52,6 +52,7 @@ @property (nonatomic, strong, nonnull) id backendController; @property (nonatomic, strong) id appNotificationHandler; +@property (nonatomic, strong) SceneDelegateHandler *sceneDelegateHandler; @property (nonatomic, strong) id settingsProvider; @property (nonatomic, strong, nullable) id dataPlanFilter; @property (nonatomic, strong, nonnull) id listenerController; diff --git a/UnitTests/Mocks/SceneDelegateHandlerMock.swift b/UnitTests/Mocks/SceneDelegateHandlerMock.swift new file mode 100644 index 000000000..d5b76e9f3 --- /dev/null +++ b/UnitTests/Mocks/SceneDelegateHandlerMock.swift @@ -0,0 +1,31 @@ +import XCTest +#if MPARTICLE_LOCATION_DISABLE + import mParticle_Apple_SDK_NoLocation +#else + import mParticle_Apple_SDK +#endif + +class OpenURLHandlerProtocolMock: OpenURLHandlerProtocol { + + var openURLWithOptionsCalled = false + var openURLWithOptionsURLParam: URL? + var openURLWithOptionsOptionsParam: [String : Any]? + + func open(_ url: URL, options: [String : Any]?) { + openURLWithOptionsCalled = true + openURLWithOptionsURLParam = url + openURLWithOptionsOptionsParam = options + } + + var continueUserActivityCalled = false + var continueUserActivityUserActivityParam: NSUserActivity? + var continueUserActivityRestorationHandlerParam: (([UIUserActivityRestoring]?) -> Void)? + var continueUserActivityReturnValue: Bool = false + + func continueUserActivity(_ userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { + continueUserActivityCalled = true + continueUserActivityUserActivityParam = userActivity + continueUserActivityRestorationHandlerParam = restorationHandler + return continueUserActivityReturnValue + } +} diff --git a/UnitTests/ObjCTests/MPKitConfigurationTests.mm b/UnitTests/ObjCTests/MPKitConfigurationTests.mm index 410e07351..bd1a69e67 100644 --- a/UnitTests/ObjCTests/MPKitConfigurationTests.mm +++ b/UnitTests/ObjCTests/MPKitConfigurationTests.mm @@ -81,7 +81,7 @@ - (void)tearDown { - (void)testInstance { XCTAssertNotNil(kitConfiguration); - XCTAssertEqualObjects(kitConfiguration.configurationHash, @(762651950)); + XCTAssertEqualObjects(kitConfiguration.configurationHash, @(969680750)); XCTAssertEqualObjects(kitConfiguration.integrationId, @37); XCTAssertEqualObjects(kitConfiguration.attributeValueFilteringHashedAttribute, @"12345"); XCTAssertEqualObjects(kitConfiguration.attributeValueFilteringHashedValue, @"54321"); @@ -183,7 +183,7 @@ - (void)testInvalidConfiguration { kitConfig = [[MPKitConfiguration alloc] initWithDictionary:configuration]; XCTAssertNotNil(kitConfig); - XCTAssertEqualObjects(kitConfig.configurationHash, @(1495473349)); + XCTAssertEqualObjects(kitConfig.configurationHash, @(-1872513399)); XCTAssertEqualObjects(kitConfig.integrationId, @80); XCTAssertNil(kitConfig.filters); diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift new file mode 100644 index 000000000..002a41967 --- /dev/null +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -0,0 +1,71 @@ + +#if MPARTICLE_LOCATION_DISABLE +import mParticle_Apple_SDK_NoLocation +#else +import mParticle_Apple_SDK +#endif +import XCTest + +final class MParticleSceneDelegateTests: XCTestCase { + + // MARK: - Properties + var mparticle: MParticle! + var sceneMock: OpenURLHandlerProtocolMock! + var testURL: URL! + var testUserActivity: NSUserActivity! + + override func setUp() { + super.setUp() + mparticle = MParticle() + testURL = URL(string: "myapp://test/path?param=value")! + testUserActivity = NSUserActivity(activityType: "com.test.activity") + testUserActivity.title = "Test Activity" + testUserActivity.userInfo = ["key": "value"] + + // The implementation calls [MParticle sharedInstance], so we need to set the mock on the shared instance + sceneMock = OpenURLHandlerProtocolMock() + let sceneHandler = SceneDelegateHandler(logger: MPLog(logLevel: .verbose), appNotificationHandler: sceneMock) + mparticle.sceneDelegateHandler = sceneHandler + } + + // MARK: - handleUserActivity Tests + func test_handleUserActivity_invokesAppNotificationHandler() { + // Act + mparticle.handleUserActivity(testUserActivity) + + // Assert - handleUserActivity directly calls the app notification handler + XCTAssertTrue(sceneMock.continueUserActivityCalled) + XCTAssertEqual(sceneMock.continueUserActivityUserActivityParam, testUserActivity) + XCTAssertNotNil(sceneMock.continueUserActivityRestorationHandlerParam) + } + + func test_handleUserActivity_withWebBrowsingActivity() { + // Arrange + let webActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + webActivity.title = "Web Page" + webActivity.webpageURL = URL(string: "https://example.com/page") + + // Act + mparticle.handleUserActivity(webActivity) + + // Assert - Direct call to app notification handler + XCTAssertTrue(sceneMock.continueUserActivityCalled) + XCTAssertEqual(sceneMock.continueUserActivityUserActivityParam, webActivity) + } + + func test_handleUserActivity_restorationHandlerIsEmpty() { + // Act + mparticle.handleUserActivity(testUserActivity) + + // Assert + XCTAssertTrue(sceneMock.continueUserActivityCalled) + + // Verify the restoration handler is provided and safe to call + let restorationHandler = sceneMock.continueUserActivityRestorationHandlerParam + XCTAssertNotNil(restorationHandler) + + // Test that calling the restoration handler doesn't crash + XCTAssertNoThrow(restorationHandler?(nil)) + XCTAssertNoThrow(restorationHandler?([])) + } +} diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index ce47935b6..747d53a29 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 351308442E6B28F5002A3AD6 /* Executor.m in Sources */ = {isa = PBXBuildFile; fileRef = 351308402E6B28F5002A3AD6 /* Executor.m */; }; 3517794F2E706BF8004BF05E /* ExecutorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3517794C2E706BE4004BF05E /* ExecutorMock.swift */; }; 351779502E706BF8004BF05E /* ExecutorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3517794C2E706BE4004BF05E /* ExecutorMock.swift */; }; + 351F6FC72EDF7B2A00E527A7 /* SceneDelegateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351F6FC62EDF7B2100E527A7 /* SceneDelegateHandler.swift */; }; + 351F6FC82EDF7B2A00E527A7 /* SceneDelegateHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351F6FC62EDF7B2100E527A7 /* SceneDelegateHandler.swift */; }; 35329FE92E54C38C009AC4FD /* MPNetworkOptions+MParticlePrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = 35329FE82E54C38C009AC4FD /* MPNetworkOptions+MParticlePrivate.m */; }; 35329FEA2E54C38C009AC4FD /* MPNetworkOptions+MParticlePrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = 35329FE82E54C38C009AC4FD /* MPNetworkOptions+MParticlePrivate.m */; }; 35329FEC2E54C483009AC4FD /* MPNetworkOptions+MParticlePrivateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35329FEB2E54C480009AC4FD /* MPNetworkOptions+MParticlePrivateTests.swift */; }; @@ -566,6 +568,10 @@ 7E0387842DB913D2003B7D5E /* MPRokt.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E0387802DB913D2003B7D5E /* MPRokt.m */; }; 7E15B2062D94617900C1FF3E /* MPRoktTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E15B2052D94617900C1FF3E /* MPRoktTests.m */; }; 7E15B2072D94617900C1FF3E /* MPRoktTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E15B2052D94617900C1FF3E /* MPRoktTests.m */; }; + 7E573D2A2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */; }; + 7E573D2B2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */; }; + 7E87E0C12EE093E100A18E3A /* SceneDelegateHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */; }; + 7E87E0C22EE093E100A18E3A /* SceneDelegateHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */; }; B31360F92E012760000DFBC9 /* MPRoktEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31360F72E012760000DFBC9 /* MPRoktEvent.swift */; }; B3D778622E02F55F00D887A4 /* MPRoktEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31360F72E012760000DFBC9 /* MPRoktEvent.swift */; }; D30CD0CB2CFF5FB100F5148A /* MPStateMachine.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A79B0629CDFB1F00E7489F /* MPStateMachine.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -632,6 +638,7 @@ 3513083F2E6B28F5002A3AD6 /* Executor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Executor.h; sourceTree = ""; }; 351308402E6B28F5002A3AD6 /* Executor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Executor.m; sourceTree = ""; }; 3517794C2E706BE4004BF05E /* ExecutorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecutorMock.swift; sourceTree = ""; }; + 351F6FC62EDF7B2100E527A7 /* SceneDelegateHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegateHandler.swift; sourceTree = ""; }; 35329FE82E54C38C009AC4FD /* MPNetworkOptions+MParticlePrivate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MPNetworkOptions+MParticlePrivate.m"; sourceTree = ""; }; 35329FEB2E54C480009AC4FD /* MPNetworkOptions+MParticlePrivateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MPNetworkOptions+MParticlePrivateTests.swift"; sourceTree = ""; }; 35329FEE2E54CA49009AC4FD /* MParticleOptions+MParticlePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MParticleOptions+MParticlePrivate.h"; sourceTree = ""; }; @@ -923,6 +930,8 @@ 7E03877F2DB913D2003B7D5E /* MPRokt.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPRokt.h; sourceTree = ""; }; 7E0387802DB913D2003B7D5E /* MPRokt.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPRokt.m; sourceTree = ""; }; 7E15B2052D94617900C1FF3E /* MPRoktTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPRoktTests.m; sourceTree = ""; }; + 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MParticleSceneDelegateTests.swift; sourceTree = ""; }; + 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegateHandlerMock.swift; path = UnitTests/Mocks/SceneDelegateHandlerMock.swift; sourceTree = ""; }; B31360F72E012760000DFBC9 /* MPRoktEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPRoktEvent.swift; sourceTree = ""; }; D33C8B312B8510C20012EDFD /* MPAudience.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPAudience.h; sourceTree = ""; }; D33C8B322B8510C20012EDFD /* MPAudience.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPAudience.m; sourceTree = ""; }; @@ -1005,6 +1014,7 @@ 53A79A6F29CCCD6400E7489F = { isa = PBXGroup; children = ( + 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */, D3BA75152B614E3D008C3C65 /* PrivacyInfo.xcprivacy */, 53A79CFE29CE12AB00E7489F /* mParticle-Apple-SDK.modulemap */, 53A79CFF29CE23D600E7489F /* mParticle-Apple-SDK-NoLocation.modulemap */, @@ -1029,6 +1039,7 @@ 53A79A7B29CCCD6400E7489F /* mParticle-Apple-SDK */ = { isa = PBXGroup; children = ( + 351F6FC62EDF7B2100E527A7 /* SceneDelegateHandler.swift */, 3513083F2E6B28F5002A3AD6 /* Executor.h */, 729721752EAC2AD60045E55C /* AppEnvironmentProvider.h */, 729721782EAC2B750045E55C /* AppEnvironmentProvider.m */, @@ -1504,6 +1515,7 @@ isa = PBXGroup; children = ( 7231B8342EB95F9F001565E5 /* MParticleBreadcrumbTests.swift */, + 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */, 7231B83E2EB9627D001565E5 /* MParticleErrorTests.swift */, 7231B83B2EB961F2001565E5 /* MParticleLTVTests.swift */, 7231B84B2EB963B3001565E5 /* MParticleKitBatchTests.swift */, @@ -1910,6 +1922,7 @@ 534CD26929CE2CE1008452B3 /* MPSurrogateAppDelegateTests.m in Sources */, 534CD26A29CE2CE1008452B3 /* MPGDPRConsentTests.m in Sources */, 356D4A5A2E58B09D00CB69FE /* SettingsProviderMock.swift in Sources */, + 7E573D2A2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */, 534CD26B29CE2CE1008452B3 /* MPBackendControllerTests.m in Sources */, 534CD26C29CE2CE1008452B3 /* MPKitConfigurationTests.mm in Sources */, 7231B80E2EB3C4AC001565E5 /* MPKitMock.swift in Sources */, @@ -1939,6 +1952,7 @@ 534CD27B29CE2CE1008452B3 /* MPZipTests.m in Sources */, 7231B8522EB964A1001565E5 /* MParticleOpenURLTests.swift in Sources */, 53E20DC82CBFFCD200146A97 /* NSArray+MPCaseInsensitiveTests.swift in Sources */, + 7E87E0C22EE093E100A18E3A /* SceneDelegateHandlerMock.swift in Sources */, 534CD27C29CE2CE1008452B3 /* MPIntegrationAttributesTest.m in Sources */, 356752932E60928B00DEEE23 /* MPStateMachineMock.swift in Sources */, 534CD27D29CE2CE1008452B3 /* MPDataModelTests.m in Sources */, @@ -2102,6 +2116,7 @@ 53FDD1BD2AE871AF003D5FA1 /* MPIHasher.swift in Sources */, D3DE316B2D5261FC00CC537F /* MPDevice.swift in Sources */, 53A79BBD29CDFB2000E7489F /* MPBackendController.m in Sources */, + 351F6FC72EDF7B2A00E527A7 /* SceneDelegateHandler.swift in Sources */, 53A79C1B29CDFB2100E7489F /* MPForwardQueueItem.m in Sources */, 53A79C1629CDFB2100E7489F /* MPEventProjection.mm in Sources */, D3CEDAC32C9DAC25001B32DF /* MPDateFormatter.swift in Sources */, @@ -2129,6 +2144,7 @@ 53A79CD129CE019F00E7489F /* MPSurrogateAppDelegateTests.m in Sources */, 53A79CE729CE019F00E7489F /* MPGDPRConsentTests.m in Sources */, 356D4A592E58B09D00CB69FE /* SettingsProviderMock.swift in Sources */, + 7E573D2B2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */, 53A79CD229CE019F00E7489F /* MPBackendControllerTests.m in Sources */, 53A79CF329CE019F00E7489F /* MPKitConfigurationTests.mm in Sources */, 7231B80D2EB3C4AC001565E5 /* MPKitMock.swift in Sources */, @@ -2158,6 +2174,7 @@ 53E20DC72CBFFCD200146A97 /* NSArray+MPCaseInsensitiveTests.swift in Sources */, 7231B8532EB964A1001565E5 /* MParticleOpenURLTests.swift in Sources */, 53A79CEC29CE019F00E7489F /* MPZipTests.m in Sources */, + 7E87E0C12EE093E100A18E3A /* SceneDelegateHandlerMock.swift in Sources */, 356752942E60928B00DEEE23 /* MPStateMachineMock.swift in Sources */, 53A79CCC29CE019F00E7489F /* MPIntegrationAttributesTest.m in Sources */, 53A79CBD29CE019F00E7489F /* MPDataModelTests.m in Sources */, @@ -2321,6 +2338,7 @@ 53A79DB429CE23F700E7489F /* MPDataModelAbstract.m in Sources */, D3DE316C2D5261FC00CC537F /* MPDevice.swift in Sources */, 53FDD1BE2AE871AF003D5FA1 /* MPIHasher.swift in Sources */, + 351F6FC82EDF7B2A00E527A7 /* SceneDelegateHandler.swift in Sources */, 53A79DB529CE23F700E7489F /* MPBackendController.m in Sources */, 53A79DB629CE23F700E7489F /* MPForwardQueueItem.m in Sources */, 53A79DB729CE23F700E7489F /* MPEventProjection.mm in Sources */, @@ -2362,6 +2380,7 @@ "$(inherited)", ); INFOPLIST_FILE = ./UnitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDK-NoLocationTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2374,6 +2393,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Debug; }; @@ -2390,6 +2410,7 @@ "$(inherited)", ); INFOPLIST_FILE = ./UnitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDK-NoLocationTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2402,6 +2423,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Release; }; @@ -2619,6 +2641,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = DLD43Y3TRP; INFOPLIST_FILE = ./UnitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDKTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2630,6 +2653,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Debug; }; @@ -2642,6 +2666,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = DLD43Y3TRP; INFOPLIST_FILE = ./UnitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDKTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2653,6 +2678,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 15.6; }; name = Release; }; diff --git a/mParticle-Apple-SDK/Include/mParticle.h b/mParticle-Apple-SDK/Include/mParticle.h index 344b18d96..74f7907de 100644 --- a/mParticle-Apple-SDK/Include/mParticle.h +++ b/mParticle-Apple-SDK/Include/mParticle.h @@ -754,6 +754,9 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp #endif /** + DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:open:sourceapplication:annotation:) + Use a UIScene lifecycle, mParticle's handleURLContext: method, and scene(_:openURLContexts:) from UISceneDelegate instead. + Informs the mParticle SDK the app has been asked to open a resource identified by a URL. This method should be called only if proxiedAppDelegate is disabled. @param url The URL resource to open @@ -761,25 +764,49 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp @param annotation A property list object supplied by the source app @see proxiedAppDelegate */ -- (void)openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nullable id)annotation; +- (void)openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nullable id)annotation DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method"); /** + DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:open:sourceapplication:annotation:) + Use a UIScene lifecycle, mParticle's handleURLContext: method, and scene(_:openURLContexts:) from UISceneDelegate instead. + Informs the mParticle SDK the app has been asked to open a resource identified by a URL. This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 9 and above. @param url The URL resource to open @param options The dictionary of launch options @see proxiedAppDelegate */ -- (void)openURL:(NSURL *)url options:(nullable NSDictionary *)options; +- (void)openURL:(NSURL *)url options:(nullable NSDictionary *)options DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method"); /** + DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:continue:restorationhandler:) + Use UIScene lifecycle, mParticle's handleUserActivity: method, and scene(_:continue:) from UISceneDelegate instead. + Informs the mParticle SDK the app has been asked to open to continue an NSUserActivity. This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 9 and above. @param userActivity The NSUserActivity that caused the app to be opened @param restorationHandler A block to execute if your app creates objects to perform the task. @see proxiedAppDelegate */ -- (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void(^ _Nonnull)(NSArray> * __nullable restorableObjects))restorationHandler; +- (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void(^ _Nonnull)(NSArray> * __nullable restorableObjects))restorationHandler DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method"); + +#if TARGET_OS_IOS == 1 +/** + Informs the mParticle SDK the app has been asked to open a resource identified by a URL. + This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 13 and above. + @param urlContext The UIOpenURLContext provided by the SceneDelegate + @see proxiedAppDelegate + */ +- (void)handleURLContext:(UIOpenURLContext *)urlContext NS_SWIFT_NAME(handleURLContext(_:)) API_AVAILABLE(ios(13.0)); +#endif + +/** + Informs the mParticle SDK the app has been asked to open to continue an NSUserActivity. + This method should be called only if proxiedAppDelegate is disabled. + @param userActivity The NSUserActivity that caused the app to be opened + @see proxiedAppDelegate + */ +- (void)handleUserActivity:(NSUserActivity *)userActivity NS_SWIFT_NAME(handleUserActivity(_:)); /** DEPRECATED: This method will permanently remove ALL MParticle data from the device, including MParticle UserDefaults and Database, it will also halt any further upload or download behavior that may be prepared diff --git a/mParticle-Apple-SDK/Kits/MPKitConfiguration.mm b/mParticle-Apple-SDK/Kits/MPKitConfiguration.mm index a3050d972..51b3dd7a5 100644 --- a/mParticle-Apple-SDK/Kits/MPKitConfiguration.mm +++ b/mParticle-Apple-SDK/Kits/MPKitConfiguration.mm @@ -23,7 +23,11 @@ - (instancetype)initWithDictionary:(NSDictionary *)configurationDictionary { return nil; } - NSData *ekConfigData = [NSJSONSerialization dataWithJSONObject:configurationDictionary options:0 error:nil]; + NSJSONWritingOptions options = 0; + if (@available(iOS 11.0, tvOS 11.0, *)) { + options = NSJSONWritingSortedKeys; + } + NSData *ekConfigData = [NSJSONSerialization dataWithJSONObject:configurationDictionary options:options error:nil]; NSString *ekConfigString = [[NSString alloc] initWithData:ekConfigData encoding:NSUTF8StringEncoding]; _configurationHash = @([[MPIHasher hashString:ekConfigString] intValue]); @@ -114,16 +118,11 @@ - (id)initWithCoder:(NSCoder *)coder { @try { configurationDictionary = [coder decodeObjectOfClass:[NSDictionary class] forKey:@"configurationDictionary"]; } - @catch ( NSException *e) { configurationDictionary = nil; MPILogError(@"Exception decoding MPKitConfiguration Attributes: %@", [e reason]); } - @finally { - self = [self initWithDictionary:configurationDictionary]; - } - self = [self initWithDictionary:configurationDictionary]; if (!self) { return nil; diff --git a/mParticle-Apple-SDK/SceneDelegateHandler.swift b/mParticle-Apple-SDK/SceneDelegateHandler.swift new file mode 100644 index 000000000..1d8c3325c --- /dev/null +++ b/mParticle-Apple-SDK/SceneDelegateHandler.swift @@ -0,0 +1,55 @@ +import Foundation + +@objc +public protocol OpenURLHandlerProtocol { + func open(_ url: URL, options: [String: Any]?) + func continueUserActivity( + _ userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool +} + +@objcMembers +public class SceneDelegateHandler: NSObject { + private let logger: MPLog + private let appNotificationHandler: OpenURLHandlerProtocol + + public init(logger: MPLog, appNotificationHandler: OpenURLHandlerProtocol) { + self.logger = logger + self.appNotificationHandler = appNotificationHandler + } + + #if os(iOS) + @available(iOS 13.0, *) + @available(iOSApplicationExtension 13.0, *) + public func handle(urlContext: UIOpenURLContext) { + logger.debug("Opening URLContext URL: \(urlContext.url)") + logger.debug("Source: \(String(describing: urlContext.options.sourceApplication ?? "unknown"))") + logger.debug("Annotation: \(String(describing: urlContext.options.annotation))") + if #available(iOS 14.5, *) { + logger.debug("Event Attribution: \(String(describing: urlContext.options.eventAttribution))") + } + logger.debug("Open in place: \(urlContext.options.openInPlace ? "True" : "False")") + + let options = ["UIApplicationOpenURLOptionsSourceApplicationKey": urlContext.options.sourceApplication]; + + self.appNotificationHandler.open(urlContext.url, options: options as [String: Any]) + } + #endif + + public func handleUserActivity(_ userActivity: NSUserActivity) { + logger.debug("User Activity Received") + logger.debug("User Activity Type: \(userActivity.activityType)") + logger.debug("User Activity Title: \(userActivity.title ?? "")") + logger.debug("User Activity User Info: \(userActivity.userInfo ?? [:])") + + if userActivity.activityType == NSUserActivityTypeBrowsingWeb { + logger.debug("Opening UserActivity URL: \(userActivity.webpageURL?.absoluteString ?? "")") + } + + _ = appNotificationHandler.continueUserActivity( + userActivity, + restorationHandler: { _ in } + ) + } +} diff --git a/mParticle-Apple-SDK/mParticle.m b/mParticle-Apple-SDK/mParticle.m index 1e1fd9d78..73add0d1f 100644 --- a/mParticle-Apple-SDK/mParticle.m +++ b/mParticle-Apple-SDK/mParticle.m @@ -54,7 +54,8 @@ @interface MParticle() stateMachine; @property (nonatomic, strong) MPKitContainer_PRIVATE *kitContainer_PRIVATE; @property (nonatomic, strong) id kitContainer; -@property (nonatomic, strong) id appNotificationHandler; +@property (nonatomic, strong) id appNotificationHandler; +@property (nonatomic, strong) SceneDelegateHandler *sceneDelegateHandler; @property (nonatomic, strong, nonnull) id backendController; @property (nonatomic, strong, nonnull) MParticleOptions *options; @property (nonatomic, strong, nullable) MPKitActivity *kitActivity; @@ -98,6 +99,7 @@ @implementation MParticle @synthesize listenerController = _listenerController; static id executor; MPLog* logger; +@synthesize sceneDelegateHandler = _sceneDelegateHandler; + (void)initialize { if (self == [MParticle class]) { @@ -149,13 +151,14 @@ - (instancetype)init { _collectSearchAdsAttribution = NO; _trackNotifications = YES; _automaticSessionTracking = YES; - _appNotificationHandler = [[MPAppNotificationHandler alloc] init]; + _appNotificationHandler = (id)[[MPAppNotificationHandler alloc] init]; _stateMachine = [[MPStateMachine_PRIVATE alloc] init]; _webView = [[MParticleWebView_PRIVATE alloc] initWithMessageQueue:executor.messageQueue]; _listenerController = MPListenerController.sharedInstance; _appEnvironmentProvider = [[AppEnvironmentProvider alloc] init]; _notificationController = [[MPNotificationController_PRIVATE alloc] init]; logger = [[MPLog alloc] initWithLogLevel:_stateMachine.logLevel]; + _sceneDelegateHandler = [[SceneDelegateHandler alloc] initWithLogger:logger appNotificationHandler:_appNotificationHandler]; return self; } @@ -697,6 +700,16 @@ - (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationH return [self.appNotificationHandler continueUserActivity:userActivity restorationHandler:restorationHandler]; } +#if TARGET_OS_IOS == 1 +- (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)) { + [self.sceneDelegateHandler handleWithUrlContext:urlContext]; +} +#endif + +- (void)handleUserActivity:(NSUserActivity *)userActivity { + [self.sceneDelegateHandler handleUserActivity:userActivity]; +} + - (void)reset:(void (^)(void))completion { [executor executeOnMessage:^{ [self.kitContainer flushSerializedKits];