diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index b6737e8038..4232f33aa8 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -227,6 +227,9 @@ 4F5D52EC2A57152B00E1C758 /* ImageSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA622A37A2E9002C2112 /* ImageSnapshot.swift */; }; 4F69EB092A14406E00ED6D4B /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F69EB082A14406E00ED6D4B /* Matchers.swift */; }; 4F69EB0A2A14406E00ED6D4B /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F69EB082A14406E00ED6D4B /* Matchers.swift */; }; + 4F6ABC782A81595900250E63 /* PaywallCacheWarming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6ABC772A81595900250E63 /* PaywallCacheWarming.swift */; }; + 4F6ABC7A2A81649800250E63 /* MockPaywallCacheWarming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6ABC792A81649800250E63 /* MockPaywallCacheWarming.swift */; }; + 4F6ABC7C2A81673F00250E63 /* PaywallCacheWarmingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6ABC7B2A81673F00250E63 /* PaywallCacheWarmingTests.swift */; }; 4F6BED592A26A14400CD9322 /* DebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BED582A26A14400CD9322 /* DebugView.swift */; }; 4F6BEDD92A26B55C00CD9322 /* DebugViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */; }; 4F6BEDE02A26B65900CD9322 /* DebugViewSheetPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BEDDF2A26B65900CD9322 /* DebugViewSheetPresentation.swift */; }; @@ -980,6 +983,9 @@ 4F5C05BC2A43A21A00651C7D /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = ""; }; 4F5C05BE2A43A2C500651C7D /* LocaleExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtensionsTests.swift; sourceTree = ""; }; 4F69EB082A14406E00ED6D4B /* Matchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matchers.swift; sourceTree = ""; }; + 4F6ABC772A81595900250E63 /* PaywallCacheWarming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallCacheWarming.swift; sourceTree = ""; }; + 4F6ABC792A81649800250E63 /* MockPaywallCacheWarming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaywallCacheWarming.swift; sourceTree = ""; }; + 4F6ABC7B2A81673F00250E63 /* PaywallCacheWarmingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallCacheWarmingTests.swift; sourceTree = ""; }; 4F6BED582A26A14400CD9322 /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = ""; }; 4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewModel.swift; sourceTree = ""; }; 4F6BEDDF2A26B65900CD9322 /* DebugViewSheetPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewSheetPresentation.swift; sourceTree = ""; }; @@ -1859,6 +1865,7 @@ 5791FBD1299184EF00F1FEDA /* MockAsyncSequence.swift */, 57CB2A7B29CCC91800C91439 /* MockProductEntitlementMappingFetcher.swift */, 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */, + 4F6ABC792A81649800250E63 /* MockPaywallCacheWarming.swift */, ); path = Mocks; sourceTree = ""; @@ -2248,6 +2255,7 @@ 4FBBD4E52A620573001CBA21 /* PaywallColor.swift */, 4F87610E2A5C9E490006FA14 /* PaywallData.swift */, 4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */, + 4F6ABC772A81595900250E63 /* PaywallCacheWarming.swift */, ); path = Paywalls; sourceTree = ""; @@ -2267,6 +2275,7 @@ 4FBBD4E22A620516001CBA21 /* Paywalls */ = { isa = PBXGroup; children = ( + 4F6ABC7B2A81673F00250E63 /* PaywallCacheWarmingTests.swift */, 4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */, 4FBBD4E32A620539001CBA21 /* PaywallColorTests.swift */, ); @@ -3391,6 +3400,7 @@ 57CD86DA291C1E2300768DE1 /* UserDefaults+Extensions.swift in Sources */, F5BE424026962ACF00254A30 /* ReceiptRefreshPolicy.swift in Sources */, 9A65E0762591977200DE00B0 /* IdentityStrings.swift in Sources */, + 4F6ABC782A81595900250E63 /* PaywallCacheWarming.swift in Sources */, F5714EAA26D7A85D00635477 /* PeriodType+Extensions.swift in Sources */, 57045B3A29C51751001A5417 /* GetProductEntitlementMappingOperation.swift in Sources */, 4FC083292A4A35FB00A97089 /* Integer+Extensions.swift in Sources */, @@ -3588,6 +3598,7 @@ 57BF87592967880C00424254 /* MockCachingTrialOrIntroPriceEligibilityChecker.swift in Sources */, 2DDF41CD24F6F4C3005BC22D /* ASN1ContainerBuilderTests.swift in Sources */, 573A10D52800A7C800F976E5 /* SKErrorTests.swift in Sources */, + 4F6ABC7A2A81649800250E63 /* MockPaywallCacheWarming.swift in Sources */, B31C8BEC285BD58F001017B7 /* MockIdentityAPI.swift in Sources */, 351B513D26D4491E00BD2BD7 /* MockDeviceCache.swift in Sources */, 57910CB329C3889B006209D5 /* DispatchTimeIntervalExtensionsTests.swift in Sources */, @@ -3659,6 +3670,7 @@ 2DDF41DF24F6F527005BC22D /* MockProductsManager.swift in Sources */, 351B514F26D44ACE00BD2BD7 /* PurchasesSubscriberAttributesTests.swift in Sources */, 57DBFA5D28AADA43002D18CA /* PurchasesLogInTests.swift in Sources */, + 4F6ABC7C2A81673F00250E63 /* PaywallCacheWarmingTests.swift in Sources */, 57D04BB827D947C6006DAC06 /* HTTPResponseTests.swift in Sources */, 5796A38127D6B78500653165 /* BaseBackendTest.swift in Sources */, 351B516226D44BEE00BD2BD7 /* CustomerInfoManagerTests.swift in Sources */, diff --git a/Sources/Logging/Strings/EligibilityStrings.swift b/Sources/Logging/Strings/EligibilityStrings.swift index 1d92ad276c..cc67d04166 100644 --- a/Sources/Logging/Strings/EligibilityStrings.swift +++ b/Sources/Logging/Strings/EligibilityStrings.swift @@ -24,7 +24,7 @@ enum EligibilityStrings { case check_eligibility_no_identifiers case check_eligibility_failed(productIdentifier: String, error: Error) case sk2_intro_eligibility_too_slow - case warming_up_eligibility_cache(PaywallData) + case warming_up_eligibility_cache(products: Set) } extension EligibilityStrings: LogMessage { @@ -54,8 +54,8 @@ extension EligibilityStrings: LogMessage { case .sk2_intro_eligibility_too_slow: return "StoreKit 2 intro eligibility took longer than expected to determine" - case let .warming_up_eligibility_cache(paywall): - return "Warming up intro eligibility cache for packages in paywall: \(paywall.config.packages)" + case let .warming_up_eligibility_cache(products): + return "Warming up intro eligibility cache for \(products.count) products" } } diff --git a/Sources/Paywalls/PaywallCacheWarming.swift b/Sources/Paywalls/PaywallCacheWarming.swift new file mode 100644 index 0000000000..2e44d0fe81 --- /dev/null +++ b/Sources/Paywalls/PaywallCacheWarming.swift @@ -0,0 +1,61 @@ +// +// 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 +// +// PaywallCacheWarming.swift +// +// Created by Nacho Soto on 8/7/23. + +import Foundation + +protocol PaywallCacheWarmingType: Sendable { + + func warmUpEligibilityCache(offerings: Offerings) + +} + +final class PaywallCacheWarming: PaywallCacheWarmingType { + + private let introEligibiltyChecker: TrialOrIntroPriceEligibilityCheckerType + + init(introEligibiltyChecker: TrialOrIntroPriceEligibilityCheckerType) { + self.introEligibiltyChecker = introEligibiltyChecker + } + + func warmUpEligibilityCache(offerings: Offerings) { + let productIdentifiers = Set( + offerings + .all + .values + .lazy + .flatMap(\.productIdentifiersInPaywall) + ) + + guard !productIdentifiers.isEmpty else { return } + + Logger.debug(Strings.eligibility.warming_up_eligibility_cache(products: productIdentifiers)) + self.introEligibiltyChecker.checkEligibility(productIdentifiers: productIdentifiers) { _ in } + } + +} + +private extension Offering { + + var productIdentifiersInPaywall: Set { + guard let paywall = self.paywall else { return [] } + + let packageTypes = Set(paywall.config.packages) + return Set( + self.availablePackages + .lazy + .filter { packageTypes.contains($0.identifier) } + .map(\.storeProduct.productIdentifier) + ) + } + +} diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index fc37cb8e03..bf92376528 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -237,6 +237,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void private let attributionPoster: AttributionPoster private let backend: Backend private let deviceCache: DeviceCache + private let paywallCache: PaywallCacheWarmingType private let identityManager: IdentityManager private let userDefaults: UserDefaults private let notificationCenter: NotificationCenter @@ -417,13 +418,17 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void } }() - let trialOrIntroPriceChecker = TrialOrIntroPriceEligibilityChecker(systemInfo: systemInfo, - receiptFetcher: receiptFetcher, - introEligibilityCalculator: introCalculator, - backend: backend, - currentUserProvider: identityManager, - operationDispatcher: operationDispatcher, - productsManager: productsManager) + let trialOrIntroPriceChecker = CachingTrialOrIntroPriceEligibilityChecker.create( + with: TrialOrIntroPriceEligibilityChecker(systemInfo: systemInfo, + receiptFetcher: receiptFetcher, + introEligibilityCalculator: introCalculator, + backend: backend, + currentUserProvider: identityManager, + operationDispatcher: operationDispatcher, + productsManager: productsManager) + ) + + let paywallCache = PaywallCacheWarming(introEligibiltyChecker: trialOrIntroPriceChecker) self.init(appUserID: appUserID, requestFetcher: fetcher, @@ -437,6 +442,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void systemInfo: systemInfo, offeringsFactory: offeringsFactory, deviceCache: deviceCache, + paywallCache: paywallCache, identityManager: identityManager, subscriberAttributes: subscriberAttributes, operationDispatcher: operationDispatcher, @@ -448,6 +454,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void purchasedProductsFetcher: purchasedProductsFetcher, trialOrIntroPriceEligibilityChecker: trialOrIntroPriceChecker) } + // swiftlint:disable:next function_body_length init(appUserID: String?, requestFetcher: StoreKitRequestFetcher, @@ -461,6 +468,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void systemInfo: SystemInfo, offeringsFactory: OfferingsFactory, deviceCache: DeviceCache, + paywallCache: PaywallCacheWarmingType, identityManager: IdentityManager, subscriberAttributes: Attribution, operationDispatcher: OperationDispatcher, @@ -470,7 +478,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void offlineEntitlementsManager: OfflineEntitlementsManager, purchasesOrchestrator: PurchasesOrchestrator, purchasedProductsFetcher: PurchasedProductsFetcherType?, - trialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityCheckerType + trialOrIntroPriceEligibilityChecker: CachingTrialOrIntroPriceEligibilityChecker ) { if systemInfo.dangerousSettings.customEntitlementComputation { @@ -504,6 +512,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void self.paymentQueueWrapper = paymentQueueWrapper self.offeringsFactory = offeringsFactory self.deviceCache = deviceCache + self.paywallCache = paywallCache self.identityManager = identityManager self.userDefaults = userDefaults self.notificationCenter = notificationCenter @@ -516,7 +525,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void self.offlineEntitlementsManager = offlineEntitlementsManager self.purchasesOrchestrator = purchasesOrchestrator self.purchasedProductsFetcher = purchasedProductsFetcher - self.trialOrIntroPriceEligibilityChecker = .create(with: trialOrIntroPriceEligibilityChecker) + self.trialOrIntroPriceEligibilityChecker = trialOrIntroPriceEligibilityChecker super.init() @@ -1608,29 +1617,18 @@ private extension Purchases { } private func updateOfferingsCache(isAppBackgrounded: Bool) { - self.offeringsManager.updateOfferingsCache(appUserID: self.appUserID, - isAppBackgrounded: isAppBackgrounded) { offerings in - if let offering = offerings.value?.current, let paywall = offering.paywall { + self.offeringsManager.updateOfferingsCache( + appUserID: self.appUserID, + isAppBackgrounded: isAppBackgrounded + ) { [cache = self.paywallCache] offerings in + if let offerings = offerings.value { self.operationDispatcher.dispatchOnWorkerThread { - self.warmUpEligibilityCache(offering: offering, paywall: paywall) + cache.warmUpEligibilityCache(offerings: offerings) } } } } - private func warmUpEligibilityCache(offering: RCOffering, paywall: PaywallData) { - let packageTypes = Set(paywall.config.packages) - let products: Set = .init( - offering.availablePackages - .lazy - .filter { packageTypes.contains($0.identifier) } - .map(\.storeProduct.productIdentifier) - ) - - Logger.debug(Strings.eligibility.warming_up_eligibility_cache(paywall)) - self.trialOrIntroPriceEligibilityChecker.checkEligibility(productIdentifiers: products) { _ in } - } - } // MARK: - Deprecations diff --git a/Tests/UnitTests/Mocks/MockPaywallCacheWarming.swift b/Tests/UnitTests/Mocks/MockPaywallCacheWarming.swift new file mode 100644 index 0000000000..95c2767b27 --- /dev/null +++ b/Tests/UnitTests/Mocks/MockPaywallCacheWarming.swift @@ -0,0 +1,37 @@ +// +// 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 +// +// MockPaywallCacheWarming.swift +// +// Created by Nacho Soto on 8/7/23. + +import Foundation +@testable import RevenueCat + +final class MockPaywallCacheWarming: PaywallCacheWarmingType { + + private let _invokedWarmUpEligibilityCache: Atomic = false + private let _invokedWarmUpEligibilityCacheOfferings: Atomic = nil + + var invokedWarmUpEligibilityCache: Bool { + get { return self._invokedWarmUpEligibilityCache.value } + set { self._invokedWarmUpEligibilityCache.value = newValue } + } + + var invokedWarmUpEligibilityCacheOfferings: Offerings? { + get { return self._invokedWarmUpEligibilityCacheOfferings.value } + set { self._invokedWarmUpEligibilityCacheOfferings.value = newValue } + } + + func warmUpEligibilityCache(offerings: Offerings) { + self.invokedWarmUpEligibilityCache = true + self.invokedWarmUpEligibilityCacheOfferings = offerings + } + +} diff --git a/Tests/UnitTests/Paywalls/PaywallCacheWarmingTests.swift b/Tests/UnitTests/Paywalls/PaywallCacheWarmingTests.swift new file mode 100644 index 0000000000..7ee3d03420 --- /dev/null +++ b/Tests/UnitTests/Paywalls/PaywallCacheWarmingTests.swift @@ -0,0 +1,134 @@ +// +// 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 +// +// PaywallCacheWarmingTests.swift +// +// Created by Nacho Soto on 8/7/23. + +import Nimble +@testable import RevenueCat +import XCTest + +class PaywallCacheWarmingTests: TestCase { + + private var eligibilityChecker: MockTrialOrIntroPriceEligibilityChecker! + private var cache: PaywallCacheWarmingType! + + override func setUp() { + super.setUp() + + self.eligibilityChecker = .init() + self.cache = PaywallCacheWarming(introEligibiltyChecker: self.eligibilityChecker) + } + + func testOfferingsWithNoPaywallsDoesNotCheckEligibility() throws { + self.cache.warmUpEligibilityCache( + offerings: try Self.createOfferings([ + Self.createOffering( + identifier: Self.offeringIdentifier, + paywall: nil, + products: [ + (.monthly, "product_1") + ] + ) + ]) + ) + + expect(self.eligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore) == false + } + + func testWarmsUpEligibilityCache() throws { + let paywall = try Self.loadPaywall("PaywallData-Sample1") + let offerings = try Self.createOfferings([ + Self.createOffering( + identifier: Self.offeringIdentifier, + paywall: paywall, + products: [ + (.monthly, "product_1"), + (.weekly, "product_2") + ] + ), + Self.createOffering( + identifier: "offering_2", + paywall: paywall, + products: [ + (.annual, "product_3") + ] + ) + ]) + + self.cache.warmUpEligibilityCache(offerings: offerings) + + expect(self.eligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore) == true + expect(self.eligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreCount) == 1 + // Paywall filters packages so only `monthly` and `annual` should is used. + expect( + self.eligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParameters + ) == [ + "product_1", + "product_3" + ] + + self.logger.verifyMessageWasLogged( + Strings.eligibility.warming_up_eligibility_cache(products: ["product_1", "product_3"]), + level: .debug, + expectedCount: 1 + ) + } + +} + +private extension PaywallCacheWarmingTests { + + static func createOffering( + identifier: String, + paywall: PaywallData?, + products: [(PackageType, String)] + ) throws -> Offering { + return Offering( + identifier: identifier, + serverDescription: identifier, + paywall: paywall, + availablePackages: products.map { packageType, productID in + .init( + identifier: Package.string(from: packageType)!, + packageType: packageType, + storeProduct: StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: productID)), + offeringIdentifier: identifier + ) + } + ) + } + + static func createOfferings(_ offerings: [Offering]) throws -> Offerings { + let offeringsURL = try XCTUnwrap(Self.bundle.url(forResource: "Offerings", + withExtension: "json", + subdirectory: "Fixtures")) + + let offeringsResponse = try OfferingsResponse.create(with: XCTUnwrap(Data(contentsOf: offeringsURL))) + + return .init( + offerings: Set(offerings).dictionaryWithKeys(\.identifier), + currentOfferingID: Self.offeringIdentifier, + response: offeringsResponse + ) + } + + static func loadPaywall(_ name: String) throws -> PaywallData { + let paywallURL = try XCTUnwrap(Self.bundle.url(forResource: name, + withExtension: "json", + subdirectory: "Fixtures")) + + return try PaywallData.create(with: XCTUnwrap(Data(contentsOf: paywallURL))) + } + + static let bundle = Bundle(for: PaywallCacheWarmingTests.self) + static let offeringIdentifier = "offering" + +} diff --git a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift index 9d300e9ef1..aa8437bbd2 100644 --- a/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/BasePurchasesTests.swift @@ -43,6 +43,7 @@ class BasePurchasesTests: TestCase { clock: self.clock) self.deviceCache = MockDeviceCache(sandboxEnvironmentDetector: self.systemInfo, userDefaults: self.userDefaults) + self.paywallCache = .init() self.requestFetcher = MockRequestFetcher() self.mockProductsManager = MockProductsManager(systemInfo: self.systemInfo, requestTimeout: Configuration.storeKitRequestTimeoutDefault) @@ -142,6 +143,7 @@ class BasePurchasesTests: TestCase { var userDefaults: UserDefaults! = nil let offeringsFactory = MockOfferingsFactory() var deviceCache: MockDeviceCache! + var paywallCache: MockPaywallCacheWarming! var subscriberAttributesManager: MockSubscriberAttributesManager! var attribution: Attribution! var identityManager: MockIdentityManager! @@ -213,7 +215,6 @@ class BasePurchasesTests: TestCase { } func initializePurchasesInstance(appUserId: String?, withDelegate: Bool = true) { - self.purchasesOrchestrator = PurchasesOrchestrator( productsManager: self.mockProductsManager, paymentQueueWrapper: self.paymentQueueWrapper, @@ -255,6 +256,7 @@ class BasePurchasesTests: TestCase { systemInfo: self.systemInfo, offeringsFactory: self.offeringsFactory, deviceCache: self.deviceCache, + paywallCache: self.paywallCache, identityManager: self.identityManager, subscriberAttributes: self.attribution, operationDispatcher: self.mockOperationDispatcher, @@ -505,6 +507,7 @@ private extension BasePurchasesTests { self.mockBeginRefundRequestHelper = nil self.purchasesOrchestrator = nil self.deviceCache = nil + self.paywallCache = nil self.purchases = nil } diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift index 36d8ee70c8..a3d0523d92 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift @@ -99,78 +99,35 @@ class PurchasesGetOfferingsTests: BasePurchasesTests { expect(self.deviceCache.clearOfferingsCacheTimestampCount) == 0 } - func testOfferingsWithNoPaywallsDoesNotCheckEligibility() { - self.systemInfo.stubbedIsApplicationBackgrounded = false - self.setupPurchases() - - expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount).toEventually(equal(1)) - - expect( - self.trialOrIntroPriceEligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore - ) == false - } - - func testOfferingsWithPaywallsWarmsUpEligibilityCache() throws { + func testWarmsUpEligibilityCache() throws { let bundle = Bundle(for: Self.self) - let paywallURL = try XCTUnwrap(bundle.url(forResource: "PaywallData-Sample1", - withExtension: "json", - subdirectory: "Fixtures")) let offeringsURL = try XCTUnwrap(bundle.url(forResource: "Offerings", withExtension: "json", subdirectory: "Fixtures")) - - let paywall = try PaywallData.create(with: XCTUnwrap(Data(contentsOf: paywallURL))) let offeringsResponse = try OfferingsResponse.create(with: XCTUnwrap(Data(contentsOf: offeringsURL))) let offering = Offering( identifier: "offering", serverDescription: "", - paywall: paywall, - availablePackages: [ - .init( - identifier: "$rc_weekly", - packageType: .weekly, - storeProduct: StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "product_1")), - offeringIdentifier: "offering" - ), - .init( - identifier: "$rc_monthly", - packageType: .monthly, - storeProduct: StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "product_2")), - offeringIdentifier: "offering" - ) - ]) + paywall: nil, + availablePackages: [] + ) + let offerings = Offerings( + offerings: [ + offering.identifier: offering + ], + currentOfferingID: offering.identifier, + response: offeringsResponse + ) self.systemInfo.stubbedIsApplicationBackgrounded = false - self.mockOfferingsManager.stubbedUpdateOfferingsCompletionResult = .success( - .init( - offerings: [ - offering.identifier: offering - ], - currentOfferingID: offering.identifier, - response: offeringsResponse - ) - ) + self.mockOfferingsManager.stubbedUpdateOfferingsCompletionResult = .success(offerings) self.setupPurchases() expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount).toEventually(equal(1)) - expect( - self.trialOrIntroPriceEligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStore - ) == true - expect( - self.trialOrIntroPriceEligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreCount - ) == 1 - // Paywall filters packages so only `monthly` should is used. - expect( - self.trialOrIntroPriceEligibilityChecker.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParameters - ) == [ - "product_2" - ] - - self.logger.verifyMessageWasLogged(Strings.eligibility.warming_up_eligibility_cache(paywall), - level: .debug, - expectedCount: 1) + expect(self.paywallCache.invokedWarmUpEligibilityCache) == true + expect(self.paywallCache.invokedWarmUpEligibilityCacheOfferings) == offerings } } diff --git a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index bfdafbe55f..b903c4fd13 100644 --- a/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/Tests/UnitTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -201,6 +201,7 @@ class PurchasesSubscriberAttributesTests: TestCase { systemInfo: systemInfo, offeringsFactory: mockOfferingsFactory, deviceCache: mockDeviceCache, + paywallCache: MockPaywallCacheWarming(), identityManager: mockIdentityManager, subscriberAttributes: attribution, operationDispatcher: mockOperationDispatcher, @@ -210,7 +211,9 @@ class PurchasesSubscriberAttributesTests: TestCase { offlineEntitlementsManager: mockOfflineEntitlementsManager, purchasesOrchestrator: purchasesOrchestrator, purchasedProductsFetcher: mockPurchasedProductsFetcher, - trialOrIntroPriceEligibilityChecker: trialOrIntroductoryPriceEligibilityChecker) + trialOrIntroPriceEligibilityChecker: .create( + with: trialOrIntroductoryPriceEligibilityChecker + )) purchasesOrchestrator.delegate = purchases purchases!.delegate = purchasesDelegate Purchases.setDefaultInstance(purchases!)