diff --git a/.github/workflows/build-secondary-platforms.yml b/.github/workflows/build-secondary-platforms.yml index 63ef85530..2d9e10f30 100644 --- a/.github/workflows/build-secondary-platforms.yml +++ b/.github/workflows/build-secondary-platforms.yml @@ -23,7 +23,7 @@ jobs: xcode-version: ${{ env.XCODE_VERSION }} - name: Setup specified simulator - uses: futureware-tech/simulator-action@v4 + uses: futureware-tech/simulator-action@v5 id: simulator with: model: iPhone 16 Pro diff --git a/.github/workflows/cross-platform-tests.yml b/.github/workflows/cross-platform-tests.yml index b93578acb..2ade257ed 100644 --- a/.github/workflows/cross-platform-tests.yml +++ b/.github/workflows/cross-platform-tests.yml @@ -40,7 +40,7 @@ jobs: run: ./gradlew runIos - name: Archive Test Results if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ios-test-results path: Users/runner/Library/Developer/Xcode/DerivedData diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e4c5ba753..311bc55f8 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -99,7 +99,7 @@ jobs: - name: Upload artifacts on failure if: failure() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: integration-test-artifacts path: ${{ env.WORKING_DIRECTORY }}/artifacts/ diff --git a/.github/workflows/native-tests.yml b/.github/workflows/native-tests.yml index a4d5e3d2f..b2e3a3881 100644 --- a/.github/workflows/native-tests.yml +++ b/.github/workflows/native-tests.yml @@ -32,7 +32,7 @@ jobs: xcode-version: ${{ env.XCODE_VERSION }} - name: Setup specified simulator - uses: futureware-tech/simulator-action@v4 + uses: futureware-tech/simulator-action@v5 id: simulator with: model: ${{ matrix.device }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3f7ecf0..cf2f41ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to the mParticle Apple SDK (core and integration kits) are d --- +# [8.44.0](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.43.1...v8.44.0) (2026-02-19) + +### Bug Fixes + +- fix: SceneDelegate Protocol Objc Selector Issue (#603) ([99364bfb](https://github.com/mParticle/mparticle-apple-sdk/commit/99364bfbf62401c6d53b195b63a21576baaf354b)) + # [8.43.1](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.42.2...v8.43.1) (2026-02-16) ### Core diff --git a/Framework/Info.plist b/Framework/Info.plist index c9656ac5d..3a210b676 100644 --- a/Framework/Info.plist +++ b/Framework/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 8.43.1 + 8.44.0 CFBundleSignature ???? CFBundleVersion diff --git a/RNExample/package-lock.json b/RNExample/package-lock.json index fa4f77d0e..49b8f36a9 100644 --- a/RNExample/package-lock.json +++ b/RNExample/package-lock.json @@ -6382,18 +6382,14 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", - "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", + "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", diff --git a/SwiftExample/mParticleSwiftExample.xcodeproj/project.pbxproj b/SwiftExample/mParticleSwiftExample.xcodeproj/project.pbxproj index 4223af168..1b57d6a7d 100644 --- a/SwiftExample/mParticleSwiftExample.xcodeproj/project.pbxproj +++ b/SwiftExample/mParticleSwiftExample.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 7E4B5EB92F338B9100B61C64 /* mParticle-Apple-SDK in Frameworks */ = {isa = PBXBuildFile; productRef = 7E4B5EB82F338B9100B61C64 /* mParticle-Apple-SDK */; }; - 7E4B5EBC2F338BA500B61C64 /* mParticle-Rokt in Frameworks */ = {isa = PBXBuildFile; productRef = 7E4B5EBB2F338BA500B61C64 /* mParticle-Rokt */; }; + 7ED594AE2F46240D00AB6433 /* mParticle-Rokt in Frameworks */ = {isa = PBXBuildFile; productRef = 7ED594AD2F46240D00AB6433 /* mParticle-Rokt */; }; A10000010000000000000001 /* mParticleSwiftExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000001 /* mParticleSwiftExampleApp.swift */; }; A10000010000000000000002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000002 /* ContentView.swift */; }; A10000010000000000000003 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000003 /* Assets.xcassets */; }; @@ -31,7 +31,7 @@ files = ( 7E4B5EB92F338B9100B61C64 /* mParticle-Apple-SDK in Frameworks */, A10000010000000000000004 /* mParticle-Apple-SDK in Frameworks */, - 7E4B5EBC2F338BA500B61C64 /* mParticle-Rokt in Frameworks */, + 7ED594AE2F46240D00AB6433 /* mParticle-Rokt in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,7 +85,7 @@ packageProductDependencies = ( A10000030000000000000001 /* mParticle-Apple-SDK */, 7E4B5EB82F338B9100B61C64 /* mParticle-Apple-SDK */, - 7E4B5EBB2F338BA500B61C64 /* mParticle-Rokt */, + 7ED594AD2F46240D00AB6433 /* mParticle-Rokt */, ); productName = mParticleSwiftExample; productReference = A10000020000000000000006 /* mParticleSwiftExample.app */; @@ -117,7 +117,7 @@ mainGroup = A10000050000000000000001; packageReferences = ( 7E4B5EB72F338B9100B61C64 /* XCRemoteSwiftPackageReference "mparticle-apple-sdk" */, - 7E4B5EBA2F338BA500B61C64 /* XCRemoteSwiftPackageReference "mparticle-apple-integration-rokt" */, + 7ED594AC2F46240D00AB6433 /* XCRemoteSwiftPackageReference "mparticle-apple-integration-rokt" */, ); productRefGroup = A10000050000000000000003 /* Products */; projectDirPath = ""; @@ -367,7 +367,7 @@ minimumVersion = 8.4.0; }; }; - 7E4B5EBA2F338BA500B61C64 /* XCRemoteSwiftPackageReference "mparticle-apple-integration-rokt" */ = { + 7ED594AC2F46240D00AB6433 /* XCRemoteSwiftPackageReference "mparticle-apple-integration-rokt" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mparticle-integrations/mparticle-apple-integration-rokt.git"; requirement = { @@ -383,9 +383,9 @@ package = 7E4B5EB72F338B9100B61C64 /* XCRemoteSwiftPackageReference "mparticle-apple-sdk" */; productName = "mParticle-Apple-SDK"; }; - 7E4B5EBB2F338BA500B61C64 /* mParticle-Rokt */ = { + 7ED594AD2F46240D00AB6433 /* mParticle-Rokt */ = { isa = XCSwiftPackageProductDependency; - package = 7E4B5EBA2F338BA500B61C64 /* XCRemoteSwiftPackageReference "mparticle-apple-integration-rokt" */; + package = 7ED594AC2F46240D00AB6433 /* XCRemoteSwiftPackageReference "mparticle-apple-integration-rokt" */; productName = "mParticle-Rokt"; }; A10000030000000000000001 /* mParticle-Apple-SDK */ = { diff --git a/SwiftExample/mParticleSwiftExample/ContentView.swift b/SwiftExample/mParticleSwiftExample/ContentView.swift index e0e6db1b7..707ae2ecc 100644 --- a/SwiftExample/mParticleSwiftExample/ContentView.swift +++ b/SwiftExample/mParticleSwiftExample/ContentView.swift @@ -1,6 +1,8 @@ // swiftlint:disable file_length import SwiftUI import mParticle_Apple_SDK +import mParticle_Rokt_Swift +import Rokt_Widget import AdSupport import AppTrackingTransparency @@ -95,6 +97,13 @@ struct ContentView: View { title: "Display Rokt Overlay (auto close)", action: selectOverlayPlacementAutoClose ) + ActionButton( + title: "Display Rokt with Event Subscription", + action: selectPlacementWithEventSubscription + ) + NavigationLink("MPRoktLayout SwiftUI Example") { + RoktLayoutExampleView() + } } } .listStyle(InsetGroupedListStyle()) @@ -202,6 +211,175 @@ struct RoktEmbeddedViewWrapper: UIViewRepresentable { } } +// MARK: - MPRoktLayout SwiftUI Example + +struct RoktLayoutExampleView: View { + @State private var sdkTriggered = false + @State private var eventLog: [String] = [] + + let attributes: [String: String] = [ + "email": "j.smith@example.com", + "firstname": "Jenny", + "lastname": "Smith", + "billingzipcode": "07762", + "confirmationref": "54321", + "sandbox": "true", + "mobile": "(555)867-5309" + ] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("MPRoktLayout Example") + .font(.title2) + .fontWeight(.bold) + .padding(.horizontal) + + Text("This demonstrates the SwiftUI-native MPRoktLayout component for embedding Rokt placements declaratively.") + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.horizontal) + + // Trigger button + Button { + sdkTriggered = true + eventLog.append("[\(formattedTime())] Triggered placement") + } label: { + Text(sdkTriggered ? "Placement Triggered" : "Trigger Rokt Placement") + .frame(maxWidth: .infinity) + .padding() + .background(sdkTriggered ? Color.gray : Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .disabled(sdkTriggered) + .padding(.horizontal) + + // Reset button + if sdkTriggered { + Button { + sdkTriggered = false + eventLog.append("[\(formattedTime())] Reset placement") + } label: { + Text("Reset") + .frame(maxWidth: .infinity) + .padding() + .background(Color.orange) + .foregroundColor(.white) + .cornerRadius(10) + } + .padding(.horizontal) + } + + // Rokt Layout - Embedded Placement + if sdkTriggered { + VStack(alignment: .leading, spacing: 8) { + Text("Embedded Placement") + .font(.headline) + .padding(.horizontal) + + MPRoktLayout( + sdkTriggered: $sdkTriggered, + viewName: "RoktExperience", + locationName: "RoktEmbedded1", + attributes: attributes, + config: createRoktConfig(), + onEvent: { roktEvent in + handleRoktEvent(roktEvent) + } + ).roktLayout + .padding(.horizontal) + } + } + + // Event Log + VStack(alignment: .leading, spacing: 8) { + Text("Event Log") + .font(.headline) + .padding(.horizontal) + + if eventLog.isEmpty { + Text("No events yet. Trigger the placement to see events.") + .font(.caption) + .foregroundColor(.secondary) + .padding(.horizontal) + } else { + ForEach(eventLog.reversed(), id: \.self) { event in + Text(event) + .font(.system(.caption, design: .monospaced)) + .padding(.horizontal) + } + } + } + .padding(.vertical) + .background(Color(.systemGray6)) + .cornerRadius(10) + .padding(.horizontal) + + Spacer() + } + .padding(.vertical) + } + .navigationTitle("MPRoktLayout") + .navigationBarTitleDisplayMode(.inline) + } + + private func createRoktConfig() -> RoktConfig { + return RoktConfig.Builder().build() + } + + private func handleRoktEvent(_ event: RoktEvent) { + let timestamp = formattedTime() + + switch event { + case is RoktEvent.ShowLoadingIndicator: + eventLog.append("[\(timestamp)] Show Loading Indicator") + + case is RoktEvent.HideLoadingIndicator: + eventLog.append("[\(timestamp)] Hide Loading Indicator") + + case let placementReady as RoktEvent.PlacementReady: + eventLog.append("[\(timestamp)] Placement Ready - ID: \(placementReady.placementId ?? "unknown")") + + case let placementInteractive as RoktEvent.PlacementInteractive: + eventLog.append("[\(timestamp)] Placement Interactive - ID: \(placementInteractive.placementId ?? "unknown")") + + case let offerEngagement as RoktEvent.OfferEngagement: + eventLog.append("[\(timestamp)] Offer Engagement - ID: \(offerEngagement.placementId ?? "unknown")") + + case let positiveEngagement as RoktEvent.PositiveEngagement: + eventLog.append("[\(timestamp)] Positive Engagement - ID: \(positiveEngagement.placementId ?? "unknown")") + + case let firstPositiveEngagement as RoktEvent.FirstPositiveEngagement: + eventLog.append("[\(timestamp)] First Positive Engagement - ID: \(firstPositiveEngagement.placementId ?? "unknown")") + + case let openUrl as RoktEvent.OpenUrl: + eventLog.append("[\(timestamp)] Open URL - \(openUrl.url)") + + case let placementClosed as RoktEvent.PlacementClosed: + eventLog.append("[\(timestamp)] Placement Closed - ID: \(placementClosed.placementId ?? "unknown")") + + case let placementCompleted as RoktEvent.PlacementCompleted: + eventLog.append("[\(timestamp)] Placement Completed - ID: \(placementCompleted.placementId ?? "unknown")") + + case let placementFailure as RoktEvent.PlacementFailure: + eventLog.append("[\(timestamp)] Placement Failure - ID: \(placementFailure.placementId ?? "unknown")") + + case let cartItem as RoktEvent.CartItemInstantPurchase: + eventLog.append("[\(timestamp)] Cart Item Purchase - \(cartItem.name ?? "unknown")") + + default: + eventLog.append("[\(timestamp)] Event: \(type(of: event))") + } + } + + private func formattedTime() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: Date()) + } +} + // MARK: - mParticle Actions func logSimpleEvent() { @@ -718,6 +896,76 @@ func selectOverlayPlacementAutoClose() { } } +func selectPlacementWithEventSubscription() { + let customAttributes: [String: String] = [ + "email": "j.smit@example.com", + "firstname": "Jenny", + "lastname": "Smith", + "sandbox": "true", + "mobile": "(555)867-5309" + ] + + let placementIdentifier = "RoktLayout" + + // Subscribe to Rokt events for this placement + MParticle.sharedInstance().rokt.events(placementIdentifier) { event in + switch event { + case let initComplete as MPRoktEvent.MPRoktInitComplete: + print("Rokt Init Complete - Success: \(initComplete.success)") + + case is MPRoktEvent.MPRoktShowLoadingIndicator: + print("Rokt: Show Loading Indicator") + + case is MPRoktEvent.MPRoktHideLoadingIndicator: + print("Rokt: Hide Loading Indicator") + + case let placementReady as MPRoktEvent.MPRoktPlacementReady: + print("Rokt Placement Ready - ID: \(placementReady.placementId ?? "unknown")") + + case let placementInteractive as MPRoktEvent.MPRoktPlacementInteractive: + print("Rokt Placement Interactive - ID: \(placementInteractive.placementId ?? "unknown")") + + case let offerEngagement as MPRoktEvent.MPRoktOfferEngagement: + print("Rokt Offer Engagement - ID: \(offerEngagement.placementId ?? "unknown")") + + case let positiveEngagement as MPRoktEvent.MPRoktPositiveEngagement: + print("Rokt Positive Engagement - ID: \(positiveEngagement.placementId ?? "unknown")") + + case let firstPositiveEngagement as MPRoktEvent.MPRoktFirstPositiveEngagement: + print("Rokt First Positive Engagement - ID: \(firstPositiveEngagement.placementId ?? "unknown")") + + case let openUrl as MPRoktEvent.MPRoktOpenUrl: + print("Rokt Open URL - ID: \(openUrl.placementId ?? "unknown"), URL: \(openUrl.url)") + + case let placementClosed as MPRoktEvent.MPRoktPlacementClosed: + print("Rokt Placement Closed - ID: \(placementClosed.placementId ?? "unknown")") + + case let placementCompleted as MPRoktEvent.MPRoktPlacementCompleted: + print("Rokt Placement Completed - ID: \(placementCompleted.placementId ?? "unknown")") + + case let placementFailure as MPRoktEvent.MPRoktPlacementFailure: + print("Rokt Placement Failure - ID: \(placementFailure.placementId ?? "unknown")") + + case let cartItem as MPRoktEvent.MPRoktCartItemInstantPurchase: + print("Rokt Cart Item Instant Purchase:") + print(" - Placement ID: \(cartItem.placementId)") + print(" - Catalog Item ID: \(cartItem.catalogItemId)") + print(" - Cart Item ID: \(cartItem.cartItemId)") + print(" - Name: \(cartItem.name ?? "unknown")") + print(" - Currency: \(cartItem.currency)") + print(" - Unit Price: \(cartItem.unitPrice ?? 0)") + print(" - Total Price: \(cartItem.totalPrice ?? 0)") + print(" - Quantity: \(cartItem.quantity ?? 0)") + + default: + print("Rokt: Unknown event type - \(type(of: event))") + } + } + + // Select the placement (this will trigger events) + MParticle.sharedInstance().rokt.selectPlacements(placementIdentifier, attributes: customAttributes) +} + #Preview { ContentView() } diff --git a/UnitTests/Mocks/MPAppNotificationHandlerMock.swift b/UnitTests/Mocks/MPAppNotificationHandlerMock.swift index 05d099f9d..73f6ad811 100644 --- a/UnitTests/Mocks/MPAppNotificationHandlerMock.swift +++ b/UnitTests/Mocks/MPAppNotificationHandlerMock.swift @@ -85,18 +85,20 @@ class MPAppNotificationHandlerMock: MPAppNotificationHandlerProtocol { var openURLWithOptionsCalled = false var openURLWithOptionsURLParam: URL? var openURLWithOptionsOptionsParam: [String: Any]? - + + @objc(openURL:options:) 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 - + + @objc(continueUserActivity:restorationHandler:) func `continue`(_ userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool { continueUserActivityCalled = true diff --git a/UnitTests/Mocks/MPBackendControllerMock.swift b/UnitTests/Mocks/MPBackendControllerMock.swift index a7128432e..b1479d8d8 100644 --- a/UnitTests/Mocks/MPBackendControllerMock.swift +++ b/UnitTests/Mocks/MPBackendControllerMock.swift @@ -69,7 +69,7 @@ class MPBackendControllerMock: NSObject, MPBackendControllerProtocol { var prepareBatchesCalled = false var prepareBatchesUploadSettingsParam: MPUploadSettings? - func prepareBatches(forUpload uploadSettings: MPUploadSettings) { + func prepareBatches(forUpload uploadSettings: MPUploadSettings) -> Void { prepareBatchesCalled = true prepareBatchesUploadSettingsParam = uploadSettings } diff --git a/UnitTests/Mocks/MPUserDefaultsMock.swift b/UnitTests/Mocks/MPUserDefaultsMock.swift index e1fcba0af..272fb5dc7 100644 --- a/UnitTests/Mocks/MPUserDefaultsMock.swift +++ b/UnitTests/Mocks/MPUserDefaultsMock.swift @@ -2,7 +2,7 @@ import XCTest import mParticle_Apple_SDK internal import mParticle_Apple_SDK_Swift -class MPUserDefaultsMock: MPUserDefaultsProtocol { +class MPUserDefaultsMock: mParticle_Apple_SDK_Swift.MPUserDefaultsProtocol { var setMPObjectCalled = false var setMPObjectValueParam: Any? var setMPObjectKeyParam: String? diff --git a/UnitTests/Mocks/SceneDelegateHandlerMock.swift b/UnitTests/Mocks/SceneDelegateHandlerMock.swift index 06967287f..98de6b2c5 100644 --- a/UnitTests/Mocks/SceneDelegateHandlerMock.swift +++ b/UnitTests/Mocks/SceneDelegateHandlerMock.swift @@ -2,22 +2,23 @@ import XCTest import mParticle_Apple_SDK class OpenURLHandlerProtocolMock: NSObject, OpenURLHandlerProtocol { - var openURLWithOptionsCalled = false var openURLWithOptionsURLParam: URL? var openURLWithOptionsOptionsParam: [String: Any]? - + + @objc(openURL:options:) 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 - + + @objc(continueUserActivity:restorationHandler:) func `continue`( _ userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void diff --git a/UnitTests/ObjCTests/MPBackendControllerTests.m b/UnitTests/ObjCTests/MPBackendControllerTests.m index 38cf89ec1..d28040f2d 100644 --- a/UnitTests/ObjCTests/MPBackendControllerTests.m +++ b/UnitTests/ObjCTests/MPBackendControllerTests.m @@ -78,8 +78,6 @@ - (void)handleApplicationDidBecomeActive:(NSNotification *)notification; - (void)logRemoteNotificationWithNotificationController:(MPNotificationController_PRIVATE *const)notificationController; - (void)parseConfigResponse:(NSDictionary *)configurationDictionary; - (void)parseResponseHeader:(NSDictionary *)responseDictionary session:(MPSession *)session; -- (NSNumber *)previousSessionSuccessfullyClosed; -- (void)setPreviousSessionSuccessfullyClosed:(NSNumber *)previousSessionSuccessfullyClosed; - (void)processOpenSessionsEndingCurrent:(BOOL)endCurrentSession completionHandler:(dispatch_block_t)completionHandler; - (void)resetUserIdentitiesFirstTimeUseFlag; - (void)saveMessage:(MPMessage *)message updateSession:(BOOL)updateSession; @@ -126,10 +124,10 @@ @implementation MPBackendControllerTests - (void)setUp { [super setUp]; - + [MPPersistenceController_PRIVATE setMpid:@1]; [MParticle sharedInstance].persistenceController = [[MPPersistenceController_PRIVATE alloc] init]; - + // Must read messageQueue AFTER [MParticle sharedInstance] triggers singleton // recreation, otherwise we get the old executor's queue. messageQueue = [MParticle messageQueue]; diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift index 8ad7b6667..a73338964 100644 --- a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -3,27 +3,56 @@ import XCTest internal import mParticle_Apple_SDK_Swift final class MParticleSceneDelegateTests: XCTestCase { - + // MARK: - Properties var mparticle: MParticle! var sceneMock: OpenURLHandlerProtocolMock! - var testURL: URL! var testUserActivity: NSUserActivity! - + override func setUp() { super.setUp() mparticle = MParticle() - testURL = URL(string: "myapp://test/path?param=value")! testUserActivity = NSUserActivity(activityType: "com.test.activity") testUserActivity.title = "Test Activity" testUserActivity.userInfo = ["key": "value"] - - // The implementation calls [MParticle sharedInstance], so we need to set the mock on the shared instance + sceneMock = OpenURLHandlerProtocolMock() let sceneHandler = SceneDelegateHandler(appNotificationHandler: sceneMock) mparticle.sceneDelegateHandler = sceneHandler } + + // MARK: - Protocol Selector Tests + + func test_openURLHandlerProtocol_respondsToOpenURLSelector() { + // This test verifies that the protocol method maps to the correct ObjC selector + let handler: OpenURLHandlerProtocol = sceneMock + let selector = NSSelectorFromString("openURL:options:") + XCTAssertTrue((handler as AnyObject).responds(to: selector), + "OpenURLHandlerProtocol should respond to openURL:options: selector") + } + + func test_openURLHandlerProtocol_respondsToContiuneUserActivitySelector() { + // This test verifies that the protocol method maps to the correct ObjC selector + let handler: OpenURLHandlerProtocol = sceneMock + let selector = NSSelectorFromString("continueUserActivity:restorationHandler:") + XCTAssertTrue((handler as AnyObject).responds(to: selector), + "OpenURLHandlerProtocol should respond to continueUserActivity:restorationHandler: selector") + } + + func test_mpAppNotificationHandler_respondsToOpenURLHandlerProtocolSelectors() { + // This test verifies that MPAppNotificationHandler responds to the selectors + // expected by OpenURLHandlerProtocol + let handler = MPAppNotificationHandler() + + let openURLSelector = NSSelectorFromString("openURL:options:") + XCTAssertTrue(handler.responds(to: openURLSelector), + "MPAppNotificationHandler must respond to openURL:options: to conform to OpenURLHandlerProtocol") + + let continueActivitySelector = NSSelectorFromString("continueUserActivity:restorationHandler:") + XCTAssertTrue(handler.responds(to: continueActivitySelector), + "MPAppNotificationHandler must respond to continueUserActivity:restorationHandler: to conform to OpenURLHandlerProtocol") + } // MARK: - handleUserActivity Tests @@ -54,14 +83,14 @@ final class MParticleSceneDelegateTests: XCTestCase { func test_handleUserActivity_restorationHandlerIsEmpty() { // Act mparticle.handleUserActivity(testUserActivity) - + // Assert XCTAssertTrue(sceneMock.continueUserActivityCalled) - + // Verify the restoration handler is provided and safe to call let restorationHandler = sceneMock.continueUserActivityRestorationHandlerParam XCTAssertNotNil(restorationHandler) - + // Test that calling the restoration handler doesn't crash XCTAssertNoThrow(restorationHandler?(nil)) XCTAssertNoThrow(restorationHandler?([])) diff --git a/mParticle-Apple-SDK.podspec b/mParticle-Apple-SDK.podspec index d68827670..75e65c545 100644 --- a/mParticle-Apple-SDK.podspec +++ b/mParticle-Apple-SDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "mParticle-Apple-SDK" - s.version = "8.43.1" + s.version = "8.44.0" s.summary = "mParticle Apple SDK." s.description = <<-DESC diff --git a/mParticle-Apple-SDK/Include/MPBackendController.h b/mParticle-Apple-SDK/Include/MPBackendController.h index 1b7042ca0..6c449a43c 100644 --- a/mParticle-Apple-SDK/Include/MPBackendController.h +++ b/mParticle-Apple-SDK/Include/MPBackendController.h @@ -56,7 +56,7 @@ extern const NSInteger kInvalidKey; - (void)setOptOut:(BOOL)optOutStatus completionHandler:(void (^ _Nonnull)(BOOL optOut, MPExecStatus execStatus))completionHandler; - (nonnull NSMutableDictionary *)userAttributesForUserId:(nonnull NSNumber *)userId; - (void)startWithKey:(nonnull NSString *)apiKey secret:(nonnull NSString *)secret networkOptions:(nullable MPNetworkOptions *)networkOptions firstRun:(BOOL)firstRun installationType:(MPInstallationType)installationType startKitsAsync:(BOOL)startKitsAsync consentState:(MPConsentState *_Nullable)consentState completionHandler:(dispatch_block_t _Nonnull)completionHandler; -- (void)prepareBatchesForUpload:(nonnull MPUploadSettings *)uploadSettings; +- (void)prepareBatchesForUpload:(nonnull MPUploadSettings *)uploadSettings NS_SWIFT_NAME(prepareBatches(forUpload:)); - (MParticleSession* _Nullable)tempSession; - (void)endSession; - (void)beginTimedEvent:(nonnull MPEvent *)event completionHandler:(void (^ _Nonnull)(MPEvent * _Nonnull event, MPExecStatus execStatus))completionHandler; @@ -134,7 +134,7 @@ extern const NSInteger kInvalidKey; - (MPExecStatus)waitForKitsAndUploadWithCompletionHandler:(void (^ _Nullable)(void))completionHandler; - (nonnull NSMutableDictionary *)userAttributesForUserId:(nonnull NSNumber *)userId; - (nonnull NSMutableArray *> *)userIdentitiesForUserId:(nonnull NSNumber *)userId; -- (void)prepareBatchesForUpload:(nonnull MPUploadSettings *)uploadSettings; +- (void)prepareBatchesForUpload:(nonnull MPUploadSettings *)uploadSettings NS_SWIFT_NAME(prepareBatches(forUpload:)); #if TARGET_OS_IOS == 1 - (void)handleDeviceTokenNotification:(nonnull NSNotification *)notification; diff --git a/mParticle-Apple-SDK/Include/SceneDelegateHandler.h b/mParticle-Apple-SDK/Include/SceneDelegateHandler.h index 025f3563d..704364a05 100644 --- a/mParticle-Apple-SDK/Include/SceneDelegateHandler.h +++ b/mParticle-Apple-SDK/Include/SceneDelegateHandler.h @@ -5,7 +5,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol OpenURLHandlerProtocol -- (void)open:(NSURL *)url options:(nullable NSDictionary *)options; +- (void)openURL:(NSURL *)url options:(nullable NSDictionary *)options; - (BOOL)continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^_Nonnull)(NSArray> * _Nullable))restorationHandler; @end @@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)handleUserActivity:(NSUserActivity *)userActivity; #if TARGET_OS_IOS == 1 -- (void)handleWithUrlContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)); +- (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)); #endif @end diff --git a/mParticle-Apple-SDK/MPBackendController.m b/mParticle-Apple-SDK/MPBackendController.m index 6d0c7ef6c..724415999 100644 --- a/mParticle-Apple-SDK/MPBackendController.m +++ b/mParticle-Apple-SDK/MPBackendController.m @@ -320,50 +320,6 @@ - (void)logUserIdentityChange:(MPUserIdentityChangePRIVATE *)userIdentityChange [self saveMessage:message updateSession:YES]; } -- (NSNumber *)previousSessionSuccessfullyClosed { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSString *stateMachineDirectoryPath = STATE_MACHINE_DIRECTORY_PATH; - NSString *previousSessionStateFile = [stateMachineDirectoryPath stringByAppendingPathComponent:kMPPreviousSessionStateFileName]; - NSNumber *previousSessionSuccessfullyClosed = nil; - if ([fileManager fileExistsAtPath:previousSessionStateFile]) { - NSDictionary *previousSessionStateDictionary = [NSDictionary dictionaryWithContentsOfFile:previousSessionStateFile]; - previousSessionSuccessfullyClosed = previousSessionStateDictionary[kMPASTPreviousSessionSuccessfullyClosedKey]; - } - - if (previousSessionSuccessfullyClosed == nil) { - previousSessionSuccessfullyClosed = @YES; - } - - return previousSessionSuccessfullyClosed; -} - -- (void)setPreviousSessionSuccessfullyClosed:(NSNumber *)previousSessionSuccessfullyClosed { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSString *stateMachineDirectoryPath = STATE_MACHINE_DIRECTORY_PATH; - NSString *previousSessionStateFile = [stateMachineDirectoryPath stringByAppendingPathComponent:kMPPreviousSessionStateFileName]; - NSDictionary *previousSessionStateDictionary = @{kMPASTPreviousSessionSuccessfullyClosedKey:previousSessionSuccessfullyClosed}; - - @try { - if (![fileManager fileExistsAtPath:stateMachineDirectoryPath]) { - NSError *dirError = nil; - [fileManager createDirectoryAtPath:stateMachineDirectoryPath withIntermediateDirectories:YES attributes:nil error:&dirError]; - if (dirError) { - MPILogError(@"Failed to create state machine directory: %@", dirError); - return; - } - } else if ([fileManager fileExistsAtPath:previousSessionStateFile]) { - [fileManager removeItemAtPath:previousSessionStateFile error:nil]; - } - - BOOL success = [previousSessionStateDictionary writeToFile:previousSessionStateFile atomically:YES]; - if (!success) { - MPILogError(@"Failed to write previous session state to file"); - } - } @catch (NSException *exception) { - MPILogError(@"Exception writing previous session state: %@", exception); - } -} - - (void)processDidFinishLaunching:(NSNotification *)notification { NSString *astType = kMPASTInitKey; NSMutableDictionary *messageInfo = [[NSMutableDictionary alloc] initWithCapacity:3]; @@ -380,8 +336,6 @@ - (void)processDidFinishLaunching:(NSNotification *)notification { isInstallOrUpgrade = YES; } - messageInfo[kMPASTPreviousSessionSuccessfullyClosedKey] = [self previousSessionSuccessfullyClosed]; - NSDictionary *userInfo = [notification userInfo]; if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) { @@ -2007,7 +1961,6 @@ - (void)handleApplicationDidEnterBackground:(NSNotification *)notification { self.timeAppWentToBackgroundInCurrentSession = currentTime; self.timeOfLastEventInBackground = currentTime; - [self setPreviousSessionSuccessfullyClosed:@YES]; [self cleanUp]; MPMessageBuilder *messageBuilder = [[MPMessageBuilder alloc] initWithMessageType:MPMessageTypeAppStateTransition diff --git a/mParticle-Apple-SDK/MPIConstants.h b/mParticle-Apple-SDK/MPIConstants.h index 891bbc562..22de8a0cd 100644 --- a/mParticle-Apple-SDK/MPIConstants.h +++ b/mParticle-Apple-SDK/MPIConstants.h @@ -206,7 +206,6 @@ extern NSString * _Nonnull const kMPUploadIntervalKey; extern NSString * _Nonnull const kMPPreviousSessionLengthKey; extern NSString * _Nonnull const kMPLifeTimeValueKey; extern NSString * _Nonnull const kMPIncreasedLifeTimeValueKey; -extern NSString * _Nonnull const kMPPreviousSessionStateFileName; extern NSString * _Nonnull const kMPHTTPMethodPost; extern NSString * _Nonnull const kMPHTTPMethodGet; extern NSString * _Nonnull const kMPPreviousSessionIdKey; @@ -384,7 +383,6 @@ extern NSString * _Nonnull const kMPASTBackgroundKey; extern NSString * _Nonnull const kMPASTForegroundKey; extern NSString * _Nonnull const kMPASTIsFirstRunKey; extern NSString * _Nonnull const kMPASTIsUpgradeKey; -extern NSString * _Nonnull const kMPASTPreviousSessionSuccessfullyClosedKey; // Network performance extern NSString * _Nonnull const kMPNetworkPerformanceMeasurementNotification; diff --git a/mParticle-Apple-SDK/MPIConstants.m b/mParticle-Apple-SDK/MPIConstants.m index cc5b7c96b..dbc104621 100644 --- a/mParticle-Apple-SDK/MPIConstants.m +++ b/mParticle-Apple-SDK/MPIConstants.m @@ -1,7 +1,7 @@ #import "MPIConstants.h" // mParticle SDK Version -NSString *const kMParticleSDKVersion = @"8.43.1"; +NSString *const kMParticleSDKVersion = @"8.44.0"; // Message Type (dt) NSString *const kMPMessageTypeKey = @"dt"; @@ -139,7 +139,6 @@ NSString *const kMPPreviousSessionLengthKey = @"psl"; NSString *const kMPLifeTimeValueKey = @"ltv"; NSString *const kMPIncreasedLifeTimeValueKey = @"iltv"; -NSString *const kMPPreviousSessionStateFileName = @"PreviousSessionState.dic"; NSString *const kMPHTTPMethodPost = @"POST"; NSString *const kMPHTTPMethodGet = @"GET"; NSString *const kMPPreviousSessionIdKey = @"pid"; @@ -309,7 +308,6 @@ NSString *const kMPASTForegroundKey = @"app_fore"; NSString *const kMPASTIsFirstRunKey = @"ifr"; NSString *const kMPASTIsUpgradeKey = @"iu"; -NSString *const kMPASTPreviousSessionSuccessfullyClosedKey = @"sc"; // Network performance NSString *const kMPNetworkPerformanceMeasurementNotification = @"MPNetworkPerformanceMeasurement"; diff --git a/mParticle-Apple-SDK/SceneDelegateHandler.m b/mParticle-Apple-SDK/SceneDelegateHandler.m index 9c491be70..692cc73bf 100644 --- a/mParticle-Apple-SDK/SceneDelegateHandler.m +++ b/mParticle-Apple-SDK/SceneDelegateHandler.m @@ -17,7 +17,7 @@ - (instancetype)initWithAppNotificationHandler:(id)appNo } #if TARGET_OS_IOS -- (void)handleWithUrlContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)) { +- (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)) { MPILogDebug(@"Opening URLContext URL: %@", urlContext.URL); MPILogDebug(@"Source: %@", urlContext.options.sourceApplication ?: @"unknown"); @@ -34,7 +34,7 @@ - (void)handleWithUrlContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13 options[@"UIApplicationOpenURLOptionsSourceApplicationKey"] = urlContext.options.sourceApplication; } - [self.appNotificationHandler open:urlContext.URL options:options]; + [self.appNotificationHandler openURL:urlContext.URL options:options]; } #endif diff --git a/mParticle-Apple-SDK/mParticle.m b/mParticle-Apple-SDK/mParticle.m index 2bd2e87d0..724488382 100644 --- a/mParticle-Apple-SDK/mParticle.m +++ b/mParticle-Apple-SDK/mParticle.m @@ -658,7 +658,7 @@ - (void)handleActionWithIdentifier:(nullable NSString *)identifier forRemoteNoti } - (void)handleURLContext:(UIOpenURLContext *)urlContext API_AVAILABLE(ios(13.0)) { - [self.sceneDelegateHandler handleWithUrlContext:urlContext]; + [self.sceneDelegateHandler handleURLContext:urlContext]; } #endif diff --git a/mParticle_Apple_SDK.json b/mParticle_Apple_SDK.json index 485f9be55..9bb995bc5 100644 --- a/mParticle_Apple_SDK.json +++ b/mParticle_Apple_SDK.json @@ -126,5 +126,6 @@ "8.42.0": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.0/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.0/mParticle_Apple_SDK.xcframework.zip", "8.42.1": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.1/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.1/mParticle_Apple_SDK.xcframework.zip", "8.42.2": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.2/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.2/mParticle_Apple_SDK.xcframework.zip", - "8.43.1": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.43.1/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.43.1/mParticle_Apple_SDK.xcframework.zip" + "8.43.1": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.43.1/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.43.1/mParticle_Apple_SDK.xcframework.zip", + "8.44.0": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.44.0/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.44.0/mParticle_Apple_SDK.xcframework.zip" }