One of the great things about being an iOS devleoper is that there are a variety of models we can use to make money off of our 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 in the future to the same app (rather than having to make a new app to earn more money!)
In the latest app I’m working on (Wild Fables,
check out a sneak peek!), I’ve decided to make the app free with one story included, and more available as in-app purchases.
In this tutorial, you’ll learn how to use in-app purchases to unlock local content embedded in your app. I’ll show you how you can deal with the tricky asycnchronous nature of in-app purchases. Take this advice with a grain of salt, as my app is still in development – but I’ll update it with any lessons learned as I go! :]
This tutorial assumes familiarity with basic iOS programming concepts. If you’re still new to iOS development, check out some of the
other tutorials on this site.
In App Rage
So what app are we going to make in this tutorial? Well, let me give you some background first…
Lately I’ve become addicted to these comics online called
rage comics, or sometimes “F7U12″. If you haven’t heard of them before, they’re basically funny little comics where someone goes through a common and frustrating situation, and has a wild rage or other humorous expression at the end.
So for this tutorial, we’re going to make a little app called “In App Rage” where people can buy some of these comics. But before we can even start coding, we need to create a placeholder app entry using iOS Developer Center and iTunes Connect.
The first step is to create an App ID for the app. So log into the
iOS Developer Center, select the “App IDs” tab, and click “New App ID”:
Fill out the page by entering a description and bundle identifier, similar to the screenshot below:
Note you should change the bundle identifier to have your own unique prefix, by using your own domain name (if you have one), or if all fails a made-up one based on your name or something else fairly unique.
When you’re done, click Submit, and viola – you have a new App ID! Now you’ll use it to create a new app on iTunes Connect.
Log onto
iTunes Connect, click “Manage Your Applications”, and then “Add New App”. Enter in an App Name, SKU number, and choose the Bundle ID you just made as shown below:
You’ll probably have to tweak the App Name, because app names need to be unique and I’ve added an entry for this one.
The next two pages will ask you for your app’s information. 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, which you don’t even have yet!)
Just so you know, here’s how I feel about this:
If you get any errors like the above, just keep entering in dummy data (you can use any image for the icons and screenshots as long as they are the right sizes). After you get through all of the errors, you should have your placeholder app created – aww yeah!
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. So now that you have a placeholder app, just click on the “Manage In App Purchases” button, as shown below:
Then click “Create New” in the upper left, and enter in the information for the first purchase, similar to the screenshot below:
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 at the end.
- Type: You can choose between non-consumable (buy once, use forever), consumable (buy once, use once), or subscription (auto-renew) here. For this tutorial, we’ll just be covering non-consumables.
- Cleared for Sale: If this in-app purchase is OK to be available when the app becomes available.
- Price Tier: How much this in-app purchase should cost.
After you’ve set that up, scroll down and add an English language entry in the Display Detail section, as shown below:
This information will be returned to you when you query the App Store later on for the in-app purchases that are available.
You might wonder why this step is necessary (after all, you could embed this information in your app!) Well, obviously Apple needs to know the price. Also in the App Store it displays some of this information (such as when it displays the 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.
Once you’re done, save the entry and create several more, similar to this screenshot below. Don’t worry about the descriptions – we won’t be using them in this tutorial.
You might notice that this process takes a while, and I could imagine it gets annoying if you have a ton of in-app purchases in your app. Luckily we’re not in that situation, but if you are in your app,
draw me a rage comic :]
Retrieving Product List
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.
We could just add the code to do this into the view controller that uses it, but that would be brittle since it wouldn’t be very easy to re-use. Instead, we will create a helper class to manage all aspects of in-app purchases for us, that you can easily re-use in your own projects!
Along with getting the list of products from the server, this 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.
OK, so let’s try it out! In XCode, go to File\New Project, choose iOS\Application\Navigation-based Application, and click Choose. Name the project InAppRage, and click Save.
Next, create a new class to manage the in-app purchase code, called IAPHelper. To do this, click on the Classes group, go to File\New File, choose iOS\Cocoa Touch Class\Objective-C class, make sure Subclass of NSObject is selected, and click Next. Name the file IAPHelper.m, make sure “Also create IAPHelper.h” is checked, and click Finish.
We’re going to start by adding the method to IAPHelper.m that starts the process of retrieving the list of products from iTunes Connect, and build from there. So add the following to IAPHelper.m:
- (void)requestProducts { self.request = [[[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers] autorelease]; _request.delegate = self; [_request start]; } |
This method assumes that we have an instance variable named _productIdentifiers that contains a list of the product identifiers to look up in iTunes Connect (such as com.raywenderlich.inapprage.drummerrage).
It then creates a new instance of SKProductsRequest, which is the Apple-written class that contains the code to pull the info from iTunes Connect. It’s very easy to use – you just give it a delegate (that conforms to the SKProductsRequestDelegate protocol) and then call start to get things running.
We set the IAPHelper class itself as the delegate, which means that it will receive a callback when the products list completes (productsRequest:didReceiveResponse). Oddly, I don’t know of an error callback if it doesn’t succeed for some reason, so we’ll deal with that later by having a timeout instead.
Update: Jerry from the forums pointed out that SKProductsRequestDelegate derives from SKRequestDelegate, which has a method request:didFailWithError: that you can implement to get notice when this fails. If you’d like, you could use this instead of the timeout method described below. Thanks Jerry!
So let’s write productsRequest:didReceiveResponse to handle when the list of products is returned:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { NSLog(@"Received products results..."); self.products = response.products; self.request = nil; [[NSNotificationCenter defaultCenter] postNotificationName:kProductsLoadedNotification object:_products]; } |
This is very simple – it just squirrels away the list of products returned (an array of SKProducts), sets the request to nil (to free the memory), and posts a notification so that anyone who is waiting for the results is aware.
The next method to add it the initializer:
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers { if ((self = [super init])) { // Store product identifiers _productIdentifiers = [productIdentifiers retain]; // Check for previously purchased products NSMutableSet * purchasedProducts = [NSMutableSet set]; for (NSString * productIdentifier in _productIdentifiers) { BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier]; if (productPurchased) { [purchasedProducts addObject:productIdentifier]; NSLog(@"Previously purchased: %@", productIdentifier); } NSLog(@"Not purchased: %@", productIdentifier); } self.purchasedProducts = purchasedProducts; } return self; } |
The initializer will check to see which products have been purchased or not (based on the values saved in NSUserDefaults) and set up the data structures appropriately.
Ok, now that you have seen the most important routines, let’s wrap things up by adding the header file, synthesize statements, and other tidbits. First modify IAPHelper.h to look like the following:
#import <Foundation/Foundation.h> #import "StoreKit/StoreKit.h" #define kProductsLoadedNotification @"ProductsLoaded" @interface IAPHelper : NSObject <SKProductsRequestDelegate> { NSSet * _productIdentifiers; NSArray * _products; NSMutableSet * _purchasedProducts; SKProductsRequest * _request; } @property (retain) NSSet *productIdentifiers; @property (retain) NSArray * products; @property (retain) NSMutableSet *purchasedProducts; @property (retain) SKProductsRequest *request; - (void)requestProducts; - (id)initWithProductIdentifiers:(NSSet *)productIdentifiers; @end |
This simply imports the StoreKit header and defines the instance variables, methods, and notification name we need.
Next synthesize release the variables in IAPHelper.m:
// 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]; } |
One last step – you need to link in the StoreKit framework. Right click on the Frameworks folder and go to Add\Existing Frameworks and choose StoreKit.framework. Then go to Build\Build and your project should compile with no errors.
Subclassing for Your App
The IAPHelper class was written so that you can easily subclass it for your own app, specifying the product identifiers for your app. 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.
Click the Classes group, go to File\New File, choose iOS\Cocoa Touch Class\Objective-C class, make sure Subclass of NSObject is selected, and click Next. Name the file InAppRageIAPHelper.m, make sure “Also create InAppRageIAPHelper.h” is checked, and click Finish.
Then replace InAppRageIAPHelper.h with the following:
#import <Foundation/Foundation.h> #import "IAPHelper.h" @interface InAppRageIAPHelper : IAPHelper { } + (InAppRageIAPHelper *) sharedHelper; @end |
This just makes this class a subclass of IAPHelper, and creates a static method to create the Singleton instance of this helper.
Next replace InAppRageIAPHelper.m with the following:
#import "InAppRageIAPHelper.h" @implementation InAppRageIAPHelper static InAppRageIAPHelper * _sharedHelper; + (InAppRageIAPHelper *) sharedHelper { if (_sharedHelper != nil) { return _sharedHelper; } _sharedHelper = [[InAppRageIAPHelper alloc] init]; return _sharedHelper; } - (id)init { NSSet *productIdentifiers = [NSSet setWithObjects: @"com.raywenderlich.inapprage.drummerrage", @"com.raywenderlich.inapprage.itunesconnectrage", @"com.raywenderlich.inapprage.nightlyrage", @"com.raywenderlich.inapprage.studylikeaboss", @"com.raywenderlich.inapprage.updogsadness", nil]; if ((self = [super initWithProductIdentifiers:productIdentifiers])) { } return self; } @end |
This first implements the sharedHelper method to make InAppRageIAPHelper a singleton. Note that this implementation makes no efforts to be threadsafe, but this isn’t a problem for this app since it will only be accessed through this routine on the main thread.
Next it just sets up a hardcoded array of the product identifiers and calls the superclass’s initializer. Note you’ll have to change these to match whatever you set up in iTunes Connect.
Go to Build\Build, and once again it should compile with no errors.
Adding Helper Code
We’re almost ready to make use of our new classes, but there are two problems about calling this code that we’ll need solutions for.
The first problem is that this code won’t work at all without an Internet connection. So we’ll need an easy way to check for that.
Second, loading the products list might take a while, so we’ll want the user to have a way of knowing what’s going on. Having an easy way to display a nice activity indicator would be nice.
So go ahead and download those projects from their source, or you can just snag them from the
resource files for this tutorial.
Once you’ve downloaded the files, drag and drop MBProgressHUD.h/m and Reachability.h/m into the Classes group in your project. Make sure “Copy items into destination group’s folder” is checked, and click Add.
One last step – you need to link in the SystemConfiguration framework, which is a dependency of the Reachability Code. Right click on the Frameworks folder and go to Add\Existing Frameworks and choose SystemConfiguration.framework. Then go to Build\Build and your project should compile with no errors.
Ok we now have all the pieces to retrieve a product list – so let’s put them together!
Displaying the Product List
Open up RootViewController.h and make the following modifications:
// Before @interface #import "MBProgressHUD.h" // Inside @interface MBProgressHUD *_hud; // After @interface @property (retain) MBProgressHUD *hud; |
This just declares an instance variable and property for MBProgressHUD (the reusable progress indicator we’ll be using).
Next switch to RootViewController.m and make the following modifications:
// At top of file #import "InAppRageIAPHelper.h" #import "Reachability.h" // Under @implementation @synthesize hud = _hud; // Uncomment viewDidLoad and add the following self.title = @"In App Rage"; // Uncomment viewWillAppear and add the following self.tableView.hidden = TRUE; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productsLoaded:) name:kProductsLoadedNotification object:nil]; Reachability *reach = [Reachability reachabilityForInternetConnection]; NetworkStatus netStatus = [reach currentReachabilityStatus]; if (netStatus == NotReachable) { NSLog(@"No internet connection!"); } else { if ([InAppRageIAPHelper sharedHelper].products == nil) { [[InAppRageIAPHelper sharedHelper] requestProducts]; self.hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; _hud.labelText = @"Loading comics..."; [self performSelector:@selector(timeout:) withObject:nil afterDelay:30.0]; } } |
The important code is in viewWillAppear. It first sets the table view to hidden by default (it will reappear once the purchases are loaded). Then it sets up a notification because this class wants to know when the products are loaded.
It then checks to see if there’s an internet connection available using the Reachability helper class. If it is available, it calls the requestProducts method on the IAPHelper class you wrote earlier to start pulling down the products.
While the products are loading, it displays a MBProgressHUD to show a “loading” panel. Just in case the products never get loaded, it also schedules a method to be called in 30 seconds (timeout) to present the user an error.
So next let’s add the code to handle the notification that the list of products have been retrieved, or the timeout:
- (void)dismissHUD:(id)arg { [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES]; self.hud = nil; } - (void)productsLoaded:(NSNotification *)notification { [NSObject cancelPreviousPerformRequestsWithTarget:self]; [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES]; self.tableView.hidden = FALSE; [self.tableView reloadData]; } - (void)timeout:(id)arg { _hud.labelText = @"Timeout!"; _hud.detailsLabelText = @"Please try again later."; _hud.customView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"37x-Checkmark.jpg"]] autorelease]; _hud.mode = MBProgressHUDModeCustomView; [self performSelector:@selector(dismissHUD:) withObject:nil afterDelay:3.0]; } |
The first method (dismissHUD) is just a helper method to hide the loading panel.
The second method (productsLoaded) is the one called with the kProductsLoadedNotification is triggered. It simply hides the loading panel, and reloads the data in the table view, which will cause the new products to be displayed.
The final method (timeout) updates the HUD to present a timeout message, and schedules for the HUD to be dismissed after a few seconds.
One last bit – the code to fill in the table view! Make the following changes to RootViewController.m next:
// Replare return 0 in numberOfRowsInSection with the following return [[InAppRageIAPHelper sharedHelper].products count]; // In cellForRowAtIndexPath, change cell style to "subtitle": cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease]; // In cellForRowAtIndexPath, under "Configure the cell" SKProduct *product = [[InAppRageIAPHelper sharedHelper].products objectAtIndex:indexPath.row]; NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; [numberFormatter setLocale:product.priceLocale]; NSString *formattedString = [numberFormatter stringFromNumber:product.price]; cell.textLabel.text = product.localizedTitle; cell.detailTextLabel.text = formattedString; if ([[InAppRageIAPHelper sharedHelper].purchasedProducts containsObject: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; } // In viewDidUnload self.hud = nil; // In dealloc [_hud release]; _hud = nil; |
The table view simply displays whatever is in the products array in the IAPHelper singleton – which was retrieved by the SKProductsRequest.
The entries in the products array are instances of SKProduct. They contain the information you set up in iTunes Connect, such as the title, description, price, etc. Here the table view simply displays the price and title. It also sets up a Buy button, but it won’t work quite yet since you haven’t added the code for that.
You’re almost ready to test this out, but there is one final (and very important) step! You need to set up your bundle identifier. Click on your InAppRage-Info.plist and modify the Bundle identifier to match what you put in the iOS Developer Center, similar to the following:
And that’s it! Compile and run your app (on your device, this won’t work on the Simulator!) and you should see a loading indicator followed by your list of products:
Show Me The Money
This is an epic-length tutorial already but the most important part is still left – making the purchase, and collecting the money!
The basic gist of making a purchase is the following:
- You make a SKPayment object and specify what productIdentifier the user wants to purchase. You add it to a payment queue.
- StoreKit will prompt the user “are you sure?”, ask them to enter their username/password (if appropriate), make the charge, and send you a success or failure. They’ll also handle the case where the user already paid for the app and is just re-downloading it, and give you a message for that as well.
- You designate a particular object to receive purchase notifications. That object needs to start the process of downloading the content (not necessary in our case, since it’s hardcoded) and unlocking the content (which in our case is just setting that flag in NSUserDefaults and storing it in the purchasedProducts array).
Don’t worry – it’s pretty easy when you see the code. Once again, most of it’s going to be in the IAPHelper class for easy reuse. Start by making the following changes to IAPHelper.h:
// Add two new notifications #define kProductPurchasedNotification @"ProductPurchased" #define kProductPurchaseFailedNotification @"ProductPurchaseFailed" // Modify @interface to add the SKPaymentTransactionObserver protocol @interface IAPHelper : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver> { // After @interface, add new method decl - (void)buyProductIdentifier:(NSString *)productIdentifier; |
Then switch to IAPHelper.m and add the following methods:
- (void)recordTransaction:(SKPaymentTransaction *)transaction { // Optional: Record the transaction on the server side... } - (void)provideContent:(NSString *)productIdentifier { NSLog(@"Toggling flag for: %@", productIdentifier); [[NSUserDefaults standardUserDefaults] setBool:TRUE forKey:productIdentifier]; [[NSUserDefaults standardUserDefaults] synchronize]; [_purchasedProducts addObject:productIdentifier]; [[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchasedNotification object:productIdentifier]; } - (void)completeTransaction:(SKPaymentTransaction *)transaction { NSLog(@"completeTransaction..."); [self recordTransaction: transaction]; [self provideContent: transaction.payment.productIdentifier]; [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } - (void)restoreTransaction:(SKPaymentTransaction *)transaction { NSLog(@"restoreTransaction..."); [self recordTransaction: transaction]; [self provideContent: transaction.originalTransaction.payment.productIdentifier]; [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; } - (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]; } - (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; } } } - (void)buyProductIdentifier:(NSString *)productIdentifier { NSLog(@"Buying %@...", productIdentifier); SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } |
Phew! A lot of code here, but it is pretty easy to go through. Start from the bottom up.
When the buy button gets tapped in the table view, it will call buyProductIdentifier. This creates a new SKPayment object and adds it to the queue. We’ll be setting this class as the delegate to receive payment transaction updates, so when the purchase completes or failes it the paymentQueue:updatedTransactions method will be called.
If the payment succeeds (or is restored), eventually provideContent gets called. Here’s the important part – it sets the flag in NSUserDefaults, and adds the entry to the array. The rest of the code will be checking this to see if the user has access to the content.
On either success of failure, a notification is posted so any interested party can update the UI accordingly, etc.
Note that recordTransaction is not implemented. If you would like, you could implement this method to send a message to your web server to record the transaction. Personally, I don’t see a lot of advantage to implementing this method if your content isn’t downloadable – but it’s an option if you need it for your app.
Also note in general how this entire solution is pretty easily hackable – but I’m not too concerned with that, as my belief is anyone who would hack an app probably isn’t willing to buy it in the first place.
Before we use this code in the table view, we need to add some code to the App Delegate so the IAPHelper gets notified when the product purchase transactions come in. So switch to InAppRageAppDelegate.m and make the following changes:
// At top of file #import "InAppRageIAPHelper.h" // In application:didFinishLaunchingWithOptions [[SKPaymentQueue defaultQueue] addTransactionObserver:[InAppRageIAPHelper sharedHelper]]; |
Without this code, the paymentQueue:updatedTransactions method wouldn’t get called, so don’t forget!
Last step – let’s hook this into our table view! Go to RootViewController.m and make the following changes
// Add new method - (IBAction)buyButtonTapped:(id)sender { UIButton *buyButton = (UIButton *)sender; SKProduct *product = [[InAppRageIAPHelper sharedHelper].products objectAtIndex:buyButton.tag]; NSLog(@"Buying %@...", product.productIdentifier); [[InAppRageIAPHelper sharedHelper] buyProductIdentifier:product.productIdentifier]; self.hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; _hud.labelText = @"Buying fable..."; [self performSelector:@selector(timeout:) withObject:nil afterDelay:60*5]; } // Add inside viewWillAppear [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productPurchased:) name:kProductPurchasedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object: nil]; // Add new methods - (void)productPurchased:(NSNotification *)notification { [NSObject cancelPreviousPerformRequestsWithTarget:self]; [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES]; NSString *productIdentifier = (NSString *) notification.object; NSLog(@"Purchased: %@", productIdentifier); [self.tableView reloadData]; } - (void)productPurchaseFailed:(NSNotification *)notification { [NSObject cancelPreviousPerformRequestsWithTarget:self]; [MBProgressHUD hideHUDForView:self.navigationController.view animated:YES]; SKPaymentTransaction * transaction = (SKPaymentTransaction *) notification.object; if (transaction.error.code != SKErrorPaymentCancelled) { UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:@"Error!" message:transaction.error.localizedDescription delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] autorelease]; [alert show]; } } |
You’re so close you can almost taste it! But first, a brief word on accounts.
In App Purchases, Accounts, and the Sandbox
While you’re running your app in XCode, you’re not running against the real In-App Purchase servers – you’re running against the 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 on your device so you can see the whole process.
To make accounts, log onto iTunes Connect and click “Manage Users”. Click “Test User”, and follow the buttons to create a test user that you can make dummy in-app purchases with on the sandbox servers.
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 “Store”, and then click “Sign Out.”
Finally, go ahead 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!
But wait a minute – where’s your comic?! You didn’t pay for no check mark! I can see the rage comic now…
Well, this tutorial is way long enough, and adding the display of the rage comics wouldn’t be much related to in-app purchases, so we’re going to leave that a practice exercise :]
The
resources zip for this tutorial contain images for all of the the comics, so if you’re so inclined you can wrap it up by showing the comic in a new view controller when a purchased row is tapped! If you want to do this, you can just check if the productIdentifier is in the purchasedProducts array of the InAppRageIAPHelper before allowing the user access to the content.
Where To Go From Here?
Here is a
sample project with all of the code we’ve developed in the above tutorial, including the reusable in-app purchase helper class.
I’m curious to hear any thoughts or advice any of you may have with this, espeically since like I am just starting to figure some of this out myself for
my upcoming app! Any comments or advice whether on the technical or business side would be much appreciated.
Update: LOL – here’s another great iOS app rage comic made by Jayant C Varma!