Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls: added Purchases.track(paywallEvent:) #3160

Merged
merged 13 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
4FFFE6C62AA9465000B2955C /* MockPaywallEventsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFFE6C52AA9465000B2955C /* MockPaywallEventsManager.swift */; };
4FFFE6C82AA9467800B2955C /* PaywallEventsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFFE6C72AA9467800B2955C /* PaywallEventsManagerTests.swift */; };
4FFFE6CA2AA946A700B2955C /* MockInternalAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFFE6C92AA946A700B2955C /* MockInternalAPI.swift */; };
4FFFE6E72AA948A600B2955C /* PaywallEventsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFFE6E62AA948A600B2955C /* PaywallEventsIntegrationTests.swift */; };
57032ABF28C13CE4004FF47A /* StoreKit2SettingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */; };
57045B3829C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */; };
57045B3A29C51751001A5417 /* GetProductEntitlementMappingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57045B3929C51751001A5417 /* GetProductEntitlementMappingOperation.swift */; };
Expand Down Expand Up @@ -1060,6 +1061,7 @@
4FFFE6C52AA9465000B2955C /* MockPaywallEventsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPaywallEventsManager.swift; sourceTree = "<group>"; };
4FFFE6C72AA9467800B2955C /* PaywallEventsManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventsManagerTests.swift; sourceTree = "<group>"; };
4FFFE6C92AA946A700B2955C /* MockInternalAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockInternalAPI.swift; sourceTree = "<group>"; };
4FFFE6E62AA948A600B2955C /* PaywallEventsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEventsIntegrationTests.swift; sourceTree = "<group>"; };
57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2SettingTests.swift; sourceTree = "<group>"; };
57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductEntitlementMappingDecodingTests.swift; sourceTree = "<group>"; };
57045B3929C51751001A5417 /* GetProductEntitlementMappingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetProductEntitlementMappingOperation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1914,6 +1916,7 @@
2CD2C541278CE0E0005D1CC2 /* RevenueCat_IntegrationPurchaseTesterConfiguration.storekit */,
2DE61A83264190830021CEA0 /* Constants.swift */,
2DE20B70264087FB004C597D /* Info.plist */,
4FFFE6E62AA948A600B2955C /* PaywallEventsIntegrationTests.swift */,
);
path = BackendIntegrationTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -3824,6 +3827,7 @@
2DE20B6F264087FB004C597D /* StoreKitIntegrationTests.swift in Sources */,
4F83F6B62A5DB773003F90A5 /* TestCase.swift in Sources */,
4FCBA84F2A15391B004134BD /* SnapshotTesting+Extensions.swift in Sources */,
4FFFE6E72AA948A600B2955C /* PaywallEventsIntegrationTests.swift in Sources */,
4FA696BD2A0020A000D228B1 /* MainThreadMonitor.swift in Sources */,
2D3BFAD126DEA45C00370B11 /* MockSK1Product.swift in Sources */,
57DD426E2926B9A50026DF09 /* StoreKitTestHelpers.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions Sources/Logging/Strings/PaywallsStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ enum PaywallsStrings {
case warming_up_images(imageURLs: Set<URL>)
case error_prefetching_image(URL, Error)

case caching_presented_paywall
case clearing_presented_paywall

// MARK: - Events

case event_manager_initialized
case event_manager_not_initialized_not_available
case event_manager_failed_to_initialize(Error)

Expand All @@ -46,8 +50,17 @@ extension PaywallsStrings: LogMessage {
case let .error_prefetching_image(url, error):
return "Error pre-fetching paywall image '\(url)': \((error as NSError).description)"

case .caching_presented_paywall:
return "PurchasesOrchestrator: caching presented paywall"

case .clearing_presented_paywall:
return "PurchasesOrchestrator: clearing presented paywall"

// MARK: - Events

case .event_manager_initialized:
return "PaywallEventsManager initialized"

case .event_manager_not_initialized_not_available:
return "Won't initialize PaywallEventsManager: not available on current device."

Expand Down
9 changes: 7 additions & 2 deletions Sources/Logging/Strings/PurchaseStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ enum PurchaseStrings {
case transaction_poster_handling_transaction(transactionID: String,
productID: String,
transactionDate: Date,
offeringID: String?)
offeringID: String?,
paywallSessionID: UUID?)
case caching_presented_offering_identifier(offeringID: String, productID: String)
case payment_queue_wrapper_delegate_call_sk1_enabled
case restorepurchases_called_with_allow_sharing_appstore_account_false
Expand Down Expand Up @@ -293,14 +294,18 @@ extension PurchaseStrings: LogMessage {
case let .sk2_transactions_update_received_transaction(productID):
return "StoreKit.Transaction.updates: received transaction for product '\(productID)'"

case let .transaction_poster_handling_transaction(transactionID, productID, date, offeringID):
case let .transaction_poster_handling_transaction(transactionID, productID, date, offeringID, paywallSessionID):
var message = "TransactionPoster: handling transaction '\(transactionID)' " +
"for product '\(productID)' (date: \(date))"

if let offeringIdentifier = offeringID {
message += " in Offering '\(offeringIdentifier)'"
}

if let paywallSessionID {
message += " with paywall session '\(paywallSessionID)'"
}

return message

case let .caching_presented_offering_identifier(offeringID, productID):
Expand Down
85 changes: 66 additions & 19 deletions Sources/Networking/Operations/PostReceiptDataOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,6 @@ import Foundation

final class PostReceiptDataOperation: CacheableNetworkOperation {

struct PostData {

let appUserID: String
let receiptData: Data
let isRestore: Bool
let productData: ProductRequestData?
let presentedOfferingIdentifier: String?
let observerMode: Bool
let initiationSource: ProductRequestData.InitiationSource
let subscriberAttributesByKey: SubscriberAttribute.Dictionary?
let aadAttributionToken: String?
/// - Note: this is only used for the backend to disambiguate receipts created in `SKTestSession`s.
let testReceiptIdentifier: String?

}

private let postData: PostData
private let configuration: AppUserConfiguration
private let customerInfoResponseHandler: CustomerInfoResponseHandler
Expand Down Expand Up @@ -131,6 +115,37 @@ final class PostReceiptDataOperation: CacheableNetworkOperation {

}

extension PostReceiptDataOperation {

struct PostData {

let appUserID: String
let receiptData: Data
let isRestore: Bool
let productData: ProductRequestData?
let presentedOfferingIdentifier: String?
let paywall: Paywall?
let observerMode: Bool
let initiationSource: ProductRequestData.InitiationSource
let subscriberAttributesByKey: SubscriberAttribute.Dictionary?
let aadAttributionToken: String?
/// - Note: this is only used for the backend to disambiguate receipts created in `SKTestSession`s.
let testReceiptIdentifier: String?

}

struct Paywall {

var sessionID: String
var revision: Int
var displayMode: PaywallViewMode
var darkMode: Bool
var localeIdentifier: String

}

}

extension PostReceiptDataOperation.PostData {

init(
Expand All @@ -146,6 +161,7 @@ extension PostReceiptDataOperation.PostData {
isRestore: data.source.isRestore,
productData: productData,
presentedOfferingIdentifier: data.presentedOfferingID,
paywall: data.paywall,
observerMode: observerMode,
initiationSource: data.source.initiationSource,
subscriberAttributesByKey: data.unsyncedAttributes,
Expand All @@ -156,6 +172,20 @@ extension PostReceiptDataOperation.PostData {

}

private extension PurchasedTransactionData {

var paywall: PostReceiptDataOperation.Paywall? {
guard let paywall = self.presentedPaywall else { return nil }

return .init(sessionID: paywall.sessionIdentifier.uuidString,
revision: paywall.paywallRevision,
displayMode: paywall.displayMode,
darkMode: paywall.darkMode,
localeIdentifier: paywall.localeIdentifier)
}

}

// MARK: - Private

private extension PostReceiptDataOperation {
Expand Down Expand Up @@ -183,7 +213,7 @@ private extension PostReceiptDataOperation {

}

// MARK: - Request Data
// MARK: - Codable

extension PostReceiptDataOperation.PostData: Encodable {

Expand All @@ -197,6 +227,7 @@ extension PostReceiptDataOperation.PostData: Encodable {
case attributes
case aadAttributionToken
case presentedOfferingIdentifier
case paywall
case testReceiptIdentifier = "test_receipt_identifier"

}
Expand All @@ -214,8 +245,8 @@ extension PostReceiptDataOperation.PostData: Encodable {
try productData.encode(to: encoder)
}

try container.encodeIfPresent(self.presentedOfferingIdentifier,
forKey: .presentedOfferingIdentifier)
try container.encodeIfPresent(self.presentedOfferingIdentifier, forKey: .presentedOfferingIdentifier)
try container.encodeIfPresent(self.paywall, forKey: .paywall)

try container.encodeIfPresent(
self.subscriberAttributesByKey
Expand All @@ -232,6 +263,22 @@ extension PostReceiptDataOperation.PostData: Encodable {

}

extension PostReceiptDataOperation.Paywall: Codable {

private enum CodingKeys: String, CodingKey {

case sessionID = "sessionId"
case revision
case displayMode
case darkMode
case localeIdentifier = "locale"

}

}

// MARK: - HTTPRequestBody

extension PostReceiptDataOperation.PostData: HTTPRequestBody {

var contentForSignature: [(key: String, value: String)] {
Expand Down
47 changes: 47 additions & 0 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
private let offlineEntitlementsManager: OfflineEntitlementsManager
private let productsManager: ProductsManagerType
private let customerInfoManager: CustomerInfoManager
private let paywallEventsManager: PaywallEventsManagerType?
private let trialOrIntroPriceEligibilityChecker: CachingTrialOrIntroPriceEligibilityChecker
private let purchasedProductsFetcher: PurchasedProductsFetcherType?
private let purchasesOrchestrator: PurchasesOrchestrator
Expand Down Expand Up @@ -339,6 +340,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
transactionFetcher: StoreKit2TransactionFetcher(),
transactionPoster: transactionPoster,
systemInfo: systemInfo)

let attributionDataMigrator = AttributionDataMigrator()
let subscriberAttributesManager = SubscriberAttributesManager(backend: backend,
deviceCache: deviceCache,
Expand All @@ -351,6 +353,24 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
attributeSyncing: subscriberAttributesManager,
appUserID: appUserID)

let paywallEventsManager: PaywallEventsManagerType?
do {
if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) {
paywallEventsManager = PaywallEventsManager(
internalAPI: backend.internalAPI,
userProvider: identityManager,
store: try PaywallEventStore.createDefault()
)
Logger.verbose(Strings.paywalls.event_manager_initialized)
} else {
Logger.verbose(Strings.paywalls.event_manager_not_initialized_not_available)
paywallEventsManager = nil
}
} catch {
Logger.verbose(Strings.paywalls.event_manager_failed_to_initialize(error))
paywallEventsManager = nil
}

let attributionPoster = AttributionPoster(deviceCache: deviceCache,
currentUserProvider: identityManager,
backend: backend,
Expand Down Expand Up @@ -453,6 +473,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
subscriberAttributes: subscriberAttributes,
operationDispatcher: operationDispatcher,
customerInfoManager: customerInfoManager,
paywallEventsManager: paywallEventsManager,
productsManager: productsManager,
offeringsManager: offeringsManager,
offlineEntitlementsManager: offlineEntitlementsManager,
Expand All @@ -479,6 +500,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
subscriberAttributes: Attribution,
operationDispatcher: OperationDispatcher,
customerInfoManager: CustomerInfoManager,
paywallEventsManager: PaywallEventsManagerType?,
productsManager: ProductsManagerType,
offeringsManager: OfferingsManager,
offlineEntitlementsManager: OfflineEntitlementsManager,
Expand Down Expand Up @@ -526,6 +548,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
self.attribution = subscriberAttributes
self.operationDispatcher = operationDispatcher
self.customerInfoManager = customerInfoManager
self.paywallEventsManager = paywallEventsManager
self.productsManager = productsManager
self.offeringsManager = offeringsManager
self.offlineEntitlementsManager = offlineEntitlementsManager
Expand Down Expand Up @@ -1031,6 +1054,30 @@ public extension Purchases {

// swiftlint:enable missing_docs

// MARK: - Paywalls

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
public extension Purchases {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

man I wish we could use package

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used to use objective-c headers added to the dependency for this kind of thing. I can't think of any clean way of doing this without having to expose the method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah...


/// Used by `RevenueCatUI` to keep track of ``PaywallEvent``s.
func track(paywallEvent: PaywallEvent) async {
switch paywallEvent {
case let .view(data):
self.purchasesOrchestrator.cachePresentedPaywall(data)

case .close:
self.purchasesOrchestrator.clearPresentedPaywall()
NachoSoto marked this conversation as resolved.
Show resolved Hide resolved

case .cancel:
// No special handling, simply track the event below.
break
}

await self.paywallEventsManager?.track(paywallEvent: paywallEvent)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be moving this to the orchestrator layer? Ideally we do as little as possible at the Facade layer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has nothing to do with purchases though. PaywallEventsManager was my way of extracting this out, though I agree ideally methods in Purchases are no more than 1 line.

}

}

// MARK: Configuring Purchases

public extension Purchases {
Expand Down
19 changes: 19 additions & 0 deletions Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class PurchasesOrchestrator {

private let _allowSharingAppStoreAccount: Atomic<Bool?> = nil
private let presentedOfferingIDsByProductID: Atomic<[String: String]> = .init([:])
private let presentedPaywall: Atomic<PaywallEvent.Data?> = nil
private let purchaseCompleteCallbacksByProductID: Atomic<[String: PurchaseCompletedBlock]> = .init([:])

private var appUserID: String { self.currentUserProvider.currentAppUserID }
Expand Down Expand Up @@ -546,6 +547,16 @@ final class PurchasesOrchestrator {
self.presentedOfferingIDsByProductID.modify { $0[productIdentifier] = identifier }
}

func cachePresentedPaywall(_ paywall: PaywallEvent.Data) {
Logger.verbose(Strings.paywalls.caching_presented_paywall)
self.presentedPaywall.value = paywall
}

func clearPresentedPaywall() {
Logger.verbose(Strings.paywalls.clearing_presented_paywall)
self.presentedPaywall.value = nil
}

#if os(iOS) || os(macOS) || VISION_OS

@available(watchOS, unavailable)
Expand Down Expand Up @@ -1077,6 +1088,7 @@ private extension PurchasesOrchestrator {
storefront: StorefrontType?,
restored: Bool) {
let offeringID = self.getAndRemovePresentedOfferingIdentifier(for: purchasedTransaction)
let paywall = self.getAndRemovePresentedPaywall()
let unsyncedAttributes = self.unsyncedAttributes
let adServicesToken = self.attribution.unsyncedAdServicesToken

Expand All @@ -1085,6 +1097,7 @@ private extension PurchasesOrchestrator {
data: .init(
appUserID: self.appUserID,
presentedOfferingID: offeringID,
presentedPaywall: paywall,
unsyncedAttributes: unsyncedAttributes,
aadAttributionToken: adServicesToken,
storefront: storefront,
Expand Down Expand Up @@ -1143,6 +1156,10 @@ private extension PurchasesOrchestrator {
return self.getAndRemovePresentedOfferingIdentifier(for: transaction.productIdentifier)
}

func getAndRemovePresentedPaywall() -> PaywallEvent.Data? {
return self.presentedPaywall.getAndSet(nil)
}

/// Computes a `ProductRequestData` for an active subscription found in the receipt,
/// or `nil` if there is any issue fetching it.
func createProductRequestData(
Expand Down Expand Up @@ -1207,6 +1224,7 @@ extension PurchasesOrchestrator {
) async throws -> CustomerInfo {
let storefront = await Storefront.currentStorefront
let offeringID = self.getAndRemovePresentedOfferingIdentifier(for: transaction)
let paywall = self.getAndRemovePresentedPaywall()
let unsyncedAttributes = self.unsyncedAttributes
let adServicesToken = self.attribution.unsyncedAdServicesToken

Expand All @@ -1215,6 +1233,7 @@ extension PurchasesOrchestrator {
data: .init(
appUserID: self.appUserID,
presentedOfferingID: offeringID,
presentedPaywall: paywall,
unsyncedAttributes: unsyncedAttributes,
aadAttributionToken: adServicesToken,
storefront: storefront,
Expand Down
Loading