diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 2c790d8077..bedc7c3611 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -323,6 +323,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 */; }; @@ -1072,6 +1073,7 @@ 4FFFE6C52AA9465000B2955C /* MockPaywallEventsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPaywallEventsManager.swift; sourceTree = ""; }; 4FFFE6C72AA9467800B2955C /* PaywallEventsManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventsManagerTests.swift; sourceTree = ""; }; 4FFFE6C92AA946A700B2955C /* MockInternalAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockInternalAPI.swift; sourceTree = ""; }; + 4FFFE6E62AA948A600B2955C /* PaywallEventsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEventsIntegrationTests.swift; sourceTree = ""; }; 57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2SettingTests.swift; sourceTree = ""; }; 57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductEntitlementMappingDecodingTests.swift; sourceTree = ""; }; 57045B3929C51751001A5417 /* GetProductEntitlementMappingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetProductEntitlementMappingOperation.swift; sourceTree = ""; }; @@ -1935,6 +1937,7 @@ 2CD2C541278CE0E0005D1CC2 /* RevenueCat_IntegrationPurchaseTesterConfiguration.storekit */, 2DE61A83264190830021CEA0 /* Constants.swift */, 2DE20B70264087FB004C597D /* Info.plist */, + 4FFFE6E62AA948A600B2955C /* PaywallEventsIntegrationTests.swift */, ); path = BackendIntegrationTests; sourceTree = ""; @@ -3862,6 +3865,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 */, diff --git a/Sources/Logging/Strings/PaywallsStrings.swift b/Sources/Logging/Strings/PaywallsStrings.swift index b8bec17c3a..e362715e9a 100644 --- a/Sources/Logging/Strings/PaywallsStrings.swift +++ b/Sources/Logging/Strings/PaywallsStrings.swift @@ -21,6 +21,9 @@ enum PaywallsStrings { case warming_up_images(imageURLs: Set) case error_prefetching_image(URL, Error) + case caching_presented_paywall + case clearing_presented_paywall + // MARK: - Events case event_manager_not_initialized_not_available @@ -46,6 +49,12 @@ 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_not_initialized_not_available: diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index f91886461c..8ecf1f995c 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -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 @@ -293,7 +294,7 @@ 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))" @@ -301,6 +302,10 @@ extension PurchaseStrings: LogMessage { message += " in Offering '\(offeringIdentifier)'" } + if let paywallSessionID { + message += " with paywall session '\(paywallSessionID)'" + } + return message case let .caching_presented_offering_identifier(offeringID, productID): diff --git a/Sources/Networking/Operations/PostReceiptDataOperation.swift b/Sources/Networking/Operations/PostReceiptDataOperation.swift index 909677cecc..7c16db6905 100644 --- a/Sources/Networking/Operations/PostReceiptDataOperation.swift +++ b/Sources/Networking/Operations/PostReceiptDataOperation.swift @@ -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 @@ -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( @@ -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, @@ -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 { @@ -183,7 +213,7 @@ private extension PostReceiptDataOperation { } -// MARK: - Request Data +// MARK: - Codable extension PostReceiptDataOperation.PostData: Encodable { @@ -197,6 +227,7 @@ extension PostReceiptDataOperation.PostData: Encodable { case attributes case aadAttributionToken case presentedOfferingIdentifier + case paywall case testReceiptIdentifier = "test_receipt_identifier" } @@ -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 @@ -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 + + } + +} + +// MARK: - HTTPRequestBody + extension PostReceiptDataOperation.PostData: HTTPRequestBody { var contentForSignature: [(key: String, value: String)] { diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 1f5d8c2107..5f329f6c05 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -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 @@ -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, @@ -351,6 +353,23 @@ 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() + ) + } 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, @@ -453,6 +472,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void subscriberAttributes: subscriberAttributes, operationDispatcher: operationDispatcher, customerInfoManager: customerInfoManager, + paywallEventsManager: paywallEventsManager, productsManager: productsManager, offeringsManager: offeringsManager, offlineEntitlementsManager: offlineEntitlementsManager, @@ -479,6 +499,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void subscriberAttributes: Attribution, operationDispatcher: OperationDispatcher, customerInfoManager: CustomerInfoManager, + paywallEventsManager: PaywallEventsManagerType?, productsManager: ProductsManagerType, offeringsManager: OfferingsManager, offlineEntitlementsManager: OfflineEntitlementsManager, @@ -526,6 +547,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 @@ -1031,6 +1053,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 { + + /// 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() + + case .cancel: + // No special handling, simply track the event below. + break + } + + await self.paywallEventsManager?.track(paywallEvent: paywallEvent) + } + +} + // MARK: Configuring Purchases public extension Purchases { diff --git a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift index ff2c3f5270..aeb6e2bb61 100644 --- a/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift +++ b/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift @@ -43,6 +43,7 @@ final class PurchasesOrchestrator { private let _allowSharingAppStoreAccount: Atomic = nil private let presentedOfferingIDsByProductID: Atomic<[String: String]> = .init([:]) + private let presentedPaywall: Atomic = nil private let purchaseCompleteCallbacksByProductID: Atomic<[String: PurchaseCompletedBlock]> = .init([:]) private var appUserID: String { self.currentUserProvider.currentAppUserID } @@ -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) @@ -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 @@ -1085,6 +1097,7 @@ private extension PurchasesOrchestrator { data: .init( appUserID: self.appUserID, presentedOfferingID: offeringID, + presentedPaywall: paywall, unsyncedAttributes: unsyncedAttributes, aadAttributionToken: adServicesToken, storefront: storefront, @@ -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( @@ -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 @@ -1215,6 +1233,7 @@ extension PurchasesOrchestrator { data: .init( appUserID: self.appUserID, presentedOfferingID: offeringID, + presentedPaywall: paywall, unsyncedAttributes: unsyncedAttributes, aadAttributionToken: adServicesToken, storefront: storefront, diff --git a/Sources/Purchasing/Purchases/TransactionPoster.swift b/Sources/Purchasing/Purchases/TransactionPoster.swift index 7006e9dfd0..92b5950d70 100644 --- a/Sources/Purchasing/Purchases/TransactionPoster.swift +++ b/Sources/Purchasing/Purchases/TransactionPoster.swift @@ -26,6 +26,7 @@ struct PurchasedTransactionData { var appUserID: String var presentedOfferingID: String? + var presentedPaywall: PaywallEvent.Data? var unsyncedAttributes: SubscriberAttribute.Dictionary? var aadAttributionToken: String? var storefront: StorefrontType? @@ -85,7 +86,8 @@ final class TransactionPoster: TransactionPosterType { transactionID: transaction.transactionIdentifier, productID: transaction.productIdentifier, transactionDate: transaction.purchaseDate, - offeringID: data.presentedOfferingID + offeringID: data.presentedOfferingID, + paywallSessionID: data.presentedPaywall?.sessionIdentifier )) self.receiptFetcher.receiptData( diff --git a/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift index 52ab2d8a29..20dbe9f766 100644 --- a/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift @@ -91,7 +91,7 @@ extension BaseStoreKitIntegrationTests { static let group3MonthlyNoTrialProductID = "com.revenuecat.monthly.1.99.no_intro" static let group3YearlyTrialProductID = "com.revenuecat.annual.10.99.1_free_week" - private var currentOffering: Offering { + var currentOffering: Offering { get async throws { return try await XCTAsyncUnwrap(try await self.purchases.offerings().current) } diff --git a/Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift b/Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift new file mode 100644 index 0000000000..2b2c8ce6b1 --- /dev/null +++ b/Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift @@ -0,0 +1,53 @@ +// +// 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 +// +// PaywallEventsIntegrationTests.swift +// +// Created by Nacho Soto on 9/6/23. + +import Foundation + +import Nimble +@testable import RevenueCat +import XCTest + +class PaywallEventsIntegrationTests: BaseStoreKitIntegrationTests { + + func testPurchasingPackageWithPresentedPaywall() async throws { + let offering = try await self.currentOffering + let paywall = try XCTUnwrap(offering.paywall) + let package = try XCTUnwrap(offering.monthly) + let event: PaywallEvent.Data = .init( + offering: offering, + paywall: paywall, + sessionID: .init(), + displayMode: .fullScreen, + locale: .current, + darkMode: true + ) + + try await self.purchases.track(paywallEvent: .view(event)) + + let transaction = try await XCTAsyncUnwrap(try await self.purchases.purchase(package: package).transaction) + + self.logger.verifyMessageWasLogged( + Strings.purchase.transaction_poster_handling_transaction( + transactionID: transaction.transactionIdentifier, + productID: package.storeProduct.productIdentifier, + transactionDate: transaction.purchaseDate, + offeringID: package.offeringIdentifier, + paywallSessionID: event.sessionIdentifier + ) + ) + + // TODO: purchasing once is clearing the cached presented paywall... + // if that fails, the second time it won't be sent + } + +} diff --git a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift index 405c1e7eba..0ad8d4e5ed 100644 --- a/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitIntegrationTests.swift @@ -59,7 +59,8 @@ class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests { transactionID: transaction.transactionIdentifier, productID: package.storeProduct.productIdentifier, transactionDate: transaction.purchaseDate, - offeringID: package.offeringIdentifier + offeringID: package.offeringIdentifier, + paywallSessionID: nil ) ) } @@ -97,7 +98,8 @@ class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests { transactionID: transaction.transactionIdentifier, productID: package.storeProduct.productIdentifier, transactionDate: transaction.purchaseDate, - offeringID: package.offeringIdentifier + offeringID: package.offeringIdentifier, + paywallSessionID: nil ) ) } diff --git a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift index 899bb4b4a8..e6c7dcaa8d 100644 --- a/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift +++ b/Tests/UnitTests/Networking/Backend/BackendPostReceiptDataTests.swift @@ -384,6 +384,55 @@ class BackendPostReceiptDataTests: BaseBackendPostReceiptDataTests { expect(self.httpClient.calls).to(haveCount(1)) } + func testPostsReceiptDataWithPresentedPaywall() throws { + self.httpClient.mock( + requestPath: .postReceiptData, + response: .init(statusCode: .success, response: Self.validCustomerResponse) + ) + + let productIdentifier = "a_great_product" + let offeringIdentifier = "a_offering" + let price: Decimal = 10.98 + let group = "sub_group" + + let currencyCode = "BFD" + + let paywallEventData: PaywallEvent.Data = .init( + offeringIdentifier: offeringIdentifier, + paywallRevision: 5, + sessionID: .init(uuidString: "73616D70-6C65-2073-7472-696E67000000")!, + displayMode: .condensedFooter, + localeIdentifier: "en_US", + darkMode: true, + date: .init(timeIntervalSince1970: 1694029328) + ) + + let productData: ProductRequestData = .createMockProductData(productIdentifier: productIdentifier, + paymentMode: nil, + currencyCode: currencyCode, + price: price, + subscriptionGroup: group) + + waitUntil { completed in + self.backend.post(receiptData: Self.receiptData, + productData: productData, + transactionData: .init( + appUserID: Self.userID, + presentedOfferingID: offeringIdentifier, + presentedPaywall: paywallEventData, + unsyncedAttributes: nil, + storefront: nil, + source: .init(isRestore: false, initiationSource: .purchase) + ), + observerMode: false, + completion: { _ in + completed() + }) + } + + expect(self.httpClient.calls).to(haveCount(1)) + } + func testIndividualParamsCanBeNil() { httpClient.mock( requestPath: .postReceiptData, diff --git a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift index aa8437bbd2..c9ce27b5d7 100644 --- a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift @@ -44,6 +44,7 @@ class BasePurchasesTests: TestCase { self.deviceCache = MockDeviceCache(sandboxEnvironmentDetector: self.systemInfo, userDefaults: self.userDefaults) self.paywallCache = .init() + self.paywallEventsManager = .init() self.requestFetcher = MockRequestFetcher() self.mockProductsManager = MockProductsManager(systemInfo: self.systemInfo, requestTimeout: Configuration.storeKitRequestTimeoutDefault) @@ -144,6 +145,7 @@ class BasePurchasesTests: TestCase { let offeringsFactory = MockOfferingsFactory() var deviceCache: MockDeviceCache! var paywallCache: MockPaywallCacheWarming! + var paywallEventsManager: MockPaywallEventsManager! var subscriberAttributesManager: MockSubscriberAttributesManager! var attribution: Attribution! var identityManager: MockIdentityManager! @@ -261,6 +263,7 @@ class BasePurchasesTests: TestCase { subscriberAttributes: self.attribution, operationDispatcher: self.mockOperationDispatcher, customerInfoManager: self.customerInfoManager, + paywallEventsManager: self.paywallEventsManager, productsManager: self.mockProductsManager, offeringsManager: self.mockOfferingsManager, offlineEntitlementsManager: self.mockOfflineEntitlementsManager, @@ -508,6 +511,7 @@ private extension BasePurchasesTests { self.purchasesOrchestrator = nil self.deviceCache = nil self.paywallCache = nil + self.paywallEventsManager = nil self.purchases = nil } diff --git a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index b903c4fd13..200b3408c3 100644 --- a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -206,6 +206,7 @@ class PurchasesSubscriberAttributesTests: TestCase { subscriberAttributes: attribution, operationDispatcher: mockOperationDispatcher, customerInfoManager: customerInfoManager, + paywallEventsManager: MockPaywallEventsManager(), productsManager: mockProductsManager, offeringsManager: mockOfferingsManager, offlineEntitlementsManager: mockOfflineEntitlementsManager,