By Troy on January 17, 2010
At first glance, adding in-app purchases seems like it would be a walk in the park. Apple providesplenty of documentation that should get developers up and running in no time.
So, why is adding in-app purchases such a royal pain in the arse?
Because, inevitably, something will go wrong. And when that moment arrives, you’re screwed. Apple provides a beastly amount of documentation on in-app purchases, but they don’t provide the right kind of documentation. Nowhere is there mention of the setup steps you have to take to get in-app purchases to work. Nowhere is there a checklist you can reference if your StoreKit
integration doesn’t work. Nowhere is there an NSError
object that tell you exactly why your product ID is invalid.
You are left to flounder and flail like a wet noodle as you exhaustively try every possible solution on the web.
Losing days of productivity on this is ridiculous. To save you the pain and suffering I went through, this post details every step you need to take to implement in-app purchases. It’s detailed. It’s long. It’s probably overly-detailed and overly-long. But, unlike the Apple docs, it contains every single step necessary for any developer to implement in-app purchases.
Without further ado, let’s get started.
Ok, folks, here’s the secret to getting in-app purchases working: break it into two distinct steps:
The first step is where you will likely run into problems. Once you can successfully fetch a product description in code, writing the code to purchase the product is cake.
We’ll tackle the product description step first.
Here is a (very) rough overview of each step required to create a new product and fetch its description:
The code for fetching a product description is really simple. The setup steps, on the other hand, are rife with peril.
NOTE: You do NOT need to create an in-app test user in iTunes Connect to fetch a product description.
To support in-app purchases, your App ID cannot include a wildcard character (“*”). To see if your App ID contains a wildcard, log in to http://developer.apple.com/iphone, and navigate to the iPhone Developer Program Portal. Select “App IDs” from the menu on the left, and look for your App ID.
This is a unique App ID:
7DW89RZKLY.com.runmonster.runmonsterfree
This is not a unique App ID:
7DW89RZKLY.com.runmonster.*
If you don’t have a unique App ID, create one as follows:
Now that you have a new App ID, you need to generate a new provisioning profile that points to the App ID.
Here’s the painfully detailed step-by-step for generating and installing a new provisioning profile:
After the profile is installed in Xcode, you need to make a couple edits to the project to use the provisioning profile:
If your application is already available on the App Store, you can skip this step.
Before you can add a product in iTunes Connect, you must add the application the product is for. Don’t worry if you aren’t a 100% done with your app. You can still submit your app wtih stub data and add the real details later.
NOTE: Only the SKU and version fields are permanent and cannot be changed later.
This detail is not mentioned anywhere in Apple’s documentation, but is a requirement nonetheless. You must submit a binary for your application in order to successfully test in-app purchases. Even if you aren’t 100% done, you need to submit a binary. However, you can immediately reject the binary so it won’t go through the review process.
This was the crucial step I missed that caused me hours of grief and frustration. Follow these steps to add the binary:
Have no fear. Apple will not review this version of the app once it is “Developer Rejected”. You can submit a new version of your app at any point, and having the status “Developer Rejected” doesn’t affect the wait time for your future submissions in the slightest.
After all that setup, we are finally ready to add the product itself to iTunes Connect.
SKProduct
in code.Now, we finally write the code the fetches the product information we just added in iTunes Connect. To access the product data, we need to use the StoreKit
framework.
NOTE: StoreKit does not work on the Simulator. You must test on a physical device.
StoreKit
framework to your project.SKProduct
to your .h file:#import <StoreKit/StoreKit.h>
#define kInAppPurchaseManagerProductsFetchedNotification @"kInAppPurchaseManagerProductsFetchedNotification"
@interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate>
{
SKProduct *proUpgradeProduct;
SKProductsRequest *productsRequest;
}
NOTE: InAppPurchaseManager is a singleton class that handles every in app purchase for our app. It’s used throughout this post as an example implementation.
- (void)requestProUpgradeProductData
{
NSSet *productIdentifiers = [NSSet setWithObject:@"com.runmonster.runmonsterfree.upgradetopro" ];
productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
productsRequest.delegate = self;
[productsRequest start];
// we will release the request object in the delegate callback
}
#pragma mark -
#pragma mark SKProductsRequestDelegate methods
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *products = response.products;
proUpgradeProduct = [products count] == 1 ? [[products firstObject] retain] : nil;
if (proUpgradeProduct)
{
NSLog(@"Product title: %@" , proUpgradeProduct.localizedTitle);
NSLog(@"Product description: %@" , proUpgradeProduct.localizedDescription);
NSLog(@"Product price: %@" , proUpgradeProduct.price);
NSLog(@"Product id: %@" , proUpgradeProduct.productIdentifier);
}
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
NSLog(@"Invalid product id: %@" , invalidProductId);
}
// finally release the reqest we alloc/init’ed in requestProUpgradeProductData
[productsRequest release];
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];
}
A couple notes about the code above:
response.products
is nil in productsRequest:didReceiveResponse:
and your product id shows up in the response.invalidProductIdentifers
array, then prepare yourself mentally for a wild goose chase. The StoreKit
API offers no help, no indication as to why your identifier was invalid, just that it is. Lovely, isn’t it?SKProduct
class conveniently offers localized versions of your app title and description, but not price. To handle this omission, here’s a category that will provide a localized price string for the product as well:#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
@interface SKProduct (LocalizedPrice)
@property (nonatomic, readonly) NSString *localizedPrice;
@end
#import "SKProduct+LocalizedPrice.h"
@implementation SKProduct (LocalizedPrice)
- (NSString *)localizedPrice
{
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:self.priceLocale];
NSString *formattedString = [numberFormatter stringFromNumber:self.price];
[numberFormatter release];
return formattedString;
}
@end
After adding all the code above, give it a shot. You should see the product information gloriously regurgitated in your console window. However, you are more than likely getting back an invalid product ID. My next post addresses exactly how to go about debugging this problem, but the very next section may in fact hold your solution.
Have you followed all the steps above to the letter, and your product is still reported as invalid? Have you painstakingly double, triple, quadruple-checked to make sure you have followed every step? Have you despaired from finding frighteningly little in-app purchase information on the web?
Then, you may just need to wait.
It takes a while for the product you added to iTunes Connect to permeate Apple’s distributed in-app sandbox environment. For me, I gave up in despair after the umpteenth time my product came back as invalid. 24 hours later, I hadn’t changed a line a code, but my IDs were coming back valid. I think it really only took a few hours for the product to propagate through Apple’s distributed network, but if you can afford to wait, you may want to give it 24 hours like I did.
At this point, you should be able to successfully fetch an SKProduct
description for your product. Adding support for purchasing the product is relatively simple compared to getting the description. There are only three steps required:
We’ll start off by taking a look at the code required to support transactions.
First, a word of warning: you are responsible for developing the user interface for purchasing your product. StoreKit
offers absolutely zero interface elements. If you want your purchase view to look like the App Store’s, well, you have to build it yourself.
All the code below is for the backend of the transaction process. It is a single class with a simple API that an outside class (like a view controller) can call to make the purchase. I recommend a similar approach if you are figuring out how best to integrate in app purchases in your app.
First, you need to conform to the SKPaymentTransactionObserver
protocol:
// add a couple notifications sent out when the transaction completes
#define kInAppPurchaseManagerTransactionFailedNotification @"kInAppPurchaseManagerTransactionFailedNotification"
#define kInAppPurchaseManagerTransactionSucceededNotification @"kInAppPurchaseManagerTransactionSucceededNotification"
…
@interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver>
{
…
}
// public methods
- (void)loadStore;
- (BOOL)canMakePurchases;
- (void)purchaseProUpgrade;
@end
评论