Files
purchases-ios/Sources/SubscriberAttributes/SubscriberAttributesManager.swift
T
NachoSoto a2fc04c09e SubscriberAttribute: converted into struct (#1648)
### Motivation:

Initially I wanted to make `SubscriberAttribute.asDictionary` work using `Codable`. However, this isn't the right refactor because we need the actual raw types to be encoded inside `UserDefaults`.
At least this refactor provides several improvements:

### Changes:

- `SubscriberAttribute` now has value semantics
- ⚠️  `SubscriberAttribute.isSynced` used to be mutable, with reference semantics, which was dangerous
- `SubscriberAttribute: Equatable` implementation is now automatic
- Cleaned up `SubscriberAttribute.asDictionary` implementation, extracting keys into an `enum`
- Moved `Dictionary` -> `SubscriberAttribute` creation to the type itself
- ⚠️ `SubscriberAttribute.init(dictionary:)` returns an `Optional` instead of using force-unwrap, avoiding potential crashes
- Added more serialization / deserialization tests
- Removed `SubscriberAttributesMarshaller` and moved method into `SubscriberAttribute` extension
- Moved `SubscriberAttributeDict` into `SubscriberAttribute.Dictionary`
2022-06-17 10:00:51 -07:00

279 lines
11 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// SubscriberAttributesManager.swift
//
// Created by Madeline Beyl on 8/10/21.
import Foundation
class SubscriberAttributesManager {
private let backend: Backend
private let deviceCache: DeviceCache
private let operationDispatcher: OperationDispatcher
private let attributionFetcher: AttributionFetcher
private let attributionDataMigrator: AttributionDataMigrator
private let lock = Lock()
init(backend: Backend,
deviceCache: DeviceCache,
operationDispatcher: OperationDispatcher,
attributionFetcher: AttributionFetcher,
attributionDataMigrator: AttributionDataMigrator) {
self.backend = backend
self.deviceCache = deviceCache
self.operationDispatcher = operationDispatcher
self.attributionFetcher = attributionFetcher
self.attributionDataMigrator = attributionDataMigrator
}
func setAttributes(_ attributes: [String: String], appUserID: String) {
Logger.debug(Strings.attribution.setting_attributes(attributes: Array(attributes.keys)))
for (key, value) in attributes {
setAttribute(key: key, value: value, appUserID: appUserID)
}
}
func setEmail(_ email: String?, appUserID: String) {
setReservedAttribute(.email, value: email, appUserID: appUserID)
}
func setPhoneNumber(_ phoneNumber: String?, appUserID: String) {
setReservedAttribute(.phoneNumber, value: phoneNumber, appUserID: appUserID)
}
func setDisplayName(_ displayName: String?, appUserID: String) {
setReservedAttribute(.displayName, value: displayName, appUserID: appUserID)
}
func setPushToken(_ pushToken: Data?, appUserID: String) {
let pushTokenString = pushToken?.asString
setPushTokenString(pushTokenString, appUserID: appUserID)
}
func setPushTokenString(_ pushTokenString: String?, appUserID: String) {
setReservedAttribute(.pushToken, value: pushTokenString, appUserID: appUserID)
}
func setAdjustID(_ adjustID: String?, appUserID: String) {
setAttributionID(adjustID, forNetworkID: .adjustID, appUserID: appUserID)
}
func setAppsflyerID(_ appsflyerID: String?, appUserID: String) {
setAttributionID(appsflyerID, forNetworkID: .appsFlyerID, appUserID: appUserID)
}
func setFBAnonymousID(_ fBAnonymousID: String?, appUserID: String) {
setAttributionID(fBAnonymousID, forNetworkID: .fBAnonID, appUserID: appUserID)
}
func setMparticleID(_ mparticleID: String?, appUserID: String) {
setAttributionID(mparticleID, forNetworkID: .mpParticleID, appUserID: appUserID)
}
func setOnesignalID(_ onesignalID: String?, appUserID: String) {
setReservedAttribute(.oneSignalID, value: onesignalID, appUserID: appUserID)
}
func setAirshipChannelID(_ airshipChannelID: String?, appUserID: String) {
setReservedAttribute(.airshipChannelID, value: airshipChannelID, appUserID: appUserID)
}
func setCleverTapID(_ cleverTapID: String?, appUserID: String) {
setReservedAttribute(.cleverTapID, value: cleverTapID, appUserID: appUserID)
}
func setMixpanelDistinctID(_ mixpanelDistinctID: String?, appUserID: String) {
setReservedAttribute(.mixpanelDistinctID, value: mixpanelDistinctID, appUserID: appUserID)
}
func setFirebaseAppInstanceID(_ firebaseAppInstanceID: String?, appUserID: String) {
setReservedAttribute(.firebaseAppInstanceID, value: firebaseAppInstanceID, appUserID: appUserID)
}
func setMediaSource(_ mediaSource: String?, appUserID: String) {
setReservedAttribute(.mediaSource, value: mediaSource, appUserID: appUserID)
}
func setCampaign(_ campaign: String?, appUserID: String) {
setReservedAttribute(.campaign, value: campaign, appUserID: appUserID)
}
func setAdGroup(_ adGroup: String?, appUserID: String) {
setReservedAttribute(.adGroup, value: adGroup, appUserID: appUserID)
}
// swiftlint:disable:next identifier_name
func setAd(_ ad: String?, appUserID: String) {
setReservedAttribute(.ad, value: ad, appUserID: appUserID)
}
func setKeyword(_ keyword: String?, appUserID: String) {
setReservedAttribute(.keyword, value: keyword, appUserID: appUserID)
}
func setCreative(_ creative: String?, appUserID: String) {
setReservedAttribute(.creative, value: creative, appUserID: appUserID)
}
func collectDeviceIdentifiers(forAppUserID appUserID: String) {
let identifierForAdvertisers = attributionFetcher.identifierForAdvertisers
let identifierForVendor = attributionFetcher.identifierForVendor
setReservedAttribute(.idfa, value: identifierForAdvertisers, appUserID: appUserID)
setReservedAttribute(.idfv, value: identifierForVendor, appUserID: appUserID)
setReservedAttribute(.ip, value: "true", appUserID: appUserID)
}
/// - Parameter syncedAttribute: will be called for every attribute that is updated
/// - Parameter completion: will be called once all attributes have completed syncing
/// - Returns: the number of attributes that will be synced
@discardableResult
func syncAttributesForAllUsers(currentAppUserID: String,
syncedAttribute: ((Error?) -> Void)? = nil,
completion: (() -> Void)? = nil) -> Int {
let unsyncedAttributesForAllUsers = unsyncedAttributesByKeyForAllUsers()
let total = unsyncedAttributesForAllUsers.count
operationDispatcher.dispatchOnWorkerThread {
let completed: Atomic<Int> = .init(0)
for (syncingAppUserId, attributes) in unsyncedAttributesForAllUsers {
self.syncAttributes(attributes: attributes, appUserID: syncingAppUserId) { error in
self.handleAttributesSynced(syncingAppUserId: syncingAppUserId,
currentAppUserId: currentAppUserID,
error: error)
syncedAttribute?(error?.asPurchasesError)
let completedSoFar: Int = completed.modify { $0 += 1; return $0 }
if completedSoFar == total {
completion?()
}
}
}
if total == 0 {
completion?()
}
}
return total
}
func handleAttributesSynced(syncingAppUserId: String, currentAppUserId: String, error: Error?) {
if error == nil {
Logger.rcSuccess(Strings.attribution.attributes_sync_success(appUserID: syncingAppUserId))
if syncingAppUserId != currentAppUserId {
deviceCache.deleteAttributesIfSynced(appUserID: syncingAppUserId)
}
} else {
let receivedNSError = error as NSError?
Logger.error(Strings.attribution.attributes_sync_error(error: receivedNSError))
}
}
func unsyncedAttributesByKey(appUserID: String) -> SubscriberAttribute.Dictionary {
let unsyncedAttributes = deviceCache.unsyncedAttributesByKey(appUserID: appUserID)
Logger.debug(Strings.attribution.unsynced_attributes_count(unsyncedAttributesCount: unsyncedAttributes.count,
appUserID: appUserID))
if !unsyncedAttributes.isEmpty {
Logger.debug(Strings.attribution.unsynced_attributes(unsyncedAttributes: unsyncedAttributes))
}
return unsyncedAttributes
}
func unsyncedAttributesByKeyForAllUsers() -> [String: SubscriberAttribute.Dictionary] {
return deviceCache.unsyncedAttributesForAllUsers()
}
func markAttributesAsSynced(_ attributesToSync: SubscriberAttribute.Dictionary?, appUserID: String) {
guard let attributesToSync = attributesToSync,
!attributesToSync.isEmpty else {
return
}
Logger.info(Strings.attribution.marking_attributes_synced(appUserID: appUserID, attributes: attributesToSync))
self.lock.perform {
var unsyncedAttributes = self.unsyncedAttributesByKey(appUserID: appUserID)
for (key, attribute) in attributesToSync {
if unsyncedAttributes[key]?.value == attribute.value {
unsyncedAttributes[key]?.isSynced = true
}
}
self.deviceCache.store(subscriberAttributesByKey: unsyncedAttributes, appUserID: appUserID)
}
}
func setAttributes(fromAttributionData attributionData: [String: Any],
network: AttributionNetwork,
appUserID: String) {
let convertedAttribution = attributionDataMigrator.convertToSubscriberAttributes(
attributionData: attributionData,
network: network.rawValue)
setAttributes(convertedAttribution, appUserID: appUserID)
}
}
private extension SubscriberAttributesManager {
func storeAttributeLocallyIfNeeded(key: String, value: String?, appUserID: String) {
let currentValue = currentValueForAttribute(key: key, appUserID: appUserID)
if currentValue == nil || currentValue != (value ?? "") {
storeAttributeLocally(key: key, value: value ?? "", appUserID: appUserID)
}
}
func setReservedAttribute(_ reservedAttribute: ReservedSubscriberAttribute, value: String?, appUserID: String) {
Logger.debug(Strings.attribution.setting_reserved_attribute(reservedAttribute))
setAttribute(key: reservedAttribute.key, value: value, appUserID: appUserID)
}
func setAttribute(key: String, value: String?, appUserID: String) {
storeAttributeLocallyIfNeeded(key: key, value: value, appUserID: appUserID)
}
func syncAttributes(attributes: SubscriberAttribute.Dictionary,
appUserID: String,
completion: @escaping (BackendError?) -> Void) {
backend.post(subscriberAttributes: attributes, appUserID: appUserID) { error in
let didBackendReceiveValues = error?.successfullySynced ?? true
if didBackendReceiveValues {
self.markAttributesAsSynced(attributes, appUserID: appUserID)
}
completion(error)
}
}
func storeAttributeLocally(key: String, value: String, appUserID: String) {
let subscriberAttribute = SubscriberAttribute(withKey: key, value: value)
Logger.debug(Strings.attribution.attribute_set_locally(attribute: subscriberAttribute.description))
deviceCache.store(subscriberAttribute: subscriberAttribute, appUserID: appUserID)
}
func currentValueForAttribute(key: String, appUserID: String) -> String? {
let attribute = deviceCache.subscriberAttribute(attributeKey: key, appUserID: appUserID)
return attribute?.value
}
func setAttributionID(_ attributionID: String?,
forNetworkID networkID: ReservedSubscriberAttribute,
appUserID: String) {
collectDeviceIdentifiers(forAppUserID: appUserID)
setReservedAttribute(networkID, value: attributionID, appUserID: appUserID)
}
}