Skip to content

Commit e5cbe84

Browse files
feat: Implement Manual SceneDelegate Support (#453)
* feat: Implement Manual SceneDelegate Support
1 parent d5e25e0 commit e5cbe84

File tree

6 files changed

+165
-11
lines changed

6 files changed

+165
-11
lines changed

UnitTests/ObjCTests/MPKitConfigurationTests.mm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ - (void)tearDown {
8181

8282
- (void)testInstance {
8383
XCTAssertNotNil(kitConfiguration);
84-
XCTAssertEqualObjects(kitConfiguration.configurationHash, @(762651950));
84+
XCTAssertEqualObjects(kitConfiguration.configurationHash, @(969680750));
8585
XCTAssertEqualObjects(kitConfiguration.integrationId, @37);
8686
XCTAssertEqualObjects(kitConfiguration.attributeValueFilteringHashedAttribute, @"12345");
8787
XCTAssertEqualObjects(kitConfiguration.attributeValueFilteringHashedValue, @"54321");
@@ -183,7 +183,7 @@ - (void)testInvalidConfiguration {
183183

184184
kitConfig = [[MPKitConfiguration alloc] initWithDictionary:configuration];
185185
XCTAssertNotNil(kitConfig);
186-
XCTAssertEqualObjects(kitConfig.configurationHash, @(1495473349));
186+
XCTAssertEqualObjects(kitConfig.configurationHash, @(-1872513399));
187187
XCTAssertEqualObjects(kitConfig.integrationId, @80);
188188

189189
XCTAssertNil(kitConfig.filters);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
#if MPARTICLE_LOCATION_DISABLE
3+
import mParticle_Apple_SDK_NoLocation
4+
#else
5+
import mParticle_Apple_SDK
6+
#endif
7+
import XCTest
8+
9+
final class MParticleSceneDelegateTests: MParticleTestBase {
10+
11+
// MARK: - Properties
12+
13+
var testURL: URL!
14+
var testUserActivity: NSUserActivity!
15+
16+
override func setUp() {
17+
super.setUp()
18+
testURL = URL(string: "myapp://test/path?param=value")!
19+
testUserActivity = NSUserActivity(activityType: "com.test.activity")
20+
testUserActivity.title = "Test Activity"
21+
testUserActivity.userInfo = ["key": "value"]
22+
23+
// The implementation calls [MParticle sharedInstance], so we need to set the mock on the shared instance
24+
MParticle.sharedInstance().appNotificationHandler = appNotificationHandler
25+
26+
// Reset mock state for each test
27+
appNotificationHandler.continueUserActivityCalled = false
28+
appNotificationHandler.continueUserActivityUserActivityParam = nil
29+
appNotificationHandler.continueUserActivityRestorationHandlerParam = nil
30+
appNotificationHandler.openURLWithOptionsCalled = false
31+
appNotificationHandler.openURLWithOptionsURLParam = nil
32+
appNotificationHandler.openURLWithOptionsOptionsParam = nil
33+
}
34+
35+
// MARK: - handleUserActivity Tests
36+
37+
func test_handleUserActivity_invokesAppNotificationHandler() {
38+
// Act
39+
mparticle.handleUserActivity(testUserActivity)
40+
41+
// Assert - handleUserActivity directly calls the app notification handler
42+
XCTAssertTrue(appNotificationHandler.continueUserActivityCalled)
43+
XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, testUserActivity)
44+
XCTAssertNotNil(appNotificationHandler.continueUserActivityRestorationHandlerParam)
45+
}
46+
47+
func test_handleUserActivity_withWebBrowsingActivity() {
48+
// Arrange
49+
let webActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
50+
webActivity.title = "Web Page"
51+
webActivity.webpageURL = URL(string: "https://example.com/page")
52+
53+
// Act
54+
mparticle.handleUserActivity(webActivity)
55+
56+
// Assert - Direct call to app notification handler
57+
XCTAssertTrue(appNotificationHandler.continueUserActivityCalled)
58+
XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, webActivity)
59+
}
60+
61+
func test_handleUserActivity_restorationHandlerIsEmpty() {
62+
// Act
63+
mparticle.handleUserActivity(testUserActivity)
64+
65+
// Assert
66+
XCTAssertTrue(appNotificationHandler.continueUserActivityCalled)
67+
68+
// Verify the restoration handler is provided and safe to call
69+
let restorationHandler = appNotificationHandler.continueUserActivityRestorationHandlerParam
70+
XCTAssertNotNil(restorationHandler)
71+
72+
// Test that calling the restoration handler doesn't crash
73+
XCTAssertNoThrow(restorationHandler?(nil))
74+
XCTAssertNoThrow(restorationHandler?([]))
75+
}
76+
}

mParticle-Apple-SDK.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,8 @@
566566
7E0387842DB913D2003B7D5E /* MPRokt.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E0387802DB913D2003B7D5E /* MPRokt.m */; };
567567
7E15B2062D94617900C1FF3E /* MPRoktTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E15B2052D94617900C1FF3E /* MPRoktTests.m */; };
568568
7E15B2072D94617900C1FF3E /* MPRoktTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E15B2052D94617900C1FF3E /* MPRoktTests.m */; };
569+
7E573D2A2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */; };
570+
7E573D2B2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */; };
569571
B31360F92E012760000DFBC9 /* MPRoktEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31360F72E012760000DFBC9 /* MPRoktEvent.swift */; };
570572
B3D778622E02F55F00D887A4 /* MPRoktEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31360F72E012760000DFBC9 /* MPRoktEvent.swift */; };
571573
D30CD0CB2CFF5FB100F5148A /* MPStateMachine.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A79B0629CDFB1F00E7489F /* MPStateMachine.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -923,6 +925,7 @@
923925
7E03877F2DB913D2003B7D5E /* MPRokt.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPRokt.h; sourceTree = "<group>"; };
924926
7E0387802DB913D2003B7D5E /* MPRokt.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPRokt.m; sourceTree = "<group>"; };
925927
7E15B2052D94617900C1FF3E /* MPRoktTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPRoktTests.m; sourceTree = "<group>"; };
928+
7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MParticleSceneDelegateTests.swift; sourceTree = "<group>"; };
926929
B31360F72E012760000DFBC9 /* MPRoktEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPRoktEvent.swift; sourceTree = "<group>"; };
927930
D33C8B312B8510C20012EDFD /* MPAudience.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPAudience.h; sourceTree = "<group>"; };
928931
D33C8B322B8510C20012EDFD /* MPAudience.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPAudience.m; sourceTree = "<group>"; };
@@ -1504,6 +1507,7 @@
15041507
isa = PBXGroup;
15051508
children = (
15061509
7231B8342EB95F9F001565E5 /* MParticleBreadcrumbTests.swift */,
1510+
7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */,
15071511
7231B83E2EB9627D001565E5 /* MParticleErrorTests.swift */,
15081512
7231B83B2EB961F2001565E5 /* MParticleLTVTests.swift */,
15091513
7231B84B2EB963B3001565E5 /* MParticleKitBatchTests.swift */,
@@ -1910,6 +1914,7 @@
19101914
534CD26929CE2CE1008452B3 /* MPSurrogateAppDelegateTests.m in Sources */,
19111915
534CD26A29CE2CE1008452B3 /* MPGDPRConsentTests.m in Sources */,
19121916
356D4A5A2E58B09D00CB69FE /* SettingsProviderMock.swift in Sources */,
1917+
7E573D2A2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */,
19131918
534CD26B29CE2CE1008452B3 /* MPBackendControllerTests.m in Sources */,
19141919
534CD26C29CE2CE1008452B3 /* MPKitConfigurationTests.mm in Sources */,
19151920
7231B80E2EB3C4AC001565E5 /* MPKitMock.swift in Sources */,
@@ -2129,6 +2134,7 @@
21292134
53A79CD129CE019F00E7489F /* MPSurrogateAppDelegateTests.m in Sources */,
21302135
53A79CE729CE019F00E7489F /* MPGDPRConsentTests.m in Sources */,
21312136
356D4A592E58B09D00CB69FE /* SettingsProviderMock.swift in Sources */,
2137+
7E573D2B2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */,
21322138
53A79CD229CE019F00E7489F /* MPBackendControllerTests.m in Sources */,
21332139
53A79CF329CE019F00E7489F /* MPKitConfigurationTests.mm in Sources */,
21342140
7231B80D2EB3C4AC001565E5 /* MPKitMock.swift in Sources */,
@@ -2362,6 +2368,7 @@
23622368
"$(inherited)",
23632369
);
23642370
INFOPLIST_FILE = ./UnitTests/Info.plist;
2371+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
23652372
MARKETING_VERSION = 1.0;
23662373
PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDK-NoLocationTests";
23672374
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2374,6 +2381,7 @@
23742381
SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h";
23752382
SWIFT_VERSION = 5.0;
23762383
TARGETED_DEVICE_FAMILY = "1,2,3";
2384+
TVOS_DEPLOYMENT_TARGET = 15.6;
23772385
};
23782386
name = Debug;
23792387
};
@@ -2390,6 +2398,7 @@
23902398
"$(inherited)",
23912399
);
23922400
INFOPLIST_FILE = ./UnitTests/Info.plist;
2401+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
23932402
MARKETING_VERSION = 1.0;
23942403
PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDK-NoLocationTests";
23952404
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2402,6 +2411,7 @@
24022411
SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h";
24032412
SWIFT_VERSION = 5.0;
24042413
TARGETED_DEVICE_FAMILY = "1,2,3";
2414+
TVOS_DEPLOYMENT_TARGET = 15.6;
24052415
};
24062416
name = Release;
24072417
};
@@ -2619,6 +2629,7 @@
26192629
CURRENT_PROJECT_VERSION = 1;
26202630
DEVELOPMENT_TEAM = DLD43Y3TRP;
26212631
INFOPLIST_FILE = ./UnitTests/Info.plist;
2632+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
26222633
MARKETING_VERSION = 1.0;
26232634
PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDKTests";
26242635
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2630,6 +2641,7 @@
26302641
SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h";
26312642
SWIFT_VERSION = 5.0;
26322643
TARGETED_DEVICE_FAMILY = "1,2,3";
2644+
TVOS_DEPLOYMENT_TARGET = 15.6;
26332645
};
26342646
name = Debug;
26352647
};
@@ -2642,6 +2654,7 @@
26422654
CURRENT_PROJECT_VERSION = 1;
26432655
DEVELOPMENT_TEAM = DLD43Y3TRP;
26442656
INFOPLIST_FILE = ./UnitTests/Info.plist;
2657+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
26452658
MARKETING_VERSION = 1.0;
26462659
PRODUCT_BUNDLE_IDENTIFIER = "com.mparticle.mParticle-Apple-SDKTests";
26472660
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2653,6 +2666,7 @@
26532666
SWIFT_OBJC_BRIDGING_HEADER = "./UnitTests/mParticle_iOS_SDKTests-Bridging-Header.h";
26542667
SWIFT_VERSION = 5.0;
26552668
TARGETED_DEVICE_FAMILY = "1,2,3";
2669+
TVOS_DEPLOYMENT_TARGET = 15.6;
26562670
};
26572671
name = Release;
26582672
};

mParticle-Apple-SDK/Include/mParticle.h

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -754,32 +754,57 @@ Defaults to false. Prevents the eventsHost above from overwriting the alias endp
754754
#endif
755755

756756
/**
757+
DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:open:sourceapplication:annotation:)
758+
Use a UIScene lifecycle, mParticle's handleURLContext: method, and scene(_:openURLContexts:) from UISceneDelegate instead.
759+
757760
Informs the mParticle SDK the app has been asked to open a resource identified by a URL.
758761
This method should be called only if proxiedAppDelegate is disabled.
759762
@param url The URL resource to open
760763
@param sourceApplication The bundle ID of the requesting app
761764
@param annotation A property list object supplied by the source app
762765
@see proxiedAppDelegate
763766
*/
764-
- (void)openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nullable id)annotation;
767+
- (void)openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nullable id)annotation DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method");
765768

766769
/**
770+
DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:open:sourceapplication:annotation:)
771+
Use a UIScene lifecycle, mParticle's handleURLContext: method, and scene(_:openURLContexts:) from UISceneDelegate instead.
772+
767773
Informs the mParticle SDK the app has been asked to open a resource identified by a URL.
768774
This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 9 and above.
769775
@param url The URL resource to open
770776
@param options The dictionary of launch options
771777
@see proxiedAppDelegate
772778
*/
773-
- (void)openURL:(NSURL *)url options:(nullable NSDictionary *)options;
779+
- (void)openURL:(NSURL *)url options:(nullable NSDictionary *)options DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method");
774780

775781
/**
782+
DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:continue:restorationhandler:)
783+
Use UIScene lifecycle, mParticle's handleUserActivity: method, and scene(_:continue:) from UISceneDelegate instead.
784+
776785
Informs the mParticle SDK the app has been asked to open to continue an NSUserActivity.
777786
This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 9 and above.
778787
@param userActivity The NSUserActivity that caused the app to be opened
779788
@param restorationHandler A block to execute if your app creates objects to perform the task.
780789
@see proxiedAppDelegate
781790
*/
782-
- (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void(^ _Nonnull)(NSArray<id<UIUserActivityRestoring>> * __nullable restorableObjects))restorationHandler;
791+
- (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void(^ _Nonnull)(NSArray<id<UIUserActivityRestoring>> * __nullable restorableObjects))restorationHandler DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method");
792+
793+
/**
794+
Informs the mParticle SDK the app has been asked to open a resource identified by a URL.
795+
This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 13 and above.
796+
@param urlContext The UIOpenURLContext provided by the SceneDelegate
797+
@see proxiedAppDelegate
798+
*/
799+
- (void)handleURLContext:(UIOpenURLContext *)urlContext NS_SWIFT_NAME(handleURLContext(_:)) API_AVAILABLE(ios(13.0));
800+
801+
/**
802+
Informs the mParticle SDK the app has been asked to open to continue an NSUserActivity.
803+
This method should be called only if proxiedAppDelegate is disabled.
804+
@param userActivity The NSUserActivity that caused the app to be opened
805+
@see proxiedAppDelegate
806+
*/
807+
- (void)handleUserActivity:(NSUserActivity *)userActivity NS_SWIFT_NAME(handleUserActivity(_:));
783808

784809
/**
785810
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

mParticle-Apple-SDK/Kits/MPKitConfiguration.mm

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ - (instancetype)initWithDictionary:(NSDictionary *)configurationDictionary {
2323
return nil;
2424
}
2525

26-
NSData *ekConfigData = [NSJSONSerialization dataWithJSONObject:configurationDictionary options:0 error:nil];
26+
NSJSONWritingOptions options = 0;
27+
if (@available(iOS 11.0, tvOS 11.0, *)) {
28+
options = NSJSONWritingSortedKeys;
29+
}
30+
NSData *ekConfigData = [NSJSONSerialization dataWithJSONObject:configurationDictionary options:options error:nil];
2731
NSString *ekConfigString = [[NSString alloc] initWithData:ekConfigData encoding:NSUTF8StringEncoding];
2832
_configurationHash = @([[MPIHasher hashString:ekConfigString] intValue]);
2933

@@ -114,16 +118,11 @@ - (id)initWithCoder:(NSCoder *)coder {
114118
@try {
115119
configurationDictionary = [coder decodeObjectOfClass:[NSDictionary class] forKey:@"configurationDictionary"];
116120
}
117-
118121
@catch ( NSException *e) {
119122
configurationDictionary = nil;
120123
MPILogError(@"Exception decoding MPKitConfiguration Attributes: %@", [e reason]);
121124
}
122125

123-
@finally {
124-
self = [self initWithDictionary:configurationDictionary];
125-
}
126-
127126
self = [self initWithDictionary:configurationDictionary];
128127
if (!self) {
129128
return nil;

mParticle-Apple-SDK/mParticle.m

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,46 @@ - (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationH
697697
return [self.appNotificationHandler continueUserActivity:userActivity restorationHandler:restorationHandler];
698698
}
699699

700+
- (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)){
701+
NSString *messageURL = [NSString stringWithFormat:@"Opening URLContext URL: %@", urlContext.URL];
702+
[logger debug:messageURL];
703+
NSString *messageSource = [NSString stringWithFormat:@"Source: %@", urlContext.options.sourceApplication ? urlContext.options.sourceApplication : @"unknown"];
704+
[logger debug:messageSource];
705+
NSString *messageAnnotation = [NSString stringWithFormat:@"Annotation: %@", urlContext.options.annotation];
706+
[logger debug:messageAnnotation];
707+
#if TARGET_OS_IOS == 1
708+
if (@available(iOS 14.5, *)) {
709+
NSString *messageEventAttribution = [NSString stringWithFormat:@"Event Attribution: %@", urlContext.options.eventAttribution];
710+
[logger debug:messageEventAttribution];
711+
}
712+
#endif
713+
NSString *messageOpenInPlace = [NSString stringWithFormat:@"Open in place: %@", urlContext.options.openInPlace ? @"True" : @"False"];
714+
[logger debug:messageOpenInPlace];
715+
716+
// Currently only one kit integration uses this dictionary for this key
717+
// https://github.com/mparticle-integrations/mparticle-apple-integration-flurry/blob/a0856e271aa9a63a6668805582395dea63f96af5/mParticle-Flurry/MPKitFlurry.m#L148C38-L148C85
718+
NSDictionary *options = @{@"UIApplicationOpenURLOptionsSourceApplicationKey": urlContext.options.sourceApplication};
719+
720+
[self.appNotificationHandler openURL:urlContext.URL options:options];
721+
}
722+
723+
- (void)handleUserActivity:(NSUserActivity *)userActivity {
724+
NSString *message = [NSString stringWithFormat:@"User Activity Received"];
725+
[logger debug:message];
726+
NSString *messageType = [NSString stringWithFormat:@"User Activity Type: %@", userActivity.activityType];
727+
[logger debug:messageType];
728+
NSString *messageTitle = [NSString stringWithFormat:@"User Activity Title: %@", userActivity.title];
729+
[logger debug:messageTitle];
730+
NSString *messageUserInfo = [NSString stringWithFormat:@"User Activity User Info: %@", userActivity.userInfo];
731+
[logger debug:messageUserInfo];
732+
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
733+
NSString *messageURL = [NSString stringWithFormat:@"Opening UserActivity URL: %@", userActivity.webpageURL];
734+
[logger debug:messageURL];
735+
}
736+
// When provided by the SceneDelegate NSUserActivity is not paired with a restorationHandler
737+
[self.appNotificationHandler continueUserActivity: userActivity restorationHandler:^(NSArray<id<UIUserActivityRestoring>> * _Nullable restorableObjects) {}];
738+
}
739+
700740
- (void)reset:(void (^)(void))completion {
701741
[executor executeOnMessage:^{
702742
[self.kitContainer flushSerializedKits];

0 commit comments

Comments
 (0)