Work with bugs-API-logic for supplies in Google Play Billing API version 3 (for anyone who uses supplies with API v3)

With version 3 of the invoicing API, Google has removed the distinction between consumable and non-consumable products . Both were combined into a new type called “managed” and behave like a hybrid: your application needs to actively call the “consume” elements method. If this is never done for the skus set, these elements basically behave as if they were not consumed.

The documentation describes the estimated flow of purchases as follows:

  • Start a purchase stream with a call to getBuyIntent .
  • Get a Bundle response on Google Play that indicates whether the purchase completed successfully.
  • If the purchase is successful, use the consumePurchase purchase.
  • Get a response code from Google Play indicating whether the consumption was successful.
  • If the consumption was successful, indicate the product in your application.

I see two problems with this approach. One of them is pretty obvious and more of a "mistake" in the documentation than the API, but the other is pretty subtle, and I still haven't figured out how to handle it better. Let's start with the obvious for completeness:

Problem 1: Losing a purchase on a single device:

The docs say the application should call getPurchases every time it starts to "check if the user owns any outstanding consumable products in the application." If so, the application should use them and provide a related element. This covers the case when the purchase flow is interrupted after the purchase is completed, but before the goods are consumed (i.e. around step 2).

But what if the flow of purchases is interrupted between steps 4 and 5? That is, the application successfully took advantage of the purchase, but it was killed (a phone call entered, but there was not enough memory, the battery worked, failure, etc.) before it had the opportunity to provide the product to the user. In this case, the purchase will no longer be included in getPurchases , and basically the user never gets what he paid for (insert an angry support email and a one-star review here) ...

Fortunately, this problem is quite easy to fix by entering a "log" ( as in the file system ) to change the purchase stream to something more (steps 1 and 2, as described above):

  1. If the purchase was successful, enter a journal entry that says: "Increase coins from 300 to 400 as soon as the purchase <order-id here> is successfully used."

  2. After confirming the journal entry, buy the purchase by calling consumePurchase .

  3. Get a response code from Google Play indicating whether the consumption was successful.
  4. If the consumption was successful, indicate the product in your application.
  5. When the setting is confirmed, change the journal entry to "purchase <order-id here> completed".

Then, every time the application starts, it should not only check getPurchases , but also the log. If there is some record for an incomplete purchase that was not reported by getPurchases , go to step 6. If a later getPurchase should ever return this order identifier as belonging again (for example, if consumption failed in the end) just ignore transaction if the journal lists this order ID as completed.

This should fix problem 1, but please let me know if you find any flaws in this approach.

Problem 2: Problems associated with multiple devices:

Say a user owns two devices (for example, a phone and a tablet) with the same account on both.

He (or she - implied from now on) can try to buy more coins on his phone , and the application can be killed after the purchase is completed, but before its consumption. Now, if he opens the application on his tablet further, getPurchases will report the product as belonging.

The application on the tablet will have to assume that the purchase was started there and that he died before the journal entry was created, so she will create a journal entry, go to the product and provide the coins.

If the phone application died before he had the opportunity to make a journal entry, coins would never be provided on the phone (insert an angry support email and a one-star review here). And if the phone application died after creating a journal entry, coins will also be provided on the phone, mainly giving the user a free purchase on the tablet (insert the lost income here).

One way is to add a unique setting or device identifier as a payload for the purchase, to check if the purchase was intended for that device. Then the tablet can simply ignore the purchase, and only the phone will ever credit coins and consume goods.

BUT: Since sku is still at the user's disposal at this stage, the Play Store will not allow the user to buy another copy, so basically until the user starts the phone application again to complete the pending transaction, he will not be able to purchase more virtual coins on your tablet (insert an angry support email, one-star review and lost revenue here).

Is there an elegant way to handle this scenario? The only solutions I can come up with are:

  • Show a message to the user to first run the application on another device (yuck!)
  • or add multiple skus for the same consumable item (should work, but still yuck!)

Is there a better way? Or maybe I just fundamentally misunderstand something, and here really is not a problem? (I understand that the chances of this problem ever appearing, but subtle, but with a fairly large user base, are "unlikely" to eventually become "all the time.")

+7
android google-play in-app-billing in-app-purchase
source share
2 answers

Here is the easiest way to fix all this that I have come up with so far. This is not the most elegant approach, but at least it should work:

  • Create a globally unique purchase identifier and save it locally on your device.
  • Run the shopping stream with getBuyIntent with the purchase ID as the developer payload.
  • Get a Bundle response on Google Play that indicates whether the purchase completed successfully.
  • If the purchase was successful, put the product and remember the purchase ID as completed (this should be done atomically).
  • If the provision was successful, use the consumePurchase purchase consumePurchase

    (I do it in the way "fire and forget").

Each time the application starts, proceed to the following:

  • Submit a getPurchases request to request their products in the user application.
  • If any supplies are found, check to see if the purchase identifier is stored on the device in the developer payload. If not, ignore the product.
  • For products with a “local” purchase ID, check to see if the purchase ID is on the completed list. If not, go to step 4 above; otherwise, continue at step 5 above.

Here, how things can go wrong on one device, and what happens afterwards:

  • If the purchase never starts or does not end, the user does not receive a fee, and the application returns to the pre-purchase state, and the user can try again. The unused purchase identifier is still in the “local” list, but it should only be a minor “memory leak” that can be fixed with some expiration logic.
  • If the purchase completes, but the application dies before step 4, when it restarts, it finds the pending purchase (the product is still reported as belonging) and can continue from step 4.
  • If the application works after step 4, but before the product is consumed, the application detects a pending purchase at reboot, but knows to ignore it, since the purchase ID is in the completed list. The application simply continues from step 5.

In the case of multiple devices, any other device will simply ignore any non-local pending purchases (consumables reported as belonging), as the purchase ID is not in this local device list.

The only problem is that the pending purchase will not allow other devices to start a parallel purchase for the same product. Thus, if the user has an incomplete transaction stuck somewhere between steps 2 and 5 (that is, after the purchase is completed, but before the purchase is completed) on his phone, he will no longer be able to make purchases of the same product on his tablet until as long as the application completes step 5, i.e. consumes the product on the phone.

This problem can be solved very easily (but not elegantly) by adding several copies (maybe 5) of each spent SKU to Google Play and changing step 2 in the first list:

  1. Run the shopping stream for the next available SKU in the set with getBuyIntent with the purchase ID as the developer payload.

Hacking note (in increasing order of difficulty for a hacker):

  • Finishing fake purchases through the Freedom APK or similar:
    These applications basically impersonate the Google Play Store to complete the purchase. To find them, you need to check the signature included in the purchase receipt, and reject purchases that do not allow you to verify that most applications do not ( on the right ). The problem is resolved in most cases (see Clause 4).
  • Increase the account balance in the application through Game Killer or similar:
    These applications will try to find out where in the memory (or local storage) your application stores the current number of coins or other supplies to change the number directly. To make this more difficult (that is, impossible for the average user), you need to create a way to store the account balance not as an integer "plain text", but in some encrypted form or together with some checksums. The problem is resolved in most cases (see Clause 4).
  • Killing the application at the right time and messing with its local storage:
    If someone buys a consumer product on their phone and manages to kill the application after the product has been prepared, but before it is consumed (probably very difficult to force), they can then modify the local storage on the tablet to add the purchase ID to the local list so that the product is awarded once on each device. Or they may corrupt the list of completed purchase IDs on the phone and restart the app to receive the reward twice. If they manage to kill the application again after its preparation, but before the product is consumed (now it’s simple by simply setting the phone to airplane mode and deleting the Google Play Store cache), they can continue to steal more and more product in this way. Again, obfuscating or controlling storage can make this a lot more difficult.
  • Decompiling and developing a patch for the application:
    This approach, of course, allows the hacker to pretty much do whatever he wants with your application (including breaking any countermeasures taken to ease points 1 and 2), and it will be extremely difficult to prevent completely. But it can be harder for a hacker to use code obfuscation ( ProGuard ) and overly complex logic for critical purchase control code (possibly lead to erroneous code, so this is not necessarily the best idea). In addition, the code can be written in such a way that its logic can be changed without affecting its function to ensure the regular deployment of alternative versions that violate any available fixes.

In general, verification of the signature for purchases and some relatively simple but unobvious checksums or the signing of relevant data (in memory and in local storage) should be sufficient to force the hacker to decompile (or otherwise reconstruct) the application in order to steal the product. If the application is not very popular, this should be a sufficient deterrent. Flexible logic in the code, combined with several frequent updates that violate any developed fixes, can lead to the application becoming a moving target for hackers.

Keep in mind that I can forget other hacks. Please comment if you know about this.

Output:

In general, this is not the cleanest solution, since you need to maintain several parallel SKUs for each product consumed, but so far I have not come up with the best one that really fixes the problems.

So, please share any other ideas you may have. + 1 is guaranteed for any good pointers. :)

+5
source share

First of all, I want to say that I agree with everything that you wrote. The problem exists, and I will try to solve it the same way you did it. I would really suggest finding someone from the Google Play relationship group and letting them know.

Now back to your decision. This is probably the best standalone solution that didn't have a server that I could think of. It is simple but pretty good. One place where this can be misused is when the attackers fake the log file and “buy” whatever they want, because getPurchases will not return anything from the managed log file.

Otherwise, what else will I try to do is reduce the likelihood that the application will be killed by the system. To do this, you can extract the logic of purchase and consumption into a smaller front service, working in a separate process. This will increase the likelihood that the service will complete its work, even if Android kills a larger game application. A more complex, but also more reliable solution would be to implement the log on the server and share it between devices. With this solution, you can always check if someone is cheating with purchases, and even solve the problem when several devices are involved.

+1
source share

All Articles