[Bug] Unity IAP 3.1.0 ConfirmPendingPurchase does not prevent DuplicatePurchase

In our game, we initially tried to handle unprocessed purchases by doing the following:

  1. Try to Purchase product.

  2. The app store interface will open for the user to insert his payment info.

  3. After entering your info and before the app store interface closes itself (while it’s processing), close the app.

  4. While opening the game again, on the first loading screen (a splash screen scene), the ProcessPurchase callback will send the receipt to be validated on our server and return PurchaseProcessingResult.Pending.

  5. When the receipt is validated, the product will be added to a list of UnhandledProducts so we can track which products we need to give to the player when the game finishes loading.

  6. When the game is loaded, we call IStoreController.ConfirmPendingPurchase using the first product from the UnhandledProducts while also giving the player that reward.

  7. When the user reopens the app, steps 4-6 will happen again with the next pending reward. We need to reward only 1 item at a time for unrelated reasons.

All of this works as expected and the player can still purchase the same consumable item again, however, for some reason, if the player had “broken” a purchase before the version where IAP Restoration was implemented (went through steps 1-3 in earlier versions of the game), calling IStoreController.ConfirmPendingPurchase only prevents the ProcessPurchase callback from being called again for that product but does not actually consume the item.

If this player tries to purchase the same consumable again, the app store interface says that he already owns that item, and like I’ve said before, ProcessPurchase does NOT get called again.

Also, if this player tries to erase the game’s data and cache (to completely erase the game’s save) and opens the game again, the ProcessPurchase callback will be called again for all of the previously broken items. This does not happen when the purchase is properly consumed.

The game does not crash and no errors happen.

If anyone from the Unity team wishes to join the Test versions for Android just let me know.

I will paste here all 3 scripts involved in this process

IAPStoreHandler

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using Firebase.Functions;
using Firebase.Extensions;
using System.Threading.Tasks;

namespace com.dod.spacezoo.iap
{
    public class IAPStoreHandler : IStoreListener
    {
        public static bool IsInitialized { get; private set; }
        private static ProductCatalog Catalog { get; set; }

        public IStoreController Controller { get; private set; }
        private IExtensionProvider Extension { get; set; }

        private IAPStoreButton PurchasingButton { get; set; }
        private PurchaseEventArgs PurchasingEvent { get; set; }

        private bool IsPurchasing { get; set; }

        public static List<Product> UnhandledProducts;

        private static IAPStoreHandler instance;
        public static IAPStoreHandler Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new IAPStoreHandler();
                }

                return instance;
            }
        }

        public Product GetProduct(string productID)
        {
            if (Controller != null && Controller.products != null && !string.IsNullOrEmpty(productID))
            {
                return Controller.products.WithID(productID);
            }
            Debug.LogError("[IAPStoreHandler] attempted to get unknown product " + productID);
            return null;
        }

        #region INITIALIZATION
        [RuntimeInitializeOnLoadMethod]
        static void Initialize()
        {
            instance = new IAPStoreHandler();

            IsInitialized = false;

            Catalog = ProductCatalog.LoadDefaultCatalog();
            if (!Catalog.IsEmpty())
            {
                Debug.Log($"[IAPStoreHandler] Initializing UnityPurchasing via IAP Store Handler IAP. Version {StandardPurchasingModule.Instance().Version}");
                StandardPurchasingModule module = StandardPurchasingModule.Instance();
                module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;

                ConfigurationBuilder builder = ConfigurationBuilder.Instance(module);

                PopulateConfigurationBuilder(ref builder, Catalog);

                UnityPurchasing.Initialize(Instance, builder);
            }
            else
            {
                Debug.LogError("[IAPStoreHandler] Catalog is empty");
            }
        }

        /// <summary>
        /// Populate a ConfigurationBuilder with products from a ProductCatalog
        /// </summary>
        /// <param name="builder">Will be populated with product identifiers and payouts</param>
        /// <param name="catalog">Source of product identifiers and payouts</param>
        public static void PopulateConfigurationBuilder(ref ConfigurationBuilder builder, ProductCatalog catalog)
        {
            foreach (var product in catalog.allValidProducts)
            {
                if (product.allStoreIDs.Count > 0)
                {
                    var ids = new IDs();
                    foreach (var storeID in product.allStoreIDs)
                    {
                        ids.Add(storeID.id, storeID.store);
                    }
                    builder.AddProduct(product.id, product.type, ids);
                }
                else
                {
                    builder.AddProduct(product.id, product.type);
                }
            }
        }

        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            Controller = controller;
            Extension = extensions;

            IsInitialized = true;

            foreach (IAPStoreButton button in IAPStoreButton.All)
            {
                button.UpdateText();
            }
        }

        public void OnInitializeFailed(InitializationFailureReason error)
        {
            Debug.LogError(string.Format("[IAPStoreHandler] Purchasing failed to initialize. Reason: {0}", error.ToString()));
        }
        #endregion INITIALIZATION

        #region PURCHASING
        public void StartPurchase(string productID, IAPStoreButton iapStoreButton)
        {
            if (Controller == null)
            {
                Debug.LogError("[IAPStoreHandler] Purchase failed because Purchasing was not initialized correctly");

                iapStoreButton.OnPurchaseFailed(null, PurchaseFailureReason.PurchasingUnavailable);

                return;
            }

            if (IsPurchasing)
            {
                Debug.LogWarning("[IAPStoreHandler] Trying to purchase a second product while the first one is still going on. Please wait the first one to finish.");
                return;
            }

            IsPurchasing = true;
            PurchasingButton = iapStoreButton;
            Controller.InitiatePurchase(productID);
        }

        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
        {
            PurchasingEvent = purchaseEvent;

            Debug.Log("[IAPStoreHandler] Processing: " + purchaseEvent.purchasedProduct.definition.id);

            ValidateReceiptFirebase(purchaseEvent);

            return (PurchasingButton != null && PurchasingButton.ConsumePurchase) ? PurchaseProcessingResult.Complete : PurchaseProcessingResult.Pending;
        }

        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            PurchasingButton.OnPurchaseFailed(product, failureReason);
            PurchasingButton = null;
            IsPurchasing = false;
        }

        #region SERVER VALIDATION
        private void ValidateReceiptFirebase(PurchaseEventArgs purchaseEvent)
        {
            UnifiedReceipt receipt = JsonUtility.FromJson<UnifiedReceipt>(purchaseEvent.purchasedProduct.receipt);
            Debug.LogFormat("[IAPStoreHandler] ValidateReceiptFirebase with receipt: Store: {0} TransactionID: {1} Payload: {2}", receipt.Store, receipt.TransactionID, receipt.Payload);

            var functions = FirebaseFunctions.DefaultInstance;
            var function = functions.GetHttpsCallable("validateReceipt_v2");

            if (Application.platform == RuntimePlatform.IPhonePlayer)//iOS
            {
                string iOSdata = receipt.Payload;

                //Debug.Log($"[IAPButton] Calling validateReceipt cloud function with iOS payload data: {iOSdata}");
                function.CallAsync(iOSdata).ContinueWithOnMainThread(ValidationReturn);
            }
            else if (Application.platform == RuntimePlatform.Android)//android
            {
                GooglePayload payload = JsonUtility.FromJson<GooglePayload>(receipt.Payload);
                //Debug.LogFormat($"[IAPButton] Calling validateReceipt cloud function with GooglePayload: {payload}");

                var androidData = new Dictionary<string, object>();
                androidData["data"] = payload.json;
                androidData["signature"] = payload.signature;

                function.CallAsync(androidData).ContinueWithOnMainThread(ValidationReturn);
            }
            else//unity editor
            {
                /*Test payload
            * { signature: 'ZOK7heRfr9SKVkXu6rKQL8YR2ovEvZyLFRy0iytmQz31RxEYYlAJEu1VMD27It+BDWQ+bFAB0qY91uRnf2EwKg3ysHpS0JnOxOL2O1QpvcvP0jxb3ECinQfWTtzxPD8wNMgnqyNV4VBvjmnLMd+qljlrOKVrweUEeIxmRLxiy072AZYuu+iCygyvIjOFzA0+d0PNPvTiwPGoNWmTmpshS08V6Xl8u8hJgg9QnhPOvcX1anYqEBt8v0b9hN2hO+PbRAh4SosYkRbZF898E2TG494DS8YDyrpL9H6NJwV5d8CrVl0Axo4J0pmoFjY1PmaSLm4tFlaSEEsD4ynaF/ntOg==',
data: '{"orderId":"GPA.3346-3485-9108-62537","packageName":"com.overtimestudios.idlespacezoo","productId":"com.overtimestudios.idlespacezoo.ticketspack1","purchaseTime":1601492100062,"purchaseState":0,"developerPayload":"{\\"developerPayload\\":\\"\\",\\"is_free_trial\\":false,\\"has_introductory_price_trial\\":false,\\"is_updated\\":false,\\"accountId\\":\\"\\"}","purchaseToken":"hfghakcolemhkphonoeaadjf.AO-J1OwcKabX1lrbN0Snjs4TY4GWSC0hbbHG2ItDXGog-stkJht7Se739fix7u-o-Zi2SMILSYIdl-LmwRYeiJb2Tjz9oS6KyS7SuhG5fQEJf9PA4fm66bFLo96gKi84HE4aJsse2ht9YkhtcxfWq_cJ_SO-xuzQPVWtNx777vaKF5L1l2QAuJw"}' }
            * */

                const string testGoogleJSON = "{\"orderId\":\"GPA.3328-9127-0883-68116\",\"packageName\":\"com.dod.starzoo\",\"productId\":\"com.dod.starzoo.ticketspack1\",\"purchaseTime\":1602285429516,\"purchaseState\":0,\"developerPayload\":\"{\\\"developerPayload\\\":\\\"\\\",\\\"is_free_trial\\\":false,\\\"has_introductory_price_trial\\\":false,\\\"is_updated\\\":false,\\\"accountId\\\":\\\"\\\"}\",\"purchaseToken\":\"kbpglgnljkgdaaaomafdaiki.AO-J1OyBCISEWNE_7aFv8bNgsEJ9-ReEHqwrQ4ywZ91kzin_cmG9R5YNrwF6FLTML6peFIsAnxCPrOSM3dSmzgx-Sjw-nFDhgPjs1gyr7LFxwjFDs-Vl9Uqz_Jr2xSPPJaJFhY_C3I6E\"}";
                const string testSignature = "hEb4hf0JaJv9XNc7eKbxbY8KFT5OgW896rbjZE7x/3FHYjXk/Fevf2gXMd621qQYFSv5gNSPecFvFTmb/BdWcIROBXqI85cAyO/nA8Jge58SXIuIbaZA/C7jiatVyEfXt8uez5BYSozXTKztXZAGZJtoG2Qji5yM3gv60OHwWQ1g7qMSibyg0lKr+t1fWdxF5uaVuEHojEFLz/7KG3Gwhjcb7cmv772Mh5NtQ2TrCkZ5ECv5B+q7N2gy5I+PLw/DkP35vHCqqI0kbWqs6u5YWcIz1fzr/UAalZgOr6VJzjF4AwcMGcyDYNy1C5cALSkqdXcwbN3pt1K2e3fZdQqbPA==]";
                const string testAppleEncodedString = "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";

                bool testWithAndroidPayload = true;
                bool causeErrorInValidation = false;

                if (testWithAndroidPayload)
                {
                    var editorAndroidData = new Dictionary<string, object>();
                    editorAndroidData["data"] = testGoogleJSON;
                    editorAndroidData["signature"] = ((causeErrorInValidation) ? "aaa" : string.Empty) + testSignature;

                    Debug.LogFormat("[IAPStoreHandler] Building data for Unity Editor Validation: data: {0}, signature: {1}", testGoogleJSON, testSignature);

                    function.CallAsync(editorAndroidData).ContinueWithOnMainThread(ValidationReturn);
                }
                else
                {
                    var editoriOSData = testAppleEncodedString + (causeErrorInValidation ? "aaaa" : string.Empty);

                    Debug.LogFormat("[IAPStoreHandler] Building data for Unity Editor Validation with iOS Payload: {0}", editoriOSData);

                    function.CallAsync(editoriOSData).ContinueWithOnMainThread(ValidationReturn);
                }
            }
        }

        public void ValidationReturn(Task<HttpsCallableResult> task)
        {
            if (task.IsFaulted)
            {
                foreach (var inner in task.Exception.InnerExceptions)
                {
                    if (inner is FunctionsException)
                    {
                        var exception = (FunctionsException)inner;
                        // Function error code, will be INTERNAL if the failure
                        // was not handled properly in the function call.
                        var code = exception.ErrorCode;
                        var message = exception.Message;
                        Debug.LogErrorFormat($"[IAPStoreHandler] ValidationReturn() Exception: error code: {code} message: {message}");
                        Firebase.Crashlytics.Crashlytics.LogException(exception);
                        ReceiptValidationFinished(false, null, message);
                    }
                }
            }
            else
            {
                try{
                    /*
                    {
                    "isValid":true,
                    "info":[
                        {"orderId":"GPA.3328-9127-0883-68116",
                        "packageName":"com.dod.starzoo",
                        "productId":"com.dod.starzoo.ticketspack1",
                        "purchaseTime":1602285429516,
                        "purchaseState":0,
                        "developerPayload":
                        "{\"developerPayload\":\"\",\"is_free_trial\":false,\"has_introductory_price_trial\":false,\"is_updated\":false,\"accountId\":\"\"}",
                        "purchaseToken":"kbpglgnljkgdaaaomafdaiki.AO-J1OyBCISEWNE_7aFv8bNgsEJ9-ReEHqwrQ4ywZ91kzin_cmG9R5YNrwF6FLTML6peFIsAnxCPrOSM3dSmzgx-Sjw-nFDhgPjs1gyr7LFxwjFDs-Vl9Uqz_Jr2xSPPJaJFhY_C3I6E",
                        "status":0,
                        "service":"google",
                        "transactionId":"kbpglgnljkgdaaaomafdaiki.AO-J1OyBCISEWNE_7aFv8bNgsEJ9-ReEHqwrQ4ywZ91kzin_cmG9R5YNrwF6FLTML6peFIsAnxCPrOSM3dSmzgx-Sjw-nFDhgPjs1gyr7LFxwjFDs-Vl9Uqz_Jr2xSPPJaJFhY_C3I6E",
                        "purchaseDate":1602285429516,
                        "quantity":1}]} */
                Debug.Log("[IAPStoreHandler] Result JSON string: " + (string)task.Result.Data);
                ValidationResult validationResult = JsonUtility.FromJson<ValidationResult>((string)task.Result.Data);
                //TODO Find better solution, don't pass PurchasingData[] as parameter
                ReceiptValidationFinished(validationResult.isValid, validationResult.info, validationResult.info.ToString());
                }
                catch(Exception e){
                    Debug.LogError(e);
                }
            }
        }

        private void ReceiptValidationFinished(bool success, PurchasingData[] info, string message)
        {
            try{
                Debug.LogFormat("[IAPStoreHandler] ReceiptValidationFinished: success: {0}, message: {1}", success, message);

            if (PurchasingButton != null)
            {
                if (success)
                {
                    Debug.Log($"[IAPStoreHandler] Calling OnPurchaseCompleted on {PurchasingButton.gameObject.name}");
                    PurchasingButton.OnPurchaseCompleted(PurchasingEvent.purchasedProduct);
                }
                else
                {
                    Debug.Log($"[IAPStoreHandler] Calling OnPurchaseFailed on {PurchasingButton.gameObject.name}");
                    PurchasingButton.OnPurchaseFailed(PurchasingEvent.purchasedProduct, PurchaseFailureReason.SignatureInvalid);

                    Firebase.Analytics.FirebaseAnalytics.SetUserProperty("cheater", "true");
                }
                PurchasingEvent = null;
                PurchasingButton = null;
                IsPurchasing = false;
            }
            else
            {
                if(success){
                    if(UnhandledProducts == null)
                        UnhandledProducts = new List<Product>();
                    Product validatedProduct = Controller.products.WithID(info[0].productId);
                    Debug.Log("[IAPStoreHandler] Added Unhandled Product: " + validatedProduct.definition.id);
                    UnhandledProducts.Add(validatedProduct);
                }
                else{
                    Debug.Log("[IAPStoreHandler] Cheater");
                }
            }
            }
            catch(Exception e){
                Debug.LogError(e);
            }
           

        }
        #endregion SERVER VALIDATION

        #endregion PURCHASING
    }

    public class GooglePayload
    {
        public string json; //A JSON encoded string provided by Google; INAPP_PURCHASE_DATA
        public string signature; //A signature for the json parameter, as provided by Google; INAPP_DATA_SIGNATURE

        public override string ToString()
        {
            return string.Format("[GooglePayload: json={0}, signature={1}]", json, signature);
        }
    }

    [Serializable]
    public class ValidationResult
    {
        public bool isValid;
        public PurchasingData[] info;
    }

    [Serializable]
    public class PurchasingData{
        public string productId;
        public string orderId;
        public string packageName;
    }
   
}

IAPListenerHandler

IAPStoreButton

You mention “however, for some reason, if the player had “broken” a purchase” can you be more specific? We would not have any information regarding earlier versions of your game. Keep in mind that we are now using purchaseToken instead of orderID as the transaction identifier, search for purchaseToken here Unity IAP package 4.12.2 is now available page-2 and this thread Unity IAP doesn't consume consumable acknowledged purchases

Hi Jeff.

With “broken a purchase” I meant that the player went through steps 1-3.

  1. Try to Purchase product.

  2. The app store interface will open for the user to insert his payment info.

  3. After entering your info and before the app store interface closes itself (while it’s processing), close the app.

In earlier versions of the game, the only differences are not having the “IAPListenerHandler” script and the IAPStoreHandler didn’t have a “UnhandledProducts” variable or any of the lines meant to add products to UnhandledProducts. Everything else was the same.

If the player went through steps 1-3 in an earlier version of the game, at that time he would simply never get the reward and would never be able to purchase the same product again (because of the “you already own this item” error). Now we were trying to prevent this, but for some reason ConfirmPendingPurchase only clears the duplicate if the user went through steps 1-3 in the current version.

Do you believe that updating the plugin can help to fix the issue then?

You must have a listener, I would not know what version your app was on previously and could not specify any differences. Please reproduce with the Sample IAP Project v2 (and don’t use Codeless, you mentioned a listener) https://discussions.unity.com/t/700293/3

We have figured out what the issue was and we have fixed it. I didn’t realize at the time, but way earlier in the project we were using an older version of the Unity IAP plugin, version 2.2.2, before updating to 3.1.0 some weeks ago, then when we took our time to read the changelogs and we saw this being posted a couple of days ago:

7385867--901463--upload_2021-8-3_14-16-53.png

That was our exact issue.
We have simply updated the plugin to 3.2.3 and the issue was fixed.

By the way, the “IAPListenerHandler” is an old name, it does not actually participate in the IAP transactions while they are happening, it only checks to see if there are products in our UnhandledProducts list.

Thank you

1 Like