In-App Purchases Tutorial: Getting Started

Ray Fix
Get started with in-app purchases to grow your app revenue!

Get started with in-app purchases to grow your app revenue!

Update note: This tutorial was updated for iOS 8 and Swift by Ray Fix. Original tutorial was by site editor-in-chief Ray Wenderlich.

One of the great things about being an iOS devleoper is that there are a variety of models you can use to make money off of your apps, including paid, free with ads, and in-app purchases.

In-app purchases are a particularly compelling option, for several reasons:

  • You can earn more money than just the price of your app. Some users are willing to spend a lot more on extra content!
  • You can release your app for free (which is a no-brainer download for most people), and if they enjoy it they can purchase more.
  • Once you have it implemented, you can keep adding additional content to the same app (rather than having to make a new app to earn more money!)

You can use In-App Purchases with varying business models. For example, the Ray Wenderlich app Wild Fables comes with three stories included with more available as in-app purchases. Battle Map 2 is an example of a paid app with optional extra content as in-app purchases.

In this tutorial, you’ll learn how to use in-app purchases to unlock local content embedded in your app.

This tutorial assumes that you are familiar with basic Swift and iOS programming concepts. If these concepts are new to you, check out some of the other tutorials on this site.

Getting Started

For this tutorial, you’re going to make a little app called In App Rage. The app allows users to buy rage comics, sometimes called “F7U12”. Readers of this site will undoubtedly recognize the genre. They’re basically funny little comics where someone goes through a common and frustrating situation, resulting in a wild rage or other humorous expression.

noautocom

Before you can start coding, you’ll need to create a placeholder app in the iOS Developer Center and iTunes Connect.

First, log into the iOS Developer Center. Select Identifiers under iOS Apps and then select the App IDs tab. Click the + button and complete the form like the following:

Dev_Portal_AppID

You must change the bundle identifier to have your own unique prefix. A common practice is to use your domain name in reverse. If all fails use a made-up one based on your name or something else unique.

Notice that In-App Purchase (and GameKit) are enabled by default. When you’re done, click Continue and then Submit. Viola – you have a new App ID! Now you’ll use it to create a new app in iTunes Connect.

Log onto iTunes Connect, click My Apps then + to add a new iOS App. If you’re prompted to choose an app type, select New iOS App (obviously). Then complete the form as shown below:

InApp_ITC_Create

If you are quick in getting to this step, you might notice that the Bunde ID is not showing up in the dropdown list. Apparently, this takes time to propagate. Take this opportunity to obey your Apple Watch, stand up, and walk around the block. Refresh the page when you get back and hopefully it will be there.

Also you’ll have to tweak the app Name, because app names need to be unique across the App Store. I’ve added an entry for this one. Maybe replace the RAF with your own initials.

Managing In App Purchases

The reason you just created a placeholder app is that before you can code in-app purchases, you have to set them up in iTunes Connect. Now that you have a placeholder app, just click In-App Purchases, as shown below:

new_app

Then click Create New in the upper left corner:

The Create New button for In-App Purchases

You will get to a screen that lets you select the type of In-App Purchase you want to add. Note that there are are two types of frequently-used In-App Purchases:

  • Consumables: can be bought more than once and can be used up. Like extra lives, in-game currency, temporary power-ups, and the like.
  • Non-Consumables. Something that you buy once, and expect to have it permanently. Things like extra levels, unlockable content, etc.

For In App Rage, you are going to be selling comics. Once the user purchases them, they should always have them, so choose Non-Consumable.

nonconsume

Note: Any Non-Consumable purchase should be available to a user on any device they have. You don’t get to charge a user twice if they have two devices!

We’ll talk more about how to allow the user to restore the non-consumable content they purchased on other devices later.

There is no such requirement for consumables though – consumables can be for just the device the user bought them on. If you want consumables to be cross-device, you’d have to implement that yourself with iCloud or some other technology.

Next, you will be taken to a page to enter some information about your in-app purchase. Fill in the fields according to the screenshot below:

InApp_ITC_NightlyRage

Let’s cover what each of these fields means:

  • Reference Name: This is what shows up in iTunes Connect for this in-app purchase. It can be whatever you want since you won’t see it anywhere in the app.
  • Product ID: Also known as “product identifier” in the Apple docs, this is the unique string that identifies your in-app purchase. Usually it’s best to start out with your Bundle ID, and then append a unique name for the purchase. In order for the sample code in this tutorial to work well without modification, you will need to use a certain naming convention discussed below.
  • Cleared for Sale: If this in-app purchase is OK for users to purchase as soon as the app becomes available.
  • Price Tier: How much this in-app purchase should cost.

After you’ve set that up, scroll down to the Language section and click Add Language. Fill out the form that pops up with the following information:

InApp_ITC_Language

This information will be returned to you when you query the App Store later on for the in-app purchases that are available. Prices will be in the correct currency for the part of the world you are selling in and you can also enable/disable these purchases on the fly. Don’t worry about the descriptions – you won’t be using them in this tutorial, so you can just use the Display Name for those.

You will also notice that there are fields for review Notes and Screenshots. While ultimately you will need to provide these for Apple’s review process, you do not need them for testing in the sandbox.

In order for the sample app to work without changes, your Product ID should take the form “YYYYY.XXXXX” where YYYYY is your unique name (mine was org.rayfix.inapprage) and XXXXX is the name of the image to be displayed. The names are: nightlyrage, girlfriendofdrummer, iphonerage, and updog.

You can now click Save. Great you just created your first In-App purchase. Now, repeat the process three more times for the remaining purchases. When you’re done, your purchases should look like this:

alliaps

You might notice that this process takes a while. I could imagine it gets annoying if you have a ton of in-app purchases in your app. Luckily you’re not in that situation, If you are in your app, draw me a rage comic :]

Quick Tour of the Starter Project

Download the starter project, unzip it and open in Xcode. Open MasterViewController.swift. This class imports StoreKit and displays a table view of available in-app purchases. Purchases are stored in an array of SKProduct objects. Each row (if not purchased) has a “Buy” button that lets you purchase the product. An NSNumberFormatter is used to show the localized price. Once purchased, you can view the comic for that purchase using the detail view. Finally, a “Restore” button lets you restore all previous purchases.

You will notice that MasterViewController.swift is using an object called RageProducts.store of type IAPHelper to do the heavy lifting. However, this class is currently stubbed out. If you build and run the app you will not run at this point.

Matching the Identifiers

For anything to work, you need to match up the bundle identifier and product identifiers in your app to the same ones you entered in iTunes Connect.

Select your project target in Project Navigator and then the General tab Change the value of Bundle Identifier to match. I used “org.rayfix.inappragedemo” but yours will be different.

inappragedemo_xcodeproj_and_iOS_Simulator_-_iPhone_5_-_iPhone_5___iOS_8_3__12F69_

Next change the product identifiers to match what you entered. Open RageProducts.swift and notice the list of four in-app purchases.

You probably only need to change the Prefix constant to match what you used in the previous sections. (It is marked with the TODO comment.)

Note: Many recommend that you pull the list of product identifiers from a web server so you can add new in-app purchases dynamically rather than requiring an app update. For this tutorial, you’re going to keep things simple and just hard-code in the product identifiers for this app.

Listing In-App Purchases

The RageProduct.store is an instance of IAPHelper. This object interacts with the StoreKit API to list and perform purchases. Open IAPHelper.swift and notice that is not yet implemented. You will now do so.

The first thing you need to do is get a list of in-app purchases from Apple’s server. Add the following private properties to IAPHelper class.

/// MARK: - Private Properties
 
// Used to keep track of the possible products and which ones have been purchased.
private let productIdentifiers: Set<ProductIdentifier>
private var purchasedProductIdentifiers = Set<ProductIdentifier>()
 
// Used by SKProductsRequestDelegate
private var productsRequest: SKProductsRequest?
private var completionHandler: RequestProductsCompletionHandler?

You will use these properties to perform your requests and keep track of what purchases have already been made. When you add this code you will immediately see a compiler error in init(productIdentifiers:). This is because the initialization rules of Swift dictate that you must initialize all class properties before calling super.init(). Fix that by adding the following to init(productIdentifiers:):

self.productIdentifiers = productIdentifiers

An IAPHelper is created by passing in the set of product identifiers supported. This is how RageProducts creates its store instance. Next, replace the implementation of requestProductsWithCompletionHandler(_:)

/// Gets the list of SKProducts from the Apple server calls the handler with the list of products.
public func requestProductsWithCompletionHandler(handler: RequestProductsCompletionHandler) {
	completionHandler = handler
	productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
	productsRequest?.delegate = self
	productsRequest?.start()
}

This code saves away the user’s completion handler so that it can be executed later. It then creates a request and fires the request off to Apple. Since IAPHelper does not yet conform to the SKProductsRequestDelegate protocol, you’ll see another compiler error. Fix that by adding the following IAPHelper extension at the end of the file:

// MARK: - SKProductsRequestDelegate
 
extension IAPHelper: SKProductsRequestDelegate {
  public func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
    println("Loaded list of products...")
    let products = response.products as! [SKProduct]
    completionHandler?(success: true, products: products)
    clearRequest()
 
    // debug printing
    for p in products {
      println("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    }
  }
 
  public func request(request: SKRequest!, didFailWithError error: NSError!) {
    println("Failed to load list of products.")
    println("Error: \(error)")
    clearRequest()
  }
 
  private func clearRequest() {
    productsRequest = nil
    completionHandler = nil
  }
}

This extension is used to get a list of products, their titles, descriptions and prices from Apple`s server by implementing the two methods required by the SKProductsRequestDelegate protocol.

productsRequest(_:didReceiveResponse:) is called when the list is succesfully retrieved. It receives an array of SKProduct objects and passes them to the previously saved completion handler. The handler reloads the table with new data. If a problem occurs, productsRequest(_:didFailWithError:) is called. In either case, when the request finishes you clear the request and completion handler with clearRequest().

Build and run now. You should now see a list of products in the table view. This part should work even in the simulator without a sandbox account.

InApp_ProductList

Didn’t work? If this didn’t work for you, there are a number of things to check (this list courtesy of itsme.manish and abgtan from the forums:

  • Go to Settings\iTunes & App Stores, log out of any account, and try again so you’re sure you’re using a Sandbox account.
  • Does your project’s Bundle ID match your App ID?
  • Check this link – if it doesn’t respond, the iTunes sandbox may be down.
  • Have you enabled In-App Purchases for your App ID?
  • Are you using the full product ID when when making an SKProductRequest?
  • Have you waited several hours since adding your product to iTunes Connect?
  • Are your bank details active on iTunes Connect?
  • Have you tried deleting the app from your device and reinstalling?

Tried all that and still stuck? Try the old forum thread or this thread’s comments for discussion with other readers.

Purchased Items

You want to be able to determine which items are already purchased. To do this you will use the purchasedProductIdentifiers property you added. If a product identifier is contained in this set, the user has purchased the item. The method for checking this is straightforward. Find the isProductPurchased(_:) function and replace the implementation with the following:

return purchasedProductIdentifiers.contains(productIdentifier)

Every time your app starts you don’t want to have to go to Apple’s server to find out if a particular purchase has been made. It is a good idea to save this information locally. You will use NSUserDefaults to save purchasedProductIdentifiers. Add the following before the call to super in your init(productIdentifiers:) method:

for productIdentifier in productIdentifiers {
	let purchased = NSUserDefaults.standardUserDefaults().boolForKey(productIdentifier)
	if purchased {
		purchasedProductIdentifiers.insert(productIdentifier)
		println("Previously purchased: \(productIdentifier)")
	} else {
		println("Not purchased: \(productIdentifier)")
	}
}

For each of your product identifiers, you check to see if the value is stored in NSUserDefaults and if it is, you insert it into the set. Later, you’ll also add an identifier to the set after a purchase is made.

Making Purchases (Show Me The Money!)

That’s great, but you need to be able to make purchases. How do you do that? That is what you will implement next. Still in IAPHelper.swift, replace the purchaseProduct(_:) implementation with the following:

/// Initiates purchase of a product.
public func purchaseProduct(product: SKProduct) {
	println("Buying \(product.productIdentifier)...")
	let payment = SKPayment(product: product)
	SKPaymentQueue.defaultQueue().addPayment(payment)
}

This creates a payment object using a SKProduct (which you got from the server) to add to a payment queue. There’s a singleton SKPaymentQueue object called defaultQueue(). Boom! Money in the bank!

How do you know if the payment went through? For that, you need your IAPHelper to observer transactions happening on the SKPaymentQueue. Go back to your init(productIdentifiers:) method and add the following line to the end of the function, right after super.init().

SKPaymentQueue.defaultQueue().addTransactionObserver(self)

This results in a compiler error because IAPHelper needs to conform to the SKPaymentTransactionObserver protocol. If you think of the compiler as a helpful todo list generator, this is the next item on the list!

Go to the end of the file and add the following extension and methods:

extension IAPHelper: SKPaymentTransactionObserver { 
  /// This is a function called by the payment queue, not to be called directly.
  /// For each transaction act accordingly, save in the purchased cache, issue notifications,
  /// mark the transaction as complete.
  public func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
    for transaction in transactions as! [SKPaymentTransaction] {
      switch (transaction.transactionState) {
      case .Purchased:
        completeTransaction(transaction)
        break
      case .Failed:
        failedTransaction(transaction)
        break
      case .Restored:
        restoreTransaction(transaction)
        break
      case .Deferred:
        break
      case .Purchasing:
        break
      }
    }
  }
 
  private func completeTransaction(transaction: SKPaymentTransaction) {
    println("completeTransaction...")
    provideContentForProductIdentifier(transaction.payment.productIdentifier)
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  }
 
  private func restoreTransaction(transaction: SKPaymentTransaction) {
    let productIdentifier = transaction.originalTransaction.payment.productIdentifier
    println("restoreTransaction... \(productIdentifier)")
    provideContentForProductIdentifier(productIdentifier)
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  }
 
  // Helper: Saves the fact that the product has been purchased and posts a notification.
  private func provideContentForProductIdentifier(productIdentifier: String) {
    purchasedProductIdentifiers.insert(productIdentifier)
    NSUserDefaults.standardUserDefaults().setBool(true, forKey: productIdentifier)
    NSUserDefaults.standardUserDefaults().synchronize()
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperProductPurchasedNotification, object: productIdentifier)
  }
 
  private func failedTransaction(transaction: SKPaymentTransaction) {
    println("failedTransaction...")
    if transaction.error.code != SKErrorPaymentCancelled {
      println("Transaction error: \(transaction.error.localizedDescription)")
    }
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  }
}

That is a lot of code! So let’s go through it in detail. paymentQueue(_:updatedTransactions:) is the only method actually required by the protocol. It gets called when one or more transactions’ states change. This method goes through an array of updated transactions and looks at their state. Based on that state it calls other methods defined here: completeTransaction(_:), restoreTransaction(_:) or failedTransaction(_:).

If the transaction was completed or restored, it adds to the set of purchases and saves the identifier in NSUserDefaults. It also posts a notification with that transaction so that any interested object in the app can listen for it to do things like update the user interface. Finally, in both the case of success or failure, it marks the transaction as finished.

Restoring Payments

If the user deletes and re-installs the app, or if they install it on another device, they need to be able to recover their purchases. In fact, Apple may reject your app if you do not implement the ability to restore non-consumable purchases.

You are already listening for when purchases have been restored. But you need to write the method that initiates it. Find the restoreCompletedTransactions() method and add the following to it:

SKPaymentQueue.defaultQueue().restoreCompletedTransactions()

That was almost too easy! You’ve already set the transaction observer and implemented the method to handle restoring transactions in the previous step.

In App Purchases, Accounts, and the Sandbox

While you’re running your app in Xcode, you’re not making transactions against the real in-app purchase servers – you’re running against sandbox servers.

This means you can buy things without fear of getting charged, etc. But you need to set up a test account, and also make sure you’re logged out of the store with your real account if you’re testing on device.

To make accounts, log onto iTunes Connect and click Users and Roles. Click Sandbox User then follow the buttons to create a test user.

Then go to your iPhone and make sure you’re logged out of your current account. To do this, go to the Settings app and tap iTunes & App Store. Tap your iCloud account name, and then select Sign Out.

Finally, go ahead and build and run your app and attempt to purchase a rage comic. Enter your test user account information and if all goes well, it should purchase with a happy check mark next to it. You can tap on it and see the comic! The list of purchases should look like this:

InApp_Final

Payment Permissions

Some devices and accounts may not permit in-app purchase. This can happen, for example, if parental controls are set to disallow it. Apple requires that you handle this situation gracefully; not doing so will likely result in an app rejection.

Open IAPHelper.swift and add the following method to the class:

public class func canMakePayments() -> Bool {
	return SKPaymentQueue.canMakePayments()
}

When canMakePayments() is false, your master view controller should display cells differently. For example, don’t show a “Buy” button, and simply say “Not Available” instead of listing the price.

To do this, open MasterViewController.swift and update the implementation of tableView(_:cellForRowAtIndexPath) as follows:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
	let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
 
	let product = products[indexPath.row]
	cell.textLabel?.text = product.localizedTitle
 
	if RageProducts.store.isProductPurchased(product.productIdentifier) {
		cell.accessoryType = .Checkmark
		cell.accessoryView = nil
		cell.detailTextLabel?.text = ""
	}
	else if IAPHelper.canMakePayments() {
		priceFormatter.locale = product.priceLocale
		cell.detailTextLabel?.text = priceFormatter.stringFromNumber(product.price)
 
		var button = UIButton(frame: CGRect(x: 0, y: 0, width: 72, height: 37))
		button.setTitleColor(view.tintColor, forState: .Normal)
		button.setTitle("Buy", forState: .Normal)
		button.tag = indexPath.row
		button.addTarget(self, action: "buyButtonTapped:", forControlEvents: .TouchUpInside)
		cell.accessoryType = .None
		cell.accessoryView = button
	} else {
		cell.accessoryType = .None
		cell.accessoryView = nil
		cell.detailTextLabel?.text = "Not Available"
	}
	return cell
}

This implementation will make the display more appropriate in the case where payments cannot be made with the device.

And there you have it – an app with in-app purchase!

Where To Go From Here?

Here is In App Rage Final.zip with all of the code you’ve developed above. Feel free to re-use the in-app purchase helper class.

One shortcoming of the sample app is that it doesn’t indicate to the user when it is communicating with Apple. A possible improvement would be to display a spinner or HUD control at appropriate times. This UI enhancement, however, is beyond the scope of this tutorial.

In-app purchases can be an important part of your business model – use them wisely and be sure to follow the guidelines about restoring purchases and failing gracefully, and you’ll be well on your way to success!

If any of you have questions or comments about this tutorial please join the forum discussion below!

And to end things off with a laugh, here’s a great iOS app rage comic made by Jayant C Varma from the original Objective-C version of this tutorial! :]

Coming Up With An iOS App Idea Rage Comic by Jayant Varma

Ray Fix

Ray Fix is a co-founder of Pelfunc, Inc., a small startup in San Diego California that, in addition to iOS and computer vision consulting, is working on new ways for people to communicate. Currently he's developing a platform called Squeak16 to do this.

Ray stays healthy by walking, swimming and playing ultimate frisbee. When he is not doing one of those things, he is writing and dreaming of code in Swift, C++, Ruby, or R.

User Comments

14 Comments

  • This is a great topic for a tutorial and everyone who wants to charge customers for something should read it. That being said, I did this tutorial back when Ray wrote the original one. I wish someone would've told me this at that time, once you enter this fake app into iTunes Connect it will be there forever because you can't just delete it when you're done. I still have the fake app sitting in my iTC, forever staring at me whenever I log in.

    The only way you can delete it from iTC is to submit some sort of binary for it (a binary you have no intention of using). Once it's been uploaded, you can then reject the app/binary and this will remove it from iTC. At least, that's what I've been told. I haven't tried this but I'm getting so tired of seeing this app on iTC that I might just do it to get rid of it.
    fbara
  • @fbara: to completely remove an app from iTC, you first need to submit the app to the Store, which means it has to pass validation. This is pretty dumb and I really hope they do something about this in the future...
    RobinD
  • An easy fix for that problem, @fbara and @ RobinD, is just develop a real app of your own and just use the same Bundle ID. That way you won't need to reject the dummy app and then remove from iTunes Connect. Instead you would develop and publish a real app just using the same Bundle ID from the one you created from this tutorial.
    malmeida
  • Hey, got a problem with the tutorial. I'm getting a 'IAPHelper' does not conform to protocol 'SKPayment TransactionObserver' and can't figure out how to correct it.
    I opened the completed one from you and get the same error.

    Is there something different from swift 2.0 that affects the SKPayment TransactionObserver?
    Lambo116
  • Figured it out.
    You will need to change the paymentQueue to: public func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) -> Void {}
    Lambo116
  • Hi

    Don't you think "success" is redundant in this code? Or I missed something?

    RequestProductsCompletionHandler = (success: Bool, products: [SKProduct]) -> ()
    beemol
  • Can anyone provide a quick code example on how to use the enum provided within the example code to load the IAP product identifiers from a JSON feed? I know how to get them from the JSON feed NSURLSession, but I don't understand to get them within an enum. Thanks.
    Hynds
  • Excellent tutorial. The IAPHelper class is very helpful. ;)

    Is there some way to register an NSNotification or something so that if the user taps restore, but the restore fails, I can then show them an alert telling them that the restore failed and they should double-check their current Apple ID?

    Or do we need to do check if a receipt exists and validate it in order to determine that a purchase has never been made and can't be restored?
    blwinters
  • How do you check for a specific NSUserDefault being set on another view that pertains to the purchases that have been made via the IAPHelper?

    Do you check for the "Product ID" that you set in iTunes Connect, as the name of a boolean?
    dmcknight
  • I want to load products from my php server. it is possible to load items from php server then let user buy in-app-purchase? because I want to add more product and delete product dynamically from my server.
    rohim-dev
  • Downloaded it; updated it to the latest Swift code and tested it. Does not display anything on the table view and the restore button never finds my apple ID.
    Fermats_Last_Account
  • I think iTunes Connect has changed.

    When I go to iTunes Connect/My Apps I don't get the header shown above but I can select it from the left side but then I get this cryptic paragraph:

    "Your first In-App Purchase must be submitted with a new app version. Select it from the apps In-App Purchases section and click Submit."

    I don't understand how to continue and am dead in the water.
    Sojourner9
  • Ok, fixed the problem. The layout of My App in iTunes Connect has changed but what I was missing was I needed to create a sandbox test account. Then I could select various types of in-App Purchases.
    Sojourner9
  • Great tutorial! Had a few issues with Swift 2, but worked them out one by one.

    Once I added the code for the tableview to handle if the user is able to buy an in-app purchase, my talbleview goes blank in the simulator. Does not load any content to the tableview at all. Another issue to work through.

    Thanks again for a great tutorial!!
    evision

Other Items of Interest

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Unity Starter Kit!

We are considering writing a new starter kit on making a game in C# with Unity.

Would this be something you'd be interested in, and if so which option would you prefer?

    Loading ... Loading ...

Our Books

Our Team

Video Team

... 12 total!

Swift Team

... 10 total!

iOS Team

... 52 total!

Android Team

... 9 total!

OS X Team

... 11 total!

Sprite Kit Team

... 10 total!

Unity Team

... 9 total!

Articles Team

... 10 total!