NEW WARNING: Google Play Billing

Yes i had the exact problem where the callback for the relevant state/phase was being triggered anywhere between 1-4 times. Initially my workaround was to use a timer of a few millis and just block the actions by using a boolean for the duration of that timer but then i noticed that this was only happening for me in live builds.

Perhaps it’s a problem that was temporarily fixed and then returned in one of the later commits of this plugin so I’ll check again tonight and see if i can reproduce it.

yeah its very strange. i just tried testing with using
[“plugin.google.iap.billing.v2”] =
{
publisherId = “com.solar2d”,
version = “v10”,
},
as @colinmorgan suggested. also tried v9
but still the same problem with multiple purchase states.

If you install the last .apk that was working, is it still working? (In case something changed on Google’s side.)

Ok heres some info after banging my head left right and center.
i found some apps are working correctly and some are not. And i found the difference is when i have
store = require( “plugin.google.iap.billing.v2” )
and initialize it in
main.lua
and then call store.purchase from a scene. It is working as intended. Although a major issue as described in the last paragraph of this.

The issue i was having with the apps that had the multiple purchase states. I had the following in the scene lua file (lets say example shop.lua)
local store = require( “plugin.google.iap.billing.v2” )
and initialize it in the shop.lua file.

What seems to be happening, every time it opens that scene, it is initialising the store over and already initialised store. which should not be the case, as store is set as local store. have never encountered this problem before. It seems each time you initialize the store even if its local it is being stored in memory so each time it is initialized its another instance of it which is active.

Now i have another totally WIERD issue. So if the store is initialised in main.lua. every time i close the app, make sure it is swiped closed and then reopen the app, it starts from the splash screen, it seems to be reinitialising the store even though in the background the previous store seems to be still functioning??? so every time i open the app fresh from the splash screen, for example if i had opened the app twice, the purchase state runs twice. if i close the app and start it fresh again, the purchase state will run 3 times. it seems the transactionCallback is getting stuck in memory and being duplicated everytime it is initialised even if the app is opened from the splash screen hence the multiple purchase.

It’s lucky there is not a looming deadline on moving to the new billing library…

One thing I’ve always done is cache transaction numbers. This also stops replay purchase hacks.

1 Like

@anon63346430 - Great advice! Kudos my friend.

I’m going to cache transaction numbers as well from the section of code that provides the purchased award (i.e. Lives and Gold Coins).

Quick question - I’ve been thinking some time to ignore purchases where the Google Play receipt does not include “GPA”. Have you found some purchases without GPA that are legit? For example, a typical receipt looks like {“orderId”:“GPA.3343-7306-9404-38030”… by requiring GPA, I’m thinking to potentially eliminate purchases that don’t occur through Google Play, presumably hacked orders. What has been your experience with this, and do you think my idea to require the GPA is a good one?

@anon63346430 although it’s not required to update before then… Any new updates you put live from 1st November DOES need to use the new billing library as the original post explains

@chris_raz you are correct :slight_smile:

Is anyone else getting the multiple purchase states on their builds? or is it just me? i have tried high and low to find the problem but cant seem to find a fix. im wondering if there is something im doing or the plugin itself?

I allow players to make fake purchases (I get hundreds per day) but I then ban them from all internet features - this way no other player will see them.

I used to block players but got hundreds of 1 star reviews - I know right?

So now I let them cheat but block them from other players and remove the from my KPIs so I worry not about them.

1 Like

Will take a look at this over the weekend

Any news on this issue because now in November an update of the game can no longer uploaded to Google!?

Which issue in particular? (There have been a lot brought up in this thread…) I’m running the latest version of the plugin with no issues. +/- 1,000 transactions in the last 30 days, nothing out of the ordinary.

1 Like

I’m running the latest, too, without issues.

Thx!
So plugin.google.iap.billing.v2” is the latest and working fine now without any issues, correct?

Im still getting the multiple purchase states. its very evident especially on consumables.
Although i would like to say it is not happening every time!
The way i seem to be able to catch it is, im adding a alert on transaction.state == “purchased” where productIdentifier is whats being purchased. EG 1000stars

When i go into the shop scene, and purchase, it shows the alert once. and 1000 stars are credited. (ALL good, working as expected)
When i exit the shop scene back to the menu scene, then open the shop scene again, purchase the 1000 stars again, I get two or three alerts, and 3000 stars are added. hence it seems to be looping through the same transaction.

For the time being i have added @anon63346430 solution above but caching transaction receipts so it checks if that purchase is already given, and if it is, dont give again. although this is a workaround to what is a problem as it loops through the same transaction.

Thank you for letting me know. Is it possible for you to post your transaction listener code maybe how you are doing it?

Sure.
Below is my transaction listener

local function transactionCallback( event )
	if ( event.name == "init" ) then
      	if not ( event.transaction.isError ) then
			 if PriceLoaded == nil then
                store.loadProducts( listOfProducts, productCallback )
            end
        else  -- Unsuccessful initialization; output error details
            print( event.transaction.errorType )
            print( event.transaction.errorString )
        end
 
    -- Store transaction event
    elseif ( event.name == "storeTransaction" ) then

        local transaction = event.transaction
        if transaction.state == "purchased" then
        	 if transaction.productIdentifier == "carstars1000" then
    	    	GiveStars(1000)
                local alert = native.showAlert("1000 stars","1000 stars added", { "OK"  }, onAlertCompleteIAP )
                timer.performWithDelay(1000,ConsumeIt)
            end
           

        elseif transaction.state == "restored" then
        
           -- You'll never reach this transaction state on Android.


        elseif transaction.state == "refunded" then
        
            -- Android-only; user refunded their purchase
            --local productId = transaction.productIdentifier

            -- Restrict/remove content associated with above productId now

        
        elseif transaction.state == "cancelled" then
        
            -- Transaction was cancelled; tell you app to react accordingly here


        elseif transaction.state == "failed" then
        
            -- Transaction failed; tell you app to react accordingly here
        	
        end
    	if (transaction.isError) then
    	    local errorType=tostring(transaction.errorType)
    	    --errorType==6, errorString="Server error, please try again."
    	    --errorType==7, errorString="Unable to buy item (response: 7: Item Already Owned)"
    	    if errorType=="7" then
    	        ConsumeIt()
    	    end
        end
        -- The following must be called after transaction is complete.
        -- If your In-app product needs to download, do not call the following
        -- function until AFTER the download is complete:

    	store.finishTransaction( transaction )
    end
end

Thank you very much! Much appreciated!

I noticed you are not checking for "consumed". Potentially your consumeIt() may try and consume before the transaction has finished processing. I keep trying to consume until it actually gets consumed. You also are not handling deferred purchases. (I’ve stripped my code down to keep it simple)

      --process transaction
      if transaction.state == "purchased" then
        if _isGoogle then
          --Google purchases must be consumed
          if timerID then timer.cancel(timerID) end
          timerID = timer.performWithDelay( 5000, function()
            store.consumePurchase( transaction.productIdentifier )
          end, -1 )
        end

        --award player now
        if isOrderNumberUnique(transaction.identifier) then
          iap:doCredit(transaction.productIdentifier)
          if callback then callback("yes"); callback = nil end
        else
          if callback then callback("no"); callback = nil end
        end

      elseif transaction.state == "consumed" then
        --this is Google only
        if timerID then timer.cancel(timerID) end

      elseif transaction.state == "failed" then
        if callback then callback("failed"); callback = nil end

      elseif transaction.state == "pending" then
        if callback then callback("pending"); callback = nil end
        --store this for later
        if string.find(iap.pendingPurchases or "", transaction.identifier or "", 1, true) == nil then
          print("IAP: Storing "..transaction.identifier.." for later")
          iap.pendingPurchases = iap.pendingPurchases..(transaction.identifier or "")..","
        end

      elseif transaction.state == "cancelled" then
        if callback then callback("no"); callback = nil end
      end
      store.finishTransaction( transaction )