What’s new in Apphud: Paywall Analytics, Stripe Web Payments Support and Xiaomi SDKLet’s see
Apphud
Why Apphud?PricingContact
Ren
Ren
June 26, 2019
17 min read

Swift tutorial: Auto-renewable subscriptions in iOS

Let's talk about auto-renewable subscriptions on iOS. I will show how to create, manage, purchase and validate auto-renewable subscriptions.

Swift tutorial: Auto-renewable subscriptions in iOS

An auto-renewable app subscription lets you monetize your apps by charging users for features, services, or content on a recurring basis. These subscriptions will automatically renew at the end of their duration, and this will end once the user decides to cancel. 

There are a huge number of subscription-based apps out there, and they are available on iOS, macOS, iPadOS, tvOS, and watchOS. Auto-renewing subscriptions have become a prevalent business model on the App store. From 2019 to 2020, global consumer spending in the top 100 App Store subscription apps experienced a 32% increase — for a total expenditure of $10.3 billion. 

And according to Adjust, 49% of the top 225 App store apps are subscription-based. It's obvious that developers are moving towards this model to increase their revenue. 

With so many auto-renewing subscriptions on the market, you might assume that setting up iOS auto-renewable subscriptions is an easy process. 

While it can certainly feel simpler as you gain experience, creating Swift monthly subscriptions that auto-renew requires quite a bit of coding. We'll walk you through the process, so you can start monetizing your app ASAP. 

What you'll learn: In this Swift subscription tutorial, I will talk about auto-renewable subscriptions on iOS 12 & iOS 13. I will show how to create, manage, purchase, and validate auto-renewable subscriptions. My Swift Apple tutorial will also talk about hidden challenges and some cases that many developers miss.

Types of In-App Purchases

Even if an app can be downloaded for free, the developer can still make revenue by implementing in-app purchases. 

In-app purchases prompt users to purchase add-ons, premium features, subscriptions, and more. In essence, an in-app purchase is any fee that an app can ask for, beyond the initial download cost (if any). The option to make an in-app purchase can be introduced at any point in the app. Mobile game apps, in particular, use strategic timing to offer in-app purchases when the player seems to need help. 

Spending money on in-app purchases gives users a premium experience while also giving extra revenue to the developer and to the App store. 

There are four kinds of in-app purchases that customers can make.

  • Consumable. The customer will have to buy the item each time they want to use it. In other words, they are not reusable. For instance, the user might buy health for a game, in-game currency, or hints. Upon changing a device or reinstalling the app, the user might lose their purchased consumable products. 
  • Non-consumable. These items are bought once and can be used without any additional fee in the future. For instance, they could pay a few to upgrade an app to its pro version. If they change their device or uninstall and reinstall the app, they will not lose the product. Or, if it does end up getting lost, the user can restore their in-app purchases to re-download the non-consumable product for free. 
  • Non-renewing subscriptions. Customers can purchase a subscription that allows them to use an item or service for a certain period of time. Once the time expires, they can re-purchase the subscription. Typical time periods for non-renewing subscriptions are 1, 3, and 6 months. 
  • iOS auto-renewable subscriptions. Similar to non-renewing subscriptions, customers will gain access to a product or service for a specified period of time. However, the subscription will automatically renew once the period of time has passed. Some examples of auto-renewable subscriptions are Netflix, Disney+, and Spotify. 

Some popular in-app purchase mechanics include the following approaches. 

  • VIP system. Users that make in-app purchases will collect "points" and, eventually, become "VIP members."
  • Battle pass. After purchasing a seasonal "battle pass," mobile app gamers can gain access to additional game content.
  • Time-limited. A discounted in-app purchase is offered to the user for a limited time.

Set up subscriptions in App Store Connect

If you already have an app in ASC (abbreviated from App Store Connect) you may skip this tutorial and go to coding part of this Swift subscription tutorial. But if you don’t have an app yet here is what you need to do.

Before going to ASC you should first login to Apple Developer Portal and open Certificates, Identifiers & Profile page. Then click on Identifiers tab. Since June 2019, Apple has finally updated this page design to match ASC.

Identifiers tab on Certificates, Identifiers & Profile pageIdentifiers tab on Certificates, Identifiers & Profile page

You should create a new explicit Bundle ID. It’s a reverser-domain name style string (i.e. com.apphud.subscriptionstest). If you scroll down, you can see the capabilities list where In-App Purchases have already been checked. After you create an App ID, go to App Store Connect.

Create a new explicit Bundle IDCreate a new explicit Bundle ID

Another main step is filling up all information in Agreements, Tax and Banking section. You won’t have an active Paid apps agreement you won’t be able to test in-app purchases.

After that, you create a new App Store app. It’s really simple. Choose your recently created Bundle ID and enter any unique name — you can change it later.

Create a new App Store appCreate a new App Store app

 If you already had an app, you can continue reading an article from here

On the app page, go to the Features section. 

Adding iOS auto-renewable subscriptions consists of several steps.

Creating a subscription identifier and a subscription group. Subscription groups are like a collection of subscriptions, where just one can be active at one moment. The free trial period can be activated only once within a group. If your app needs two subscriptions at a time, then you will need to create two subscription groups.

You can read more about iOS auto-renewable subscriptions in this article.

Next, you will need to fill subscription page: duration, display name, and description. If you add your first subscription to a group you would need to specify a group display name as well. Save changes from time to time: ASC often freezes.

Fill subscription page on In-App PurchasesFill subscription page on In-App Purchases

Finally, add the subscription price. This includes the regular price, introductory offers, and promotional offers. Add the price in your currency — it will be automatically recalculated for other currencies. Introductory offers let you create a free trial, “pay as you go” or “pay upfront” offers. Promotional offers is a new thing that was added recently: you can set up personal offers for users who canceled their subscription to bring them back.

Shared secret key

On the in-app purchases page you may notice “App-Specific Shared Secret” button. It’s a special key that is used for validating receipts. In our case, we use it to get subscription status.

The shared secret key can be app-specific or account-specific (Master shared secret key). Don’t ever touch shared secrets if you have live apps: you won’t be able to validate purchases with an invalid key.

App-Specific Shared SecretApp-Specific Shared Secret

Copy all your subscriptions IDs and shared keys. Now is the programming part.

Programming auto-renewable subscriptions

How to make a good subscriptions manager class? This class at least should be capable of doing these:

  • Purchasing subscriptions
  • Checking subscription status
  • Refreshing a receipt
  • Restoring transactions (don’t confuse with receipt refreshing!)

Purchasing subscriptions

When you implement an in-app purchase in iOS, the process of purchasing a subscription can be divided into two steps: loading products information and implementing payment. But in advance we should set an observer for payment queue SKPaymentTransactionObserver:

// Starts products loading and sets transaction observer delegate
@objc func startWith(arrayOfIds : Set<String>!, sharedSecret : String){
    SKPaymentQueue.default().add(self)
    self.sharedSecret = sharedSecret
    self.productIds = arrayOfIds
    loadProducts()
}

private func loadProducts(){
    let request = SKProductsRequest.init(productIdentifiers: productIds)
    request.delegate = self
    request.start()
}

public func productsRequest(\_ request: SKProductsRequest, didReceive response: SKProductsResponse) {                
    products = response.products
    DispatchQueue.main.async {
        NotificationCenter.default.post(name: IAP\_PRODUCTS\_DID\_LOAD\_NOTIFICATION, object: nil)
    }
}

func request(\_ request: SKRequest, didFailWithError error: Error){
    print("error: \\(error.localizedDescription)")
}

Full code is available at the end of this article.

When we request products information a delegate method will be called back. IAP_PRODUCTS_DID_LOAD_NOTIFICATION can be used to update UI in the app.

Later let's write payment initializing:

func purchaseProduct(product : SKProduct, success: @escaping SuccessBlock, failure: @escaping FailureBlock){
    guard SKPaymentQueue.canMakePayments() else {
        return
    }
    guard SKPaymentQueue.default().transactions.last?.transactionState != .purchasing else {
        return
    }
    
    self.successBlock = success
    self.failureBlock = failure
    
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
}

And here’s our SKPaymentTransactionObserver delegate method:

extension IAPManager: SKPaymentTransactionObserver {
    
    public func paymentQueue(\_ queue: SKPaymentQueue, updatedTransactions transactions: \[SKPaymentTransaction\]) {
        for transaction in transactions {
            switch (transaction.transactionState) {
            case .purchased:
                SKPaymentQueue.default().finishTransaction(transaction)
                notifyIsPurchased(transaction: transaction)
                break
            case .failed:
                SKPaymentQueue.default().finishTransaction(transaction)
                print("purchase error : \\(transaction.error?.localizedDescription ?? "")")
                self.failureBlock?(transaction.error)
                cleanUp()
                break
            case .restored:
                SKPaymentQueue.default().finishTransaction(transaction)
                notifyIsPurchased(transaction: transaction)
                break
            case .deferred, .purchasing:
                break
            default:
                break
            }
        }
    }
    
    private func notifyIsPurchased(transaction: SKPaymentTransaction) {
        refreshSubscriptionsStatus(callback: { 
            self.successBlock?()
            self.cleanUp()
        }) { (error) in            
            // couldn't verify receipt
            self.failureBlock?(error)
            self.cleanUp()
        }
    }
    
    func cleanUp(){
        self.successBlock = nil
        self.failureBlock = nil
    }
}

After the in-app purchase Swift operation finishes, it calls the func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) delegate method. We check the transaction’s state to check whether the purchase succeeded or not.

Okay, a subscription has been purchased. But how to get its expiration date?

Checking the subscription status

It is actually the same as validating receipt with the App Store. We send a POST request, called verifyReceipt to Apple, in this request we include the local receipt as a base64encoded string parameter. We get JSON in response, containing all our transactions. There’s an array of transactions with all purchases including renewals (by latest_receipt_info key). All we need to do is loop through an array and check the expiration date for all of those transactions and get active ones.

At WWDC 2017 a new key has been added as a request parameter: exclude-old-transactions. If we set it to true, then Apple will return only active transactions in a verifyReceipt request.

func refreshSubscriptionsStatus(callback : @escaping SuccessBlock, failure : @escaping FailureBlock){
    // save blocks for further use
    self.refreshSubscriptionSuccessBlock = callback
    self.refreshSubscriptionFailureBlock = failure
    guard let receiptUrl = Bundle.main.appStoreReceiptURL else {
            refreshReceipt()
            // do not call block yet
            return
    }
    #if DEBUG
        let urlString = "https://sandbox.itunes.apple.com/verifyReceipt"
    #else 
        let urlString = "https://buy.itunes.apple.com/verifyReceipt"
    #endif
    let receiptData = try? Data(contentsOf: receiptUrl).base64EncodedString()
    let requestData = \["receipt-data" : receiptData ?? "", "password" : self.sharedSecret, "exclude-old-transactions" : true\] as \[String : Any\]
    var request = URLRequest(url: URL(string: urlString)!)
    request.httpMethod = "POST"
    request.setValue("Application/json", forHTTPHeaderField: "Content-Type")
    let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: \[\])
    request.httpBody = httpBody
    URLSession.shared.dataTask(with: request)  { (data, response, error) in
        DispatchQueue.main.async {
            if data != nil {
                if let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments){
                    self.parseReceipt(json as! Dictionary<String, Any>)
                    return
                }
            } else {
                print("error validating receipt: \\(error?.localizedDescription ?? "")")
            }
            self.refreshSubscriptionFailureBlock?(error)
            self.cleanUpRefeshReceiptBlocks()                
        }
    }.resume()        
}

At the beginning of this method, you will find that if a local receipt is not found then a request is not being sent. That’s correct: local receipt may not exist in some cases: for example, when you install an app from iTunes. In such cases we need to refresh a receipt first, then call this function again.

Refreshing a receipt

There is a special SKReceiptRefreshRequest class for it:

private func refreshReceipt(){
    let request = SKReceiptRefreshRequest(receiptProperties: nil)
    request.delegate = self
    request.start()
}

func requestDidFinish(\_ request: SKRequest) {
  // call refresh subscriptions method again with same blocks
    if request is SKReceiptRefreshRequest {
        refreshSubscriptionsStatus(callback: self.successBlock ?? {}, failure: self.failureBlock ?? {\_ in})
    }
}

func request(\_ request: SKRequest, didFailWithError error: Error){
        if request is SKReceiptRefreshRequest {
            self.refreshSubscriptionFailureBlock?(error)
            self.cleanUpRefeshReceiptBlocks()            
        }
        print("error: \\(error.localizedDescription)")
}

After we started this request we will get a delegate method requestDidFinish(_ request : SKRequest) which calls refreshSubscriptionsStatus again.

How do we parse JSON responses from Apple? Here’s an example of how you can do that:

private func parseReceipt(\_ json : Dictionary<String, Any>) {
    // It's the most simple way to get latest expiration date. Consider this code as for learning purposes. Do not use current code in production apps.
    guard let receipts\_array = json\["latest\_receipt\_info"\] as? \[Dictionary<String, Any>\] else {
        self.refreshSubscriptionFailureBlock?(nil)
        self.cleanUpRefeshReceiptBlocks()
        return
    }
    for receipt in receipts\_array {
        let productID = receipt\["product\_id"\] as! String 
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
        if let date = formatter.date(from: receipt\["expires\_date"\] as! String) {
            if date > Date() {
                // do not save expired date to user defaults to avoid overwriting with expired date
                UserDefaults.standard.set(date, forKey: productID)
            }
        }
    }
    self.refreshSubscriptionSuccessBlock?()
    self.cleanUpRefeshReceiptBlocks()
}

We loop through the array looking for expires_date key and save it if is not expired yet. It’s a simple example which considered to be for learning purposes. It doesn’t handle errors or several cases, for example, canceling (refunding) a subscription.

To check if the subscription is active or not you should compare the current date with the subscription’s expiration date:

func expirationDateFor(\_ identifier : String) -> Date?{
        return UserDefaults.standard.object(forKey: identifier) as? Date
    }
    
let subscriptionDate = IAPManager.shared.expirationDateFor("YOUR\_PRODUCT\_ID") ?? Date()
let isActive = subscriptionDate > Date()

Restoring transactions

It’s just one method: SKPaymentQueue.default().restoreCompletedTransactions(). These functions restore transactions so that observer calls `updatedTransactions` delegate method as many times as many transactions you have.

A popular question is being asked:

Restoring transactions vs. refreshing a receipt. What are the differences?

Well, both methods let you restore purchases information. Here’s a good screenshot from this WWDC video:

Restoring transactions vs. refreshing a receiptRestoring transactions vs. refreshing a receipt

In most cases, refreshing the receipt is enough. And you don't really need to restore transactions. In our auto-renewable subscription iOS example, we send a local receipt to Apple for validation in order to get the subscription's expiration date. And we don't actually care about transactions. However, if you use Apple-hosted content in your purchases — or your target device below iOS 7 — you will have to use the first method.

Sandbox testing

What about iOS testing for the in-app purchase? To test auto-renewable subscriptions you will need to add sandbox testers first. Subscriptions can be tested only using a device. So in ASC go to Users & Access then go to Sandbox Testers where you will create your first sandbox tester.C

Create sandbox testerCreate sandbox tester

While creating a new tester you can enter any text. However, don’t forget to enter an email and a password!

Previously when testing in-app purchases you used to log out from the App Store. This was very annoying: your iTunes music was being removed from the device. Now this problem has gone, the sandbox test account is separated from the real App Store account.

The process of making an in-app purchase is similar to a real purchase. But it has some nuances:

- You will also have to enter sandbox credentials: Apple ID & password. Using the device’s Touch ID / Face ID is still not supported.

  • If you entered credentials correctly but the system keeps showing the same dialog box, then you should tap on “Cancel”, hide the app, then go back and try again. Seems ridiculous but it works. Sometimes payment process may go on from the second attempt.
  • You can’t test subscriptions cancellations.
  • Sandbox subscription durations are much less than real durations. And they renew only 6 times a day. Here’s a table:
Sandbox subscription durationsSandbox subscription durations

What’s new in StoreKit in iOS 13?

Well, the only new thing is SKStorefront, which gives you a country code of the user’s App Store account. It may be useful if you need to show different in-app purchases for different countries. Previously, developers had to check iPhone region settings or location to get the country. But now it is made within one line of code: SKPaymentQueue.default().storefront?.countryCode. There’s also a delegate method that tells you if the user’s App Store country has been changed during the purchase process. In this case, you may continue or cancel the payment process.

Caveats when working with auto-renewable subscriptions

  • Online receipt validation directly from a device is not recommended by Apple. It’s being said in this video from WWDC (starting at 5:50) and in documentation. It’s not secure, because your data may be compromised with a man-in-the-middle attack. The only right way to validate receipts — using local receipt validation or server-to-server validation.
  • There is a problem related to an expiration date comparing. If you don’t use the server-to-server validation, a user may change the system date to a passed one, and the given code will give an unexpected result — an expired subscription will become active. To avoid this problem, you should get the world time from any online service.
  • Not every user is eligible for a free trial. He may already use it. But many developers don’t handle this case, they just display “Start Free Trial” text on a button. To handle this case, you should validate the receipt before showing a purchase screen and check JSON for trial eligibility.
  • If users have canceled their trial via Apple Care (made a refund), then JSON will provide a new cancellation_date key. But an expires_date key will still be there. That means that you should always check for the cancellation date first, as it’s primary.
  • You shouldn’t refresh the receipt too often. This action sometimes asks for the user’s Apple ID password. But we don’t want that — we need everything to be done behind the scenes. You should refresh the receipt when showing the purchase screen if needed or when the user taps on the Restore Purchases button.
  • How often you should validate a receipt to get the latest subscription info? Probably the good way is to call once during the app launch. Or you may do this only when the subscription expires. However, don’t forget possible cancellations (refunds). Users will be able to use your app for free until the end of the billing period.

Conclusion

I hope this article was useful. I tried not only include the code but explain some important caveats. The full class code can be downloaded here.

This class will be useful for learning purposes. However, for live apps you should use more complex solutions, such as Apphud  —  an in-app purchase management platform with real-time analytics and subscriber engagement tools.

Ren
Ren
Co-founder at Apphud
Ex iOS app and game developer. 11 years in the industry since iOS 3. More than 50 apps are in the background with 4 exits. Entrepreneur and traveler.