Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI Preview Mode to SDK #4693

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Sources/Misc/DangerousSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ import Foundation
*/
@objc public let autoSyncPurchases: Bool

/**
* if `true`, the SDK will return a set of mock products instead of the
* products obtained from StoreKit. This is useful for testing or preview purposes.
*/
@objc public let uiPreviewMode: Bool

/**
* A property meant for apps that do their own entitlements computation, separated from RevenueCat.
* It:
Expand Down Expand Up @@ -101,13 +107,24 @@ import Foundation

}

/// Designated initializer
/**
* Only use a Dangerous Setting if suggested by RevenueCat support team.
*
* - Parameter uiPreviewMode: if `true`, the SDK will return a set of mock products instead of the
* products obtained from StoreKit. This is useful for testing or preview purposes.
*/
public convenience init(uiPreviewMode: Bool) {
self.init(autoSyncPurchases: false, internalSettings: Internal.default, uiPreviewMode: uiPreviewMode)
}

internal init(autoSyncPurchases: Bool,
customEntitlementComputation: Bool = false,
internalSettings: InternalDangerousSettingsType) {
internalSettings: InternalDangerousSettingsType,
uiPreviewMode: Bool = false) {
self.autoSyncPurchases = autoSyncPurchases
self.internalSettings = internalSettings
self.customEntitlementComputation = customEntitlementComputation
self.uiPreviewMode = uiPreviewMode
}

}
Expand Down
55 changes: 54 additions & 1 deletion Sources/Purchasing/OfferingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ private extension OfferingsManager {
)
}

// swiftlint:disable:next cyclomatic_complexity function_body_length
func createOfferings(
from response: OfferingsResponse,
fetchPolicy: FetchPolicy,
Expand All @@ -192,7 +193,7 @@ private extension OfferingsManager {
return
}

self.productsManager.products(withIdentifiers: productIdentifiers) { result in
self.fetchProducts(withIdentifiers: productIdentifiers) { result in
let products = result.value ?? []

guard products.isEmpty == false else {
Expand Down Expand Up @@ -275,6 +276,58 @@ private extension OfferingsManager {
}
}

private func fetchProducts(withIdentifiers identifiers: Set<String>, completion: @escaping ProductsManagerType.Completion) {
if self.systemInfo.dangerousSettings.uiPreviewMode {
let mockProducts = self.createMockProducts(productIdentifiers: identifiers)
completion(.success(mockProducts))
} else {
self.productsManager.products(withIdentifiers: identifiers, completion: completion)
}
}

private func createMockProducts(productIdentifiers: Set<String>) -> Set<StoreProduct> {
let productTypes = [
(type: "weekly", price: 1.99, period: SubscriptionPeriod(value: 1, unit: .week)),
(type: "monthly", price: 5.99, period: SubscriptionPeriod(value: 1, unit: .month)),
(type: "yearly", price: 59.99, period: SubscriptionPeriod(value: 1, unit: .year)),
(type: "lifetime", price: 199.99, period: nil)
]

let products = productIdentifiers.enumerated().map { index, identifier -> StoreProduct in
let productType = productTypes[index % productTypes.count]
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we could act a bit smarter here and instead of assigning basically a random period to each product, we could try to get the package type provided by the offerings (which has the duration). In case the package type is custom, we could then do something "smart" like analyzing the product id for some keywords like week, month, year.... To assign the duration. And then if we don't find any, as a fallback, assign a random period.


let introductoryDiscount: TestStoreProductDiscount? = {
guard productType.period != nil && Bool.random() else { return nil }
Copy link
Member

Choose a reason for hiding this comment

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

I like the idea, but the randomness could add friction if you want to see a discount and all random()s happen to be false. (You'll need to go back and reopen the preview / the app.)

What do you think about having a fixed (set of) product type(s) always having a discount, e.g. monthly and yearly?

Copy link
Member Author

Choose a reason for hiding this comment

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

I totally see your point about the friction due to the randomness. I also believe that what we show in previews should be more deterministic.
On the other hand, I do imagine some clients wanting to show the exact information of their products on the previews. However, the introduced randomness does not solve this problem either.
WDYT @aboedo?

Copy link
Member

Choose a reason for hiding this comment

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

I do imagine some clients wanting to show the exact information of their products on the previews.

Just for some context: this is something I believe we will do at some point. We could on top of that allow users to configure the preview before/when showing it, e.g. with a setting that controls discounts. However these are not solutions for in this PR haha.

This is not a blocker for me btw!

Copy link
Member

@aboedo aboedo Jan 22, 2025

Choose a reason for hiding this comment

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

Makes sense, I can imagine the frustration and the confusion if the paywall looks different between opens.

Agreed with Joop that eventually we'll want to get realistic data in there, but in the meantime probably the easiest way to go would be to have a way to tell the SDK what to show.

That way we could do something like what the dashboard does, where it allows you to select whether to preview with intro offer eligible or not.

Probably not something that needs to go into the MVP of this feature and can be added on later before shipping to prod.

And it will set us up nicely for doing something similar for Customer Center, where we'll inevitably need to find a way to select whether to show the CC as a user with an active subscription would see it vs empty subs

return TestStoreProductDiscount(
identifier: "intro",
price: 0,
localizedPriceString: "$0.00",
paymentMode: .freeTrial,
subscriptionPeriod: SubscriptionPeriod(value: 1, unit: .week),
numberOfPeriods: 1,
type: .introductory
)
}()

let testProduct = TestStoreProduct(
localizedTitle: "PRO \(productType.type)",
price: Decimal(productType.price),
localizedPriceString: String(format: "$%.2f", productType.price),
productIdentifier: identifier,
productType: productType.period == nil ? .nonConsumable : .autoRenewableSubscription,
localizedDescription: "\(productType.type) subscription",
subscriptionGroupIdentifier: productType.period == nil ? nil : "group",
subscriptionPeriod: productType.period,
introductoryDiscount: introductoryDiscount,
discounts: []
)

return testProduct.toStoreProduct()
}

return Set(products)
}

}

extension OfferingsManager {
Expand Down