Episode #44

In App Purchases

35 minutes
Published on December 6, 2012

This video is only available to subscribers. Get access to this video and 573 others.

In this episode I dive into the world of IAP (In App Purchases) using StoreKit. I start by creating a product in iTunes Connect, retrieving that product on the device, and emulating the App Store buy confirmation buttons using a handy CocoaPod.

Episode Links

Fetching your products from the store


// IAPGateway.h

#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

NSString * const IAPGatewayProductPurchased;

typedef void (^IAPGatewayProductsBlock)(BOOL success, NSArray *products);

@interface IAPGateway : NSObject

- (id)initWithProductIds:(NSSet *)productIds;
- (void)fetchProductsWithBlock:(IAPGatewayProductsBlock)block;

@end

// IAPGateway.m
@implementation IAPGateway

- (id)initWithProductIds:(NSSet *)productIds {
    self = [super init];
    if (self) {
        self.productIds = productIds;
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

- (void)fetchProductsWithBlock:(IAPGatewayProductsBlock)block {
    self.callback = block;
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:self.productIds];
    request.delegate = self;
    [request start];
}

#pragma mark -

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    for (SKProduct *product in response.products) {
        NSLog(@"Found product: %@ %@ %@",
              product.localizedTitle,
              product.localizedDescription,
              product.price);
    }

    self.callback(YES, response.products);
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    NSLog(@"ERROR: %@", error);
    self.callback(NO, nil);
}

@end

This class is meant to be subclassed as a singleton. This allows you to populate the list of product ids once, as well as enables the singleton to be a continuous observer of the StoreKit transaction callbacks (which we'll need later).

Checking to see if a product is purchased

In this episode, we went with a very naive and vulnerable approach to storing purchases, and that is in NSUserDefaults. It's should be said that you can fake this and malicious user's could easily gain access to purchases without going through StoreKit, however this is a decent starting point to get a working example. We'll cover the more secure approach in a future screencast.

So we'll add this method to our IAPGateway:

- (BOOL)isProductPurchased:(SKProduct *)product {
    return [[NSUserDefaults standardUserDefaults] boolForKey:product.productIdentifier];
}

Our app-specific subclass of IAPGateway

I mentioned that IAPGateway is meant to be reused across projects, so we'll create our app specific subclass here. The only real behavior we have to provide here is our list of products for this app. Reminder: You have to create your products first in iTunes Connect before you can use them in the Simulator.

// MyIAPGateway.h
#import "IAPGateway.h"
@interface MyIAPGateway : IAPGateway
+ (id)sharedGateway;
@end
// MyIAPGateway.m
#import "MyIAPGateway.h"

@implementation MyIAPGateway

+ (id)sharedGateway {
    static MyIAPGateway *__instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSSet *products = [NSSet setWithObject:@"com.nsscreencast.iap.backstage_pass"];
        __instance = [[MyIAPGateway alloc] initWithProductIds:products];
    });
    return __instance;
}

@end

Showing the List of Products in a View Controller

We created a simple UITableViewController to display the products. We start with implementing the UIRefreshControl to show activity while we are fetching the products, and then we kick off a refresh.

// MasterViewController.m
- (void)viewDidLoad {
  [super viewDidLoad];

  self.title = @"Products";

  self.refreshControl = [[UIRefreshControl alloc] init];
  [self.refreshControl addTarget:self action:@selector(refresh) forControlEvents:UIControlEventValueChanged];
  [self.refreshControl beginRefreshing];
  [self refresh];
}

Next, we implement the refresh method to actually go fetch the products. To do this, we'll need to declare an NSArray property for the products, and import
MyIAPGateway.h at the top.

- (void)refresh {
    [[MyIAPGateway sharedGateway] fetchProductsWithBlock:^(BOOL success, NSArray *products) {
        self.products = products;
        [self.tableView reloadData];
        [self.refreshControl endRefreshing];
    }];
}

Finally, we need to display the cell for the product. The purchase button is implemented using the MAConfirmButton cocoa pod.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.products.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                      reuseIdentifier:CellIdentifier];
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    }

    SKProduct *product = [self.products objectAtIndex:indexPath.row];
    cell.textLabel.text = product.localizedTitle;
    cell.detailTextLabel.text = product.localizedDescription;

    if ([[MyIAPGateway sharedGateway] isProductPurchased:product]) {
        cell.accessoryView = nil;
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
    } else {
        cell.accessoryType = UITableViewCellAccessoryNone;
        cell.accessoryView = [self confirmButtonForRow:indexPath.row];
    }

    return cell;
}

- (MAConfirmButton *)confirmButtonForRow:(NSInteger)row {
    MAConfirmButton *button = [MAConfirmButton buttonWithTitle:@"Buy" confirm:@"Confirm?"];
    button.tag = row;
    [button addTarget:self
               action:@selector(purchaseProduct:)
     forControlEvents:UIControlEventTouchUpInside];
    return button;
}

Clicking the purchase button

One the user taps on the purchase button, the purchaseProduct: selector will be invoked. We need a reference to the button so that we can pull the tag property (this was where we stored the index of the product to purchase).

- (void)purchaseProduct:(id)sender {
    MAConfirmButton *button = sender;
    SKProduct *product = [self.products objectAtIndex:button.tag];
    [button disableWithTitle:@"Purchasing"];
    [SVProgressHUD show];
    [[MyIAPGateway sharedGateway] purchaseProduct:product];
}

At this point we have an activity spinner present, and the purchase is happening. We now need to listen for a notification that the purchase was completed. We only want to do this while the view is visible on our screen, so we toggle the notification observer in viewWillAppear and viewWillDisappear.

- (void)viewWillAppear:(BOOL)animated {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(onProductPurchased:)
                                                 name:IAPGatewayProductPurchased
                                               object:nil];
}

- (void)viewWillDisappear:(BOOL)animated {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)onProductPurchased:(NSNotification *)notification {
    [SVProgressHUD showSuccessWithStatus:@"Thanks!"];
    NSString *productIdentifier = notification.object;
    [self.products enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        SKProduct *product = obj;
        if ([product.productIdentifier isEqualToString:productIdentifier]) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:idx inSection:0];
            [self.tableView reloadRowsAtIndexPaths:@[indexPath]
                                  withRowAnimation:UITableViewRowAnimationFade];
            *stop = YES;
        }
    }];
}

At this point, users can purchase products, however if they delete the app or move to a new device, their purchases won't be there. To rectify this, we must provide a way to restore purchases.

- (UIBarButtonItem *)restoreButton {
    return [[UIBarButtonItem alloc] initWithTitle:@"Restore"
                                            style:UIBarButtonItemStyleBordered
                                           target:self
                                           action:@selector(restore)];
}

// in viewDidLoad
self.navigationItem.rightBarButtonItem = [self restoreButton];

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

The success path should just work at this point, since the same notification is raised.

What's left?

We should probably handle the case where a purchase cannot be made by raising a separate notification. We should also be validating the purchases with Apple, and we'll cover this in a future screencast.