This “simple everyday” feature has caused me many headaches on many occasions. Implementing a simple unlockable feature isn’t as straightforward as it perhaps could be – and I’ll make it as simple as possible (so I won’t forget next time I need to add it to an app).
In this example we’re going to have one single feature that is already setup in iTunes Connect. It’s a non-consumable feature, such as “The Full Version”. To keep all our methods together in one place we’ll create a new NSObject class called “Shop”. This is where we’ll do most of the coding. Later we’ll also add code to the AppDelegate class.
This is a LONG article – grab a coffee and don’t get overwhelmed. It’ll take several tries to get comfortable with this matter (it’s not just you).
Let’s start with our new Shop class. It’ll have two properties – these can be private:
@property (nonatomic, strong) NSArray *currentProducts; @property (nonatomic, strong) SKProduct *mainProduct;
One holds an array of product identifiers (reverse domain notation, matching those in iTunes Connect), the other holds our actual product. Here’s a custom initialiser for the first property – it creates an array with one item (so you can expand with more products later):
- (NSArray *)currentProducts { // list of current product identifiers if (!_currentProducts) { _currentProducts = @[@"com.yourapp.fullversion"]; } return _currentProducts; }
Our Shop class needs a public method that can be called from the class that’s initiating the purchase. Here it is:
- (BOOL)validateProductIdentifiers { // let's check if the products exist in the App Store SKProductsRequest *request = [[SKProductsRequest alloc]initWithProductIdentifiers:[NSSet setWithArray:self.currentProducts]]; request.delegate = self; [request start]; return YES; }
It looks more complicated than it really is: we’re initiating an SKProductsRequest with our Product ID and call its start method. The App Store will come back to us in a moment by calling methods of the SKProductsRequestDelegate protocol.
When we’re ready to initiate the purchase, create a new Shop instance and call this method. Use a property to hold your Shop instance, otherwise the instance is likely de-alloced before a response is received.
The App Store will get back to us with the products title, description and price a bit later on. Note that for a response to be received, your device needs to be online. So before calling the above method, make sure you’ve checked for network connectivity – and if that’s not available, don’t call the method.
Which reminds me: let’s make our Shop.h file react to the response by adding said protocol:
@interface Shop : NSObject <SKProductsRequestDelegate>
The SKProductsRequestDelegate Method
The App Store calls this method when it’s ready – no matter if the product exists or not. All we have to do is implement it:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { // invalid products for (NSString *invalidIIdentifier in response.invalidProductIdentifiers) { NSLog(@"The following ID is invalid: %@", invalidIIdentifier); } // grab a reference for later self.mainProduct = [response.products objectAtIndex:0]; // display UI - if user can make payments if ([SKPaymentQueue canMakePayments]) { // display store UI [self displayStoreUIwithProduct:[response.products objectAtIndex:0]]; } else { // display can't buy UI [self cantBuyAnything]; } }
The App Store will give us the actual SKProduct which we’ll grab hold of in our self.mainProduct property. If the user can make payments, the Store UI is called giving the user a choice to buy the product. If in-app purchases are disabled, the user gets a different UI explaining the problem.
We’ll also check if this product is invalid – which of course it shouldn’t be, but it’s good for testing. Typos can lurk everywhere.
Store UI
In our example we’ll have a UIAlertView that displays either a buying choice or an explanation as to why the user can’t buy anything. These methods are called by the above:
- (void)displayStoreUIwithProduct:(SKProduct *)product { // display local currency NSNumberFormatter *formatter = [[NSNumberFormatter alloc]init]; [formatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [formatter setNumberStyle:NSNumberFormatterCurrencyStyle]; [formatter setLocale:product.priceLocale]; NSString *price = [NSString stringWithFormat:@"Buy for %@", [formatter stringFromNumber:product.price]]; // a UIAlertView brings up the purchase option UIAlertView *storeUI = [[UIAlertView alloc]initWithTitle:product.localizedTitle message:product.localizedDescription delegate:self.delegate cancelButtonTitle:@"Perhaps Later" otherButtonTitles:price, nil]; [storeUI show]; } - (void)cantBuyAnything { // tell user that payments are switched off UIAlertView *noStoreAlert = [[UIAlertView alloc]initWithTitle:nil message:@"You can unlock the full version, but your device does not allow in-app purchases at this time." delegate:self.delegate cancelButtonTitle:@"I'll check my Settings" otherButtonTitles:nil, nil]; [noStoreAlert show]; }
The first method looks a bit difficult, but it’s all code from Apple’s Guidelines. We use a number formatter to display the localised price with correct currency symbol. Next we’ll bring up a UIAlertView with the title and description of our product – this comes directly from iTunes Connect. You can translate your product there with a localised version.
The second method is called if in-app purchases are switched off on the device (under Settings – General – Restrictions).
Making the Purchase
Since we’re using an alert view here to react to the purchase, its delegate needs to react. In my example this happens in a different class – not the Shop class (hence the above alert views’ delegates are set to self.delegate rather than self). Therefore we need a second public method in our Shop class which our main class can call if the user decides to go ahead with the purchase. Here it is:
- (BOOL)makeThePurchase { // take payment for our product SKPayment *payment = [SKPayment paymentWithProduct:self.mainProduct]; [[SKPaymentQueue defaultQueue]addPayment:payment]; return YES; }
It’s short and sweet: create an SKPayment object with our product (glad we’ve saved it earlier), and add it to the SKPaymentQueue. In English: tell the App Store that our user wants to buy the product.
Now App Store goes to work and will get back to us in a moment. It will post notifications about the outcome, to which we can listen by conforming to another protocol. But we can’t implement those methods in our Shop class because it could be called at any time – even long after a purchase. Imagine the old “connection goes dead” ploy right after a user hits the buy button. By the time he’s back online it may be decades later.
App Store will remember that a payment was successful (or not) and will want to tell our app about it. So the only place we can put those delegate methods is in AppDelegate. The good news is we’re done with our Shop Class – just make sure those two public methods are declared in Shop.h:
- (BOOL)validateProductIdentifiers; - (BOOL)makeThePurchase;
Conforming to the SKTransactionObserver Protocol in AppDelegate
I only touch AppDelegate in rare circumstances, and this is one of them. First let’s tell him that we want to conform to said protocol (in AppDelegate.h):
@interface AppDelegate : UIResponder <UIApplicationDelegate, SKPaymentTransactionObserver>
Next, in AppDelegate.m, implement the following observer methods. We need to do this so that AppDelegate will listen to notifications posted by the App Store and react accordingly. This is the part I thought was a bit sketchy and confusing in Apple’s In-App Purchase Programming Guide:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // StoreKit Observer [[SKPaymentQueue defaultQueue]addTransactionObserver:self]; return YES; }
The following method is called when the App Store gets back to us, and since AppDelegate is now the observer, we’ll listen to them at all times when our app is running:
#pragma mark - StoreKit Observer - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: { // user has purchased [self saveTransactionReceipt:transaction]; [self unlockFullVersion]; // download content here if necessary // let App Store know we're done [[SKPaymentQueue defaultQueue]finishTransaction:transaction]; break; } case SKPaymentTransactionStateFailed: { // transaction didn't work break; } case SKPaymentTransactionStateRestored: { // purchase has been restored break; } case SKPaymentTransactionStatePurchasing: { // currently purchasing break; } default: break; } } }
We use a for-in loop to react to every transaction in the queue. Conceivably users could have bought multiple products, that’s why – however unlikely in our case. Depending on the current transaction’s transactionState we can react accordingly. For simplicity I’ve only covered a successful purchase here, you get the idea with the comments in the other sections of the switch statement.
In our “success” scenario, we need to unlock the full version (i.e. set a BOOL in NSUserDefaults or iCloud) and save the transaction receipt for later fraud detection and validation. If you’re offering downloadable content this is the time to download it.
When all these things have finished, you need to tell the App Store that you’re done processing this transaction so that it is removed from the SKPaymentQueue.
Save Receipt and Unlock Full Version
For completion, here are two methods that save the receipt and saving a “fullVersion” BOOL in NSUserDefaults. Your app can check this next time a premium feature is used. If the user has bought your product, go ahead and make it available. If not set, display the Store UI:
- (void)saveTransactionReceipt:(SKPaymentTransaction *)transaction { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString *receiptID = transaction.transactionIdentifier; NSArray *storedReceipts = [defaults arrayForKey:@"receipts"]; if (!storedReceipts) { // store the first receipt [defaults setObject:@[receiptID] forKey:@"receipts"]; } else { // add a receipt to the array NSArray *updatedReceipts = [storedReceipts arrayByAddingObject:receiptID]; [defaults setObject:updatedReceipts forKey:@"receipts"]; } [defaults synchronize]; } - (void)unlockFullVersion { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setBool:YES forKey:@"fullVersion"]; [defaults synchronize]; }
Note that the transactionidentifier string is only available in iOS 7 and later. iOS 6 and earlier can similarly save the full receipt object as data. The Apple Documentation has more on how to do this.
Saving receipts isn’t strictly necessary, but if you do then you can check a valid purchase with the App Store sometime later.