Skip to content

Commit

Permalink
Paywalls: `TrialOrIntroEligibilityChecker.eligibility(for packages:…
Browse files Browse the repository at this point in the history
…)` (#2846)

This will be used for the multi-package template.

Includes #2858.
  • Loading branch information
NachoSoto committed Aug 3, 2023
1 parent cca0ce4 commit 0cb5cb6
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 16 deletions.
17 changes: 12 additions & 5 deletions RevenueCatUI/Data/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,18 @@ internal enum TestData {
extension TrialOrIntroEligibilityChecker {

/// Creates a mock `TrialOrIntroEligibilityChecker` with a constant result.
static func producing(eligibility: IntroEligibilityStatus) -> Self {
return .init { product in
return product.hasIntroDiscount
? eligibility
: .noIntroOfferExists
static func producing(eligibility: @autoclosure @escaping () -> IntroEligibilityStatus) -> Self {
return .init { packages in
return Dictionary(
uniqueKeysWithValues: Set(packages)
.map { package in
let result = package.storeProduct.hasIntroDiscount
? eligibility()
: .noIntroOfferExists

return (package, result)
}
)
}
}

Expand Down
20 changes: 9 additions & 11 deletions RevenueCatUI/Helpers/TrialOrIntroEligibilityChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ import RevenueCat
@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *)
final class TrialOrIntroEligibilityChecker: ObservableObject {

typealias Checker = @Sendable (StoreProduct) async -> IntroEligibilityStatus
typealias Checker = @Sendable ([Package]) async -> [Package: IntroEligibilityStatus]

let checker: Checker

convenience init(purchases: Purchases = .shared) {
self.init { product in
guard product.hasIntroDiscount else {
return .noIntroOfferExists
}

return await purchases.checkTrialOrIntroDiscountEligibility(product: product)
self.init {
return await purchases.checkTrialOrIntroDiscountEligibility(packages: $0)
.mapValues(\.status)
}
}

Expand All @@ -35,12 +32,13 @@ final class TrialOrIntroEligibilityChecker: ObservableObject {
@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *)
extension TrialOrIntroEligibilityChecker {

func eligibility(for product: StoreProduct) async -> IntroEligibilityStatus {
return await self.checker(product)
func eligibility(for package: Package) async -> IntroEligibilityStatus {
return await self.eligibility(for: [package])[package] ?? .unknown
}

func eligibility(for package: Package) async -> IntroEligibilityStatus {
return await self.eligibility(for: package.storeProduct)
/// Computes eligibility for a list of packages in parallel, returning them all in a dictionary.
func eligibility(for packages: [Package]) async -> [Package: IntroEligibilityStatus] {
return await self.checker(packages)
}

}
Expand Down
30 changes: 30 additions & 0 deletions Sources/Purchasing/IntroEligibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ private extension IntroEligibilityStatus {
self.status = .unknown
}

public override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Self else { return false }

return other.status == self.status
}

public override var hash: Int {
var hasher = Hasher()
hasher.combine(self.status)

return hasher.finalize()
}

}

extension IntroEligibility {
Expand All @@ -147,6 +160,23 @@ extension IntroEligibility {
}
}

public override var debugDescription: String {
let name = "\(type(of: self))"

switch self.status {
case .eligible:
return "\(name).eligible"
case .ineligible:
return "\(name).ineligible"
case .noIntroOfferExists:
return "\(name).noIntroOfferExists"
case .unknown:
return "\(name).unknown"
@unknown default:
return "Unknown"
}
}

}

extension IntroEligibility: Sendable {}
12 changes: 12 additions & 0 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,18 @@ public extension Purchases {
return await checkTrialOrIntroductoryDiscountEligibilityAsync(productIdentifiers)
}

@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *)
func checkTrialOrIntroDiscountEligibility(packages: [Package]) async -> [Package: IntroEligibility] {
let result = await self.checkTrialOrIntroDiscountEligibility(
productIdentifiers: packages.map(\.storeProduct.productIdentifier)
)

return Set(packages)
.dictionaryWithValues { (package: Package) in
result[package.storeProduct.productIdentifier] ?? .init(eligibilityStatus: .unknown)
}
}

@objc(checkTrialOrIntroDiscountEligibilityForProduct:completion:)
func checkTrialOrIntroDiscountEligibility(product: StoreProduct,
completion: @escaping (IntroEligibilityStatus) -> Void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ private func checkAsyncMethods(purchases: Purchases) async {
let _: [String: IntroEligibility] = await purchases.checkTrialOrIntroDiscountEligibility(
productIdentifiers: [String]()
)
let _: [Package: IntroEligibility] = await purchases.checkTrialOrIntroDiscountEligibility(
packages: [Package]()
)
let _: PromotionalOffer = try await purchases.promotionalOffer(
forProductDiscount: discount,
product: stp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,51 @@ class PurchasesGetProductsTests: BasePurchasesTests {
) == true
}

@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *)
func testGetEligibilityForPackages() async throws {
try AvailabilityChecks.iOS13APIAvailableOrSkipTest()

let packages: [Package] = [
.init(identifier: "package1",
packageType: .weekly,
storeProduct: .init(sk1Product: MockSK1Product(mockProductIdentifier: "product1")),
offeringIdentifier: "offering"),
.init(identifier: "package2",
packageType: .monthly,
storeProduct: .init(sk1Product: MockSK1Product(mockProductIdentifier: "product2")),
offeringIdentifier: "offering"),
.init(identifier: "package3",
packageType: .annual,
storeProduct: .init(sk1Product: MockSK1Product(mockProductIdentifier: "product3")),
offeringIdentifier: "offering"),
.init(identifier: "package4",
packageType: .annual,
storeProduct: .init(sk1Product: MockSK1Product(mockProductIdentifier: "product4")),
offeringIdentifier: "offering"),
.init(identifier: "package5",
packageType: .annual,
storeProduct: .init(sk1Product: MockSK1Product(mockProductIdentifier: "product1")),
offeringIdentifier: "offering")
]

self.trialOrIntroPriceEligibilityChecker
.stubbedCheckTrialOrIntroPriceEligibilityFromOptimalStoreReceiveEligibilityResult = [
"product1": .init(eligibilityStatus: .eligible),
"product2": .init(eligibilityStatus: .noIntroOfferExists),
"product3": .init(eligibilityStatus: .ineligible)
]

let result = await self.purchases.checkTrialOrIntroDiscountEligibility(packages: packages)

expect(result) == [
packages[0]: .init(eligibilityStatus: .eligible),
packages[1]: .init(eligibilityStatus: .noIntroOfferExists),
packages[2]: .init(eligibilityStatus: .ineligible),
packages[3]: .init(eligibilityStatus: .unknown),
packages[4]: .init(eligibilityStatus: .eligible)
]
}

}

class PurchasesGetProductsBackgroundTests: BasePurchasesTests {
Expand Down

0 comments on commit 0cb5cb6

Please sign in to comment.