Google have released a new version of their In App Billing (Version 3). So I’ve followed suit and updated my tutorial!
Eclipse Project Source Code Download
There are a fair few changes, too many to fit into one tutorial but the overview is:
– All purchases are now managed purchases
This means that once a user buys your item they own it, you have to ‘consume the purchase’ if you want to allow it to be purchased again. This is used in this tutorial but not explained or coded in a clean way so it is left for my next tut.
The benefits of this are that all purchases will be stored by Google Play and it will be easy to retrieve the status of whether a user has already purchased an item.
– The Android team have released a ‘helper’ class
This is to make in app purchases that much simpler. It has a vague resemblance to my original ‘BillingHelper’ class only it is called ‘IabHelper’ .. very interesting. This helper class makes things a hell of a lot easier.
Let’s get straight into it.
To start with I’d like to say, this isn’t the only way to do this. I myself have followed the developer tutorials on developer.android and managed to create this test project which I can now use as a reference when making other projects. Feel free to comment on the style and ask any questions. I would recommend you download the test project I have attached rather than copy and paste, this will stop minor syntax errors.
Developer.Android Links:
Google Billing Overview
Google Billing Integration
Ok first the outline.
Were going to create and app that has one button, when you click this button it informs the android market you want to make a purchase. On confirmation of this purchase you have bought a picture of a passport and it is shown to you in the app.
Simple setup stuff will be skipped (it is all in the download source).
First you create your project with the IInAppBillingService.aidl file included. You then need to add the Billing Helper classes, look at the source code, I have added these in the android.vending.billing.util package. They are the same as the Android example except the IabHelper which I have modified when I found some slight bugs.
Create 3 Activities, the first is the start up activity. This checks if In App Purchase is available on the device. If it is we load the main activity if it isn’t we don’t let them go any further:
StartUpActivity.java
package com.blundell.tutorial.simpleinappbillingv3.ui; import android.os.Bundle; import com.android.vending.billing.util.IabHelper.OnIabSetupFinishedListener; import com.android.vending.billing.util.IabResult; import com.blundell.tutorial.simpleinappbillingv3.ui.base.PurchaseActivity; import com.blundell.tutorial.simpleinappbillingv3.util.Log; /** * Checks that In App Purchasing is available on this device * * @author Blundell * */ public class StartUpActivity extends PurchaseActivity implements OnIabSetupFinishedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d("App started"); } @Override public void onIabSetupFinished(IabResult result) { if (result.isSuccess()) { Log.d("In-app Billing set up" + result); dealWithIabSetupSuccess(); } else { Log.d("Problem setting up In-app Billing: " + result); dealWithIabSetupFailure(); } } @Override protected void dealWithIabSetupSuccess() { navigate().toMainActivity(); finish(); } @Override protected void dealWithIabSetupFailure() { popBurntToast("Sorry In App Billing isn't available on your device"); } }
This StartUpActivity extends the PurchaseActivity. The PurchaseActivity is what talks to the Android helper service. Whenever the activity is created it will check that in app billing is still supported at this time.
PurchaseActivity.java
package com.blundell.tutorial.simpleinappbillingv3.ui.base; import android.content.Intent; import android.os.Bundle; import com.android.vending.billing.util.*; import com.android.vending.billing.util.IabHelper.OnIabPurchaseFinishedListener; import com.android.vending.billing.util.IabHelper.OnIabSetupFinishedListener; import com.blundell.tutorial.simpleinappbillingv3.AppProperties; import com.blundell.tutorial.simpleinappbillingv3.R; import com.blundell.tutorial.simpleinappbillingv3.domain.items.Passport; import com.blundell.tutorial.simpleinappbillingv3.util.Log; /** * This class should be the parent of any Activity that wants to do in app purchases * it makes our life easier by wrapping up the talking to the IabHelper and just exposing what is needed. * * When this activity starts it will bind to the Google Play IAB service and check for its availability * * After that you can purchase items using purchaseItem(String sku) and listening for the result * by overriding dealWithPurchaseFailed(IabResult result) and dealWithPurchaseSuccess(IabResult result, Purchase info) * * @author Blundell * */ public abstract class PurchaseActivity extends BlundellActivity implements OnIabSetupFinishedListener, OnIabPurchaseFinishedListener { private IabHelper billingHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_purchase); setResult(RESULT_CANCELED); billingHelper = new IabHelper(this, AppProperties.BASE_64_KEY); billingHelper.startSetup(this); } @Override public void onIabSetupFinished(IabResult result) { if (result.isSuccess()) { Log.d("In-app Billing set up" + result); dealWithIabSetupSuccess(); } else { Log.d("Problem setting up In-app Billing: " + result); dealWithIabSetupFailure(); } } protected abstract void dealWithIabSetupSuccess(); protected abstract void dealWithIabSetupFailure(); protected void purchaseItem(String sku) { billingHelper.launchPurchaseFlow(this, sku, 123, this); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); billingHelper.handleActivityResult(requestCode, resultCode, data); } /** * Security Recommendation: When you receive the purchase response from Google Play, make sure to check the returned data * signature, the orderId, and the developerPayload string in the Purchase object to make sure that you are getting the * expected values. You should verify that the orderId is a unique value that you have not previously processed, and the * developerPayload string matches the token that you sent previously with the purchase request. As a further security * precaution, you should perform the verification on your own secure server. */ @Override public void onIabPurchaseFinished(IabResult result, Purchase info) { if (result.isFailure()) { dealWithPurchaseFailed(result); } else if (Passport.SKU.equals(info.getSku())) { dealWithPurchaseSuccess(result, info); } finish(); } protected void dealWithPurchaseFailed(IabResult result) { Log.d("Error purchasing: " + result); } protected void dealWithPurchaseSuccess(IabResult result, Purchase info) { Log.d("Item purchased: " + result); // DEBUG XXX // We consume the item straight away so we can test multiple purchases billingHelper.consumeAsync(info, null); // END DEBUG } @Override protected void onDestroy() { disposeBillingHelper(); super.onDestroy(); } private void disposeBillingHelper() { if (billingHelper != null) { billingHelper.dispose(); } billingHelper = null; } }
Then our MainActivity, this just has the button for purchasing a passport. It will also receive a callback saying if the passport was bought or not.
The MainActivity starts the PurchasePassportActivity using startActivityForResult, this is how we inform it of a purchase. As with the StartUpActivity the PurchasePassportActivity extends our PurchaseActivity allowing it to talk to the Google Play Billing Service (and it’s helper class).
When the PurchasePassportActivity is started, it will get a callback saying billing is available and go onto to purchase the passport. The only thing special in this Activity is that is passed the SKU (the items ID from Google Play) to the Helper service. In this tutorial we use the “android.test.purchased” SKU meaning purchases will always be successful.
PurchasePassportActivity
package com.blundell.tutorial.simpleinappbillingv3.ui; import android.os.Bundle; import com.android.vending.billing.util.IabResult; import com.android.vending.billing.util.Purchase; import com.blundell.tutorial.simpleinappbillingv3.domain.items.Passport; import com.blundell.tutorial.simpleinappbillingv3.ui.base.PurchaseActivity; /** * This activity will purchase a Passport from Google Play. * * If you wanted to change to purchase something else all you have to change is the SKU (item id) that is used * you could even pass this in as an Intent EXTRA to avoid duplication for multiple items to purchase * * N.B that we extend PurchaseActivity if you don't understand something look up to this class * * @author Blundell * */ public class PurchasePassportActivity extends PurchaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Set the result as cancelled in case anything fails before we purchase the item setResult(RESULT_CANCELED); // Then wait for the callback if we have successfully setup in app billing or not (because we extend PurchaseActivity) } @Override protected void dealWithIabSetupFailure() { popBurntToast("Sorry buying a passport is not available at this current time"); finish(); } @Override protected void dealWithIabSetupSuccess() { purchaseItem(Passport.SKU); } @Override protected void dealWithPurchaseSuccess(IabResult result, Purchase info) { super.dealWithPurchaseSuccess(result, info); setResult(RESULT_OK); finish(); } @Override protected void dealWithPurchaseFailed(IabResult result) { super.dealWithPurchaseFailed(result); setResult(RESULT_CANCELED); finish(); } }
Note- The code below in the PurchaseActivity.java would be removed if you wanted to use this. It is there because the new version of in app billing treats all purchases as “managed items” i.e. when you buy them once you own them until you use them or they are refunded. So this code automatically ‘refunds’ the purchase so the demo doesn’t just work first time!
// DEBUG XXX // We consume the item straight away so we can test multiple purchases billingHelper.consumeAsync(info, null); // END DEBUG
That’s it, the rest of the code are just helpers and comments to try and explain what is going on and making it easier to extend.
As I always say This code tutorial isn’t foolproof and I feel I may of swept over a few things. But I really want to just give an alternative to the tutorial that is on the developer.android site. You could read this and understand the smaller concepts then go on to make a better implementation.
If you want to productionise this code you will want to go away and look at some of the security tutorials, obfuscation etc.
Remember to say thanks and enjoy!
Here are the code links again:
This seems like a possible glitch in IAB3:
1. I used my own app to purchase a non-consumable item on my Nexus 7. This is a real (non-static) SKU but purchased using a test account, with the app defined as a draft (not published). The app is signed with the same key as the app I uploaded to Google Play store via the developer console, and has the same version number and version code. No problem with this purchase.
2. I then ran my same signed app on my Nexus One, which has the same test account as its main user account. It performs a queryInventoryAsync when it starts up (and periodically subsequent to that). However, it does not see the new purchase, even though it is specifically checking for it. My Inventory request SUCCEEDS, but when I perform a inventory.getPurchase(MY_PURCHASED_SKU) it returns NULL.
3. If, at this point (and this is only about 15 minutes from having made the purchase on the N7), I attempt to initiate a *new* PURCHASE of the item from the N1, I get a message that the item is already owned. So, Google’s servers clearly realize that I’ve purchased the item.
4. Once I’ve *made* the purchase *attempt* from the N1, subsequent calls to queryInventoryAsync() *do* return a Purchase object for MY_PURCHASED_SKU, and the purchase state returned for that object by getPurchaseState() is Purchase.INAPP_PURCHASE_STATE_PURCHASED.
In other words, my failed attempt to re-purchase the item from a device other than the one used to purchase the item initially seems to have added the missing purchase to subsequent results on that second (N1) device by somehow “forcing the issue” when I attempted to purchase it again.
My question would be: Would the same thing have happened if I had just waited longer without trying to purchase the item? Would the purchase eventually have appeared? And, is this a known problem?
OK, I’ve found that if I power down the N1 and power it back up again, then it *does* detect the purchase made on the Nexus 7, even without attempting a purchase from the N1. So some kind of updating of the Google Play app from the GP servers seems to occur at the point that the device is booted but not subsequently.
This seems like a poor design functionally, but it is probably intended to conserve server bandwidth since if every app instance is probably checking every time the app starts (per recommended practice) they are not going to want to make a round trip to the server each time that occurs.
Carl,
You seem to have a lot of experience with this.
Do you know the basics (or even have some sample code) on how to restore a purchase after an uninstall and re-install?
Thanks.
Thank you for good tutorial.
but
this line
”
purchasesbillingHelper.consumeAsync(info, null);
”
not work
error is 5 : Developer Error
is anroid.test.purchased not consumable ?
Hmm interesting,, maybe they’ve updated the test purchase.
Developer Error usually means that you’re testing with an app that is either signed differently than the APK you uploaded, or has a different version/version code than the APK that you uploaded.
how to restore purchase in v3?
here my condition :
– using v3 i success buy item from my app with my second account credit card
– when i check my dev checkout account, i got information that my item already bought by my second accound
– i uninstall my app, clear data google play, and remove my second account
– login again with my second account, install my app again, but my item didnt restore, i have to buy it again.
how to make restore it?