Upcoming and OnDemand Webinars View full list

Monetizing Your Apps with Apple’s In-App Purchases

Steve Sparks

Raise your hand if you wanna get paid.

Taylor Swift

It’s hard monetizing your apps, and as much as Apple has done to make in-app
purchasing (aka IAP) easy, it’s still not easy. The code is the easy part. There are a
bunch of things to get straight before we even get to the code. Today we’re
going to walk through the process from start to finish.

It’s All Business

If you’re the product owner or project manager, the first half of this article is for you.
Keeping an eye on the lead time for launch means leaving adequate time for implementing and
testing (ESPECIALLY testing!) these features. When the app is complete except for IAP,
it’s especially painful because it’s done except for that all important “get the money”
part.

Ground Rules

Getting to the IAP testing stage with a test app is kind of a pain. It’s much
easier to go through it “for real”. I would suggest that you do this work on an app you intend to monetize.

Also, you must make sure all the financial stuff is straight. Apple needs to
know they have a way to send you money. They need to believe you have the legit
right to sell what you’re selling (ie. you can’t call yourself Disney without
proving that you really represent Disney.) And they need to know that stuff
before they’ll even show you the products in the sandbox environment.

Understanding the Terms

App Identifier or App ID: When Apple asks for these things, they
mean the numeric Apple ID for your app, not the reverse-DNS bundle ID we
use in the app. The Apple ID will be shown on the iTunes Connect
App Information screen, under General Information, after we’ve
created a record for your app.

Product ID is an string identifier you attach to your IAP record. If your
game sells a power-up for a buck, you’ll assign some unique ID to that
power-up such as, say, powerUp.diamond_shields. In some organizations,
the product ID is provided by the accounting department so they can
tie purchases back to their system.
According to Apple,
you can use letters, numbers, periods and underscores.

Review is a many-faceted thing that, for the most part, we don’t deal with
until we’re almost ready for release. There are three pieces of review: External
TestFlight review, App Store Review, and In App Purchase Review.

The Sandbox is the test Apple purchase environment. When using a build that
is sandboxed, all of the IAP screens will indicate that they are using the
sandbox. Do not try to use real iTunes users in the sandbox, and don’t ever ever
use a sandbox iTunes user for any kind of real login, such as the App Store or Apple ID management. Doing so renders the sandbox user permanently unusable. It’s too easy to slip up and make this mistake, so you should really be careful with your sandbox IDs.

TestFlight is Apple’s test deployment system. You use it to upload and
distribute builds. You can use HockeyApp or other deployment services as well,
but for ease of discussion, we’ll be using TestFlight.

Let’s Do It!

You’ll need to flip the “In App Purchases” switch in Xcode, which will poke the
relevant bits in the provisioning portal. (If you’re using source control, and
you should be, this makes for a nice atomic commit. So do it.)

Xcode capabilities

Then, log in to iTunes Connect. Go right into
the Agreements, Tax and Banking module and make sure everything is set up,
including the banking details. Seriously, cross every T and dot every I.

Agreements, Tax and Banking module

When you are all done, you must wait for Apple’s approval. They work fairly
quickly when compared to app review, as it’s straightforward accounting and
legal stuff. And remember, before you grouse about it, this is how Apple gets
you paid. Agree with glee!

From time to time Apple updates their license agreements and you must log into iTunes Connect
to re-agree to them. You should be logging in to view your reports, anyway, so periodically
swing by the finance module and make sure we’re all still good with Apple.

While you’re waiting, go into the My Apps section and set that up.
It’s in there that you will create your products for sale.

Setting up the My Apps section

In order to get your app into the store, you need to create a record for it.
This is pretty straightforward; select the Xcode managed app ID and take it
from there. You don’t need to worry about all the descriptions and images just yet.

Once you’ve created your app record, switch to the “Features” tab. It starts on
the In-App Purchases feature, so all you have to do is poke the ‘+’ to create a new product.

IAP Screen

You’ll then be presented with a set of choices as to what sort of thing you’re selling:

  • Consumables are items such as a magic potion or an extra 10,000 coins for a player to spend in your game. They can be bought many times.
  • Non-Consumables are items that you purchase and don’t use up. This might be for a feature unlock.
  • Non-Renewing Subscriptions are a one-time purchase that has an expiration date that you can programmatically extract.
  • Auto-Renewing Subscriptions charge repeatedly until cancelled. This is where you might offer a monthly subscription to your content.

Choose your item type and click Create, and then you’re on to the details.
The Reference Name is just for your reports and Product ID is the value
we’ll be searching for in the app. As mentioned above, you might
use a value that maps back to your accounting system.

Warning: When you click Save here, you are creating a record that cannot
be deleted. You can amend it, but not remove it. I give you this warning only
because in learning about this, I cluttered up one of my apps and now will feel the shame forever.

Shame!

Off to Xcode

import UIKit
import StoreKit
import Freddy

class IAPProcessor: NSObject {
    static let shared = IAPProcessor()

    // Shared Secret for validation. This value comes from one of two 
    // places in iTunes Connect:
    // 
    // A) On your "My Apps", next to the plus, is an ellipsis (…). 
    //    In that menu, select 'In-App Purchase Shared Secret'.
    // B) On the In-App Purchases feature panel where you created your
    //    IAP record, on the right side of the list, is 'View Shared Secret'.
    
    public var sharedSecret = "**yours here**"
    public var productIdentifier : String = "**yours here**" { 
        didSet {
            searchForProduct()
        }
    }

    // We store the callback for the delegate calls
    var completionBlock: ((Bool, String?, Error?) -> Void)?
    
    let paymentQueue = SKPaymentQueue.default()
    var product : SKProduct?
    
    var runningRequest : SKProductsRequest?

    public var availableForPurchase : Bool {
        return !products.isEmpty
    }

    public func startListening() {
        paymentQueue.add(self)
        searchForProduct()
    }    
}

Also, go over to the App Delegate and add this line to the bottom of ...didFinishLaunching...:

         IAPProcessor.shared.startListening()

The payment queue is the primary interface into
StoreKit. You send in your purchases, and then at some later point, you get
notified of transaction updates. The payment queue’s add() method is
overloaded such that it can take an SKPaymentTransactionObserver or an
SKPayment representing your purchase request.

We create this object and call startListening() on it. This attempts to add
our IAPProcessor to the payment queue.
Since we’re neither a payment or a transaction observer, Xcode will now
begin complaining. We’ll become an observer down below. First…

Finding Products to Purchase

Before we offer our diamond shield up for purchase, we should probably make
sure that it’s available for sale. We do that for searching by product
identifier. The search is encapsulated in a SKProductsRequest that is
meant to search for an entire array of product identifiers at one time. There
are code examples available that handle multiple, but for simplicity, we’re
asking for one, and expecting one reply.

extension IAPProcessor : SKProductsRequestDelegate, SKRequestDelegate {
    func searchForProduct() {
        let productIdentifiers = Set([productIdentifier])
        let productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productsRequest.delegate = self;
        productsRequest.start()
        runningRequest = productsRequest
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        NSLog("Request fail (error)")
        runningRequest = nil
    }
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        let productsFromResponse = response.products
        NSLog("Search for Products received response, (productsFromResponse.count) objects")

        if let product = productsFromResponse.first {
            self.product = product
        }
        runningRequest = nil
    }
}

In this block of code, we encapsulate the search logic. Since we are only
searching for one productIdentifier, we know we’ll get back one record or none
at all. The SKProductRequest constructor takes a Set containing only our
identifier, and start() kicks off the product search as a background job.
Also, we must retain the request, or it is deallocated before it begins.

So once we have our SKProduct object, we can ask the user to buy one! If he
does, here’s how we request the purchase, as well as restore them:

    public func purchase(completion: @escaping (Bool, String?, Error?) -> Void) {
        if let product = product {
            if SKPaymentQueue.canMakePayments() {
                completionBlock = completion
                let payment = SKPayment(product: product)
                paymentQueue.add(payment);
            } else {
                completion(false, "User cannot make payments", nil)
            }
        } else {
            completion(false, "Product not found.", nil)
        }
    }

    public func restorePurchases(completion: @escaping (Bool, String?, Error?) -> Void) {
        self.completionBlock = completion
        paymentQueue.restoreCompletedTransactions()
    }

The canMakePayments() method reports on whether the device has been restricted
from In-App Purchases. This is done in Settings > General > Restrictions. In
your UI, you should disable your purchase controls and provide alternate
messaging if this returns false.

When we add our payment to the payment queue, our app will be overlaid by system
dialogs completing the in-app purchase. The user may cancel the transaction or
complete it; we’ll find out later, in the SKPaymentTransactionObserver
delegate calls. Let’s implement those now:

extension IAPProcessor : SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        NSLog("Received (transactions.count) updated transactions")
        var shouldProcessReceipt = false
        for trans in transactions where trans.payment.productIdentifier == self.productIdentifier {
            switch trans.transactionState {
            case .purchased, .restored:
                shouldProcessReceipt = true
                paymentQueue.finishTransaction(trans)
            case .failed:
                NSLog("Transaction failed!")
                if let block = completionBlock {
                    block(false, "The purchase failed.", trans.error)
                }
                paymentQueue.finishTransaction(trans)
            default:
                NSLog("Not sure what to do with that")
            }
        }
        if(shouldProcessReceipt) {
            processReceipt()
        }
    }

At this point, Apple has completed the purchase transaction and written the
receipt data to the location in Bundle.main.appStoreReceiptURL. For consumable
and non-consumable item purchases, you can implement a processReceipt() that
looks like this:

    func processReceipt() {
        completionBlock?(true, productIdentifier, nil)
    }

Your code might put an annotation in your database, or in the UserDefaults, or
some other place. That’s it; we’re done with code. Jump down to the testing
section.

If you’re doing subscriptions, there is just a bit more to be done…

Validating Receipts with Apple

There are a number of reasons to validate the receipt with Apple, but chief
among them is to find out the expiration date of the product your user just
purchased. Many different configurations are possible. Not only may they be
recurring, but there are also several options for free trial periods.

For many services – say, your fitness program or other content-driven app – it
may make more sense to validate those receipts on the server. But if you have no
backend, or your backend is just storing your content, then you’re going to have
to validate the receipt yourself.

extension IAPProcessor {
    func processReceipt() {
        if let receiptURL = Bundle.main.appStoreReceiptURL, 
            FileManager.default.fileExists(atPath: receiptURL.path) {
        
            expirationDateFromProd(completion: { (date, sandbox, error) in
                if let error = error {
                    self.completionBlock?(false, "The purchase failed.", error)
                } else if let date = date, Date().compare(date) == .orderedAscending {
                    self.completionBlock?(true, self.productIdentifier, nil) 
                }
            })
        } else {
            let request = SKReceiptRefreshRequest(receiptProperties: nil)
            request.delegate = self
            request.start()
        }
    }
}

Apple doesn’t tell you, the app developer, whether a given purchase is a sandbox
one or not. According to their Receipt Validation Programming Guide, you can find out if a purchase is a sandbox one by making a request to their production receipt validator; if it fails with a status code of 21007, it’s a sandbox purchase.
Therefore we need to set up our code to check production, and on failure, to
then check the sandbox.

Thankfully, the only difference is the URL, so it’s simple enough to reuse the
parsing code between them. We are using Freddy for our JSON parser, but it works just fine with
JSONSerialization.

My apologies, but it’s a bit of a code-dump. It’s pretty routine stuff.

enum RequestURL: String {
    case production = "https://buy.itunes.apple.com/verifyReceipt"
    case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
}

extension IAPProcessor {
    func expirationDateFromProd(completion: @escaping (Date?, Bool, Error?) -> Void) {
        if let requestURL = URL(string: RequestURL.production.rawValue) {
            expirationDate(requestURL) { (expiration, status, error) in
                if status = 21007 {
                    self.expirationDateFromSandbox(completion: completion)
                } else {
                    completion(expiration, false, error)
                }
            })
        }
    }
    
    func expirationDateFromSandbox(completion: @escaping (Date?, Bool, Error?) -> Void) {
        if let requestURL = URL(string: RequestURL.sandbox.rawValue) {
            expirationDate(requestURL) { (expiration, status, error) in
                completion(expiration, true, error)
            }
        }
    }

    func expirationDate(_ requestURL: URL, completion: @escaping (Date?, Int?, Error?) -> Void) {
        guard let receiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: receiptURL.path) else {
            NSLog("No receipt available to submit")
            completion(nil, nil, nil)
            return;
        }
        
        do {
            let request = try receiptValidationRequest(for: requestURL)
            
            URLSession.shared.dataTask(with: request) { (data, response, error) in
                var code : Int = -1
                var date : Date?
                
                if let error = error {
                    if let httpR = response as? HTTPURLResponse {
                        code = httpR.statusCode
                    }
                } else if let data = data {
                    (code, date) = self.extractValues(from: data)
                } else {
                    NSLog("No response!")
                }
                completion(date, code, error)
            }.resume()
        } catch let error {
            completion(nil, -1, error)
        }
    }
    
    func receiptValidationRequest(for requestURL: URL) throws -> URLRequest {
        let receiptURL = Bundle.main.appStoreReceiptURL!
        let receiptData : Data = try Data(contentsOf:receiptURL)
        let payload = ["receipt-data": receiptData.base64EncodedString().toJSON(),
                       "password" : sharedSecret.toJSON()]
        let serializedPayload = try JSON.dictionary(payload).serialize()
        
        var request = URLRequest(url: requestURL)
        request.httpMethod = "POST"
        request.httpBody = serializedPayload

        return request
    }
    
    func extractValues(from data: Data) -> (Int, Date?) {
        var date : Date?
        var statusCode : Int = -1
        
        do {
            let jsonData = try JSON(data: data)
            statusCode = try jsonData.getInt(at: "status")

            let receiptInfo = try jsonData.getArray(at: "latest_receipt_info")
            if let lastReceipt = receiptInfo.last {
                let formatter = DateFormatter()
                formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
                date = formatter.date(from: try lastReceipt.getString(at: "expires_date"))
            }
        } catch {
        }

        return (statusCode, date)
    }
}

Automatically Recurring Payments

This one is pretty easy. Remember how we set up the listener in the App
Delegate? That was because if a payment occurred while the app was not running,
the transaction will be delivered right after the app comes up. You should treat
this the same way and update your expiration dates.

Testing

Once you’ve got this code all lined up, and you’ve received confirmation from
Apple that they’ve done their bit, you should begin to see your SKProduct
objects in the simulator. But you can’t purchase them yet, because you don’t
have a sandbox user.

Let’s go back to iTunes Connect, and go to the Users and Roles module:

Users

There are three tabs here:

  • iTunes Connect Users, which are real iTunes accounts of people on your team.
  • TestFlight Beta Testers, which are real iTunes accounts that can access your builds via the TestFlight app.
  • Sandbox Testers, which are your IAP test accounts. These must correspond to real e-mail addresses that you control, but must not exist anywhere else
    in Apple’s ecosystem.

It’s vitally important that you don’t mess this one up: If you try to
log in to any other Apple service with a sandbox ID, it permanently borks
up that ID. You’ll have to toss it and start over. We recommend here that
you create a new e-mail address specifically for this (eg. my_app_sandbox_1337@gmail.com) and use that
address for your Sandbox user. You will need to click a link in the email to
activate, so make sure it’s a real email address. (It is also possible to do plus-based email segmentation, ie. myaddress+thisapp@mycompany.com.)

In order to see the builds in the TestFlight app, however, your device needs to
be logged in to one of the accounts shown on the second tab. Thus the testing
cycle becomes a bit of a hassle, going something like this:

  • Increment your version and/or build number.
  • Archive and upload your build.
  • In iTunes Connect, select the build for Internal Testing.
  • Log into the store on your device as one of your internal testers.
  • Launch TestFlight and install the new build.
  • Log out of the iTunes Store.
  • Run your build and attempt your purchase.
  • When prompted, log in with your Sandbox credentials.
  • After testing, in Settings, log your sandbox credentials out of the app store.

Kind of ugly, isn’t it? Good reason to have a second device handy just for
testing, too. Who wants to log their personal phone out of all that stuff?

Before going down the hole of device testing,
you can be heartened by the news that the simulator now supports IAP. For the
most part, you can proof out your process there. You should leave the
account blank in the simulator’s Settings and force the app to sign you in.

When you do distribute builds to external testers via TestFlight, your app
will be reviewed by Apple. This review tends to be fairly cursory, not on
the order of an App Store review.

One last note: Automatically recurring subscriptions in the sandbox run on a compressed schedule
and auto-expire after six transactions, allowing you to observe the complete
lifecycle.

Off to the App Store

When you’re ready to go and you’ve tested your work not just in the simulator
but also on a device, you’re ready to put it in the store. Each new IAP must be
submitted to the store with a new app version, which should make sense. Your app
will be reviewed and then your purchase will be reviewed separately. You will
need to give directions on how to invoke the purchase.

After you’re reviewed and approved, you can launch. Now go make that money!

Daffy

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project