8
votes

I implemented in-app billing into my app and am now testing its handling of refunds.

I bought my app's managed in-app billing item with a test account and refunded it. My app got the refund broadcast as expected and it sees that the item was refunded when restoring transactions, so everything is good up to that point.

My problem is that I can't re-buy the item to test other scenarios. When I try to purchase the item, the Google Play interface comes up and displays an error message saying "You already own this item." with 2 buttons "OK" and "Details". If I press details, Google Play crashes and I return to my app.

Did anyone have a similar experience? Is it forbidden for a user to purchase an in-app item if they previously had it refunded?

5
Just to clarify: When you say that "it sees that the item was refunded when restoring transactions," do you mean that when you invoke the getPurchases() method of the InAppBillingService, that the returned Bundle no longer has the SKU in its RESPONSE_INAPP_ITEM_LIST, just as if the item has never been purchased? Or are you referring to something else, such as a positive notice (beyond the IN_APP_NOTIFY broadcast) that the specific refund has occurred? The TrivialDrive in-app billing sample app does not handle the refund scenario. Do you know of an example that does? TIA.Carl
What I mean is that the item is no longer listed in the list of purchased items when I send a RESTORE_TRANSACTIONS request. Same as if the item was never bought in the first place. However, this refers to version 2 of the In-app Billing API. The TrivialDrive sample uses version 3 of the API.Pooks

5 Answers

6
votes

I was seeing the same issue. GP crash and everything.

In addition to waiting a few hours, you may want to open up 'Google Play' app info and clear cache and clear data. This solved it for me. It appears GP caches purchase information on the device and only checks Google's servers rarely, if ever, for refund information.

Update: You may also want to kill the Google Play process since it appears to keep purchase info in memory too.

4
votes

I asked Google about this issue and they told me that it's not possible to re-buy an in-app billing item on Google Play if it was previously refunded. But when I tried to buy it again about 24 hours later, the purchase went through ...

So it looks like it's possible to re-buy, but only after some delay.

4
votes

I know this is an old question, but i have been looking for an answer to this same question and eventually came to my own conclusion. Google doesn't spell it out, but I believe they want you to decide on your own logic as to how to handle cancelled and refunded purchases. Another point to keep in mind is that there there is essentially no difference between a consumable and non consumable managed product. All managed products are consumable.

For me, when a user cancels a purchase, or if I decide to give the user a refund, what I want to happen is that 1) the user receives their money back and 2) the user loses access to the paid feature and 3) the user has the option to purchase the feature again if they choose.

What I did was to check the purchaseState of the purchase on my back end server using the in-app billing API. If the returned purchaseState is a 1 (canceled) or 2 (refunded), I consume the purchase in my app. Google handles item 1, giving the user their money back. The logic in my app handles 2, locking access to the paid features. Consuming the purchase handles 3, giving the user the option to purchase the feature again.

The basic gist of it is, when a purchase is sent to my back end server for verification, I check the purchase state. If the purchase state is a 1 or a 2, I return an appropriate code to my app. When my app receives the code indicating the purchase is cancelled or refunded, my app consumes the purchase.

I use the PHP version of the API, so my simplified code to get the purchase state is :

$purchases = $service->purchases_products->get($packageName, $productId, $purchaseToken);

$purchaseState = $purchases->getPurchaseState();

if($purchaseState === 1){
        $serverResponseCode = 3;
    }

    if($purchaseState === 2){
        $serverResponseCode = 4;
    }

...and then in my app, I check the server response codes.

if(serverResponseCode == 3 || serverResponseCode ==4 ){
     lockFeatures();
     ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
     billingClient.consumeAsync(params, listener);
}

I hope this helps someone else looking for an answer to this problem.

2
votes

In case somebody needs android and not kotlin code. All the explanation that smitty wrote:

When starting up the application , You have to check queryPurchases and look for the refunded items. like that:

if (purchase.getPurchaseState() != Purchase.PurchaseState.UNSPECIFIED_STATE)
{
     handleConsumablePurchasesAsync(purchasesList);
     return false;
}

Than you CONSUME this item. smitty1 is a Genius

private void handleConsumablePurchasesAsync(List<Purchase> consumables) {
    Log.d(TAG, "handleConsumablePurchasesAsync");
    for (Purchase purchase : consumables) {
        ConsumeParams params = ConsumeParams.newBuilder()
                .setPurchaseToken(purchase.getPurchaseToken())
                .build();

        billingClient.consumeAsync(params, (billingResult, purchaseToken) -> {
            if (billingResult.getResponseCode() == OK) {
                Log.d(TAG, "Consumed the old purchase that hasn't already been acknowledged");
            } else {
                Log.d(TAG, "Error consume the old purchase that hasn't already been acknowledged -> %s" + String.valueOf(billingResult.getResponseCode()));
            }
        });
    }
}
1
votes

I noticed that by checking the Remove Entitlements field on the refund page, you will be able to re-buy the product outright without waiting as suggested by the accepted answer.

Remove Entitlements after refunding