From 853ba3717fd70295f8ce9b1bc2615180e101ef21 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 25 Jul 2023 01:25:11 +0200 Subject: [PATCH] `Paywalls`: pre-fetch intro eligibility for paywalls (#2860) --- .../Logging/Strings/EligibilityStrings.swift | 4 + Sources/Purchasing/Purchases/Purchases.swift | 21 ++++-- .../Mocks/MockOfferingsManager.swift | 16 ++-- .../PurchasesGetOfferingsTests.swift | 74 +++++++++++++++++++ 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/Sources/Logging/Strings/EligibilityStrings.swift b/Sources/Logging/Strings/EligibilityStrings.swift index 752cb15440..1d92ad276c 100644 --- a/Sources/Logging/Strings/EligibilityStrings.swift +++ b/Sources/Logging/Strings/EligibilityStrings.swift @@ -24,6 +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) } extension EligibilityStrings: LogMessage { @@ -52,6 +53,9 @@ 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)" } } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index f9e8fe6129..71e6f24719 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -670,9 +670,7 @@ public extension Purchases { } self.systemInfo.isApplicationBackgrounded { isAppBackgrounded in - self.offeringsManager.updateOfferingsCache(appUserID: self.appUserID, - isAppBackgrounded: isAppBackgrounded, - completion: nil) + self.updateOfferingsCache(isAppBackgrounded: isAppBackgrounded) } } } @@ -1615,8 +1613,21 @@ private extension Purchases { private func updateOfferingsCache(isAppBackgrounded: Bool) { self.offeringsManager.updateOfferingsCache(appUserID: self.appUserID, - isAppBackgrounded: isAppBackgrounded, - completion: nil) + isAppBackgrounded: isAppBackgrounded) { offerings in + if let offering = offerings.value?.current, let paywall = offering.paywall { + let packageTypes = Set(paywall.config.packages) + let products: Set = .init( + offering.availablePackages + .lazy + .filter { packageTypes.contains($0.packageType) } + .map(\.storeProduct.productIdentifier) + ) + + Logger.debug(Strings.eligibility.warming_up_eligibility_cache(paywall)) + + self.trialOrIntroPriceEligibilityChecker.checkEligibility(productIdentifiers: products) { _ in } + } + } } } diff --git a/Tests/UnitTests/Mocks/MockOfferingsManager.swift b/Tests/UnitTests/Mocks/MockOfferingsManager.swift index ab7a8f12f7..11cd527cab 100644 --- a/Tests/UnitTests/Mocks/MockOfferingsManager.swift +++ b/Tests/UnitTests/Mocks/MockOfferingsManager.swift @@ -30,7 +30,9 @@ typealias OfferingsCompletion = @MainActor @Sendable (Result) var invokedOfferingsParametersList = [(appUserID: String, fetchPolicy: FetchPolicy, completion: OfferingsCompletion??)]() - var stubbedOfferingsCompletionResult: Result? + var stubbedOfferingsCompletionResult: Result = .failure( + .configurationError("Stub not setup", underlyingError: nil) + ) override func offerings(appUserID: String, fetchPolicy: FetchPolicy, @@ -41,7 +43,7 @@ typealias OfferingsCompletion = @MainActor @Sendable (Result) self.invokedOfferingsParametersList.append((appUserID, fetchPolicy, completion)) OperationDispatcher.dispatchOnMainActor { [result = self.stubbedOfferingsCompletionResult] in - completion?(result!) + completion?(result) } } @@ -49,14 +51,15 @@ typealias OfferingsCompletion = @MainActor @Sendable (Result) let appUserID: String let isAppBackgrounded: Bool let fetchPolicy: OfferingsManager.FetchPolicy - let completion: (@MainActor @Sendable (Result) -> Void)? } var invokedUpdateOfferingsCache = false var invokedUpdateOfferingsCacheCount = 0 var invokedUpdateOfferingsCacheParameters: InvokedUpdateOfferingsCacheParameters? var invokedUpdateOfferingsCachesParametersList = [InvokedUpdateOfferingsCacheParameters]() - var stubbedUpdateOfferingsCompletionResult: Result? + var stubbedUpdateOfferingsCompletionResult: Result = .failure( + .configurationError("Stub not setup", underlyingError: nil) + ) override func updateOfferingsCache( appUserID: String, @@ -70,15 +73,14 @@ typealias OfferingsCompletion = @MainActor @Sendable (Result) let parameters = InvokedUpdateOfferingsCacheParameters( appUserID: appUserID, isAppBackgrounded: isAppBackgrounded, - fetchPolicy: fetchPolicy, - completion: completion + fetchPolicy: fetchPolicy ) self.invokedUpdateOfferingsCacheParameters = parameters self.invokedUpdateOfferingsCachesParametersList.append(parameters) OperationDispatcher.dispatchOnMainActor { [result = self.stubbedUpdateOfferingsCompletionResult] in - completion?(result!) + completion?(result) } } diff --git a/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift b/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift index 6f2eeec569..51cf64f61a 100644 --- a/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift +++ b/Tests/UnitTests/Purchasing/Purchases/PurchasesGetOfferingsTests.swift @@ -99,4 +99,78 @@ 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 { + 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: "weekly", + packageType: .weekly, + storeProduct: StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "product_1")), + offeringIdentifier: "offering" + ), + .init( + identifier: "monthly", + packageType: .monthly, + storeProduct: StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "product_2")), + offeringIdentifier: "offering" + ) + ]) + + self.systemInfo.stubbedIsApplicationBackgrounded = false + self.mockOfferingsManager.stubbedUpdateOfferingsCompletionResult = .success( + .init( + offerings: [ + offering.identifier: offering + ], + currentOfferingID: offering.identifier, + response: offeringsResponse + ) + ) + + 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) + } + }