IAP Android, refunded transactions & validation

Hello,

It seems that receipts for transactions that are refunded validate as valid, so how do I make sure that the item is actually owned when validating the receipt? Ie. report the receipt as invalid if transaction was refunded.

I’ve made some test orders, refunded them, now I need IAP / receipt validator to say no to these purchases.

Have you checked “Revoke”? According to Google documentation, if “Revoke” is not checked then the subscription still remains active.

Refund and revoke: If you refund the most recent order in a subscription, the order is refunded, the user’s subscription is removed immediately, and future recurrences are automatically canceled. You can also perform this action with the Google Play Developer API.
Refund only: If you refund an older order in a subscription, the order is refunded and the subscription remains active. You can also perform this action with the Google Play Developer API

This is not a subscription and I don’t see a Revoke button. It is also not a consumable.

6078846--659547--brave_EHT6V1F1jY[1].png

What are the properties in the receipt now? Please show your code.

I use this simple code borrowed from Unity - Manual: Receipt validation

I store the receipt on disk and after refunding the transaction purchaseState is still Purchased (even after a day of waiting).

    public void ValidateReceipt(string receipt) {
        var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
        bool unlock = false;

        if (Application.isEditor) {
            LevelData.fullVersion = true;
            return;
        }

        try {
            var result = validator.Validate(receipt);
         
            foreach (GooglePlayReceipt productReceipt in result) {
                if (productReceipt.productID == "full_version") {
                    Debug.Log("purchase state: " + productReceipt.purchaseState);
                    if (productReceipt.purchaseState == GooglePurchaseState.Purchased) {
                        unlock = true;
                    }
                }
            }
        } catch (IAPSecurityException) {
            Debug.Log("Invalid receipt, not unlocking content");
        }

        if (unlock) {
            LevelData.fullVersion = true;
            // SaveStringReceipt(receipt);
            return;
        }

        // ClearStringReceipt();
    }

If you store it on disk, of course nothing will change. Please show your full Purchasing code used during initialization.

Right, so the question is how should I be verifying if the user still has the purchased item on app start? I am using a codeless IAP Button to make the purchase and this works correctly for the purchase itself but I need to verify on app start that the user still owns the purchased non-consumable product.

This is related to IAP receipt validation on app start

I had offered a solution in your other post. Some developers use PlayerPrefs to store purchase history, but it’s not entirely secure and may be deleted during reinstall. There are other services like ChiliConnect and PlayFab, but during early app development and prototyping, probably easiest to just use PlayerPrefs and swap it out for a more robust solution later on and once you have your logic working Unity - Scripting API: PlayerPrefs

Thank you but my question is not how to store information that a purchase has been completed but how to verify that the user still owns the product in all subsequent launches of the app. The content needs to go back to locked when the user no longer owns the product ie. because of a refund.

1 Like

Please share your IAP initialization script and your ProcessPurchase method. Please place appropriate Debug.Log statements showing the run time values, and provide the device logs (which will contain the Debug.Log output) How To - Capturing Device Logs on Android

I think we are trying to solve the same issue:
https://discussions.unity.com/t/801156

It’s my understanding that the receipt should be removed, and you should not see it during initialization. Can you confirm in your logs with Debug.Log statements?

Does this mean that for a non-consumable non-subscription product, a receipt should appear during initialization if the product is owned, and not appear if the product is not owned for any reason?

My original question here is as follows: How do I make sure the user still owns the product at any given time in the future? A refund may occur and then the product is no longer owned.

1 Like

For non-consumable products, they can be refunded and revoke too.
6136037--669266--upload_2020-7-27_16-13-24.png

6136037--669269--upload_2020-7-27_16-13-55.png

Thanks. You are correct! How long do I need to wait after refund+revoke for hasReceipt to be false and/or receipt to be null or have purchaseState different than 0?

Actually, it depends on the store and out of our control, but according to my testing experience, it usually takes less than one minute. I encourage you to try to test it yourself.

I’m having issues with this as well. Checking after initialization to see if a non-consumeable has been refunded and then revoking the product inside the game. However I refund/revoke it still checks as if the product is owned.

First checking the IsOwned bool, then if it has a reciept and then the state of the reciept. They always return as if the product is still owned. Tried on two different accounts. Using Unity 2019.3.10 and IAP 1.23.4. Oh, and I just have one product in the entire game.

        if(m_StoreExtensionProvider != null && m_StoreController != null) {
            ownsPremiumInApp = m_StoreExtensionProvider.GetExtension<IGooglePlayStoreExtensions>().IsOwned(m_StoreController.products.WithID(products[0].androidId));
            Debug.Log("InAppManager: IsOwned is reported as " + ownsPremiumInApp);
            UnityEngine.Purchasing.Product product = m_StoreController.products.WithID(products[0].androidId);
            if (product != null) {
                if(!product.hasReceipt || string.IsNullOrEmpty(product.receipt)) {
                    Debug.Log("InAppManager: Product has no receipt, invalidate purchase.");
                    ownsPremiumInApp = false;
                } else {
                    bool onePurchaseIsValid = false;
                    var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
                    var validationResult = validator.Validate(product.receipt);
                    foreach (IPurchaseReceipt productReceipt in validationResult) {
                        GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
                        if (null != google) {
                            if (google.purchaseState == GooglePurchaseState.Purchased) {
                                onePurchaseIsValid = true;
                            }
                        }
                    }

                    if(!onePurchaseIsValid) {
                        Debug.Log("InAppManager: All purchases were canceled or refunded.");
                        ownsPremiumInApp = false;
                    } else {
                        Debug.Log("InAppManager: One purchase is valid.");
                    }
                }
            }

            if (!ownsPremiumInApp && !GameManager.Instance.unlockPremiumAtStart) {
                Debug.Log("InAppManager: Player doesn't own Premium SKU, disabling Premium.");
                GameManager.Instance.SetRemovePremium();
            } else if(ownsPremiumInApp) {
                Debug.Log("InAppManager: Player owns Premium SKU, enabling Premium.");
                GameManager.Instance.SetBoughtPremium();
            }
        }
1 Like

Got it working, the issue was the delay in which in-apps are considered refunded. Seems to be up towards a day until it counts as refunded. Also the IsOwned() function crashes if called on a refunded in-app. So I try catched that and if crashed moved onto the next check instead.

3 Likes

I can confirm that this was also the issue we were having.

It seem to take a long time for the IAP revoke to actually be propagated to the devices (several hours ++).

1 Like

This is being deprecated…
ownsPremiumInApp = m_StoreExtensionProvider.GetExtension().IsOwned(m_StoreController.products.WithID(products[0].androidId));
The .isowned part.
Unity docs don’t recommend anything else in it’s place. Does anyone know the workaround?