Tuesday, November 15, 2011

In-app payment - some example code

So, I'm reviewing the in-app payment code, now that it's implemented, to see more carefully what exactly it all does. This post is basically a listing of all the code needed to implement an in-app purchase.


Note that this is based on an excellent tutorial by Rich Levine at http://www.raywenderlich.com/2797/introduction-to-in-app-purchases.


Another absolute must for anyone doing not just in-app purchases, but app submission with xcode 4 in general, is this great tutorial by Troy Bryant: http://troybrant.net/blog/2010/01/in-app-purchases-a-full-walkthrough/


Let's take a look at all the code: We'll take a look at it from the client perspective first.

Here's the header file:


//
// NextLevelViewController.h
// JlptQuizApp
//
// Created by Mark Donaghue on 9/20/11.
// Copyright 2011 Kanjisoft Systems. All rights reserved.
//

#import

// in app purchase
#import "InAppMyAppIAPHelper.h"
#import "MBProgressHUD.h"


@interface NextLevelViewController : UIViewController {
UIImageView *imageView;
UILabel *upperCongratsMessage;
UILabel *congratsMessage;
UILabel *art_title;
UILabel *url;

UIButton *continueButtton;

// Button to purchase with
UIButton *buyButtton;

// Show whirly progress indicator when waiting for an app purchase
MBProgressHUD *_hud;
}


@property (nonatomic, retain) IBOutlet UIImageView *imageView;
@property (nonatomic, retain) IBOutlet UILabel *congratsMessage;
@property (nonatomic, retain) IBOutlet UILabel *upperCongratsMessage;
@property (nonatomic, retain) IBOutlet UILabel *url;
@property (nonatomic, retain) IBOutlet UILabel *art_title;
@property (nonatomic, retain) IBOutlet UIButton *continueButton;

// In app purchase
@property (nonatomic, retain) IBOutlet UIButton *buyButton;
@property (retain) MBProgressHUD *hud;

- (IBAction)dismissView:(id)sender;

// In app purchase
- (IBAction)buyButtonTapped:(id)sender;

@end


So, essentially we have a buy button, the progress indicator for the purchase, and the declaration of the method method to call when the buy button is tapped.

Now, what do we do in the implementation?

First, we need to import the network status checking class (and my own class utils):

#import "Utils.h"
#import "Reachability.h"

First, we synthesize the stuff declared above:

@synthesize imageView, upperCongratsMessage, congratsMessage, art_title, url, buyButton, continueButton;

@synthesize hud = _hud;


I'm not in love with the convention of naming properties after internally declared variables without the underscore - what's the point? I guess there's a good reason for it - it must have something to do with how synthesize works. I'll figure it out later.

Next, we need to hide the purchase button if it's already been purchased. For that we have this code:


// check whether or not to display the purchase button
- (void) checkPurchaseButton {

if ([Utils isFreeVersion]) {
[buyButton setHidden:NO];

}
else {
[buyButton setHidden:YES];
}

}


This is called from "viewWillAppear". We'll drill into how it checks what's been purchased later on.

In the viewDidLoad, we identify the methods to be called when the app purchase is done, be it success or failure:



// in app purchase - specify what to do when app is purchased
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productPurchased:) name:kProductPurchasedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object: nil];




All you really have to know is this causes the "productPurchased" and the "ProductPurchaseFailed" methods to be called in on a purchase and purchase failure, respectively.

The next method is this:



// Dismiss the swirly progress indicator
- (void)dismissHUD:(id)arg {

[MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];
self.hud = nil;

}


As you might imagine, this is called when the app purchase is completed and dismisses the circly-swirly doodle progress indicator.

This is the purchase button pressed method:


-
- (IBAction)buyButtonTapped:(id)sender {


NSLog(@"NextLevelController, buyButtonTapped");

// needs to be called to set up the products array
[InAppMyAppIAPHelper listProducts];


// This is (currently) a one-product app. Retrieve the only product.
SKProduct *product = [[InAppMyAppIAPHelper sharedHelper].products objectAtIndex:0];


// create object to check for net status
Reachability *reach = [Reachability reachabilityForInternetConnection];

// check for nets status
NetworkStatus netStatus = [reach currentReachabilityStatus];

if (netStatus == NotReachable) {
NSLog(@"Arggh! - no internet connection!");
}

else {

// Display the swirly progress indicator
// this takes care of adding to the view - no need to add to the xlb
self.hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];

// Text for the progress indicator
_hud.labelText = @"Acessing new vocab...";


NSLog(@"Buying %@...", product.productIdentifier);

// Get the singleton shared helper and purchase. Use the product identifier
// from the product which you got from the products array a few lines ago
[[InAppMyAppIAPHelper sharedHelper] buyProductIdentifier:product.productIdentifier];

// call method named "timeout" ofter delay specified
[self performSelector:@selector(timeout:) withObject:nil afterDelay:60*5];

}
}



We'll delve back into this after we finish listing the client code. The next method is the callback for the product purchased:


// Handle product purchase
- (void)productPurchased:(NSNotification *)notification {

// Drop any previous requests
[NSObject cancelPreviousPerformRequestsWithTarget:self];

// Get rid of the swirly-doodle progress indicator
[MBProgressHUD hideHUDForView:self.view animated:YES];

// Get the product identified
NSString *productIdentifier = (NSString *) notification.object;


NSLog(@"Purchased: %@", productIdentifier);


// hide the purchase button
[self checkPurchaseButton];


}


And the purchase fail:

// Handle product purchase fail
- (void)productPurchaseFailed:(NSNotification *)notification {

NSLog(@"NextLevelViewContoller - productPurchase failed");

// Drop any previous requests
[NSObject cancelPreviousPerformRequestsWithTarget:self];

// Get rid of the swirly-doodle progress indicator
[MBProgressHUD hideHUDForView:self.view animated:YES];


// Grab the transaction from the notification
SKPaymentTransaction * transaction = (SKPaymentTransaction *) notification.object;

// Show an alert with info about the reason for failure
// as long as it's other than the (user-ititiated cancel?)

if (transaction.error.code != SKErrorPaymentCancelled) {

UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:@"Error!"
message:transaction.error.localizedDescription
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil] autorelease];

// show the alert
[alert show];
}

}


Finally, we have the code to display the timeout:

// Process a purchase timeout
- (void)timeout:(id)arg {

// set the swirly-gig-doodle progress indicator text
_hud.labelText = @"Timeout!";

// Give some hopeful advice
_hud.detailsLabelText = @"Please try again later.";

// set an image on the the swirly-gig-doodle progress indicator
_hud.customView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"37x-Checkmark.jpg"]] autorelease];

// Set the swirly-gig-doodle progress indicator to show the image
_hud.mode = MBProgressHUDModeCustomView;

// Drop the timeout display after a delay
[self performSelector:@selector(dismissHUD:) withObject:nil afterDelay:3.0];

}

Ok, we promised we'd take a look at the objects which actually perform the purchase. This is done through the use of the In-App Purchase helper class and its app-specific subclass. Since it's shorter, let's look at the subclass first. Here's the header:

#import
#import "IAPHelper.h"

@interface InAppMyAppIAPHelper : IAPHelper {
}

+ (InAppMyAppIAPHelper *) sharedHelper;

+ (void) listProducts;

@end


It's pretty straightforward. There are static declarations of two methods. The first provides a singleton instance of itself, and the second is a utility method to list the products.

Here's the implementation:

#import "InAppMyAppIAPHelper.h"

@implementation InAppMyAppIAPHelper

// Singleton instance of self
static InAppMyAppIAPHelper * _sharedHelper;


// Return the singleton instance of self
+ (InAppMyAppIAPHelper *) sharedHelper {

// return it if it's been instantiated already
if (_sharedHelper != nil) {
return _sharedHelper;
}

// It's not yet been created, so go ahead and create it
_sharedHelper = [[InAppMyAppIAPHelper alloc] init];

// and return it
return _sharedHelper;

}


// initialize
- (id)init {

// create a list of product identifier
NSSet *productIdentifiers = [NSSet setWithObjects:
@"com.kanjisoft.JlptVocabularyQuiz.AdditionalVocab",
nil];

// invoke superclass's method, which iterates through this list, checks
// NSPrefs, and if it's been purchased, add it to the "productsPurchased array
if ((self = [super initWithProductIdentifiers:productIdentifiers])) {

}

// done
return self;

}

// Method to list products

+ (void) listProducts {

// retreive the count of products
int productsCount = [[InAppMyAppIAPHelper sharedHelper].products count];

// Log a message if it's zero
if (productsCount == 0) {
NSLog(@"InAppMyAppAPHelper, productCount was zero: %d", productsCount);
}



// Iterate through the product list
for (int i = 0; i < productsCount; i++) {

// get the product
SKProduct *product = [[InAppMyAppIAPHelper sharedHelper].products objectAtIndex:i];

// allocate a number formatter
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];

// No idea what 10_4 is
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];

// Set the style to currency
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];

// Set the locale to the price locale
[numberFormatter setLocale:product.priceLocale];

// formate the price string
NSString *formattedString = [numberFormatter stringFromNumber:product.price];

// release the formatter
[numberFormatter release];

// log the info
NSLog(@"product title: %@", product.localizedTitle);

NSLog(@"product cost: %@", formattedString);


// check against the purchase producst array and log wheter it was
// purchased or not
if ([[InAppMyAppIAPHelper sharedHelper].purchasedProducts containsObject:product.productIdentifier]) {
NSLog(@"product was purchased");

} else
{
NSLog(@"product was *not* purchased");

}

}

}


@end


Ok, let's look at the big kahouna - the IAPHelper class.


Here's the interface:

//
// IAPHelper.h
// JlptVocabularyQuiz
//
// Created by Mark Donaghue on 10/28/11.
// Copyright 2011 Kanjisoft Systems. All rights reserved.
//

#import
#import "StoreKit/StoreKit.h"

#define kProductsLoadedNotification @"ProductsLoaded"
// Add two new notifications
#define kProductPurchasedNotification @"ProductPurchased"
#define kProductPurchaseFailedNotification @"ProductPurchaseFailed"


@interface IAPHelper : NSObject {

// Set of product ids, provided by the app specific subclass.
NSSet * _productIdentifiers;

// list of all products, purchased or not
NSArray * _products;

// list of purchase products, created by checking against prefs
NSMutableSet * _purchasedProducts;

// In app purchase use
SKProductsRequest * _request;
}

@property (retain) NSSet *productIdentifiers;
@property (retain) NSArray *products;
@property (retain) NSMutableSet *purchasedProducts;
@property (retain) SKProductsRequest *request;

// get a list of products
- (void)requestProducts;

// create the list of purchased products
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;

// buy the product
- (void)buyProductIdentifier:(NSString *)productIdentifier;

@end


Pretty self-explanatory. And finally, the implementation:


//
// IAPHelper.m
// JlptVocabularyQuiz
//
// Created by Mark Donaghue on 10/28/11.
// Copyright 2011 Kanjisoft Systems. All rights reserved.
//

#import "IAPHelper.h"


@implementation IAPHelper

// Under @implementation
@synthesize productIdentifiers = _productIdentifiers;
@synthesize products = _products;
@synthesize purchasedProducts = _purchasedProducts;
@synthesize request = _request;


// In dealloc
- (void)dealloc
{
[_productIdentifiers release];
_productIdentifiers = nil;
[_products release];
_products = nil;
[_purchasedProducts release];
_purchasedProducts = nil;
[_request release];
_request = nil;
[super dealloc];
}


// Request a list of products from app store

- (void)requestProducts {

// Create a requestt for a list of products, using the list
// of product identifiers as a parameter
self.request = [[[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers] autorelease];

// this instance is the delegate for the request object
_request.delegate = self;

// start the request
[_request start];

}


// receives response for the request with the listing of products

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {


// set the list of products to the list of product objects received in the response
self.products = response.products;

// nil out the request
self.request = nil;


// log the products received
for (id product in _products){
NSLog(@"IAHelper, received product is: %@", product);
}


// notify methods set up on the notification center of the product's receipt
[[NSNotificationCenter defaultCenter] postNotificationName:kProductsLoadedNotification object:_products];
}


// invoked from the app-specific subclasss, which provides
// a list of the product ids
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers {

// self to super
if ((self = [super init])) {

// Store product identifiers recieved from the subclass
_productIdentifiers = [productIdentifiers retain];


// Check for previously purchased products
NSMutableSet * purchasedProducts = [NSMutableSet set];

// iterate through the product identifiers
for (NSString * productIdentifier in _productIdentifiers) {

// see if the product id is in prefs
BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];

// if it is
if (productPurchased) {

// add it to the list of purchase products
[purchasedProducts addObject:productIdentifier];

// log it
NSLog(@"Previously purchased: %@", productIdentifier);
}
else {

// log that it's not in the list of purchased products
NSLog(@"Not purchased: %@", productIdentifier);
}
}

// set the purchased products array
self.purchasedProducts = purchasedProducts;

}
return self;
}



- (void)recordTransaction:(SKPaymentTransaction *)transaction {
// Optional: Record the transaction on the server side...
}


// look like this is called when the product is purchased
- (void)provideContent:(NSString *)productIdentifier {

NSLog(@"Toggling flag for: %@", productIdentifier);

// set prefs to true for the product id
[[NSUserDefaults standardUserDefaults] setBool:TRUE forKey:productIdentifier];

// save it
[[NSUserDefaults standardUserDefaults] synchronize];

// add it to the list of purchase prodcut
[_purchasedProducts addObject:productIdentifier];


// float the notification of the product purchase
[[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchasedNotification object:productIdentifier];

}


// call back from purchase
- (void)completeTransaction:(SKPaymentTransaction *)transaction {

NSLog(@"completeTransaction...");

// call the optional record transaction
[self recordTransaction: transaction];

// add it to the list of products purchase
[self provideContent: transaction.payment.productIdentifier];

// set the transaction to finished
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

}

// restore transaction
- (void)restoreTransaction:(SKPaymentTransaction *)transaction {

NSLog(@"restoreTransaction...");

[self recordTransaction: transaction];
[self provideContent: transaction.originalTransaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

}


// transaction failed
- (void)failedTransaction:(SKPaymentTransaction *)transaction {

if (transaction.error.code != SKErrorPaymentCancelled)
{
NSLog(@"Transaction error: %@", transaction.error.localizedDescription);
}

[[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchaseFailedNotification object:transaction];

[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

}


// add transaction to the payment queue
- (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;
}
}
}


// purchase the product
- (void)buyProductIdentifier:(NSString *)productIdentifier {

NSLog(@"Buying %@...", productIdentifier);

SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];

[[SKPaymentQueue defaultQueue] addPayment:payment];

}

@end



Ok, that's it.

No comments:

Post a Comment