In not-yet-released iOS version of my game, I have a “restore purchases” UI button. I’m testing the game with a sandbox account. If I’ve purchased a consumable once, every time I tap on “restore purchases” the game gives me that consumable item, again and again. Unlimited soft currency. Below is my whole class of how I’ve implemented Unity Purchasing. Any pointers on why this is happening is greatly appreaciated.
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
using AE.Debugging;
public class InAppPurchasing : MonoBehaviour, IStoreListener {
public static InAppPurchaseSuccessfulEvent InAppPurchaseSuccessful;
public GameObject MarketPanelGameObject;
public MarketItemBase[] Products;
public bool Initialized;
public IStoreController StoreController;
private IExtensionProvider _extensionProvider;
private IAppleExtensions _appleExtensions;
private bool _validatorInitialized;
private CrossPlatformValidator _validator;
private bool _initializing;
private int _marketPanelGameObjectInstanceId;
private void Awake() {
UIPanelTransitioner.UIPanelTransitionStarted += OnUIPanelTransitionStarted;
MarketItemDiamondPack.PurchaseInitiated += OnPurchaseInitiated;
MarketItemItemPack.PurchaseInitiated += OnPurchaseInitiated;
MarketItemRemoveAds.PurchaseInitiated += OnPurchaseInitiated;
_marketPanelGameObjectInstanceId = MarketPanelGameObject.GetInstanceID();
}
private void OnUIPanelTransitionStarted(GameObject panelGo, int tweenGroup) {
if(Initialized || _initializing) {
return;
}
if(panelGo.GetInstanceID() != _marketPanelGameObjectInstanceId) {
return;
}
ConfigurationBuilder b = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
for(int i = 0; i < Products.Length; i++) {
if(!(Products[i] is IMarketInappPurchasable)) {
AEDebug.LogWarning($"{Products[i].name} is not an IMarketInappPurchasable. Skipping.");
continue;
}
IMarketInappPurchasable p = Products[i] as IMarketInappPurchasable;
b.AddProduct(p.ProductId, p.ProductType);
}
UnityPurchasing.Initialize(this, b);
_initializing = true;
}
public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {
_initializing = false;
Initialized = true;
StoreController = controller;
_extensionProvider = extensions;
_appleExtensions = _extensionProvider.GetExtension<IAppleExtensions>();
#if AE_DEBUG
AEDebug.Log("IAP::NOTIFICATION::Inapp purchasing initialized with below products");
for(int i = 0; i < StoreController.products.all.Length; i++) {
Product p = StoreController.products.all[i];
AEDebug.Log($"id:{p.definition.id} title:{p.metadata.localizedTitle} price:{p.metadata.localizedPrice} price string:{p.metadata.localizedPriceString}");
}
#endif
}
public void OnInitializeFailed(InitializationFailureReason error) {
_initializing = false;
Initialized = false;
AEDebug.LogError($"IAP::ERROR::{error}");
}
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason) {
AEDebug.LogError($"IAP::ERROR::PURCHASE::{product} purchase failed becaues {failureReason}");
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent) {
// If not one of the in-app purchase platforms, just mock as if the purchase was successful, this is here for editor testing
if(Application.platform != RuntimePlatform.Android && Application.platform != RuntimePlatform.IPhonePlayer && Application.platform != RuntimePlatform.OSXPlayer) {
InAppPurchaseSuccessful?.Invoke(purchaseEvent.purchasedProduct.definition.id);
return PurchaseProcessingResult.Complete;
}
bool validPurchase = true;
IPurchaseReceipt[] receipts = new IPurchaseReceipt[0];
if(!_validatorInitialized) {
_validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
_validatorInitialized = true;
}
try {
receipts = _validator.Validate(purchaseEvent.purchasedProduct.receipt);
for(int i = 0; i < receipts.Length; i++) {
if(Application.platform == RuntimePlatform.Android) {
GooglePlayReceipt googleReceipt = receipts[i] as GooglePlayReceipt;
if(googleReceipt.purchaseState != GooglePurchaseState.Purchased) {
validPurchase = false;
AEDebug.Log($"IAP::NOTIFICATION::PURCHASE::Product refunded or purchase cancelled, not a valid purchase, product id: {googleReceipt.productID}");
break;
}
}
AEDebug.Log("IAP::NOTIFICATION::PURCHASE::Product purchased with the following receipt:");
AEDebug.Log($"id:{receipts[i].productID} date:{receipts[i].purchaseDate} transaction:{receipts[i].transactionID}");
}
} catch(IAPSecurityException) {
validPurchase = false;
}
if(validPurchase) {
for(int i = 0; i < receipts.Length; i++) {
InAppPurchaseSuccessful?.Invoke(receipts[i].productID);
}
} else {
AEDebug.LogError("IAP::ERROR::PURCHASE::Invalid purchase detected, receipt is not accepted");
}
return PurchaseProcessingResult.Complete;
}
public void OnPurchaseInitiated(string productId) {
if(!Initialized) {
return;
}
StoreController.InitiatePurchase(productId);
}
public void OnRestorePurchasesButton() {
if(!Initialized) {
return;
}
if(Application.platform != RuntimePlatform.IPhonePlayer && Application.platform != RuntimePlatform.OSXPlayer) {
return;
}
_appleExtensions.RestoreTransactions(AppleRestorePurchasesCallback);
}
private void AppleRestorePurchasesCallback(bool success) {
AEDebug.Log($"Restoring purchases callback return value is: {success} ");
}
}
IMarketInappPurchasable is an interface implemented by some of my market item classes.
public interface IMarketInappPurchasable {
string ProductId { get; set; }
UnityEngine.Purchasing.ProductType ProductType { get; set; }
}
And upon checking I see those items that are given repeatedly are set as ProductType.Consumable as well.