[Resolved] Goole IAP: need to check transaction callback (refunded event)

I’ve noted the problem here, but I thought it is worth its own thread.

As described in the linked post, I’ve tested the refunded event, but I don’t know how to go about triggering my game to check for the refunded event at the start of the game.

Here’s the problem.

  1. User makes a purchase using In-app Billing.

  2. User gets what the user purchased.

  3. Later on, user regrets that he/she made the purchase, and requests a refund.

  4. Merchant (publisher/me) would process the refund – but this refund event could easily take place after the user has already quit the game.

  5. When the user returns to the game (in my case, after uninstalling and installing the game again), the game does not automatically detect the refunded event. However, it detects the refund when the user actually opens the google checkout, which triggers the refund action inside my game.

If I can check the refunded event status when the game is launched, I can think of ways to handle different cases. For example, I could check to see if the refund is made to a newly installed app (that has never been played before), or if the refund is related to virtual items the user already has on his/her account, etc., and handle it accordingly.

Am I going about doing this wrong? If the user never quits from the game, is the refunded event recognized by the game behind the scene (even if the user is not playing the game and doing something else on his/her phone)…?

With this problem in mind, what might be the best way to handle the refunded event?

Naomi
[import]uid: 67217 topic_id: 24973 reply_id: 324973[/import]

Hello Naomi,

The refunded event in Corona is raised when the Android marketplace app sends a Refunded notification to your app. This won’t happen on app startup. It can happen at any time. The Android marketplace app will send the Refunded notification to your app periodically until your app sends it a confirmation response by calling [lua]store.finishTransaction()[/lua].

Since the refunded notification can be sent to your app while it is running, I suggest that you code your app to handle that scenario. You can then “finish” the refunded transaction, update your configuration file or SQLite database to exclude that feature, and then it is up to you to choose to remove that feature while the app is running or upon the next time your app is started up. [import]uid: 32256 topic_id: 24973 reply_id: 101372[/import]

Thank you, Joshua, for the detailed explanation.

It sounds like I can simply have my game handle the refund event as soon as it receives the notification (meaning, my game can check relevant variables and determine what action to take upon the receipt of the refund notification.

I was worried that my game would not receive any notification after it exits and relaunch the game. So long as the notification is repeatedly pushed to the game (until Google receives the confirmation from my game), it’s not an issue with me.

Thanks again!

Naomi

[import]uid: 67217 topic_id: 24973 reply_id: 101381[/import]

Naomi, I have to say i’m watching you every post, I am in the middle of writing an app that is scheduled to have a bunch of In-App purchases and you started these post’s that I needed to review just when I needed them :slight_smile:

Larry [import]uid: 11860 topic_id: 24973 reply_id: 101409[/import]

This answers my question about refunds but my next question would be when to call the store.finishTransaction() function. Just in some common area in the menu every time they pass it? [import]uid: 126161 topic_id: 24973 reply_id: 101411[/import]

Oh sweet thanks for the code.

[import]uid: 11860 topic_id: 24973 reply_id: 101430[/import]

You are aware right that there are NO refunds on the Android Market IAP’s correct? [import]uid: 29181 topic_id: 24973 reply_id: 101464[/import]

Hey, Ninja Pig, I’m not sure what you mean?

Here’s what I can do so far:

  1. As a test user, buy unmanaged item (charging to the test user’s real credit card).

  2. I go to Google Checkout, signing in as the merchant/publisher who owns the game.

  3. I see the purchase transaction, showing the name of the test user, what the test user bought and the amount of payment charged to his/her credit card.

  4. While the credit card has not been processed, I can cancel the transaction. However, once the credit card charge is fully processed, instead of canceling the transaction, I can refund the transaction.

  5. As soon as I refund the transaction, the payout to me as a merchant is reduced by the amount refunded.

  6. The test user receives an email, telling him/her that the money was refunded.

  7. Android IAP integrated into Corona SDK receives “refunded” event notification, and upon receipt, it fires a function inside the “refunded” event. And the function I wrote in there does its work and removes the virtual item from the game.

Did you mean that Android IAP does not automatically take away the products upon refunded event? If so, you are correct. All it does is to let the app/game know that the refund happened. We need to code what we want our app/game to do when the game receives the info.

I hope this makes sense?

Naomi
[import]uid: 67217 topic_id: 24973 reply_id: 101476[/import]

Oh I didn’t understand that’s how it worked. Honestly though, if someone buys an IAP you shouldn’t refund them. [import]uid: 29181 topic_id: 24973 reply_id: 101491[/import]

Ha ha ha. Yeah, we wouldn’t want to refund them, do we?

We’d need to read some fine prints regarding how merchants are expected to behave, but based on my experience, if there is a dispute, paying consumers always win, and it really doesn’t help when a merchant refuses to make the refund without a good reason . Especially when the product sold is a virtual item, I don’t know how easy/difficult it is to keep the money when the consumer demands it back.

I don’t know the “chargeback” policy at Google Play/Checkout, but merchants can be hit by a hefty chargeback, which can cost more than simply refunding some disgruntled (or sleazy) customer – especially if the product isn’t a very expensive item.

Naomi

P.S. The user who demands refund would be a better user than those who simply pirate and play for free, don’t you think? [import]uid: 67217 topic_id: 24973 reply_id: 101500[/import]

@doubleslashdesign, glad to hear my posts are timely for you. :wink:

@3 Dreams Gaming, store.finishTransaction() function is called from inside the transactionCallback, and transactionCallback is called when I initialize the store in main.lua.


Edit: So, I think, calling once (i.e., initializing once) at the start of the game is enough… Ah, but I see, if the user left the game in some suspended state, how do we initialize the store again when the user resume from some suspended state? I suppose if it was suspended, the store is still active (and eventually resume listening for the transaction). At least that seems like how it should be handled? Hmmm… good question though. May need some further testing on that one. :slight_smile:

Here’s the relevant code structure, and in the order they show up:

[lua]-- main.lua

function transactionCallback(event)
if transaction.state == “purchased” then

elseif transaction.state == “restored” then

elseif transaction.state == “refunded” then

elseif transaction.state == “cancelled” then

elseif transaction.state == “failed” then

else

end

store.finishTransaction( transaction )

end

– Connect to store at startup
if store.availableStores.apple then
store.init(“apple”, transactionCallback)
elseif store.availableStores.google then
store.init(“google”, transactionCallback)
end [/lua]

To fill out the details, might be wroth reading up on this tutorial blog post:

http://blog.anscamobile.com/2012/03/getting-started-with-android-in-app-billing/

Naomi [import]uid: 67217 topic_id: 24973 reply_id: 101427[/import]

Everyone,

You should only call the store.finishTransaction() function AFTER you saved the purchased item to file/database. This is just in case your app crashes or has been force-quit by the device (due to low memory) before it has a chance to write the purchased item setting to file.

If the purchase involves downloadable content (aka: DLC), then you should only call the store.finishTransaction() function AFTER it has been downloaded, such as in Corona’s network listener. This is more likely to fail on a mobile device since Internet access can easily be lost while the user is walking around.

If your app does crash or if the download fails, then you should not call the store.finishTransaction() function. Instead, you should wait for the Android marketplace to send the purchase notification again and re-attempt to save/download the purchased item in your app. The Android marketplace will send the purchase notification repeatedly until your app finishes it or until the marketplace gives up and times it out. [import]uid: 32256 topic_id: 24973 reply_id: 101665[/import]

Naomi,

You only have to call store.init() once on app startup. You do not have to call this function after a suspend/resume. Corona will continue to listen for marketplace notifications even while suspended, but it won’t send the notifications to your Lua code until it resumes. Corona will just queue up the notification until resumed and then send it to your Lua callback. [import]uid: 32256 topic_id: 24973 reply_id: 101678[/import]

Edit: I went back to basics and looked at http://lua-users.org/wiki/CoroutinesTutorial – and it sounds like to me that I don’t really need to change the code structure I have (posted in #5 above). So, I’m scratching the idea posted in #13 above. The functions placed inside the if/elseif statement inside the transactionCallback has to complete (without an error) before store.finishTransaction() is triggered anyway.

I think what Joshua meant is, if content delivery must take place (and if the execution of such delivery takes a while), there will be a real risk for unsuccessful delivery. So, you don’t want to call store.finishTransaction() merely after setting a flag (or whatever else) for the app/game to download the content (but not actually executing the delivery).

Since I’m executing the actual delivery of the purchased item in the savePurchase function itself, and since store.finishTransaction() is called outside the if/elseif statement, I think it does what it should.

Whew. Got confused a bit there.

Naomi

P.S., thank you for clarifying how Corona handles store & store related notifications at suspend/resume event. [import]uid: 67217 topic_id: 24973 reply_id: 101687[/import]

Thank you, Joshua, for the detailed message as to how we should handle store.finishTransaction() function.

I have a question relating to this. Using single code base, I build for both iOS and Android, and most of the IAP related code did not need any changes (which is awesome, by the way.)

Currently, the store.finishTransaction appear inside the transactionCallback (as shown in the shell structure posted above), and I believe that’s the best place for iOS version. (If it isn’t true, please let me know.)

Edit: I’m scratching the alternative code structure I posted. I think I misunderstood what was intended/suggested.

[code deleted – because it wouldn’t do any good.]

Naomi

P.S. I just posted a question regarding “failed” event on tutorial blog comment section. I debated whether I should simply start a new thread or post it on the blog – couldn’t tell which might be the better way to handle it, and ended up posting a comment on the blog. Please let me know what is more appropriate approach when I have a very specific question like this:

http://blog.anscamobile.com/2012/03/getting-started-with-android-in-app-billing/comment-page-1/#comment-7535

If it’s better to start a new thread, please feel free to have the comment removed from the blog post. I’m happy to repost it here (or under “cancelled” event related thread.) Thanks!
[import]uid: 67217 topic_id: 24973 reply_id: 101686[/import]

Right. If you don’t have any downloadable content, then finishing the transaction within the transaction callback will work just fine.

So, does this mean everything is working correctly for you now?
Just following up to be sure. [import]uid: 32256 topic_id: 24973 reply_id: 101978[/import]

Thank you so much, Joshua, for following up. Yes, everything is working well. Only “unknown” I have is the “failed” event. I’m not sure if I should handle it like I do with “cancelled” event (i.e., message the user that transaction failed), or if I should mirror the “refunded” event (i.e., message the user and take away the purchased item.)

The reason why I’m not sure is because I have not encountered “failed” event during my testing – so I don’t know what might cause the failure. Would it be always before the transaction completes? Or can it happen both before and after the transaction completes? I’m assuming it’s before the “purchased” event takes place, but I could think of a few reasons why transaction might fail after “purchased” state occur (especially after seeing “cancelled” event can happen both before and after the “purchased” event.)

Thanks again.

Naomi

Edit: Hey, Joshua, I saw your reply post on Blog section just now: http://blog.anscamobile.com/2012/03/getting-started-with-android-in-app-billing/

Thank you for getting back to me on it. [import]uid: 67217 topic_id: 24973 reply_id: 101992[/import]

The “failed” event is really general and can mean many things. On Android, Google’s Android Marketplace does not give us any error message at all. So, the “event.transaction.errorString” sent to the transaction callback will always be an empty string. My advise would be to display your own error message if “event.transaction.errorString” is nil or empty (ie: string length <= 0).

Now, you can determine the exact nature of the error by looking at the “event.transaction.errorType” property, which returns a string. Here are all of the possible error types:

  • “none”: Indicates that no error occurred.
  • “unknown”: Indicates that an unknown or unexpected error occurred.
  • “cancelled”: Indicates that the transaction/payment was cancelled. If the “event.transaction.state” is set to “failed”, then this indicates the credit card transaction was cancelled after the user approved the purchase in your app.
  • “invalidClient”: Indicates that the mobile device is not properly configured to make in-app purchases. For example, an account has not be assigned to the marketplace app, credit card information is wrong, or the user has not agreed to the marketplace license agreement yet.
  • “invalidPayment”: Indicates that the marketplace did not recognize the payment settings provided to the marketplace app. (I’ve never seen this error happen on Android.)
  • “paymentNotAllowed”: Indicates that the user is not authorized to make purchases on the device. For example, parental controls to prevent kids from making purchases.
    [import]uid: 32256 topic_id: 24973 reply_id: 102162[/import]

Wow, Joshua, thank you for the laundry list of all possible error types. This definitely will help dealing with the error handling/messaging.

Thank you so much!

Naomi [import]uid: 67217 topic_id: 24973 reply_id: 102165[/import]

Hey, Joshua, I’d like one more clarification regarding the event.transaction.errorType.

Does App Store return the same errorType, or does the list of errorType you provided above only work with Google Play?

The reason why I’m asking this is because I want to make an educated decision for error handling messages. If they don’t mirror each other, I’ll probably use simple “cancelled” or "failed message for iOS while I may give additional reason why it was cancelled or failed for Android.

Naomi [import]uid: 67217 topic_id: 24973 reply_id: 102962[/import]