Codeless IAP, purchases automatically refunded.

Hello! We have been struggling with this for almost a month now, we’ve read through every post we have seen about it, and still have made little or no progress at all.

We have a pretty simple ‘shop’, a single button that unlocks 10 more levels and disables ads. It works on the editor, however, once we build it and upload the aab into Google Play, we are not able to finish the purchase. It should enter the UnlockLevel method, but it will not do so. Levels won’t be unlocked, ads will remain there, and the boolean won’t turn true (Purchase of product XXX failed due to Unknown).

We are still able to interact with the button, but it will display a ‘Item already purchased’ text, and after 5mins, we will receive a Google Play mail saying that our purchase has been refunded.

I’ve seen a lot of people with the very same problem than us, and we have tried to follow the mail’s solution, but it involves modifiying the graddle and we are quite new to Unity, so we haven’t been able to do anything at all. As far as we know, it may be something related to the Google Billing.

Unity 2019.2.8f1, IAP 1.06/2.2.2/2.2.7, none of them worked.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEngine.Purchasing;
using UnityEngine.UI;

public class Shop : MonoBehaviour
{
    DataManager dataManager;
    public LevelStats levelStats;
    public GameObject iapButton, iapButton2;
    public Text text;

    private void OnEnable()
    {
        dataManager = GameObject.FindGameObjectWithTag("DataManager").GetComponent<DataManager>();

        if (dataManager.dlc)
        {
            iapButton.SetActive(false);
        }
    }

    public void UnlockLevel(Product product)
    {
        Debug.Log("DLC " + product.definition.id + "purchased.");
        dataManager.dlc = true;
        dataManager.Save();
        levelStats.DLCUnlocked();
#if UNITY_EDITOR
        StartCoroutine("Delay");
#else
        iapButton.SetActive(false);
#endif
    }

    public void PurchaseFailed(Product product, PurchaseFailureReason purchaseFailureReason)
    {
        Debug.Log("Purchase of product " + product.definition.id + " failed due to " + purchaseFailureReason);
        text.text = "Purchase of product " + product.definition.id + " failed due to " + purchaseFailureReason;
    }

    private IEnumerator Delay()
    {
        yield return new WaitForEndOfFrame();
        iapButton.SetActive(false);
    }
}

@nullwaresoft Ensure you select the “Consume” checkbox for the IAP Button properties in the Inspector.

Hey! It indeed is.

Today I even tried to use Unity 2020.3.5f1 with IAP 3.1.0, just in case it had to do with older versions.

So is your issue resolved? I wasn’t sure from your reply. If not, please provide the device logs https://discussions.unity.com/t/699654 You might consider testing (and publishing!) the Sample IAP Project here https://discussions.unity.com/t/700293

Sorry, I wasnt very clear before. The issue remains the same, I checked the “Consume” checkbox the very first day. I will try to have the logs for tomorrow!

Sounds good. Also, show a screenshot that shows your IAP products as defined on your Google Play developer dashboard, showing the same ProductIDs as you have listed in your game https://docs.unity3d.com/Manual/UnityIAPGoogleConfiguration.html . Ensure you download from Google Play as a tester to properly test IAP. Also, you won’t want to be using Codeless IAP since you will need to add code to customize.

Hey! Sorry for not being able to reach out to you yesterday. I have the log and the screenshots here ^^

7085389--843394--Captura.PNG
7085389--843397--Captura2.PNG
7085389--843400--Screenshot_20210428-101352_Google_Play_Store.jpg
7085389--843403--Screenshot_20210428-101409_OsirisRevenge.jpg
7085389–843406–log.txt (28.9 KB)

If you look towards the bottom of the logs, I believe you will see the issue. “Purchase not correctly processed”. Again, I would recommend Scripted IAP for reasons like this.

I’ll try so. However, why is that happening? Shouldn’t this be working, at least for simple purchases?

Please use the Sample IAP Project, I would not be able to debug your game. Is your IAP Button active on the very first loading screen? Otherwise, are you using an IAP Listener on start? Scripted IAP avoids these issues.

Hello! We switched to Scripted IAP using that very same script (a bit modified).

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;
using UnityEngine.Purchasing.Security;


public class IAP : MonoBehaviour, IStoreListener
{
    private static IStoreController m_StoreController;          // The Unity Purchasing system.
    private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    private IAppleExtensions m_AppleExtensions;
    private IGooglePlayStoreExtensions m_GoogleExtensions;

    // ProductIDs
    public static string NONCONSUMABLE1 = "com.nullwaresoft.osirisrevenge.content";

    public Text myText;
    DataManager dataManager;
    public LevelStats levelStats;
    public GameObject iapButton;

    void Start()
    {
        // If we haven't set up the Unity Purchasing reference
        if (m_StoreController == null)
        {
            // Begin to configure our connection to Purchasing, can use button click instead
            InitializePurchasing();
        }

        dataManager = GameObject.FindGameObjectWithTag("DataManager").GetComponent<DataManager>();

        if (dataManager.dlc)
        {
            iapButton.SetActive(false);
        }
        else
        {
            iapButton.SetActive(true);
        }
    }

    public void MyInitialize()
    {
        InitializePurchasing();
    }

    public void InitializePurchasing()
    {
        if (IsInitialized())
        {
            return;
        }

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

        builder.AddProduct(NONCONSUMABLE1, ProductType.NonConsumable);

        MyDebug("Starting Initialized...");
        UnityPurchasing.Initialize(this, builder);
    }


    private bool IsInitialized()
    {
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }

    public void BuyNonConsumable()
    {
        BuyProductID(NONCONSUMABLE1);
    }

    public void RestorePurchases()
    {
        m_StoreExtensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(result => {
            if (result)
            {
                MyDebug("Restore purchases succeeded.");
            }
            else
            {
                MyDebug("Restore purchases failed.");
            }
        });
    }

    void BuyProductID(string productId)
    {
        if (IsInitialized())
        {
            UnityEngine.Purchasing.Product product = m_StoreController.products.WithID(productId);

            if (product != null && product.availableToPurchase)
            {
                MyDebug(string.Format("Purchasing product:" + product.definition.id.ToString()));
                m_StoreController.InitiatePurchase(product);
            }
            else
            {
                MyDebug("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
            }
        }
        else
        {
            MyDebug("BuyProductID FAIL. Not initialized.");
        }
    }

    public void ListProducts()
    {

        foreach (UnityEngine.Purchasing.Product item in m_StoreController.products.all)
        {
            if (item.receipt != null)
            {
                MyDebug("Receipt found for Product = " + item.definition.id.ToString());
            }
        }
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        MyDebug("OnInitialized: PASS");

        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;
        m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
        m_GoogleExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();

        m_GoogleExtensions?.SetDeferredPurchaseListener(OnPurchaseDeferred);

        Dictionary<string, string> dict = m_AppleExtensions.GetIntroductoryPriceDictionary();

        foreach (UnityEngine.Purchasing.Product item in controller.products.all)
        {

            if (item.receipt != null)
            {
                string intro_json = (dict == null || !dict.ContainsKey(item.definition.storeSpecificId)) ? null : dict[item.definition.storeSpecificId];

                if (item.definition.type == ProductType.Subscription)
                {
                    SubscriptionManager p = new SubscriptionManager(item, intro_json);
                    SubscriptionInfo info = p.getSubscriptionInfo();
                    MyDebug("SubInfo: " + info.getProductId().ToString());
                    MyDebug("isSubscribed: " + info.isSubscribed().ToString());
                    MyDebug("isFreeTrial: " + info.isFreeTrial().ToString());
                }
            }
        }
    }

    public void OnPurchaseDeferred(Product product)
    {

        MyDebug("Deferred product " + product.definition.id.ToString());
    }

    public void OnInitializeFailed(InitializationFailureReason error)
    {
        // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
        MyDebug("OnInitializeFailed InitializationFailureReason:" + error);
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    {

        try
        {
            var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
            var result = validator.Validate(args.purchasedProduct.receipt);
            MyDebug("Validate = " + result.ToString());

            foreach (IPurchaseReceipt productReceipt in result)
            {
                MyDebug("Valid receipt for " + productReceipt.productID.ToString());
                if (String.Equals(args.purchasedProduct.definition.id, NONCONSUMABLE1, StringComparison.Ordinal))
                {
                    MyDebug(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                    UnlockLevel();
                }
            }
        }
        catch (Exception e)
        {
            MyDebug("Error is " + e.Message.ToString());
        }

        MyDebug(string.Format("ProcessPurchase: " + args.purchasedProduct.definition.id));

        return PurchaseProcessingResult.Complete;
    }


    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureReason failureReason)
    {
        MyDebug(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    }


    private void MyDebug(string debug)
    {

        Debug.Log(debug);
        myText.text += "\r\n" + debug;
    }

    public void UnlockLevel()
    {
        dataManager.dlc = true;
        dataManager.Save();
        levelStats.DLCUnlocked();
        iapButton.SetActive(false);
    }
}

However, it’s not even getting into the try/catch method, it’s still giving us a ‘PurchaseFailureReason: Unkown’. Here is the log ^^

7089466–844222–log2.txt (40.6 KB)

@nullwaresoft Are you testing by first downloading from Google Play? This is necessary. What Android device are you testing on? Can you try with a new product? We’ve heard of other reports of the Unknown error, but we are not able to reproduce.

Yes, we are using the store to download the app through internal testing. We tested with three different products. Now we’ve tried with an empty project, same script, and we got a different error (product name is com.nullwaresoft.test.test, just to make the search easier).

With this project, we are not getting the refund mail (hence, we are not able to buy the product again after 5mins, since its a non consumable), however it doesn’t seem to be working either.

7090324–844372–log.txt (27.1 KB)

What is the error? Do you get the purchase dialog? Are you receiving ProcessPurchase? It looks like the receipt validator threw an exception, can you confirm? Please show your updated code, your previous code would not be expected to call UnlockLevel (which is checking for NONCONSUMABLE1). If this was a new project you are testing, did you generate new Tangle files?

Hey! I was able to get it working on the test project just by generating tangle files again, something must had gone wrong the first time. However, after doing the same on the other project, we are still getting the Unknown error.

We’re using the very same script, new tangle files, but we can’t get past this Unknown error. Both scripts here (first is the test project, second is the main one)

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;
using UnityEngine.Purchasing.Security;


public class IAP : MonoBehaviour, IStoreListener
{
    private static IStoreController m_StoreController;          // The Unity Purchasing system.
    private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    private IAppleExtensions m_AppleExtensions;
    private IGooglePlayStoreExtensions m_GoogleExtensions;

    // ProductIDs
    //public static string NONCONSUMABLE1 = "com.nullwaresoft.test.test2";
    public static string NONCONSUMABLE1 = "com.defaultcompany.iaptest.prueba";


    public Text myText;
    public GameObject image;

    void Start()
    {
        // If we haven't set up the Unity Purchasing reference
        if (m_StoreController == null)
        {
            // Begin to configure our connection to Purchasing, can use button click instead
            InitializePurchasing();
        }
    }

    public void MyInitialize()
    {
        InitializePurchasing();
    }

    public void InitializePurchasing()
    {
        if (IsInitialized())
        {
            return;
        }

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

        builder.AddProduct(NONCONSUMABLE1, ProductType.NonConsumable);

        MyDebug("Starting Initialized...");
        UnityPurchasing.Initialize(this, builder);
    }


    private bool IsInitialized()
    {
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }

    public void BuyNonConsumable()
    {
        BuyProductID(NONCONSUMABLE1);
    }

    public void RestorePurchases()
    {
        m_StoreExtensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(result => {
            if (result)
            {
                MyDebug("Restore purchases succeeded.");
            }
            else
            {
                MyDebug("Restore purchases failed.");
            }
        });
    }

    void BuyProductID(string productId)
    {
        if (IsInitialized())
        {
            UnityEngine.Purchasing.Product product = m_StoreController.products.WithID(productId);

            if (product != null && product.availableToPurchase)
            {
                MyDebug(string.Format("Purchasing product:" + product.definition.id.ToString()));
                m_StoreController.InitiatePurchase(product);
            }
            else
            {
                MyDebug("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
            }
        }
        else
        {
            MyDebug("BuyProductID FAIL. Not initialized.");
        }
    }

    public void ListProducts()
    {

        foreach (UnityEngine.Purchasing.Product item in m_StoreController.products.all)
        {
            if (item.receipt != null)
            {
                MyDebug("Receipt found for Product = " + item.definition.id.ToString());
            }
        }
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        MyDebug("OnInitialized: PASS");

        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;
        m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
        m_GoogleExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();

        m_GoogleExtensions?.SetDeferredPurchaseListener(OnPurchaseDeferred);

        Dictionary<string, string> dict = m_AppleExtensions.GetIntroductoryPriceDictionary();

        foreach (UnityEngine.Purchasing.Product item in controller.products.all)
        {

            if (item.receipt != null)
            {
                string intro_json = (dict == null || !dict.ContainsKey(item.definition.storeSpecificId)) ? null : dict[item.definition.storeSpecificId];

                if (item.definition.type == ProductType.Subscription)
                {
                    SubscriptionManager p = new SubscriptionManager(item, intro_json);
                    SubscriptionInfo info = p.getSubscriptionInfo();
                    MyDebug("SubInfo: " + info.getProductId().ToString());
                    MyDebug("isSubscribed: " + info.isSubscribed().ToString());
                    MyDebug("isFreeTrial: " + info.isFreeTrial().ToString());
                }
            }
        }
    }

    public void OnPurchaseDeferred(Product product)
    {

        MyDebug("Deferred product " + product.definition.id.ToString());
    }

    public void OnInitializeFailed(InitializationFailureReason error)
    {
        // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
        MyDebug("OnInitializeFailed InitializationFailureReason:" + error);
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    {

        try
        {
            var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
            var result = validator.Validate(args.purchasedProduct.receipt);
            MyDebug("Validate = " + result.ToString());

            foreach (IPurchaseReceipt productReceipt in result)
            {
                MyDebug("Valid receipt for " + productReceipt.productID.ToString());
                if (String.Equals(args.purchasedProduct.definition.id, NONCONSUMABLE1, StringComparison.Ordinal))
                {
                    MyDebug(args.purchasedProduct.definition.id.ToString());
                    MyDebug(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                    image.SetActive(true);
                }
            }
        }
        catch (Exception e)
        {
            MyDebug("Error is " + e.Message.ToString());
        }

        MyDebug(string.Format("ProcessPurchase: " + args.purchasedProduct.definition.id));

        return PurchaseProcessingResult.Complete;

    }


    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureReason failureReason)
    {
        MyDebug(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    }


    private void MyDebug(string debug)
    {

        Debug.Log(debug);
        myText.text += "\r\n" + debug;
    }

}
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;
using UnityEngine.Purchasing.Security;


public class IAP : MonoBehaviour, IStoreListener
{
    private static IStoreController m_StoreController;          // The Unity Purchasing system.
    private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    private IAppleExtensions m_AppleExtensions;
    private IGooglePlayStoreExtensions m_GoogleExtensions;

    // ProductIDs
    public static string NONCONSUMABLE1 = "com.nullwaresoft.osirisrevenge.content";

    public Text myText;
    DataManager dataManager;
    public LevelStats levelStats;
    public GameObject iapButton;

    void Start()
    {
        // If we haven't set up the Unity Purchasing reference
        if (m_StoreController == null)
        {
            // Begin to configure our connection to Purchasing, can use button click instead
            InitializePurchasing();
        }

        dataManager = GameObject.FindGameObjectWithTag("DataManager").GetComponent<DataManager>();

        if (dataManager.dlc)
        {
            iapButton.SetActive(false);
        }
        else
        {
            iapButton.SetActive(true);
        }
    }

    public void MyInitialize()
    {
        InitializePurchasing();
    }

    public void InitializePurchasing()
    {
        if (IsInitialized())
        {
            return;
        }

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

        builder.AddProduct(NONCONSUMABLE1, ProductType.NonConsumable);

        MyDebug("Starting Initialized...");
        UnityPurchasing.Initialize(this, builder);
    }


    private bool IsInitialized()
    {
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }

    public void BuyNonConsumable()
    {
        BuyProductID(NONCONSUMABLE1);
    }

    public void RestorePurchases()
    {
        m_StoreExtensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(result => {
            if (result)
            {
                MyDebug("Restore purchases succeeded.");
            }
            else
            {
                MyDebug("Restore purchases failed.");
            }
        });
    }

    void BuyProductID(string productId)
    {
        if (IsInitialized())
        {
            UnityEngine.Purchasing.Product product = m_StoreController.products.WithID(productId);

            if (product != null && product.availableToPurchase)
            {
                MyDebug(string.Format("Purchasing product:" + product.definition.id.ToString()));
                m_StoreController.InitiatePurchase(product);
            }
            else
            {
                MyDebug("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
            }
        }
        else
        {
            MyDebug("BuyProductID FAIL. Not initialized.");
        }
    }

    public void ListProducts()
    {

        foreach (UnityEngine.Purchasing.Product item in m_StoreController.products.all)
        {
            if (item.receipt != null)
            {
                MyDebug("Receipt found for Product = " + item.definition.id.ToString());
            }
        }
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        MyDebug("OnInitialized: PASS");

        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;
        m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
        m_GoogleExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();

        m_GoogleExtensions?.SetDeferredPurchaseListener(OnPurchaseDeferred);

        Dictionary<string, string> dict = m_AppleExtensions.GetIntroductoryPriceDictionary();

        foreach (UnityEngine.Purchasing.Product item in controller.products.all)
        {

            if (item.receipt != null)
            {
                string intro_json = (dict == null || !dict.ContainsKey(item.definition.storeSpecificId)) ? null : dict[item.definition.storeSpecificId];

                if (item.definition.type == ProductType.Subscription)
                {
                    SubscriptionManager p = new SubscriptionManager(item, intro_json);
                    SubscriptionInfo info = p.getSubscriptionInfo();
                    MyDebug("SubInfo: " + info.getProductId().ToString());
                    MyDebug("isSubscribed: " + info.isSubscribed().ToString());
                    MyDebug("isFreeTrial: " + info.isFreeTrial().ToString());
                }
            }
        }
    }

    public void OnPurchaseDeferred(Product product)
    {

        MyDebug("Deferred product " + product.definition.id.ToString());
    }

    public void OnInitializeFailed(InitializationFailureReason error)
    {
        // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
        MyDebug("OnInitializeFailed InitializationFailureReason:" + error);
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    {

        try
        {
            var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
            var result = validator.Validate(args.purchasedProduct.receipt);
            MyDebug("Validate = " + result.ToString());

            foreach (IPurchaseReceipt productReceipt in result)
            {
                MyDebug("Valid receipt for " + productReceipt.productID.ToString());
                if (String.Equals(args.purchasedProduct.definition.id, NONCONSUMABLE1, StringComparison.Ordinal))
                {
                    MyDebug(args.purchasedProduct.definition.id.ToString());
                    MyDebug(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                    UnlockLevel();
                }
            }
        }
        catch (Exception e)
        {
            MyDebug("Error is " + e.Message.ToString());
        }

        MyDebug(string.Format("ProcessPurchase: " + args.purchasedProduct.definition.id));

        return PurchaseProcessingResult.Complete;
    }


    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureReason failureReason)
    {
        MyDebug(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    }


    private void MyDebug(string debug)
    {

        Debug.Log(debug);
        myText.text += "\r\n" + debug;
    }

    public void UnlockLevel()
    {
        dataManager.dlc = true;
        dataManager.Save();
        levelStats.DLCUnlocked();
        iapButton.SetActive(false);
    }
}

You said our code would not be expected to call UnlockLevel, however, it did work on the other script by enabling the image I referenced. Where would that piece of code go, then? It seems to be working there (and I doubt the Unknown error comes from that piece of code).

We’ll be testing our main project on Unity 2020 later today, since that’s the only difference between our both projects right now.

Update: it worked flawlessly after updating to Unity 2020.1.4f1 ! The only doubt that remains is how to handle several products, as I expect it would be through the ‘PurchaseProcessingResult ProcessPurchase’ method, but I’m not sure.

Thanks for all your help :slight_smile:

Got it! Interesting that it was Unity version related. Make sure you are not using a Codeless IAP Button if you are using scripted IAP. Each product will trigger ProcessPurchase, you can inspect the arguments to see which product it is.