How can I use "refunded" state with new Google In App v3?

You actually have to buy the item, then later go into Google Wallet and cancel the purchase if you want to get a refunded state.  Just canceling the dialog while starting a purchase wont trigger it.

Rob

I think you misunderstood my post. I am indeed cancelling (refunding) the transaction in the developer merchant account (wallet.google.com), which then refunds the test purchase, and the tester account gets an email stating it was refunded. I am not referring to simply canceling the payment dialog as you suggested I was.

So with that being cleared up, can you address the question?

EDIT: I’m also particularly curious to find out an answer to my other question above about restore() usage.

Rob or Brent, any insight?

@csavalas - This is a known widespread issue with the Google API, nothing to do with Corona.  The problem is that v3 locally stores purchase state in cache, and it can take some time for it to update on the device.  Some users reported it taking up to 30 days to finally change to Refunded state on device.  Unfortunately there isn’t much that can be done until Google corrects the issue.  

Thanks for answering Jon. Where did you come across that info? That sounds like a nightmare for testing…

My other question: is it correct procedure to call restore() every time the app starts to try to catch the “refunded” state in the callback, or is “refunded” pushed to the callback at any given time after calling init(), making the restore call at launch unnecessary?

I hope it’s the latter, because calling restore() at every app launch seems clunky.

If you search around stackoverflow you will see people talking about it.  

My other question: is it correct procedure to call restore() every time the app starts to try to catch the “refunded” state in the callback, or is “refunded” pushed to the callback at any given time after calling init(), making the restore call at launch unnecessary?

 

I hope it’s the latter, because calling restore() at every app launch seems clunky.

The state of each item should automatically update in the callback you use when you initialize the store.  To be safe you should look for the item(s) in the Purchased state, as this returns all items previously purchased.  Items cancelled/refunded/consumed (in theory) will not be tagged “purchased”.  

That makes logical sense to me, so no need to call restore() every time, but at least call init() every time. In Rob’s tutorial, (relevant excerpt below), he doesn’t even initialize the store unless your the local isPaid variable in your app is false. It would be nice to hear his thoughts on this too, as it seems to me that the app should call init() every time it starts for Android devices, to handle things like refunds that can be pushed to the device at any time. Thanks again Jon

 if store.availableStores.google and not mySettings.isPaid then timer.performWithDelay( 1000, function() store.init( "google", transactionCallback ); restoring = true; store.restore(); end ) end

Rob? :slight_smile:

The code I took that example from didn’t really consider refunds. You could simply remove that check and always init.

Rob

Thanks for the reply Rob, but now the more I think about it, I think calling restore every time may be necessary after all, since init() does not trigger the callback to get the ownership state of items. After Jon suggested to be safe by checking if items are indeed “purchased” at every startup rather than relying on the “refunded” state ever coming in, out of curiosity I tried to make the purchase of my non consumable “pro version” upgrade item on the device that still hasn’t gotten the “refunded” state for that item, so the app is still in the “paid” state. Lo and behold, it was a valid, purchaseable item, meaning that google is not falsely reporting that I still own it, it just never sent out the “refunded” state, so it never locked back up. So that being said, here are implications/questions I see following Jon’s advice:

  1. Do I call restore() every app start, and check if I get “purchased” back for the item, and if not, lock the app up?

  2. In the case that a PAID user starts the program at some point with no internet, will it still report back “purchased” on the restore() call, since that was the last known state of the item? Or will it falsely lock things up?

  3. Will calling restore at every startup pi*s off google?

  4. Is this seriously the best way to handle it? lol

Cheers

According to Google the purchase state is cached locally, so the device doesn’t need to depend on internet connection.  However I will tell you in my experience using another platform (not Corona), I had a number of users of varying devices report that the “pro” app locked back up when there was no internet connection, and successfully unlock when there was.  After hours of research I couldn’t find a reasonable solution, so instead I saved the purchase state locally myself (like in iOS).  Yes this does open you up to security issues, but 99% of users won’t bother (at least not with my apps).  I was not able to replicate the issue these users were having a several different devices I own.

After doing some further research it seems that Google Play Store does not send the refund state to the device until it receives the official refund confirmation from Google Merchant.  I am wondering if the significant delay (weeks) from Merchant is because they have to wait on the banks to confirm the transaction was returned?  Who knows…

Anyhow, if I were you I would just call restore() every time on Android.  

  1. Google, yes you should call store.restore() every time the app starts up or comes back from suspended. Apple, no, you must have a “restore” button and allow the user to choose when to restore.

  2. If you don’t have internet I don’t believe you will get any results from store.restore(). Your app should have saved the last known purchase state and should operate from those settings.

  3. No

  4. Yes, for Google.

Thanks again to both of you.

So it seems going down this route, I’d just eliminate handling of “refunded” altogether, but thinking it through some more, let’s say the user did indeed get a refund from me, and the item isn’t owned anymore. Calling restore() is not going to even trigger the callback if the user doesn’t own anything, correct? So I can’t simply add a blanket statement like ( if tstate ~= purchased then lock it up…) at the end of the listener. It’ would just be a silent call. And since the listener isn’t triggered for restore() with no internet connection either, I don’t see a way to differentiate between those two different conditions.

I’m beginning to think I may have to just let refunds “slide”, as this is driving me nuts! haha

The default state of your app is to be locked (free).  At every start you check restore for purchased items.  If the Pro Upgrade is purchased then you run the unlock code.  If not the app remains locked.  This takes care of refunds automatically because if they were refunded then purchase = false and app remains locked.

A refund is from something that is owned, so store.restore() will trigger and you can undo anything refunded. But for Android @JonPM’s method works, assume its locked and let the purchased items unlock as needed. The only draw back to this method is if they are off line, they don’t get their benefits.

Rob

Again let me stress that I had a few select users who’s app would lock up while offline, and I myself could never replicate the issue (meaning on my devices the app remained unlocked while offline).  Per Google purchase states are stored locally.  I would suggest csavalas try it the proper way and assume that there is no difference if the device is online/offline when restoring.  Only if you actually have issues then try other solutions (like I did with storing purchase states locally myself).  

Jon:

By “proper way”, do you mean the “standard way” in that I would leave it unlocked indefinitely after a user purchases the upgrade, and then only lock up if I ever get a “refunded” state at some point?

Because I am more intrigued by your idea of having the default state being locked, and then relying on restore() to unlock the app when the listener receives “purchased” on the restore call. That being said, I already store the purchase state locally, but if I’m resetting the default state to locked every time anyway, I don’t understand how that ended up being a workaround for your handful of users that where experiencing issues.

Rob mentioned handling suspend/resume as well. How do you handle this? Do you set your local state to locked on every suspend? It seems to me that could get messy since the user could very well be in the middle of something. And have you also decided to not even “handle” the “refunded” state? It seems to me “refunded” becomes irrelevant if you’re using your method.

Rob:

It is my understanding that the “refunded” state is pushed to the device repeatedly until your app handles it with finishtransaction, and once that happens, then “refunded” no longer will be present in the callback. Am I mistaken?

General:

While I don’t want people being able to cheat the app by getting a refund and continuing to use the pro version, it seems like it may be the lesser of two evils, the other being genuine paid users experiencing issues with the app. I don’t know…

I can’t say for certain, but that would make sense.

Rob

No sorry, what I meant was you should call restore() every launch without worrying if the device is online or offline.  Because in theory (and according to Google) it should work properly either way.  

I still recommend your app defaults to a locked state, and only unlock if purchase is found.  This way your refunds are handled automatically.  Although as previously discussed it might take a while for the refund state to trigger, and the purchase to be set to false.  It’s also worth noting that for In-App Purchases, you the developer have to manually issue refunds.  Users cannot obtain refunds for IAPs by any other method.  

As for suspend/resume, there is nothing needed there.  What I do is set a global variable (isUnlocked) to false by default, and only set to true through the restore() or purchase() methods.  This variable will persist while the app in use or in background.  I think it is easier to explain through a diagram:

User Opens App >

isUnlocked is false by default >

restore() or purchase() runs, purchase is true, sets isUnlocked to true >

Your app’s items are all unlocked >

User suspends app (hits home button) >

User resumes app (assume the OS hasn’t killed it), isUnlocked retains true state >

User closes app, OS kills app >

Next app launch starts this process over again.

Again, I wouldn’t put too much stress on refunded state.  For an IAP, the user has to find you, email you, request the refund, and you decide whether or not to issue it (which you always should IMO).  This won’t happen very often.  

Thanks for the reply. So as I suspected, on your suspend/resume you’re not checking for refunds as Rob was suggesting, just on app startup. What was your eventual resolution for the users experiencing the locking issue while not connected to the net?

On a personal note, what kind of apps do you develop? I’d love to check them out.

Rob, last question on this. Is it necessary to call restore() to catch a “refunded” coming in, or will it come in any time randomly after an init(). Since it takes so long for “refunded” to come in, I can’t really beta test it, but I’d at least like to try to correctly account for it in the user flow.

Thanks!