'You already own this item' Error for consumable items, Immediately after reconnecting Internet for Unity IAP (v4.12.2) and Google Billing (v6.2.1):

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:

  1. I successfully completed the purchase of a consumable item using the “Test card, always approves”.
  2. Immediately after the purchase is completed, I turn off the internet connection.
  3. I reconnect the internet almost immediately.
  4. When I try to purchase the same consumable item right away, I get the error: “You already own this item”.
  5. 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:

  1. I’ve ensured that Unity IAP is correctly initialized and set up.
  2. Checked the Google Play Console for purchase logs and there are no issues from Google Play’s side.
  3. 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:

  1. Why does the “You already own this item” error occur immediately after reconnecting the internet?
  2. 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?
  3. 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!

1 Like

Hello @MohammedRilwan

It seems like you are using a test account and that your purchase isn’t being finished.
As outlined in Google’s documentation, the purchase will be refunded after 3 minutes if it hasn’t been acknowledged when testing.

Could you confirm by going in your Google Play Console under Order Management that the test orders are being refunded?

In this case, when the finish transaction fails because of the Internet connection being down, you will receive a callback to OnPurchaseFailed.
The purchase should reach your ProcessPurchase again the next time purchases are fetched (when foregrounding the application, restoring transaction, initializing IAP), but you can also call ConfirmPendingPurchase to reattempt finishing the transaction.