From 565703d789151ec0e84008299a86de2341e793e8 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Wed, 6 Aug 2025 14:08:48 -0400 Subject: [PATCH 1/3] fix: Expose RoktLayout object --- mParticle-Rokt-Swift/MPRoktLayout.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mParticle-Rokt-Swift/MPRoktLayout.swift b/mParticle-Rokt-Swift/MPRoktLayout.swift index 0695dca..9dda290 100644 --- a/mParticle-Rokt-Swift/MPRoktLayout.swift +++ b/mParticle-Rokt-Swift/MPRoktLayout.swift @@ -18,7 +18,7 @@ import mParticle_Rokt @available(iOS 15, *) public struct MPRoktLayout: View { - private var roktLayout: RoktLayout + public var roktLayout: RoktLayout public init( sdkTriggered: Binding, From aa80081883aadcbe34413a6e8d92e0962018793e Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Fri, 15 Aug 2025 10:11:07 -0400 Subject: [PATCH 2/3] fix: Add identify for SwiftUI --- mParticle-Rokt-Swift/MPRoktLayout.swift | 67 +++++++++++++++---- .../mParticle_Rokt_SwiftTests.swift | 44 ++++-------- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/mParticle-Rokt-Swift/MPRoktLayout.swift b/mParticle-Rokt-Swift/MPRoktLayout.swift index 9dda290..33f44af 100644 --- a/mParticle-Rokt-Swift/MPRoktLayout.swift +++ b/mParticle-Rokt-Swift/MPRoktLayout.swift @@ -17,8 +17,8 @@ import mParticle_Apple_SDK import mParticle_Rokt @available(iOS 15, *) -public struct MPRoktLayout: View { - public var roktLayout: RoktLayout +public class MPRoktLayout { + public var roktLayout: RoktLayout? = nil public init( sdkTriggered: Binding, @@ -28,19 +28,60 @@ public struct MPRoktLayout: View { config: RoktConfig? = nil, onEvent: ((RoktEvent) -> Void)? = nil ) { - let preparedAttributes = MPKitRokt.prepareAttributes(attributes, filteredUser: Optional.none, performMapping: true) + confirmUser(attributes: attributes) { + let preparedAttributes = MPKitRokt.prepareAttributes(attributes, filteredUser: Optional.none, performMapping: true) - self.roktLayout = RoktLayout.init( - sdkTriggered: sdkTriggered, - viewName: viewName, - locationName: locationName, - attributes: preparedAttributes, - config: config, - onEvent: onEvent - ) + self.roktLayout = RoktLayout.init( + sdkTriggered: sdkTriggered, + viewName: viewName, + locationName: locationName, + attributes: preparedAttributes, + config: config, + onEvent: onEvent + ) + } } +} - public var body: some View { - return self.roktLayout.body +func confirmUser( + attributes: [String: String]?, + completion: @escaping () -> Void +) { + let email = attributes?["email"] + let hashedEmail = attributes?["emailsha256"] + + if let user = MParticle.sharedInstance().identity.currentUser { + let userEmailIdentity = user.identities[NSNumber(value: MPIdentity.email.rawValue)] + let userHashedEmailIdentity = user.identities[NSNumber(value: MPIdentity.other.rawValue)] + + let emailMismatch = email != nil && email != userEmailIdentity + let hashedEmailMismatch = hashedEmail != nil && hashedEmail != userHashedEmailIdentity + + if emailMismatch || hashedEmailMismatch { + // If there is an existing email or hashed email but it doesn't match what was passed in, warn the customer + if let email = email, let userEmail = userEmailIdentity { + print("The existing email on the user (\(userEmail)) does not match the email passed in to `selectPlacements:` (\(email)). Please remember to sync the email identity to mParticle as soon as you receive it. We will now identify the user before continuing to `selectPlacements:`") + } else if let hashedEmail = hashedEmail, let userHashedEmail = userHashedEmailIdentity { + print("The existing hashed email on the user (\(userHashedEmail)) does not match the email passed in to `selectPlacements:` (\(hashedEmail)). Please remember to sync the email identity to mParticle as soon as you receive it. We will now identify the user before continuing to `selectPlacements:`") + } + + let identityRequest = MPIdentityApiRequest(user: user) + identityRequest.setIdentity(email, identityType: .email) + identityRequest.setIdentity(hashedEmail, identityType: .other) + + MParticle.sharedInstance().identity.identify(identityRequest) {apiResult, error in + if let error = error { + print("Failed to sync email from selectPlacement to user: \(error)") + completion() + } else { + if let identities = apiResult?.user.identities { + print("Updated user identity based off selectPlacement's attributes: \(identities)") + } + completion() + } + } + } else { + completion() + } } } diff --git a/mParticle_RoktTests/mParticle_Rokt_SwiftTests.swift b/mParticle_RoktTests/mParticle_Rokt_SwiftTests.swift index ea34e61..3a1dae7 100644 --- a/mParticle_RoktTests/mParticle_Rokt_SwiftTests.swift +++ b/mParticle_RoktTests/mParticle_Rokt_SwiftTests.swift @@ -31,7 +31,7 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout body should not be nil") + #expect(layout.roktLayout != nil, "Layout.roktLayout should not be nil") } @MainActor @available(iOS 15, *) @@ -58,7 +58,7 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout body should not be nil") + #expect(layout.roktLayout != nil, "Layout.roktLayout should not be nil") } @MainActor @available(iOS 15, *) @@ -76,7 +76,7 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout should handle empty attributes") + #expect(layout.roktLayout != nil, "Layout should handle empty attributes") } @MainActor @available(iOS 15, *) @@ -94,7 +94,7 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout should handle sandbox attribute") + #expect(layout.roktLayout != nil, "Layout should handle sandbox attribute") } // MARK: - Attribute Preparation Tests @@ -183,25 +183,7 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout is any View, "MPRoktLayout should conform to SwiftUI View protocol") - } - - @MainActor @available(iOS 15, *) - @Test func testMPRoktLayoutBodyProperty() { - // Given - let sdkTriggered = Binding.constant(false) - let attributes: [String: String] = ["test": "value"] - - // When - let layout = MPRoktLayout( - sdkTriggered: sdkTriggered, - locationName: "test", - attributes: attributes - ) - - // Then - let body = layout.body - #expect(body != nil, "Layout body should be accessible") + #expect(layout.roktLayout is any View, "MPRoktLayout should conform to SwiftUI View protocol") } // MARK: - Parameter Validation Tests @@ -212,16 +194,16 @@ struct mParticle_Rokt_SwiftTests { let sdkTriggered = Binding.constant(false) let longLocationName = String(repeating: "a", count: 1000) let attributes: [String: String] = ["test": "value"] - + // When let layout = MPRoktLayout( sdkTriggered: sdkTriggered, locationName: longLocationName, attributes: attributes ) - + // Then - #expect(layout.body != nil, "Layout should handle long location names") + #expect(layout.roktLayout != nil, "Layout should handle long location names") } @MainActor @available(iOS 15, *) @@ -242,7 +224,7 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout should handle special characters and unicode") + #expect(layout.roktLayout != nil, "Layout should handle special characters and unicode") } // MARK: - State Management Tests @@ -261,13 +243,13 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout should be created with initial state") + #expect(layout.roktLayout != nil, "Layout should be created with initial state") // When state changes sdkTriggered.wrappedValue = true // Then - #expect(layout.body != nil, "Layout should handle state changes") + #expect(layout.roktLayout != nil, "Layout should handle state changes") } // MARK: - Integration Tests @@ -291,7 +273,7 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout should properly integrate attribute processing") + #expect(layout.roktLayout == nil, "Layout should attempt to identify user attributes and fail") } @MainActor @available(iOS 15, *) @@ -325,6 +307,6 @@ struct mParticle_Rokt_SwiftTests { ) // Then - #expect(layout.body != nil, "Layout should handle complex configurations") + #expect(layout.roktLayout != nil, "Layout should handle complex configurations") } } From d511dc19c4b5fd9c50da3470e111fd5bcda69c2e Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Fri, 15 Aug 2025 14:19:46 -0400 Subject: [PATCH 3/3] refactored code --- mParticle-Rokt-Swift/MPRoktLayout.swift | 69 +++++++++++++++---------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/mParticle-Rokt-Swift/MPRoktLayout.swift b/mParticle-Rokt-Swift/MPRoktLayout.swift index 33f44af..3b8e51a 100644 --- a/mParticle-Rokt-Swift/MPRoktLayout.swift +++ b/mParticle-Rokt-Swift/MPRoktLayout.swift @@ -19,6 +19,7 @@ import mParticle_Rokt @available(iOS 15, *) public class MPRoktLayout { public var roktLayout: RoktLayout? = nil + let mparticle = MParticle.sharedInstance() public init( sdkTriggered: Binding, @@ -41,16 +42,18 @@ public class MPRoktLayout { ) } } -} - -func confirmUser( - attributes: [String: String]?, - completion: @escaping () -> Void -) { - let email = attributes?["email"] - let hashedEmail = attributes?["emailsha256"] - if let user = MParticle.sharedInstance().identity.currentUser { + func confirmUser( + attributes: [String: String]?, + completion: @escaping () -> Void + ) { + guard let user = mparticle.identity.currentUser else { + completion() + return + } + let email = attributes?["email"] + let hashedEmail = attributes?["emailsha256"] + let userEmailIdentity = user.identities[NSNumber(value: MPIdentity.email.rawValue)] let userHashedEmailIdentity = user.identities[NSNumber(value: MPIdentity.other.rawValue)] @@ -59,29 +62,39 @@ func confirmUser( if emailMismatch || hashedEmailMismatch { // If there is an existing email or hashed email but it doesn't match what was passed in, warn the customer - if let email = email, let userEmail = userEmailIdentity { - print("The existing email on the user (\(userEmail)) does not match the email passed in to `selectPlacements:` (\(email)). Please remember to sync the email identity to mParticle as soon as you receive it. We will now identify the user before continuing to `selectPlacements:`") - } else if let hashedEmail = hashedEmail, let userHashedEmail = userHashedEmailIdentity { - print("The existing hashed email on the user (\(userHashedEmail)) does not match the email passed in to `selectPlacements:` (\(hashedEmail)). Please remember to sync the email identity to mParticle as soon as you receive it. We will now identify the user before continuing to `selectPlacements:`") + if emailMismatch { + print("The existing email on the user (\(userEmailIdentity ?? "nil")) does not match the email passed in to `selectPlacements:` (\(email ?? "nil")). Please remember to sync the email identity to mParticle as soon as you receive it. We will now identify the user before creating the layout") } - - let identityRequest = MPIdentityApiRequest(user: user) - identityRequest.setIdentity(email, identityType: .email) - identityRequest.setIdentity(hashedEmail, identityType: .other) - - MParticle.sharedInstance().identity.identify(identityRequest) {apiResult, error in - if let error = error { - print("Failed to sync email from selectPlacement to user: \(error)") - completion() - } else { - if let identities = apiResult?.user.identities { - print("Updated user identity based off selectPlacement's attributes: \(identities)") - } - completion() - } + if hashedEmailMismatch { + print("The existing hashed email on the user (\(userHashedEmailIdentity ?? "nil")) does not match the email passed in to `selectPlacements:` (\(hashedEmail ?? "nil")). Please remember to sync the email identity to mParticle as soon as you receive it. We will now identify the user before creating the layout") } + + syncIdentities(user: user, email: email, hashedEmail: hashedEmail, completion: completion) } else { completion() } } + + func syncIdentities( + user: MParticleUser, + email: String?, + hashedEmail: String?, + completion: @escaping () -> Void + ) { + let identityRequest = MPIdentityApiRequest(user: user) + identityRequest.setIdentity(email, identityType: .email) + identityRequest.setIdentity(hashedEmail, identityType: .other) + + mparticle.identity.identify(identityRequest) {apiResult, error in + if let error = error { + print("Failed to sync email from selectPlacement to user: \(error)") + completion() + } else { + if let identities = apiResult?.user.identities { + print("Updated user identity based off selectPlacement's attributes: \(identities)") + } + completion() + } + } + } }