Android in-app purchase possible bug: event.transaction.productIdentifier returning wrong ProductId.

Hello. I’m setting in app purchases for my android game, and I have seen same strange behaviors that looks to be some kind of bug.

In my game I have 3 buttons to allow users to buy “coins”

10.000 coins

25.000 coins

40.000 coins

The purchase works but when the callback function is called,  event.transaction.productIdentifier get’s a wrong value, if it is not the first purchase of the user on the game.

Checking the logs generated by print("receipt: " … tostring(event.transaction.receipt)) I noticed, that the callback doesnt return just the last purchase, but  it returns the last 10 purchases, and  event.transaction.productIdentifier  will return the first productId of the purchase list, and not the last one as it should.

This is how it looks:

receipt: {"nonce":2643703143987774433,"orders":[{"notificationId":"2120875375272880860","orderId":"12999763169054705758.1361928580590696","packageName":"net.game.name","productId":"10000coins.gamename","purchaseTime":1374610594000,"purchaseState":0,"purchaseToken":"xwxommmsyyomngprqwwpnmri"},{"notificationId":"-5087359489997754121","orderId":"12999763169054705758.1317937227200979","packageName":"net.game.name","productId":"10000coins.gamename","purchaseTime":1374610661000,"purchaseState":0,"purchaseToken":"ianelarmopftqqtxgsglusns"},{"notificationId":"-8809612338636471508","orderId":"12999763169054705758.1314428912834317","packageName":"net.game.name","productId":"25000coins.gamename","purchaseTime":1374610720000,"purchaseState":0,"purchaseToken":"dycnukexzquoluhattmehohq"},{"notificationId":"5244043765150494203","orderId":"12999763169054705758.1333974387111916","packageName":"net.game.name","productId":"10000coins.gamename","purchaseTime":1374610780000,"purchaseState":0,"purchaseToken":"grehrtyyzdhbyynshrzphwuo"},{"notificationId":"4634449798447896352","orderId":"12999763169054705758.1373356492242957","packageName":"net.game.name","productId":"10000coins.gamename","purchaseTime":1374610804000,"purchaseState":0,"purchaseToken":"tcmraordzttjesdekcpnamjk"},{"notificationId":"7217312897016584546","orderId":"12999763169054705758.1323725642031193","packageName":"net.game.name","productId":"10000coins.gamename","purchaseTime":1374610849000,"purchaseState":0,"purchaseToken":"vqlppfunuyomlabkyttduxni"},{"notificationId":"-7008381092592509620","orderId":"12999763169054705758.1339380580697182","packageName":"net.game.name","productId":"40000coins.gamename","purchaseTime":1374611152000,"purchaseState":0,"purchaseToken":"yjoqkohafzwppulznxwqlxlp"},{"notificationId":"-1090875021658922717","orderId":"12999763169054705758.1352732450791116","packageName":"net.game.name","productId":"10000coins.gamename","purchaseTime":1374611316000,"purchaseState":0,"purchaseToken":"uncliflpqdmqpvktvftwsnqx"},{"notificationId":"-939834656069426704","orderId":"12999763169054705758.1367275796937765","packageName":"net.game.name","productId":"25000coins.gamename","purchaseTime":1374612206000,"purchaseState":0,"purchaseToken":"pderxjndgwypezgxktuaacyg"},{"notificationId":"-2400558402594912129","orderId":"12999763169054705758.1329423969997620","packageName":"net.game.name","productId":"40000coins.gamename","purchaseTime":1374612352000,"purchaseState":0,"purchaseToken":"cbdwvbnuzcfiwsdanibtgxib"}]} I/Corona  ( 3755): signature: VERYBIGSIGNATURECODE==

I’m almost sure this is a bug, so I’d like to hear something from the staff.

For iOs it gets the right responses.


Another bug that I noticed is that even my credit card being not accepted, the result was “purchased”

oh God… Now I discovered that the store.init callback listener is looping forever…

Can you post your code?  Its sounds like you’re never calling the store.finishTransaction( transaction ) bits.

I’m calling it. But it looks like it is not working, exactly like reported in this old post: http://forums.coronalabs.com/topic/29621-storefinishtransaction-is-not-working-properly/

Here is the essence of my callback code. All my purchases are consumable. I did some workarounds to avoid this problem, and for now it is working.

function transactionCallback(event)      if(haspurchased==false) then --I setted this as a workaround for the problem, then after the purchase I change it to true, and avoid from running the callback twice or more.     local infoString         local function CompleteCoinTransaction(coinsbought)             --save coins collected or anything i want         end          if(device.isSimulator ==false) then --check if it is not simulator     print("transactionCallback: Received event " .. tostring(event.name))     print("state: " .. tostring(event.transaction.state))     print("errorType: " .. tostring(event.transaction.errorType))     print("errorString: " .. tostring(event.transaction.errorString))     if event.transaction.state == "purchased" then         infoString = "Transaction successful!"         print(infoString)         print("receipt: " .. tostring(event.transaction.receipt))         print("signature: " .. tostring(event.transaction.signature))            --this is just another workaround, due to the problem if(device.isAndroid) then --if it is android device, get the last purchasedId from the event.transaction.receipt, otherwise it wont get the last purchase local jsonresponse=json.decode(event.transaction.receipt) 

            purchase_product_id=jsonresponse.orders[#jsonresponse.orders].productId

        else

            purchase_product_id=event.transaction.productIdentifier

        end

        

        if(purchase_product_id == “android.test.purchased”) then --if just testing

            CompleteCoinTransaction(10000) 

            print(“Test Purchase”)

        elseif(purchase_product_id == “10000coins.uphills”) then

            CompleteCoinTransaction(10000)

            print(“10.000 coins”)

        elseif(purchase_product_id == “25000coins.uphills”) then

            CompleteCoinTransaction(25000)

            print(“25000 coins”)

        elseif(purchase_product_id == “40000coins.uphills”) then

            CompleteCoinTransaction(40000)

            print(“40000 coins”)

        elseif(purchase_product_id == “500000coins.uphills”) then

            CompleteCoinTransaction(500000)

            print(“500000 coins”)

        end          

        

        

        

    elseif  event.transaction.state == “restored” then

        – Reminder: your app must store this information somewhere

        – Here we just display some of it

        infoString = “Restoring transaction:” …

                            "\n   Original ID: " … tostring(event.transaction.originalTransactionIdentifier) …

                            "\n   Original date: " … tostring(event.transaction.originalDate)

        print(infoString)

        --descriptionArea.text = infoString

        print("productIdentifier: " … tostring(event.transaction.productIdentifier))

        print("receipt: " … tostring(event.transaction.receipt))

        print("transactionIdentifier: " … tostring(event.transaction.transactionIdentifier))

        print("date: " … tostring(event.transaction.date))

        print("originalReceipt: " … tostring(event.transaction.originalReceipt))

    elseif  event.transaction.state == “refunded” then

        – Refunds notifications is only supported by the Google Android Marketplace.

        – Apple’s app store does not support this.

        – This is your opportunity to remove the refunded feature/product if you want.

        infoString = “A previously purchased product was refunded by the store.”

        print(infoString … "\nFor product ID = " … tostring(event.transaction.productIdentifier))

        --descriptionArea.text = infoString

    elseif event.transaction.state == “cancelled” then

        infoString = “Transaction cancelled by user.”

        print(infoString)

        --descriptionArea.text = infoString

    elseif event.transaction.state == “failed” then        

        infoString = "Transaction failed, type: " … 

        tostring(event.transaction.errorType) … " " … tostring(event.transaction.errorString)

        print(infoString)

    –    descriptionArea.text = infoString

        

    else

        infoString = “Unknown event”

        print(infoString)

        —descriptionArea.text = infoString

    end

    – Tell the store we are done with the transaction.

    – If you are providing downloadable content, do not call this until

    – the download has completed.

    

    print(“finished transation here”)

    store.finishTransaction( event.transaction ) 

else

   --its simulator 

   CompleteCoinTransaction(10000) --simulate a purchase of 10.000 coins

   return true

end

  haspurchased=true --change to true

  end

end

        

just out of curiosity can I see your button code including the handler that starts this whole process off?

Ok… So this is the full code of my, module to that allows player to buy coins.

local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local widget = require "widget" local haspurchased=false function BacktoCarSelect()             can\_move\_background=false             storyboard.showOverlay( "car\_select", {effect = "fade",time = 0,isModal = true} )  end        function transactionCallback(event)          if(haspurchased==false) then              local infoString          --re-enable the iap buttons to allow new purchases     get10000coins:setEnabled(true)     get25000coins:setEnabled(true)     get40000coins:setEnabled(true)          get10000coins.isVisible = true     get25000coins.isVisible = true     get40000coins.isVisible = true             --remove the spinning wheel                 if(spinnerDefault~=nil) then                     spinnerDefault:removeSelf()                     spinnerDefault = nil                 end          --re-enable goback button     goback.isVisible = true                      local function CompleteCoinTransaction(coinsbought)                                native.showAlert("Notice", translations["You were rewarded with "][language]..coinsbought..translations[" coins, and the ads were removed."][language], { "OK" } )                                      coins\_collected=coins\_collected+coinsbought                   myGameSettings.coins=coins\_collected                                    if(myGameSettings.noAds==false) then                     --ads.hide()                 end                                          myGameSettings.noAds=true --remove Ads                   admobheight=0                   display\_total\_coins.text=coins\_collected                   saveTable(myGameSettings, "mygamesettings.json")                            BacktoCarSelect()                            end          if(device.isSimulator ==false) then --check if it is not simulator     --local transaction = event.transaction     -- Log transaction info.     print("transactionCallback: Received event " .. tostring(event.name))     print("state: " .. tostring(event.transaction.state))     print("errorType: " .. tostring(event.transaction.errorType))     print("errorString: " .. tostring(event.transaction.errorString))     if event.transaction.state == "purchased" then         infoString = "Transaction successful!"         print(infoString)         --descriptionArea.text = infoString         print("receipt: " .. tostring(event.transaction.receipt))         print("signature: " .. tostring(event.transaction.signature))            if(device.isAndroid) then             local jsonresponse=json.decode(event.transaction.receipt)              purchase\_product\_id=jsonresponse.orders[#jsonresponse.orders].productId         else             purchase\_product\_id=event.transaction.productIdentifier         end                  if(purchase\_product\_id == "android.test.purchased") then --if just testing             CompleteCoinTransaction(10000)              print("Test Purchase")         elseif(purchase\_product\_id == "10000coins.uphills") then             CompleteCoinTransaction(10000)             print("10.000 coins")         elseif(purchase\_product\_id == "25000coins.uphills") then             CompleteCoinTransaction(25000)             print("25000 coins")         elseif(purchase\_product\_id == "40000coins.uphills") then             CompleteCoinTransaction(40000)             print("40000 coins")         elseif(purchase\_product\_id == "500000coins.uphills") then             CompleteCoinTransaction(500000)             print("500000 coins")         end                                          elseif  event.transaction.state == "restored" then         -- Reminder: your app must store this information somewhere         -- Here we just display some of it         infoString = "Restoring transaction:" ..                             "\n   Original ID: " .. tostring(event.transaction.originalTransactionIdentifier) ..                             "\n   Original date: " .. tostring(event.transaction.originalDate)         print(infoString)         --descriptionArea.text = infoString         print("productIdentifier: " .. tostring(event.transaction.productIdentifier))         print("receipt: " .. tostring(event.transaction.receipt))         print("transactionIdentifier: " .. tostring(event.transaction.transactionIdentifier))         print("date: " .. tostring(event.transaction.date))         print("originalReceipt: " .. tostring(event.transaction.originalReceipt))     elseif  event.transaction.state == "refunded" then         -- Refunds notifications is only supported by the Google Android Marketplace.         -- Apple's app store does not support this.         -- This is your opportunity to remove the refunded feature/product if you want.         infoString = "A previously purchased product was refunded by the store."         print(infoString .. "\nFor product ID = " .. tostring(event.transaction.productIdentifier))         --descriptionArea.text = infoString     elseif event.transaction.state == "cancelled" then         infoString = "Transaction cancelled by user."         print(infoString)         --descriptionArea.text = infoString     elseif event.transaction.state == "failed" then                 infoString = "Transaction failed, type: " ..          tostring(event.transaction.errorType) .. " " .. tostring(event.transaction.errorString)         print(infoString)     --    descriptionArea.text = infoString              else         infoString = "Unknown event"         print(infoString)         ---descriptionArea.text = infoString     end     -- Tell the store we are done with the transaction.     -- If you are providing downloadable content, do not call this until     -- the download has completed.          print("finished transation here")     store.finishTransaction( event.transaction )  else    --its simulator     CompleteCoinTransaction(10000) --simulate a purchase of 10.000 coins    return true end   haspurchased=true   end end local function loadProductsCallback( event )     local validProducts = event.products     local invalidProducts = event.invalidProducts     print( "Valid Products:", #validProducts )     for i = 1,#validProducts do         local currentItem = validProducts[i]         print( currentItem.title )         print( currentItem.description )         print( currentItem.price )         print( currentItem.productIdentifier )     end     print( "Invalid Products:", #invalidProducts )     for i = 1,#invalidProducts do         print( invalidProducts[i] )     end end local arrayOfProductIdentifiers =  {     "10000coins.uphills",     "25000coins.uphills",     "40000coins.uphills",     "500000coins.uphills"     } print("NOW IT CHECKS IF STORE IS ACTIVE") if ( store.isActive ) then     print("YES, THE STORE IS REALLLY ACTIVE")     --if ( store.canLoadProducts ) then --checa se pode carregar a lista de produtos, util só no iphone        -- store.loadProducts( arrayOfProductIdentifiers, loadProductsCallback )        -- print("GET PRODUCT LIST")     --else         --this store does not support an app fetching products         --nao pode carregar lsita de produtos     --end end        -- Called when the scene's view does not exist:\ function scene:createScene( event )     local group = self.view     about\_title=display.newText(translations["Get More Coins"][language],0,admobheight+40,titles\_font,28)     about\_title:setTextColor(0,0,0)     about\_title.x=display.contentWidth/2              group:insert(about\_title)          backgroundmenu = display.newImageRect( "Images/background\_menu\_cars.png",568,384)     backgroundmenu.x=display.contentWidth/2     backgroundmenu.y=display.contentHeight/2     group:insert(backgroundmenu)              local function BuyCoins(event)             haspurchased=false             print(event.target.id)                      --disable gobackbutton while buying stuff             goback.isVisible = false                          --disable buttons to avoid multiple clicks on them             get10000coins:setEnabled(false)             get25000coins:setEnabled(false)             get40000coins:setEnabled(false)             get10000coins.isVisible = false             get25000coins.isVisible = false             get40000coins.isVisible = false                                                       spinnerDefault = widget.newSpinner             {                 left = 0,                 top = 0,             }             spinnerDefault.x=display.contentWidth/2             spinnerDefault.y=display.contentHeight/2             spinnerDefault:start()             group:insert(spinnerDefault)                                                                     --example 10000coins.uphills                  -- store.purchase( { event.target.id.."coins.uphills" } )                  --[[                  if(device.isAndroid) then                     if(event.target.id==10000) then                         store.purchase( { "android.test.canceled"} )                     else                         store.purchase( { "android.test.purchased"} )                     end                 else                 --]]                     if(device.isSimulator) then --check if device is the simulator, to simulate a purchase                         local function simulatepurchase()                             transactionCallback()                         end                                                 timer.performWithDelay(1000,simulatepurchase)                     else                         store.purchase( { event.target.id.."coins.uphills" } )                     end                 --end         end                                   get10000coins = widget.newButton{                 label="10.000 "..translations["Coins & Ad Free"][language].." - USD 0,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=10000,                 width=350, height=37,                 onRelease = BuyCoins    -- event listener function             }                          get10000coins.x=display.contentWidth/2             get10000coins.y=about\_title.y+50             group:insert(get10000coins)                                       get25000coins = widget.newButton{                 label="25.000 "..translations["Coins & Ad Free"][language].."  - USD 1,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=25000,                 width=350, height=41,                 onRelease = BuyCoins    -- event listener function             }             get25000coins.x=display.contentWidth/2             get25000coins.y=get10000coins.y+47             group:insert(get25000coins)                                                get40000coins = widget.newButton{                 label="40.000 "..translations["Coins & Ad Free"][language].." - USD 2,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=40000,                 width=350, height=41,                 onRelease = BuyCoins    -- event listener function             }             get40000coins.x=display.contentWidth/2             get40000coins.y=get25000coins.y+47             group:insert(get40000coins)                                               --[[                get500000coins = widget.newButton{                 label="500.000 "..translations["Coins & Ad Free"][language].." - USD 9,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=500000,                 width=350, height=41,                 onRelease = BuyCoins    -- event listener function             }             get500000coins.x=display.contentWidth/2             get500000coins.y=get25000coins.y+47                              group:insert(get500000coins)             --]]          local function GoBack(event)         if event.phase == "began" then             BacktoCarSelect()         end     end          goback = display.newImageRect( "Images/backnext.png",43,88)     goback.x=40     goback.y=display.contentHeight/2     goback:addEventListener( "touch", GoBack)     group:insert(goback)     group:insert(about\_title) end -- Called BEFORE scene has moved onscreen: function scene:willEnterScene( event )         local group = self.view end function scene:enterScene( event )         local group = self.view          end function scene:exitScene( event )         local group = self.view end function scene:didExitScene( event )         local group = self.view end function scene:destroyScene( event )         local group = self.view end -- Called if/when overlay scene is displayed via storyboard.showOverlay() function scene:overlayBegan( event )         local group = self.view         local overlay\_name = event.sceneName  -- name of the overlay scene        end -- Called if/when overlay scene is hidden/removed via storyboard.hideOverlay() function scene:overlayEnded( event )         local group = self.view         local overlay\_name = event.sceneName  -- name of the overlay scene                          get10000coins:removeSelf()                 get10000coins = nil                 get25000coins:removeSelf()                 get25000coins = nil                 get40000coins:removeSelf()                 get40000coins = nil                 --get500000coins:removeSelf()                 --get500000coins = nil                 goback:removeSelf()                 goback = nil                 about\_title:removeSelf()                 about\_title = nil                                  if(spinnerDefault~=nil) then                     spinnerDefault:removeSelf()                     spinnerDefault = nil                 end                          backgroundmenu:removeSelf()       end --------------------------------------------------------------------------------- -- END OF YOUR IMPLEMENTATION --------------------------------------------------------------------------------- -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "willEnterScene" event is dispatched before scene transition begins scene:addEventListener( "willEnterScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "didExitScene" event is dispatched after scene has finished transitioning out scene:addEventListener( "didExitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be -- automatically unloaded in low memory situations, or explicitly via a call to -- storyboard.purgeScene() or storyboard.removeScene(). scene:addEventListener( "destroyScene", scene ) -- "overlayBegan" event is dispatched when an overlay scene is shown scene:addEventListener( "overlayBegan", scene ) -- "overlayEnded" event is dispatched when an overlay scene is hidden/removed --scene:addEventListener( "overlayEnded", scene ) if store.availableStores.apple then     store.init("apple", transactionCallback)     print("Using Apple's in-app purchase system.")      elseif store.availableStores.google then     store.init("google", transactionCallback)     print("Using Google's Android In-App Billing system.") else     print("In-app purchases is not supported on this system/device.") end --------------------------------------------------------------------------------- return scene  

Remembering, that for ios it is working as it should. Testing the inapps using a test user, works as expected. The problem is presend just on android

The problem is related to the fact that widget.newButton’s generate multiple phases for when it’s first pressed and then when it’s released.  These phases are just like touch events.  There is a “began” phase and an “ended” phase.  Please look at this block of code:

-- Function to handle button events local function handleButtonEvent( event )     local phase = event.phase     if "ended" == phase then         print( "You pressed and released a button!" )     end     return true end

If you do not put that if test in to check the phase, you will end up calling your code twice because this function gets called twice.  Therefore you get duplicate calls to buy your items.

Hello. I did exactly what you said, even being almost sure it was not going to work, because I call the function to buy, on the “onRelease” of the widget button.

As I expected the callback looped forcing the app to crash, even calling the store.purchase, from inside the

local function BuyCoins(event)             if "ended" == event.phase then                 store.purchase("item.to.buy")             end end

Please, consider the possibility that store.finishTransaction()  is not working with the last build. I’m using now build 1129, the same problem also with 1128

The current daily is 1170.  Is there a reason you’re using a build older than the latest public build (1137)?

Sorry. I got confused. I’m using the latest daily build. I just got confuse when copying the version number. I was not on the first page, of the daily build pages. I meant Im using now 1170 and the problem is also in 1168

Can you build and run the IAP sample app?  Before I ask the Engineers, they are going to want to know if you’ve done that.

oh God… Now I discovered that the store.init callback listener is looping forever…

Can you post your code?  Its sounds like you’re never calling the store.finishTransaction( transaction ) bits.

I’m calling it. But it looks like it is not working, exactly like reported in this old post: http://forums.coronalabs.com/topic/29621-storefinishtransaction-is-not-working-properly/

Here is the essence of my callback code. All my purchases are consumable. I did some workarounds to avoid this problem, and for now it is working.

function transactionCallback(event)      if(haspurchased==false) then --I setted this as a workaround for the problem, then after the purchase I change it to true, and avoid from running the callback twice or more.     local infoString         local function CompleteCoinTransaction(coinsbought)             --save coins collected or anything i want         end          if(device.isSimulator ==false) then --check if it is not simulator     print("transactionCallback: Received event " .. tostring(event.name))     print("state: " .. tostring(event.transaction.state))     print("errorType: " .. tostring(event.transaction.errorType))     print("errorString: " .. tostring(event.transaction.errorString))     if event.transaction.state == "purchased" then         infoString = "Transaction successful!"         print(infoString)         print("receipt: " .. tostring(event.transaction.receipt))         print("signature: " .. tostring(event.transaction.signature))            --this is just another workaround, due to the problem if(device.isAndroid) then --if it is android device, get the last purchasedId from the event.transaction.receipt, otherwise it wont get the last purchase local jsonresponse=json.decode(event.transaction.receipt) 

            purchase_product_id=jsonresponse.orders[#jsonresponse.orders].productId

        else

            purchase_product_id=event.transaction.productIdentifier

        end

        

        if(purchase_product_id == “android.test.purchased”) then --if just testing

            CompleteCoinTransaction(10000) 

            print(“Test Purchase”)

        elseif(purchase_product_id == “10000coins.uphills”) then

            CompleteCoinTransaction(10000)

            print(“10.000 coins”)

        elseif(purchase_product_id == “25000coins.uphills”) then

            CompleteCoinTransaction(25000)

            print(“25000 coins”)

        elseif(purchase_product_id == “40000coins.uphills”) then

            CompleteCoinTransaction(40000)

            print(“40000 coins”)

        elseif(purchase_product_id == “500000coins.uphills”) then

            CompleteCoinTransaction(500000)

            print(“500000 coins”)

        end          

        

        

        

    elseif  event.transaction.state == “restored” then

        – Reminder: your app must store this information somewhere

        – Here we just display some of it

        infoString = “Restoring transaction:” …

                            "\n   Original ID: " … tostring(event.transaction.originalTransactionIdentifier) …

                            "\n   Original date: " … tostring(event.transaction.originalDate)

        print(infoString)

        --descriptionArea.text = infoString

        print("productIdentifier: " … tostring(event.transaction.productIdentifier))

        print("receipt: " … tostring(event.transaction.receipt))

        print("transactionIdentifier: " … tostring(event.transaction.transactionIdentifier))

        print("date: " … tostring(event.transaction.date))

        print("originalReceipt: " … tostring(event.transaction.originalReceipt))

    elseif  event.transaction.state == “refunded” then

        – Refunds notifications is only supported by the Google Android Marketplace.

        – Apple’s app store does not support this.

        – This is your opportunity to remove the refunded feature/product if you want.

        infoString = “A previously purchased product was refunded by the store.”

        print(infoString … "\nFor product ID = " … tostring(event.transaction.productIdentifier))

        --descriptionArea.text = infoString

    elseif event.transaction.state == “cancelled” then

        infoString = “Transaction cancelled by user.”

        print(infoString)

        --descriptionArea.text = infoString

    elseif event.transaction.state == “failed” then        

        infoString = "Transaction failed, type: " … 

        tostring(event.transaction.errorType) … " " … tostring(event.transaction.errorString)

        print(infoString)

    –    descriptionArea.text = infoString

        

    else

        infoString = “Unknown event”

        print(infoString)

        —descriptionArea.text = infoString

    end

    – Tell the store we are done with the transaction.

    – If you are providing downloadable content, do not call this until

    – the download has completed.

    

    print(“finished transation here”)

    store.finishTransaction( event.transaction ) 

else

   --its simulator 

   CompleteCoinTransaction(10000) --simulate a purchase of 10.000 coins

   return true

end

  haspurchased=true --change to true

  end

end

        

just out of curiosity can I see your button code including the handler that starts this whole process off?

Ok… So this is the full code of my, module to that allows player to buy coins.

local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local widget = require "widget" local haspurchased=false function BacktoCarSelect()             can\_move\_background=false             storyboard.showOverlay( "car\_select", {effect = "fade",time = 0,isModal = true} )  end        function transactionCallback(event)          if(haspurchased==false) then              local infoString          --re-enable the iap buttons to allow new purchases     get10000coins:setEnabled(true)     get25000coins:setEnabled(true)     get40000coins:setEnabled(true)          get10000coins.isVisible = true     get25000coins.isVisible = true     get40000coins.isVisible = true             --remove the spinning wheel                 if(spinnerDefault~=nil) then                     spinnerDefault:removeSelf()                     spinnerDefault = nil                 end          --re-enable goback button     goback.isVisible = true                      local function CompleteCoinTransaction(coinsbought)                                native.showAlert("Notice", translations["You were rewarded with "][language]..coinsbought..translations[" coins, and the ads were removed."][language], { "OK" } )                                      coins\_collected=coins\_collected+coinsbought                   myGameSettings.coins=coins\_collected                                    if(myGameSettings.noAds==false) then                     --ads.hide()                 end                                          myGameSettings.noAds=true --remove Ads                   admobheight=0                   display\_total\_coins.text=coins\_collected                   saveTable(myGameSettings, "mygamesettings.json")                            BacktoCarSelect()                            end          if(device.isSimulator ==false) then --check if it is not simulator     --local transaction = event.transaction     -- Log transaction info.     print("transactionCallback: Received event " .. tostring(event.name))     print("state: " .. tostring(event.transaction.state))     print("errorType: " .. tostring(event.transaction.errorType))     print("errorString: " .. tostring(event.transaction.errorString))     if event.transaction.state == "purchased" then         infoString = "Transaction successful!"         print(infoString)         --descriptionArea.text = infoString         print("receipt: " .. tostring(event.transaction.receipt))         print("signature: " .. tostring(event.transaction.signature))            if(device.isAndroid) then             local jsonresponse=json.decode(event.transaction.receipt)              purchase\_product\_id=jsonresponse.orders[#jsonresponse.orders].productId         else             purchase\_product\_id=event.transaction.productIdentifier         end                  if(purchase\_product\_id == "android.test.purchased") then --if just testing             CompleteCoinTransaction(10000)              print("Test Purchase")         elseif(purchase\_product\_id == "10000coins.uphills") then             CompleteCoinTransaction(10000)             print("10.000 coins")         elseif(purchase\_product\_id == "25000coins.uphills") then             CompleteCoinTransaction(25000)             print("25000 coins")         elseif(purchase\_product\_id == "40000coins.uphills") then             CompleteCoinTransaction(40000)             print("40000 coins")         elseif(purchase\_product\_id == "500000coins.uphills") then             CompleteCoinTransaction(500000)             print("500000 coins")         end                                          elseif  event.transaction.state == "restored" then         -- Reminder: your app must store this information somewhere         -- Here we just display some of it         infoString = "Restoring transaction:" ..                             "\n   Original ID: " .. tostring(event.transaction.originalTransactionIdentifier) ..                             "\n   Original date: " .. tostring(event.transaction.originalDate)         print(infoString)         --descriptionArea.text = infoString         print("productIdentifier: " .. tostring(event.transaction.productIdentifier))         print("receipt: " .. tostring(event.transaction.receipt))         print("transactionIdentifier: " .. tostring(event.transaction.transactionIdentifier))         print("date: " .. tostring(event.transaction.date))         print("originalReceipt: " .. tostring(event.transaction.originalReceipt))     elseif  event.transaction.state == "refunded" then         -- Refunds notifications is only supported by the Google Android Marketplace.         -- Apple's app store does not support this.         -- This is your opportunity to remove the refunded feature/product if you want.         infoString = "A previously purchased product was refunded by the store."         print(infoString .. "\nFor product ID = " .. tostring(event.transaction.productIdentifier))         --descriptionArea.text = infoString     elseif event.transaction.state == "cancelled" then         infoString = "Transaction cancelled by user."         print(infoString)         --descriptionArea.text = infoString     elseif event.transaction.state == "failed" then                 infoString = "Transaction failed, type: " ..          tostring(event.transaction.errorType) .. " " .. tostring(event.transaction.errorString)         print(infoString)     --    descriptionArea.text = infoString              else         infoString = "Unknown event"         print(infoString)         ---descriptionArea.text = infoString     end     -- Tell the store we are done with the transaction.     -- If you are providing downloadable content, do not call this until     -- the download has completed.          print("finished transation here")     store.finishTransaction( event.transaction )  else    --its simulator     CompleteCoinTransaction(10000) --simulate a purchase of 10.000 coins    return true end   haspurchased=true   end end local function loadProductsCallback( event )     local validProducts = event.products     local invalidProducts = event.invalidProducts     print( "Valid Products:", #validProducts )     for i = 1,#validProducts do         local currentItem = validProducts[i]         print( currentItem.title )         print( currentItem.description )         print( currentItem.price )         print( currentItem.productIdentifier )     end     print( "Invalid Products:", #invalidProducts )     for i = 1,#invalidProducts do         print( invalidProducts[i] )     end end local arrayOfProductIdentifiers =  {     "10000coins.uphills",     "25000coins.uphills",     "40000coins.uphills",     "500000coins.uphills"     } print("NOW IT CHECKS IF STORE IS ACTIVE") if ( store.isActive ) then     print("YES, THE STORE IS REALLLY ACTIVE")     --if ( store.canLoadProducts ) then --checa se pode carregar a lista de produtos, util só no iphone        -- store.loadProducts( arrayOfProductIdentifiers, loadProductsCallback )        -- print("GET PRODUCT LIST")     --else         --this store does not support an app fetching products         --nao pode carregar lsita de produtos     --end end        -- Called when the scene's view does not exist:\ function scene:createScene( event )     local group = self.view     about\_title=display.newText(translations["Get More Coins"][language],0,admobheight+40,titles\_font,28)     about\_title:setTextColor(0,0,0)     about\_title.x=display.contentWidth/2              group:insert(about\_title)          backgroundmenu = display.newImageRect( "Images/background\_menu\_cars.png",568,384)     backgroundmenu.x=display.contentWidth/2     backgroundmenu.y=display.contentHeight/2     group:insert(backgroundmenu)              local function BuyCoins(event)             haspurchased=false             print(event.target.id)                      --disable gobackbutton while buying stuff             goback.isVisible = false                          --disable buttons to avoid multiple clicks on them             get10000coins:setEnabled(false)             get25000coins:setEnabled(false)             get40000coins:setEnabled(false)             get10000coins.isVisible = false             get25000coins.isVisible = false             get40000coins.isVisible = false                                                       spinnerDefault = widget.newSpinner             {                 left = 0,                 top = 0,             }             spinnerDefault.x=display.contentWidth/2             spinnerDefault.y=display.contentHeight/2             spinnerDefault:start()             group:insert(spinnerDefault)                                                                     --example 10000coins.uphills                  -- store.purchase( { event.target.id.."coins.uphills" } )                  --[[                  if(device.isAndroid) then                     if(event.target.id==10000) then                         store.purchase( { "android.test.canceled"} )                     else                         store.purchase( { "android.test.purchased"} )                     end                 else                 --]]                     if(device.isSimulator) then --check if device is the simulator, to simulate a purchase                         local function simulatepurchase()                             transactionCallback()                         end                                                 timer.performWithDelay(1000,simulatepurchase)                     else                         store.purchase( { event.target.id.."coins.uphills" } )                     end                 --end         end                                   get10000coins = widget.newButton{                 label="10.000 "..translations["Coins & Ad Free"][language].." - USD 0,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=10000,                 width=350, height=37,                 onRelease = BuyCoins    -- event listener function             }                          get10000coins.x=display.contentWidth/2             get10000coins.y=about\_title.y+50             group:insert(get10000coins)                                       get25000coins = widget.newButton{                 label="25.000 "..translations["Coins & Ad Free"][language].."  - USD 1,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=25000,                 width=350, height=41,                 onRelease = BuyCoins    -- event listener function             }             get25000coins.x=display.contentWidth/2             get25000coins.y=get10000coins.y+47             group:insert(get25000coins)                                                get40000coins = widget.newButton{                 label="40.000 "..translations["Coins & Ad Free"][language].." - USD 2,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=40000,                 width=350, height=41,                 onRelease = BuyCoins    -- event listener function             }             get40000coins.x=display.contentWidth/2             get40000coins.y=get25000coins.y+47             group:insert(get40000coins)                                               --[[                get500000coins = widget.newButton{                 label="500.000 "..translations["Coins & Ad Free"][language].." - USD 9,99",                 labelColor = { default={0}, over={0} },                 fontSize=12,                 id=500000,                 width=350, height=41,                 onRelease = BuyCoins    -- event listener function             }             get500000coins.x=display.contentWidth/2             get500000coins.y=get25000coins.y+47                              group:insert(get500000coins)             --]]          local function GoBack(event)         if event.phase == "began" then             BacktoCarSelect()         end     end          goback = display.newImageRect( "Images/backnext.png",43,88)     goback.x=40     goback.y=display.contentHeight/2     goback:addEventListener( "touch", GoBack)     group:insert(goback)     group:insert(about\_title) end -- Called BEFORE scene has moved onscreen: function scene:willEnterScene( event )         local group = self.view end function scene:enterScene( event )         local group = self.view          end function scene:exitScene( event )         local group = self.view end function scene:didExitScene( event )         local group = self.view end function scene:destroyScene( event )         local group = self.view end -- Called if/when overlay scene is displayed via storyboard.showOverlay() function scene:overlayBegan( event )         local group = self.view         local overlay\_name = event.sceneName  -- name of the overlay scene        end -- Called if/when overlay scene is hidden/removed via storyboard.hideOverlay() function scene:overlayEnded( event )         local group = self.view         local overlay\_name = event.sceneName  -- name of the overlay scene                          get10000coins:removeSelf()                 get10000coins = nil                 get25000coins:removeSelf()                 get25000coins = nil                 get40000coins:removeSelf()                 get40000coins = nil                 --get500000coins:removeSelf()                 --get500000coins = nil                 goback:removeSelf()                 goback = nil                 about\_title:removeSelf()                 about\_title = nil                                  if(spinnerDefault~=nil) then                     spinnerDefault:removeSelf()                     spinnerDefault = nil                 end                          backgroundmenu:removeSelf()       end --------------------------------------------------------------------------------- -- END OF YOUR IMPLEMENTATION --------------------------------------------------------------------------------- -- "createScene" event is dispatched if scene's view does not exist scene:addEventListener( "createScene", scene ) -- "willEnterScene" event is dispatched before scene transition begins scene:addEventListener( "willEnterScene", scene ) -- "enterScene" event is dispatched whenever scene transition has finished scene:addEventListener( "enterScene", scene ) -- "exitScene" event is dispatched before next scene's transition begins scene:addEventListener( "exitScene", scene ) -- "didExitScene" event is dispatched after scene has finished transitioning out scene:addEventListener( "didExitScene", scene ) -- "destroyScene" event is dispatched before view is unloaded, which can be -- automatically unloaded in low memory situations, or explicitly via a call to -- storyboard.purgeScene() or storyboard.removeScene(). scene:addEventListener( "destroyScene", scene ) -- "overlayBegan" event is dispatched when an overlay scene is shown scene:addEventListener( "overlayBegan", scene ) -- "overlayEnded" event is dispatched when an overlay scene is hidden/removed --scene:addEventListener( "overlayEnded", scene ) if store.availableStores.apple then     store.init("apple", transactionCallback)     print("Using Apple's in-app purchase system.")      elseif store.availableStores.google then     store.init("google", transactionCallback)     print("Using Google's Android In-App Billing system.") else     print("In-app purchases is not supported on this system/device.") end --------------------------------------------------------------------------------- return scene  

Remembering, that for ios it is working as it should. Testing the inapps using a test user, works as expected. The problem is presend just on android

The problem is related to the fact that widget.newButton’s generate multiple phases for when it’s first pressed and then when it’s released.  These phases are just like touch events.  There is a “began” phase and an “ended” phase.  Please look at this block of code:

-- Function to handle button events local function handleButtonEvent( event )     local phase = event.phase     if "ended" == phase then         print( "You pressed and released a button!" )     end     return true end

If you do not put that if test in to check the phase, you will end up calling your code twice because this function gets called twice.  Therefore you get duplicate calls to buy your items.

Hello. I did exactly what you said, even being almost sure it was not going to work, because I call the function to buy, on the “onRelease” of the widget button.

As I expected the callback looped forcing the app to crash, even calling the store.purchase, from inside the

local function BuyCoins(event)             if "ended" == event.phase then                 store.purchase("item.to.buy")             end end

Please, consider the possibility that store.finishTransaction()  is not working with the last build. I’m using now build 1129, the same problem also with 1128

The current daily is 1170.  Is there a reason you’re using a build older than the latest public build (1137)?

Sorry. I got confused. I’m using the latest daily build. I just got confuse when copying the version number. I was not on the first page, of the daily build pages. I meant Im using now 1170 and the problem is also in 1168