With version 3 of the Billing API, Google has removed the distinction between consumable and non-consumable products. Both have been combined into a new type called "managed" and behave somewhat like a hybrid: Your app needs to actively call a method to "consume" the items. If that is never done for a set of skus, those items basically behave as if they were non-consumable.
The documentation describes the intended purchase flow as follows:
Launch a purchase flow with a getBuyIntent call.
Get a response Bundle from Google Play indicating if the purchase completed successfully.
If the purchase was successful, consume the purchase by making a consumePurchase call.
Get a response code from Google Play indicating if the consumption completed successfully.
If the consumption was successful, provision the product in your application.
I see two problems with this approach. One is fairly obvious and more a "bug" in the documentation than the API, but the other is rather subtle and I still haven't figured out how to best handle it. Let's start with the obvious one for completeness:
Problem 1: Lost purchases on single device:
The docs say that an app should call getPurchases every time it is launched to "check if the user owns any outstanding consumable in-app products". If so, the app should consume these and provision the associated item. This covers the case where the purchase flow is interrupted after the purchase is completed, but before the item is consumed (i.e. around step 2).
But what if the purchase flow is interrupted between step 4 and 5? I.e. the app has successfully consumed the purchase but it got killed (phone call came in and there wasn't enough memory around, battery died, crash, whatever) before it had a chance to provision the product to the user. In such a case, the purchase will no longer be included in getPurchases and basically the user never receives what he paid for (insert angry support email and one-star review here)...
Luckily this problem is fairly easy to fix by introducing a "journal" (like in a file system) to change the purchase flow to something more like this (Steps 1 and 2 same as above):
If the purchase was successful, make entry into journal saying "increase coins from 300 to 400 once purchase <order-id here> is successfully consumed."
After journal entry is confirmed, consume the purchase by making a consumePurchase call.
Get a response code from Google Play indicating if the consumption completed successfully.
If the consumption was successful, provision the product in your application.
When provisioning is confirmed, change journal entry to "purchase <order-id here> completed".
Then, every time the app starts, it shouldn't just check getPurchases, but also the journal. If there is an entry there for an incomplete purchase that wasn't reported by getPurchases, continue at step 6. If a later getPurchase should ever return that order ID as owned again (e.g. if the consumption failed after all), simply ignore the transaction if the journal lists this order ID as complete.
This should fix problem 1, but please do let me know if you find any flaws in this approach.
Problem 2: Issues when multiple devices are involved:
Let's say a user owns two devices (a phone and a tablet, for example) with the same account on both.
He (or she - to be implied from now on) could try to purchase more coins on his phone and the app could get killed after the purchase completed, but before it is consumed. Now, if he opens the app on his tablet next, getPurchases will report the product as owned.
The app on the tablet will have to assume that the purchase was initiated there and that it died before the journal entry was created, so it will create the journal entry, consume the product, and provision the coins.
If the phone app died before it had a chance to make the journal entry, the coins will never be provisioned on the phone (insert angry support email and one-star review here). And if the phone app died after the journal entry was created, the coins will also be provisioned on the phone, basically giving the user a purchase for free on the tablet (insert lost revenue here).
One way around this is to add some unique install or device ID as a payload to the purchase to check whether the purchase was meant for this device. Then, the tablet can simply ignore the purchase and only the phone will ever credit the coins and consume the item.
BUT: Since the sku is still in the user's possession at this point, the Play Store will not allow the user to buy another copy, so basically, until the user launches the app again on his phone to complete the pending transaction, he will not be able to purchase any more virtual coins on the tablet (insert angry support email, one-star review, and lost revenue here).
Is there an elegant way to handle this scenario? The only solutions I can think of are:
Show a message to the user to please launch the app on the other device first (yuck!)
or add multiple skus for the same consumable item (should work, but still yuck!)
Is there a better way? Or am I maybe just fundamentally misunderstanding something and there really is no issue here? (I realize that the chances of this problem ever coming up are slim, but with a large enough user-base, "unlikely" eventually becomes "all-the-time".)
Here's the simplest way to fix all this, that I have come up with so far. It's not the most elegant approach, but at least it should work:
Generate a globally unique purchase ID and store it locally on the device.
Launch a purchase flow with getBuyIntent with the purchase ID as the developer payload.
Get a response Bundle from Google Play indicating if the purchase completed successfully.
If purchase was successful, provision the product and remember the purchase ID as completed (this must be done atomically).
If the provisioning was successful, consume the purchase by making a consumePurchase call(I do this in a "fire-and-forget" manner).
Every time the app is launched, go through the following:
Send a getPurchases request to query the owned in-app products for the user.
If any consumable products are found, check if the purchase ID in the developer payload is stored on the device. If not, ignore the product.
For products with a "local" purchase ID, check if the purchase ID is included in the completed-list. If not, continue at step 4 above, otherwise continue at step 5 above.
Here's how things can go wrong on a single device and what happens then:
If the purchase never starts or doesn't complete, the user doesn't get charged and the app goes back to the pre-purchase-state and the user can try again. The unused purchase ID still is in the "local"-list, but that should only be a fairly minor "memory-leak" that can be fixed with some expiration-logic.
If the purchase completes, but the app dies before step 4, when it gets restarted, it finds the pending purchase (the product is still reported as owned) and can continue with step 4.
If the app dies after step 4 but before the product is consumed, the app finds the pending purchase on restart, but knows to ignore it as the purchase ID is in the completed-list. The app simply continues with step 5.
In the multiple-device-case, any other device will simply ignore any non-local pending purchases (consumables reported as owned) as the purchase ID is not in that device's local list.
The one issue is that a pending purchase will prevent other devices from being able to start a parallel purchase for the same product. So, if a user has an incomplete transaction stuck somewhere between step 2 and 5 (i.e. after purchase completion, but before consumption completion) on his phone, he won't be able to do any more purchases of the same product on his tablet until the app completes step 5, i.e. consumes the product, on the phone.
This issue can be resolved very easily (but not elegantly) by adding multiple copies (5 maybe?) of each consumable SKU to Google Play and changing step 2 in the first list to:
Launch a purchase flow for the next available SKU in the set with getBuyIntent with the purchase ID as the developer payload.
A note on hackability (in order of increasing difficulty for the hacker):
Completing fake purchases via Freedom APK or similar:These apps basically impersonate the Google Play Store to complete the purchase. To detect them, one needs to verify the signature included in the purchase receipt and reject purchases that fail the check, which most apps don't do (right). Problem solved in most cases (see point 4).
Increasing in-app account balance of consumable via Game Killer or similar:These apps will try to figure out where in memory (or local storage) your app stores the current number of coins or other consumable products to modify the number directly. To make this harder (i.e. impossible for the average user), one needs to come up with a way to store the account balance not as a "plain-text" integer, but in some encrypted way or along with some checksums. Problem solved in most cases (see point 4).
Killing the app at the right time and messing with its local storage: If someone purchases a consumable product on their phone and manages to kill the app after the product has been provisioned but before it has been consumed (likely very difficult to force), they could then modify the local storage on their tablet to add the purchase ID to the local list to have the product awarded once on each device. Or, they could corrupt the list of completed purchase IDs on the phone and restart the app to get the award twice. If they again manage to kill the app after provisioning but before consumption of the product (easy now by simply setting the phone to airplane mode and deleting the Google Play Store Cache), they can keep stealing more and more product in this way. Again, obfuscating or checksumming the storage can make this much harder.
Decompiling and developing a patch for the app:This approach, of course, allows the hacker to pretty much do anything they want with your app (including breaking any countermeasures taken to alleviate points 1 and 2) and it will be extremely hard to prevent entirely. But it can be made harder for the hacker by using code obfuscation (ProGuard) and overly complex logic for the critical purchase-management code (might lead to buggy code, though, so this is not necessarily the best idea). Also, the code can be written in a way that its logic can be modified without affecting its function to allow for regular deployment of alternate versions that break any available patches.
Overall, signature verification for the purchases and some relatively simple but non-obvious checksumming or signing of the relevant data (in memory and in the local storage) should be sufficient to force a hacker to decompile (or otherwise reverse-engineer) the app in order to steal product. Unless the app gets hugely popular this should be a sufficient deterrent. Flexible logic in the code combined with somewhat frequent updates that break any developed patches can keep the app a moving target for hackers.
Keep in mind that I might be forgetting some other hacks. Please comment if you know of one.
Conclusion:
Overall, this is not the cleanest solution as one needs to maintain multiple parallel SKUs for each consumable product, but so far I haven't come up with a better one that actually fixes the issues.
So, please do share any other ideas you might have. +1`s guaranteed for any good pointers. :)
First of all I want to say I agree with everything you wrote. The problem exists and I would try to solve it similarly to how you did it. I would really suggest to find someone from Google Play relation team and make them aware of it.
Now back to your solution. This is probably the best standalone solution involving no server I could think about. It's simple but fairly good. One place where it can be misused would be when attackers would fake journal file and "buy" whatever they want, because getPurchases won't return anything from a manipulated journal file.
Otherwise, what else I would try to do is to reduce a probability the app gets killed by the system. For that you might extract purchasing and consumption logic into a smaller foreground service running in a separate process. This will increase probability the service finishes its work, even when Android will kill the bigger game application. More complex, but also a more reliable solution would be to implement journal on the server and share it between devices. With this solution you can always check whether someone is cheating with the purchases and even solve the issue when multiple devices are involved.
My app has been pirated recently and I'm trying to prevent further losses in revenue.
I am using an In App Purchase system to purchase the premium version of the app.
Apparently someone bought my app and released a pirated version by doing a backup (where premium= true)
Now i would like to run an IAP check when my app is installed to check if the user really has purchased the premium version.
Is there a way to do this as soon as my app is installed or updated?
UPDATE:
I have to run the IAP without using SharedPreferences as they can easily be backed up and the backup distributed
Why not just check it once a day (or week)? You can set the SharedPreference to the last time a successful check was completed. Of course, you'll want to check for future dates and reject those, since that's what I'd do to get around a system like that. You might want to give a 24-hour leeway on future dates to accommodate for time-zone changing, but it shouldn't be too hard.
This way, even if someone backed up your app, it would only work for the next day or so before checking again.
To be honest, though, this might not help much. If people are pirating your app, they'll find ways to get around (just about) anything you throw at them.
I've seen this question: Android - how to check if in app purchase has already been done?
It is not very clear to me however. I am using the AndroidBillingLibrary, mentioned in the answer, and I have a couple of questions.
When using restoreTransactions(), does that mean that at that point the purchases are stored on the device itself? Couldn't that be manipulated somehow?
And what if I buy an item on an other device, how does the first device know this? Do I need to restore again?
Thanks in advance
For the first question, every purchase is managed with an ID, thus only managed items work with restoreTransactions(). Also, restoreTransactions() does not work for reserved Product IDs. Thus it is still safe.
Ideally you should call restoreTransactions() every time the app runs. The Key thing is that all transactions are paired with the device/user ID (not sure how Google handles authentication on their server side but I assume it is using a combination of your Google account as well as the phones that the Google account is paired to).
So, if someone were to modify the transaction file, it will get invalidated when it is compared to the logfile on Google's side.
Hope this clarifies things :)
Some people trying to do in-app purchase within my Android game report that they can never complete the purchase -- that they always get an error message. (I.e., The market app reports an error to my application, and I show the user this error.)
Unfortunately, I don't have any real log data for this one, because it only happens for certain customers of my game, not for me.
What's strange is that after updating to a new version of the app, for some users the problem goes away, and for some users the problem starts. So user A might have the problem in version 1 of the app, but it clears up when they update to version 2. User B might not have the problem in version 1, but it appears when they update to version 2.
I say "intermittent" above, but by that I mean that it only seems to affect a small number of users. But for any given user, once they get in this state, they seem to get the message all the time. HOWEVER, I have had some cases where the problem does clear up suddenly, without an app update. I'm not sure if, for instance, power cycling, or, say, making an in-app-purchase in another app is away to "break out" of this state.
I realize that without a specific error message or API call to name/blame, this question is difficult to answer. I'm just trying to understand if this pattern -- of some users mysteriously getting stuck in a state where they are unable to make any in-app-purchase within a given app -- sounds familiar to anyone for Google Play in-app-purchase.
Also, I build my apps on top of Marmalade, so it's possible that the problem is in the Marmalade layer, not in my app or in the Google Play market itself.
The same problem here: a lot of users can buy items in my app but some users send me emails complaining why they can't buy considering that they can successful purchase items in other apps. I think it occurs because of a temporary problem on the Google Play server or is something to do with the service on Android. I am still looking for an answer to this weird problem...
I've implemented in-app billing and I'm running into an issue with it. Here is what I see.
Place an order for an item
Wait for a little while for the order to go through
If the purchase is taking a while, the user hits the back button to cancel the purchase
My app gets notified that the purchase was canceled and it confirms this
The user and myself both get an email stating that the purchase was canceled
When the user attempts to purchase the item again, the Market throws an error saying "You already have a pending order for this item."
The response code is "Service Unavailable"
Restore transactions yields no transactions
You can't ever purchase this item with this account
I have found some information about this on the web.
http://www.google.com/support/forum/p/Android+Market/thread?tid=375490c831e02ab5&hl=en
http://code.google.com/p/marketbilling/issues/detail?id=39
I contacted Google and got an autobot response that they are looking into this.
However, I'm wondering if there is anything I could be doing to cause this.
Oh, and I've also made sure my PendingIntent is good. I have had successful orders.
Update:
Here is my stock e-mail to customers that see this. It seems the Android Market has been getting better, as I get fewer and fewer of these now anyhow.
Hello,
This "pending order" error is unfortunately a bug in the Android Market that I cannot control.
To help them raise the priority of this issue, please contact Google at the following web form.
http://www.google.com/support/androidmarket/bin/request.py?contact_type=market_phone_tablet
You can tell them to reference bug 5126349, which is their internal tracking number for this.
While there are issues with in-app billing like the links presented here (i was active commenter on issue 39), there are some worthwhile things to check on as well that can cause a 500 response from google.
Namely, "restore transactions" and abusing that call. Abuse of that call is done on a per user basis and i've seen a google account get blocked with a 500 for a few days, which can certainly happen during development to you depending on how you implement.
Best advice here is to expand your testers to more users (or devices with different primary google accounts), running different versions of the android market. If you can get one positive response from someone, then more than likely you're good to go and the rest is at google's door.