Question about IAP

I have a quick question about handling IAP.

I have it set up so that I have a menu with different options, some of them are “locked” and lead to a IAP store scene. On the Store scene I have the store.init() running and all the code to make the purchase using Mr. Miracle’s tutorial code (Which is awesome, thanks for that!).

So my question is about Google and refunds. What is the process on how Corona covers that? The tutorial shows the refund and restore transactions as a part of the store… But I think it would make more sense for me to have the refund and restore transaction checks to be on the menu page instead.

Should I put another store.init() so it runs on app startup, looks specifically for a refunded transaction state and locks a function up if something is found, then end the transaction? Will this catch any refunded queues properly?

Also, I understand Corona will listen for marketplace queues even while suspended, and then act on them when resumed. What if the app is been fully closed before the user uses the Play app to make a refund? WIll corona catch it next time the app opens?

Google will queue refunds and when you call store.restore() to collect previously purchased items, then you will get transactions in that batch that will include refunds.  You don’t need to provide a button to check for refunds.  First, it suggests to people that they can get them, which you probably don’t want to encourage and secondly, a user would think that’s how they get a refund, which it’s not.  It would only call store.restore() to check for outstanding transactions.  So if I see that, I’ll go to google, get a refund, then never hit your button and continue to get the thing I bought for free.  Besides, you’re going to get them anyway when you call store.restore() and if you don’t process them, you may not get them in the future.

Rob

I definitely don’t want to have a refund button.

But what happens if they never push the restore purchases button anyway? Do they keep the refunded IAP forever? 

Is there any way to handle refunds automatically? So it checks for a refund on app startup or something?

For Google, you don’t need a button, just call store.restore() after you init() (perhaps on a 1 sec. timer).

The reason you provide a button for Apple, is it prompts you to login and Apple wants that login defered until it’s needed and since Apple doesn’t do refunds, it doesn’t matter.  The users will want to hit the restore button if they’ve reinstalled to get their purchase back.

Rob

Ahh, that makes sense. I’ll just have it check to see if it’s Android, then use restore(), otherwise it will show a button. Perfect!

Just to clarify, this method will also work for refunds that go through while the app is running? EG: If the refund is processed while the app is running/suspended it will just go through next time it is started from scratch (and the store.restore() function is called), Correct?

I believe you will get the refund the next time your app calls store.restore().

Hi Rob. So I’ve just gone through testing the refunds and am having no luck. I bought a IAP on my app, and then went into the merchant account and refunded it. Then I waited 18 hours to give Google a chance to send the refunded message to my app.

On my menu.lua I have it set to check if the google store is available, then if it is, call store.restore(). In the transactionCallback() I have the “restored” and “refunded” setup just like your tutorial. The “restored” part works perfectly (Reinstalling the app loads the purchased items), however the “refunded” does nothing.

I even tested the store.purchase() to see if the “refunded” would be called there, also no luck. 

Another odd thing is if I uninstall the app and reinstall it (After the IAP is still been refunded) the refunded IAP is being “restored” as if it’s still “purchased”.

As far as I can tell the “refunded” state never gets called.

*Edit*

I just did some more testing with the debugging terminal open. It’s showing that the IAP that should be refunded is still coming in as “purchased”

debug stuff:

D/Finsky  ( 6408): [5168] InAppBillingUtils.getPreferredAccount: com.cncconsulting.speedsandfeeds: Account from first account - [ySa0LHYkMhwIEOdQ_F_LQ6x3giQ]

D/Finsky  ( 6408): [5168] InAppBillingUtils.getPreferredAccount: com.cncconsulting.speedsandfeeds: Account from first account - [ySa0LHYkMhwIEOdQ_F_LQ6x3giQ]

I/qtaguid ( 6408): Failed write_ctrl(u 39) res=-1 errno=22

I/qtaguid ( 6408): Untagging socket 39 failed errno=-22

W/NetworkManagementSocketTagger( 6408): untagSocket(39) failed with errno -22

D/Finsky  ( 6408): [1] MarketBillingService.sendResponseCode: Sending response RESULT_OK for request 8211714729784685140 to com.cncconsulting.speedsandfeeds.

I/Corona  (11277): In transactionCallback purchased

I/Corona  (11277): Transaction restored (from previous session)

I/Corona  (11277): done with store business for now

I/Corona  (11277): In transactionCallback purchased

I/Corona  (11277): Transaction restored (from previous session)

I/Corona  (11277): done with store business for now

I/Corona  (11277): In transactionCallback purchased

I/Corona  (11277): Transaction restored (from previous session)

I/Corona  (11277): done with store business for now

menu.lua code:

local function transactionCallback( event ) print("In transactionCallback", event.transaction.state) local transaction = event.transaction local tstate = event.transaction.state local product = event.transaction.productIdentifier if tstate == "purchased" then tstate = "restored" end if tstate == "restored" then buyCount = buyCount + 1 print("Transaction restored (from previous session)") if "com.speedfeed.iap.sine" == product then storeSettings.sinePaid = true elseif "com.speedfeed.iap.trig" == product then storeSettings.trigPaid = true elseif "com.speedfeed.iap.bolt" == product then storeSettings.boltPaid = true -- elseif "com.speedfeed.iap.speed" == product then -- storeSettings.speedPaid = true end loadsave.saveTable(storeSettings, "store.json") if buyCount == 1 then timer.performWithDelay(1500, function() composer.gotoScene( "restorePage", { effect="fade", time=100} ); end) end store.finishTransaction( transaction ) elseif tstate == "refunded" then print("User requested a refund -- locking app back") if "com.speedfeed.iap.sine" == product then storeSettings.sinePaid = false elseif "com.speedfeed.iap.trig" == product then storeSettings.trigPaid = false elseif "com.speedfeed.iap.bolt" == product then storeSettings.boltPaid = false -- elseif "com.speedfeed.iap.speed" == product then -- storeSettings.speedPaid = false end loadsave.saveTable(storeSettings, "store.json") myData.refund = true store.finishTransaction( transaction ) elseif tstate == "cancelled" then print("User cancelled transaction") store.finishTransaction( transaction ) elseif tstate == "failed" then print("Transaction failed, type:", transaction.errorType, transaction.errorString) native.showAlert("Failed", transaction.errorType.." - "..transaction.errorString, {"Okay"}) store.finishTransaction( transaction ) else print("unknown event") store.finishTransaction( transaction ) end print("done with store business for now") end appleRestore = function(event) if event.phase == "ended" then store.init( "apple", transactionCallback) timer.performWithDelay(500, store.restore) end end googleRefund = function() if myData.refund then composer.gotoScene( "restorePage", { effect="fade", time=100} ) end end --------------------------------------------------------------------------------- -- "scene:create()" function scene:create( event ) local sceneGroup = self.view myData.inch = false myData.refund = false buyCount = 0 timesOpen2 = loadsave.loadTable("timesOpen2.json") if timesOpen2.opened == 5 then native.showAlert ( "Find this App useful?", "Leave a review and help others find it!", { "Never", "Later", "OK" }, alertListener ) end print("Times Opened "..timesOpen2.opened) storeSettings = loadsave.loadTable("store.json") if (loadsave.loadTable("store.json") == nil) then storeSettings = {} storeSettings.trigPaid = false storeSettings.sinePaid = false storeSettings.boltPaid = false loadsave.saveTable(storeSettings, "store.json") end -- storeSettings.sinePaid = true -- storeSettings.trigPaid = true -- storeSettings.boltPaid = true going = {} goingTo = {} going.num = 1 bought = 4 back = display.newRect( sceneGroup, display.contentCenterX, display.contentCenterY, display.contentWidth, display.contentHeight ) backEdgeX = back.contentBounds.xMin backEdgeY = back.contentBounds.yMin Runtime:addEventListener( "key", onKeyEvent ) logo = display.newImageRect(sceneGroup, "Images/title.png", 175, 100) logo.x = backEdgeX + 10 logo.anchorX = 0 logo.anchorY = 0.5 logo.y = logo.contentHeight / 2 + 40 topBar = display.newRect( sceneGroup, 0, 0, display.contentWidth, 30 ) topBar:setFillColor(0.15, 0.4, 0.729, 0.75) topBar.anchorX = 0 topBar.anchorY = 0 facebookButt = display.newImageRect(sceneGroup, "Images/facebook.png", 42, 42) facebookButt.anchorX = 0 facebookButt.anchorY = 0.5 facebookButt.x = logo.x facebookButt.y = logo.y \* 2 facebookButt:addEventListener ( "touch", goingFacebook ) if device.isApple then restoreBut = display.newImageRect(sceneGroup, "Images/restoreBut.png", 42, 42) restoreBut.anchorX = 0 restoreBut.anchorY = 0.5 restoreBut.x = logo.x + facebookButt.contentWidth + 15 restoreBut.y = logo.y \* 2 restoreBut.alpha = 0.6 restoreBut:addEventListener("touch", appleRestore) local restoreLabel = display.newText( { parent = sceneGroup, text = "Restore Purchases", 0, 0, font = "BerlinSansFB-Reg", fontSize = 14, width = 85, align = "center"}) restoreLabel.x = restoreBut.x + 21 restoreLabel.y = restoreBut.y + 40 restoreLabel:setFillColor(0.608, 0, 0, 0.6) else if store.availableStores.google then store.init( "google", transactionCallback ) timer.performWithDelay( 300, store.restore) timer.performWithDelay( 1500, googleRefund) end end ...

@spowell83,

I didn’t look at your code, but from my experience testing Google Play refunds in my currently released app, it sometimes took several days for the status to change to refunded in the response from Google.  But not always.  It definitely would confuse me.  But since that data is coming back from Google and is not tied to any of your code, there isn’t much that you can really do about it on your side.  You can only act on what Google is telling you.

As I recall, but haven’t looked in a while, Google also used to send a purchased status, then sent a refunded status after, for refunded items.  Not sure if they ever changed that.  You might log the Google response and see if it is actually coming back as purchased and refunded, and then check to see if you’re only acting on one of them.

@thegdog

I was wondering if it was just an issue of Google taking forever to send out the “refunded” state.

I do have a print(“In transactionCallback”, event.transaction.state) in the transaction function and it shows the state is only coming back as “purchased”. You can see what is coming in with the debug lines I put in my original post.

Google will queue refunds and when you call store.restore() to collect previously purchased items, then you will get transactions in that batch that will include refunds.  You don’t need to provide a button to check for refunds.  First, it suggests to people that they can get them, which you probably don’t want to encourage and secondly, a user would think that’s how they get a refund, which it’s not.  It would only call store.restore() to check for outstanding transactions.  So if I see that, I’ll go to google, get a refund, then never hit your button and continue to get the thing I bought for free.  Besides, you’re going to get them anyway when you call store.restore() and if you don’t process them, you may not get them in the future.

Rob

I definitely don’t want to have a refund button.

But what happens if they never push the restore purchases button anyway? Do they keep the refunded IAP forever? 

Is there any way to handle refunds automatically? So it checks for a refund on app startup or something?

For Google, you don’t need a button, just call store.restore() after you init() (perhaps on a 1 sec. timer).

The reason you provide a button for Apple, is it prompts you to login and Apple wants that login defered until it’s needed and since Apple doesn’t do refunds, it doesn’t matter.  The users will want to hit the restore button if they’ve reinstalled to get their purchase back.

Rob

Ahh, that makes sense. I’ll just have it check to see if it’s Android, then use restore(), otherwise it will show a button. Perfect!

Just to clarify, this method will also work for refunds that go through while the app is running? EG: If the refund is processed while the app is running/suspended it will just go through next time it is started from scratch (and the store.restore() function is called), Correct?

I believe you will get the refund the next time your app calls store.restore().

Hi Rob. So I’ve just gone through testing the refunds and am having no luck. I bought a IAP on my app, and then went into the merchant account and refunded it. Then I waited 18 hours to give Google a chance to send the refunded message to my app.

On my menu.lua I have it set to check if the google store is available, then if it is, call store.restore(). In the transactionCallback() I have the “restored” and “refunded” setup just like your tutorial. The “restored” part works perfectly (Reinstalling the app loads the purchased items), however the “refunded” does nothing.

I even tested the store.purchase() to see if the “refunded” would be called there, also no luck. 

Another odd thing is if I uninstall the app and reinstall it (After the IAP is still been refunded) the refunded IAP is being “restored” as if it’s still “purchased”.

As far as I can tell the “refunded” state never gets called.

*Edit*

I just did some more testing with the debugging terminal open. It’s showing that the IAP that should be refunded is still coming in as “purchased”

debug stuff:

D/Finsky  ( 6408): [5168] InAppBillingUtils.getPreferredAccount: com.cncconsulting.speedsandfeeds: Account from first account - [ySa0LHYkMhwIEOdQ_F_LQ6x3giQ]

D/Finsky  ( 6408): [5168] InAppBillingUtils.getPreferredAccount: com.cncconsulting.speedsandfeeds: Account from first account - [ySa0LHYkMhwIEOdQ_F_LQ6x3giQ]

I/qtaguid ( 6408): Failed write_ctrl(u 39) res=-1 errno=22

I/qtaguid ( 6408): Untagging socket 39 failed errno=-22

W/NetworkManagementSocketTagger( 6408): untagSocket(39) failed with errno -22

D/Finsky  ( 6408): [1] MarketBillingService.sendResponseCode: Sending response RESULT_OK for request 8211714729784685140 to com.cncconsulting.speedsandfeeds.

I/Corona  (11277): In transactionCallback purchased

I/Corona  (11277): Transaction restored (from previous session)

I/Corona  (11277): done with store business for now

I/Corona  (11277): In transactionCallback purchased

I/Corona  (11277): Transaction restored (from previous session)

I/Corona  (11277): done with store business for now

I/Corona  (11277): In transactionCallback purchased

I/Corona  (11277): Transaction restored (from previous session)

I/Corona  (11277): done with store business for now

menu.lua code:

local function transactionCallback( event ) print("In transactionCallback", event.transaction.state) local transaction = event.transaction local tstate = event.transaction.state local product = event.transaction.productIdentifier if tstate == "purchased" then tstate = "restored" end if tstate == "restored" then buyCount = buyCount + 1 print("Transaction restored (from previous session)") if "com.speedfeed.iap.sine" == product then storeSettings.sinePaid = true elseif "com.speedfeed.iap.trig" == product then storeSettings.trigPaid = true elseif "com.speedfeed.iap.bolt" == product then storeSettings.boltPaid = true -- elseif "com.speedfeed.iap.speed" == product then -- storeSettings.speedPaid = true end loadsave.saveTable(storeSettings, "store.json") if buyCount == 1 then timer.performWithDelay(1500, function() composer.gotoScene( "restorePage", { effect="fade", time=100} ); end) end store.finishTransaction( transaction ) elseif tstate == "refunded" then print("User requested a refund -- locking app back") if "com.speedfeed.iap.sine" == product then storeSettings.sinePaid = false elseif "com.speedfeed.iap.trig" == product then storeSettings.trigPaid = false elseif "com.speedfeed.iap.bolt" == product then storeSettings.boltPaid = false -- elseif "com.speedfeed.iap.speed" == product then -- storeSettings.speedPaid = false end loadsave.saveTable(storeSettings, "store.json") myData.refund = true store.finishTransaction( transaction ) elseif tstate == "cancelled" then print("User cancelled transaction") store.finishTransaction( transaction ) elseif tstate == "failed" then print("Transaction failed, type:", transaction.errorType, transaction.errorString) native.showAlert("Failed", transaction.errorType.." - "..transaction.errorString, {"Okay"}) store.finishTransaction( transaction ) else print("unknown event") store.finishTransaction( transaction ) end print("done with store business for now") end appleRestore = function(event) if event.phase == "ended" then store.init( "apple", transactionCallback) timer.performWithDelay(500, store.restore) end end googleRefund = function() if myData.refund then composer.gotoScene( "restorePage", { effect="fade", time=100} ) end end --------------------------------------------------------------------------------- -- "scene:create()" function scene:create( event ) local sceneGroup = self.view myData.inch = false myData.refund = false buyCount = 0 timesOpen2 = loadsave.loadTable("timesOpen2.json") if timesOpen2.opened == 5 then native.showAlert ( "Find this App useful?", "Leave a review and help others find it!", { "Never", "Later", "OK" }, alertListener ) end print("Times Opened "..timesOpen2.opened) storeSettings = loadsave.loadTable("store.json") if (loadsave.loadTable("store.json") == nil) then storeSettings = {} storeSettings.trigPaid = false storeSettings.sinePaid = false storeSettings.boltPaid = false loadsave.saveTable(storeSettings, "store.json") end -- storeSettings.sinePaid = true -- storeSettings.trigPaid = true -- storeSettings.boltPaid = true going = {} goingTo = {} going.num = 1 bought = 4 back = display.newRect( sceneGroup, display.contentCenterX, display.contentCenterY, display.contentWidth, display.contentHeight ) backEdgeX = back.contentBounds.xMin backEdgeY = back.contentBounds.yMin Runtime:addEventListener( "key", onKeyEvent ) logo = display.newImageRect(sceneGroup, "Images/title.png", 175, 100) logo.x = backEdgeX + 10 logo.anchorX = 0 logo.anchorY = 0.5 logo.y = logo.contentHeight / 2 + 40 topBar = display.newRect( sceneGroup, 0, 0, display.contentWidth, 30 ) topBar:setFillColor(0.15, 0.4, 0.729, 0.75) topBar.anchorX = 0 topBar.anchorY = 0 facebookButt = display.newImageRect(sceneGroup, "Images/facebook.png", 42, 42) facebookButt.anchorX = 0 facebookButt.anchorY = 0.5 facebookButt.x = logo.x facebookButt.y = logo.y \* 2 facebookButt:addEventListener ( "touch", goingFacebook ) if device.isApple then restoreBut = display.newImageRect(sceneGroup, "Images/restoreBut.png", 42, 42) restoreBut.anchorX = 0 restoreBut.anchorY = 0.5 restoreBut.x = logo.x + facebookButt.contentWidth + 15 restoreBut.y = logo.y \* 2 restoreBut.alpha = 0.6 restoreBut:addEventListener("touch", appleRestore) local restoreLabel = display.newText( { parent = sceneGroup, text = "Restore Purchases", 0, 0, font = "BerlinSansFB-Reg", fontSize = 14, width = 85, align = "center"}) restoreLabel.x = restoreBut.x + 21 restoreLabel.y = restoreBut.y + 40 restoreLabel:setFillColor(0.608, 0, 0, 0.6) else if store.availableStores.google then store.init( "google", transactionCallback ) timer.performWithDelay( 300, store.restore) timer.performWithDelay( 1500, googleRefund) end end ...

@spowell83,

I didn’t look at your code, but from my experience testing Google Play refunds in my currently released app, it sometimes took several days for the status to change to refunded in the response from Google.  But not always.  It definitely would confuse me.  But since that data is coming back from Google and is not tied to any of your code, there isn’t much that you can really do about it on your side.  You can only act on what Google is telling you.

As I recall, but haven’t looked in a while, Google also used to send a purchased status, then sent a refunded status after, for refunded items.  Not sure if they ever changed that.  You might log the Google response and see if it is actually coming back as purchased and refunded, and then check to see if you’re only acting on one of them.

@thegdog

I was wondering if it was just an issue of Google taking forever to send out the “refunded” state.

I do have a print(“In transactionCallback”, event.transaction.state) in the transaction function and it shows the state is only coming back as “purchased”. You can see what is coming in with the debug lines I put in my original post.