iOS purchase restore issue

Hello!

The problem is that I cant check if user already bought non-consumable items.

What happens: User downloads app, shop initializes correctly and loads correct prices. Then he buys product, shop returns that purchase was processed and I save it in PlayerPrefs and everything is fine. But if the user deletes the app and installs it again PlayerPrefs are gone and it shouldn’t be a problem, because as I understand RestorePurchases should try to buy all products again those that he owns. But it does nothing. When he tries to buy product again he gets a pop up its already owned and I get no function called. What is most strange to me in this is that everything works as expected in testing “Sandbox”, restore purchase button restores purchases and if you already own it and buy it again it buys it. The problem is only in the release version.

Hoping someone can help, because I’m out of options. If you need more info please ask.

Thank you

Is this on iOS? Please show the code that you are using for restore Unity - Manual: Restoring Transactions Can you confirm that ProcessPurchase is being fired in your testing? How are you debugging?

Is this on iOS? Yes
Can you confirm that ProcessPurchase is being fired in your testing? Yes it logs all the messages and purchase happens.
How are you debugging? Test version - logs with logcat. Release version - I dont. Dont know how.

My Purchaser script. I can see in unity when IAPCover is showing.

public static Purchaser instance;

    private static IStoreController m_StoreController;          // The Unity Purchasing system.
    private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.

    public string allDolls;
    public string uniCamiDoll;
    public string bettyBerryDoll;
    public string cottonSandyDoll;

    private void Awake()
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
    }

    private void Start()
    {
        // If we haven't set up the Unity Purchasing reference
        if (m_StoreController == null)
        {
            // Begin to configure our connection to Purchasing
            InitializePurchasing();
        }
       
        CheckIfDollsBought();
    }

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.
        ConfigurationBuilder builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
       
        builder.AddProduct(allDolls, ProductType.NonConsumable);
        builder.AddProduct(uniCamiDoll, ProductType.NonConsumable);
        builder.AddProduct(bettyBerryDoll, ProductType.NonConsumable);
        builder.AddProduct(cottonSandyDoll, ProductType.NonConsumable);
       
        // Kick off the remainder of the set-up with an asynchrounous call, passing the configuration
        // and this class' instance. Expect a response either in OnInitialized or OnInitializeFailed.
        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;
    }

public void BuyProduct(string productName)
    {
        if(productName == "allDolls")
        {
            BuyProductID(allDolls);
        }
        else if( productName == "uniCamiDoll")
        {
            BuyProductID(uniCamiDoll);
        }
        else if(productName == "bettyBerryDoll")
        {
            BuyProductID(bettyBerryDoll);
        }
        else if (productName == "cottonSandyDoll")
        {
            BuyProductID(cottonSandyDoll);
        }
    }

    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);
                PurchaserHelper.instance.SetIAPCover(true);
            }
            // 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.
            IAppleExtensions 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) => {
                CheckIfDollsBought();
                // 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);
        }
    }

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;
       
        CheckIfDollsBought();
    }

void CheckIfDollsBought()
    {
        if (m_StoreController != null)
        {
            if (m_StoreController.products.WithID(allDolls).hasReceipt)
            {
                PurchaserHelper.instance.TurnOffLockAllDolls();
                SavedData.SetInt("bought_all_dolls", 1);
                if (FindObjectOfType<PurchaserHelper>())
                {
                    PurchaserHelper.instance.TurnOffLockAllDolls();
                }
            }
            else
            {
                if (m_StoreController.products.WithID(uniCamiDoll).hasReceipt)
                {
                    PurchaserHelper.instance.TurnOffLockUniCami();
                    SavedData.SetInt("bought_uniCami", 1);
                    if (FindObjectOfType<PurchaserHelper>())
                    {
                        PurchaserHelper.instance.TurnOffLockUniCami();
                    }
                }

                if (m_StoreController.products.WithID(bettyBerryDoll).hasReceipt)
                {
                    PurchaserHelper.instance.TurnOffLockBettyBerry();
                    SavedData.SetInt("bought_bettyBerry", 1);
                    if (FindObjectOfType<PurchaserHelper>())
                    {
                        PurchaserHelper.instance.TurnOffLockBettyBerry();
                    }
                }

                if (m_StoreController.products.WithID(cottonSandyDoll).hasReceipt)
                {
                    PurchaserHelper.instance.TurnOffLockCottonSandy();
                    SavedData.SetInt("bought_cottonSandy", 1);
                    if (FindObjectOfType<PurchaserHelper>())
                    {
                        PurchaserHelper.instance.TurnOffLockCottonSandy();
                    }
                }
            }
            SavedData.Save();
        }
    }

public void OnInitializeFailed(InitializationFailureReason error)
    {
        // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
        PurchaserHelper.instance.SetIAPCover(false);
       Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    {
        if (string.Equals(args.purchasedProduct.definition.id, allDolls, StringComparison.Ordinal))
        {
            PurchaserHelper.instance.TurnOffLockAllDolls();
            SavedData.SetInt("bought_all_dolls", 1);
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
        }
        else if (string.Equals(args.purchasedProduct.definition.id, uniCamiDoll, StringComparison.Ordinal))
        {
            PurchaserHelper.instance.TurnOffLockUniCami();
            SavedData.SetInt("bought_uniCami", 1);
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
        }
        else if (string.Equals(args.purchasedProduct.definition.id, bettyBerryDoll, StringComparison.Ordinal))
        {
            PurchaserHelper.instance.TurnOffLockBettyBerry();
            SavedData.SetInt("bought_bettyBerry", 1);
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
        }
        else if (string.Equals(args.purchasedProduct.definition.id, cottonSandyDoll, StringComparison.Ordinal))
        {
            PurchaserHelper.instance.TurnOffLockCottonSandy();
            SavedData.SetInt("bought_cottonSandy", 1);
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
        }
        else
        {
            //GameObject.Find("PurchaseCover").GetComponent<Animator>().SetTrigger("Hide");
            Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
        }
        SavedData.Save();

        // 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.
        PurchaserHelper.instance.SetIAPCover(false);
        FindObjectOfType<PlayerController>().dollUnlocked = true;
        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.
        PurchaserHelper.instance.SetIAPCover(false);
        Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    }

    public string GetProductPriceFromStore(string _id)
    {
        if (m_StoreController != null && m_StoreController.products != null)
        {
            return m_StoreController.products.WithID(_id).metadata.localizedPriceString;
        }
        else
        {
            return "";
        }
    }

    public IEnumerator LoadPrices()
    {
        while (!IsInitialized())
        {
            yield return null;
        }
        Debug.Log("Purchaser initialized");
        PurchaserHelper.instance.SetPrices(GetProductPriceFromStore(uniCamiDoll), GetProductPriceFromStore(bettyBerryDoll), GetProductPriceFromStore(cottonSandyDoll), GetProductPriceFromStore(allDolls));
    }

Logcat is on Android, not iOS. Did Apple accept your application without Restore working? Usually they catch things like that.

I meant on xcode, my bad. Yes, like 4 times :slight_smile:

Without logs or additional debug information, there is unfortunately nothing we can do. If Restore is happening on Sandbox but not on Release, that would likely be an Apple issue. You’ll want to ensure that you are not seeing an issue with your PlayerPrefs logic, and confirm that ProcessPurchase is being triggered.

But if the user can buy it first time, that should mean its happening. On the second try when the user is trying to buy an Item that it already bough that is when it doesn’t get called.

You seem to be changing the topic now. Are you discussing Restore on iOS, or a second purchase? Restore is the process that occurs when a user reinstalls the game, or installs the game on a new iOS device. Previous non-consumable purchases will automatically be made available as a separate call for each product to ProcessPurchase after calling the iOS extension method RestorePurchases. This is separate from the user attempting to make a second purchase. A second purchase should not be possible for a non-consumable or a subscription, an error would be expected to occur. You should disable the Purchase button if a user has already purchased such a product so they can’t purchase a second time. Apple does not allow a re-purchase of a non-consumable or subscriptions as by definition they are a one-time purchase.

Sorry if that sounded that way. Yes I disable purchase if user has bough it. The problem is I don’t know it happened when app is reinstalled because restore purchase doesn’t do anything and when they try to buy it (Because I have no information that they done it before) the app store shows an error that item is already bought and I don’t get a process purchase call, but when in sandbox (not release version) I get it. So as I understand I should write this problem to AppStore and see if they can help me.

Thank you :slight_smile:

No, IAP is behaving as expected, a second purchase is not possible. You will want to confirm if ProcessPurchase is firing during Restore by debugging. Sandbox and Release should behave the same, you’ll need to document proof if you submit an appeal to Apple. PlayerPrefs is not the best way to store purchase information, it can get deleted on reinstall. You’ll want to look through the receipts in the product controller during IAP OnInitialization. See the sample IAP project for an example, look in IAPManager.cs https://discussions.unity.com/t/700293