From c1c0d54ddf06cec0115ff318e62d6e94dd1a1404 Mon Sep 17 00:00:00 2001 From: Ajay Subramanya Date: Fri, 8 Mar 2024 11:17:03 -0600 Subject: [PATCH 1/3] fixed profile properties not being sent during token register call --- Sources/KlaviyoSwift/StateManagement.swift | 88 +++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/Sources/KlaviyoSwift/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement.swift index ba9ac624..fa48320c 100644 --- a/Sources/KlaviyoSwift/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement.swift @@ -484,12 +484,96 @@ extension KlaviyoState { return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) } - func buildTokenRequest(apiKey: String, anonymousId: String, pushToken: String, enablement: PushEnablement) -> KlaviyoAPI.KlaviyoRequest { + mutating func buildTokenRequest(apiKey: String, anonymousId: String, pushToken: String, enablement: PushEnablement) -> KlaviyoAPI.KlaviyoRequest { + print("in token update", pendingProfile) + guard let pendingProfile = pendingProfile else { + let payload = PushTokenPayload( + pushToken: pushToken, + enablement: enablement.rawValue, + background: environment.getBackgroundSetting().rawValue, + profile: Profile(email: email, phoneNumber: phoneNumber, externalId: externalId), + anonymousId: anonymousId) + let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.registerPushToken(payload) + return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) + } + + var firstName: String? + var lastName: String? + var address1: String? + var address2: String? + var title: String? + var organization: String? + var city: String? + var region: String? + var country: String? + var zip: String? + var image: String? + var latitude: Double? + var longitude: Double? + var customProperties: [String: Any] = [:] + + for (key, value) in pendingProfile { + switch key { + case .firstName: + firstName = value.value as? String + case .lastName: + lastName = value.value as? String + case .address1: + address1 = value.value as? String + case .address2: + address2 = value.value as? String + case .title: + title = value.value as? String + case .organization: + organization = value.value as? String + case .city: + city = value.value as? String + case .region: + region = value.value as? String + case .country: + country = value.value as? String + case .zip: + zip = value.value as? String + case .image: + image = value.value as? String + case .latitude: + latitude = value.value as? Double + case .longitude: + longitude = value.value as? Double + case let .custom(customKey: customKey): + customProperties[customKey] = value.value + } + } + + let location = Profile.Location( + address1: address1, + address2: address2, + city: city, + country: country, + latitude: latitude, + longitude: longitude, + region: region, + zip: zip) + + let profile = Profile( + email: email, + phoneNumber: phoneNumber, + externalId: externalId, + firstName: firstName, + lastName: lastName, + organization: organization, + title: title, + image: image, + location: location, + properties: customProperties) + + self.pendingProfile = nil + let payload = PushTokenPayload( pushToken: pushToken, enablement: enablement.rawValue, background: environment.getBackgroundSetting().rawValue, - profile: .init(email: email, phoneNumber: phoneNumber, externalId: externalId), + profile: profile, anonymousId: anonymousId) let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.registerPushToken(payload) return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) From 2ecc0008c0367c12e47d3691efe64cd6aac17872 Mon Sep 17 00:00:00 2001 From: Ajay Subramanya Date: Fri, 8 Mar 2024 16:00:50 -0600 Subject: [PATCH 2/3] some refactor --- Sources/KlaviyoSwift/KlaviyoState.swift | 206 +++++++++++++++++---- Sources/KlaviyoSwift/StateManagement.swift | 125 +------------ 2 files changed, 174 insertions(+), 157 deletions(-) diff --git a/Sources/KlaviyoSwift/KlaviyoState.swift b/Sources/KlaviyoSwift/KlaviyoState.swift index 3370515a..5791c23d 100644 --- a/Sources/KlaviyoSwift/KlaviyoState.swift +++ b/Sources/KlaviyoSwift/KlaviyoState.swift @@ -84,6 +84,7 @@ struct KlaviyoState: Equatable, Codable { } } + // state related stuff var apiKey: String? var email: String? var anonymousId: String? @@ -190,42 +191,53 @@ struct KlaviyoState: Equatable, Codable { var attributes = profile.data.attributes var location = profile.data.attributes.location ?? .init() var properties = profile.data.attributes.properties.value as? [String: Any] ?? [:] + let updatedProfile = Profile.updateProfileWithProperties(dict: pendingProfile) - // Optionally (if not already specified) overwrite attributes and location with pending profile information. - for (key, value) in pendingProfile { - switch key { - case .firstName: - attributes.firstName = attributes.firstName ?? value.value as? String - case .lastName: - attributes.lastName = attributes.lastName ?? value.value as? String - case .address1: - location.address1 = location.address1 ?? value.value as? String - case .address2: - location.address2 = location.address2 ?? value.value as? String - case .title: - attributes.title = attributes.title ?? value.value as? String - case .organization: - attributes.organization = attributes.organization ?? value.value as? String - case .city: - location.city = location.city ?? value.value as? String - case .region: - location.region = location.region ?? value.value as? String - case .country: - location.country = location.country ?? value.value as? String - case .zip: - location.zip = location.zip ?? value.value as? String - case .image: - attributes.image = attributes.image ?? value.value as? String - case .latitude: - location.latitude = location.latitude ?? value.value as? Double - case .longitude: - location.longitude = location.longitude ?? value.value as? Double - case let .custom(customKey: customKey): - properties[customKey] = properties[customKey] ?? value.value - } + if let firstName = updatedProfile.firstName { + attributes.firstName = attributes.firstName ?? firstName } + if let lastName = updatedProfile.lastName { + attributes.lastName = attributes.lastName ?? lastName + } + if let title = updatedProfile.title { + attributes.title = attributes.title ?? title + } + if let organization = updatedProfile.organization { + attributes.organization = attributes.organization ?? organization + } + if !updatedProfile.properties.isEmpty { + attributes.properties = AnyCodable(properties.merging(updatedProfile.properties, uniquingKeysWith: { _, new in new })) + } + + if let address1 = updatedProfile.location?.address1 { + location.address1 = location.address1 ?? address1 + } + if let address2 = updatedProfile.location?.address2 { + location.address2 = location.address2 ?? address2 + } + if let city = updatedProfile.location?.city { + location.city = location.city ?? city + } + if let region = updatedProfile.location?.region { + location.region = location.region ?? region + } + if let country = updatedProfile.location?.country { + location.country = location.country ?? country + } + if let zip = updatedProfile.location?.zip { + location.zip = location.zip ?? zip + } + if let image = updatedProfile.image { + attributes.image = attributes.image ?? image + } + if let latitude = updatedProfile.location?.latitude { + location.latitude = location.latitude ?? latitude + } + if let longitude = updatedProfile.location?.longitude { + location.longitude = location.longitude ?? longitude + } + attributes.location = location - attributes.properties = AnyCodable(properties) self.pendingProfile = nil return .init(data: .init(attributes: attributes)) @@ -278,6 +290,54 @@ struct KlaviyoState: Equatable, Codable { return pushTokenData != newPushTokenData } + + func buildProfileRequest(apiKey: String, anonymousId: String, properties: [String: Any] = [:]) -> KlaviyoAPI.KlaviyoRequest { + let payload = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.CreateProfilePayload( + data: .init( + profile: Profile( + email: email, + phoneNumber: phoneNumber, + externalId: externalId, + properties: properties), + anonymousId: anonymousId) + ) + let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.createProfile(payload) + + return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) + } + + mutating func buildTokenRequest(apiKey: String, anonymousId: String, pushToken: String, enablement: PushEnablement) -> KlaviyoAPI.KlaviyoRequest { + var profile: Profile + + if let pendingProfile = pendingProfile { + profile = Profile.updateProfileWithProperties( + email: email, + phoneNumber: phoneNumber, + externalId: externalId, + dict: pendingProfile) + self.pendingProfile = nil + } else { + profile = Profile(email: email, phoneNumber: phoneNumber, externalId: externalId) + } + + let payload = PushTokenPayload( + pushToken: pushToken, + enablement: enablement.rawValue, + background: environment.getBackgroundSetting().rawValue, + profile: profile, + anonymousId: anonymousId) + let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.registerPushToken(payload) + return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) + } + + func buildUnregisterRequest(apiKey: String, anonymousId: String, pushToken: String) -> KlaviyoAPI.KlaviyoRequest { + let payload = UnregisterPushTokenPayload( + pushToken: pushToken, + profile: .init(email: email, phoneNumber: phoneNumber, externalId: externalId), + anonymousId: anonymousId) + let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.unregisterPushToken(payload) + return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) + } } // MARK: Klaviyo state persistence @@ -347,3 +407,83 @@ private func createAndStoreInitialState(with apiKey: String, at file: URL) -> Kl storeKlaviyoState(state: state, file: file) return state } + +extension Profile { + fileprivate static func updateProfileWithProperties( + email: String? = nil, + phoneNumber: String? = nil, + externalId: String? = nil, + dict: [Profile.ProfileKey: AnyEncodable]) -> Self { + var firstName: String? + var lastName: String? + var address1: String? + var address2: String? + var title: String? + var organization: String? + var city: String? + var region: String? + var country: String? + var zip: String? + var image: String? + var latitude: Double? + var longitude: Double? + var customProperties: [String: Any] = [:] + + for (key, value) in dict { + switch key { + case .firstName: + firstName = value.value as? String + case .lastName: + lastName = value.value as? String + case .address1: + address1 = value.value as? String + case .address2: + address2 = value.value as? String + case .title: + title = value.value as? String + case .organization: + organization = value.value as? String + case .city: + city = value.value as? String + case .region: + region = value.value as? String + case .country: + country = value.value as? String + case .zip: + zip = value.value as? String + case .image: + image = value.value as? String + case .latitude: + latitude = value.value as? Double + case .longitude: + longitude = value.value as? Double + case let .custom(customKey: customKey): + customProperties[customKey] = value.value + } + } + + let location = Profile.Location( + address1: address1, + address2: address2, + city: city, + country: country, + latitude: latitude, + longitude: longitude, + region: region, + zip: zip) + + let profile = Profile( + email: email, + phoneNumber: phoneNumber, + externalId: externalId, + firstName: firstName, + lastName: lastName, + organization: organization, + title: title, + image: image, + location: location, + properties: customProperties) + + return profile + } +} diff --git a/Sources/KlaviyoSwift/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement.swift index fa48320c..cf576b6d 100644 --- a/Sources/KlaviyoSwift/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement.swift @@ -217,7 +217,7 @@ struct KlaviyoReducer: ReducerProtocol { state.pendingRequests.append(.pushToken(pushToken, enablement)) return .none } - guard state.shouldSendTokenUpdate(newToken: pushToken, enablement: enablement) else { + if !state.shouldSendTokenUpdate(newToken: pushToken, enablement: enablement) { return .none } @@ -466,129 +466,6 @@ extension Store where State == KlaviyoState, Action == KlaviyoAction { reducer: KlaviyoReducer()) } -extension KlaviyoState { - func checkPreconditions() {} - - func buildProfileRequest(apiKey: String, anonymousId: String, properties: [String: Any] = [:]) -> KlaviyoAPI.KlaviyoRequest { - let payload = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.CreateProfilePayload( - data: .init( - profile: Profile( - email: email, - phoneNumber: phoneNumber, - externalId: externalId, - properties: properties), - anonymousId: anonymousId) - ) - let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.createProfile(payload) - - return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) - } - - mutating func buildTokenRequest(apiKey: String, anonymousId: String, pushToken: String, enablement: PushEnablement) -> KlaviyoAPI.KlaviyoRequest { - print("in token update", pendingProfile) - guard let pendingProfile = pendingProfile else { - let payload = PushTokenPayload( - pushToken: pushToken, - enablement: enablement.rawValue, - background: environment.getBackgroundSetting().rawValue, - profile: Profile(email: email, phoneNumber: phoneNumber, externalId: externalId), - anonymousId: anonymousId) - let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.registerPushToken(payload) - return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) - } - - var firstName: String? - var lastName: String? - var address1: String? - var address2: String? - var title: String? - var organization: String? - var city: String? - var region: String? - var country: String? - var zip: String? - var image: String? - var latitude: Double? - var longitude: Double? - var customProperties: [String: Any] = [:] - - for (key, value) in pendingProfile { - switch key { - case .firstName: - firstName = value.value as? String - case .lastName: - lastName = value.value as? String - case .address1: - address1 = value.value as? String - case .address2: - address2 = value.value as? String - case .title: - title = value.value as? String - case .organization: - organization = value.value as? String - case .city: - city = value.value as? String - case .region: - region = value.value as? String - case .country: - country = value.value as? String - case .zip: - zip = value.value as? String - case .image: - image = value.value as? String - case .latitude: - latitude = value.value as? Double - case .longitude: - longitude = value.value as? Double - case let .custom(customKey: customKey): - customProperties[customKey] = value.value - } - } - - let location = Profile.Location( - address1: address1, - address2: address2, - city: city, - country: country, - latitude: latitude, - longitude: longitude, - region: region, - zip: zip) - - let profile = Profile( - email: email, - phoneNumber: phoneNumber, - externalId: externalId, - firstName: firstName, - lastName: lastName, - organization: organization, - title: title, - image: image, - location: location, - properties: customProperties) - - self.pendingProfile = nil - - let payload = PushTokenPayload( - pushToken: pushToken, - enablement: enablement.rawValue, - background: environment.getBackgroundSetting().rawValue, - profile: profile, - anonymousId: anonymousId) - let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.registerPushToken(payload) - return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) - } - - func buildUnregisterRequest(apiKey: String, anonymousId: String, pushToken: String) -> KlaviyoAPI.KlaviyoRequest { - let payload = UnregisterPushTokenPayload( - pushToken: pushToken, - profile: .init(email: email, phoneNumber: phoneNumber, externalId: externalId), - anonymousId: anonymousId) - let endpoint = KlaviyoAPI.KlaviyoRequest.KlaviyoEndpoint.unregisterPushToken(payload) - return KlaviyoAPI.KlaviyoRequest(apiKey: apiKey, endpoint: endpoint) - } -} - extension Event { func updateEventWithState(state: inout KlaviyoState) -> Event { let identifiers = Identifiers( From 5db7f4d300019dd5e20eff6fc1e0fb0c6492fad2 Mon Sep 17 00:00:00 2001 From: Ajay Subramanya Date: Thu, 14 Mar 2024 14:15:40 -0500 Subject: [PATCH 3/3] updated exissting tests --- .../StateManagementTests.swift | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/Tests/KlaviyoSwiftTests/StateManagementTests.swift b/Tests/KlaviyoSwiftTests/StateManagementTests.swift index 4474d94e..aa90c734 100644 --- a/Tests/KlaviyoSwiftTests/StateManagementTests.swift +++ b/Tests/KlaviyoSwiftTests/StateManagementTests.swift @@ -355,26 +355,26 @@ class StateManagementTests: XCTestCase { initialState.flushing = false let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - let profileActions: [(Profile.ProfileKey, Any)] = [ - (.city, "Sharon"), - (.region, "New England"), - (.address1, "123 Main Street"), - (.address2, "Apt 6"), - (.zip, "02067"), - (.country, "Mexico"), - (.latitude, 23.0), - (.longitude, 46.0), - (.title, "King"), - (.organization, "Klaviyo"), - (.firstName, "Jeffrey"), - (.lastName, "Lebowski"), - (.image, "foto.png"), + let profileAttributes: [(Profile.ProfileKey, Any)] = [ + (.city, Profile.test.location!.city!), + (.region, Profile.test.location!.region!), + (.address1, Profile.test.location!.address1!), + (.address2, Profile.test.location!.address2!), + (.zip, Profile.test.location!.zip!), + (.country, Profile.test.location!.country!), + (.latitude, Profile.test.location!.latitude!), + (.longitude, Profile.test.location!.longitude!), + (.title, Profile.test.title!), + (.organization, Profile.test.organization!), + (.firstName, Profile.test.firstName!), + (.lastName, Profile.test.lastName!), + (.image, Profile.test.image!), (.custom(customKey: "foo"), 20) ] var pendingProfile = [Profile.ProfileKey: AnyEncodable]() - for (key, value) in profileActions { + for (key, value) in profileAttributes { pendingProfile[key] = AnyEncodable(value) _ = await store.send(.setProfileProperty(key, AnyEncodable(value))) { $0.pendingProfile = pendingProfile @@ -382,18 +382,44 @@ class StateManagementTests: XCTestCase { } var request: KlaviyoAPI.KlaviyoRequest? + _ = await store.send(.flushQueue) { $0.enqueueProfileOrTokenRequest() $0.requestsInFlight = $0.queue $0.queue = [] $0.flushing = true + $0.pendingProfile = nil request = $0.requestsInFlight[0] + switch request?.endpoint { + case let .registerPushToken(payload): + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.city, Profile.test.location!.city) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.region, Profile.test.location!.region!) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.address1, Profile.test.location!.address1!) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.address2, Profile.test.location!.address2!) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.zip, Profile.test.location!.zip!) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.country, Profile.test.location!.country!) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.latitude, Profile.test.location!.latitude!) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.location?.longitude, Profile.test.location!.longitude!) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.title, Profile.test.title) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.organization, Profile.test.organization) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.firstName, Profile.test.firstName) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.lastName, Profile.test.lastName) + XCTAssertEqual(payload.data.attributes.profile.data.attributes.image, Profile.test.image) + + if let customProperties = payload.data.attributes.profile.data.attributes.properties.value as? [String: Any], + let foo = customProperties["foo"] as? Int { + XCTAssertEqual(foo, 20) + } + default: + XCTFail("Wrong endpoint called, expected token update when store's initial state contains token data") + } } await store.receive(.sendRequest) await store.receive(.deQueueCompletedResults(request!)) { $0.requestsInFlight = $0.queue $0.flushing = false + $0.pendingProfile = nil $0.pushTokenData = initialState.pushTokenData } }