Help Needed: OnPurchaseFailed, PurchaseFailureReason: ProductUnavailable

Hey folks

We have been using Unity IAP for a while and came across these cases where the purchase have return ProductUnavailable result.

From our logs we can confirm that these cases happens with user that has Initialized the IStoreExtension successfully. The product ID is a perfect match with our google developer console iap products.
Our setup is pretty much the same as the IAP example with builder initialization on the Start function of our Purchaser class.


Upon further investigation we found that the User even successfully fetch the product to send it along with our other analytics event, before starting the transaction.
Right now we’re still not sure what would be the cause of the issue, Any help pointing to the right direction would be greatly appreciated.

@CreativityHub Please share your full purchase code as an attachment (not in screenshots). Are you able to reproduce? Are you testing in Closed Testing on Google? How do you know the user “fetched the product”, please provide the device logs that demonstrates. Your Debug.Log statements will show in the logs, please place them in all the purchase callbacks as in the sample IAP project How To - Capturing Device Logs on Android and Sample IAP Project

Here’s our purchaser code:

using System;
using System.Collections.Generic;
using System.Globalization;
using com.adjust.sdk;
using Firebase.Extensions;
using Firebase.Firestore;
using MonsterWar.Script.AdjustIntegration;
using MonsterWar.Script.AnalyticsSystem;
using MonsterWar.Script.Utilities;
using UnityEngine;
using UnityEngine.Purchasing;
using Product = UnityEngine.Purchasing.Product;


namespace MonsterWar.Script.GameCloud.Utils
{
    public class Purchaser : MonoBehaviour, IStoreListener
    {
        public static Purchaser Instance => _purchaser;
        private static Purchaser _purchaser;

        public string[] ProductIdArray => productIdArray;

        [SerializeField] private string[] productIdArray =
        {
            "diamonds001",
            "diamonds002",
            "diamonds003",
            "diamonds004",
            "diamonds005",
            "diamonds006",
            "starterpack_a",
            "starterpack_b",
            "starterpack_c",
            "01_frozenchampion",
            "02_starter1_small",
            "03_starter2_medium",
            "04_welcomepack_medium",
            "05_welcomepack_large",
            "06_dailyrecruitpack",
            "07_dailysupportpack",
            "08_dailyupgradepack",
            "09_dailyfriendshippack",
            "10_summonticketbundlespack",
            "11_essentialspack",
            "12_powerpack",
            "13_familypack",
            "14_grandarmypack",
            "15_highrollerpack",
            "16_championspack",
            "17_friendsforeverpack",
            "18_bunnywarriorpack",
            "19_caninefriends",
            "20_firestarterpack",
            "21_waterstarterpack",
            "22_grassstarterpack",
            "23_neutralstarterpack",
            "24_lightstarterpack",
            "25_darkstarterpack",
            "bp_chapter02",
            "bp_chapter03",
            "bp_chapter04",
            "mc_standard",
            "mc_premium",
            "bp_chapter05",
            "bp_chapter06",
            "bp_chapter07"
        };

        private Action<Product> _callback;

        public static Purchaser Initialize()
        {
            if (Instance != null)
                return _purchaser;

            var purchaserObject = new GameObject("Purchasing");
            DontDestroyOnLoad(purchaserObject);
            _purchaser = purchaserObject.AddComponent<Purchaser>();

            return _purchaser;
        }

        private static IStoreController _storeController; // The Unity Purchasing system.
        private static IExtensionProvider _storeExtensionProvider; // The store-specific Purchasing subsystems.
        private Product _currentProduct;


        private void Start()
        {
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
            foreach (var productId in productIdArray)
                builder.AddProduct(productId, ProductType.Consumable);
            UnityPurchasing.Initialize(this, builder);
        }

        private void InitializePurchasing()
        {
            LogPurchasableProducts();
        }

        private static void LogPurchasableProducts()
        {
            if (_storeController == null)
                return;

            if (_storeController.products.all.Length <= 0)
                return;

            foreach (var item in _storeController.products.all)
                if (item.availableToPurchase)
                    Debug.LogError(string.Join(" - ", item.metadata.localizedTitle, item.metadata.localizedDescription,
                        item.metadata.isoCurrencyCode,
                        item.metadata.localizedPrice.ToString(CultureInfo.InvariantCulture),
                        item.metadata.localizedPriceString, item.transactionID, item.receipt));
        }



        #region UNITY IAP EVENTS

        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
#if !PROD_BUILD
            Debug.Log("OnInitialized: PASS");
#endif
            _storeController = controller;
            _storeExtensionProvider = extensions;

            _purchaser.InitializePurchasing();
        }

        public void OnInitializeFailed(InitializationFailureReason error)
        {
            CrashlyticExceptionHandler.LogPurchaserInitializationFailed(error);
#if !PROD_BUILD
            Debug.LogError("OnInitializeFailed InitializationFailureReason:" + error);
#endif
        }


        // UNITY IAP PROCESS PURCHASE EVENT
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
        {
#if !PROD_BUILD
            Debug.Log($"Purchase : {args.purchasedProduct.definition.id}");
#endif
            if (args.purchasedProduct.hasReceipt)
            {

#if !PROD_BUILD
                Debug.Log($"TransactionID : {args.purchasedProduct.transactionID}");
                Debug.Log($"Receipt : {args.purchasedProduct.receipt}");
#endif

                _callback?.Invoke(args.purchasedProduct);
                WriteReceiptToFirestore(args);
#if !PROD_BUILD
                Debug.LogError(string.Format("OnPurchaseComplete: " + args.purchasedProduct.metadata.localizedTitle +
                                             " : " + args.purchasedProduct.metadata.localizedPriceString));
#endif
                LogAdjustPurchasing(args);

                return PurchaseProcessingResult.Complete;
            }

            return PurchaseProcessingResult.Complete;
        }

        // UNITY IAP PURCHASE FAILED EVENT
        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            CrashlyticExceptionHandler.LogPurchasingFailed(
                $"OnPurchaseFailed: FAIL. Product: '{product.definition.storeSpecificId}', PurchaseFailureReason: {failureReason}");
            Debug.LogError(
                $"OnPurchaseFailed: FAIL. Product: '{product.definition.storeSpecificId}', PurchaseFailureReason: {failureReason}");
        }


        // Restore purchases previously made by this customer. Some platforms automatically restore purchases, like Google.
        // Apple currently requires explicit purchase restoration for IAP, conditionally displaying a password prompt.
        public void RestorePurchases()
        {
            // If Purchasing has not yet been set up ...
            if (_storeController == null && _storeExtensionProvider == null)
            {
                // ... report the situation and stop restoring. Consider either waiting longer, or retrying initialization.
                Debug.Log("RestorePurchases FAIL. Not initialized.");
                return;
            }

            if (UnityEngine.Application.platform == RuntimePlatform.IPhonePlayer ||
                UnityEngine.Application.platform == RuntimePlatform.OSXPlayer)
            {
                Debug.Log("RestorePurchases started ...");

                var apple = _storeExtensionProvider.GetExtension<IAppleExtensions>();
                apple.RestoreTransactions((result) =>
                {
                    Debug.Log("RestorePurchases continuing: " + result +
                              ". If no further messages, no purchases available to restore.");
                });
            }
            else
            {
                Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " +
                          UnityEngine.Application.platform);
            }
        }

        #endregion


        private static void LogAdjustPurchasing(PurchaseEventArgs args)
        {
            var productPrice = args.purchasedProduct.metadata.localizedPrice;
            var productCurrency = args.purchasedProduct.metadata.localizedPriceString;

            var adjustEventKey = AdjustEventHandler.GetIapEventToken(args.purchasedProduct.definition.id);
            var adjustEvent = new AdjustEvent(adjustEventKey);
            adjustEvent.setRevenue((double) productPrice, productCurrency);
            adjustEvent.setTransactionId(args.purchasedProduct.transactionID);
            Adjust.trackEvent(adjustEvent);
            Debug.LogError("Adjust Verification Passed : " + args.purchasedProduct.transactionID + " : " +
                           productPrice + " : " + productCurrency);
        }





        private static void WriteReceiptToFirestore(PurchaseEventArgs args)
        {
            var purchaseRecord = new Dictionary<string, object>
            {
                {"UserID", FirebaseManager.firebase.FirebaseUser.UserId},
                {"TransactionID", args.purchasedProduct.transactionID},
                {"ProductID", args.purchasedProduct.definition.id},
                {"Receipt", args.purchasedProduct.receipt}
            };

            var documentReference = FirebaseFirestore.DefaultInstance
                .Collection(Globals.FirestoreReceiptKey)
                .Document(FirebaseManager.firebase.FirebaseUser.UserId)
                .Collection(Globals.FirestoreReceiptKey).Document(args.purchasedProduct.transactionID);

            documentReference.SetAsync(purchaseRecord, SetOptions.MergeAll).ContinueWithOnMainThread(task =>
            {
#if !PROD_BUILD
                Debug.LogError("Save Receipt to Firestore : " +
                               FirebaseManager.firebase.FirebaseUser.UserId + " : " +
                               args.purchasedProduct.transactionID + " : " +
                               args.purchasedProduct.definition.id);
#endif
            });

        }

        public Product GetProductFromId(string id)
        {
            return _storeController.products.WithID(id);
        }

        public bool StartIapTransaction(string productID, Action<Product> callback = null)
        {
            _callback = null;
            _callback = callback;
            return InitiateIapTransaction(productID);
        }

        private bool InitiateIapTransaction(string productId)
        {
            if (_storeController != null && _storeExtensionProvider != null)
            {

                var product = _storeController.products.WithID(productId);
                if (product == null)
                    CrashlyticExceptionHandler.LogPurchasingFailed("BuyProductID: " + productId +
                                                                   " FAIL. Product not found");

                if (product != null && product.availableToPurchase)
                {
                    Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
                    _currentProduct = product;
                    _storeController.InitiatePurchase(productId);
                    return true;
                }

                CrashlyticExceptionHandler.LogPurchasingFailed(
                    "BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
                Debug.Log(
                    "BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
            }
            else
            {
                CrashlyticExceptionHandler.LogPurchasingFailed("BuyProductID FAIL. Not initialized.");
                Debug.Log("BuyProductID FAIL. Not initialized.");
            }

            return false;
        }


    }
}

We’re in Open Testing in Google Development console.

We know the product has been fetched successfully before because we have an analytic log events that contain the detail of the product which is a perfect match with the one in the Google Development console.

using System;
using System.Collections.Generic;
using GameReward;
using MonsterWar.Script.AnalyticsSystem;
using MonsterWar.Script.Data;
using MonsterWar.Script.GameCloud.Utils;
using MonsterWar.Script.UI;
using MonsterWar.Script.UI.Inventory;
using MonsterWar.Script.UI.Popups;
using MonsterWar.Script.Utilities;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Serialization;
using User;

namespace MonsterWar.Script.ShopSystem
{
    public class PackBannerEntryController : MonoBehaviour
    {

        [FormerlySerializedAs("itemEntryList")]
        [Header("Visual References")]
        [SerializeField] private List<GameObject> entryCenterTransform = new List<GameObject>();

        [SerializeField] private UILabel headerLabel;

        [SerializeField] private UIButton purchaseButton;
        [SerializeField] private UIButton iapPurchaseButton;
        [SerializeField] private UIButton infoButton;
        [SerializeField] private PackInfoDisplayController packInfoDisplayController;
        [SerializeField] private GameObject lockGameObject;

        [Header("In-game Price Display")]
        [SerializeField] private GameObject inGamePriceObject;
        [SerializeField] private UILabel priceLabel;
        [SerializeField] private UITexture priceCurrencyTexture;

        [Header("Google Transaction Price Display")]
        [SerializeField] private GameObject googleTransactionObject;
        [SerializeField] private UILabel googlePriceLabel;
        [Header("Pack Buy Amount Limit")]
        [SerializeField] private UILabel buyAmountLimitLabel;
        [SerializeField] private UILabel packRefreshTimeLabel;
        [SerializeField] private bool lockedScrollDrag;
        private RewardData _inGamePrice;
        private string _googlePlayProductId;
        private bool _isInGameTransaction;
        private List<ItemData> _bannerItemDataList;
        private string[] _payoutAmountArray;

        private List<RewardPackage> _rewardPackageList = new List<RewardPackage>();
        private PackData _packData;


        [Header("Data References")]
        [SerializeField]
        private GameObject itemEntryPrefab;
        [SerializeField] private GameObject unitEntryPrefab;
        private List<UnitData> _unitRewardDataList = new List<UnitData>();
        private bool _isBackgroundOnlyBanner;
        private Product _product;
        private LockObjectFeedbackController _lockObjectFeedbackController;
        private float _maxTimeout = 1;
        private bool _isUpdatingPackRefreshTime;
        private float _currentTimeout;

        private void Start()
        {
            if (!lockedScrollDrag)
            {
                if (purchaseButton.GetComponent<UIDragScrollView>() == null)
                    purchaseButton.gameObject.AddComponent<UIDragScrollView>();
                if (infoButton.GetComponent<UIDragScrollView>() == null)
                    infoButton.gameObject.AddComponent<UIDragScrollView>();
                if (iapPurchaseButton.GetComponent<UIDragScrollView>() == null)
                    iapPurchaseButton.gameObject.AddComponent<UIDragScrollView>();
            }
        }

        public void Initialize(PackData packData, List<ItemData> itemDataList, string[] payoutAmountArray, List<UnitData> unitRewardDataList)
        {
            if (entryCenterTransform.Count <= 0) _isBackgroundOnlyBanner = true;

            _rewardPackageList.Clear();
            _packData = packData;
            _bannerItemDataList = itemDataList;
            _payoutAmountArray = payoutAmountArray;
            _isInGameTransaction = packData.IsInGameTransaction;
            _unitRewardDataList = unitRewardDataList;

            PopulateBuyAmountLimitLabel();
            StartUpdatingPackRefreshTime();

            if (_packData.IsLocked)
            {
                if (lockGameObject != null)
                {
                    googlePriceLabel.gameObject.SetActive(false);
                    lockGameObject.SetActive(true);
                    iapPurchaseButton.defaultColor = Color.grey;
                    _lockObjectFeedbackController = gameObject.AddComponent<LockObjectFeedbackController>();

                    var childObject = lockGameObject.GetComponentInChildren<UITexture>().gameObject;
                    _lockObjectFeedbackController.SetLockObject(childObject);
                    _lockObjectFeedbackController.Override(iapPurchaseButton, null);
                }
                else
                    Debug.LogError("Lock Object is missing");
                return;
            }


            if (_isInGameTransaction)
                _inGamePrice = new RewardData(packData.InGamePrice);
            else
            {
                _googlePlayProductId = packData.GoogleProductId;
                _product = Purchaser.Instance.GetProductFromId(_googlePlayProductId);
            }

            if (headerLabel != null) headerLabel.text = packData.HeaderText;

            SetupButtonEvents();
            SetupPriceDisplay();


            PopulateItemRewardPack(_bannerItemDataList);
            if (!_isBackgroundOnlyBanner)
                PopulateDisplayEntry(_bannerItemDataList, unitRewardDataList);

        }

        private void PopulateBuyAmountLimitLabel()
        {
            if (buyAmountLimitLabel == null) return;

            if (_packData.BuyAmountLimit > 0)
            {
                if (_packData.BuyAmountLimit > 1)
                    buyAmountLimitLabel.text = Globals.OnlyString + " " + _packData.BuyAmountLimit.ToString() + " " + Globals.TimesString.ToLower() + " " + Globals.PerAccountString.ToLower();
                else
                    buyAmountLimitLabel.text = Globals.OnlyString + " " + _packData.BuyAmountLimit.ToString() + " " + Globals.TimeString.ToLower() + " " + Globals.PerAccountString.ToLower();
            }
            else
                buyAmountLimitLabel.text = string.Empty;
        }

        private void PopulateItemRewardPack(List<ItemData> itemDataList)
        {
            for (var i = 0; i < itemDataList.Count; i++)
            {
                var rewardPackage = GenerateRewardPackageFromItemData(itemDataList, i);
                _rewardPackageList.Add(rewardPackage);
            }
        }

        private void PopulateDisplayEntry(List<ItemData> itemDataList, List<UnitData> unitRewardDataList)
        {
            var index = 0;
            foreach (var unitData in unitRewardDataList)
            {
                var parentObject = entryCenterTransform[index];
                if (parentObject == null) return;
                var unitEntryObject = Instantiate(unitEntryPrefab, parentObject.transform);
                var unitEntryInstance = unitEntryObject.GetComponent<UnitEntry>();
                unitEntryInstance.Initialize(
                    new UserData.TroopManage.UserTroopData(null, new TroopData
                    {
                        Identity = "",
                        UnitID = unitData.UnitID,
                        Happiness = 120,
                        Upgrade = 1,
                        Status = 0,
                        UnixTimeStamp = 0,
                        Rarity = (int)unitData.Rarity,
                    }));
                unitEntryInstance.EnableHappinessIndicator(false);
                index++;
            }


            foreach (var rewardPackage in _rewardPackageList)
            {
                var parentObject = entryCenterTransform[index];
                if (parentObject == null) return;
                var itemEntryObject = Instantiate(itemEntryPrefab, parentObject.transform);
                var itemEntryInstance = itemEntryObject.GetComponent<ItemEntry>();
                var data = new RewardData(rewardPackage);
                itemEntryInstance.InitializeRewardEntry(data, true);

                index++;
            }
        }

        private RewardPackage GenerateRewardPackageFromItemData(List<ItemData> itemDataList, int i)
        {
            RewardPackage rewardPackage;
            if (itemDataList[i].ItemType == ItemType.Currency)
            {
                if (itemDataList[i].ItemID == CurrencyType.Gold.ToString())
                    rewardPackage = new RewardPackage
                    {
                        Id = CurrencyType.Gold.ToString(),
                        Amount = int.Parse(_payoutAmountArray[i]),
                        Type = Globals.Currency
                    };
                else
                    rewardPackage = new RewardPackage
                    {
                        Id = CurrencyType.Diamond.ToString(),
                        Amount = int.Parse(_payoutAmountArray[i]),
                        Type = Globals.Currency
                    };
            }
            else
                rewardPackage = new RewardPackage
                {
                    Id = itemDataList[i].ItemID,
                    Amount = int.Parse(_payoutAmountArray[i]),
                    Type = Globals.Item
                };

            return rewardPackage;
        }

        private void SetupPriceDisplay()
        {
            if (_isInGameTransaction)
            {
                googleTransactionObject.SetActive(false);
                priceLabel.text = _inGamePrice.Amount.ToString();
                if (_inGamePrice.Currency == CurrencyType.Gold)
                    priceCurrencyTexture.mainTexture = Store.Instance.img.currency.Gold;
                else
                    priceCurrencyTexture.mainTexture = Store.Instance.img.currency.Diamond;

                inGamePriceObject.SetActive(true);
            }
            else
            {
                inGamePriceObject.SetActive(false);
                googlePriceLabel.text = _product.metadata.localizedPriceString;
                googleTransactionObject.SetActive(true);
            }
        }

        private bool IsPackBuyAmountLimitReached()
        {
            if (_packData.BuyAmountLimit > 0)
            {
                if (UserData.userData.profile.PackBoughtDict.ContainsKey(_packData.PackId))
                {
                    if (UserData.userData.profile.PackBoughtDict[_packData.PackId] >= _packData.BuyAmountLimit)
                        return true;
                    else
                        return false;
                }
                else
                    return false;
            }
            else
                return false;
        }

        private void SetupButtonEvents()
        {
            purchaseButton.onClick.Clear();
            purchaseButton.onClick.Add(new EventDelegate(ProcessTransaction));

            infoButton.onClick.Clear();
            infoButton.onClick.Add(new EventDelegate(ShowPackInfo));

            iapPurchaseButton.onClick.Clear();
            iapPurchaseButton.onClick.Add(new EventDelegate(ProcessTransaction));
        }

        private void ShowPackInfo()
        {
            var popup = PopupHandler.OpenPackDetailPopup(_rewardPackageList, _unitRewardDataList, _packData);

            if (ShopPage.page != null)
                ShopPage.page.EnableCurrentTooltip(popup.gameObject);
        }

        private void ProcessTransaction()
        {
            if (_isInGameTransaction)
            {
                if (UserData.userData.inventory.IsEnoughCurrency(_inGamePrice.Currency, _inGamePrice.Amount))
                {
                    UserData.userData.inventory.SpendCurrency(_inGamePrice.Currency, _inGamePrice.Amount);

                    PopupHandler.OpenCollectAllPopup(_rewardPackageList, _unitRewardDataList);
                    GivePackContentToPlayer();
                }
                else
                    Manager.Instance.ErrorHintController.TriggerErrorHint(Globals.InsufficientResource);
            }
            else
            {
                if (IsPackBuyAmountLimitReached())
                {
                    Manager.Instance.ErrorHintController.TriggerErrorHint(Globals.PackBuyAmountLimitReached);
                    return;
                }

                AnalyticEventHandler.LogInAppPurchaseViewEvent(_googlePlayProductId, UserData.userData.inventory.Gold,
                    UserData.userData.inventory.Diamond, (float)_product.metadata
                        .localizedPrice, _product.metadata.localizedPriceString);
                Purchaser.Instance.StartIapTransaction(_googlePlayProductId, (callback) =>
                {
                    PopupHandler.OpenCollectAllPopup(_rewardPackageList, _unitRewardDataList);
                    GivePackContentToPlayer();
                    UpdatePackBoughtProfile();
                    StartUpdatingPackRefreshTime();
                });

            }
        }

        private void UpdatePackBoughtProfile()
        {
            UpdatePackBoughtDict();
            UpdatePackLastBoughtUnixTimeDict();
        }

        private void UpdatePackLastBoughtUnixTimeDict()
        {
            var currentTime = TimeHelper.GetCurrentTime();

            var packLastBoughtUnixTime = UserData.userData.profile.PackLastBoughtUnixTimeDict;

            if (packLastBoughtUnixTime.ContainsKey(_packData.PackId))
                packLastBoughtUnixTime[_packData.PackId] = currentTime;
            else
                packLastBoughtUnixTime.Add(_packData.PackId, currentTime);

            UserData.userData.profile.PackLastBoughtUnixTimeDict = packLastBoughtUnixTime;
        }

        private void UpdatePackBoughtDict()
        {
            var packBoughtDict = UserData.userData.profile.PackBoughtDict;

            if (packBoughtDict.ContainsKey(_packData.PackId))
                packBoughtDict[_packData.PackId]++;
            else
                packBoughtDict.Add(_packData.PackId, 1);

            UserData.userData.profile.PackBoughtDict = packBoughtDict;
        }

        private void GivePackContentToPlayer()
        {
            foreach (var rewardPackage in _rewardPackageList)
            {
                if (string.Equals(rewardPackage.Type, Globals.Item))
                {
                    UserData.userData.inventory.AddItemById(rewardPackage.Id, rewardPackage.Amount);
                }
                else if (string.Equals(rewardPackage.Type, Globals.Currency))
                {
                    if (string.Equals(rewardPackage.Id, CurrencyType.Gold.ToString()))
                        UserData.userData.inventory.AddCurrency(CurrencyType.Gold, rewardPackage.Amount);
                    else if (string.Equals(rewardPackage.Id, CurrencyType.Diamond.ToString()))
                        UserData.userData.inventory.AddCurrency(CurrencyType.Diamond, rewardPackage.Amount);
                }
            }

            foreach (var unitData in _unitRewardDataList)
                UserData.userData.troopManage.AddTroop(unitData, TimeHelper.GetCurrentTime());
        }

        public void Refresh()
        {
            StartUpdatingPackRefreshTime();
        }

        private void StartUpdatingPackRefreshTime()
        {
            SetCurrentRefreshTimeLabel();
            _isUpdatingPackRefreshTime = true;
            _currentTimeout = _maxTimeout;
        }

        private void SetCurrentRefreshTimeLabel()
        {
            if (packRefreshTimeLabel == null) return;

            var packLastBoughtUnixTimeDict = UserData.userData.profile.PackLastBoughtUnixTimeDict;

            if (packLastBoughtUnixTimeDict.ContainsKey(_packData.PackId))
            {
                var lastBoughtUnixTime = packLastBoughtUnixTimeDict[_packData.PackId];
                var lastBoughtDateTime = TimeHelper.ConvertUnixTimeToDateTime(lastBoughtUnixTime);

                var currentUnixTime = TimeHelper.GetCurrentTime();
                var currentDateTime = TimeHelper.ConvertUnixTimeToDateTime(currentUnixTime);

                var totalSecondsPassed = (int)(currentDateTime - lastBoughtDateTime).TotalSeconds;
                var totalSecondsLeft = _packData.RefreshTimeInSeconds - totalSecondsPassed;

                if (totalSecondsPassed >= _packData.RefreshTimeInSeconds)
                {
                    SetIsUpdatingPackRefreshTime(false);
                }
                else
                {
                    var refreshTimeSpan = new TimeSpan();
                    refreshTimeSpan = TimeSpan.FromSeconds(totalSecondsLeft);
                    packRefreshTimeLabel.text = TimeHelper.GetShortTimeFormat(refreshTimeSpan);
                    SetIsUpdatingPackRefreshTime(true);
                }
            }
            else
                packRefreshTimeLabel.gameObject.SetActive(false);
        }

        private void SetIsUpdatingPackRefreshTime(bool isEnable)
        {
            _isUpdatingPackRefreshTime = isEnable;
            packRefreshTimeLabel.gameObject.SetActive(isEnable);
            purchaseButton.isEnabled = !isEnable;
            iapPurchaseButton.isEnabled = !isEnable;
        }

        private void Update()
        {
            if (_isUpdatingPackRefreshTime)
            {
                _currentTimeout -= Time.deltaTime;
                if (_currentTimeout <= 0)
                {
                    SetCurrentRefreshTimeLabel();
                    _currentTimeout = _maxTimeout;
                }
            }
        }
    }
}

The sequence are as follow:
Populating the Banner Product > Player Click On Banner to Purchase > Analytics Event ‘generate_lead’ with product data is being fired > Purchaser.StartIapTransaction is being called

Here’s our relevent analytics code that’s a match to the event posted in the start of the thread:

 public static void LogInAppPurchaseViewEvent(string iapItemId, int inventoryGold, int inventoryDiamond,
            float quotedPrice, string priceString)
        {
            LogEvent(FirebaseAnalytics.EventGenerateLead,
                new Parameter(FirebaseAnalytics.ParameterItemId, iapItemId),
                new Parameter(Globals.PlayerGold, inventoryGold),
                new Parameter(Globals.PlayerDiamond, inventoryDiamond),
                new Parameter(Globals.QuotedPrice, quotedPrice),
                new Parameter(Globals.QuotedPriceString, priceString)

            );

        }

The error is being logged by the firebase crashlytics. Right now, we’re still unable to reproduce this bug on our side

One note, you look to be calling _purchaser.InitializePurchasing() inside of OnInitialized (a purchasing callback that is triggered AFTER initialization succeeds). But I doubt that is your issue. Unfortunately I would not be able to debug your code. We will either require logs from an affected device or specific steps to reproduce. Be sure to be using IAP 4.1.2

Right now, we’re on Unity IAP 3.1.0.
I will:

  • Update the Unity IAP plugin to 4.1.2
  • Check for initialize failed if the Unity IAP OnPurchaseFailed getting called and reinitialize the IAP Builder if that’s the case
  • Check for Internet connections on our Shop

Do you think these measure would address all of the issue we’re facing on our side?
We appreciate your guidance, Jeff

No don’t reinitialize if you get OnPurchaseFailed. Only initialize once. If IAP initialization fails, you won’t be able to initiate a purchase in the first place. If a purchase fails, it’s not due to initialization failure.

Got it, will just update the version and check internet connection before transaction processing.

Will report back if there is more detail on the issue, Thanks.

You want to check internet connection before IAP initialization, first. You can then optionally check the connection prior to a purchase.