Question:
I’m facing an issue with in-app purchases (IAP) in Unity, specifically when using Unity version 2022.3.55f1, Unity In-App Purchasing v4.12.2, and Google Billing v6.2.1 for a consumable product on the Google Play Store.
Problem Behavior:
- I successfully completed the purchase of a consumable item using the “Test card, always approves”.
- Immediately after the purchase is completed, I turn off the internet connection.
- I reconnect the internet almost immediately.
- When I try to purchase the same consumable item right away, I get the error: “You already own this item”.
- After waiting for a few minutes, I can purchase the item again successfully.
This issue seems related to the transaction process not fully completing or syncing immediately after reconnecting to the internet. I don’t know if this is the actual behaviour. Please help me.
What I’ve tried:
- I’ve ensured that Unity IAP is correctly initialized and set up.
- Checked the Google Play Console for purchase logs and there are no issues from Google Play’s side.
- Cleared app data and cache, but the issue persists.
Request for Code Assistance:
Could anyone help me understand why this error occurs immediately after reconnecting to the internet, and how I can handle this in my code? Here’s the current code for handling purchases:
public partial class AndroidInAppPurchaseManager : StoreSpecificPurchaseManager IDetailedStoreListener, IRestorePurchaseListner
{
private IGooglePlayStoreExtensions _googlePlayStoreExtensions;
#region SINGLETON
private static readonly AndroidInAppPurchaseManager _instance = new(); // Singleton pattern
static AndroidInAppPurchaseManager() { }
private AndroidInAppPurchaseManager() { }
public static AndroidInAppPurchaseManager Instance => _instance;
#endregion
// Initialize In-App Purchase system
public new void InitializeInAppPurchaseSystem()
{
base.InitializeInAppPurchaseSystem();
ConfigureIAPForPlayStore();
}
private void ConfigureIAPForPlayStore() // Configuring Unity IAP for Google Play
{
if (IsInitialized) return;
ConfigurationBuilder builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(AppStore.GooglePlay));
builder.Configure<IGooglePlayConfiguration>()
.SetServiceDisconnectAtInitializeListener(() =>
{
ToastController.Instance.ShowToastMessage("Unable to connect to the Google Play Billing service.");
})
.SetQueryProductDetailsFailedListener((int retryCount) =>
{
ToastController.Instance.ShowToastMessage($"Failed to query product details {retryCount} times.");
})
.SetDeferredPurchaseListener(OnDeferredPurchase);
AddProductsToConfigurationBuilder(builder, gameProductsDictionary);
UnityPurchasing.Initialize(this, builder);
}
// Add products to IAP configuration
protected override void AddProductsToConfigurationBuilder(ConfigurationBuilder builder, Dictionary<string, InAppProduct> productDict)
{
if (gameProductsDictionary == null || gameProductsDictionary.Count == 0)
{
gameProductsDictionary = ProductManager.Instance?.GetProducts();
}
foreach (var product in gameProductsDictionary.Keys)
{
if (!string.IsNullOrEmpty(product))
{
builder.AddProduct(gameProductsDictionary[product].AndroidProductID, gameProductsDictionary[product].ProductType);
}
else
{
CustomFirebaseAnalytics.RecordCrashlyticsException("Product key is Null Or Empty in AddProductsToBuilder method");
}
}
}
#region Unity IAP Initialization
public new void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
base.OnInitialized(controller, extensions);
_googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
if (storeController == null || storeController.products == null)
{
CustomFirebaseAnalytics.RecordCrashlyticsException("StoreController is Null On OnInitialized");
return;
}
Product[] products = storeController.products.all;
if (products == null)
{
Debug.LogWarning("Android Product is null on initialization.");
return;
}
UpdatePricesForIAPProducts(products, GetProductIdFromAndroidId);
PlayerDataHandler.Instance?.LoadPurchasedProduct();
PlayerDataHandler.Instance?.PerformLocalReceiptValidation(products);
}
public new void OnInitializeFailed(InitializationFailureReason error)
{
base.OnInitializeFailed(error);
}
public new void OnInitializeFailed(InitializationFailureReason error, string message)
{
base.OnInitializeFailed(error, message);
}
#endregion
#region Android Purchase Process
public new PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
var productItem = purchaseEvent.purchasedProduct;
FinalizePurchaseAndGrantReward(productItem);
return PurchaseProcessingResult.Complete; // Complete the purchase immediately
}
public new void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
base.OnPurchaseFailed(product, failureReason);
}
public new void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
base.OnPurchaseFailed(product, failureDescription);
}
protected override void FinalizePurchaseAndGrantReward(Product product)
{
var purchaseStatus = _googlePlayStoreExtensions.GetPurchaseState(product);
switch (purchaseStatus)
{
case GooglePurchaseState.Purchased:
if (product.definition.type == ProductType.NonConsumable)
{
PlayerDataHandler.Instance.SavePurchaseProduct(product.definition.id, true);
}
rewardsController.RewardPlayer(product.definition.id);
CustomFirebaseAnalytics.SetEvent(FirebaseAnalyticsEvents.IAP_PURCHASE_SUCCESS, product.metadata.localizedTitle, product.definition.storeSpecificId);
break;
case GooglePurchaseState.Refunded:
rewardsController.UnrewardPlayer(product.definition.id);
PlayerDataHandler.Instance.SavePurchaseProduct(product.definition.id, false);
break;
case GooglePurchaseState.Deferred:
OnDeferredPurchase(product);
break;
case GooglePurchaseState.Cancelled:
OnPurchaseFailed(product, PurchaseFailureReason.UserCancelled);
break;
}
}
#endregion
#region Deferred Purchase Handling
private bool IsPurchasedProductDeferred(string productId)
{
var product = storeController.products.WithID(productId);
return _googlePlayStoreExtensions.IsPurchasedProductDeferred(product);
}
private void OnDeferredPurchase(Product product)
{
ToastController.Instance.ShowToastMessage($"Product {product.metadata.localizedTitle} Purchase has been deferred. Complete the transaction in the Play Store");
CustomFirebaseAnalytics.SetEvent(FirebaseAnalyticsEvents.IAP_PURCHASE_DEFFERED, product.metadata.localizedTitle, product.definition.storeSpecificId);
}
#endregion
}
}
Questions:
- Why does the “You already own this item” error occur immediately after reconnecting the internet?
- Is there a delay in processing the purchase or syncing the purchase state after reconnecting, and how can I handle this programmatically to allow an immediate purchase?
- Is there a way to ensure that Unity IAP properly recognizes and processes a new purchase after reconnecting the internet without having to wait for a few minutes?
Expected Behavior:
I should be able to complete the purchase of a consumable item immediately after reconnecting the internet, without getting the error “You already own this item”, and without waiting for any delay.
I’m using Unity 2022.3.55f1, Unity IAP v4.12.2, and Google Billing v6.2.1. Any help or suggestions would be greatly appreciated!