Google Play's IAP Auto-Refunds Big Problem

Apparently, a number of my players are purchasing the ‘removeAds’ non-consumable, then when they finish playing for a couple of days (without ads), they are uninstalling and/or requesting refunds. This is accounting for far too many purchases whereby they enjoy the benefit and then get a refund. Is there any way to prevent this auto-refund in the Google billing API? When I looked online, it seems others have experienced this auto-refund problem.

The problem is deeper. I can now see a $49.99 purchase to unlock everything in the game, only to see that 3 days later, they are getting a refund. And here’s the kicker. My database is showing that they are still playing the game while enjoying the benefits of a completely unlocked game.

Is there a process we can go through each time they load up the game to test if their non-consumable purchases are still valid?

As I’m looking online, I’m reading that this may also have something to do with our IAP plugin failing to call the Google API for acknowledgePurchase. What do you think?

In my product purchase callback, after I see event.transaction.state==“finished”, I am already calling store.finishTransaction(event.transaction). Does our IAP plugin (google.iap.billing) use this syntax to call the Google API for “acknowledgePurchase”?

Any sincere help is very much appreciated.

1 Like

One of the changes (and really only one) when updating to billing library was to make sure you’re calling finished callback. Please make sure you are calling it:

1 Like

Yes I am. I will double check, but if I am as expected, then that means something in our plugin is not properly calling the acknowledgePurchase method.

Is a delay required between when we receive the “finished” callback and when we need to call store.finishTransaction?

I have solved this issue and am reporting what I found for the benefit of our Solar2D community.

When I originally implemented the new plugin.google.iap.billing, the ‘state’ returned after a purchase on android was NOT ‘purchased’, it was always ‘finished’. So for android, different from iOS, that’s when I called store.finishTransaction.

However, somehow, without anyone letting me know, the plugin’s behavior (or more likely Google’s API response) changed to properly send a ‘purchased’ state, instead of a ‘finished’ state.

In other words, when the plugin.google.iap.billing initially came out, Google was doing the store.finishTransaction (i.e. the acknowledgePurchase) automatically.

But this has since changed.

So, to fix this problem, I am doing the following:

When either a ‘finished’ or ‘purchased’ state comes in the callback, I make sure that if store.finishTransaction has not yet been called for the current transaction, it is now called. I keep a temporary variable for the transaction.identifier to check if store.finishTransaction was already called for this transaction, and if so, it doesn’t result in calling store.finishTransaction twice for the same transaction. This now works.

On a positive note, once this was implemented, the store.restore now works properly again, too.

4 Likes

Wow. Btw, I don’t think anything wrong would happen if you call finishTransaction several times. All other transactions would be no-ops.

Above, I wrote that on Android, when I receive a ‘finished’ or ‘purchased’ state in the callback, I then call store.finishTransaction. For consumables, it is vital to call store.consumePurchase, or the store will not allow any additional purchases of the same product. However, there must be a delay before calling store.consumePurchse after calling store.finishTransaction or you may end up receiving an error from the server. I use a delay of half a second (timer.performWithDelay(500,…)

I have also inserted some code to consume products in the event of an error within the transaction callback as follows:

--used primarily for android
if (event.transaction.isError) then
    local errorType=tostring(e.transaction.errorType)
    --errorType==6, errorString="Server error, please try again."
    --errorType==7, errorString="Unable to buy item (response: 7: Item Already Owned)"
    if G.platform=="android" and (errorType=="6" or errorType=="7") then
        timer.performWirthDelay(500,function () store.consumePurchase(transaction.productIdentifier) end)

I use G for my global variables and G.platform to "ios" or "android" - you can change to however you check for a specific platform OS.

Also, if may not be necessary to convert the errorType to a string; I just get in the habit of doing this occasionally to avoid runtime errors that occur from data mismatches.
1 Like

Hi @troylyndon
I’ve noted a mistake in your code:
Your delay is wrong, you put timer.performWirthDelay, instead of timer.performWithDelay. Note the r between the i and t

1 Like