Skip to content

Commit

Permalink
Paywalls: extracted PaywallCacheWarming (#2977)
Browse files Browse the repository at this point in the history
This refactors #2860 extracting it to a separate type so we can easily
extend it to pre-warm the image cache as well (PWL-10).
  • Loading branch information
NachoSoto authored Aug 7, 2023
1 parent bd91793 commit 42e2258
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 88 deletions.
12 changes: 12 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,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 */; };
Expand Down Expand Up @@ -964,6 +967,9 @@
4F5C05BC2A43A21A00651C7D /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = "<group>"; };
4F5C05BE2A43A2C500651C7D /* LocaleExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtensionsTests.swift; sourceTree = "<group>"; };
4F69EB082A14406E00ED6D4B /* Matchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matchers.swift; sourceTree = "<group>"; };
4F6ABC772A81595900250E63 /* PaywallCacheWarming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallCacheWarming.swift; sourceTree = "<group>"; };
4F6ABC792A81649800250E63 /* MockPaywallCacheWarming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaywallCacheWarming.swift; sourceTree = "<group>"; };
4F6ABC7B2A81673F00250E63 /* PaywallCacheWarmingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallCacheWarmingTests.swift; sourceTree = "<group>"; };
4F6BED582A26A14400CD9322 /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = "<group>"; };
4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewModel.swift; sourceTree = "<group>"; };
4F6BEDDF2A26B65900CD9322 /* DebugViewSheetPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewSheetPresentation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1836,6 +1842,7 @@
5791FBD1299184EF00F1FEDA /* MockAsyncSequence.swift */,
57CB2A7B29CCC91800C91439 /* MockProductEntitlementMappingFetcher.swift */,
4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */,
4F6ABC792A81649800250E63 /* MockPaywallCacheWarming.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -2225,6 +2232,7 @@
4FBBD4E52A620573001CBA21 /* PaywallColor.swift */,
4F87610E2A5C9E490006FA14 /* PaywallData.swift */,
4F87612B2A5CAB980006FA14 /* PaywallTemplate.swift */,
4F6ABC772A81595900250E63 /* PaywallCacheWarming.swift */,
);
path = Paywalls;
sourceTree = "<group>";
Expand All @@ -2240,6 +2248,7 @@
4FBBD4E22A620516001CBA21 /* Paywalls */ = {
isa = PBXGroup;
children = (
4F6ABC7B2A81673F00250E63 /* PaywallCacheWarmingTests.swift */,
4F05876E2A5DE03F00E9A834 /* PaywallDataTests.swift */,
4FBBD4E32A620539001CBA21 /* PaywallColorTests.swift */,
);
Expand Down Expand Up @@ -3356,6 +3365,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 */,
Expand Down Expand Up @@ -3551,6 +3561,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 */,
Expand Down Expand Up @@ -3622,6 +3633,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 */,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Logging/Strings/EligibilityStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>)
}

extension EligibilityStrings: LogMessage {
Expand Down Expand Up @@ -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"
}
}

Expand Down
61 changes: 61 additions & 0 deletions Sources/Paywalls/PaywallCacheWarming.swift
Original file line number Diff line number Diff line change
@@ -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<String>(
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<String> {
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)
)
}

}
50 changes: 24 additions & 26 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -437,6 +442,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
systemInfo: systemInfo,
offeringsFactory: offeringsFactory,
deviceCache: deviceCache,
paywallCache: paywallCache,
identityManager: identityManager,
subscriberAttributes: subscriberAttributes,
operationDispatcher: operationDispatcher,
Expand All @@ -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,
Expand All @@ -461,6 +468,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
systemInfo: SystemInfo,
offeringsFactory: OfferingsFactory,
deviceCache: DeviceCache,
paywallCache: PaywallCacheWarmingType,
identityManager: IdentityManager,
subscriberAttributes: Attribution,
operationDispatcher: OperationDispatcher,
Expand All @@ -470,7 +478,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
offlineEntitlementsManager: OfflineEntitlementsManager,
purchasesOrchestrator: PurchasesOrchestrator,
purchasedProductsFetcher: PurchasedProductsFetcherType?,
trialOrIntroPriceEligibilityChecker: TrialOrIntroPriceEligibilityCheckerType
trialOrIntroPriceEligibilityChecker: CachingTrialOrIntroPriceEligibilityChecker
) {

if systemInfo.dangerousSettings.customEntitlementComputation {
Expand Down Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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<String> = .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
Expand Down
37 changes: 37 additions & 0 deletions Tests/UnitTests/Mocks/MockPaywallCacheWarming.swift
Original file line number Diff line number Diff line change
@@ -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<Bool> = false
private let _invokedWarmUpEligibilityCacheOfferings: Atomic<Offerings?> = 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
}

}
Loading

0 comments on commit 42e2258

Please sign in to comment.