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