Adding an Upgrade Path to In App Purchases

Tilt to Live HD’s Viva la Turret mode is now live and people seem to be enjoying it. One of the bigger hurdles in this prior update was getting in app purchases to work the way we wanted them to without violating the standard user experience of IAP. I figured I’d share how I went about solving this for Tilt to Live HD specifically. We had a few business goals in mind:

  • We wanted to maintain parity with the upgrade model we went with on the iPhone, which was a simple in-app purchase to get the new mode.
  • We still wanted to maintain the ‘demo then upgrade to full version’ packaging of Tilt to Live HD for a single price, as we still believe it appeals to a certain type of gamer that wants a more traditional model for buying a game.

In a way, it seems our two goals were at odds with each other. We wanted players to have an optional upgrade, but at the same time we didn’t want the ‘full version’ to break down into “meh..it’s kind of the full version..but not really”. Our solution was to treat it as a paid upgrade for existing users, but bundle the new mode in with any new customers who bought the full version of Tilt to Live. The caveat being we did not want to break it down into some sort of ‘store’ list, where a user could inadvertently end up buying the wrong version and be double charged for overlapping content. We were pretty proud of the IAP integration in HD where it was a relatively seamless experience that didn’t involve any UI that required a “store” button. We simply showed you the gametype option and it was locked if you didn’t have it, and unlocked if you did.

With the additional new mode, we now had 3 basic states you could possible be in.

1. You have the demo version of Tilt to Live HD, so the gametype select screen is pretty standard and you see 1 in app purchase that includes everything:

2. You have the full version of Tilt to Live HD prior to February 8th. The gametype select screen should show you just the small IAP for the single Viva la Turret game mode:

3. You have the brand new full version of Tilt to Live HD, so everything is unlocked:

Now this sounds all well and good and pretty straight forward when I first considered it. Then came the technical details:

  • The behavior of IAP is such that it mirrors functionality like App Store purchases. If you’ve ever bought that IAP before, you can buy it again, but a pop up will notify you that you already own it and are downloading it again for free.
  • If a person has never restored, then simply checking what modes in the client are unlocked is sufficient and is the path of least resistance.
  • If a person has restored/re-installed, there is no API from Apple to determine if a person has already bought a piece of content before they try to buy it again, which could result in angry customers being double charged if they were in state #2 (pictured above) but have re-installed and ended up buying the full version again as if it was in state #1 (pictured above).

Essentially, when a prior customer re-installs our game for whatever reason, there was no way for me to determine whether they never bought the old full version and we should upsell the entire new full version, bought the old full verison and we should upsell just the game mode, or if they already have the entire thing unlocked (either by buying the old full version + the turret IAP or by buying the new full version that includes everything). Confused yet?

The riskiest thing was that I did not want to have a situation where a person could inadvertently buy the entire brand new ‘full version’ IAP when all they needed to buy was the small turret IAP. With the StoreKit API lacking any sort of method to determine a user’s purchase history, and with Tilt to Live HD not using any sort of server solution to track receipts,  I was running out of ideas. Then I got the idea of experimenting with a little “hack” to simulate the same IAP experience.

When a user taps on the ‘buy’ button for any IAP in the game I decided to instead force it to restore purchases all the time. Using a small collection of state variables when a restore was finished I would check to see if what they were purchasing was in line with what was restored:

  • If so, do nothing and give a standard dialog showing they restored successfully.
  • If it was in conflict, let them know nothing was purchased but their previous items restored. I would then refresh the upsell screen showing what they could really purchase.
  • If there was no conflict and they, in fact, don’t own any part of the IAP, then I would immediately push the purchase through. The great thing here is since it happens right after the restore, the user is never prompted for their password again since they already did so during the initial tap of the buy button to trigger the restore.

When going about implementing this I had a few false starts and dead ends, but after re-reading the documentation and a bit of experimentation I finally grokked most of the restoration API from StoreKit.

There are two methods involving the end of restoring tranactions. There is one that is called at the end of each IAP item restored:

[code]
-(void)restoreTransaction:(SKPaymentTransaction*)transaction
[/code]

And then there’s a method that is called when none, or all IAP items have been restored:
[code]
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
[/code]

The former I left alone to act as it normally would under any restoration of transactions. The latter method, I modified to behave as an ending point if the user really tapped on ‘restore purchases’ or a beginning point to analyze what was restored and to continue with the purchase if needed. Below is the method in full in case you’re curious:

[code]
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
if(iapTransactionState == kIAPTransStateRestoring) // restore as is.
{
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseRestoreSucceededNotification object:self userInfo:nil];
}
else if(iapTransactionState == kIAPTransStatePurchasing)
{
if([self isContentUnlocked:purchaseItemID])
{
// if it’s been restored or unlocked already then no need to do it again.
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseTransactionAlreadyHaveItemNotification object:self userInfo:nil];
} else {
//…unless we’re already in some half-bought state (full -> full+turret)
bool makePurchase = true;
if([purchaseItemID isEqualToString:kIAPFullAndVivaLaTurretID])
{
// if after restoring we found the player purchased the full and viva separately
// don’t go forward with payment for the bundle.
if([self isContentUnlocked:kIAPVivaLaTurretID] || [self isContentUnlocked:kIAPFullVersionID])
{
makePurchase = false;
#ifdef DEBUG
NSLog(@”bundling purchase error”);
#endif
}
}

if(makePurchase)
{
// continue with the purchase
SKPayment *payment = [SKPayment paymentWithProductIdentifier:purchaseItemID];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
else {
// send a bundling error message
// or a restore complete if they have entire bundle
// because they purchased it separately (upgraders)
bool hasBundle = [self isContentUnlocked:kIAPVivaLaTurretID] && [self isContentUnlocked:kIAPFullVersionID];
if(hasBundle)
{
#ifdef DEBUG
NSLog(@”has complete bundle, no purchase made”);
#endif
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseTransactionAlreadyHaveItemNotification object:self userInfo:nil];
}
else
{
[[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseTransactionAlreadyHavePartialBundleNotification object:self userInfo:nil];
}

}

}
}
#ifdef DEBUG
NSLog(@”restore done”);
#endif
// [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseRestoreSucceededNotification object:self userInfo:nil];
}
[/code]

I can’t say this is the most ideal solution since I feel it’s using the StoreKit API in a slightly unorthodox way, but it works well. So this is one way of implementing a ‘paid upgrade’ for IAP items. If we were selling more items in a more ‘freemium’ type model it might’ve been a good idea to generalize the above code to handle upgrades for any item, but since Tilt to Live HD’s model is a one-off IAP I didn’t bother generalizing.

1 comment

Leave a Reply

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