-
Notifications
You must be signed in to change notification settings - Fork 3
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
[DO NOT MERGE] Support Redeeming iOS Win-Back Offers on Developer Paywalls #291
base: main
Are you sure you want to change the base?
Conversation
@@ -446,4 +605,12 @@ public actual class Purchases private constructor(private val iosPurchases: IosP | |||
|
|||
public actual fun setCreative(creative: String?): Unit = | |||
iosPurchases.setCreative(creative) | |||
|
|||
private fun isIOSVersion18OrAbove(): Boolean { | |||
return currentOSVersion() > "18.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's necessary for us to check the OS version here. Despite the native functions being marked as available on iOS 18.0+, macOS 15.0+, tvOS 18.0+, watchOS 11.0+, and visionOS 2.0+, KMP allows us to call these functions regardless of the OS versions 🤠
Other hybrids skirt around this issue in two ways:
- They perform an availability check in their Obj-C/Swift wrappers
- PHC also performs an availability check
Since we're calling the native functions here directly from Kotlin, we can't take advantage of those mechanisms. This technically works on iOS, but it's not great for a few reasons:
- It depends on the
UIDevice
class, which isn't available in all OS's - It's only checking for the iOS version, ignoring the OS requirements for other OS's.
Do you happen to know of a better way to handle this @JayShortway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of now, our KMP SDK only supports iOS. (See our configured compilation targets here.) So I think that avoids those downsides?
(I would like the KMP SDK to support more Apple targets in the future. When we do, we can use expect
/actual
to make sure this works on all Apple targets. There's #126 to track it.)
The comparison might be a bit brittle, as it's comparing strings, but I think it should work (Unlikely, but if currentOSVersion
ever prefixes a 0, it won't work anymore.)
Although shouldn't it be this?
return currentOSVersion() > "18.0.0" | |
return currentOSVersion() >= "18.0.0" |
@@ -0,0 +1,164 @@ | |||
{ | |||
"appPolicies" : { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding the SKConfig file allows us to test win-back offers on products :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice! Adding this to the tester app is amazing! Just had some minor comments.
onError = { error -> }, | ||
onSuccess = { products -> | ||
val product = products.first() | ||
|
||
// Fetch the eligible win-back offers for the product | ||
Purchases.sharedInstance.getEligibleWinBackOffersForProduct( | ||
storeProduct = product, | ||
onError = { error -> }, | ||
onSuccess = { eligibleWinBackOffers -> | ||
val winBackOffer = eligibleWinBackOffers.first() | ||
|
||
// Purchase the product with the win-back offer | ||
Purchases.sharedInstance.purchase( | ||
storeProduct = product, | ||
winBackOffer = winBackOffer, | ||
onError = { error -> }, | ||
onSuccess = { transaction, customerInfo -> } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other SDKs we should define the types here instead of relying on type inference, as type inference won't catch it if we accidentally change the type. It is not strictly necessary in KMP because it uses the binary-compatibility-validator to enforce this. But maybe it's a good idea to do it for consistency?
I'm talking about doing things like this:
- onError = { error -> },
- onSuccess = { products ->
- val product = products.first()
+ onError = { error: PurchasesError -> },
+ onSuccess = { products: List<StoreProduct> ->
+ val product: StoreProduct = products.first()
// etc.
This would also apply to checkRedeemingWinBackOffersForPackage()
below.
@@ -67,6 +69,8 @@ fun App() { | |||
) | |||
} | |||
} | |||
|
|||
is Screen.WinBackTesting -> WinBackTestingScreen() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this!
@@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.onStart | |||
@Composable | |||
fun MainScreen( | |||
onShowPaywallClick: (offering: Offering?, footer: Boolean) -> Unit, | |||
setScreen: (Screen) -> Unit, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming suggestion: navigateTo
.
@@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.onStart | |||
@Composable | |||
fun MainScreen( | |||
onShowPaywallClick: (offering: Offering?, footer: Boolean) -> Unit, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to remove this one and just use setScreen
/navigateTo
for the paywall too. I think that should be possible?
Text("Use this screen to fetch eligible win-back offers, purchase products without a win-back offer, and purchase products with an eligible win-back offer.") | ||
|
||
Text("This test relies on products and offers defined in the SKConfig file, so be sure to launch the PurchaseTester app from Xcode with the SKConfig file configured.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! I love a tester app that explains on-screen how it should be used 😗👌
@@ -446,4 +605,12 @@ public actual class Purchases private constructor(private val iosPurchases: IosP | |||
|
|||
public actual fun setCreative(creative: String?): Unit = | |||
iosPurchases.setCreative(creative) | |||
|
|||
private fun isIOSVersion18OrAbove(): Boolean { | |||
return currentOSVersion() > "18.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of now, our KMP SDK only supports iOS. (See our configured compilation targets here.) So I think that avoids those downsides?
(I would like the KMP SDK to support more Apple targets in the future. When we do, we can use expect
/actual
to make sure this works on all Apple targets. There's #126 to track it.)
The comparison might be a bit brittle, as it's comparing strings, but I think it should work (Unlikely, but if currentOSVersion
ever prefixes a 0, it won't work anymore.)
Although shouldn't it be this?
return currentOSVersion() > "18.0.0" | |
return currentOSVersion() >= "18.0.0" |
Motivation
This PR makes it possible for developers to fetch win-backs that a subscriber is eligible for for a given product or package, and then purchase them from their custom paywalls.
PHC Dependency
This PR depends on RevenueCat/purchases-hybrid-common#1008, and should not be merged until that PR has been deployed and the new PHC version adopted in the KMP SDK. This branch will not build until that change has been taken in. Please review that PHC PR when reviewing this PR.
Description
New Interfaces
This PR introduces a new
WinBackOffer
interface, which contains aStoreDiscount
:New Public Functions
It also introduces four new functions, each of which are only implemented on iOS:
The idea here is that a developer will first fetch the eligible win-back offers for a product/package with one of the
getEligibleWinBackOffers
functions, and display those offers on their paywall, and then redeem it with one of the newpurchase
functions. This mirrors the API design in the other hybrids and the native iOS SDK.New
DiscountType
CasesAdds a new
WINBACK
case to theDiscountType
enumPurchase Tester Additions
This PR adds a new screen to test the various win-back functions, accessible from a new button on the main screen.