Skip to content

Commit

Permalink
PostReceiptOperation: added ability to also post AdServices token
Browse files Browse the repository at this point in the history
- Added `aadAttributionToken` to `PostReceiptDataOperation`
- Exposed `AttributionPoster.adServicesTokenToPostIfNeeded`
- Added snapshot test to verify it's sent
- Added `PurchasesOrchestrator` tests (SK1/SK2) for sending the attribution token
- Added missing `PurchasesOrchestrator` tests for sending attributes
- Added log when marking `AdServices` token as synced
- Exposed `Purchases.attribution` for custom entitlement computation framework (and added to API tester)
- Exposed `Purchases.enableAdServicesAttributionTokenCollection`  for custom entitlement computation framework (and added to API tester)
  • Loading branch information
NachoSoto committed May 30, 2023
1 parent 81a867a commit 223f9a9
Show file tree
Hide file tree
Showing 19 changed files with 410 additions and 39 deletions.
38 changes: 29 additions & 9 deletions Sources/Attribution/AttributionPoster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,37 @@ final class AttributionPoster {
@available(tvOS, unavailable)
@available(watchOS, unavailable)
func postAdServicesTokenIfNeeded() {
guard latestNetworkIdAndAdvertisingIdentifierSent(network: .adServices) == nil else {
return
guard let attributionToken = self.adServicesTokenToPostIfNeeded else { return }

self.post(adServicesToken: attributionToken)
}

var adServicesTokenToPostIfNeeded: String? {
#if os(tvOS) || os(watchOS)
return nil
#else
guard #available(iOS 14.3, macOS 11.1, macCatalyst 14.3, *) else {
return nil
}

guard let attributionToken = attributionFetcher.adServicesToken else {
return
guard self.latestNetworkIdAndAdvertisingIdentifierSent(network: .adServices) == nil else {
return nil
}

self.post(adServicesToken: attributionToken)
return self.attributionFetcher.adServicesToken
#endif
}

@discardableResult
func markAdServicesToken(_ token: String, asSyncedFor userID: String) -> [AttributionNetwork: String] {
Logger.info(Strings.attribution.adservices_marking_as_synced(appUserID: userID))

var newDictToCache = self.deviceCache.latestAdvertisingIdsByNetworkSent(appUserID: userID)
newDictToCache[AttributionNetwork.adServices] = token

self.deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: userID)

return newDictToCache
}

func postPostponedAttributionDataIfNeeded() {
Expand Down Expand Up @@ -167,11 +189,9 @@ final class AttributionPoster {
let currentAppUserID = self.currentUserProvider.currentAppUserID

// set the cache in advance to avoid multiple post calls
var newDictToCache = self.deviceCache.latestAdvertisingIdsByNetworkSent(appUserID: currentAppUserID)
newDictToCache[AttributionNetwork.adServices] = adServicesToken
self.deviceCache.set(latestAdvertisingIdsByNetworkSent: newDictToCache, appUserID: currentAppUserID)
var newDictToCache = self.markAdServicesToken(adServicesToken, asSyncedFor: currentAppUserID)

backend.post(adServicesToken: adServicesToken, appUserID: currentAppUserID) { error in
self.backend.post(adServicesToken: adServicesToken, appUserID: currentAppUserID) { error in
guard let error = error else {
Logger.debug(Strings.attribution.adservices_token_post_succeeded)
return
Expand Down
4 changes: 4 additions & 0 deletions Sources/Logging/Strings/AttributionStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum AttributionStrings {
case adservices_token_fetch_failed(error: Error)
case adservices_token_post_failed(error: BackendError)
case adservices_token_post_succeeded
case adservices_marking_as_synced(appUserID: String)
case adservices_token_unavailable_in_simulator
case latest_attribution_sent_user_defaults_invalid(networkKey: String)
case copying_attributes(oldAppUserID: String, newAppUserID: String)
Expand Down Expand Up @@ -138,6 +139,9 @@ extension AttributionStrings: CustomStringConvertible {
case .adservices_token_post_succeeded:
return "AdServices attribution token successfully posted"

case let .adservices_marking_as_synced(userID):
return "Marking AdServices attribution token as synced for App User ID: \(userID)"

case .adservices_token_unavailable_in_simulator:
return "AdServices attribution token is not available in the simulator"

Expand Down
7 changes: 6 additions & 1 deletion Sources/Networking/Operations/PostReceiptDataOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class PostReceiptDataOperation: CacheableNetworkOperation {
let observerMode: Bool
let initiationSource: ProductRequestData.InitiationSource
let subscriberAttributesByKey: SubscriberAttribute.Dictionary?
let aadAttributionToken: String?

}

Expand Down Expand Up @@ -142,7 +143,8 @@ extension PostReceiptDataOperation.PostData {
presentedOfferingIdentifier: data.presentedOfferingID,
observerMode: observerMode,
initiationSource: data.source.initiationSource,
subscriberAttributesByKey: data.unsyncedAttributes
subscriberAttributesByKey: data.unsyncedAttributes,
aadAttributionToken: data.aadAttributionToken
)
}

Expand Down Expand Up @@ -184,6 +186,7 @@ extension PostReceiptDataOperation.PostData: Encodable {
case observerMode
case initiationSource
case attributes
case aadAttributionToken
case presentedOfferingIdentifier

}
Expand All @@ -210,6 +213,8 @@ extension PostReceiptDataOperation.PostData: Encodable {
.map(AnyEncodable.init),
forKey: .attributes
)

try container.encodeIfPresent(self.aadAttributionToken, forKey: .aadAttributionToken)
}

}
Expand Down
31 changes: 29 additions & 2 deletions Sources/Purchasing/Purchases/Attribution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ import Foundation
private let subscriberAttributesManager: SubscriberAttributesManager
private let currentUserProvider: CurrentUserProvider
private let attributionPoster: AttributionPoster
private let systemInfo: SystemInfo

private var appUserID: String { self.currentUserProvider.currentAppUserID }
private var automaticAdServicesAttributionTokenCollection: Bool = false

weak var delegate: AttributionDelegate?

init(subscriberAttributesManager: SubscriberAttributesManager,
currentUserProvider: CurrentUserProvider,
attributionPoster: AttributionPoster) {
attributionPoster: AttributionPoster,
systemInfo: SystemInfo) {
self.subscriberAttributesManager = subscriberAttributesManager
self.currentUserProvider = currentUserProvider
self.attributionPoster = attributionPoster
self.systemInfo = systemInfo

super.init()

Expand All @@ -58,17 +62,26 @@ public extension Attribution {
*/
@objc func enableAdServicesAttributionTokenCollection() {
self.automaticAdServicesAttributionTokenCollection = true

self.postAdServicesTokenIfNeeded()
}

internal func postAdServicesTokenIfNeeded() {
if self.automaticAdServicesAttributionTokenCollection {
if self.automaticAdServicesAttributionTokenCollection,
self.automaticAdServicesTokenPostingEnabled {
self.attributionPoster.postAdServicesTokenIfNeeded()
}
}

private var automaticAdServicesTokenPostingEnabled: Bool {
/// In custom entitlements computation mode, ad services token is sent only through `PostReceiptOperation`
return !self.systemInfo.dangerousSettings.customEntitlementComputation
}

}

#if !CUSTOM_ENTITLEMENTS_COMPUTATION

public extension Attribution {

/**
Expand Down Expand Up @@ -352,6 +365,8 @@ public extension Attribution {

}

#endif

// @unchecked because:
// - It contains mutable state (`weak var delegate`).
extension Attribution: @unchecked Sendable {}
Expand Down Expand Up @@ -387,6 +402,14 @@ extension Attribution {
self.subscriberAttributesManager.unsyncedAttributesByKey(appUserID: appUserID)
}

var unsyncedAdServicesToken: String? {
guard self.automaticAdServicesAttributionTokenCollection else {
return nil
}

return self.attributionPoster.adServicesTokenToPostIfNeeded
}

@discardableResult
func syncAttributesForAllUsers(currentAppUserID: String,
syncedAttribute: (@Sendable (PurchasesError?) -> Void)? = nil,
Expand All @@ -400,6 +423,10 @@ extension Attribution {
self.subscriberAttributesManager.markAttributesAsSynced(attributesToSync, appUserID: appUserID)
}

func markAdServicesTokenAsSynced(_ token: String, appUserID: String) {
self.attributionPoster.markAdServicesToken(token, asSyncedFor: appUserID)
}

}

protocol AttributionDelegate: AnyObject, Sendable {
Expand Down
9 changes: 2 additions & 7 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,8 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
/// Current version of the ``Purchases`` framework.
@objc public static var frameworkVersion: String { SystemInfo.frameworkVersion }

#if !ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION

@objc public let attribution: Attribution

#endif

@objc public var finishTransactions: Bool {
get { self.systemInfo.finishTransactions }
set { self.systemInfo.finishTransactions = newValue }
Expand Down Expand Up @@ -351,7 +347,8 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
subscriberAttributesManager: subscriberAttributesManager)
let subscriberAttributes = Attribution(subscriberAttributesManager: subscriberAttributesManager,
currentUserProvider: identityManager,
attributionPoster: attributionPoster)
attributionPoster: attributionPoster,
systemInfo: systemInfo)
let introCalculator = IntroEligibilityCalculator(productsManager: productsManager, receiptParser: receiptParser)
let offeringsManager = OfferingsManager(deviceCache: deviceCache,
operationDispatcher: operationDispatcher,
Expand Down Expand Up @@ -500,9 +497,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
self.userDefaults = userDefaults
self.notificationCenter = notificationCenter
self.systemInfo = systemInfo
#if !ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION
self.attribution = subscriberAttributes
#endif
self.operationDispatcher = operationDispatcher
self.customerInfoManager = customerInfoManager
self.productsManager = productsManager
Expand Down
27 changes: 23 additions & 4 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ private extension PurchasesOrchestrator {

func markSyncedIfNeeded(
subscriberAttributes: SubscriberAttribute.Dictionary?,
adServicesToken: String?,
error: BackendError?
) {
if let error = error {
Expand All @@ -916,6 +917,9 @@ private extension PurchasesOrchestrator {
}

self.attribution.markAttributesAsSynced(subscriberAttributes, appUserID: self.appUserID)
if let adServicesToken = adServicesToken {
self.attribution.markAdServicesTokenAsSynced(adServicesToken, appUserID: self.appUserID)
}
}

// swiftlint:disable:next function_body_length
Expand All @@ -931,6 +935,8 @@ private extension PurchasesOrchestrator {

let currentAppUserID = self.appUserID
let unsyncedAttributes = self.unsyncedAttributes
let adServicesToken = self.attribution.unsyncedAdServicesToken

// Refresh the receipt and post to backend, this will allow the transactions to be transferred.
// https://rev.cat/apple-restoring-purchased-products
self.receiptFetcher.receiptData(refreshPolicy: receiptRefreshPolicy) { receiptData in
Expand Down Expand Up @@ -978,6 +984,7 @@ private extension PurchasesOrchestrator {
observerMode: self.observerMode) { result in
self.handleReceiptPost(result: result,
subscriberAttributes: unsyncedAttributes,
adServicesToken: adServicesToken,
completion: completion)
}
}
Expand All @@ -987,10 +994,12 @@ private extension PurchasesOrchestrator {

func handleReceiptPost(result: Result<CustomerInfo, BackendError>,
subscriberAttributes: SubscriberAttribute.Dictionary,
adServicesToken: String?,
completion: (@Sendable (Result<CustomerInfo, PurchasesError>) -> Void)?) {
self.handlePostReceiptResult(
result,
subscriberAttributes: subscriberAttributes
subscriberAttributes: subscriberAttributes,
adServicesToken: adServicesToken
)

if let completion = completion {
Expand All @@ -1001,12 +1010,14 @@ private extension PurchasesOrchestrator {
}

func handlePostReceiptResult(_ result: Result<CustomerInfo, BackendError>,
subscriberAttributes: SubscriberAttribute.Dictionary) {
subscriberAttributes: SubscriberAttribute.Dictionary,
adServicesToken: String?) {
if let customerInfo = result.value {
self.customerInfoManager.cache(customerInfo: customerInfo, appUserID: self.appUserID)
}

self.markSyncedIfNeeded(subscriberAttributes: subscriberAttributes,
adServicesToken: adServicesToken,
error: result.error)
}

Expand All @@ -1015,19 +1026,23 @@ private extension PurchasesOrchestrator {
restored: Bool) {
let offeringID = self.getAndRemovePresentedOfferingIdentifier(for: purchasedTransaction)
let unsyncedAttributes = self.unsyncedAttributes
let adServicesToken = self.attribution.unsyncedAdServicesToken

self.transactionPoster.handlePurchasedTransaction(
purchasedTransaction,
data: .init(
appUserID: self.appUserID,
presentedOfferingID: offeringID,
unsyncedAttributes: unsyncedAttributes,
aadAttributionToken: adServicesToken,
storefront: storefront,
source: self.purchaseSource(for: purchasedTransaction.productIdentifier,
restored: restored)
)
) { result in
self.handlePostReceiptResult(result, subscriberAttributes: unsyncedAttributes)
self.handlePostReceiptResult(result,
subscriberAttributes: unsyncedAttributes,
adServicesToken: adServicesToken)

if let completion = self.getAndRemovePurchaseCompletedCallback(forTransaction: purchasedTransaction) {
self.operationDispatcher.dispatchOnMainActor {
Expand Down Expand Up @@ -1135,6 +1150,7 @@ extension PurchasesOrchestrator {
let storefront = await Storefront.currentStorefront
let offeringID = self.getAndRemovePresentedOfferingIdentifier(for: transaction)
let unsyncedAttributes = self.unsyncedAttributes
let adServicesToken = self.attribution.unsyncedAdServicesToken

return try await withCheckedThrowingContinuation { continuation in
self.transactionPoster.handlePurchasedTransaction(
Expand All @@ -1143,12 +1159,15 @@ extension PurchasesOrchestrator {
appUserID: self.appUserID,
presentedOfferingID: offeringID,
unsyncedAttributes: unsyncedAttributes,
aadAttributionToken: adServicesToken,
storefront: storefront,
source: .init(isRestore: self.allowSharingAppStoreAccount,
initiationSource: initiationSource)
)
) { result in
self.handlePostReceiptResult(result, subscriberAttributes: unsyncedAttributes)
self.handlePostReceiptResult(result,
subscriberAttributes: unsyncedAttributes,
adServicesToken: adServicesToken)

continuation.resume(with: result.mapError(\.asPurchasesError))
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Purchasing/Purchases/TransactionPoster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct PurchasedTransactionData {
var appUserID: String
var presentedOfferingID: String?
var unsyncedAttributes: SubscriberAttribute.Dictionary?
var aadAttributionToken: String?
var storefront: StorefrontType?
var source: PurchaseSource

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
2DD778ED270E23460079CBD4 /* OfferingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C726EBE7EA007DDB75 /* OfferingAPI.swift */; };
2DD778EE270E23460079CBD4 /* EntitlementInfosAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C526EBE7EA007DDB75 /* EntitlementInfosAPI.swift */; };
2DD778EF270E23460079CBD4 /* EntitlementInfoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D614C826EBE7EA007DDB75 /* EntitlementInfoAPI.swift */; };
4F592A502A1FDC6F00851F36 /* AttributionAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F592A4F2A1FDC6F00851F36 /* AttributionAPI.swift */; };
570EC3B229F9A0830036A023 /* RevenueCat_CustomEntitlementComputation in Frameworks */ = {isa = PBXBuildFile; productRef = 570EC3B129F9A0830036A023 /* RevenueCat_CustomEntitlementComputation */; };
570FAF562864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570FAF552864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift */; };
5738F40C27866DD00096D623 /* StoreProductDiscountAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5738F40B27866DD00096D623 /* StoreProductDiscountAPI.swift */; };
Expand Down Expand Up @@ -50,6 +51,7 @@
/* Begin PBXFileReference section */
2C396F5B281C64AF00669657 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; };
2DD778D0270E233F0079CBD4 /* SwiftAPITester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAPITester.app; sourceTree = BUILT_PRODUCTS_DIR; };
4F592A4F2A1FDC6F00851F36 /* AttributionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionAPI.swift; sourceTree = "<group>"; };
570FAF552864EE1D00D3C769 /* NonSubscriptionTransactionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonSubscriptionTransactionAPI.swift; sourceTree = "<group>"; };
5738F40B27866DD00096D623 /* StoreProductDiscountAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProductDiscountAPI.swift; sourceTree = "<group>"; };
5738F429278673A80096D623 /* SubscriptionPeriodAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPeriodAPI.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -116,6 +118,7 @@
A55F62B726EAFFD200A1B466 /* SwiftAPITester */ = {
isa = PBXGroup;
children = (
4F592A4F2A1FDC6F00851F36 /* AttributionAPI.swift */,
B32554412825E5EA00DA62EA /* ConfigurationAPI.swift */,
A5D614C426EBE7EA007DDB75 /* CustomerInfoAPI.swift */,
A5D614C826EBE7EA007DDB75 /* EntitlementInfoAPI.swift */,
Expand Down Expand Up @@ -238,6 +241,7 @@
B32554422825E5EA00DA62EA /* ConfigurationAPI.swift in Sources */,
5738F40C27866DD00096D623 /* StoreProductDiscountAPI.swift in Sources */,
5740FCD52996D7B800E049F9 /* VerificationResultAPI.swift in Sources */,
4F592A502A1FDC6F00851F36 /* AttributionAPI.swift in Sources */,
5758EE582786542200B3B703 /* StoreTransactionAPI.swift in Sources */,
B3A4C834280DE72600D4AE17 /* PromotionalOfferAPI.swift in Sources */,
5738F42A278673A80096D623 /* SubscriptionPeriodAPI.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// AttributionAPI.swift
// SwiftAPITester
//
// Created by Nacho Soto on 5/25/23.
//

import RevenueCat_CustomEntitlementComputation

private var attribution: Attribution!

func checkAttributionAPI() {
attribution.enableAdServicesAttributionTokenCollection()
}
Loading

0 comments on commit 223f9a9

Please sign in to comment.