Offering a subscription product in your application involves more than integrating with Apple's StoreKit framework. You need to set up subscription products in App Store Connect, keep track of the user's subscription status, and restrict access to the content or features that are exclusive to subscribers. Things get even more complicated if your application offers multiple tiers or is available on multiple platforms.

RevenueCat is a popular third party service that makes it straightforward to offer in-app purchases. It is much more than a wrapper around Apple's StoreKit framework, as you find out in this episode. RevenueCat supports multiple platforms, making it a perfect fit for companies or developers that have a multi-platform offering.

Last but not least, RevenueCat offers the ability to dynamically change your application's offerings, that is, without you having to submit an update to Apple or Google. This is ideal for price testing or special events, such as Black Friday, when you want to change your offering quickly and for a limited time.

In this episode, you learn how to integrate with RevenueCat, integrate the RevenueCat SDK into your project, enable purchases, and restrict access to features that are only available to customers with an active subscription. Even though this seems like a lot of work, the amount of code we need to write is surprisingly small thanks to the RevenueCat SDK. It does most of the heavy lifting as you find out later in this episode.

Starter Project

The starter project for this episode is Sleepy, an application that logs or tracks the user's sleep. The application displays three tabs. The first tab displays the user's sleep log, the second tab displays the user's sleep stats, and the third tab displays the user's subscription status and a button to restore purchases.

Sleepy

The goal for this episode is to implement a paywall that restricts access to the user's sleep stats. Users with an active subscription have access to their sleep stats. Users without a subscription or with a lapsed subscription don't have access to their sleep stats.

Subscriptions

This episode focuses primarily on the integration with RevenueCat and the RevenueCat SDK. This episode assumes you are familiar with the fundamentals of Swift and Apple development. I won't cover the basics in this episode.

RevenueCat makes it straightforward to set up in-app purchases, but remember that Apple is in charge of payments and subscriptions. If your application offers in-app purchases, then you need to carry out a few tasks in App Store Connect. I already completed some of these tasks, but I walk you through the steps you need to take to set yourself up for a smooth integration with RevenueCat.

Speaking of App Store Connect, let's take a look at the configuration of Sleepy in App Store Connect. I already added two subscription products to Sleepy. You can access the subscription products of an application by clicking Subscriptions in the Features section on the left. Sleepy defines one subscription group, Sleepy Plus. The subscription group defines two subscription products.

Setting Up Subscriptions in App Store Connect

The subscription products differ in their price and duration. The first subscription product has a monthly duration. The second subscription product has a yearly duration. We use the product identifier of the subscription products later in this episode to integrate with RevenueCat. I adopted the naming convention RevenueCat recommends, a short identifier for the application, the price of the subscription product, and the duration of the subscription product.

Setting Up Subscriptions in App Store Connect

Creating a RevenueCat Project

To integrate with RevenueCat, we first need to create a project. Sign in to your RevenueCat account, select Projects at the top, and click Create new project. Enter a project name and click Create Project.

Creating a RevenueCat Project

RevenueCat supports several platforms. For this episode, we are only interested in the Apple platform. Click App Store from the list of platforms. Enter your application's name and its bundle identifier.

We also need to provide RevenueCat with an app-specific shared secret to make sure RevenueCat can talk to Apple. This is easy to set up. Revisit App Store Connect, navigate to your application, and click App Information in the General section on the left. Scroll to App-Specific Shared Secret and click Manage. Click Generate to generate an app-specific shared secret. Copy the shared secret.

Generating an App-Specific Shared Secret in App Store Connect

Revisit RevenueCat, click Set Secret on the left of the App Store Connect App-Specific Shared Secret text field, and paste the shared secret you copied from App Store Connect.

Setting the App-Specific Shared Secret in RevenueCat

Click Save Changes in the top right to update the project's configuration.

Defining Products and Entitlements

RevenueCat doesn't automagically know which products you defined in App Store Connect. RevenueCat offers you two options to add the products you defined in App Store Connect to RevenueCat. It can import the products through the App Store Connect API or you can add the products manually. I want to show you the process step by step so we add the products manually in this episode. The process to add products is straightforward and only takes a few minutes.

Before we add the products, we define an entitlement. Let me explain what an entitlement is with an example. When the user purchases a subscription product, they receive the entitlement that is attached to the subscription product. Let's take Sleepy as an example. When the user purchases a Sleepy Plus subscription, they are entitled to view their sleep stats. To make that work, we need to define an entitlement in RevenueCat.

Click Entitlements in the Products and Pricing section on the left. Create an entitlement by clicking the + New button in the top right. For this example, we set the identifier of the entitlement to plus and the description to Sleepy Plus. The identifier is used in your project so choose an identifier that is short yet descriptive. Click Add to create the entitlement.

Adding an Entitlement in RevenueCat

Note that the entitlement isn't linked to any products. Let's fix that. Click Products in the Products and Pricing section. Create a product by clicking the + New button in the top right. Select the application you defined in App Store Connect and enter the identifier of one of your products. Click Create Product to create the product in RevenueCat.

Adding a Product in RevenueCat

We repeat these steps for any other products we defined in App Store Connect. For Sleepy, we add a product for the monthly subscription and a product for the yearly subscription. We end up with two products.

Adding a Product in RevenueCat

Revisit the entitlements section. Select the entitlement you created earlier and click the Attach button in the Associated Products section. Choose a product from the list of products and click the Add button to attach the product to the entitlement. Repeat these steps for any other products you defined in RevenueCat. For Sleepy, we end up with one entitlement that has two attached products, the monthly subscription and the yearly subscription.

Attaching Products to an Entitlement in RevenueCat

The next step is creating an offering. Even though offerings aren't strictly necessary, RevenueCat recommends using offerings as they make it easier to run experiments and change the products your application offers. It is a very neat feature.

Click Offerings in the Products and pricing section and click the + New button to create an offering. Give your offering an identifier and a description. As I mentioned earlier, make sure to choose an identifier that is descriptive to avoid confusion. I set the identifier of this offering to default as it is the default offering. Click the Save button to create the offering.

Adding an Offering in RevenueCat

Note that the offering has no packages. Packages are the last piece of the RevenueCat puzzle. As the name suggests, a package is a group or collection of products. The products in a package are equivalent. What does that mean? If we have a package for a monthly subscription, then we would add the monthly subscription products for Apple's and Google's platforms to the same package. Packages facilitate multi-platform support. Let me show you how this works in RevenueCat.

Click the default offering and add a package to the offering by clicking the + New button in the Packages section. The identifier of the package defines the duration of the package. You can choose Custom if you want to add a product that isn't a subscription, for example, a consumable or a non-consumable product. Provide a description for the package and click the Add button to create the package.

Adding a Package in RevenueCat

Click the package and click the Attach button in the Products section to attach a product to it. Choose a product from the list of products and click the Attach button in the top left.

Adding a Package in RevenueCat

For Sleepy, I follow these steps for the monthly package and the yearly package. We end up with an offering that has two packages. Each package has one product attached to it.

Adding a Package in RevenueCat

Setting up products, packages, offerings, and entitlements can be confusing at first, but once you grasp the purpose of each of these entities, you start to understand the possibilities and flexibility you receive in return. We successfully set up products, packages, offerings, and entitlements. It is time to integrate the RevenueCat SDK into the project.

Installing the RevenueCat SDK

There are several options to install the RevenueCat SDK. For this project, we opt for the Swift Package Manager. Select the project in the Project Navigator, select the project in the Project section, and open the Package Dependencies tab. Click the + button at the bottom of the list of packages.

Installing the RevenueCat SDK

Enter the URL of the package in the text field in the top right. Select the package, set Dependency Rule to Up to Next Major Version, and set Add to Project to your project. Click Add Package in the bottom right to add the package to your project.

Installing the RevenueCat SDK

We are only interested in the the RevenueCat package product. Check the RevenueCat checkbox and click Add Package in the bottom left.

Installing the RevenueCat SDK

The RevenueCat package is now listed under Package Dependencies in the Project Navigator on the left.

Installing the RevenueCat SDK

Initializing the RevenueCat SDK

Before we can use the RevenueCat SDK, we need to initialize it. RevenueCat's documentation lists two options. Let's stick with the simplest option for this episode. Open SleepyApp.swift and add an import statement for the RevenueCat SDK at the top.

import SwiftUI
import RevenueCat

@main
struct SleepyApp: App {

    // MARK: - Properties

    private let subscriptionProvider = SubscriptionManager()

    // MARK: - App

    var body: some Scene {
        WindowGroup {
            ContentView(
                viewModel: .init(
                    subscriptionProvider: subscriptionProvider
                )
            )
        }
    }

}

The next step is declaring an initializer for the SleepyApp struct. In the initializer, we invoke the static configure(withAPIKey:) method of the Purchases class. The static configure(withAPIKey:) method defines one parameter, an API key.

Revisit the RevenueCat dashboard. Navigate to the project you created earlier and click API Keys in the Project settings section. The API key we need to initialize the SDK is listed in the Public app-specific API keys section. Click Show key and copy the public app-specific API key.

Configuring the RevenueCat SDK

Pass the public app-specific API key to the static configure(withAPIKey:) method of the Purchases class.

import SwiftUI
import RevenueCat

@main
struct SleepyApp: App {

    // MARK: - Properties

    private let subscriptionProvider = SubscriptionManager()

    // MARK: - Initialization

    init() {
        // Initialize SDK
        Purchases.configure(withAPIKey: "PUBLIC_APP_SPECIFIC_API_KEY")
    }

    // MARK: - App

    var body: some Scene {
        WindowGroup {
            ContentView(
                viewModel: .init(
                    subscriptionProvider: subscriptionProvider
                )
            )
        }
    }

}

The Purchases class defines a static logLevel property. This property is set to debug for debug builds to facilitate debugging during development. It is set to info for release builds. You may want to set it to verbose during development to debug any issues you encounter while integrating the RevenueCat SDK into your project.

Build and run the application in the simulator to verify we successfully initialized the RevenueCat SDK. Because the log level is set to debug for debug builds, the RevenueCat SDK outputs a wealth of information to the console.

Displaying the Current Offering

The stats view displays the charts view if the user is subscribed and it displays the offerings view if the user isn't subscribed. The OfferingsViewModel class defines what the OfferingsView displays. Let's take a look at its implementation.

The OfferingsViewModel class declares a property with name packageViewModels of type [PackageViewModel]. The packageViewModels property defines the packages the OfferingsView displays. The start() method is invoked when the OfferingsView appears and in that method we ask the RevenueCat SDK for the current offering. The purchase(_:) method is invoked when the user taps the Buy button of a package. Don't worry if this sounds confusing. It will makes sense in a few moments.

We first add an import statement for the RevenueCat SDK at the top.

import Foundation
import RevenueCat

@MainActor
final class OfferingViewModel: ObservableObject {

	...

}

The next step is implementing the start() method. In the start() method, we ask the Purchases singleton for the offering we defined earlier in the RevenueCat dashboard by invoking the asynchronous offerings() method. Because that method is throwing, we wrap the method call in a do-catch statement. In the catch clause, we print the error to the console.

func start() async {
    do {
        let offerings = try await Purchases.shared.offerings()
    } catch {
        print("Unable to Fetch Offerings \(error)")
    }
}

The return value of the offerings() method is an Offerings instance. It encapsulates the offerings you define in the RevenueCat dashboard. To access the offering we defined earlier, we ask the Offerings instance for the current offering through the current computed property. We can access the packages included in the offering through the availablePackages property. Because the computed current property returns an optional offering, we fall back to an empty array if no offering is defined.

func start() async {
    do {
        let offerings = try await Purchases.shared.offerings()
        let packages = offerings.current?.availablePackages ?? []
    } catch {
        print("Unable to Fetch Offerings \(error)")
    }
}

The next step is converting the array of Package objects to an array of PackageViewModel objects. For that to work, we need to update the implementation of the PackageViewModel struct. Open PackageViewModel.swift and add an import statement for the RevenueCat SDK at the top.

import RevenueCat

struct PackageViewModel: Identifiable {

	...

}

Declare a property with name package of type Package.

import RevenueCat

struct PackageViewModel: Identifiable {

    // MARK: - Properties

    let package: Package

	...

}

The next step is updating the three computed properties of the PackageViewModel struct. In the body of the computed id property, we return the value of the identifier property of the Package instance.

var id: String {
    package.identifier
}

In the body of the computed price property, we access the localized price as a string through the storeProduct property of the Package instance.

var price: String {
    package.storeProduct.localizedPriceString
}

The implementation of the computed title property requires a few more lines. We first safely access the subscription period of the package through the storeProduct property of the Package instance. The subscriptionPeriod property is of an optional type because not every product is a subscription product. Consumable and non-consumable products, for example, don't have a subscription period. We return nil in the else clause of the guard statement.

var title: String? {
    guard let subscriptionPeriod = package.storeProduct.subscriptionPeriod else {
        return nil
    }
}

We switch on the value of the unit property of the subscription period. As the name suggests, the unit property defines the unit of the subscription period. We are only interested in the month and year cases. We return Monthly and Yearly respectively, returning nil in the default case.

var title: String? {
    guard let subscriptionPeriod = package.storeProduct.subscriptionPeriod else {
        return nil
    }

    switch subscriptionPeriod.unit {
    case .month:
        return "Monthly"
    case .year:
        return "Yearly"
    default:
        return nil
    }
}

With the implementation of the PackageViewModel struct in place, we can revisit the offering view model. In the start() method, we invoke the map(_:) method on the array of Package instances, passing in a reference to the initializer of the PackageViewModel struct. We assign the result to the packageViewModels property.

func start() async {
    do {
        let offerings = try await Purchases.shared.offerings()
        let packages = offerings.current?.availablePackages ?? []
        packageViewModels = packages.map(PackageViewModel.init(package:))
    } catch {
        print("Unable to Fetch Offerings \(error)")
    }
}

Build and run the application in the simulator to see the result. The user doesn't have a subscription so the stats view should display the packages we defined for the current offering in the RevenueCat dashboard.

Displaying the Current Offering

Making a Purchase

Making a purchase with the RevenueCat SDK couldn't be easier. Revisit OfferingViewModel.swift and navigate to the purchase(_:) method. The purchase(_:) method is synchronous, but the RevenueCat API to make a purchase is asynchronous so we create a Task in which we execute the asynchronous method call.

func purchase(_ viewmodel: PackageViewModel) {
    Task {
    	
    }
}

Making a purchase is a throwing method so we wrap the method call in a do-catch statement. In the catch clause, we print the error to the console.

func purchase(_ viewmodel: PackageViewModel) {
    Task {
        do {
        	
        } catch {
            print("Failed to Purchase Package \(error)")
        }
    }
}

Making a purchase requires one line of code. The view model invokes the purchase(_:) method on the Purchases singleton, passing in the package the user is about to purchase. We access the Package instance through the PackageViewModel object that is passed to the view model's purchase(package:) method. The purchase(package:) method returns the result of the purchase operation, but we ignore that for now.

func purchase(_ viewmodel: PackageViewModel) {
    Task {
        do {
            _ = try await Purchases.shared.purchase(package: viewmodel.package)
        } catch {
            print("Failed to Purchase Package \(error)")
        }
    }
}

Fetching Customer Info

As I mentioned earlier, RevenueCat is more than a wrapper around Apple's StoreKit framework. It is the source of truth for purchases. If a user purchased a subscription on another device, regardless of the platform, then RevenueCat is aware of that. What that means for you is that you don't need to create a backend that takes care of that. Let me illustrate this with Sleepy.

The user is able to purchase a subscription, but we haven't unlocked the stats view for users with an active subscription. You may think we need to store the user's subscription status locally, but that isn't something I recommend. Let RevenueCat take care of that. Like I said, it is the source of truth for purchases.

Open SubscriptionManager.swift. The SubscriptionManager class conforms to the SubscriptionProvider protocol, which means that it exposes a property with name isSubscribed of type Bool and a publisher with name subscribedPublisher. The publisher's Output type is Bool and its Failure type is Never.

import Combine
import Foundation

final class SubscriptionManager: NSObject, SubscriptionProvider {

    // MARK: - Properties

    @Published private(set) var isSubscribed = false

    var subscribedPublisher: AnyPublisher<Bool, Never> {
        $isSubscribed
            .eraseToAnyPublisher()
    }

}

How does the subscription manager know when the user's subscription status changed? That is where RevenueCat comes into play. Revisit SleepyApp.swift. The SleepyApp struct owns the subscription manager that is used throughout the application. You could say that the subscription manager is the source of truth for the user's subscription status in the application. We can notify the subscription manager when the user's subscription status changes by making it the delegate of the Purchases singleton.

import SwiftUI
import RevenueCat

@main
struct SleepyApp: App {

    // MARK: - Properties

    private let subscriptionProvider = SubscriptionManager()

	...

}

In the initializer of the SeepyApp struct, we make the subscription manager the delegate of the Purchases singleton. That is the first step.

// MARK: - Initialization

init() {
    // Initialize SDK
    Purchases.configure(withAPIKey: "PUBLIC_APP_SPECIFIC_API_KEY")

    // Configure Singleton
    Purchases.shared.delegate = subscriptionProvider
}

The compiler throws an error because the SubscriptionManager class doesn't conform to the PurchasesDelegate protocol. Let's fix that. Open SubscriptionManager.swift, add an import statement for the RevenueCat SDK, and conform the SubscriptionManager class to the PurchasesDelegate protocol.

import Combine
import Foundation
import RevenueCat

final class SubscriptionManager: NSObject, PurchasesDelegate, SubscriptionProvider {

	...

}

The methods of the PurchasesDelegate protocol are optional. The method we are interested in is the purchases(_:receivedUpdated:) method. The RevenueCat SDK invokes this method every time the customer info changed. This is convenient because it means you don't need to poll the RevenueCat API for customer info changes. The RevenueCat SDK does that for you.

import Combine
import Foundation
import RevenueCat

final class SubscriptionManager: NSObject, PurchasesDelegate, SubscriptionProvider {

    // MARK: - Properties

    @Published private(set) var isSubscribed = false

    var subscribedPublisher: AnyPublisher<Bool, Never> {
        $isSubscribed
            .eraseToAnyPublisher()
    }

    // MARK: - Purchases Delegate

    func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {

    }

}

Let's focus on the implementation of the purchases(_:receivedUpdated:) method. We are interested in the CustomerInfo instance, which encapsulates details about the customer and its entitlements. We use a guard statement to safely obtain a reference to the entitlement we defined in the RevenueCat dashboard, the plus entitlement. We access the customer's entitlements through the customer info's entitlements property. The return value is of type EntitlementInfos and provides access to a dictionary of entitlements through its all property. We use subscript syntax to obtain a reference to the plus entitlement. In the else clause we set isSubscribed to false and exit early from the purchases(_:receivedUpdated:) method.

// MARK: - Purchases Delegate

func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {
    guard let entitlement = customerInfo.entitlements.all["plus"] else {
    	isSubscribed = false
        return
    }
}

The remainder of the implementation is quite simple. We can ask an entitlement, an instance of the EntitlementInfo class, whether it is active through its isActive property. We use the isActive property to update the isSubscribed property of the SubscriptionManager class.

// MARK: - Purchases Delegate

func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {
    guard let entitlement = customerInfo.entitlements.all["plus"] else {
    	isSubscribed = false
        return
    }

    isSubscribed = entitlement.isActive
}

Displaying the Subscription Status

There is one more detail we need to take care of. The profile view displays the customer's subscription status. We can leverage the subscription manager to implement that detail. Open ProfileViewModel.swift. The ProfileViewModel class has a reference to the subscription provider. It defines a Published property with name subscriptionStatus of type String.

@Published private(set) var subscriptionStatus = SubscriptionStatus.notSubscribed.rawValue

To avoid string literals and code duplication, the ProfileViewModel class defines a private enum with name SubscriptionStatus. The raw values of the SubscriptionStatus enum are of type String. The enum defines two cases, subscribed and notSubscribed. I find an enum more convenient than a boolean as it is more flexible and extensible.

private enum SubscriptionStatus: String {

    // MARK: - Cases

    case subscribed = "Subscribed"
    case notSubscribed = "Not Subscribed"

}

The ProfileViewModel class defines a start() method. The view invokes this method when it is about to appear. In that method, the view model subscribes to the subscription provider's subscribedPublisher and updates its subscriptionStatus property every time the publisher emits an element. Let me show you how that works.

The view model accesses the subscription provider's subscribedPublisher and applies the map operator. The closure we pass to the map operator accepts a boolean as an argument and it returns a string. In the body of the closure, we use Swift's ternary operator to return the subscription status as a string by inspecting the value of the isSubscribed parameter.

In true Combine fashion, we invoke the assign(to:) method on the publisher the map operator returns to update the value of the subscriptionStatus property every time the subscription provider's subscribedPublisher emits an element. That's it.

// MARK: - Public API

func start() {
    subscriptionProvider.subscribedPublisher
        .map { isSubscribed -> String in
            isSubscribed
                ? SubscriptionStatus.subscribed.rawValue
                : SubscriptionStatus.notSubscribed.rawValue
        }
        .assign(to: &$subscriptionStatus)
}

Restoring Purchases

Apple requires that an application offers the user the ability to restore their purchases. That makes sense. It is possible a user purchases a subscription, deletes the application, and reinstalls the application a few weeks or months later. The user should still have access to the content or features it unlocked through its subscription.

As you may have guessed, the RevenueCat SDK makes this almost trivial. Open ProfileView.swift. The ProfileView displays a button that allows the user to restore their purchases. It invokes the view model's restorePurchases() method. Revisit ProfileViewModel.swift and add an import statement for the RevenueCat SDK.

Navigate to the restorePurchases() method. To restore the user's purchases, the view model invokes the restorePurchases() method on the Purchases singleton. Because the restorePurchases() method is asynchronous, we first create a Task. The restorePurchases() method is throwing so we wrap the method call in a do-catch statement. In the catch clause, we print the error to the console. In the do clause, we invoke the restorePurchases() method on the Purchases singleton.

func restorePurchases() {
    Task {
        do {
            _ = try await Purchases.shared.restorePurchases()
        } catch {
            print("Unable to Restore Purchases \(error)")
        }
    }
}

I very much like how consistent and intuitive the API of the RevenueCat SDK is. Remember that the purchase(package:) method of the Purchases singleton returns a CustomerInfo instance. The restorePurchases() method also returns a CustomerInfo instance. We are not interested in the CustomerInfo instance because the subscription provider is notified by the RevenueCat SDK through the PurchasesDelegate protocol. It should just work.

Testing In-App Purchases

It is time to test the implementation. We have two options, (1) we can use a physical device and a sandbox tester or (2) we can use a simulator and a StoreKit configuration file. RevenueCat supports both options. In this episode, we choose for the latter option. How that works is documented in detail on RevenueCat's website.

Let's take a look at the three pieces of the puzzle. We first create a StoreKit configuration file. Add a Swift file to the Sleepy group by choosing the StoreKit Configuration File template from the iOS > Other section. Name the StoreKit configuration Sleepy and check Sync this file with an app in App Store Connect. Select your team and the application you created in App Store Connect.

If you don't have an application in App Store Connect, then you need to manually add the products to the StoreKit configuration file. You can test in-app purchases even if you haven't defined any products in App Store Connect. That is one of the niceties of using a StoreKit configuration file. It is important to note, however, that you need to add the products to RevenueCat if you want to test the integration with RevenueCat.

Adding a StoreKit Configuration File in Xcode

The receipts that are generated using a StoreKit configuration file are signed with a different app-specific certificate that you need to upload to RevenueCat. We export the certificate by selecting the StoreKit configuration file and choosing Save Public Certificate from Xcode's Editor menu. Revisit your application in the RevenueCat dashboard, scroll to the StoreKit testing framework section, and upload the public certificate.

Uploading the Public Certificate to RevenueCat

The last step is adding a scheme for testing in-app purchases. Click the Sleepy scheme and choose Manage Schemes... from the menu. Select the Sleepy scheme, click the button on the right of the - button, and choose Duplicate. Name the scheme Sleepy IAP Tests.

Select Run on the left and open the Options tab at the top. Set StoreKit Configuration to Sleepy.storekit, the StoreKit configuration file we created earlier. Click Close to exit the scheme editor.

Creating a Scheme for Testing In-App Purchases

Select the Sleepy IAP Tests scheme and run the application in a simulator. Navigate to the stats view and tap the Buy button of one of the packages. Tap Subscribe at the bottom to confirm your purchase. After successfully completing the purchase, you should see the charts view. Navigate to the profile view to verify that the user is subscribed to Sleepy Plus.

Enhancements, Bells, and Whistles

In this episode, we integrated with RevenueCat to enable in-app purchases in Sleepy. Note that we kept the implementation basic. There is room for improvement. For example, we need to properly handle errors in the purchase flow and it is advisable to show a progress view of some kind when the user purchases a product or when they restore their purchases. These are more than nice-to-have features in my view as a purchase flow is a critical component of an application.

What's Next?

Even though this episode is sponsored by RevenueCat, I hope you agree with me that RevenueCat's offering is compelling. Not only does it hide the StoreKit framework from your project, it drastically simplifies the steps you need to take to enable in-app purchases. And, as I mentioned earlier, RevenueCat does more than providing an elegant API around the StoreKit framework. It provides the infrastructure that is needed to support a multi-platform offering. Changing the offering on the fly is as simple as making a few changes in RevenueCat's dashboard. For some applications, there is no longer a need for a custom backend. If your application offers in-app purchases, then I recommend taking a look at RevenueCat before building a custom solution.