Raise your hand if you wanna get paid.

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.)

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.

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.

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.

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.

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:

There are three tabs here:

iTunes Connect Users , which are real iTunes accounts of people on your team.

, 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.

, 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!