The last entry about in-app purchases left off with the products correctly displayed on the store and ready for purchase. Let’s push that buy button!
Purchasing a product
The store is fully populated, the items are displayed and, if all goes well, the user presses the buy button. What now?
At this point I check that in-app purchases are enabled and if they aren’t, I put up a message box. Apple says you can also do this earlier, but I figured I’d rather show users what’s available and entice them to buy it. By the time they get that message, they’ll be much more likely to enable them and continue the purchase than if they never saw the store in the first place.
Actually, before I even do that check, I see if the item is currently offered for free. If it is, the code takes an alternative path that doesn’t involve the App Store because apparently you can’t set an in-app purchase to free. Really! Why not? It’s yet something else we need to implement ourselves. Please, Apple, fix that. In the case of free purchases (like the Bonus Seeds included in the full version of Flower Garden) I contact my server directly and download everything from there. If you’re thinking that a hacker can just set the free flag to true and have access to all your content, it’s not that easy, because the server checks that the product is really marked as free when it’s contacted, and if it isn’t, it declines the transaction. So they have to work a bit harder than that to crack things (more on that next time).
But if the product is not marked as free and in-app purchases are enabled, then you can add the payment to the StoreKit payment queue which is, of course, another asynchronous call. So now we’re back to having to make a decision of what we want the user to do after they attempt to purchase something. Does someone really want to move away from the screen before they complete the purchase? I don’t, so I set another blocking activity screen explaining what’s going on with an activity indicator.
if ([SKPaymentQueue canMakePayments]) { SKPayment* payment = [SKPayment paymentWithProductIdentifier:m_product.m_id]; SKPaymentQueue* queue = [SKPaymentQueue defaultQueue]; [queue addPayment:payment]; [m_progressViewController setText:@"Accessing store" completed:NO]; [m_progressViewController display:YES animated:YES]; }
To be able to test the actual purchase, you need to create a test account in iTunes Connect. It’s slightly annoying that you need to explicitly log out from the Settings | Store screen and you can’t do that from the app itself, but it’s not nearly as much of a big deal as not being able to test purchases from the simulator at all. That’s a big fail as far as I’m concerned, and I can’t imagine a single reason why that was done that way (other than lack of time/manpower on Apple’s side).
Completing the purchase
You think we might be done by now, but not. Far from it. We’re just getting warmed up.
Now that the payment has entered the queue, you’re going to get a callback notification every time its status changes. That means you’ll get one right away because it was just added to the queue, another time when the payment is approved by the App Store, or when it fails, or if it’s detected that it was already purchased and this a re-purchase.
While that’s happening, the StoreKit code will bring up those ugly message boxes asking the user if they’re really sure they want to purchase the item for that price, and possibly ask them for their login info. It’s funny that the first time I implemented this part, I thought that it was up to me to put those message boxes up, so they were showing up twice. You don’t have to do anything for that, whether you want to or not. What’s worse, is that each time one of those message boxes comes up, your app gets a message putting it on hold, which usually means the sound is temporarily muted. Looks very unprofessional and I’m surprised Apple forces that on us while they leave everything else so open ended and requiring so much work.
Once the purchase is marked as complete, you can obtain the content for that purchase and make it available in your app. After that step is complete, you can remove the payment from the queue and the transaction is considered complete. In the case of Flower Garden, most purchases involve accessing my server and downloading some data from there for new seeds. Some purchases don’t require any downloading though, and all they do is twiddle a few bits internally.
It’s important to understand the payment queue well. First of all, it’s persistent, so once a purchase payment has been added to it, it will be there next time the application starts if it was never removed. It was designed that way to make sure an application can deliver on the product that was purchased even if the app is stopped, crashes, or is interrupted in any way. So initially, a payment is added to the queue meaning that the user has initiated the purchase transaction. At that point, the transaction can fail for multiple reasons: User cancels payment during one of the prompts, wrong user ID, the servers are down, the product was removed from the store, etc. Assuming the purchase is successful, the user account will be charged the cost of the product and the payment will be marked as successful. It’s at this time that you have to deliver the goods. Only once you’ve successfully done that you can go ahead and remove the payment from the queue. Otherwise, next time that the app starts, your code should detect a payment there and continue processing it. You really never want to get in a situation that the user pays for something and it’s never delivered to them.
I’m happy with how the StoreKit API will keep track of past purchases, and if a user tries to buy the same item again, they’ll be given the option to redownload it for free. It’s a great relief not to have to keep track of that ourselves. It will also cut down on customer support emails a huge amount because users can always redownload something if they had to uninstall their app, or somehow, in spite of all our precautions, they bought something but it was never updated in their app. In that case a quick re-download should fix it (just make sure your code is pretty stateless and can deal with re-downloading the same content multiple times).
It gets a bit trickier though. It turns out the payment queue can have more than one payment in it. Why is that? First of all, because the buy process wasn’t blocking the way Apple designed it, a hyperactive user could initiate multiple purchases at once. The other, more realistic situation is that the user initiates a purchase, it’s verified with the App Store, but before you can complete the download, your application is shut down. The Store Kit queue will be restored next time the application starts, but at that point the user could go to the store again and start another purchase.
In any case, you really need to deal with that situation, otherwise you risk losing purchases and angering users (and you really don’t want to rely on users re-downloading something for free if you can avoid it). This is trickier than it sounds when you’re dealing with downloads from your own server because it might involve multiple downloads, multiple files, etc. So in my case, whenever I get an update of status in the SKPaymentQueue, I add it to my own queue. Then I can process that queue sequentially, one purchase at the time.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction* transaction in transactions) [m_transactionQueue addObject:transaction]; [self processNextTransaction]; } - (void)processNextTransaction { if (m_processingTransaction) return; while (m_transactionQueue.count > 0) { SKPaymentTransaction* transaction = [m_transactionQueue objectAtIndex:0]; [m_transactionQueue removeObjectAtIndex:0]; switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: m_processingTransaction = true; [self completeTransaction:transaction]; return; case SKPaymentTransactionStateFailed: m_processingTransaction = true; [self failedTransaction:transaction]; return; case SKPaymentTransactionStateRestored: m_processingTransaction = true; [self restoreTransaction:transaction]; return; default: break; } } }
Whenever a transaction is complete, you can flag it as finished and it will be completely removed from the payment queue:Â [[SKPaymentQueue defaultQueue] finishTransaction:info.m_transaction];
A whole  step I skipped for now is verifying the purchase with your server. I’ll cover that in the next part of this post when I talk about anti-piracy measures.
It’s really fascinating to me to learn what’s going on behind the scenes in Flower Garden. Thanks! (C’mon Apple, approve that new seed pack!)