14

I'm having trouble figuring out how to detect when a refund has been issued for a managed (uncomsumable) in-app product in Android using com.android.billingclient:billing:2.0.3. The problem seems fairly deep though maybe I'm making it more complicated than it ought to be.

To begin, I've made a test purchase which has been acknowledged AND refunded:

enter image description here

Looking at the logs of my app I see the following:

D/BillingManager: Got a verified purchase: Purchase. Json: {"orderId":"GPA.3362-7185-5389-78416","packageName":"com.glan.input","productId":"pro","purchaseTime":1567672759460,"purchaseState":0,"purchaseToken":"pbkpcaadklleoecegjfjdpbl.AO-J1OwsR6WVaVZCCYOU6JyYN1r0qJsrwitIPZfhc3jX4yketRUwNzKqwMgYx0TgZ2GebEGbXDL0RlMyogwtSKSPsaHCJ4RA4MPlIGay-aM1-QhmnqwjXjQ","acknowledged":true}
I/BillingManager: purchase pbkpcaadklleoecegjfjdpbl.AO-J1OwsR6WVaVZCCYOU6JyYN1r0qJsrwitIPZfhc3jX4yketRUwNzKqwMgYx0TgZ2GebEGbXDL0RlMyogwtSKSPsaHCJ4RA4MPlIGay-aM1-QhmnqwjXjQ is in 1 state

There's something funny going on here:

  1. We can see the order IDs match up between what's in the image and the detected purchase
  2. The first log line is printing the purchase with Log.d(TAG, "Got a verified purchase: " + purchase); which is printing the underlying JSON which represents the purchase.
  3. Note that "purchaseState":0
  4. The second log line is issued with Log.i(TAG, "purchase " + purchase.getPurchaseToken() + " is in " + purchase.getPurchaseState() + " state");.
  5. Note that here purchase.getPurchaseState() is resulting in a value of 1

If I look at the implementation of getPurchaseState in Android Studio I see the following:

  public @PurchaseState int getPurchaseState() {
    switch (mParsedJson.optInt("purchaseState", PurchaseState.PURCHASED)) {
      case 4:
        return PurchaseState.PENDING;
      default:
        return PurchaseState.PURCHASED;
    }
  }

Earlier in the file the PurchaseState interface is declared as:

  @Retention(SOURCE)
  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;
  }

It seems like getPurchaseState never returns PurchaseState.UNSPECIFIED_STATE and only returns PENDING which the JSON comes with a value of 4. I've confirmed that a state of PENDING is correctly returned when the purchase is performed with a payment method that takes a while to approve.

I've found posts like In-App Billing v3 - Don't detect refund which suggest that Play Services are caching purchases but I'm not convinced that's causing this problem because if I modify my code betweens runs of my app to acknowledge/consume the purchase those get state changes get immediately reflected in the JSON of the purchase.

How am I supposed to detect a refunded managed product?

Paymahn Moghadasian
  • 9,301
  • 13
  • 56
  • 94

2 Answers2

7

I have one purchase (SkuType.INAPP) in my application. I make a test purchase and then make a refund.

Problem:

purchase.getOriginalJson()  // contains "purchaseState":0
purchase.getPurchaseState() // returns 1

Inside com.android.billingclient.api.Purchase:

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

Hacky way to check purchaseState from original json:

purchase.getOriginalJson().contains(String.format("\"purchaseState\":%s", Purchase.PurchaseState.PURCHASED))

Unfortunately, this problem still exists!

More details here.

kitfist0
  • 97
  • 2
  • 4
2

You can check if still purchase exits following

 binding.btnRestore.setOnClickListener(v->{


            Purchase.PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP);
            for ( Purchase purchase : purchasesResult.getPurchasesList()){
                handlePurchase(purchase);

            }
});

Google Play returns the purchases made by the user account logged in to the device. If the request is successful, the Play Billing Library stores the query results in a List of Purchase objects.

Note: Only active subscriptions appear on this list. As long as the in-app product is on this list, the user should have access to it. For further information, see the Handle SUBSCRPTION_ON_HOLD section of Add subscription-specific features. To retrieve the list, call getPurchasesList() on the PurchasesResult. You can then call a variety of methods on the Purchase object to view relevant information about the item, such as its purchase state or time. To view the types of product detail information that are available, see the list of methods in the Purchase class.

You should call queryPurchases() at least twice in your code:

Call queryPurchases() every time your app launches so that you can restore any purchases that a user has made since the app last stopped. Call queryPurchases() in your onResume() method, because a user can make a purchase when your app is in the background (for example, redeeming a promo code in the Google Play Store app). Calling queryPurchases() on startup and resume guarantees that your app finds out about all purchases and redemptions the user may have made while the app wasn't running. Furthermore, if a user makes a purchase while the app is running and your app misses it for any reason, your app still finds out about the purchase the next time the activity resumes and calls queryPurchases().

Query most recent purchases The queryPurchases() method uses a cache of the Google Play Store app without initiating a network request. If you need to check the most recent purchase made by the user for each product ID, you can use queryPurchaseHistoryAsync(), passing the purchase type and a PurchaseHistoryResponseListener to handle the query result.

queryPurchaseHistoryAsync() returns a PurchaseHistory object that contains info about the most recent purchase made by the user for each product ID, even if that purchase is expired, cancelled, or consumed. Use queryPurchases() whenever possible, as it uses the local cache, instead of queryPurchaseHistoryAsync(). If using queryPurchaseHistoryAsync(), you can also combine it with a Refresh button, allowing users to update their list of purchases.

The following code demonstrates how you can override the onPurchaseHistoryResponse() method:

  private void handlePurchase(Purchase purchase) {
        if(purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {

            if (purchase.getSku().equals(skuPro)) {
                EntityPRO entityPRO = RoomDB.getDatabase(context).proDAO().getLastItem();
                entityPRO.isBought = true;
                RoomDB.getDatabase(context).proDAO().updateSpecificSLI(entityPRO);
                Toast.makeText(context, context.getString(R.string.pro_succesfully_bought), Toast.LENGTH_LONG).show();

            }

            if (!purchase.isAcknowledged()) {
                AcknowledgePurchaseParams acknowledgePurchaseParams =
                        AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.getPurchaseToken())
                                .build();
                mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
            }

        }

    }
Ucdemir
  • 2,852
  • 2
  • 26
  • 44
  • 1
    One note for queryPurchases(). I suppose "Only active subscriptions appear on this list." is not correct for now. I see cancelled subscription in original json with "purchaseState":0. Looks like it's UNSPECIFIED_STATE as said here https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#unspecified_state. This subscription is no longer present in Subscriptions on Google Play app and i got email that is was cancelled. Also I have tried to sync data and wait, but nothing changed. – Evgen Sep 25 '19 at 10:31
  • Any solution for this? – sejn Jun 03 '22 at 09:40