Skip to content
13 changes: 13 additions & 0 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,19 @@ public extension Purchases {
return await checkTrialOrIntroductoryDiscountEligibilityAsync(product)
}

@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:)
func checkTrialOrIntroDiscountEligibility(package: Package,
completion: @escaping (IntroEligibilityStatus) -> Void) {
trialOrIntroPriceEligibilityChecker.checkEligibility(
product: package.storeProduct,
completion: completion
)
}

func checkTrialOrIntroDiscountEligibility(package: Package) async -> IntroEligibilityStatus {
return await checkTrialOrIntroductoryDiscountEligibilityAsync(package.storeProduct)
}

#if os(iOS) || targetEnvironment(macCatalyst) || VISION_OS
@available(iOS 13.4, macCatalyst 13.4, *)
@objc func showPriceConsentIfNeeded() {
Expand Down
52 changes: 52 additions & 0 deletions Sources/Purchasing/Purchases/PurchasesType.swift

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not for this PR, but now that we're adding the singular package: overload to PurchasesType, I realized that the plural checkTrialOrIntroDiscountEligibility(packages:) still lives only on the concrete Purchases class and not in the protocol. I don't think there's a reason for that? Might be worth a follow-up to add it here for consistency (I'd be fine with sneaking it in with this PR though).

Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,58 @@ public protocol PurchasesType: AnyObject {
func checkTrialOrIntroDiscountEligibility(product: StoreProduct) async
-> IntroEligibilityStatus

/**
* Computes whether or not a user is eligible for the introductory pricing period of a given package.
* You should use this method to determine whether or not you show the user the normal product price or
* the introductory price. This also applies to trials (trials are considered a type of introductory pricing).
* [iOS Introductory Offers](https://docs.revenuecat.com/docs/ios-subscription-offers).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* [iOS Introductory Offers](https://docs.revenuecat.com/docs/ios-subscription-offers).
* [iOS Introductory Offers](https://docs.revenuecat.com/docs/ios-subscription-offers).

*
* - Note: If you're looking to use Promotional Offers instead,
* use ``Purchases/getPromotionalOffer(forProductDiscount:product:completion:)``.
*
* - Note: Subscription groups are automatically collected for determining eligibility. If RevenueCat can't
* definitively compute the eligibility, most likely because of missing group information, it will return
* ``IntroEligibilityStatus/unknown``. The best course of action on unknown status is to display the non-intro
* pricing, to not create a misleading situation. To avoid this, make sure you are testing with the latest
* version of iOS so that the subscription group can be collected by the SDK.
*
* - Parameter package: The ``Package`` for which you want to compute eligibility.
* - Parameter completion: A block that receives an ``IntroEligibilityStatus``.
*
* ### Related symbols
* - ``Purchases/checkTrialOrIntroDiscountEligibility(productIdentifiers:completion:)``
* - ``Purchases/checkTrialOrIntroDiscountEligibility(product:completion:)``
*/
Comment on lines +744 to +747

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my previous point, in "Related symbols" we could add the checkTrialOrIntroDiscountEligibility(packages:) method. Conversely, it'd be nice to add a back-reference to package: from the existing product:/productIdentifiers: doc comments

@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:)
func checkTrialOrIntroDiscountEligibility(
package: Package,
completion: @escaping (IntroEligibilityStatus) -> Void
)

/**
* Computes whether or not a user is eligible for the introductory pricing period of a given package.
* You should use this method to determine whether or not you show the user the normal product price or
* the introductory price. This also applies to trials (trials are considered a type of introductory pricing).
* [iOS Introductory Offers](https://docs.revenuecat.com/docs/ios-subscription-offers).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* [iOS Introductory Offers](https://docs.revenuecat.com/docs/ios-subscription-offers).
* [iOS Introductory Offers](https://docs.revenuecat.com/docs/ios-subscription-offers).

*
* - Note: If you're looking to use Promotional Offers instead,
* use ``Purchases/getPromotionalOffer(forProductDiscount:product:completion:)``.
*
* - Note: Subscription groups are automatically collected for determining eligibility. If RevenueCat can't
* definitively compute the eligibility, most likely because of missing group information, it will return
* ``IntroEligibilityStatus/unknown``. The best course of action on unknown status is to display the non-intro
* pricing, to not create a misleading situation. To avoid this, make sure you are testing with the latest
* version of iOS so that the subscription group can be collected by the SDK.
*
* - Parameter package: The ``Package`` for which you want to compute eligibility.
*
* ### Related symbols
* - ``Purchases/checkTrialOrIntroDiscountEligibility(productIdentifiers:)``
* - ``Purchases/checkTrialOrIntroDiscountEligibility(product:)``
*/
func checkTrialOrIntroDiscountEligibility(package: Package) async
-> IntroEligibilityStatus

/**
* Use this method to fetch ``PromotionalOffer``
* to use in ``Purchases/purchase(package:promotionalOffer:)``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,13 @@ private func checkAsyncMethods(purchases: Purchases) async {
let _: [String: IntroEligibility] = await purchases.checkTrialOrIntroDiscountEligibility(productIdentifiers: [""])
let _: [Package: IntroEligibility] = await purchases.checkTrialOrIntroDiscountEligibility(packages: [pack])
let _: IntroEligibilityStatus = await purchases.checkTrialOrIntroDiscountEligibility(product: stp)
let _: IntroEligibilityStatus = await purchases.checkTrialOrIntroDiscountEligibility(package: pack)
} catch {}
}

func checkNonAsyncMethods(_ purchases: Purchases) {
let storeProduct: StoreProduct! = nil
let package: Package! = nil

if #available(iOS 15.0, *) {
#if os(iOS)
Expand All @@ -283,6 +285,9 @@ func checkNonAsyncMethods(_ purchases: Purchases) {
purchases.checkTrialOrIntroDiscountEligibility(product: storeProduct) { introEligibilityStatus in
let _: IntroEligibilityStatus = introEligibilityStatus
}
purchases.checkTrialOrIntroDiscountEligibility(package: package) { introEligibilityStatus in
let _: IntroEligibilityStatus = introEligibilityStatus
}
}

private func checkConfigure() -> Purchases! {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ + (void)checkAPI {
[p syncPurchasesWithCompletion:^(RCCustomerInfo *i, NSError *e) {}];

[p checkTrialOrIntroDiscountEligibilityForProduct:storeProduct completion:^(RCIntroEligibilityStatus status) { }];
[p checkTrialOrIntroDiscountEligibilityForPackage:pack completion:^(RCIntroEligibilityStatus status) { }];
[p checkTrialOrIntroDiscountEligibility:@[@""] completion:^(NSDictionary<NSString *,RCIntroEligibility *> *d) { }];
[p getPromotionalOfferForProductDiscount:stpd
withProduct:storeProduct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ private func checkPurchasesPurchasingAPI(purchases: Purchases) {
purchases.checkTrialOrIntroDiscountEligibility(productIdentifiers: [String]()) { (_: [String: IntroEligibility]) in
}

purchases.checkTrialOrIntroDiscountEligibility(package: pack) { (_: IntroEligibilityStatus) in }

purchases.getPromotionalOffer(
forProductDiscount: discount,
product: storeProduct
Expand Down Expand Up @@ -314,6 +316,7 @@ private func checkAsyncMethods(purchases: Purchases) async {

do {
let _: IntroEligibilityStatus = await purchases.checkTrialOrIntroDiscountEligibility(product: stp)
let _: IntroEligibilityStatus = await purchases.checkTrialOrIntroDiscountEligibility(package: pack)
let _: [String: IntroEligibility] = await purchases.checkTrialOrIntroDiscountEligibility(
productIdentifiers: [String]()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,15 @@ class StoreKit1IntegrationTests: BaseStoreKitIntegrationTests {
expect(eligibility) == .eligible
}

func testEligibleForIntroBeforePurchaseForPackage() async throws {
try await self.verifyReceiptIsPresentBeforeEligibilityChecking()

let package = try await self.monthlyPackage

let eligibility = try await self.purchases.checkTrialOrIntroDiscountEligibility(package: package)
expect(eligibility) == .eligible
}

func testNoIntroOfferIfProductHasNoIntro() async throws {
let product = try await XCTAsyncUnwrap(await self.monthlyNoIntroProduct)

Expand Down
11 changes: 11 additions & 0 deletions Tests/UnitTests/Mocks/MockPurchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@ extension MockPurchases: PurchasesType {
self.unimplemented()
}

func checkTrialOrIntroDiscountEligibility(
package: Package,
completion: @escaping (IntroEligibilityStatus) -> Void
) {
self.unimplemented()
}

func checkTrialOrIntroDiscountEligibility(package: Package) async -> IntroEligibilityStatus {
self.unimplemented()
}

func getPromotionalOffer(
forProductDiscount discount: StoreProductDiscount,
product: StoreProduct,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,46 @@ class PurchasesGetProductsTests: BasePurchasesTests {
) == true
}

func testGetEligibilityForPackage() throws {
let package = Package(identifier: "package",
packageType: .monthly,
storeProduct: .init(sk1Product: MockSK1Product(mockProductIdentifier: "product1")),
offeringIdentifier: "offering",
webCheckoutUrl: nil)

self.trialOrIntroPriceEligibilityChecker
.stubbedCheckTrialOrIntroPriceEligibilityFromOptimalStoreReceiveEligibilityResult = [
"product1": .init(eligibilityStatus: .eligible)
]

let status = try XCTUnwrap(waitUntilValue { completed in
self.purchases.checkTrialOrIntroDiscountEligibility(package: package, completion: completed)
})

expect(status) == .eligible
expect(self.trialOrIntroPriceEligibilityChecker
.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParameters) == ["product1"]
}

func testGetEligibilityForPackageAsync() async {
let package = Package(identifier: "package",
packageType: .monthly,
storeProduct: .init(sk1Product: MockSK1Product(mockProductIdentifier: "product1")),
offeringIdentifier: "offering",
webCheckoutUrl: nil)

self.trialOrIntroPriceEligibilityChecker
.stubbedCheckTrialOrIntroPriceEligibilityFromOptimalStoreReceiveEligibilityResult = [
"product1": .init(eligibilityStatus: .ineligible)
]

let status = await self.purchases.checkTrialOrIntroDiscountEligibility(package: package)

expect(status) == .ineligible
expect(self.trialOrIntroPriceEligibilityChecker
.invokedCheckTrialOrIntroPriceEligibilityFromOptimalStoreParameters) == ["product1"]
}

func testGetEligibilityForPackages() async throws {
let packages: [Package] = [
.init(identifier: "package1",
Expand Down
4 changes: 4 additions & 0 deletions api/revenuecat-api-ios-simulator.swiftinterface
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,8 @@ extension RevenueCat.Purchases {
final public func checkTrialOrIntroDiscountEligibility(packages: [RevenueCat.Package]) async -> [RevenueCat.Package : RevenueCat.IntroEligibility]
@objc(checkTrialOrIntroDiscountEligibilityForProduct:completion:) final public func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc final public func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct) async -> RevenueCat.IntroEligibilityStatus
@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:) final public func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc final public func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package) async -> RevenueCat.IntroEligibilityStatus
@available(iOS 13.4, macCatalyst 13.4, *)
@objc final public func showPriceConsentIfNeeded()
@available(iOS 14.0, *)
Expand Down Expand Up @@ -2384,6 +2386,8 @@ extension RevenueCat.PurchasesAreCompletedBy : Swift.Codable {
@objc func checkTrialOrIntroDiscountEligibility(productIdentifiers: [Swift.String]) async -> [Swift.String : RevenueCat.IntroEligibility]
@objc(checkTrialOrIntroDiscountEligibilityForProduct:completion:) func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct) async -> RevenueCat.IntroEligibilityStatus
@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:) func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package) async -> RevenueCat.IntroEligibilityStatus
@objc(getPromotionalOfferForProductDiscount:withProduct:withCompletion:) func getPromotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.PromotionalOffer?, RevenueCat.PublicError?) -> Swift.Void)
@objc func promotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct) async throws -> RevenueCat.PromotionalOffer
@objc func eligiblePromotionalOffers(forProduct product: RevenueCat.StoreProduct) async -> [RevenueCat.PromotionalOffer]
Expand Down
4 changes: 4 additions & 0 deletions api/revenuecat-api-ios.swiftinterface
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,8 @@ extension RevenueCat.Purchases {
final public func checkTrialOrIntroDiscountEligibility(packages: [RevenueCat.Package]) async -> [RevenueCat.Package : RevenueCat.IntroEligibility]
@objc(checkTrialOrIntroDiscountEligibilityForProduct:completion:) final public func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc final public func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct) async -> RevenueCat.IntroEligibilityStatus
@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:) final public func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc final public func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package) async -> RevenueCat.IntroEligibilityStatus
@available(iOS 13.4, macCatalyst 13.4, *)
@objc final public func showPriceConsentIfNeeded()
@available(iOS 14.0, *)
Expand Down Expand Up @@ -2384,6 +2386,8 @@ extension RevenueCat.PurchasesAreCompletedBy : Swift.Codable {
@objc func checkTrialOrIntroDiscountEligibility(productIdentifiers: [Swift.String]) async -> [Swift.String : RevenueCat.IntroEligibility]
@objc(checkTrialOrIntroDiscountEligibilityForProduct:completion:) func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct) async -> RevenueCat.IntroEligibilityStatus
@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:) func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package) async -> RevenueCat.IntroEligibilityStatus
@objc(getPromotionalOfferForProductDiscount:withProduct:withCompletion:) func getPromotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.PromotionalOffer?, RevenueCat.PublicError?) -> Swift.Void)
@objc func promotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct) async throws -> RevenueCat.PromotionalOffer
@objc func eligiblePromotionalOffers(forProduct product: RevenueCat.StoreProduct) async -> [RevenueCat.PromotionalOffer]
Expand Down
4 changes: 4 additions & 0 deletions api/revenuecat-api-macos.swiftinterface
Original file line number Diff line number Diff line change
Expand Up @@ -2158,6 +2158,8 @@ extension RevenueCat.Purchases {
final public func checkTrialOrIntroDiscountEligibility(packages: [RevenueCat.Package]) async -> [RevenueCat.Package : RevenueCat.IntroEligibility]
@objc(checkTrialOrIntroDiscountEligibilityForProduct:completion:) final public func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc final public func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct) async -> RevenueCat.IntroEligibilityStatus
@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:) final public func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc final public func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package) async -> RevenueCat.IntroEligibilityStatus
@objc(getPromotionalOfferForProductDiscount:withProduct:withCompletion:) final public func getPromotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.PromotionalOffer?, RevenueCat.PublicError?) -> Swift.Void)
@objc final public func promotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct) async throws -> RevenueCat.PromotionalOffer
@objc final public func eligiblePromotionalOffers(forProduct product: RevenueCat.StoreProduct) async -> [RevenueCat.PromotionalOffer]
Expand Down Expand Up @@ -2323,6 +2325,8 @@ extension RevenueCat.PurchasesAreCompletedBy : Swift.Codable {
@objc func checkTrialOrIntroDiscountEligibility(productIdentifiers: [Swift.String]) async -> [Swift.String : RevenueCat.IntroEligibility]
@objc(checkTrialOrIntroDiscountEligibilityForProduct:completion:) func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc func checkTrialOrIntroDiscountEligibility(product: RevenueCat.StoreProduct) async -> RevenueCat.IntroEligibilityStatus
@objc(checkTrialOrIntroDiscountEligibilityForPackage:completion:) func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package, completion: @escaping (RevenueCat.IntroEligibilityStatus) -> Swift.Void)
@objc func checkTrialOrIntroDiscountEligibility(package: RevenueCat.Package) async -> RevenueCat.IntroEligibilityStatus
@objc(getPromotionalOfferForProductDiscount:withProduct:withCompletion:) func getPromotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct, completion: @escaping (RevenueCat.PromotionalOffer?, RevenueCat.PublicError?) -> Swift.Void)
@objc func promotionalOffer(forProductDiscount discount: RevenueCat.StoreProductDiscount, product: RevenueCat.StoreProduct) async throws -> RevenueCat.PromotionalOffer
@objc func eligiblePromotionalOffers(forProduct product: RevenueCat.StoreProduct) async -> [RevenueCat.PromotionalOffer]
Expand Down
Loading