Meet the New RevenueCat SDK
Our biggest release to date!
We’re excited to announce that we’ve just released v4 of our iOS SDK! This is a huge release, with a handful of updates and improvements for you to take note of. So what’s new with us? Let’s dive in.
Swift Migration
For starters, we migrated our entire codebase from Objective-C to Swift. You can read more about the motivation and process for that transition here. This migration allows us to provide StoreKit 2 features and Swift Concurrency support, as well as iterate more quickly to continue improving the SDK in the future.
Objective-C programmers, worry not! Even though our codebase is now in Swift, it’s still 100% compatible with Objective-C. We ❤️ Objective-C, and we have no plans to stop supporting it.
Async/await API alternatives
We added async/await alternatives to all our public APIs. These are entirely optional, but you can use them to simplify your code quite a bit.
For example, fetching Offerings with completion blocks forces you to have a nested block of code to process the response. Since this API is compatible with Objective-C, it returns both an Offerings object and an Error object, and you have to handle nullability of the Offerings result and the Error:
1Purchases.shared.getOfferings { offerings, error in
2 guard error == nil else {
3 print("Failed to get offerings! Error: \(error!.localizedDescription)")
4 return
5 }
6
7 guard let offerings = offerings,
8 print("Didn't get an error but didn't get offerings!")
9 return
10 }
11
12 // do something with offerings
13 setUpPaywall(with: offerings.current)
14}
This is a lot simpler with async/await:
1do {
2 let offerings = try await Purchases.shared.offerings()
3 // do something with offerings
4 self.setUpPaywall(with: offerings.current)
5} catch {
6 print("Operation failed! Error: \(error.localizedDescription)")
7}
With this new version, we no longer have to worry about nullability — the Offerings object is guaranteed to be non-nil if there isn’t an error.
Even better, we don’t even need to consider the case where Offerings and Error are nil at the same time. While the SDK would never send this value, the compiler still forces us to handle that case in the completion-blocks syntax.
The difference is even more significant with more complex use cases like Subscription Offers. In v3, you’d have to do something like this:
1Purchases.shared.getOfferings { offerings, error in
2 guard error == nil else {
3 print("Failed to get offerings! Error: \(error!.localizedDescription)")
4 return
5 }
6
7 guard let offerings = offerings else {
8 print("Didn't get an error but didn't get offerings!")
9 return
10 }
11
12 guard let currentOffering = offerings.current,
13 let monthlyPackage = currentOffering.monthly else {
14 return
15 }
16
17 if let discount = monthlyPackage.storeProduct.discounts.first {
18 Purchases.shared.getPromotionalOffer(forProductDiscount: discount,
19 product: monthlyPackage.storeProduct) { (promoOffer, error) in
20 if let promoOffer = promoOffer {
21 // Promotional Offer validated, show terms of your offer to your customers
22 self.show(promoOffer: promoOffer)
23 } else {
24 // Promotional Offer was not validated, default to normal package terms
25 }
26 }
27 }
28}
Using v4’s async/await syntax, this becomes much simpler:
1do {
2 let offerings = try await Purchases.shared.offerings()
3 guard let currentOffering = offerings.current,
4 let monthlyPackage = currentOffering.monthly else {
5 return
6 }
7 if let discount = monthlyPackage.storeProduct.discounts.first {
8 let promoOffer = try await Purchases.shared.getPromotionalOffer(forProductDiscount: discount,
9 product: monthlyPackage.storeProduct)
10 self.show(promoOffer: promoOffer)
11 }
12
13} catch {
14 print("Operation failed! Error: \(error.localizedDescription)")
15}
We also introduced an AsyncStream for CustomerInfo updates. Instead of using the PurchasesDelegate to get updated PurchaserInfo, you can get notified whenever there are updates to the CustomerInfo by doing:
1for await customerInfo in Purchases.shared.customerInfoStream {
2 // do something with customerInfo
3}
Cleaned-up APIs
StoreKit Wrappers
One of our main goals with v4 was to allow customers to use StoreKit 2 under the hood whenever it would be useful. To do this, we added wrappers around StoreKit’s types so our APIs would be agnostic of which StoreKit version is in use.
A few examples of this are: –StoreProduct
, which wraps SKProduct
and StoreKit.Product
–StoreTransaction
, which wraps SKPaymentTransaction
and StoreKit.Transaction
–StoreProductDiscount
, which wraps SKProductDiscount
and StoreKit.Product.SubscriptionOffer
These wrappers also include some convenience methods. For example, StoreProduct includes a convenience method called getEligiblePromotionalOffers()
.
This can be used to simplify our async/await Subscription Offers example even further:
1do {
2 let offerings = try await Purchases.shared.offerings()
3 guard let currentOffering = offerings.current,
4 let monthlyPackage = currentOffering.monthly else {
5 return
6 }
7 let promoOffers = await monthlyPackage.storeProduct.getEligiblePromotionalOffers()
8 self.show(promoOffer: promoOffers.first)
9} catch {
10 print("Operation failed! Error: \(error.localizedDescription)")
11}
With these wrappers in place, the API is fully agnostic of the StoreKit version in use, and the SDK can seamlessly switch implementations to use the one that is most reliable for each feature.
Consistency and clarity
We updated some names in our SDK to improve clarity in places where we often saw confusion from customers.For example, v3 had two methods called restoreTransactions
and syncPurchases
. In v4, we changed these names to restorePurchases
and syncPurchases
to make them more consistent.We also added more context into our docstrings to make it easier to understand how to use methods without leaving Xcode. One example is the updated docstring for logIn
(v3 version, v4 version).
Customer support APIs
We introduced two new APIs that take advantage of StoreKit 2’s new features: showManageSuscriptions
allows you to open up the subscription management page for the user. Unlike StoreKit’s native method, though, showManageSuscriptions
is smart enough to determine whether the subscription was purchased through the App Store, Google Play, Stripe, or Amazon Store and open up the appropriate management page.beginRefundRequest
allows you to provide a way for your users to start a refund request right from the app.
StoreKit 2 support
V4 of our SDK automatically uses StoreKit 2 under the hood for a few features, like checking introductory price eligibility. This only happens if the device supports StoreKit 2 — otherwise, the SDK automatically defaults to StoreKit 1.In addition, you can opt into using StoreKit 2 for all features, including actually making purchases. This is currently in beta, and you can enable it for your app by passing in the useStoreKit2IfAvailable
flag when configuring the SDK.
New docs page
We updated our reference docs page using DocC. This allows us to have cleaner docs, helps us keep our links up to date, and gives us exciting new docs features. More on this in a future blog post!
With a grand total of 528 PRs, 1320 changed files, 47465 additions, 25528 deletions, and 10 contributors, this is the biggest SDK release we’ve ever done. 🎉
We put an inordinate amount of work into testing every step of the way, and we received a ton of help from our incredible developer community.
V4 of our SDK is already in production in numerous apps, big and small. Try it out!
You might also like
- Blog post
Customer Center: Automate in-app subscription support
Give your customers control: manage subscriptions, prevent churn, and collect feedback.
- Blog post
We bought a spicy audiobook app
Thinking outside the box to help developers make more money
- Blog post
2024 Ship-a-ton Winners
Announcing the winners of the first RevenueCat Ship-a-ton.