Skip to content

Commit

Permalink
Merge pull request #163 from klaviyo/as/chnl-6305-fix-user-properties
Browse files Browse the repository at this point in the history
CHNL-6305:  Fix set profile properties method
  • Loading branch information
ajaysubra authored Mar 19, 2024
2 parents e0e4c40 + 4163a27 commit c441ab5
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 88 deletions.
206 changes: 173 additions & 33 deletions Sources/KlaviyoSwift/KlaviyoState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct KlaviyoState: Equatable, Codable {
}
}

// state related stuff
var apiKey: String?
var email: String?
var anonymousId: String?
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
41 changes: 1 addition & 40 deletions Sources/KlaviyoSwift/StateManagement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -466,45 +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)
}

func buildTokenRequest(apiKey: String, anonymousId: String, pushToken: String, enablement: PushEnablement) -> KlaviyoAPI.KlaviyoRequest {
let payload = PushTokenPayload(
pushToken: pushToken,
enablement: enablement.rawValue,
background: environment.getBackgroundSetting().rawValue,
profile: .init(email: email, phoneNumber: phoneNumber, externalId: externalId),
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(
Expand Down
56 changes: 41 additions & 15 deletions Tests/KlaviyoSwiftTests/StateManagementTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,45 +355,71 @@ 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
}
}

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
}
}
Expand Down

0 comments on commit c441ab5

Please sign in to comment.