Getting Started
Things you’ll need before moving forward with this tutorial:
- Corona SDK build 2012.760 or later.
- An active Indie-Android or Corona Pro subscription.
- An active Android Developer account.
1. Android Developer Console
Whether or not you’ve started development on your app, you’ll need to ensure the “BILLING’ permission is set in your build.settings, build/re-build the APK (even if your app is currently blank—it’s strange, I know), and then upload it to the Android Developer Console to begin adding In-app Billing Products to your app.
If you haven’t even started developing your app, or are mid-way through, this is going to seem really strange, but it’s a necessary step (mandated by Google) before you’re able to add any In-app Products to your app.
In the event your app is already uploaded (or even published), you’ll still need to add the “BILLING” permission to your build.settings, rebuild, and then re-upload the APK. Once you do that, the In-app Products link underneath your app (in the Android Developer Console) will work properly (more on that in a moment).
Here’s how to add the “BILLING” permission to your build.settings file:
settings =
{
orientation =
{
default = "landscapeRight",
},
android =
{
usesPermissions =
{
“com.android.vending.BILLING”,
},
}
}
In the example, the part to pay attention to is the android > usesPermissions > “com.android.vending.BILLING” part. The orientation section was added so you get a better idea of where the ‘android’ section goes in your build.settings.
Once you’ve done that, open up the Corona Simulator, navigate to the File menu > Build > Android…. Once your APK is built, go to the Android Developer Console and upload your APK as usual (but don’t hit ‘Publish’ just yet).
From there, you can follow this guide to add In-app Products to your apps in the Android Developer Console.
IMPORTANT NOTE: Once you’re ready to begin testing your actual In-app products, be sure you click Activate (this is different than Publish!) on the APK you uploaded with the proper build.settings or the products associated with your app will come back as unavailable.
Google Documentation
I already linked to one of these, but it’s worth mentioning again. Google has provided some pretty good guides on how to set up In-app Products for each of your apps, as well as how to test them (screenshots and all!). If you need help learning how to set things up on the Android Developer Console side of things, please refer to the following two guides:
2. The Corona “store” API
For the sake of this tutorial, I’m going to use Google’s test product identifiers, which have specific actions associated with each one (but feel free to use your own):
- android.test.purchased – Will always be a successful purchase.
- android.test.canceled – Will always be a cancelled transaction.
- android.test.item_unavailable – Will always indicate that the product is unavailable.
The really nice thing about the above test products is that their associated actions allow you to test the different results, which should make it easier to design how your app will handle those specific situations. For example, you can use the ‘android.test.canceled’ product to test your app flow in the event a user cancels the transaction.
store.init( “google”, transactionCallback )
The first point-of-contact your app will have with the store API will be through the store.init(). For Android In-app Billing (for use with Google Play only, formerly known as the Android Marketplace), you’ll specify “google” as the first argument, and then a function that will handle transaction events as the second argument.
Here’s an example of how to use store.init() with a transaction callback listener function (go here to view the code if you cannot see it below):
If you scroll down to the very bottom of the example, you’ll see that we call store.init() and pass the transactionCallback() function as the callback listener for Android In-app Billing events.
Handling Refunds
Transaction events for Android In-app billing mirrors that of iOS, with one more addition: the “refunded” transaction state. The Android platform, unlike iOS, allows users to refund purchases, so if you want to “take back” your content from your users, you’ll want to handle all that during the “refunded” transaction state.
And as shown in the example, the important piece of data you’ll receive during the “refunded” transaction state is event.transaction.productIdentifier. With that, it’s your responsibility to have your app react accordingly with the information you’re given. You could take this opportunity to re-block the content that was refunded, or delete the premium content rendering it inaccessible to the user (if the in-app product was downloaded).
No “restored” State on Android
The store.restore() function allows you to retrieve a listing of product Ids associated with the logged-in user’s account. With iOS, this product listing is delivered via the “restored” transaction state in the transactionCallback listener. However, on Android, you’ll never receive a “restored” transaction state. Instead, the products will come through again as “purchased”. It’s a very subtle difference, but important to consider when designing your transactionCallback listener function.
Other Transaction Event Data
All other data associated with transaction events are identical to In-app purchases in iOS, so please see the Transaction Listener Callback Events section of the previously published In-app Purchases Guide for more information on data you’ll receive during transaction events.
store.finishTransaction( transaction )
The ultimate line of the transactionCallback() function in the example is very important. store.finishTransaction must be called at the end of every transaction. It’s easy to think, I’ll just place this at the very end of the transactionCallback listener function and that’ll be that—which is actually okay in some situations.
However, if you have downloadable content, you’ll want to wait to call store.finishTransaction() until AFTER the download is complete (e.g. somewhere in your network.download() listener) instead of at the end of the transactionCallback() function.
If you’re simply unlocking content that’s already included in the app bundle, then you can just place store.finishTransaction() at then end of your callback listener (as shown in the example).
In either case, don’t forget to pass the event.transaction table as the first argument!
Here’s a quick example:
store.finishTransaction( event.transaction )
3. Purchasing Products
Now that you know how to “handle” transactions such as purchases, refunds, failed/cancelled transactions, etc. it’s time to begin a transaction. Just as you would with iOS In-app purchases, you initiate a transaction using store.purchase(), which is usually called in an event listener for a “Buy” button of some kind (that part’s completely up to you):
-- single product purchase
store.purchase( { "android.test.purchased" } )
– multi-item purchase
store.purchase( { “android.test.purchased”, “android.test.canceled” } )
Once again, you can use the test products provided by Google (as shown in the example), but you’ll obviously want to substitute them with your own product identifiers at some point.
For more information, please see the existing store.purchase() documentation.
Cross-Platform Development
For those of you who are taking full advantage of what Corona has to offer by publishing your apps across many different platforms, we’ve added a new property to the store API that allows you to easily check if a store is available on a specific type of device.
Since Apple’s In-app Purchases are only available on iOS devices, and Android In-app Billing are available on devices with Google Play (Android Marketplace) only (so no Nook or Kindle Fire), you can use the newly added store.availableStores table to easily check which stores are available for the device you’re targeting.
Here’s an example of how to use it with an app that has In-app purchases for iOS, and In-app Billing for enabled Android devices:
if store.availableStores.apple then
store.init("apple", transactionCallback)
elseif store.availableStores.google then
store.init(“google”, transactionCallback)
end
The biggest difference between iOS In-app Purchases and Android In-app Billing (besides the name) is that you cannot retrieve information on individual In-app products with Android as you can with iOS. This is a Google-imposed limitation. What this means to you is that you’ll need to store the product information (such as name, description, etc.) on your own, either within your app or on an external server—whichever works best for your app, of course.
With that said, the store.loadProducts() function works for iOS in-app purchases only. If you’re developing a cross-platform app, you can check if the function is available by using the new store.canLoadProducts property (which returns true or false depending on which platform you passed with store.init()).
Additional Resources
To see a working example of Android In-app Billing, which uses Google’s test product Id’s by default (but you can easily substitute for your own product Id’s for testing purposes), please see the updated ‘InAppPurchase’ sample in the Corona SDK download located at:
/SampleCode/Networking/InAppPurchase
Also, please see the existing In-app Purchases Guide, which was originally written for iOS in-app purchases, but most of also applies to Android In-app Billing. And of course, I recommend taking a look at the In-app Purchases API Reference for information on individual store API functions.
Remember, this new feature is available to subscribers-only at the moment from the Daily Builds page (build 2012.760 or later). If you’re a trial user and want to take advantage of this popular and often very lucrative monetization strategy, subscribe now so you can get the latest build today.




Ingemar
Nice tutorial, thanks!
However, it seems like the “refunded” state never gets called in a real world scenario.
I’ve implemented this in my own app and done some testing. Refunds for IAP Purchases have to be initiated by the merchant. When I issue a refund in my Google Checkout page the event is never sent to the device.
I may be missing something, but can’t figure it out.
Piotr Blerdo.com
But how i can get localized price on android if there is no store.loadProducts() ?
Now on iOS i get it by event.products[i].localizedPrice in the store.loadProducts() event handler.
How to handle this on Android?
Joshua Quick
Piotr,
The Android Marketplace does not support fetching product information. So fetching localized prices is NOT an option. This means you should not display prices within your Corona app on Android. Instead, you should just list the products within your app and when you call store.purchase(), Android’s purchase screen will display the localized price.
Have a look at our sample app “NetworkingInAppPurchase” that is included with the Corona SDK. Notice that we do not display prices within the app on Android, but we do show the prices on iOS. See that app’s setupMyStore() function on how we do this.
Philipp Lenssen
Is it possible to allow for a similar “code once, run on different systems” approach Corona follows elsewhere? Specifying e.g. google as an argument seems to suggest developers will need to support two different approaches in the same program. I suppose both functionalities are very different behind the scenes, but is it possible to hide these differences as best as possible, like you guys do with the rest of Corona?
Anyway, thanks!
Adrian Tymes
Is usesPermissions a new element in build.settings? Is there documentation for it yet? (Previously, permissions for android were set in settings->androidPermissions, rather than settings->android->usesPermissions.)
Joshua Quick
Philipp,
Specifying which store you will init with is absolutely essential unfortunately. You see, Google’s in-app purchase feature is not available on all devices or countries. This means that an Android app developer would need to support multiple in-app purchase solutions in order to maximize his/her profits. Ansca plans on adding at least 1 more in-app purchase solution on Android for this release, making this necessary.
That said, if you omit the store name in the store.init() function call, then it will attempt to automatically select a store for you. So the store API is still backward compatible, but please realize that store.init() will fail if Google in-app purchasing is not available on that device/region, which is why we added the store.isActive property. Also realize that Google does not support store.loadProducts(), so you’ll have to handle that on your end too. There’s nothing Ansca can do about that because that is a limitation imposed by Google. Also, your product string keys might differ between Apple and Google, so that may be another gotcha.
Please have a look at our sample app “Networking/InAppPurchase” that is included with the Corona SDK. That app supports both Apple and Google in-app purchases. I think what we’ve put together is an elegant solution considering the differences between store behaviors.
Joshua Quick
Adrian,
The “settings->android->usesPermissions” is new as of release build 704. No we have not documented it yet. We’re in the middle of reorganizing things so that all Android related settings are always under the android table… just like how it is for iOS… and eventually our Mac app solution. We still support the “androidPermissions” table and its permission settings will be correctly merged with the “usesPermissions” table when your app is built. In fact, you can put the billing permission in the “androidPermissions” table and it will still be injected into your built APK correctly.
Bob Dickinson
Looks great guys. Now we just need Amazon support.
Piotr Blerdo.com
Joshua thank you for clarification on this.
Andrew
Joshua – I am trying to download the In-App-Purchase demo. But All I can find is the InAppDemo from:
http://developer.anscamobile.com/sample-code/networking
It looks like the code in that demo was from early 2011 and only supported iOS. Do you have a different Demo in mind that you say supports Google IAP as well? Thx!
Henry
Apologies if this is a dumb question but does this work with any android store (eg Amazon App store, BN Nook and Google Play). Or just with Google Play (Android Market).
Henry
Just got my answer to “does this work with any android store?”
Currently Amazon and Nook don’t have this feature.
http://www.ubergizmo.com/2012/04/amazon-reportedly-testing-out-in-app-purchase-feature-for-their-appstore/
Naomi
I have a question regarding the “failed” event. I’d like to know how the transaction could “fail” in terms of timing.
The reason why I’m asking this is because I have tested a conflicting cases for “cancelled” event. (Transaction can be cancelled before as well as after the “purchased” event, which would require two separate handling.) There’s a work-around for it, so it’s non-issue for me at the moment. (It is described here.)
As for the “failed” event, I could be completely wrong, but I can think of two possible reasons why it could fail and when:
Case 1: After the user places an order, Google Checkout approves the purchase, and my game triggers the “purchased” event. Then, somewhile later, as the Google Checkout processes the credit card payment, it finds a problem with the credit card and notifies the game that the transaction failed. At this point, the game will need to remove the purchased item.
Case 2: After the user places an order, Google Checkout does not approve the purchase (because, say, the user’s credit card has already reached its max spending, etc.) and immediately notifies the game that the transaction failed. Under this scenario, the game would notify the user that he/she could not purchase the product.
So, what I want to know is, how would I handle this two different scenario. Or are there two scenarios at all? Does “failed” event always happen only one of the two cases (most likely, the Case 2)? I’d so appreciate some clarification (since I can’t test this case to work out what to do.)
Joshua Quick
Andrew,
The Android in-app purchase feature is not available in our release build (#704) yet. It is only available via our daily builds, which only our paid subscribers have access to.
Henry,
This in-app purchase feature only works with Google’s Android marketplace. It does not work with Amazon’s or Barnes&Noble’s app stores. We plan on adding Amazon in-app purchase support in the near future. Barnes&Noble does not have an in-app purchase solution yet.
Naomi,
Regarding Case 1 & 2, the “transaction.errorType” should be set to “invalidPayment”. Are you saying that the Android marketplace is sending you a PURCHASED notification and then reversing it saying it was invalid? If so, that’s the first I’ve ever seen that, but that would also be beyond our control since Corona merely “passes the message” to your Lua code. If the marketplace sends you a PURCHASED notification, then you app has no choice but to trust it because you have no means of predicting if the marketplace will later reverse it. You are the first person I’ve heard of that has mentioned this… but I’ll keep a sharp eye out for anyone else who has ran into similar issues.
Naomi
Joshua, thank you for the response. Yes, Android marketplace is sending me a PURCHASED notification immediately after the test user places an order. And because I was testing this process, I went to Google checkout the moment the PURCHASED notification game. There I saw the credit card payment being processed (i.e., payment has been authorized but actual payment has not been made yet). It looks like until the payment is fully processed, we have an option to cancel the order (instead of refunding the order). So, I cancelled the order, which sent the “cancelled” event notification to my game (as well as email notice to the test user). As I noted elsewhere, I can workaround it by never canceling any order but simply wait for the payment to go through, and then refunding the user instead.
As for the FAILED event, I suppose it will happen before the PURCHASED event is sent? Let’s say, it could be caused by connectivity failure or by user’s credit card risk factor being too high? The more I think about it, the chances of FAILED event happening after the PURCHASED event is rather very small.
Thanks again.
Naomi
Alex
In the iOS store, if I need to restore any product purchase all I need to call is:
store.restore()
How to proceed in the case of Google Play for the refunds?
iqSoup
Alex, did you ever get an answer on this? Do we called store.refund() or something like that? I’m pretty confused on how to get refunds working. Can anyone else enlighten me? Thanks!
Andrew
Joshua –
I have been working on adding in-app-billing to one of our products. I am running into a weird (intermittent) problem where Google Play doesn’t respond to the purchase request. My IAP Product is published, my app is signed and uploaded into Google Play, my test device has a different GoogleID than the one used for my Google Play developer account. When I push the purchase button (on Device), I sometimes get the proper purchase screen and can complete purchase. But most of the time, I get a page with the “Accept & Download” button, I push the button, and I don’t get a transactionCallback. I even tried the test code with my IAP Product, and the test code fails. So, I am fairly sure it has to do with either the way my product is setup, or the way I am conducting the testing. Any thoughts/pointers would be much appreciated.
henry.liu
My android app has in-app billing, and i can buy the items in my app and the credit card has paid.
And I want to send the recepit to our server to signature verification.
I send the ‘event.transation.recepit’ to our server, the data was like 64382835091xx96 or 90328327062xx88, (xx are two numbers) . And our server can’t handle that data.
How can I get the data that the server need to perform signature verification step through corona’s IAP api?
hikim
Buy enterprise license, and write native code.
Ben
Hi all,
I notice the following when I run the sample app (InAppPurchase) on an Android device:
1. The transactionCallback( event ) method is called twice when I click on “Lemonade refill” button.
2. Printouts of the first call:
event: storeTransaction
state: purchased
errorType: none
errorString:
3. Printouts of the second call:
event: storeTransaction
state: failed
errorType: invalidClient
errorString:
My question is why are there 2 calls to transactionCallback( event ) and why did the 2nd call fail?
Not sure if this helps but my Google account has been setup and I’m able to make purchases for other apps.
Any help is appreciated.
Regards,
Ben
Emi
Ben, that’s exactly what’s happening to me. Did you solve this issue?
Thanks
Anthony
Ben & Emi – This issue happened to me as well, but only when my transactionCallback erred out before I could call store.finishTransaction( transaction )
Anthony
It looks like the transactionCallback is getting called on a separate thread, and it doesn’t have access to any of the variables defined in the file. Updates to the UI didn’t seem to work either.
One suggestion for anyone out there that runs into this problem would be to use a custom event listener in the main thread, and use Runtime:dispatchEvent( event ) from transactionCallback to update the UI after a purchase.
Richard Harris
Hi guys, does anyone know if the current iteration of in-app billing for Corona and Android supports in-app billing version 3 API by Google?
I have done some testing and can pull back and purchase consumable products, but subscriptions return “item not found”..
Thanks!
Derek Liu
This article should be updated as Corona had updated for Google in-app billing v3 and support for signature verification and other functions in Google billing V3. Any links to references and examples would be great.