Converting a Paid App to In-App Subscriptions

Your guide to making the switch

Converting a Paid App to Subscriptions
Cody Kerns

Cody Kerns

PublishedLast updated

In the early days of the App Store, the choice between making your app free versus paid was simple. If you wanted to earn revenue for your product and didn’t want to display ads, you listed your app as paid.

Many priced their apps as low as 99¢, an LTV of just 70¢ after Apple’s cut. At that price, you’d have to sell over 100,000 copies each year to earn the equivalent of the starting salary of a junior iOS developer!

Unfortunately, many studios and independent developers closed up shop because sales just weren’t high enough to make the 99¢ price point work — especially given how hard it was to generate additional revenue after that initial purchase.

For most app developers, it just isn’t sustainable to provide years of updates and improvements to an app while only charging a one-time fee of 99¢, or even $3-5. Enter: subscriptions!

Why Switch to Subscriptions?

Subscriptions are a great way for developers to generate continuous revenue for as long as a customer uses an app. This ensures you receive steady financial support even if a customer re-downloads your app years after originally getting it.

The fact is, if your product is valuable, your customers want to see it thrive. David Barnard, RevenueCat’s Developer Advocate, writes about this at length in his article “Subscribers Are Your True Fans“. He comments:

Everyone has a handful of things they value, or a handful of tasks that are mission-critical to their lives or jobs. People aren’t tired of subscriptions, at least not any more than they are tired of paying rent, the cell phone bill, a gym membership, etc.

The premise is simple: if you need to generate revenue consistently over time, sell your product as a service. The customers who value your product will support you by subscribing.

Why Use RevenueCat?

If you’re ready to add subscription logic to your app, RevenueCat makes it easy. Most developers who use RevenueCat say it takes less than an hour to implement the SDK and get subscriptions up and running. RevenueCat has everything you need to convert your paid app to a subscription app — without the headache of supporting your own backend, sifting through complicated receipt files, and responding to App Store changes on the fly.

Not only does RevenueCat handle subscription logic for thousands of apps, it’s also home to some of the most knowledgeable in-app purchase engineers on the planet. Our team works around the clock — literally! We have engineers all over the world, continuously updating and improving our SDKs and infrastructure. 

How to Keep Current Users Happy

It’s important to think about how you’ll handle existing customers who have previously paid for your app. You don’t want to force everyone to purchase a subscription if you’ve already been offering a paid version. For example, if someone purchases your app one day before it becomes free with a subscription, you don’t want to charge them again!

That being said, you may build some features that are provided only for new subscription users, and only allow previously paid purchases to access the “old” premium features.

This post will walk you through one method of converting your paid iOS app to free with subscriptions. We’ll talk about entitlements and how they work and see how to grant entitlement access client-side for iOS apps using the RevenueCat SDK.

Making the Switch

RevenueCat’s Purchases SDK is dead-simple to install and integrate into new and existing apps. If you haven’t already, you’ll need to register your app in RevenueCat before installing and initializing the SDK.

You can install the SDK by adding Purchases to your .podfile, or by adding the GitHub repo as a Swift package in Xcode. If you need to manually install the SDK, you can do that too. For more information about installing the SDK, check out our installation docs.

Setting Up Entitlements

An entitlement represents access to certain features and can be either active or inactive. In our case, we’ll be switching a paid, premium app to free with subscription, so existing paid features will fall under a single entitlement that we will call “premium”. For more information about setting up entitlements, check out the full guide in our docs.

Entitlements become “active” when a purchase is made for a product that is attached to that entitlement. For a subscription product, the entitlement will be active for the duration of the subscription. If a subscription expires or is cancelled, the entitlement is deactivated at the end of the paid term.

We’re going to use the premium entitlement when we convert our paid app to free so we can continue providing premium features to paid customers.

Parsing Your App Receipt

Now that you have your app registered in RevenueCat, the SDK is installed, and you’ve created an entitlement, it’s time to dig into device receipts.

Apple uses a binary receipt file to store data for purchases related to apps. These receipt files are complicated to sift through, but contain necessary information about the app download and purchase.

Each receipt contains the original_application_version parameter. On iOS, this corresponds to the build number of the version that the user first downloaded. This is the main way to determine a user’s entitlement access for paid versions. If a user’s original_application_version is that of a paid version, you should grant paid access to a particular feature or entitlement.

Not only does the receipt need to be parsed, it also has to be validated. Client-side validation is challenging — you can’t ensure the integrity of a receipt because jailbroken devices can spoof their on-device receipt.

Going into detail about how to parse and validate this receipt is a rabbit hole that is way outside the scope of this post. If you’re interested in doing this manually, you’ll need to validate receipts on your own server and securely provide the data back to your app.

But we don’t have to do any of that today — we’re using RevenueCat!

Save Time, Let RevenueCat Do It for You!

RevenueCat automatically validates receipts server-side and syncs with the RevenueCat SDK to ensure all of your users have access to the entitlements they deserve.

Even better, the SDK lets you get that original_application_version — without the hassle of dealing with receipts!

After you sync transactions with RevenueCat, your user’s CustomerInfo will include the property originalApplicationVersion. Voilà!

To view that data, just request the user’s CustomerInfo:

import RevenueCat

Purchases.shared.getCustomerInfo { (info, error) in

   // access latest CustomerInfo

   let version = info?.originalApplicationVersion

}

Keep in mind that this value from CustomerInfo could potentially be nil if transactions haven’t been restored or the receipt is not available on your device. In that case, you’ll want to restrict access to premium features until you verify a user’s originalApplicationVersion is valid.

Building an Entitlement Access Manager

So, now you have access to a user’s originalApplicationVersion to check if they have grandfathered access to premium features. Additionally, you’ll need to use the Purchases SDK to check if a user has an entitlement active in RevenueCat, as new users may have purchased a subscription.

To make this simpler, let’s create a new Swift file in Xcode called “CompatibilityAccessManager.swift” that will be our new source of truth for entitlement access:

Add the following code as a base for our new class:

import Foundation
import RevenueCat

class CompatibilityAccessManager {
	static let shared = CompatibilityAccessManager()
}

CompatibilityAccessManager will be accessible from a single instance — CompatibilityAccessManager.shared — where we can check entitlement access.

To handle which originalApplicationVersions should grant access to an entitlement, we’ll use a registration system. Create a new struct in the CompatibilityAccessManager file called “BackwardsCompatibilityEntitlement” that contains properties for an array of versions as well as an entitlement string:

struct BackwardsCompatibilityEntitlement: Equatable {
  var entitlement: String
  var versions: [String]

  static func == (lhs: Self, rhs: Self) -> Bool {
  	return lhs.entitlement == rhs.entitlement
  }
}

This simple struct gives us a way to easily store our version and entitlement data in CompatibilityAccessManager and to check against it when requesting entitlement access. Let’s add the following registration code below our shared declaration:

var registeredVersions: [BackwardsCompatibilityEntitlement] = []

public func register(entitlement: BackwardsCompatibilityEntitlement) {
	if !registeredVersions.contains(entitlement) {
    	registeredVersions.append(entitlement)
	} else {
    	print("'\(entitlement.entitlement)' already registered")
	}
}

To register a new set of versions for a specific entitlement, simply call that method as early as possible in your app’s lifecycle:

CompatibilityAccessManager.shared.register(entitlement:
	.init(entitlement: "premium", versions: ["1", "2", "3"])
)

Remember, the versions you register with this method are the build versions of your app.

In order for a user’s CustomerInfo to have a non-nil value for originalApplicationVersion, we’ll need to restore transactions so our receipt is in sync with RevenueCat. Let’s add a method to CompatibilityAccessManager that we’ll call right after configuring Purchases in our App Delegate:

func syncIfNeeded(completion: ((CustomerInfo?) -> Void)? = nil) {
	Purchases.shared.getCustomerinfo { (info, error) in
    	if let originalApplicationVersion = info?.originalApplicationVersion {
        	completion?(info)
    	} else {
        	if let receiptURL = Bundle.main.appStoreReceiptURL,
           	let _ = try? Data(contentsOf: receiptURL) {

            	Purchases.shared.syncPurchases { (info, error) in
                	completion?(info)
            	}

        	} else {
            	completion?(info)
        	}
    	}
	}
}

This method grabs the user’s CustomerInfo and if there is no originalApplicationVersion, it checks for a receipt and then syncs transactions. It’s important to make sure this is called after you configure Purchases in your AppDelegate, otherwise CompatibilityAccessManager won’t be able to sync with RevenueCat.

CompatibilityAccessManager now needs to fetch CustomerInfo from the Purchases SDK, check if an entitlement is active in RevenueCat, check the originalApplicationVersion, and return whether or not a particular user has access to an entitlement.

Let’s add a new helper function we can call from anywhere to do that! Add the following code to your CompatibilityAccessManager class:

func isActive(entitlement: String, result: @escaping ((Bool, CustomerInfo?) -> Void)) {
	Purchases.shared.getCustomerInfo { (info, error) in
    	if let info = info {
         	/// If a user has access to an entitlement via RevenueCat, return true
         	if info.entitlements[entitlement]?.isActive == true {
             	return result(true, info)
         	} else {
             	/// If a user doesn't have access to an entitlement via RevenueCat, check original downloaded version and compare to registered backwards compatibility versions
             	if let originalVersion = info.originalApplicationVersion {
                 	for version in self.registeredVersions {
                     	if version.entitlement == entitlement, version.versions.contains(originalVersion) {
                         	return result(true, info)
                     	}
                 	}
             	}

             	/// No registered backwards compatibility versions, or no available originalApplicationVersion to check against
             	return result(false, info)
         	}
     	} else {
        	/// CustomerInfo not available, so not able to check against originalApplicationVersion
        	return result(false, nil)
    	}
	}
}

In sandbox mode, the originalApplicationVersion is always “1.0” — this means testing may be challenging outside of production. For a workaround, check out the package mentioned at the end of this article.

This might seem like a lot, but it’s actually pretty simple, as the Purchases SDK does most of the heavy lifting. The function isActive accepts an entitlement string and a resulting closure. The closure includes a boolean to determine if the entitlement is active and returns an optional CustomerInfo object, since this method will replace Purchases.shared.CustomerInfo.

First, the function calls that Purchases method to fetch the CustomerInfo from RevenueCat or its cached value. If the CustomerInfo is nil, we return a false value in the result closure.

If CustomerInfo is available, we check if the entitlement we provided is active with RevenueCat. This is for the new, free users that will be purchasing your subscription. Since CompatibilityAccessManager is our source of truth, we have to make sure these users are taken care of in this method as well. If an entitlement is active via RevenueCat, we return a true value in the result closure.

If CustomerInfo is fetched, but the entitlement isn’t active in RevenueCat, we could be looking at a paid user. The method checks to make sure we have access to an originalApplicationVersion, then loops through our registered entitlements to check if our entitlement should be active with our user’s version. If the entitlement is found, we return a true value in the result closure. Otherwise, if none of those checks are valid, we return a false value in the result closure.

It’s time to test it out! With our premium entitlement, you should check for access like this:

CompatibilityAccessManager.shared.isActive(entitlement: "premium") { (isActive, info) in
	if isActive {
		// provide premium content
	} else {
		// restore purchases to sync your paid receipt, or present a paywall for new users
	}
}

CompatibilityAccessManager uses the Purchases SDK’s caching mechanisms for CustomerInfo, so this method is safe to call as often as needed throughout your app anytime you need to display “premium” content.

Conclusion

You’ve successfully migrated paid users to our new Entitlements feature and have a class that handles all of the access logic. It’s easily extensible, too! If you want to add separate entitlement access for different app versions, you can easily register a separate entitlement for different versions like we did above.

If you’re interested in a drop-in solution, I’ve packaged up CompatibilityAccessManager into a small, open-source Swift library called PurchasesHelper. The library includes additional features like sandbox workarounds, proper logging, the ability to unregister entitlements, and paywall helpers for displaying the terms of your subscription packages. Feel free to poke around and make improvements!

In-App Subscriptions Made Easy

See why thousands of the world's tops apps use RevenueCat to power in-app purchases, analyze subscription data, and grow revenue on iOS, Android, and the web.

Related posts

Vision Pro apps powered by RevenueCat: What’s available on launch day
Engineering

Vision Pro apps powered by RevenueCat: What’s available on launch day

Over 160 of the first visionOS apps are powered by RevenueCat

Charlie Chapman

Charlie Chapman

February 2, 2024

How I successfully migrated my indie app to RevenueCat Paywalls
Engineering

How I successfully migrated my indie app to RevenueCat Paywalls

And how a simple experiment increased my LTV by 33%.

Charlie Chapman

Charlie Chapman

January 3, 2024

StoreKit 2 tutorial: implementing in-app purchases in a SwiftUI app
StoreKit 2によるiOSのアプリ内課金のチュートリアル
Engineering

StoreKit 2によるiOSのアプリ内課金のチュートリアル

実際に動作するサンプルコードが付属したアプリ内課金のステップ・バイ・ステップ形式の解説です。

Kishikawa Katsumi

Kishikawa Katsumi

January 1, 2024

Want to see how RevenueCat can help?

RevenueCat enables us to have one single source of truth for subscriptions and revenue data.

Olivier Lemarié, PhotoroomOlivier Lemarié, Photoroom
Read Case Study