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".)