Android Purchasing OnInitializeFailed - NoProductsAvailable

Unity 2020.3.7f1
In App Purchasing 3.2.3
We have an issue with purchasing initialization. The callback OnInitializeFailed is called with the Initialization Failure Reason = NoProductsAvailable. This issue is happened not for all users and we can’t reproduce it on our test devices. But this callback is logged on our own server when it occurs in our game and we have many logs with this error. The internet is available on devices when purchasing initialization is failed because the log with the error description is sent to our server.
What can be the reason for this issue? Why products can be not available for some users?

using System;
using System.Collections.Generic;
using System.Globalization;
using Core.Analytics.Signal;
using Core.ApplicationState;
using Core.Authentication.Controller;
using Core.ServerApi;
using Core.Utils;
using Game.Models.Player;
using Game.Models.Сurrency;
using Game.Providers;
using Game.Signals;
using UniRx;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
using Zenject;
using Random = UnityEngine.Random;

namespace Game.Controllers
{
    public class PurchaseController : IPurchaseController, IStoreListener, IInitializable, IDisposable
    {
        private const int ValidateResponseCodeOk = 1;
        private const int ValidateResponseCodeServerError = 3;
        private readonly IPlayerService _playerService;
        private readonly IApplicationState _applicationState;
        private readonly IPlayer _player;
        private readonly IAuthenticationController _authenticationController;
        private readonly IServerApi _serverApi;
        public IObservable<PurchaseStatus> OnPurchaseComplete => _onPurchaseComplete;
        public IObservable<PurchaseFailureStatus> OnPurchaseFail => _onPurchaseFail;
        private readonly Subject<PurchaseStatus> _onPurchaseComplete;
        private readonly Subject<PurchaseFailureStatus> _onPurchaseFail;
        public bool IsInitialized => m_Controller != null;
        private bool _isInitProcess;
        private IDisposable _iapRepeatedTimer;
        private const int RepeatedTimeSeconds = 10;
        private readonly HashSet<string> _pendingProducts = new HashSet<string>();

        public InitializationFailureReason InitializationFailureReason { get; private set; }

        private IStoreController m_Controller;

        private IAppleExtensions m_AppleExtensions;
        //private ISamsungAppsExtensions m_SamsungExtensions;
        private IMicrosoftExtensions m_MicrosoftExtensions;
        private ITransactionHistoryExtensions m_TransactionHistoryExtensions;
        private IGooglePlayStoreExtensions m_GooglePlayStoreExtensions;

        private readonly List<ProductCatalogItem> _productItemsByType = new List<ProductCatalogItem>();
        private readonly CompositeDisposable _disposable = new CompositeDisposable();
        private readonly SignalBus _signalBus;
        ProductCatalog _catalog;
        private CrossPlatformValidator validator;
        private IDisposable timer;

        public PurchaseController(IPlayerService playerService, IApplicationState applicationState,
            IPlayer player, IAuthenticationController authenticationController, IServerApi serverApi,
            SignalBus signalBus)
        {
            _onPurchaseComplete = new Subject<PurchaseStatus>();
            _onPurchaseFail = new Subject<PurchaseFailureStatus>();

            _playerService = playerService;
            _applicationState = applicationState;
            _player = player;
            _authenticationController = authenticationController;
            _serverApi = serverApi;
            _signalBus = signalBus;
        }

        public void Initialize()
        {
            _signalBus.GetStream<SignalDataLoaded>()
                .Subscribe(_ => InitUnityPurchasing())
                .AddTo(_disposable);

            _applicationState.OnApplicationFocus
                .Subscribe(hasFocus =>
                {
                    if (hasFocus)
                    {
                        InitUnityPurchasing();
                    }   
                })
                .AddTo(_disposable);
        }

        public void Dispose()
        {
            _disposable.Dispose();
        }

        public void InitUnityPurchasing()
        {
            timer?.Dispose();

            if (!_applicationState.HasInternet && !IsInitialized)
            {
                timer = Observable.Timer(TimeSpan.FromSeconds(10)).Subscribe(_ => InitUnityPurchasing());
            }

            if (IsInitialized || _isInitProcess)
                return;

            Debug.Log("PurchaseController Initialize start");

            _isInitProcess = true;

            _catalog = ProductCatalog.LoadDefaultCatalog();
            var module = StandardPurchasingModule.Instance();
            var builder = ConfigurationBuilder.Instance(module);

            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);
                }
            }

#if !UNITY_EDITOR
            if (Application.platform == RuntimePlatform.Android ||
                Application.platform == RuntimePlatform.IPhonePlayer)
            {
                var appIdentifier = Application.identifier;
                validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), appIdentifier);
            }
#endif

            builder.Configure<IAppleConfiguration>().SetApplePromotionalPurchaseInterceptorCallback(OnPromotionalPurchase);

            UnityPurchasing.Initialize(this, builder);
        }

        private void OnPromotionalPurchase(Product item)
        {
            m_Controller.InitiatePurchase(m_Controller.products.WithID(item.definition.id));
        }

        public void Purchase(string id)
        {
#if UNITY_EDITOR
            if (Application.isEditor)
            {
                var timeIntervalRnd = Random.Range(0.3f, 3f);
                Observable.Timer(TimeSpan.FromSeconds(timeIntervalRnd))
                    .Subscribe(_ =>
                    {
                        var transactionId = Guid.NewGuid().ToString();
                        var statusData = new PurchaseStatusData()
                        {
                            ProductId = id,
                            ReceiptJson = "",
                            ProductReceipt = null,
                            TransactionId = transactionId
                        };
                        PurchaseComplete(new PurchaseStatus(statusData));
                    });
                return;
            }
#endif

            Debug.Log($"PurchaseController Purchase start id = {id}");
            m_Controller.InitiatePurchase(m_Controller.products.WithID(id));
        }

        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            Debug.Log($"PurchaseController OnInitialized success! Products count = {controller.products.all.Length}");

            _isInitProcess = false;
            m_Controller = controller;
            m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
            //m_SamsungExtensions = extensions.GetExtension<ISamsungAppsExtensions>();
            m_MicrosoftExtensions = extensions.GetExtension<IMicrosoftExtensions>();
            m_TransactionHistoryExtensions = extensions.GetExtension<ITransactionHistoryExtensions>();
            m_GooglePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();

            // On Apple platforms we need to handle deferred purchases caused by Apple's Ask to Buy feature.
            // On non-Apple platforms this will have no effect; OnDeferred will never be called.
            m_AppleExtensions.RegisterPurchaseDeferredListener(OnDeferred);
        }

        public void OnInitializeFailed(InitializationFailureReason error)
        {
            _isInitProcess = false;
            InitializationFailureReason = error;
            Debug.LogError($"PurchaseController OnInitializeFailed! Error = {error}");

            var errorCode = (int) ApplicationErrorCodes.PurchaseInitializeFailed;
            var errorRequest = CreateErrorRequestData(GetType().Name, CoreUtils.GetCaller(), errorCode, error.ToString());
            _serverApi.SendError(errorRequest, null);
        }

        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
        {
            try
            {
                var result = validator.Validate(e.purchasedProduct.receipt);
                var transactionId = string.Empty;
                foreach (IPurchaseReceipt productReceipt in result)
                {
                    GooglePlayReceipt google = productReceipt as GooglePlayReceipt;
                    if (null != google)
                    {
                        transactionId = google.purchaseToken;
                    }

                    AppleInAppPurchaseReceipt apple = productReceipt as AppleInAppPurchaseReceipt;
                    if (null != apple)
                    {
                        transactionId = apple.originalTransactionIdentifier;
                    }

                    var purchaseStatusData = new PurchaseStatusData
                    {
                        ProductId = e.purchasedProduct.definition.id,
                        ReceiptJson = e.purchasedProduct.receipt,
                        ProductReceipt = productReceipt,
                        Product = e.purchasedProduct,
                        TransactionId = transactionId
                    };

                    PurchaseComplete(new PurchaseStatus(purchaseStatusData));
                }
            }
            catch (IAPSecurityException ex)
            {
                Debug.LogError("PurchaseController ProcessPurchase Invalid receipt, not unlocking content. " + ex);

                _onPurchaseFail.OnNext(new PurchaseFailureStatus(PurchaseFailureReason.Unknown, false, true));

                var errorCode = (int) ApplicationErrorCodes.NotValidatePurchasedProductReceipt;
                var errorRequest = CreateErrorRequestData(GetType().Name, CoreUtils.GetCaller(), errorCode, ex.Message);
                _serverApi.SendError(errorRequest, null);

                return PurchaseProcessingResult.Complete;
            }


            return PurchaseProcessingResult.Pending;
        }


        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            // Detailed debugging information
            var storeSpecificErrorCode = "Store specific error code: " + m_TransactionHistoryExtensions.GetLastStoreSpecificPurchaseErrorCode();
            var purchaseFailureDescriptionMessage = string.Empty;
            if (m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription() != null)
            {
                purchaseFailureDescriptionMessage = "Purchase failure description message: " + m_TransactionHistoryExtensions.GetLastPurchaseFailureDescription().message;
            }

            _onPurchaseFail.OnNext(new PurchaseFailureStatus(failureReason, false, false));

            if (failureReason != PurchaseFailureReason.UserCancelled)
            {
                var errorCode = (int)ApplicationErrorCodes.OnPurchaseFailed;
                var errorMessage = failureReason + "; " + storeSpecificErrorCode + "; " + purchaseFailureDescriptionMessage;
                var errorRequest = CreateErrorRequestData(GetType().Name, CoreUtils.GetCaller(), errorCode, errorMessage);
                _serverApi.SendError(errorRequest, null);
            }
        }

        private void OnDeferred(Product item)
        {
            Debug.Log("PurchaseController Purchase deferred: " + item.definition.id);
        }

        private void PurchaseComplete(PurchaseStatus status)
        {
            var validateLocalDoubleTransactionId = status.Data != null
                                                   && !string.IsNullOrEmpty(status.Data.TransactionId)
                                                   && !_player.HasPurchaseTransactionId(
                                                       status.Data.TransactionId);

            if (validateLocalDoubleTransactionId)
            {
                StartServerValidate(status.Data);
            }
            else
            {
                var purchaseFailureStatus = new PurchaseFailureStatus(PurchaseFailureReason.DuplicateTransaction, false, false);

                _onPurchaseFail.OnNext(purchaseFailureStatus);

                var errorCode = (int) ApplicationErrorCodes.NotValidateLocalDoubleTransaction;
                var errorRequest = CreateErrorRequestData(GetType().Name, CoreUtils.GetCaller(), errorCode, "not validate local double transaction");
                _serverApi.SendError(errorRequest, null);
            }
        }

        private void StartServerValidate(PurchaseStatusData statusData, bool useReserveServer = false)
        {
#if UNITY_EDITOR
            if (Application.isEditor)
            {
                ApplyReward(statusData);
                return;
            }
#endif
            if (statusData.ProductReceipt is GooglePlayReceipt googlePlayReceipt)
            {
                var data = GetIapValidateGpData(googlePlayReceipt, statusData.TransactionId, statusData.ReceiptJson);
                _serverApi.IapValidateGp(data, statusData, ServerValidateResponse, useReserveServer);
                return;
            }

            if (statusData.ProductReceipt is AppleInAppPurchaseReceipt appleReceipt)
            {
                var data = GetIapValidateAppleData(appleReceipt, statusData.TransactionId, statusData.ReceiptJson);
                _serverApi.IapValidateApple(data, statusData, ServerValidateResponse, useReserveServer);
                return;
            }
        }

        private void ServerValidateResponse(IapValidateResponse response)
        {
            _iapRepeatedTimer?.Dispose();

            if (response.IsServerAvailable)
            {
                m_Controller.ConfirmPendingPurchase(response.Data.Product);

                if (_pendingProducts.Contains(response.Data.ProductId))
                    _pendingProducts.Remove(response.Data.ProductId);

                var isValidResponse = response.Response.ResponseCode == ValidateResponseCodeOk && response.IsValid;
                if (isValidResponse)
                {
                    ApplyReward(response.Data);
                    return;
                }

                if (response.Response.ResponseCode == ValidateResponseCodeServerError)
                {
                    StartValidateReserve(response);
                    return;
                }

                _onPurchaseFail.OnNext(new PurchaseFailureStatus(PurchaseFailureReason.Unknown, false, true));

                var errorCode = (int) ApplicationErrorCodes.InvalidServerTransaction;
                var errorRequest = CreateErrorRequestData(GetType().Name, CoreUtils.GetCaller(), errorCode, $"ProductId: {response.Data.ProductId}");
                _serverApi.SendError(errorRequest, null);
            }
            else
            {
                StartValidateReserve(response);
            }
        }

        private void StartValidateReserve(IapValidateResponse response)
        {
            if (!response.IsUseReserveServer)
            {
                StartServerValidate(response.Data, true);
            }
            else
            {
                if (!_pendingProducts.Contains(response.Data.ProductId))
                {
                    _pendingProducts.Add(response.Data.ProductId);
                    _onPurchaseFail.OnNext(new PurchaseFailureStatus(PurchaseFailureReason.Unknown, true, false));

                    var errorCode = (int) ApplicationErrorCodes.RepeatValidationProcess;
                    var errorRequest = CreateErrorRequestData(GetType().Name, CoreUtils.GetCaller(), errorCode, $"ProductId: {response.Data.ProductId}");
                    _serverApi.SendError(errorRequest, null);
                }

                _iapRepeatedTimer = Observable.Timer(TimeSpan.FromSeconds(RepeatedTimeSeconds))
                    .Subscribe(_ => { StartServerValidate(response.Data, false); });
            }
        }

        private IapValidateGpData GetIapValidateGpData(GooglePlayReceipt googlePlayReceipt, string transactionId,
            string receiptJson)
        {
            var data = new IapValidateGpData
            {
                RequestId = CreateNewRequestId(),
                OrderId = googlePlayReceipt.orderID,
                TransactionId = transactionId,
                PackageName = Application.identifier,
                ProductId = googlePlayReceipt.productID,
                UnityReceipt = receiptJson,
                AppVersion = Application.version
            };
            return data;
        }

        private IapValidateAppleData GetIapValidateAppleData(AppleInAppPurchaseReceipt appleReceipt,
            string transactionId, string receiptJson)
        {
            var data = new IapValidateAppleData
            {
                RequestId = CreateNewRequestId(),
                Receipt = GetApplePayload(receiptJson),
                TransactionId = transactionId,
                ProductId = appleReceipt.productID,
                UnityReceipt = receiptJson,
                AppVersion = Application.version
            };
            return data;
        }

        private string GetApplePayload(string receipt)
        {
            return JsonUtility.FromJson<ApplePayloadFromJson>(receipt).Payload;
        }

        private string CreateNewRequestId()
        {
            return Guid.NewGuid().ToString();
        }

        private void SendIapLog(string id, string transactionId)
        {
            var logData = CreateIapLogData(id, transactionId);

            _serverApi.IapLog(logData, null);
        }

        private IapLogData CreateIapLogData(string id, string transactionId)
        {
            var data = new IapLogData();
            _authenticationController.GetIdFromProfiles(out var googleId, out var appleId, out var fbId);
            data.UserId = _serverApi.GetUserId(googleId, appleId, fbId);
            data.Platform = Application.platform switch
            {
                RuntimePlatform.Android => "GooglePlay",
                RuntimePlatform.IPhonePlayer => "Apple",
                _ => ""
            };
            data.TransactionId = transactionId;
            data.DeviceId = SystemInfo.deviceUniqueIdentifier;
            data.AppVersion = Application.version;
            data.InstallDate = _playerService.GetInstallDate();
            data.Level = _playerService.Player.Level.Value.ToString();
            data.ProductId = id;

            data.BeforeCoins = data.AfterCoins = _playerService.Player.Coins.Value.ToString();
            data.BeforeCrystals = data.AfterCrystals = _playerService.Player.Cash.Value.ToString();
            data.BeforeEnergy = data.AfterEnergy = _playerService.Player.Energy.Value.ToString(CultureInfo.InvariantCulture);

            var reward = GetCurrencyFromProductById(id);
            switch (reward.Type)
            {
                case ECurrencyType.Soft:
                    data.BeforeCoins = (_playerService.Player.Coins.Value - reward.Amount).ToString();
                    break;
                case ECurrencyType.Hard:
                    data.BeforeCrystals = (_playerService.Player.Cash.Value - reward.Amount).ToString();
                    break;
                case ECurrencyType.Energy:
                    data.BeforeEnergy =
                        (_playerService.Player.Energy.Value - reward.Amount).ToString(CultureInfo.InvariantCulture);
                    break;
            }

            return data;
        }

        private ErrorRequestData CreateErrorRequestData(string className, string methodName, int errorCode,
            string message)
        {
            var data = new ErrorRequestData
            {
                Platform = Application.platform.ToString(),
                DeviceId = SystemInfo.deviceUniqueIdentifier,
                AppVersion = Application.version,
                ClassName = className,
                MethodName = methodName,
                ErrorCode = errorCode.ToString(),
                Message = message
            };

            return data;
        }

        private void ApplyReward(PurchaseStatusData statusData)
        {
            var bonus = _player.GetLengthPurchaseLog() == 0 ? true : false;
            var rewardCurrency = GetCurrencyFromProductById(statusData.ProductId, bonus);
            if (rewardCurrency.Type == ECurrencyType.Energy)
            {
                _playerService.PurchaseEnergy(rewardCurrency.Amount);
            }
            else
            {
                var reward = new List<CurrencyVo>() {rewardCurrency};
                _playerService.AddCurrencies(reward, true, true);
            }

            _player.AddPurchaseLog(statusData.TransactionId);
            _onPurchaseComplete.OnNext(new PurchaseStatus(statusData));

            if (!Application.isEditor)
            {
                if (statusData.ProductReceipt is GooglePlayReceipt googlePlayReceipt)
                    SendIapLog(statusData.ProductId, googlePlayReceipt.orderID);

                if (statusData.ProductReceipt is AppleInAppPurchaseReceipt)
                    SendIapLog(statusData.ProductId, statusData.TransactionId);

                _signalBus.Fire(new SignalPurchaseCompleteEvent(statusData));
            }
        }

        public CurrencyVo GetCurrencyFromProductById(string id, bool bonus = false)
        {
            var productItem = GetProductCatalogItemById(id);
            var quantity = int.Parse(productItem.Payouts[0].quantity.ToString(CultureInfo.InvariantCulture));
            var type = (ECurrencyType) Enum.Parse(typeof(ECurrencyType), productItem.Payouts[0].subtype);
            if (bonus)
                quantity = quantity * 2; //x2 for first purchase
            return new CurrencyVo(type, quantity);
        }

        public List<ProductCatalogItem> GetProductItemsByType(ECurrencyType type)
        {
            _productItemsByType.Clear();
            var typeStr = type.ToString();

            foreach (var product in _catalog.allValidProducts)
            {
                if (product.Payouts != null && product.Payouts.Count > 0)
                {
                    if (product.Payouts[0].subtype == typeStr)
                        _productItemsByType.Add(product);
                }
            }

            return _productItemsByType;
        }

        public ProductCatalogItem GetProductCatalogItemById(string id)
        {
            foreach (var product in _catalog.allValidProducts)
            {
                if (product.id == id)
                    return product;
            }

            return null;
        }

        public Product GetProductById(string id)
        {
            return m_Controller.products.WithID(id);
        }
    }
}

@AlexFreeman The user needs to be logged into the Play Store on the device, you’ll want to test.

@JeffDUnity3D Thank you for your reply.
It happens for users which are logged into the Play Store as well. We cannot find the steps to reproduce it but sometimes it happens on our test devices that are logged into the Play Store. And also in the logs on our server, we have too many users to imagine that all of them are logged off from the store.

Understood. Engineering is looking into this, no ETA at this time. If you can determine specific to reproduce, please let us know.

We investigated the issue.
The problem happens in the method QuerySkuDetailsService.ConsolidateOnSkuDetailsReceived() of unity purchasing plugin.

We have there:
billingResult.responseCode = 6
billingResult.debugMessage = “An internal error occurred.”

The issue affects about 7% of players.
When the initialization is failed we try to reinitialize the purchasing plugin. And after that for 50% of failed tries, the plugin is initialized after the second try of initialization but for the left 50% of users, it’s needed more than 5 tries to get success initialization.

@JeffDUnity3D Maybe it can help your analysis.
What can be the reason for this issue?

This should be addressed in IAP 4.0.3 now available.

@JeffDUnity3D Thank you for your reply.
We updated IAP to 4.0.3 but still have the same issue.
I had a look at the latest version of IAP. If I understood correctly, the method IsRecoverable(IGoogleBillingResult billingResult) of the class SkuDetailsQueryResponse is responsible to check should IAP retry request or not. But this method returns “true” only for the response codes “ServiceUnavailable” and “DeveloperError”.
return billingResult.responseCode == GoogleBillingResponseCode.ServiceUnavailable || billingResult.responseCode == GoogleBillingResponseCode.DeveloperError;
In our cases, we have the response code “FatalError” = 6.
Is that the correct way to check in this method also status “FatalError”? Or it is not recoverable status and we need another solution?

Please provide steps to reproduce. Can you reproduce every time? If so, it’s a different issue.

No, we can’t reproduce it every time. But after update to the latest IAP version, we still have the same logs for some players as I described in the previous posts.

We are checking.

Hello, we recently updated Unity IAP from 2.0.6 to 3.2.2 and also have noticed a spike of “NoProductsAvailable” errors for our release that was not present for previous releases on the older 2.0.6 package. Using Unity 2020.3.14f1.

Partial callstack:

EDIT: We confirmed through our data reporting that one of the affected users was still able to make a purchase so it seems to recover.

1 Like

I recently added IAP to my released app and I also see some spike of “NoProductsAvailable” to some of the users.
I know it is been suggested that the IAP should be not initailized again once it has already been initailized, but I just want to ask if it returns an “OnInitializeFailed” callback, is it safe to invoke the initailization again after some time interval?

The “NoProductsAvailable” can only happen when IAP receives an answer when retrieving products from the store. If none of the retrieved products matches the ones on your IAP’s side, then the error will appear.
Could there either be a race condition between building your product list and the call to the ConfigurationBuilder.AddProduct or that IAP is being initialized without any products being added?

Unity IAP is not intended to be initialized multiple times, but we got some reports that this does work, but it can cause a few issues as described here:

I would still recommend trying to solve the NoProductsAvailable instead of finding a workaround to avoid new issues.

Under what circumstances will OnInitializeFailed be called? I am actually seeing alot of custom parameter being “(not set)” instead of “NoProductsAvailable” as well, what can it mean?

This is what my code looks like:

        public void Initialize()
        {
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
            builder.AddProduct(id, productType);
            UnityPurchasing.Initialize(this, builder);
        }

If products were added incorrectly, it should affect all the players instead of a selected few, so what could go wrong?

OnInitializeFailed can be called in 2 cases:
-The initial products were retrieved from the store, but there’s no available products to purchase (when either no products were retrieved or the products obtained from the store don’t match the ones on IAP)
-The initial product retrieval from the Google Play Store failed because the connection to the Google Play Store is unavailable (We are adding additional information in 4.6.0 regarding the error happening)

Could you include your logs and what the custom parameter “(not set)” is?

The first case should usually happen to everyone, but if what you’re seeing is only a few users, then it’s most likely the second case with the connection to the Google Play Store.

You could try to listen to this callback if you wanted to notify the user:
https://docs.unity3d.com/Packages/com.unity.purchasing@4.5/manual/UnityIAPGooglePlay.html#listen-for-recoverable-initialization-interruptions