IAP Caching products price

Hello guys!

We are having many problems with price caching from products, getting the product from Google Play using In App Purchasing.

We can get the price in the first time when app open, but when need update the price in Google Play Console, the price don’t update in game. But when click to buy the game, the Google Play modal show the correct price.

We try use IAP version 2 with Unity 2019.3.0.f6 and now using IAP 4.1.2 and using Unity 2021.2.7.f1. Both versions have the same problem.

We tried force update the price getting again the product using the method “FetchAdditionalProducts” but this doesn’t work too.

Even opening and closing the game many times this make no difference.

We think this problem is with IAP caching, specifically in "product.metadata".

The code:

using System;
using UnityEngine;
using UnityEngine.Purchasing;
using TMPro;
 
public class Purchaser : Singleton<Purchaser>, IStoreListener
{
        [SerializeField]
        private TextMeshProUGUI fullPrice_android;
        [SerializeField]
        private TextMeshProUGUI price_android;
        [SerializeField]
        private TextMeshProUGUI fullPrice_iOS;
        [SerializeField]
        private TextMeshProUGUI price_iOS;
        private Product product;
        private static IStoreController m_StoreController;          // The Unity Purchasing system.
        private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
 
        public static string removeAds = "com.dinitystudios.crashdashone.removeads";
 
        // Apple App Store-specific product identifier for the subscription product.
        private static string kProductNameAppleSubscription = "com.unity3d.subscription.new";
 
        // Google Play Store-specific product identifier subscription product.
        private static string kProductNameGooglePlaySubscription = "com.unity3d.subscription.original";
 
        void Start()
        {
            // If we haven't set up the Unity Purchasing reference
            if (m_StoreController == null)
            {
                // Begin to configure our connection to Purchasing
                InitializePurchasing();
            }
        }
 
        public void InitializePurchasing()
        {
            // If we have already connected to Purchasing ...
            if (IsInitialized())
            {
                // ... we are done here.
                return;
            }
 
            // Create a builder, first passing in a suite of Unity provided stores.
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
 
            // Add a product to sell / restore by way of its identifier, associating the general identifier
            builder.AddProduct(removeAds, ProductType.NonConsumable);
 
            UnityPurchasing.Initialize(this, builder);
        }
 
 
        private bool IsInitialized()
        {
            // Only say we are initialized if both the Purchasing references are set.
            return m_StoreController != null && m_StoreExtensionProvider != null;
        }
 
        private void SetAdPrice()
        {
            product = m_StoreController.products.WithID(removeAds);
 
            var price = product.metadata.localizedPrice;
            var isoCurrencyCode = product.metadata.isoCurrencyCode;
 
            price_android.text = $"{price} {isoCurrencyCode}";
 
            price_iOS.text = $"{price} {isoCurrencyCode}";
        }
 
        public void BuyRemoveAds()
        {
            BuyProductID(removeAds);
        }
 
        void BuyProductID(string productId)
        {
            // If Purchasing has been initialized ...
            if (IsInitialized())
            {
                // ... look up the Product reference with the general product identifier and the Purchasing
                // system's products collection.
                Product product = m_StoreController.products.WithID(productId);
 
                // If the look up found a product for this device's store and that product is ready to be sold ...
                if (product != null && product.availableToPurchase)
                {
                    Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
                    // ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed
                    // asynchronously.
                    m_StoreController.InitiatePurchase(product);
                }
                // Otherwise ...
                else
                {
                    // ... report the product look-up failure situation
                    Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
                }
            }
            // Otherwise ...
            else
            {
                // ... report the fact Purchasing has not succeeded initializing yet. Consider waiting longer or
                // retrying initiailization.
                Debug.Log("BuyProductID FAIL. Not initialized.");
            }
        }
 
 
        // 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 (!IsInitialized())
            {
                // ... report the situation and stop restoring. Consider either waiting longer, or retrying initialization.
                Debug.Log("RestorePurchases FAIL. Not initialized.");
                return;
            }
 
            // If we are running on an Apple device ...
            if (Application.platform == RuntimePlatform.IPhonePlayer ||
                Application.platform == RuntimePlatform.OSXPlayer)
            {
                // ... begin restoring purchases
                Debug.Log("RestorePurchases started ...");
 
                // Fetch the Apple store-specific subsystem.
                var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
                // Begin the asynchronous process of restoring purchases. Expect a confirmation response in
                // the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
                apple.RestoreTransactions((result) =>
                {
                    // The first phase of restoration. If no more responses are received on ProcessPurchase then
                    // no purchases are available to be restored.
                    Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
                });
            }
            // Otherwise ...
            else
            {
                // We are not running on an Apple device. No work is necessary to restore purchases.
                Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
            }
        }
 
 
        //
        // --- IStoreListener
        //
 
        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            // Purchasing has succeeded initializing. Collect our Purchasing references.
            Debug.Log("OnInitialized: PASS");
 
            // Overall Purchasing system, configured with products for this application.
            m_StoreController = controller;
            // Store specific subsystem, for accessing device-specific store features.
            m_StoreExtensionProvider = extensions;
 
            SetAdPrice();
        }
 
 
        public void OnInitializeFailed(InitializationFailureReason error)
        {
            // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
            Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
        }
 
 
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
        {
            if (String.Equals(args.purchasedProduct.definition.id, removeAds, StringComparison.Ordinal))
            {
                Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
 
                AdManager.Instance.SetAdsRemoved();
            }
            // Or ... an unknown product has been purchased by this user. Fill in additional products here....
            else
            {
                Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
            }
 
            // Return a flag indicating whether this product has completely been received, or if the application needs
            // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still
            // saving purchased products to the cloud, and when that save is delayed.
            return PurchaseProcessingResult.Complete;
        }
 
 
        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing
            // this reason with the user to guide their troubleshooting actions.
            Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
        }
    }

@savala02 You should not change the price of a product once you publish, and definitely don’t change the price after a customer makes a purchase. If you make a price change, I suspect you’ll need to publish a new version of your game also.

Hello @JeffDUnity3D

We have no intention of change the price once this is defined, our concern is that in google play console the price is automatically generated in local currency accordingly with the region, the exchange rate can vary along of time so the price in the Google Play modal and the in-game price will no be the same in this case.

Got it! I see what you mean, makes perfect sense. I will pass this along to engineering. Agreed, it has the appearance of an issue at our end that we would need to address, hopefully in a future release. We have quite a back log of tasks at the moment and a bit resource constrained, so likely no immediate solution. Can you share a specific example of such a price change that isn’t reflected in the localizedPrice? A screenshot of the Google popup on the device, the device locale, the account locale, a screenshot of the price defined your dashboard, the countries you’ve selected on the Google dashboard and the corresponding Debug.Log output in the logcat logs showing the localizedPrice would help to speed things along. Please let me know if you need help with any of those.

Just checking, are you able to provide the requested information?

Hello, sorry for taking too long to answer.
I was trying to get an android account setup with a different country to make adicional tests but I didn’t manage to so the best I can do for now is simulate the issue.

Test condition:
google play console account locale - Brazil
google play console currency - BRL
device locale - Brazil
android account locale - Brazil
android account currency - BRL

Replicating the issue:

The precification was defined to 10 BRL on google play console.

The application was runned in the device for the first time. The value is shown as 10 BRL in the game IU and in the google play IU

The precification was defined to 5 BRL on google play console to simulate a price variation due exchange rate fluctuation.

The application was runned in the device for the second time. The value is shown as 10 BRL in the game IU and 5 BRL in the google play IU.

The test was conducted in 2 different devices and on an android emulator, the 3 tests had the same result.
There is no log error to report

note: the green text field is received product.metadata.localizedPrice + product.metadata.localizedPrice.




Can you share your full purchasing code (or send it to me in a private message)? Specifically, when and where are you retrieving product.metadata.localizedPrice.

The full code is in the first post, the product.metadata.localized Price is retrieved in line 68 inside the function “SetAdPrice”. The script is attached to a gameobject in the first scene that loads when the game starts (this gameobject is not destroyed when a new scene loads).
the code execution sequence is: function “Start”(line 29) calls “InitializePurchasing”(line 39), if the callback “OnInitialized”(line 158) occur the “SetAdPrice”(line 68) is called, “SetAdPrice” retrieved product.metadata.localized and pass the value to a UI text field.

Got it! I will follow up. What is the localizedPrice in ProcessPurchase, does that match the dashboard? Granted too late there to set the price in the UI, just troubleshooting so far. Is it then set correctly the next time the app launches?

In general once the app get the price from the dashboard the price will always be the same and will not be correct the next time the app launches, but in all the tests i saw the app update the price for the new value on two occasions although i was not able to identify what has triggered this update.

I suspect a purchase triggered the update.

It could be but at the time i’m sure there was no purchase to trigger this update. We were not testing purchase just trying to retrieve the localizedPrice correctly

I am asking you to confirm, not as a solution but to troubleshoot. Does the price get set correctly in the cache after a purchase? Please refer to my “granted too late” comment previously also.

The price do not get set correctly in the cache after a purchase. Once the cache gets a value, it will never change.

Again, what is the price in ProcessPurchase? Is it correct there? Check the product object passed into ProcessPurchase. If it is not correct there, unfortunately there is nothing we (Unity IAP) can do, we are getting the price directly via the Google Billing API.

@savala02 Have you had a chance to check the price within the ProcessPurchase method?

I haven’t had a chance to test it yet. I will do it as soon as possible.

Same issue here.
And I just found that changing language in phone will trigger updating the prcie text.

Hi, I got the similar issue, the price in Japan iOS appstore is adjusted but the localised price string is not reflected. Is there anyway to refresh the price?

We seem to notice the same issue as well on the iOS, the price didn’t change in game after we change the price at Appstore there