Android Google BillingClient how to get unconsumed items?
Asked Answered
P

3

6

Question: using the Google Play BillingClient 1.1 how can I get only unconsumed purchases?

Background:

In our Android app we wanted to change the "old" billing libraries (using the V.3 IabHelper.java class from the examples) using the new

com.android.billingclient:billing:1.1

The idea was to use

BillingClient.queryPurchases(BillingClient.SkuType.INAPP)

in order to get a "history" of all purchases the user made. This is what the API tells (https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#queryPurchases(java.lang.String)) and it works fine. In the old version one would only get the unconsumed items which is useless for something like a "purchase history". However if I want to know whether there are unconsumed items, I can no longer get that information.

Same for queryPurchaseHistoryAsync().

The Purchase objects don't seem to contain any information about their consumption. How can I get only not-consumed items (or at least filter them)?

Please note, I'm aware that the purchases eventually have to be managed on our server side. However until this is successful, it would be good to know which item was "redeemed" (consumed) already or not. So I can't rely on our backend only.

---- EDIT ----

The Purchase objects (JSON) coming from the query all look like this:

{
  "orderId": "[Id]",
  "packageName": "[just the app's package]",
  "productId": "[the SKU name]",
  "purchaseTime": 1531224460215,
  "purchaseState": 0,
  "purchaseToken": "[a purchase token]"
}

So nothing that appears to be a consumption state.

---- EDIT 2 ----

I found a (bad) solution for it. When trying to consume a purchase, google tells me with it's return code whether it was already consumed or not. But that is not a solution for us since we wanted to keep it "marked as unconsumed" until it's redeemed on our server.

Paratuberculosis answered 10/7, 2018 at 12:2 Comment(0)
P
6

Okay, I guess I found the answer. In case you have the same issue as I do, here is the solution:

The billingClient has two method calls.

  • The first one is

billingClient.queryPurchaseHistoryAsync().

This async call (internet required) is fetching a history of the last 11 purchases the user has made. The newest one is the first one (element 0 of the list). However this shows all purchases - no matter whether they are consumed or not. So if you want to know their state you can't do this here. If you have a backend system (and a working network connection) you may send them all to the server and validate them. Since our app is "offline first" this was not always possible for our users.

You may check this however when trying to purchase them again. A call of billingClient.launchBillingFlow() would return responseCode ITEM_ALREADY_OWNED (7) but you don't want to wait for the user trying to purchase it again. Especially since we wanted to guarantee the purchase already even without internet connection.

  • The solution is the other method.

The BillingClient offers:

billingClient.queryPurchases(BillingClient.SkuType.INAPP)

This returns a cached list of previous purchases. The API states this as

Get purchases details for all the items bought within your app.

However this seems to return a list of only the non consumed items. This way you can get non consumed items from the list of all (recent) purchases. Note however that this is cached by the playstore. If you need exact data you will still have to establish a connection with the google server (also when the playstore cache is cleared etc.).

This gives the option of a history of all purchases with additional information about non consumed items, which makes your app (partially) offline usable.

At least this is the best approach I was coming up with but it seems to work. Please let me know if you have any corrections or better solutions!!!

Paratuberculosis answered 12/7, 2018 at 10:13 Comment(4)
Thanks for this. I was wondering the same thing, "How do I only get a list of un-consumed items?" The Google documentation should be updated to show that queryPurchases only returns un-consumed items.Unexpected
Glad to help. Let me know if there are any other issues I didn't came up.Paratuberculosis
@TobiasReich "This async call (internet required) is fetching a history of the last 11 purchases the user has made" from where did u get this info ?Corncob
Actually it is just observation. Not sure if this is documented somewhere. It just does so for me.Paratuberculosis
P
2

The official documentation says that it is recommended to check purchases with the https://developers.google.com/android-publisher/api-ref/purchases/products/get request. In the response from JSON, there is a consumptionState field that shows whether the purchase is consumed.

{
  "kind": "androidpublisher#productPurchase",
  "purchaseTimeMillis": long,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer
}

Learn more https://developers.google.com/android-publisher/api-ref/purchases/products#resource. In this way, you can receive a purchase using the queryPurchaseHistoryAsync network query and check it as described above. But this way is not easy, because the request requires authorization, which needs to be configured https://developers.google.com/android-publisher/authorization

Plasia answered 19/12, 2018 at 6:46 Comment(1)
I assumed this is for backend side purchase checks but not on device. Well, I give it a try. Thanks!Paratuberculosis
P
2
            // When you call consumeAsync(),you shoud set developerPayload to distinguish consumed purchases.
            ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).setDeveloperPayload("userid_time_consumed").build();
            mBillingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
                @Override
                public void onConsumeResponse(BillingResult result, String purchaseToken) {
                    if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                        //do something
                    } else {
                        String errorMsg = "Recovery consume failed: " + result.getDebugMessage() + " Response code:" + result.getResponseCode();
                        Log.e(TAG, errorMsg);
                        //do something
                    }
                }
            });
        mBillingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, new PurchaseHistoryResponseListener() {
            @Override
            public void onPurchaseHistoryResponse(BillingResult result, List<PurchaseHistoryRecord> purchaseHistoryRecords) {
                if (mBillingClient == null) {
                    String errorMsg = "Store recovery failed, mBillingClient is null.";
                    Log.e(TAG, errorMsg);
                    return;
                }

                if (result.getResponseCode() != BillingClient.BillingResponseCode.OK) {
                    String errorMsg = "Query purchase history failed. " + result.getResponseCode();
                    Log.e(TAG, errorMsg);
                    return;
                }

                if (purchaseHistoryRecords != null && purchaseHistoryRecords.size() > 0) {
                    for (PurchaseHistoryRecord purchase : purchaseHistoryRecords) {
                        // Purchases need to be repaid only if payload is empty, which indicates failed to notify app server.
                        String payload = purchase.getDeveloperPayload();
                        if (payload == null || "null".equalsIgnoreCase(payload) || payload.isEmpty()) {
                            //notify app server ,when get the result "OK" from app server ,you should call consumeAsync(). (ps:you also need to set the "developerPayload")
                        }
                    }
                } else {
                    Log.d(TAG, "Purchase history is empty.");
                }
            }
        });
Procter answered 3/7, 2020 at 10:45 Comment(3)
maybe try to give facts or an explaination to your answer, give this a read on how to write an answerDoelling
PurchaseHistoryResponseListener code was so helpful! Thanks!Forced
I am not getting the purchaseList in my queryPurchase for the already purchased app. Does the above will help in the case if the purchase list is empty?Reactance

© 2022 - 2024 — McMap. All rights reserved.