Files
Rick aaf5d0f02f feat(checkpoints): resolve subscriber dimensions from config and receipts endpoints (#7919)
* feat(checkpoints): resolve subscriber dimensions from config and receipts

* fix(checkpoints): address subscriber dimension review feedback

* fix(checkpoints): provide user defaults in integration tests

* fix(checkpoints): resolve integration test lint errors
2026-10-06 14:36:58 +02:00

224 lines
10 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
//
// SubscribersAPI.swift
//
// Created by Joshua Liebowitz on 11/17/21.
import Foundation
final class CustomerAPI {
typealias CustomerInfoResponseHandler = Backend.ResponseHandler<CustomerInfo>
typealias SimpleResponseHandler = (BackendError?) -> Void
// swiftlint:disable:next type_name
typealias IsPurchaseAllowedByRestoreBehaviorResponseHandler =
Backend.ResponseHandler<IsPurchaseAllowedByRestoreBehaviorResponse>
private let backendConfig: BackendConfiguration
private let customerInfoCallbackCache: CallbackCache<CustomerInfoCallback>
private let isPurchaseAllowedByRestoreBehaviorCallbacksCache:
CallbackCache<IsPurchaseAllowedByRestoreBehaviorCallback>
private let attributionFetcher: AttributionFetcher
private let subscriberDimensionsStore: SubscriberDimensionsStoreType
init(
backendConfig: BackendConfiguration,
attributionFetcher: AttributionFetcher,
subscriberDimensionsStore: SubscriberDimensionsStoreType
) {
self.backendConfig = backendConfig
self.attributionFetcher = attributionFetcher
self.subscriberDimensionsStore = subscriberDimensionsStore
self.customerInfoCallbackCache = CallbackCache<CustomerInfoCallback>()
self.isPurchaseAllowedByRestoreBehaviorCallbacksCache =
CallbackCache<IsPurchaseAllowedByRestoreBehaviorCallback>()
}
func getCustomerInfo(appUserID: String,
isAppBackgrounded: Bool,
allowComputingOffline: Bool,
completion: @escaping CustomerInfoResponseHandler) {
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
appUserID: appUserID)
let factory = GetCustomerInfoOperation.createFactory(
configuration: config,
customerInfoCallbackCache: self.customerInfoCallbackCache,
offlineCreator: allowComputingOffline
? self.backendConfig.offlineCustomerInfoCreator
: nil
)
let callback = CustomerInfoCallback(cacheKey: factory.cacheKey,
appUserID: appUserID,
source: factory.operationType,
completion: completion)
let cacheStatus = self.customerInfoCallbackCache.addOrAppendToPostReceiptDataOperation(callback: callback)
self.backendConfig.addCacheableOperation(with: factory,
delay: .default(forBackgroundedApp: isAppBackgrounded),
cacheStatus: cacheStatus)
}
func post(subscriberAttributes: SubscriberAttribute.Dictionary,
appUserID: String,
completion: SimpleResponseHandler?) {
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
appUserID: appUserID)
let operation = PostSubscriberAttributesOperation(configuration: config,
subscriberAttributes: subscriberAttributes,
completion: completion)
self.backendConfig.operationQueue.addOperation(operation)
}
func post(attributionData: [String: Any],
network: AttributionNetwork,
appUserID: String,
completion: SimpleResponseHandler?) {
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
appUserID: appUserID)
let postAttributionDataOperation = PostAttributionDataOperation(configuration: config,
attributionData: attributionData,
network: network,
responseHandler: completion)
self.backendConfig.operationQueue.addOperation(postAttributionDataOperation)
}
func post(adServicesToken: String,
appUserID: String,
completion: SimpleResponseHandler?) {
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
appUserID: appUserID)
let postAttributionDataOperation = PostAdServicesTokenOperation(configuration: config,
token: adServicesToken,
responseHandler: completion)
self.backendConfig.operationQueue.addOperation(postAttributionDataOperation)
}
func isPurchaseAllowedByRestoreBehavior(
appUserID: String,
transactionJWS: String,
isAppBackgrounded: Bool,
completion: @escaping IsPurchaseAllowedByRestoreBehaviorResponseHandler
) {
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
appUserID: appUserID)
let postData = PostIsPurchaseAllowedByRestoreBehaviorOperation.PostData(
transactionJWS: transactionJWS
)
let factory = PostIsPurchaseAllowedByRestoreBehaviorOperation.createFactory(
configuration: config,
postData: postData,
isPurchaseAllowedByRestoreBehaviorCallbackCache: self.isPurchaseAllowedByRestoreBehaviorCallbacksCache
)
let callback = IsPurchaseAllowedByRestoreBehaviorCallback(cacheKey: factory.cacheKey, completion: completion)
let cacheStatus = self.isPurchaseAllowedByRestoreBehaviorCallbacksCache.add(callback)
self.backendConfig.addCacheableOperation(
with: factory,
delay: .default(forBackgroundedApp: isAppBackgrounded),
cacheStatus: cacheStatus
)
}
// swiftlint:disable function_parameter_count
func post(receipt: EncodedAppleReceipt,
productData: ProductRequestData?,
transactionData: PurchasedTransactionData,
postReceiptSource: PostReceiptSource,
observerMode: Bool,
originalPurchaseCompletedBy: PurchasesAreCompletedBy?,
appTransaction: String?,
associatedTransactionId: String?,
sdkOriginated: Bool = false,
appUserID: String,
containsAttributionData: Bool,
completion: @escaping CustomerAPI.CustomerInfoResponseHandler) {
var subscriberAttributesToPost: SubscriberAttribute.Dictionary?
if !self.backendConfig.systemInfo.dangerousSettings.customEntitlementComputation {
subscriberAttributesToPost = transactionData.unsyncedAttributes ?? [:]
let attributionStatus = self.attributionFetcher.authorizationStatus
let consentStatus = SubscriberAttribute(attribute: ReservedSubscriberAttribute.consentStatus,
value: attributionStatus.description,
dateProvider: self.backendConfig.dateProvider,
ignoreTimeInCacheIdentity: true)
subscriberAttributesToPost?[consentStatus.key] = consentStatus
}
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
appUserID: appUserID)
let postData = PostReceiptDataOperation.PostData(
transactionData: transactionData.withAttributesToPost(subscriberAttributesToPost),
postReceiptSource: postReceiptSource,
appUserID: appUserID,
productData: productData,
receipt: receipt,
observerMode: observerMode,
purchaseCompletedBy: originalPurchaseCompletedBy,
testReceiptIdentifier: self.backendConfig.systemInfo.testReceiptIdentifier,
appTransaction: appTransaction,
transactionId: associatedTransactionId,
containsAttributionData: containsAttributionData,
sdkOriginated: sdkOriginated
)
let offlineCustomerInfoCreator: OfflineCustomerInfoCreator? =
self.backendConfig.systemInfo.supportsOfflineEntitlements
? self.backendConfig.offlineCustomerInfoCreator
: nil
let factory = PostReceiptDataOperation.createFactory(
configuration: config,
postData: postData,
customerInfoCallbackCache: self.customerInfoCallbackCache,
offlineCustomerInfoCreator: offlineCustomerInfoCreator,
subscriberDimensionsStore: self.subscriberDimensionsStore
)
let callbackObject = CustomerInfoCallback(cacheKey: factory.cacheKey,
appUserID: appUserID,
source: PostReceiptDataOperation.self,
completion: completion)
let cacheStatus = customerInfoCallbackCache.add(callbackObject)
self.backendConfig.operationQueue.addCacheableOperation(with: factory, cacheStatus: cacheStatus)
}
}
private extension PurchasedTransactionData {
func withAttributesToPost(_ newAttributes: SubscriberAttribute.Dictionary?) -> Self {
var copy = self
copy.unsyncedAttributes = newAttributes
return copy
}
}
// MARK: -
private extension SystemInfo {
/// This allows the backend to disambiguate between receipts created across
/// separate test invocations when in the sandbox.
var testReceiptIdentifier: String? {
#if DEBUG
return self.dangerousSettings.internalSettings.testReceiptIdentifier
#else
return nil
#endif
}
}