Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android in-app billing - How to handle refunds

I am trying to implement in-app billing in my app, everything works great except for when it comes to refunds. I have been banging my head against this for the past few days and it seems unbelievable that there is no way to know, form the app side, if a user requested a refund. I would like to be able to revoke access to a one-time managed product (remove ads) after the user has been refunded. I am not using a backend so I am relying on Google Play APIs.

What I have tried is querying the Google Play APIs with queryPurchaseHistoryAsync which returns a list of recent purchases made by the user. This does not seem to work as the purchases are still there after asking for a refund (been waiting for one day before writing this).

Here's what I did:

  1. Install the app on a real device
  2. Buy the in-app content
  3. Verify that app unlocks the content
  4. Go to my Google Play order history and ask for refund for the in-app product
  5. 10 minutes later the transaction got refunded (without me as a developer being involved at all)
  6. App still provides the paid content
  7. Cleaning Play Store App data & cache
  8. App still provides the paid content

So any user can buy my in-app product and then immediately go to his Google Play page and ask for a refund? Is it me missing something obvious or this API is a nightmare?

The PurchaseState enum is

  public @interface PurchaseState {
    // Purchase with unknown state.
    int UNSPECIFIED_STATE = 0;
    // Purchase is completed.
    int PURCHASED = 1;
    // Purchase is waiting for payment completion.
    int PENDING = 2;
  }

I do not see anything related to refunded here, This seems to me a pretty normal use case, so I'm still thinking that I am missing some key piece of information, how do I do this?

Thanks for any help

like image 624
Francesco Rigoni Avatar asked May 11 '20 16:05

Francesco Rigoni


1 Answers

Problem

Any made purchase will still be recorded even when a user makes a refund, and actually gets the refund. The same is true when you (the developer/app owner) issue a refund/revoke request for the in-app product.

The only difference will be the purchase's "purchaseState". The problem here with Google's Billing Library is that they mask this "purchaseState" value in the purchase.getPurchaseState() call to either PENDNG or PURCHASED state. See in the decompiled code:

public int getPurchaseState() {
    switch(this.zzc.optInt("purchaseState", 1)) {
    case 4:
        return 2; // PENDING
    default:
        return 1; // PURCHASED
    }
}

When you have an in-app product with a refunded state, its state is rather UNSPECIFIED_STATE, which is masked to PURCHASED as in the code above.

Simple Solution

To get over this, simply just ignore the library's purchase.getPurchaseState() method, and instead use your own unmasked custom:

int getUnmaskedPurchaseState(Purchase purchase) {
    int purchaseState = purchase.getPurchaseState();
    try {
        purchaseState = new JSONObject(purchase.getOriginalJson()).optInt("purchaseState", 0);
    } catch (JSONException e) {
        e.printStackTrace();
    }

    return purchaseState;
}

Hard Solution

It seems Google has just done that masked to push you to do this solution. Use the Voided Purchases API to provide a list of orders that are associated with purchases that a user has voided. According to their documentation...

A purchase can be voided in the following ways:

  • The user requests a refund for their order. The user cancels their order.
  • An order is charged back.
  • Developer cancels or refunds order. Note: only revoked orders will be shown in the Voided Purchases API.
  • If developer refunds without setting the revoke option, orders will not show up in the API.
  • Google cancels or refunds order.

However, this solution is not currently available as a Java android library, and you will have instead to make your server requests, and to use Google Play Developer APIs.

like image 179
innoviler Avatar answered Nov 19 '22 20:11

innoviler