Research Group for Applied Software Engineering
Forschungsgruppe für Angewandte Softwaretechnik

iOS Store Kit Framework / In-App Purchases Tutorial

by Michael Beyer for TU München's Chair for Applied Software Engineering Seminar: Games Development with iOS (WS 13/14).

 

This is to be used along the StoreKitPresentationExtended.pdf, a more detailed presentation as the one used during the course.
 

Introduction

In-App Purchase allows you to embed a store inside your app using the Store Kit framework. The framework connects to the AppStore on your app’s behalf to securely process payments from users, prompting them to authorize payment.

The framework then notifies your app, which provides the purchased items to users.

You should use In-App Purchase to collect payment for additional features and content.

StoreKit is the In-App Purchase Payment System – it manages transactions for In-App Purchases.

 

Motivation

For example, using In-App Purchase, you can implement the following scenarios:

  • a basic version of your app with additional, paid, premium features
  • a magazine app that lets users purchase and download new issues
  • a game that offers new environments (levels) to explore
  • an online game that allows players to purchase virtual property (money, etc.)


But there are some more reasons for developers to implement In-App Purchases:

  • Money: you can earn more money than just one time the price of your app! Some users are willing to spend a lot more on extra content („freemium“ strategy).
  • Market Share / New Business Models: you can release your app for free, which is a no-brainer download for most people → more downloads / users reached. If users enjoy your app, they can purchase more in-app functionality.
  • Easy Maintenance: you can keep adding additional content in the future to the same app, rather than having to make a new app to earn more money.

 

To summarize, there is a quote from Jens Begemann, Co-Founder and CEO of Wooga you might want to keep in mind: 

„It allows us to reach many people who otherwise have a fear to purchase games.“
 

Example applications

Lets take a look at the top grossing apps on the AppStore, the apps with the strongest sales. Note that there are a lot of free apps (44 out of top 50, 88%) with the strongest sales
This is only possible since In-App Purchases are considered.

iTunes 

Overview

In-App Purchase is implemented using the StoreKit API, introduced with iOS 3.0 and Mac OS 10.7 (In-App Purchase works for Mac Apps too).

StoreKit communicates with the AppStore on behalf of your application: it prompts for payment and securely authorizes transaction.

Your application uses StoreKit to receive information from the AppStore about products you want to offer in your application. Your application displays this information to users and allows them to purchase items.

 

StoreKit

 

The StoreKit implementation can be generally devised into 3 main parts.

  • First: the set up of products done in iTunes Connect and Xcode
  • Second: the purchase process itself on the device.
  • Third: verify the purchase.

Long story short: StoreKit is the In-App Purchase Payment System.

 

It follows a certain logical flow:

  • First two steps: catch the products identifiers from the server (which is iTunes in our case)
  • Second: purchase the product
  • Third: process the transaction
  • Fourth: make asset available
  • Last: never forget to finish transaction


StoreKit Flow

Implementation

To start, here is a short list of Do‘s and Don‘ts with In-App Purchases:

Do:

  • deliver your digital good or service within your app
  • and as for all apps: give users an added value.


Don‘t:

  • use In-App Purchase to sell real-world goods and services
  • offer items for purchase that contain, or relate to, pornography, hate speech, defamation ... same guidelines as for app admission!
  • In-App Purchase items cannot be shared across applications or platforms.

 

Now, let‘s look at the types of In-App Purchases!

Non-consumable

Non-consumable products are designed to be purchased once, comparable to a paid app paid once. You should consider these purchases as durable, persistent. They can be restored on all user‘s devices – this is managed by StoreKit. You as a developer are responsible for ensuring the purchase is present on all devices of the user using restoreCompletedTransactions method.

For example, non-consumable are used for:

  • new level in a game
  • additional feature in an app 
  • ad-freeness

 

Consumable

Consumable products are designed to be purchased multiple times by the user. There is no restore possible.

For example, consumable are used for:

  • items consumed as part of game play, i.e. power-ups, coins, etc.
  • virtual currency as a means of advancement (check AppStore admission guidelines).

 

Non-renewing subscription

Usually not implemented anymore, since Apple's documentation seems to suggest that they are deprecated in favor of auto-renewing subscriptions.

 

Auto-renewing subscription

Auto-renewing subscriptions enable true subscription support within your app, like for magazines, etc. They are automatically renewed at the end of the subscription period, unless the user resigns. New content delivered to all devices with same customer Apple ID.

We have several duration options: 7 days, 1 month, 2 months, 3 months, 6 months, 1 year.

There is an incentive for users to provide you their email address (like a free month of subscription or so) in order to accomplish customer relationship management, send him info mails, get in touch with him, etc.

Awesome features, requires awesome capabilities: it‘s the most complex In-App Purchase type to implement.

And as auto-renewing subscriptions are rather not that important to games, we won‘t implement them or go into detail with this kind of In-App Purchase.

 

iTunes Connect Metadata Setup

iTunes ConnectBriefly for those who don‘t know it yet: iTunes Connect is a suite of web-based tools created for developers to submit and manage their apps for sale via the App Store. You will be able to check the status of your contracts, manage test users, obtain sales and finance reports, view app crash logs, request promotional codes, set up iAd Network and Game Center preferences, as well as add or manage app metadata, binaries, and most important for us: In-App Purchases.

For this tutorial, we‘re going to make a little app called “BuyFruit” where people can buy some tasty (virtual) fruits :-)

Before you can even start coding, we need to create a placeholder app entry using iOS Developer Center and iTunes Connect. First, we need to create an App ID for the app.

In the iOS Developer Center‘s „Certificates, Identifiers & Profiles“ section, select the „Identifiers“ tab. Check if you‘re correctly in the „iOS App IDs“ section and click the + button on the upper right corner. Enter a description for the app, which is the the app‘s name. The App ID Prefix is predefined by your account. The App ID Suffix must be defined by you, and for using In-App purchases, an explicit (not wildcard) App ID is strongly recommended... use the reverse domain name format.

Important: this MUST match with your apps Bundle ID!

Scroll down, you‘ll see that In-App purchase is by default selected and cannot be changed ... so no mistakes possible here. Click continue. You‘ll get a confirmation screen.

 

So, that was the App ID part. Moving on, we need to create an In-App purchase item in iTunes Connect, consisting of:

  • the type (consumable, non-consumable, etc.),
  • a reference name,
  • the Product ID,
  • a display name and display description,
  • and of course: a price

 

Log into iTunes Connect with the Apple ID connected to your Apple Development Account. Select the „Manage Your Apps“ tab and click on „Add New App“ on the upper left corner. 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... your app will connect to iTunes Connect thru the StoreKit API.

Create an App with the app creation process in iTunes Connect. You‘ll be asked for information: First: enter an App Name, SKU number, and choose the Bundle ID (we just created for our App ID). Next: set the availability date as much as possible in the future, so that our unfinished app doesn‘t hit the AppStore by accident. Choose a price tier and click continue. Just put in placeholder information for now – you can change all of this later. But unfortunately, you have to fill out each and every field, including adding screenshots... just use a black screen, that‘s fairly enough.

 

So now that you have a placeholder app, just click on the “Manage In-App Purchases” button. We‘ll walk thru the In-App Purchases set up step by step. Click the upper left button „create new“. First: Select the In-App Purchase Type. For our tutorial‘s purposes, we‘ll choose a non-consumable In-App Purchase product.

Next, you will be taken to a page to enter some information about your In-App Purchase product. Fill in the fields. Select a price tier for the In-App purchase product. Give your product a name the users see and a description.

Add a few products... This is it!

You might wonder why this step is necessary... after all, you could embed this information in your app! Obviously Apple needs to know the price of your product. Also in the AppStore it displays some of this information (i.e. top in-app purchases). Finally, it might make things easier for you too, since it avoids having this information hard-coded into your app and allows you to enable/disable purchases on the fly.

 

StoreKit on iOS

The StoreKit on iOS section of today‘s session is divided in some sub-parts:

  • we will handle how to get product information into our app
  • process of purchasing 
  • we‘re gonna a short look into restoring completed transactions
  • very short: receipts

 

Now, let‘s start with our coding exercise. Open Xcode, create a new, empty, iOS project and name the project BuyFruit. The Bundle ID of you app MUST match the one in iTunes Connect!

For our first, very simple demo app, we can simply use an empty application template, we‘re programmatically gonna fill with a table view and a detail view into.

Next, we need to add the library required for In-App Purchases to our project: StoreKit. It‘s become pretty simple in Xcode 5.0 with iOS 7. Simply go to your project, select capabilities tab and active In-App Purchases.

Note that this only works, if you have your own Developer Account configured on your Mac.

We now create a helper class we will do most of our In-App Purchase configuration in. Name it IAPHelper and set it a subclass of a NSObject. IAPHelper stands for In-App Purchase Helper. As said, we will use the helper class we just created in order to manage all aspects of in-app purchases for us. Plus: you can easily re-used in other projects!

That‘s it for a first move.

Next: we want to get the product information and can finally start coding.

 

Getting product information

Before you can allow the user to purchase any products from your app, you must issue a query to iTunes Connect to retrieve the list of available products from the server, so that they can be displayed to the user.

Therefor, we first do a product request and than get back a product response.

Along with getting the list of products from the server, our IAPHelper helper class will also keep track of which products have been purchased or not. It will save the product identifier for each product that has been purchased in NSUserDefaults. 

In the IAPHelper.h add to the top:

// Block definition
typedef void (^RequestProductsCompletionHandler)(BOOL success, NSArray * products);

Blocks will let you define actions that take place in response to an event. All of the code for a feature is in one place (right where you set up the listener for that event), which makes maintenance easier. This can save a mess of code and make your application much more organized.

In the header file of IAPHelper add two methods:

initWithProductIdentifier:, which is an initializer that takes a list of product identifiers, such as com.domain.AppName.ProductName, etc.

requestProductsWithCompletionHandler:, a method to retrieve information about the products from iTunes Connect.

@interface IAPHelper : NSObject
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler;
@end

 

Next switch to IAPHelper.m and add the first part of the implementation.

#import "IAPHelper.h"
// You need to use StoreKit to access the In-App Purchase APIs, so you import the StoreKit here.
@import StoreKit;
NSString *const IAPHelperProductPurchasedNotification = @"IAPHelperProductPurchasedNotification";
// To receive a list of products from StoreKit, you need to implement the SKProductsRequestDelegate protocol.
// Here you mark the class as implementing this protocol in the class extension.
// For purchasing: modify the class extension to mark the class as implementing the SKPaymentTransactionObserver:
@interface IAPHelper () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
@end
@implementation IAPHelper
{
    // You create an instance variable to store the SKProductsRequest you will issue to retrieve a list of products, while it is active.
    SKProductsRequest *_productsRequest;
    // You also keep track of the completion handler for the outstanding products request, ...
    RequestProductsCompletionHandler _completionHandler;
    // ... the list of product identifiers passed in, ...
    NSSet *_productIdentifiers;
    // ... and the list of product identifiers that have been previously purchased.
    NSMutableSet * _purchasedProductIdentifiers;
}

Next, add the initializer. This will check to see which products have been purchased or not. This works based on the values saved in NSUserDefaults and also keep track of the product identifiers that have been purchased in a list.

- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers
{
    self = [super init];
    if (self) {
        // Store product identifiers
        _productIdentifiers = productIdentifiers;
        // Check for previously purchased products
        _purchasedProductIdentifiers = [NSMutableSet set];
        for (NSString * productIdentifier in _productIdentifiers) {
            BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];
            if (productPurchased) {
                [_purchasedProductIdentifiers addObject:productIdentifier];
                NSLog(@"Previously purchased: %@", productIdentifier);
            } else {
                NSLog(@"Not purchased: %@", productIdentifier);
            }
        }
    }
    return self;
}

 

Next, add the method to retrieve the product information from iTunes Connect. We set the IAPHelper class itself as the delegate, which means that it will receive a callback when the products list completes (productsRequest:didReceiveResponse) or fails (request:didFailWithError).

- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler
{
    // a copy of the completion handler block inside the instance variable
    _completionHandler = [completionHandler copy];
    // Create a new instance of SKProductsRequest, which is the Apple-written class that contains the code to pull the info from iTunes Connect
    _productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers];
    _productsRequest.delegate = self;
    [_productsRequest start];
}

 

Speaking of delegate callbacks, add those next!

Here we implement the two delegate callbacks – for success ...

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    NSLog(@"Loaded products...");
    _productsRequest = nil;
    NSArray * skProducts = response.products;
    for (SKProduct * skProduct in skProducts) {
        NSLog(@"Found product: %@ – Product: %@ – Price: %0.2f", skProduct.productIdentifier, skProduct.localizedTitle, skProduct.price.floatValue);
    }
    _completionHandler(YES, skProducts);
    _completionHandler = nil;
}

... and failure:

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
    UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Failed to load list of products."
                                                      message:nil
                                                     delegate:nil
                                            cancelButtonTitle:@"OK"
                                            otherButtonTitles:nil];
    [message show];
    NSLog(@"Failed to load list of products.");
    _productsRequest = nil;
    _completionHandler(NO, nil);
    _completionHandler = nil;
}

 

A lot of people recommend that you pull the list of product identifiers from a web server along with other information so you can add new In-App purchases dynamically rather than requiring an app update. This is true and definitely recommended, but for the purposes of this tutorial we are going to keep things simple and just hard-code in the product identifiers for this app. For this, we create a FruitIAPHelper class, that we make a subclass of IAPHelper.

First, add this code in the header file of the class:

#import "IAPHelper.h"
@interface FruitIAPHelper : IAPHelper
+ (FruitIAPHelper *)sharedInstance;
@end

 

The sharedInstance method implements the singleton pattern in Objective-C to return a single, global instance of the FruitIAPHelper class. It calls the superclasses initializer to pass in all the product identifiers that you created with iTunes Connect.

#import "FruitIAPHelper.h"
static NSString *kIdentifierApple       = @"de.tum.in.www1.sgdws13.BuyFruit.Apple";
static NSString *kIdentifierBlackberry  = @"de.tum.in.www1.sgdws13.BuyFruit.Blackberry";
static NSString *kIdentifierOrange      = @"de.tum.in.www1.sgdws13.BuyFruit.Orange";
static NSString *kIdentifierPear        = @"de.tum.in.www1.sgdws13.BuyFruit.Pear";
static NSString *kIdentifierTomato      = @"de.tum.in.www1.sgdws13.BuyFruit.Tomato";
 
@implementation FruitIAPHelper
 
+ (FruitIAPHelper *)sharedInstance {
    static FruitIAPHelper *sharedInstance;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        NSSet *productIdentifiers = [NSSet setWithObjects:
                                     kIdentifierApple,
                                     kIdentifierBlackberry,
                                     kIdentifierOrange,
                                     kIdentifierPear,
                                     kIdentifierTomato,
                                     nil];
        sharedInstance = [[self alloc] initWithProductIdentifiers:productIdentifiers];
    });
    return sharedInstance;
}

 

Now, we‘ve got all of the pieces in place to return the product info, so it’s finally time to see something appear on the screen!

First, create a new class as we did previously with our helper-class and call it FruitTableViewController and make it a subclass of UITableViewController.

Then, create another new class as we did previously and call it FruitDetailViewController and make it a subclass of UIViewController.

 

In the FruitTableViewController header file, import the FruitDetailViewController and instantiate the FruitDetailViewController.

@import UIKit;
#import "FruitDetailViewController.h"
@interface FruitTableViewController : UITableViewController 
@property (nonatomic, strong) FruitDetailViewController *fruitDetailViewController;
@end

 

In the FruitTableViewController implementation file, import the FruitIAPHelper class and instantiate the two variables products and priceFormatter. NSNumberFormatter allows you to flexibly convert backwards and forwards between numbers (NSNumber) and localized string (NSString) representations of those numbers

#import "FruitTableViewController.h"
#import "FruitIAPHelper.h"
@interface FruitTableViewController ()
@property (nonatomic, strong) NSArray *products;
@property (nonatomic, strong) NSNumberFormatter *priceFormatter;
@end
@implementation FruitTableViewController

 

In the FruitTableViewController implementation file, add the init method, set a title, and as we want to show the price in our tableView, allocate the NSNumberFormatter and set the formatter behavior on NSNumberFormatterCurrencyStyle.

- (id)init
{
    self = [super init];
    if (self) {
        self.title = @"Buy Fruits";
        self.priceFormatter = [[NSNumberFormatter alloc] init];
        [self.priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
        [self.priceFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    }
    return self;
}

 

In the FruitTableViewController implementation file, add the viewWillAppear and viewWillDisappear methods.

The viewWillAppear method will call the reload method, which we will implement during the next steps, add the observer, show the view, and implements the refreshControl.

The viewWillDisappear method clears the observer.

- (void)viewWillAppear:(BOOL)animated
{
    [self reload];
    [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(productPurchased:)
name:IAPHelperProductPurchasedNotification
object:nil];
    [super viewWillAppear:animated];
    self.refreshControl = [[UIRefreshControl alloc] init];
    [self.refreshControl addTarget:self action:@selector(reload) forControlEvents:UIControlEventValueChanged];
    [self.refreshControl beginRefreshing];
}
 
- (void)viewWillDisappear:(BOOL)animated
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

 

Next, go to the Table View Data Source section. Make the numberOfSectionsInTableView method return 1 and make the numberOfRowsInSection method count how many products we have and give that value back.

#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 1;
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return [self.products count];
}

 

In the cell configuration, set the cellStyle to UITableViewCellStyleSubtitle, since we want to display the price as a subtitle. Load the products into the rows of the table, set the row‘s title to the product title (name) and set the price as detailText. If the product was purchased, we want to show a checkmark, if not (else case) we want to show a button that the user can tap in order to buy the product.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    // Configure the cell...
    if (cell == nil) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
    }
    SKProduct *product = self.products[indexPath.row];
    cell.textLabel.text = product.localizedTitle;
    cell.detailTextLabel.text = [product.price stringValue];
    [self.priceFormatter setLocale:product.priceLocale];
    cell.detailTextLabel.text = [self.priceFormatter stringFromNumber:product.price];
    if ([[FruitIAPHelper sharedInstance] productPurchased:product.productIdentifier]) {
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
        cell.accessoryView = nil;
    } else {
        UIButton *buyButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        buyButton.frame = CGRectMake(0, 0, 72, 37);
        [buyButton setTitle:@"Buy" forState:UIControlStateNormal];
        buyButton.tag = indexPath.row;
        [buyButton addTarget:self action:@selector(buyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
        cell.accessoryType = UITableViewCellAccessoryNone;
        cell.accessoryView = buyButton;
    }
    return cell;
}

 

In the didSelectRowAtIndexPath method, you should:

  • get the product of the selected row
  • create the detailViewController and parse the product information which was selected to the DetailViewController
  • push the DetailViewController onto the TableViewController 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Gives the product back
    SKProduct *product = self.products[indexPath.row];
    // Creates the DetailViewController
    self.fruitDetailViewController = [[FruitDetailViewController alloc]init];
    self.fruitDetailViewController.product = product;
    // Pushes the DetailViewController
    [self.navigationController pushViewController:self.fruitDetailViewController animated:YES];
}

 

Create a pragma mark for the StoreKit methods we need to use in the TableViewController

The reload method calls the FruitIAPHelper class and gets the products. Once reloading is done, the refreshControl should stop refreshing.

#pragma mark - StoreKit
- (void)reload {
    self.products = nil;
    [self.tableView reloadData];
    [[FruitIAPHelper sharedInstance] requestProductsWithCompletionHandler:^(BOOL success, NSArray *products) {
        if (success) {
            self.products = products;
            [self.tableView reloadData];
        }
        [self.refreshControl endRefreshing];
    }];
}
 
- (void)productPurchased:(NSNotification *)notification {
    NSString * productIdentifier = notification.object;
    [self.products enumerateObjectsUsingBlock:^(SKProduct * product, NSUInteger idx, BOOL *stop) {
        if ([product.productIdentifier isEqualToString:productIdentifier]) {
            [self.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:idx inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
            *stop = YES;
        }
    }];
}


buyButtonTapped gets called when the user wants to buy something in the list of products.

- (void)buyButtonTapped:(id)sender
{
    UIButton *buyButton = (UIButton *)sender;
    SKProduct *product = self.products[buyButton.tag];
    NSLog(@"Buying %@...", product.productIdentifier);
    [[FruitIAPHelper sharedInstance] buyProduct:product];
}

Build & Run ON YOUR DEVICE (!)

 

Process of purchasing

Now we have a user interface that shows nice fruits we can buy and the helper-classes set up, we want to set up the purchasing process.

The basic gist of making a purchase is the following:

First: you create a SKPayment object and specify what productIdentifier the user wants to purchase and add it to a payment queue.

Second: StoreKit will prompt the user “Are you sure?”, ask them to enter their username/password, make the charge, and send you a success or failure. StoreKit also handles the case where the user already paid for the product and is just re-downloading it, and give you a message for that as well.

Third: you designate a particular object to receive purchase notifications. That object needs to start the process of downloading the content (not necessary in your case, since it’s hardcoded) and unlocking the content (which in your case is just setting that flag in NSUserDefaults and storing it in the purchasedProducts array).

Once again, most of this is going to be in the IAPHelper class for easy reuse.

 

Add to the top of the file the import StoreKit statement and the notification declaration. Into the method definition section, add the two methods for buying products and verifying if a product has been purchased.

@import Foundation;
@import StoreKit;
UIKIT_EXTERN NSString *const IAPHelperProductPurchasedNotification; 
typedef void (^RequestProductsCompletionHandler)(BOOL success, NSArray *products); 
@interface IAPHelper : NSObject
// Method definition
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler;
- (void)buyProduct:(SKProduct *)product;
- (BOOL)productPurchased:(NSString *)productIdentifier;

 

Next switch to IAPHelper.m and add the following. First we check if the product is allowed to be purchased, and if so it marks the purchase as in-progress, and issue an SKPayment to the SKPaymentQueue. The SKPayment class defines a request to the Apple App Store to process a payment.

- (BOOL)productPurchased:(NSString *)productIdentifier
{
    return [_purchasedProductIdentifiers containsObject:productIdentifier];
}
 
- (void)buyProduct:(SKProduct *)product
{
    NSLog(@"Buying %@...", product.productIdentifier);
    SKPayment * payment = [SKPayment paymentWithProduct:product];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

 

If you’re letting your users give you money, you better give them something good in return! So you need to add some code to identify when a payment “transaction” has finished, and process it accordingly.

Add the transaction observer to the bottom of the initWithProductIdentifiers: method. Now, when the IAPHelper class is initialized, it will make itself the transaction observer of the SKPaymentQueue. In other words: Apple will tell you when somebody purchased something. 

@interface IAPHelper () <SKProductsRequestDelegate, SKPaymentTransactionObserver> 
...
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers {
    if ((self = [super init])) {
       ....
        for (NSString * productIdentifier in _productIdentifiers) {
            BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];
            if (productPurchased) {
                ...
            }
        }
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

 

For this observer to work well, you should register your class as a transaction observer as early as possible in your app initialization... this is done in the AppDelegate.

Switch to the AppDelegate.m file and add the FruitIAPHelper import. Add [FruitIAPHelper sharedInstance] at the beginning of the application:didFinishLaunchingWithOptions: method. 

#import "AppDelegate.h"
// register our class as a transaction observer
#import "FruitIAPHelper.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [self.window makeKeyAndVisible];
   ...
    return YES;
}

 

Now, as soon as your app launches it will create the singleton FruitIAPHelper. 

You still have to implement the SKPaymentTransactionObserver protocol, so switch back to IAPHelper.m and add the following. 

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
            default:
            break;
        }
    };
}

 

„StateRestored“ is important if the users have the same app on multiple devices (or deletes it and reinstalls it) and wants to get access to their prior purchases.

Now, important too, is implementing the completeTransaction, restoreTransaction, and failedTransaction methods. completeTransaction: and restoreTransaction: do the same thing: they call a helper function to provide the given content, which we‘ll write next, so don‘t wonder about the errors.

failedTransaction is a bit different though: it calls a helper method to notify the user that the purchase failed (which we‘ll also write next), marks the purchase as no longer in progress, and finishes the transaction.

// called when the transaction was successful
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    NSLog(@"completeTransaction...");
    [self provideContentForProductIdentifier:transaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Bought successfully!"
                                                      message:@"Thank you for your purchase. Enjoy!"
                                                     delegate:nil
                                            cancelButtonTitle:@"OK"
                                            otherButtonTitles:nil];
    [message show];
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:transaction.payment.productIdentifier];
}
 
// called when a transaction has been restored and successfully completed
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
    NSLog(@"restoreTransaction...");
    UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Restored successfully!"
                                                      message:@"Enjoy!"
                                                     delegate:nil
                                            cancelButtonTitle:@"OK"
                                            otherButtonTitles:nil];
    [message show];
 
    [self provideContentForProductIdentifier:transaction.originalTransaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
 
// called when a transaction has failed
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
    NSLog(@"failedTransaction...");
    if (transaction.error.code != SKErrorPaymentCancelled) {
        NSLog(@"Transaction error: %@", transaction.error.localizedDescription);
        UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Ups!"
                                                          message:transaction.error.localizedDescription
                                                         delegate:nil
                                                cancelButtonTitle:@"OK"
                                                otherButtonTitles:nil];
        [message show];
    }
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

 

We‘re now adding the provideContentForProductIdentifier method. When a product is purchased, this method adds the product identifier to the list of purchased product identifiers, marks it as purchased in NSUserDefaults, and sends a notification so others can be aware of the purchase.

- (void)provideContentForProductIdentifier:(NSString *)productIdentifier {
    NSLog(@"provideContentForProductIdentifier");
    [_purchasedProductIdentifiers addObject:productIdentifier];
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier];
    [[NSUserDefaults standardUserDefaults] synchronize];
    [[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchasedNotification object:productIdentifier userInfo:nil];
}

That‘s it. All we have to do now, is updating the User Interface.

 

Restoring completed transactions

Restoring completed transactions is about getting non-consumable purchases back, on another device of the customer for example.

If you have non-consumable or auto-renew subscription types, you must implement this feature, or Apple will reject your app.

You should not auto-restore on launch, since this might confuse your user.

First, go to the header file of our IAPHelper class and declare the restoreCompletedTransactions method.

@import Foundation;
@import StoreKit;
UIKIT_EXTERN NSString *const IAPHelperProductPurchasedNotification;
typedef void (^RequestProductsCompletionHandler)(BOOL success, NSArray * products);
@interface IAPHelper : NSObject
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler;
- (void)buyProduct:(SKProduct *)product;
- (BOOL)productPurchased:(NSString *)productIdentifier;
- (void)restoreCompletedTransactions;
@end

 

Next, go to the implementation file and implement the restoreCompletedTransactions method as shown. What that will do is contact iTunes Connect and find out which products the user has already purchased. 

- (void)restoreCompletedTransactions {
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

 

It will then call paymentQueue:updatedTransactions for each of the products, with the SKPaymentTransactionStateRestored case, we already implement earlier.

Now, all we have to do is make the content available again. The restoreTransaction method calls the provideContentForProductIdentifier method, which sets the NSUserDefaults on YES for the correspondent product. Don't forget to call the finishTransaction method!

// called when a transaction has been restored and successfully completed
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
    NSLog(@"restoreTransaction...");
    UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Restored successfully!"
                                                      message:@"Enjoy!"
                                                     delegate:nil
                                            cancelButtonTitle:@"OK"
                                            otherButtonTitles:nil];
    [message show];
    [self provideContentForProductIdentifier:transaction.originalTransaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
 
- (void)provideContentForProductIdentifier:(NSString *)productIdentifier
{
    NSLog(@"provideContentForProductIdentifier");
    [_purchasedProductIdentifiers addObject:productIdentifier];
    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier];
    [[NSUserDefaults standardUserDefaults] synchronize];
    [[NSNotificationCenter defaultCenter] postNotificationName:IAPHelperProductPurchasedNotification object:productIdentifier userInfo:nil];
}

 

Last implementation step is in the FruitTableViewController.m: Add the rightBarButtonItem to the navigationBar. This button calls the restoreTapped method when pushed. The restoreTapped method simply calls restoreCompletedTransactions (a SKPaymentQueue method), we just implemented in IAPHelper. 

#pragma mark - User Interface set up
- (id)init {
    self = [super init];
    if (self) {
        self.title = @"Buy Fruits";
        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Restore"
style:UIBarButtonItemStyleBordered
target:self
action:@selector(restoreTapped:)];
        self.priceFormatter = [[NSNumberFormatter alloc] init];
        [self.priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
        [self.priceFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    }
    return self;
...
#pragma mark - StoreKit
- (void)restoreTapped:(id)sender {
    [[FruitIAPHelper sharedInstance] restoreCompletedTransactions];
}

 

Receipts

Receipts are files signed by Apple. They allow to securely verify the purchase of an In-App Purchase.

They were different on Mac OS / iOS... But, since iOS 7 we can use the same receipt format for iOS 7 and Mac OS X.

There is only one receipt per purchase and receipt data is made available by SKTransaction.

 

In-App Purchase Tips:

  • First: always call finishTransaction on all transactions! (most In-App Purchase errors) ... If you don‘t: StoreKit will not know you’ve finished processing the purchase, and will continue delivering the transaction to your app each time it launches.
  • Second: register as SKPaymentQueue observer on launch, as we did! This enables checking pending transactions as soon as the app launches.
  • Third: I already mentioned: only restore In-App Purchases when user needs access to In-App Purchases.

 

Testing in Sandbox

For testing In-App purchases without spending our real money, we need a test environment for In-App Purchases: Sandbox. You must be logged out from your personal AppStore account on the device you‘re testing the In-App purchase. If you successfully use the sandbox environment, you‘ll see a „Environment: Sandbox“ label.

When needed, log in with the Test User (created in iTunes Connect), Verify you‘re in the Sandbox environment & confirm the purchase.

 

Summary

So, in today‘s In-App Purchase session, we first learned about In-App purchase types.

  • There are 3 to 4 types, depending on how one evaluates it: consumables, non-consumables, auto-renewing subscriptions.
  • Auto-renewable subscriptions are complicated, too complicated for this introduction session and mostly don‘t fit a game strategy. They more apply to magazines or news-apps.
  • Restoration of In-App Purchases is mostly required. Of course it‘s not, when your are selling a consumable product, like game money.
  • Use a device for testing In-App Purchases.
  • Have a Paid Applications Contract in effect with Apple.

 

You can download the finished project here!  

 

Outlook

Where could we take the app we created from here?

  • First: we could host contents to download on Apple‘s servers... This is a great solution if you are offering navigation maps for example. The user doesn't have to download all the maps and fill his device with stuff he doesn't need: he just buys/downloads the maps he needs.
  • Second: we could move to a fully Server-Based System with which we could add new In-App purchases without updating the app.
  • Third: We could offer not our own products, but products from the iTunes Store: music, movies, other apps, etc. to be bought directly within our app.
  • Fourth: a very nice UI feature: a download progress bar and display, especially when downloading larger files.

For closing, I want to give you a bit more financial outlook of In-App Purchases.

  • The consulting firm PwC expects substantial growth jumps with the „free-to-play“ business model.
  • Players in Germany have spent in 2012 260 million € for virtual goods. By 2016, there will be over 400 million € spent.
  • Almost half of all video and computer gamers is now willing to pay for digital assistants or a higher level of play.
  • "The high growth rates also encourage other game companies to rethink their strategies and focus on virtual goods" – PwC's technology expert Werner Ballhaus.

 

References

https://developer.apple.com/in-app-purchase/

https://developer.apple.com/in-app-purchase/In-App-Purchase-Guidelines.pdf

https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction/Introduction.html

https://developer.apple.com/library/ios/documentation/StoreKit/Reference/StoreKit_Collection/_index.html

http://www.raywenderlich.com/21081/introduction-to-in-app-purchases-in-ios-6-tutorial

https://developer.apple.com/library/ios/technotes/tn2259/_index.html

https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/1_Introduction/Introduction.html

https://developer.apple.com/library/ios/technotes/tn2259/_index.html#//apple_ref/doc/uid/DTS40009578-CH1-FREQUENTLY_ASKED_QUESTIONS

https://developer.apple.com/library/ios/qa/qa1329/_index.html

https://developer.apple.com/appstore/guidelines.html