Rss

An In-App Purchase Brain Dump

Oh thank goodness. Apple has finally come up with an API that’s a bigger pain in the ass than Core Audio. Namely, In-App Purchase. So no more bitching from any of you about kAudioUnitProperty_SetRenderCallback until you’ve tried validating a restored purchase.

I’m not complaining, not entirely, because both of these are inherently complex propositions, and the APIs are, by and large, designed to account for the hard parts of each problem domain.

That said, I spent as much time developing Road Tip‘s (née Next Exit) in-app purchase of continued mapping service as I did the app’s core functionality, so that’s gotta tell you that working through I-AP is no weekend project.

Apple’s programming guide is a decent-enough overview, as is the WWDC 2009 session, but I still felt like I ran into enough surprises that a brain-dump of tips and hints is in order.

  • Leave yourself a lot of time for I-AP. Seriously, it took me a person-month to get it all together.
  • The most critical decision is to figure out exactly what you’re selling and how it fits into I-AP’s view of the world. The one case that’s straightforward is “non-consumables” or “durables”, stuff that the user buys and has forever, like eBooks, or new levels in a game. Since these never disappear, Apple requires that you support copying these purchases to other devices, which you do with restoreCompletedTransactions. In fact, this is the only way you can do it: nothing in a purchase transaction tells you anything about the user (like their iTunes ID), so this method call is the only way of discovering a user’s previous purchases.
  • Other purchases are meant to be used up, like ammo in a game. If you buy 500 rounds of howitzer ammo on your iPhone, you shouldn’t be able to use it up and then magically restore it on your iPod touch. So restoreCompletedTransactions doesn’t work for these.
  • The biggest problem in I-AP is the design of “subscription” products. These are defined as consumable products that copy between a user’s devices. There’s an inherent design problem here: if something is consumable, there’s usually a degree to which it has been used up. How many rounds of ammo have I fired? How far into my one-year subscription am I? The degree of consumption is a state variable, something that can be tracked on one device, but can’t be communicated by means of a restoreCompletedTransactions call. Actually, it gets worse; restoreCompletedTransactions doesn’t return subscription products at all, so there’s no practical way for a copy of your app running on a new device to discover what subscriptions the user has purchased on other devices. I consider this a design bug and filed it with Apple (see the Open Radar copy).
  • Assuming you figure out a product model that works for you, you’re going to be working with at least three data sources:
    1. A list of product ids, perhaps saved with your app as a .plist or in a database, or available from the cloud
    2. The Store Kit APIs, which provide SKProduct objects for given ids and include localized name, description, and price. You’ll then present these products to the user and purchase them via other SK APIs.
    3. Apple’s validation webservice.
  • The last of these catches some people by surprise. This is easy to overlook, but the idea goes something like this: if you want an audit trail of user purchases, you need that data sent back to you somehow (since the purchases go from user devices to Apple’s servers, neither of which you control). You can log purchases by having the app call back to a server of yours, but how do you know that you’re getting called from a real copy of your app and not just some hacker somewhere? You call an Apple webservice at https://buy.itunes.apple.com/verifyReceipt with a purchase receipt received on the device, and get a response telling you whether the receipt is good or bogus.
  • Communicating with the Apple web service is done with JSON, and the purchase receipt needs to be sent as base64. I ended up using the MBBase64 category posted to CocoaDev to do the base64 encode on the phone, send that and other data to my web service. I wrote my web service in Java on Google App Engine, using json.org’s Java library to format my submits to the Apple web service and parse the responses.
  • Back on the device, one thing you’ll notice is that the Store Kit APIs are highly asynchronous: instead of blocking when you request products or make a purchase, you set a delegate or observer and implement a callback. It seems natural to put your commerce stuff in one central place in your app, and have that communicate changes to the rest of your app by means of delegates or NSNotifications.
  • Another surprise is that the payment queue (into which you put purchase requests) is persistent, so an unfinished purchase hangs around between application launches. This saves you from losing the user’s purchase if the app dies or loses its network connection in mid-purchase. Good thing. But it also means your observer will get called back shortly after you create the SKPaymentQueue singleton, even if your user hasn’t touched the purchase stuff.
  • You remove purchases from the queue by calling a finishTransaction method. You only do this when you’re good and sure the purchase has gone through. Apple’s WWDC session suggested not unlocking functionality until you hear back from your webservice. I thought that was a little strict: I’m unlocking when I get the “purchased” callback from the queue, but not finishing the transaction until I hear back from the webservice. If my webservice ever went down, the user would still have the unlocked feature, but the purchase would hang around in the payment queue, prompting a new validation attempt with my webservice every time the app comes up. That seemed more elegant to me.
  • A thought for you: how do you persist the “unlocked” state from one launch to another? You could write a .plist, a database record, or other local file, but that struck me as rather hackable by the pirate/jailbreak crowd. It’s also bad for consumables if a user could use up most or all of a consumable, then wipe the app (and your state data), reload, and get free stuff. I opted for putting this data — in my case an install date, which marks the beginning of a free trial period — in the keychain, whose data survives wipes (see the Apple devforum thread Keychain is now my best friend against in app purchase piracy). Only downside is that the keychain API is fairly hard to use, and Apple’s sample code is so determined to put a pretty Obj-C wrapper around the keychain, they actually make the material harder to learn (more info in this thread).
  • One thing that came up in the forum was the idea of when to try a restoreCompletedTransactions to get already-purchased items onto the current device. One developer said he or she planned to do this check at every startup, but I think that’s a bad idea: the user will typically be prompted for a password when you call restore, and that would be highly annoying to have to do all the time, especially as the need to pull old purchases to a new device is likely to be somewhat rare. I opted to make it a manual action, which means the user will only be prompted for a password as a direct result of a restore action.

  • Here’s another presentation concern: what should the purchase options look like? It seems like there’s an emerging consensus to put it in a “buy” button that’s placed at the right side of a table cell. This way, you can leave tapping the cell itself as a “preview” or “more info” gesture, like the iTunes application does, and make purchasing a more explicit gesture. Here’s what it looks like in a typical third-party app with in-app purchase, Weekly Astro Boy Magazine for iPhone / iPod touch




    Slight problem with this approach: it is actually fairly difficult to create a working button on the right side of a table cell. The API for accessory buttons doesn’t support rendering custom content into a right-side button (like “buy” or a price), so I opted to use a custom table cell, and a custom button class that can crawl the container hierarchy to find the parent table and call a selector when it’s tapped.




    Note that this image is debugging only: the published app will only present the user with the product that extends their service period by exactly one year.

OK, I think that’s all I’ve got to dump at the moment. Hope this helps people work through the challenges of I-AP. It’s an important API, but man it’s a struggle to actually put into practice.

Previous Post

Comments (9)

  1. This is a fantastic post — I am about to investigate this for my own app, currently available in both a free lite version and a paid version. My plan is to change the paid version to a free version with IAP being used to upgrade to the full version. This post, combined with the thread on the dev forums, has given me a few new ideas on how to go about to do it now.

    Thanks!
    dennis

  2. Giving credit where it’s due: The definitive in-app purchase HOWTO on the Apple dev forums, by user fxtech, covers important steps relating to setting up your products in the iPhone dev portal and iTunes Connect.

  3. […] that the in-app purchases for extending the trial period remain an issue of contention, for reasons discussed earlier. The app comes with a three-month trial period, so there’s time to resolve this before it […]

  4. […] of full time work into RoadTip… actually, three when you account for the month spent on the nightmare of implementing in-app purchase. That was time that I lived off a credit card, meaning I used a high-interest loan from Capital One […]

  5. tkarren

    Hi – thanks for posting this informative commentary on the IAP topic. I too am trying to wire it up for my application and have hit a snag trying to verify the receipt with Apple’s web service from my server. Could you elaborate at all on how you did that in App Engine?

    Thanks!

    -Tom

  6. […] was on In-App Purchase. In many ways, it covered the same hard-earned experience that I covered in An In-App Purchase Brain Dump. I spent a little time on both the iPhone Provisioning Portal (to set up an AppID, authorize it for […]

  7. gangadhar.bonda

    Hi,
    Thanks for posting the information on In app purchases. I am trying to implement IAP in my application but I was stuck in implementing the subscription based product. My requirement is as follows
    i) From the day of starting the application I will provide the user a free trail period of 30 days after which I have to prompt the user to purchase the application using subscription based product.

    Can you please suggest me a approach in implementing the functionality.

    Thanks in advance

    • This is basically what I do in Road Tip. I persist the install date and purchase history, and check it at startup. It should be pretty straightforward to write an isPaidUp-type method that looks to see if the current date is within the free period (install date + free trial period) or is covered by the subscription (subscription start date + length of subscription). Check this at startup and push a modal “pay up now” view instead of your root view controller.

      The trick is how to make this work without allowing the user to get a new free trial by deleting and reinstalling the app. If you just write the install/purchase history to your app’s Documents folder, they’ll be able to do this. What you can do instead is write to the keychain, which survives application wipes. One of the bullet points in this blog entry explains this further.

  8. […] since my most-popular blogs have always been these brain-dump things — Core Audio, OpenAL, and In-App Purchase — I figured I’d roll back to that old […]

Leave a Reply

Your email address will not be published. Required fields are marked *