Skip to content

Commit

Permalink
Paywalls: pre-fetch intro eligibility for paywalls (#2860)
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Sep 8, 2023
1 parent c5df9bd commit 853ba37
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 12 deletions.
4 changes: 4 additions & 0 deletions Sources/Logging/Strings/EligibilityStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)"
}
}

Expand Down
21 changes: 16 additions & 5 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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<String> = .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 }
}
}
}

}
Expand Down
16 changes: 9 additions & 7 deletions Tests/UnitTests/Mocks/MockOfferingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ typealias OfferingsCompletion = @MainActor @Sendable (Result<Offerings, Error>)
var invokedOfferingsParametersList = [(appUserID: String,
fetchPolicy: FetchPolicy,
completion: OfferingsCompletion??)]()
var stubbedOfferingsCompletionResult: Result<Offerings, Error>?
var stubbedOfferingsCompletionResult: Result<Offerings, Error> = .failure(
.configurationError("Stub not setup", underlyingError: nil)
)

override func offerings(appUserID: String,
fetchPolicy: FetchPolicy,
Expand All @@ -41,22 +43,23 @@ typealias OfferingsCompletion = @MainActor @Sendable (Result<Offerings, Error>)
self.invokedOfferingsParametersList.append((appUserID, fetchPolicy, completion))

OperationDispatcher.dispatchOnMainActor { [result = self.stubbedOfferingsCompletionResult] in
completion?(result!)
completion?(result)
}
}

struct InvokedUpdateOfferingsCacheParameters {
let appUserID: String
let isAppBackgrounded: Bool
let fetchPolicy: OfferingsManager.FetchPolicy
let completion: (@MainActor @Sendable (Result<Offerings, Error>) -> Void)?
}

var invokedUpdateOfferingsCache = false
var invokedUpdateOfferingsCacheCount = 0
var invokedUpdateOfferingsCacheParameters: InvokedUpdateOfferingsCacheParameters?
var invokedUpdateOfferingsCachesParametersList = [InvokedUpdateOfferingsCacheParameters]()
var stubbedUpdateOfferingsCompletionResult: Result<Offerings, Error>?
var stubbedUpdateOfferingsCompletionResult: Result<Offerings, Error> = .failure(
.configurationError("Stub not setup", underlyingError: nil)
)

override func updateOfferingsCache(
appUserID: String,
Expand All @@ -70,15 +73,14 @@ typealias OfferingsCompletion = @MainActor @Sendable (Result<Offerings, Error>)
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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}

0 comments on commit 853ba37

Please sign in to comment.