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