From 775315ca8d8dcf62c813dbfae70ee1043733f115 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Mon, 17 Nov 2025 08:41:42 -0500 Subject: [PATCH 1/8] feat: Implement Manual SceneDelegate Support --- .../MParticleSceneDelegateTests.swift | 123 ++++++++++++++++++ mParticle-Apple-SDK.xcodeproj/project.pbxproj | 6 + mParticle-Apple-SDK/Include/mParticle.h | 28 +++- mParticle-Apple-SDK/mParticle.m | 40 ++++++ 4 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift new file mode 100644 index 000000000..80039fa51 --- /dev/null +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -0,0 +1,123 @@ +// +// MParticleSceneDelegateTests.swift +// mParticle-Apple-SDK +// +// Created by Brandon Stalnaker on 11/13/25. +// + +#if MPARTICLE_LOCATION_DISABLE +import mParticle_Apple_SDK_NoLocation +#else +import mParticle_Apple_SDK +#endif +import XCTest + +@available(iOS 13.0, *) +final class MParticleSceneDelegateTests: MParticleTestBase { + + // MARK: - Properties + + var testURL: URL! + var testUserActivity: NSUserActivity! + + override func setUp() { + super.setUp() + 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 + MParticle.sharedInstance().appNotificationHandler = appNotificationHandler + + // Reset mock state for each test + appNotificationHandler.continueUserActivityCalled = false + appNotificationHandler.continueUserActivityUserActivityParam = nil + appNotificationHandler.continueUserActivityRestorationHandlerParam = nil + appNotificationHandler.openURLWithOptionsCalled = false + appNotificationHandler.openURLWithOptionsURLParam = nil + appNotificationHandler.openURLWithOptionsOptionsParam = nil + } + + // MARK: - Method Existence Tests + + func test_handleURLContext_methodExists() { + // Verify the method exists with correct signature + XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) + } + + func test_handleUserActivity_methodExists() { + // Verify the method exists with correct signature + XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) + } + + // MARK: - handleURLContext Tests + // Note: Testing with mock URLContext is complex due to system class limitations + // These tests focus on method availability and basic functionality + + func test_handleURLContext_availabilityCheck() { + // Verify the method is only available on iOS 13+ + if #available(iOS 13.0, *) { + XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) + } else { + XCTAssertFalse(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) + } + } + + // MARK: - handleUserActivity Tests + + func test_handleUserActivity_invokesAppNotificationHandler() { + // Act + mparticle.handleUserActivity(testUserActivity) + + // Assert - handleUserActivity directly calls the app notification handler + XCTAssertTrue(appNotificationHandler.continueUserActivityCalled) + XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, testUserActivity) + XCTAssertNotNil(appNotificationHandler.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(appNotificationHandler.continueUserActivityCalled) + XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, webActivity) + } + + func test_handleUserActivity_restorationHandlerIsEmpty() { + // Act + mparticle.handleUserActivity(testUserActivity) + + // Assert + XCTAssertTrue(appNotificationHandler.continueUserActivityCalled) + + // Verify the restoration handler is provided and safe to call + let restorationHandler = appNotificationHandler.continueUserActivityRestorationHandlerParam + XCTAssertNotNil(restorationHandler) + + // Test that calling the restoration handler doesn't crash + XCTAssertNoThrow(restorationHandler?(nil)) + XCTAssertNoThrow(restorationHandler?([])) + } + + // MARK: - iOS Version Availability Tests + + func test_handleUserActivity_alwaysAvailable() { + // handleUserActivity should be available on all iOS versions + XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) + } + + // MARK: - Integration Tests + + func test_bothMethods_exist() { + // Verify both SceneDelegate support methods exist + XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) + XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) + } +} diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index ce47935b6..be89327c8 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -566,6 +566,8 @@ 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 */; }; 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, ); }; }; @@ -923,6 +925,7 @@ 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 = ""; }; 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 = ""; }; @@ -1504,6 +1507,7 @@ isa = PBXGroup; children = ( 7231B8342EB95F9F001565E5 /* MParticleBreadcrumbTests.swift */, + 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */, 7231B83E2EB9627D001565E5 /* MParticleErrorTests.swift */, 7231B83B2EB961F2001565E5 /* MParticleLTVTests.swift */, 7231B84B2EB963B3001565E5 /* MParticleKitBatchTests.swift */, @@ -1910,6 +1914,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 */, @@ -2129,6 +2134,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 */, diff --git a/mParticle-Apple-SDK/Include/mParticle.h b/mParticle-Apple-SDK/Include/mParticle.h index 344b18d96..e933ba69c 100644 --- a/mParticle-Apple-SDK/Include/mParticle.h +++ b/mParticle-Apple-SDK/Include/mParticle.h @@ -754,6 +754,8 @@ 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:) + 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 +763,45 @@ 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:) + 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:) + 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"); + +/** + 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)); + +/** + 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/mParticle.m b/mParticle-Apple-SDK/mParticle.m index 1e1fd9d78..9998f4547 100644 --- a/mParticle-Apple-SDK/mParticle.m +++ b/mParticle-Apple-SDK/mParticle.m @@ -697,6 +697,46 @@ - (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationH return [self.appNotificationHandler continueUserActivity:userActivity restorationHandler:restorationHandler]; } +- (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)){ + NSString *messageURL = [NSString stringWithFormat:@"Opening URLContext URL: %@", urlContext.URL]; + [logger debug:messageURL]; + NSString *messageSource = [NSString stringWithFormat:@"Source: %@", urlContext.options.sourceApplication ? urlContext.options.sourceApplication : @"unknown"]; + [logger debug:messageSource]; + NSString *messageAnnotation = [NSString stringWithFormat:@"Annotation: %@", urlContext.options.annotation]; + [logger debug:messageAnnotation]; +#if TARGET_OS_IOS == 1 + if (@available(iOS 14.5, *)) { + NSString *messageEventAttribution = [NSString stringWithFormat:@"Event Attribution: %@", urlContext.options.eventAttribution]; + [logger debug:messageEventAttribution]; + } +#endif + NSString *messageOpenInPlace = [NSString stringWithFormat:@"Open in place: %@", urlContext.options.openInPlace ? @"True" : @"False"]; + [logger debug:messageOpenInPlace]; + + // Currently only one kit integration uses this dictionary for this key + // https://github.com/mparticle-integrations/mparticle-apple-integration-flurry/blob/a0856e271aa9a63a6668805582395dea63f96af5/mParticle-Flurry/MPKitFlurry.m#L148C38-L148C85 + NSDictionary *options = @{@"UIApplicationOpenURLOptionsSourceApplicationKey": urlContext.options.sourceApplication}; + + [[MParticle sharedInstance].appNotificationHandler openURL:urlContext.URL options:options]; +} + +- (void)handleUserActivity:(NSUserActivity *)userActivity { + NSString *message = [NSString stringWithFormat:@"User Activity Received"]; + [logger debug:message]; + NSString *messageType = [NSString stringWithFormat:@"User Activity Type: %@", userActivity.activityType]; + [logger debug:messageType]; + NSString *messageTitle = [NSString stringWithFormat:@"User Activity Title: %@", userActivity.title]; + [logger debug:messageTitle]; + NSString *messageUserInfo = [NSString stringWithFormat:@"User Activity User Info: %@", userActivity.userInfo]; + [logger debug:messageUserInfo]; + if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) { + NSString *messageURL = [NSString stringWithFormat:@"Opening UserActivity URL: %@", userActivity.webpageURL]; + [logger debug:messageURL]; + } + // When provided by the SceneDelegate NSUserActivity is not paired with a restorationHandler + [[MParticle sharedInstance].appNotificationHandler continueUserActivity: userActivity restorationHandler:^(NSArray> * _Nullable restorableObjects) {}]; +} + - (void)reset:(void (^)(void))completion { [executor executeOnMessage:^{ [self.kitContainer flushSerializedKits]; From 066f11f4fc44f90410a768e2b623719c733dbc92 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Mon, 17 Nov 2025 11:35:41 -0500 Subject: [PATCH 2/8] test fixes --- .../MParticleSceneDelegateTests.swift | 18 ++++-------------- mParticle-Apple-SDK.xcodeproj/project.pbxproj | 8 ++++++++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift index 80039fa51..66cbf7946 100644 --- a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -41,6 +41,7 @@ final class MParticleSceneDelegateTests: MParticleTestBase { // MARK: - Method Existence Tests + @available(tvOS 13.0, *) func test_handleURLContext_methodExists() { // Verify the method exists with correct signature XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) @@ -51,19 +52,6 @@ final class MParticleSceneDelegateTests: MParticleTestBase { XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) } - // MARK: - handleURLContext Tests - // Note: Testing with mock URLContext is complex due to system class limitations - // These tests focus on method availability and basic functionality - - func test_handleURLContext_availabilityCheck() { - // Verify the method is only available on iOS 13+ - if #available(iOS 13.0, *) { - XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) - } else { - XCTAssertFalse(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) - } - } - // MARK: - handleUserActivity Tests func test_handleUserActivity_invokesAppNotificationHandler() { @@ -117,7 +105,9 @@ final class MParticleSceneDelegateTests: MParticleTestBase { func test_bothMethods_exist() { // Verify both SceneDelegate support methods exist - XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) + if #available(tvOS 13.0, *) { + XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) + } XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) } } diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index be89327c8..95a14fe2c 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -2368,6 +2368,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)"; @@ -2380,6 +2381,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; }; @@ -2396,6 +2398,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)"; @@ -2408,6 +2411,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; }; @@ -2625,6 +2629,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)"; @@ -2636,6 +2641,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; }; @@ -2648,6 +2654,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)"; @@ -2659,6 +2666,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; }; From e1ba42d050a29a446cce8d51398621904530bf92 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Mon, 17 Nov 2025 13:59:21 -0500 Subject: [PATCH 3/8] MPKitConfigurationTest fix --- UnitTests/ObjCTests/MPKitConfigurationTests.mm | 4 ++-- mParticle-Apple-SDK/Kits/MPKitConfiguration.mm | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) 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/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; From 225013107b0d908645d205c8691493dc75086f68 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Wed, 19 Nov 2025 08:58:51 -0500 Subject: [PATCH 4/8] Address Dennis comments --- .../MParticleSceneDelegateTests.swift | 38 +------------------ mParticle-Apple-SDK/Include/mParticle.h | 3 ++ mParticle-Apple-SDK/mParticle.m | 4 +- 3 files changed, 6 insertions(+), 39 deletions(-) diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift index 66cbf7946..9d74a62f7 100644 --- a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -1,9 +1,3 @@ -// -// MParticleSceneDelegateTests.swift -// mParticle-Apple-SDK -// -// Created by Brandon Stalnaker on 11/13/25. -// #if MPARTICLE_LOCATION_DISABLE import mParticle_Apple_SDK_NoLocation @@ -38,20 +32,7 @@ final class MParticleSceneDelegateTests: MParticleTestBase { appNotificationHandler.openURLWithOptionsURLParam = nil appNotificationHandler.openURLWithOptionsOptionsParam = nil } - - // MARK: - Method Existence Tests - - @available(tvOS 13.0, *) - func test_handleURLContext_methodExists() { - // Verify the method exists with correct signature - XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) - } - - func test_handleUserActivity_methodExists() { - // Verify the method exists with correct signature - XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) - } - + // MARK: - handleUserActivity Tests func test_handleUserActivity_invokesAppNotificationHandler() { @@ -93,21 +74,4 @@ final class MParticleSceneDelegateTests: MParticleTestBase { XCTAssertNoThrow(restorationHandler?(nil)) XCTAssertNoThrow(restorationHandler?([])) } - - // MARK: - iOS Version Availability Tests - - func test_handleUserActivity_alwaysAvailable() { - // handleUserActivity should be available on all iOS versions - XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) - } - - // MARK: - Integration Tests - - func test_bothMethods_exist() { - // Verify both SceneDelegate support methods exist - if #available(tvOS 13.0, *) { - XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:)))) - } - XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:)))) - } } diff --git a/mParticle-Apple-SDK/Include/mParticle.h b/mParticle-Apple-SDK/Include/mParticle.h index e933ba69c..cd6685da5 100644 --- a/mParticle-Apple-SDK/Include/mParticle.h +++ b/mParticle-Apple-SDK/Include/mParticle.h @@ -755,6 +755,7 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp /** 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. @@ -767,6 +768,7 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp /** 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. @@ -778,6 +780,7 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp /** 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. diff --git a/mParticle-Apple-SDK/mParticle.m b/mParticle-Apple-SDK/mParticle.m index 9998f4547..232d6857d 100644 --- a/mParticle-Apple-SDK/mParticle.m +++ b/mParticle-Apple-SDK/mParticle.m @@ -717,7 +717,7 @@ - (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)) // https://github.com/mparticle-integrations/mparticle-apple-integration-flurry/blob/a0856e271aa9a63a6668805582395dea63f96af5/mParticle-Flurry/MPKitFlurry.m#L148C38-L148C85 NSDictionary *options = @{@"UIApplicationOpenURLOptionsSourceApplicationKey": urlContext.options.sourceApplication}; - [[MParticle sharedInstance].appNotificationHandler openURL:urlContext.URL options:options]; + [self.appNotificationHandler openURL:urlContext.URL options:options]; } - (void)handleUserActivity:(NSUserActivity *)userActivity { @@ -734,7 +734,7 @@ - (void)handleUserActivity:(NSUserActivity *)userActivity { [logger debug:messageURL]; } // When provided by the SceneDelegate NSUserActivity is not paired with a restorationHandler - [[MParticle sharedInstance].appNotificationHandler continueUserActivity: userActivity restorationHandler:^(NSArray> * _Nullable restorableObjects) {}]; + [self.appNotificationHandler continueUserActivity: userActivity restorationHandler:^(NSArray> * _Nullable restorableObjects) {}]; } - (void)reset:(void (^)(void))completion { From a7fc201573be88258dcb7d702d165f3464b98cc1 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Wed, 19 Nov 2025 15:54:01 -0500 Subject: [PATCH 5/8] final comment --- UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift index 9d74a62f7..4a36185f7 100644 --- a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -6,7 +6,6 @@ import mParticle_Apple_SDK #endif import XCTest -@available(iOS 13.0, *) final class MParticleSceneDelegateTests: MParticleTestBase { // MARK: - Properties From b8326ee081246217ac3bb8607a8d509f7c501e46 Mon Sep 17 00:00:00 2001 From: Denis Chilik Date: Wed, 3 Dec 2025 09:34:33 -0500 Subject: [PATCH 6/8] - example --- mParticle-Apple-SDK.xcodeproj/project.pbxproj | 6 ++ .../SceneDelegateHandler.swift | 61 +++++++++++++++++++ mParticle-Apple-SDK/mParticle.m | 42 +++---------- 3 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 mParticle-Apple-SDK/SceneDelegateHandler.swift diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index 95a14fe2c..e7a250e2c 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 */; }; @@ -634,6 +636,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 = ""; }; @@ -1032,6 +1035,7 @@ 53A79A7B29CCCD6400E7489F /* mParticle-Apple-SDK */ = { isa = PBXGroup; children = ( + 351F6FC62EDF7B2100E527A7 /* SceneDelegateHandler.swift */, 3513083F2E6B28F5002A3AD6 /* Executor.h */, 729721752EAC2AD60045E55C /* AppEnvironmentProvider.h */, 729721782EAC2B750045E55C /* AppEnvironmentProvider.m */, @@ -2107,6 +2111,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 */, @@ -2327,6 +2332,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 */, diff --git a/mParticle-Apple-SDK/SceneDelegateHandler.swift b/mParticle-Apple-SDK/SceneDelegateHandler.swift new file mode 100644 index 000000000..fdead6e9b --- /dev/null +++ b/mParticle-Apple-SDK/SceneDelegateHandler.swift @@ -0,0 +1,61 @@ +// +// SceneDelegateHandler.swift +// mParticle-Apple-SDK +// +// Created by Denis Chilik on 12/2/25. +// + +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 + } + + @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 os(iOS) + if #available(iOS 14.5, *) { + logger.debug("Event Attribution: \(String(describing: urlContext.options.eventAttribution))") + } + #endif + 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]) + } + + public func continueUserActivity(_ 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 232d6857d..566ece016 100644 --- a/mParticle-Apple-SDK/mParticle.m +++ b/mParticle-Apple-SDK/mParticle.m @@ -54,7 +54,7 @@ @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, nonnull) id backendController; @property (nonatomic, strong, nonnull) MParticleOptions *options; @property (nonatomic, strong, nullable) MPKitActivity *kitActivity; @@ -697,44 +697,16 @@ - (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationH return [self.appNotificationHandler continueUserActivity:userActivity restorationHandler:restorationHandler]; } -- (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)){ - NSString *messageURL = [NSString stringWithFormat:@"Opening URLContext URL: %@", urlContext.URL]; - [logger debug:messageURL]; - NSString *messageSource = [NSString stringWithFormat:@"Source: %@", urlContext.options.sourceApplication ? urlContext.options.sourceApplication : @"unknown"]; - [logger debug:messageSource]; - NSString *messageAnnotation = [NSString stringWithFormat:@"Annotation: %@", urlContext.options.annotation]; - [logger debug:messageAnnotation]; -#if TARGET_OS_IOS == 1 - if (@available(iOS 14.5, *)) { - NSString *messageEventAttribution = [NSString stringWithFormat:@"Event Attribution: %@", urlContext.options.eventAttribution]; - [logger debug:messageEventAttribution]; - } -#endif - NSString *messageOpenInPlace = [NSString stringWithFormat:@"Open in place: %@", urlContext.options.openInPlace ? @"True" : @"False"]; - [logger debug:messageOpenInPlace]; - // Currently only one kit integration uses this dictionary for this key - // https://github.com/mparticle-integrations/mparticle-apple-integration-flurry/blob/a0856e271aa9a63a6668805582395dea63f96af5/mParticle-Flurry/MPKitFlurry.m#L148C38-L148C85 - NSDictionary *options = @{@"UIApplicationOpenURLOptionsSourceApplicationKey": urlContext.options.sourceApplication}; - - [self.appNotificationHandler openURL:urlContext.URL options:options]; + + +- (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)) { + MPartiche.shredInstanse.hsceneDelegareHandler.andleURLContext + [handler handleWithUrlContext:urlContext]; } - (void)handleUserActivity:(NSUserActivity *)userActivity { - NSString *message = [NSString stringWithFormat:@"User Activity Received"]; - [logger debug:message]; - NSString *messageType = [NSString stringWithFormat:@"User Activity Type: %@", userActivity.activityType]; - [logger debug:messageType]; - NSString *messageTitle = [NSString stringWithFormat:@"User Activity Title: %@", userActivity.title]; - [logger debug:messageTitle]; - NSString *messageUserInfo = [NSString stringWithFormat:@"User Activity User Info: %@", userActivity.userInfo]; - [logger debug:messageUserInfo]; - if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) { - NSString *messageURL = [NSString stringWithFormat:@"Opening UserActivity URL: %@", userActivity.webpageURL]; - [logger debug:messageURL]; - } - // When provided by the SceneDelegate NSUserActivity is not paired with a restorationHandler - [self.appNotificationHandler continueUserActivity: userActivity restorationHandler:^(NSArray> * _Nullable restorableObjects) {}]; + [handler handleUserActivity:userActivity]; } - (void)reset:(void (^)(void))completion { From b4767f1db4d4bf1141b9857eb18771e52c8f1235 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Wed, 3 Dec 2025 14:06:45 -0500 Subject: [PATCH 7/8] move new code to swift --- UnitTests/MParticle+PrivateMethods.h | 1 + .../Mocks/SceneDelegateHandlerMock.swift | 31 ++++++++++++++++ .../MParticleSceneDelegateTests.swift | 35 ++++++++----------- mParticle-Apple-SDK.xcodeproj/project.pbxproj | 6 ++++ mParticle-Apple-SDK/Include/mParticle.h | 2 ++ .../SceneDelegateHandler.swift | 20 ++++------- mParticle-Apple-SDK/mParticle.m | 14 +++++--- 7 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 UnitTests/Mocks/SceneDelegateHandlerMock.swift 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..ca4ad9665 --- /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 SceneDelegateHandlerMock: 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/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift index 4a36185f7..626edcfc4 100644 --- a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -6,42 +6,37 @@ import mParticle_Apple_SDK #endif import XCTest -final class MParticleSceneDelegateTests: MParticleTestBase { +final class MParticleSceneDelegateTests: XCTestCase { // MARK: - Properties - + var mparticle: MParticle! + var sceneMock: SceneDelegateHandlerMock! 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 - MParticle.sharedInstance().appNotificationHandler = appNotificationHandler - - // Reset mock state for each test - appNotificationHandler.continueUserActivityCalled = false - appNotificationHandler.continueUserActivityUserActivityParam = nil - appNotificationHandler.continueUserActivityRestorationHandlerParam = nil - appNotificationHandler.openURLWithOptionsCalled = false - appNotificationHandler.openURLWithOptionsURLParam = nil - appNotificationHandler.openURLWithOptionsOptionsParam = nil + sceneMock = SceneDelegateHandlerMock() + let sceneHandler = SceneDelegateHandler(logger: MPLog(logLevel: .verbose), appNotificationHandler: sceneMock) + mparticle.sceneDelegateHandler = sceneHandler } - // MARK: - handleUserActivity Tests - + // MARK: - handleUserActivity Tests func test_handleUserActivity_invokesAppNotificationHandler() { // Act mparticle.handleUserActivity(testUserActivity) // Assert - handleUserActivity directly calls the app notification handler - XCTAssertTrue(appNotificationHandler.continueUserActivityCalled) - XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, testUserActivity) - XCTAssertNotNil(appNotificationHandler.continueUserActivityRestorationHandlerParam) + XCTAssertTrue(sceneMock.continueUserActivityCalled) + XCTAssertEqual(sceneMock.continueUserActivityUserActivityParam, testUserActivity) + XCTAssertNotNil(sceneMock.continueUserActivityRestorationHandlerParam) } func test_handleUserActivity_withWebBrowsingActivity() { @@ -54,8 +49,8 @@ final class MParticleSceneDelegateTests: MParticleTestBase { mparticle.handleUserActivity(webActivity) // Assert - Direct call to app notification handler - XCTAssertTrue(appNotificationHandler.continueUserActivityCalled) - XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, webActivity) + XCTAssertTrue(sceneMock.continueUserActivityCalled) + XCTAssertEqual(sceneMock.continueUserActivityUserActivityParam, webActivity) } func test_handleUserActivity_restorationHandlerIsEmpty() { @@ -63,10 +58,10 @@ final class MParticleSceneDelegateTests: MParticleTestBase { mparticle.handleUserActivity(testUserActivity) // Assert - XCTAssertTrue(appNotificationHandler.continueUserActivityCalled) + XCTAssertTrue(sceneMock.continueUserActivityCalled) // Verify the restoration handler is provided and safe to call - let restorationHandler = appNotificationHandler.continueUserActivityRestorationHandlerParam + let restorationHandler = sceneMock.continueUserActivityRestorationHandlerParam XCTAssertNotNil(restorationHandler) // Test that calling the restoration handler doesn't crash diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index e7a250e2c..747d53a29 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -570,6 +570,8 @@ 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, ); }; }; @@ -929,6 +931,7 @@ 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 = ""; }; @@ -1011,6 +1014,7 @@ 53A79A6F29CCCD6400E7489F = { isa = PBXGroup; children = ( + 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */, D3BA75152B614E3D008C3C65 /* PrivacyInfo.xcprivacy */, 53A79CFE29CE12AB00E7489F /* mParticle-Apple-SDK.modulemap */, 53A79CFF29CE23D600E7489F /* mParticle-Apple-SDK-NoLocation.modulemap */, @@ -1948,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 */, @@ -2169,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 */, diff --git a/mParticle-Apple-SDK/Include/mParticle.h b/mParticle-Apple-SDK/Include/mParticle.h index cd6685da5..74f7907de 100644 --- a/mParticle-Apple-SDK/Include/mParticle.h +++ b/mParticle-Apple-SDK/Include/mParticle.h @@ -790,6 +790,7 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp */ - (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. @@ -797,6 +798,7 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp @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. diff --git a/mParticle-Apple-SDK/SceneDelegateHandler.swift b/mParticle-Apple-SDK/SceneDelegateHandler.swift index fdead6e9b..1d8c3325c 100644 --- a/mParticle-Apple-SDK/SceneDelegateHandler.swift +++ b/mParticle-Apple-SDK/SceneDelegateHandler.swift @@ -1,14 +1,7 @@ -// -// SceneDelegateHandler.swift -// mParticle-Apple-SDK -// -// Created by Denis Chilik on 12/2/25. -// - import Foundation @objc -public protocol OpenUrlHandlerProtocol { +public protocol OpenURLHandlerProtocol { func open(_ url: URL, options: [String: Any]?) func continueUserActivity( _ userActivity: NSUserActivity, @@ -19,31 +12,32 @@ public protocol OpenUrlHandlerProtocol { @objcMembers public class SceneDelegateHandler: NSObject { private let logger: MPLog - private let appNotificationHandler: OpenUrlHandlerProtocol + private let appNotificationHandler: OpenURLHandlerProtocol - public init(logger: MPLog, 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 os(iOS) if #available(iOS 14.5, *) { logger.debug("Event Attribution: \(String(describing: urlContext.options.eventAttribution))") } - #endif 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 continueUserActivity(_ userActivity: NSUserActivity) { + public func handleUserActivity(_ userActivity: NSUserActivity) { logger.debug("User Activity Received") logger.debug("User Activity Type: \(userActivity.activityType)") logger.debug("User Activity Title: \(userActivity.title ?? "")") diff --git a/mParticle-Apple-SDK/mParticle.m b/mParticle-Apple-SDK/mParticle.m index 566ece016..4139ce4fb 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; } @@ -700,13 +703,14 @@ - (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationH +#if TARGET_OS_IOS == 1 - (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)) { - MPartiche.shredInstanse.hsceneDelegareHandler.andleURLContext - [handler handleWithUrlContext:urlContext]; + [self.sceneDelegateHandler handleWithUrlContext:urlContext]; } +#endif - (void)handleUserActivity:(NSUserActivity *)userActivity { - [handler handleUserActivity:userActivity]; + [self.sceneDelegateHandler handleUserActivity:userActivity]; } - (void)reset:(void (^)(void))completion { From 58d58f716d2cc190ca7b2f9b78d9f42b2a56d4fb Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Wed, 3 Dec 2025 16:42:10 -0500 Subject: [PATCH 8/8] minor comments from Denis --- UnitTests/Mocks/SceneDelegateHandlerMock.swift | 2 +- .../SwiftTests/MParticle/MParticleSceneDelegateTests.swift | 4 ++-- mParticle-Apple-SDK/mParticle.m | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/UnitTests/Mocks/SceneDelegateHandlerMock.swift b/UnitTests/Mocks/SceneDelegateHandlerMock.swift index ca4ad9665..d5b76e9f3 100644 --- a/UnitTests/Mocks/SceneDelegateHandlerMock.swift +++ b/UnitTests/Mocks/SceneDelegateHandlerMock.swift @@ -5,7 +5,7 @@ import XCTest import mParticle_Apple_SDK #endif -class SceneDelegateHandlerMock: OpenURLHandlerProtocol { +class OpenURLHandlerProtocolMock: OpenURLHandlerProtocol { var openURLWithOptionsCalled = false var openURLWithOptionsURLParam: URL? diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift index 626edcfc4..002a41967 100644 --- a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -10,7 +10,7 @@ final class MParticleSceneDelegateTests: XCTestCase { // MARK: - Properties var mparticle: MParticle! - var sceneMock: SceneDelegateHandlerMock! + var sceneMock: OpenURLHandlerProtocolMock! var testURL: URL! var testUserActivity: NSUserActivity! @@ -23,7 +23,7 @@ final class MParticleSceneDelegateTests: XCTestCase { testUserActivity.userInfo = ["key": "value"] // The implementation calls [MParticle sharedInstance], so we need to set the mock on the shared instance - sceneMock = SceneDelegateHandlerMock() + sceneMock = OpenURLHandlerProtocolMock() let sceneHandler = SceneDelegateHandler(logger: MPLog(logLevel: .verbose), appNotificationHandler: sceneMock) mparticle.sceneDelegateHandler = sceneHandler } diff --git a/mParticle-Apple-SDK/mParticle.m b/mParticle-Apple-SDK/mParticle.m index 4139ce4fb..73add0d1f 100644 --- a/mParticle-Apple-SDK/mParticle.m +++ b/mParticle-Apple-SDK/mParticle.m @@ -700,9 +700,6 @@ - (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];