How to Set Up and Test an Auto-Renewable Subscription for an iOS App

Development iOS

An auto-renewable subscription is an iOS In-App Purchase category that allows an app to provide and charge for content or features over a set amount of time. We talked about the criteria of an auto-renewable In-App Purchase (IAP) and the things Apple could do to improve on its subscription IAP model in an earlier post. Now let us walk you through the steps needed to implement and test an auto-renewable In-App Purchase in your app.

Setting Up an Auto-Renewable In-App Purchase in iTunes Connect

Actual implementation of the purchasing of an auto-renewable In-App Purchase is not dissimilar to the same process for other IAP types. There are many tutorials that cover this process for the other IAP types. This one, by Neil North on RayWenderlich.com, focuses on Non-Renewing IAPs (you can jump to the middle at "Adding Your Subscriptions to the Product List"). The difference in setting up an auto-renewable In-App Purchase versus a Non-Renewing IAP is each subscription duration gets set within a single auto-renewable In-App Purchase entry, rather than as separate products. Each duration then has its own product ID.

At this point things get ... bizarre. Just kidding. That's not until later

Implementing an Auto-Renewable In-App Purchase in the App

We've found that the general implementation strategy of auto-renewable In-App Purchases is not much different than the other IAP types in that using an In-App Purchase helper class is the best option. However, the difference with auto-renewable In-App Purchases is that there's an added necessity for receipt management, which can be broken into its own helper class to keep your IAP helper reusable beyond just auto-renewable In-App Purchases. For the sake of brevity, we are keeping all methods within the same IAP helper class for this tutorial. In actual practice, however, we keep the receipt management code separate in its own class or make that portion server-side code due to the fact that there is a secret key element in the receipt exchange. We'll talk more about this later in the tutorial.

For the IAPHelper, you'll need to import StoreKit then declare your helper as an SKProductsRequestDelegate and SKPaymentTransactionObserver:

import UIKit
import StoreKit
class IAPHelper: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver

Then you'll declare your initial properties (we'll add more properties in a minute):

var productID:NSSet = NSSet(object: "string")
var productsRequest:SKProductsRequest = SKProductsRequest()
var products = [String : SKProduct]()

We'll implement the IAPHelper as a singleton in our app, so we'll need to create a way to do that. As of 1.2, Swift provides a much cleaner and concise way of doing this, especially when compared to the dispatch_once implementation in Objective-C:

static let sharedInstance = IAPHelper()

So now, instead of laughing every time I see the "don't abuse the singleton method,” due to the multiline implementation required in Objective-C, I say "cover the kid's eyes,” roll up my Swift sleeves and go Oprah with singletons."You get a singleton! You get a singleton! You get a singleton!"

Now that we have our properties ready, it's time to jump into some methods!

First, we need to hit up the app store for our product identifiers and kick off our product request, so we'll need a public method for that:

 func requestProductsWithProductIdentifiers(productIdentifiers: NSSet) {

let productIdentifiers:NSSet = NSSet(objects: "com.ourApp.monthly", "com.ourApp.annually")

productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as Set<NSObject>)
productsRequest.delegate = self;
productsRequest.start()

}

With our SKProductsRequest delegate set and the request started, we need a way to grab the response, so we'll implement the SKProductsRequest productsRequest delegate method:

func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {

    if let productsFromResponse = response.products as? [SKProduct] {

        for product in productsFromResponse {
        products[product.productIdentifier] = product

        }

    }

 }

Now we have our setup code implemented, we just need to call requestProducts on our sharedInstance and our IAPHelper is armed and ready.

First, let's add a new property, a closure that, depending on the state of the purchase in a particular method, we'll either handle or pass around. This closure will take a Bool (true if succeeded and false if failed) and an optional String which will be a way to send back a descriptive message string in the case that our purchase failed:

var purchaseCompleted: ((Bool, String?) -> Void)

The first step to purchasing a product is to actually set up a method to receive a request from, say a button in our app, to begin the purchase of one of our subscription durations:

func beginPurchaseFor(productIDString: String, purchaseSucceeded:(Bool, String?) -> Void) {

    if SKPaymentQueue.canMakePayments() {

        if let product: SKProduct = products[productIDString] {

            purchaseCompleted = purchaseSucceeded
            var payment = SKPayment(product: product)
            SKPaymentQueue.defaultQueue().addTransactionObserver(self)
            SKPaymentQueue.defaultQueue().addPayment(payment);

        } else {

            purchaseSucceeded(false, "Product \(productIDString) not found")

        }

     } else {

        purchaseSucceeded(false, "User cannot make payments")

    }

 }

In the first line of our declaration, you can see the second parameter is a closure that acts as a completion handler for the function, which matches our purchaseCompleted closure property above.

We'll first verify if the device can make payments, and if so, we'll then check if we have a product that matches the passed-in product ID. If that's good to go, we set our purchaseCompleted property to the purchaseSucceeded closure as we're not quite ready to give it a pat on the back and send it on its way. We then create an SKPayment object with the product, add our IAPHelper as a transaction observer and our SKPayment object to the default payment queue.

If any of our "ifs" fail (the device can't make payments or there is no product that matches the productID), we send back our purchaseSucceeded handler with false for the Bool and a message (“Product (productIDString) not found” or "User cannot make purchases” depending on what failed) for the optional String. We then let whoever was waiting for the handler decide what to do from there.

Assuming the product is successful, we need a way to listen for our purchase response. To do that we'll implement SKPaymentTransactionObserver's paymentQueue method. But let's break the explanation up into two parts. First we'll tackle a successful purchase response:

func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
    for transaction: AnyObject in transactions {

        if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction {

            switch trans.transactionState {
                case .Purchased:
                SKPaymentQueue.defaultQueue().finishTransaction(trans)
                if let receiptURL = NSBundle.mainBundle().appStoreReceiptURL where

                NSFileManager.defaultManager().fileExistsAtPath(receiptURL.path!) 
{
                    if let purchaseSuccessCallback = purchaseCompleted {

                        purchaseSuccessCallback(true, nil)

                    }

               self.isPurchasing = false
               self.receiptValidation()
        } else {

            if !isRefreshingReceipt {

                self.isPurchasing = false
                isRefreshingReceipt = true
                let request = SKReceiptRefreshRequest(receiptProperties: nil)
                request.delegate = self
                request.start()

                if let _ = purchaseCompleted {

                    purchaseCompleted**(true, nil)
                }
            }
        }

            break

Assuming the purchase was successful and we get a happy ".Purchased" transaction state, we now start to break away from the standard IAP implementation and, as you can see in the code above, really want and need that coveted receipt! With a successful transaction, the app should have a path to the user's local receipt in appStoreReceiptURL. If that's the case, we can then jump into our (soon to be made!) receiptValidation function for the next step in setting up and managing a user's subscription. It's at this point that we can give our purchase completion handler -- the purchaseSuccessCallBack property that was passed from beginPurchaseFor above -- a hug and a true value for the Bool, because it was successful, and nil for the optional String error message, then send it on its way to whoever sent the kid to our IAPHelper with our good news.

If, however, the purchase was successful but the receipt hasn't been received yet, we can give it a little kick in the rear by creating and starting a SKReceiptRefreshRequest.

We'll jump out of the paymentQueue and take a look at the SKReceiptRefreshRequest requestDidFinish delegate method:

func requestDidFinish(request: SKRequest!) {

    if let appStoreReceiptURL = NSBundle.mainBundle().appStoreReceiptURL where
    NSFileManager.defaultManager().fileExistsAtPath(appStoreReceiptURL.path!) {

        self.receiptValidation()

        if let purchaseSuccessCallback = purchaseCompleted {

            purchaseSuccessCallback(true, nil)

         }           

    } else {

        if let purchaseSuccessCallback = purchaseCompleted {
            purchaseSuccessCallback(false, "Cannot find receipt")

        }
    }
}

When we get a response through requestDidFinish, we can check for a file at appStoreReceiptURL.path and, if it exists, we make our way to validating our receipt while also notifying whoever's waiting on the completion handler that the purchase was, indeed, completed.

If we still aren't finding a receipt, then we can send back our sad completion handler with false for the Bool and an overly emotional "Cannot find receipt" message for our optional String. Seeing as our IAPHelper is overcome by grief (and assuming the digital receipt printer is jammed), we can let whoever initiated the purchase handle the error while IAPHelper gathers itself.

Wow. That was heavy. Let's ... let's move on to something happier.

So what happens if we receive ".Failed" in the paymentQueue delegate method? What!? More failure!? I thought we were through the doom and gloom!

Fine. Whatever. Let's finish out the func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) from above:

       case .Failed:

        self.isPurchasing = false

            if let _ = purchaseCompleted {

                var errorMessage: String?

                    if let _ = transaction.error {

                        errorMessage = transaction.error.localizedDescription

                     }

                 purchaseCompleted(false, errorMessage)

            }

        SKPaymentQueue.defaultQueue().finishTransaction(trans)
        SKPaymentQueue.defaultQueue().removeTransactionObserver(self)
        break

       default:
       break

           }
       }
    }
}

So it failed. It's an Apple API, right? It works perfectly and everything is sunshine and rainbows, so if it failed, that's on you and was caused by something you did. So step back, take a long look in the mirror and figure out how you angered the Apple gods. I'm not seeing an Apple Watch on that wrist. Yeah, I think we know what happened.

Let's take that failure and make the best of it. We'll see if an error was sent, and if so, we'll snag localizedDescription from it, send back the completion handler with a false, our errorMessage optional String. And tears. Then we tell SKPaymentQueue.defaultQueue() that we finished the transaction battle ... but not the war.

That takes us through the setup and the purchasing of our auto-renewable In-App Purchase. Now we'll look at why we actually need that receipt and what we're going to do with it once we wrap our hands around its digital paper neck

Subscription Management with Auto-Renewable In-App Purchases

Before diving headfirst into code, let's take a look at how auto-renewable In-App Purchase subscription management works and what that means for our app.

Apple does not provide anything built into iOS or a REST API that gives you simple subscription details, nor are there any callbacks that you can listen for and respond to in regards to renewal or cancellation. Apple does have an API that, when given a user's local receipt and a “shared secret” generated in iTunes Connect, returns a JSON object of the user's purchase history for your app, including their current subscription information.

We then need a way to monitor the subscription and check to see if the subscription has been renewed or cancelled by periodically retrieving that JSON encoded remote receipt and parsing it to see if the expiration date has changed. If it has, the subscription has been renewed, if it has not and the expiration date has passed, then the subscription has not been renewed and we can either assume it has been cancelled (which can be further verified by parsing the response for a cancellation date) or there was an issue with the user's payment information on Apple's side. Whatever the case may be, the subscription is not currently valid and the app can be then set back into its unpaid state for the user.

You can do this locally in the app, or server side using your own setup or a BaaS like Parse.

Enough talk, let's code!

Okay, I lied, first we need to generate our shared secret in iTunes Connect. To do that, jump into the IAP section of your app, and under the list of IAPs, you'll see a "View or generate a shared secret.” Click that and you're good to go.

View or generate a shared secret

Now let's work on our receipt handling code!

Our first receipt handling method is:

func receiptValidation() {

var error: NSError?

    if let receiptPath = NSBundle.mainBundle().appStoreReceiptURL?.path where
    NSFileManager.defaultManager().fileExistsAtPath(receiptPath), let receiptData 
= NSData(contentsOfURL: 
    NSBundle.mainBundle().appStoreReceiptURL!), let receiptDictionary = ["receipt-
data" : 
    receiptData.base64EncodedStringWithOptions(nil), "password" : "your shared 
secret"], let requestData = 
    NSJSONSerialization.dataWithJSONObject(receiptDictionary, options: nil, 
error:&error) as NSData! {

        let storeURL = NSURL(string: 
"https://sandbox.itunes.apple.com/verifyReceipt")!
        var storeRequest = NSMutableURLRequest(URL: storeURL)
        storeRequest.HTTPMethod = "POST"
        storeRequest.HTTPBody = requestData

        let session = NSURLSession(configuration: 
NSURLSessionConfiguration.defaultSessionConfiguration())

        session.dataTaskWithRequest(storeRequest, completionHandler: { (data: 
NSData!, response: NSURLResponse!,
        connection: NSError!) → Void in

           if let jsonResponse: NSDictionary = 
NSJSONSerialization.JSONObjectWithData(data, options:
           NSJSONReadingOptions.MutableContainers, error: &error) as? 
NSDictionary, let expirationDate: NSDate = 
           self.expirationDateFromResponse(jsonResponse) {

               self.updateIAPExpirationDate(expirationDate)

           }
       })
    }
}

After declaring an optional NSError variable we get into a nice bit that goes above and beyond exploiting Swift 1.2's optional chaining capabilities. It first checks for the existence of an appStoreReceiptURL (which we've previously verified during our purchase completion extravaganza, but, as in all things Swift, better safe than sorry). Then it sees if a file exists at the path and, if so, it creates our receiptData object from the contents at appStoreReceiptURL, which, paired with the shared secret, creates our receiptDictionary for our JSON data.

If all that checks out, we can go ahead make a NSMutableRequest using the requestData.

In this example, we're using the sandbox URL for receipt verification, but in production you would change "sandBox" to "buy."

We send off our requestData to Apple and wait for the response in the completionHandler closure. We then use an optional chain to check the response and, if the first part of the chain is successful, pass that to a method we will make next, expirationDateFromResponse, that parses the response for the expiration date.

If that all goes well, we can handle that date as we see fit -- checking to see if that expiration date is different then what we currently have stored (if it is, then that means the subscription has renewed), or is the same and is older than today (which means the subscription was not renewed and most likely canceled, which can be verified by checking for a Cancellation Date field in the JSON response).

Now we need to create our expirationDateFromResponse method that takes the JSON response we get from Apple and parse it for our expiration date:

func expirationDateFromResponse(jsonResponse: NSDictionary) → NSDate? {

   if let receiptInfo: NSArray = jsonResponse["latest_receipt_info"] as? NSArray {

      let lastReceipt = receiptInfo.lastObject as! NSDictionary
      var formatter = NSDateFormatter()
      formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"

      let expirationDate: NSDate = 
formatter.dateFromString(lastReceipt["expires_date"] as! String) as NSDate!

      return expirationDate

   } else {

      return nil

   }
}

We first check if we get an NSArray from the jsonResponse if we pass the "latest_receipt_info" key and, if we do, we grab the last object of that array, then check if we there's an object for the key "expires_date" that we then convert to an NSDate. If all that checks out, we return our expirationDate object. But if any of those optional chains fail, we return nil (hence the optional NSDate return for the method).

Next we'll look at how to test subscriptions our new auto-renewable In-App Purchases.

Testing Auto-Renewable In-App Purchases

Having this receipt verification code set up is one thing, but how do you verify the verifier? By testing! How do you do that if your code depends on an outside component you have no control over? By testing according to the tools they give you!

As I mentioned a few paragraphs ago, there are two URLs for receipt verification, one for testing and one for production. Purchasing an auto-renewable In-App Purchase in sandbox mode is a similar process as test-purchasing any IAP during development; you create a Sandbox Tester account in iTunes Connect's Users and Roles area (or a few, which, in the case of auto-renewable In-App Purchase is a good idea), then use that sandbox tester account to make your purchase on your device.

Where things vary with an AR-IAP is that the test purchases have a lifecycle baked in to simulate a recurring, then expiring subscription. The subscription will renew five times and then expire. Now you're probably thinking, "wait, for my monthly subscription, I have to wait 5 months before I can see if my expiration handling code works?" Auto-renewable In-App Purchase in the sandbox are on an accelerated lifecycle, so the renewal time is minutes or hours. If you're testing a monthly subscription, they renew every five minutes, so five renewals means that in 25 minutes your test subscription will expire.

Subscription durations for testing

One thing to note is that subscriptions do not always renew in the test environment. I've found that about a third of the time, a monthly test subscription will hit the first five minute mark and stay there, expired, without renewing. Remember those multiple sandbox accounts I said you should create? That's where those come in handy if you want to test your subscription renewal code. If, however, you want to test expiration code, then stay with that sandbox account, as the non-renewing test subscription bug is great for that, cutting your test wait time down to a fifth.

That being said, waiting 15 minutes between tests of your one week auto-renewable In-App Purchase is not a great way to spend your day. Plus, what about testing a subscription cancellation? Well, you can't. As of now, there's no way to manually cancel a sandbox subscription as one can do with a live subscription. So, the best course of action is to start testing with one of your other sandbox users, then come back to the original one when the expiration time is nearing, and bounce between them in that fashion

Concluding Note

At this point you are not only ready to implement auto-renewable In-App Purchases in your app, but have the low down on testing them as well. One thing not covered in this tutorial is moving your receipt validation code (and your IAP shared secret) out of your app and keep it server side. That's something we will explore in the future, but for now, good luck and happy subscription management!

Jaz is an app developer who enjoys creating tasteful interactions, fleets of world-dominating bots, and....puns.

You made it this far so...