Skip to content

Commit 775315c

Browse files
feat: Implement Manual SceneDelegate Support
1 parent a1e51fc commit 775315c

File tree

4 files changed

+194
-3
lines changed

4 files changed

+194
-3
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// MParticleSceneDelegateTests.swift
3+
// mParticle-Apple-SDK
4+
//
5+
// Created by Brandon Stalnaker on 11/13/25.
6+
//
7+
8+
#if MPARTICLE_LOCATION_DISABLE
9+
import mParticle_Apple_SDK_NoLocation
10+
#else
11+
import mParticle_Apple_SDK
12+
#endif
13+
import XCTest
14+
15+
@available(iOS 13.0, *)
16+
final class MParticleSceneDelegateTests: MParticleTestBase {
17+
18+
// MARK: - Properties
19+
20+
var testURL: URL!
21+
var testUserActivity: NSUserActivity!
22+
23+
override func setUp() {
24+
super.setUp()
25+
testURL = URL(string: "myapp://test/path?param=value")!
26+
testUserActivity = NSUserActivity(activityType: "com.test.activity")
27+
testUserActivity.title = "Test Activity"
28+
testUserActivity.userInfo = ["key": "value"]
29+
30+
// The implementation calls [MParticle sharedInstance], so we need to set the mock on the shared instance
31+
MParticle.sharedInstance().appNotificationHandler = appNotificationHandler
32+
33+
// Reset mock state for each test
34+
appNotificationHandler.continueUserActivityCalled = false
35+
appNotificationHandler.continueUserActivityUserActivityParam = nil
36+
appNotificationHandler.continueUserActivityRestorationHandlerParam = nil
37+
appNotificationHandler.openURLWithOptionsCalled = false
38+
appNotificationHandler.openURLWithOptionsURLParam = nil
39+
appNotificationHandler.openURLWithOptionsOptionsParam = nil
40+
}
41+
42+
// MARK: - Method Existence Tests
43+
44+
func test_handleURLContext_methodExists() {
45+
// Verify the method exists with correct signature
46+
XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:))))
47+
}
48+
49+
func test_handleUserActivity_methodExists() {
50+
// Verify the method exists with correct signature
51+
XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:))))
52+
}
53+
54+
// MARK: - handleURLContext Tests
55+
// Note: Testing with mock URLContext is complex due to system class limitations
56+
// These tests focus on method availability and basic functionality
57+
58+
func test_handleURLContext_availabilityCheck() {
59+
// Verify the method is only available on iOS 13+
60+
if #available(iOS 13.0, *) {
61+
XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:))))
62+
} else {
63+
XCTAssertFalse(mparticle.responds(to: #selector(mparticle.handleURLContext(_:))))
64+
}
65+
}
66+
67+
// MARK: - handleUserActivity Tests
68+
69+
func test_handleUserActivity_invokesAppNotificationHandler() {
70+
// Act
71+
mparticle.handleUserActivity(testUserActivity)
72+
73+
// Assert - handleUserActivity directly calls the app notification handler
74+
XCTAssertTrue(appNotificationHandler.continueUserActivityCalled)
75+
XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, testUserActivity)
76+
XCTAssertNotNil(appNotificationHandler.continueUserActivityRestorationHandlerParam)
77+
}
78+
79+
func test_handleUserActivity_withWebBrowsingActivity() {
80+
// Arrange
81+
let webActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
82+
webActivity.title = "Web Page"
83+
webActivity.webpageURL = URL(string: "https://example.com/page")
84+
85+
// Act
86+
mparticle.handleUserActivity(webActivity)
87+
88+
// Assert - Direct call to app notification handler
89+
XCTAssertTrue(appNotificationHandler.continueUserActivityCalled)
90+
XCTAssertEqual(appNotificationHandler.continueUserActivityUserActivityParam, webActivity)
91+
}
92+
93+
func test_handleUserActivity_restorationHandlerIsEmpty() {
94+
// Act
95+
mparticle.handleUserActivity(testUserActivity)
96+
97+
// Assert
98+
XCTAssertTrue(appNotificationHandler.continueUserActivityCalled)
99+
100+
// Verify the restoration handler is provided and safe to call
101+
let restorationHandler = appNotificationHandler.continueUserActivityRestorationHandlerParam
102+
XCTAssertNotNil(restorationHandler)
103+
104+
// Test that calling the restoration handler doesn't crash
105+
XCTAssertNoThrow(restorationHandler?(nil))
106+
XCTAssertNoThrow(restorationHandler?([]))
107+
}
108+
109+
// MARK: - iOS Version Availability Tests
110+
111+
func test_handleUserActivity_alwaysAvailable() {
112+
// handleUserActivity should be available on all iOS versions
113+
XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:))))
114+
}
115+
116+
// MARK: - Integration Tests
117+
118+
func test_bothMethods_exist() {
119+
// Verify both SceneDelegate support methods exist
120+
XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleURLContext(_:))))
121+
XCTAssertTrue(mparticle.responds(to: #selector(mparticle.handleUserActivity(_:))))
122+
}
123+
}

mParticle-Apple-SDK.xcodeproj/project.pbxproj

Lines changed: 6 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 */,

mParticle-Apple-SDK/Include/mParticle.h

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -754,32 +754,54 @@ 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+
757759
Informs the mParticle SDK the app has been asked to open a resource identified by a URL.
758760
This method should be called only if proxiedAppDelegate is disabled.
759761
@param url The URL resource to open
760762
@param sourceApplication The bundle ID of the requesting app
761763
@param annotation A property list object supplied by the source app
762764
@see proxiedAppDelegate
763765
*/
764-
- (void)openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nullable id)annotation;
766+
- (void)openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(nullable id)annotation DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method");
765767

766768
/**
769+
DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:open:sourceapplication:annotation:)
770+
767771
Informs the mParticle SDK the app has been asked to open a resource identified by a URL.
768772
This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 9 and above.
769773
@param url The URL resource to open
770774
@param options The dictionary of launch options
771775
@see proxiedAppDelegate
772776
*/
773-
- (void)openURL:(NSURL *)url options:(nullable NSDictionary *)options;
777+
- (void)openURL:(NSURL *)url options:(nullable NSDictionary *)options DEPRECATED_MSG_ATTRIBUTE("iOS 27 will no longer support this protocol method");
774778

775779
/**
780+
DEPRECATED: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:continue:restorationhandler:)
781+
776782
Informs the mParticle SDK the app has been asked to open to continue an NSUserActivity.
777783
This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 9 and above.
778784
@param userActivity The NSUserActivity that caused the app to be opened
779785
@param restorationHandler A block to execute if your app creates objects to perform the task.
780786
@see proxiedAppDelegate
781787
*/
782-
- (BOOL)continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(void(^ _Nonnull)(NSArray<id<UIUserActivityRestoring>> * __nullable restorableObjects))restorationHandler;
788+
- (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");
789+
790+
/**
791+
Informs the mParticle SDK the app has been asked to open a resource identified by a URL.
792+
This method should be called only if proxiedAppDelegate is disabled. This method is only available for iOS 13 and above.
793+
@param urlContext The UIOpenURLContext provided by the SceneDelegate
794+
@see proxiedAppDelegate
795+
*/
796+
- (void)handleURLContext:(UIOpenURLContext *)urlContext NS_SWIFT_NAME(handleURLContext(_:)) API_AVAILABLE(ios(13.0));
797+
798+
/**
799+
Informs the mParticle SDK the app has been asked to open to continue an NSUserActivity.
800+
This method should be called only if proxiedAppDelegate is disabled.
801+
@param userActivity The NSUserActivity that caused the app to be opened
802+
@see proxiedAppDelegate
803+
*/
804+
- (void)handleUserActivity:(NSUserActivity *)userActivity NS_SWIFT_NAME(handleUserActivity(_:));
783805

784806
/**
785807
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/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+
[[MParticle sharedInstance].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+
[[MParticle sharedInstance].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)