Product receipt from ProductCollection on Apple

in GooglePlay, for consumable products which are pending (so any product that I have not yet confirmed), I am able to retrieve the receipt from the Product which is stored in IStoreController.products, and from there, I can carry on wherever I left off in the purchase validation with my backend server before it was interrupted. However, on Apple, the Products don’t seem to contain the receipt data. Should this be consistent across platforms and maybe I am just missing something? or is the Apple behaviour meant to be different?

I can get it from the ProcessPurchase call, but I have some logic that is sensitive to timing issues, so I was hoping there was a way to know if there are purchases which will need to be processed on initialization.

Doesn’t seem, can you elaborate? It should be base 64 encoded https://docs.unity3d.com/Manual/UnityIAPPurchaseReceipts.html and https://discussions.unity.com/t/825345

I’ll try to elaborate. For purchases that are made, I send the receipt to my server for validation and to award the product to the user’s account. In cases where the response is interrupted (either from a connection issue, or closing the app prematurely), I want to re-attempt the request to my backend server. To do this, on initialization, I am getting all products where hasReceipt is true, and then passing that along to my server again. This works fine in GooglePlay, since products which have not yet been acknowledged have the receipt attached, but on Apple, every product’s hasReceipt returns false (except for subscriptions).

So for instance, I’d use something like _storeController.products.all.FindAll(x => x.hasReceipt) to retrieve all products which have a receipt in OnInitialized

I haven’t actually checked the receipt yet to see if maybe the hasReceipt property is wrong.

Apple may behave differently on interrupted purchases. Can you try the same test by returning Pending from ProcessPurchase, and restarting the app?

I am already returning Pending from ProcessPurchase, and the purchases do run through ProcessPurchase again when the app is restarted, but the receipts are not visible through the ProductCollection when I initialize.

The issue is that I have some code that needs to wait on all purchases to be processed, but if I don’t know if there even are purchases to process on initialization, I have no way of setting a signal or a flag which can handle that. So the purchases do process, but by the time they do, it’s too late in the state setup.

ProductCollection? Please show your initialization code.

Where we initialize the products and call UnityPurchasing.Initialize:

private void InitializeProducts()
        {
            Debug.Log("Purchaser.InitializeProducts()");

            if (_bridge is null)
            {
                Debug.LogError("Bridge is null during initialization process");
                return;
            }

            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
            foreach (var product in _bridge.Products.products)
            {
                var hasAppleId = !string.IsNullOrEmpty(product.AppleProductId);
                var hasGoogleId = !string.IsNullOrEmpty(product.GoogleProductId);

                if (hasAppleId && hasGoogleId)
                {
                    builder.AddProduct(product.Id, product.ProductType, new IDs()
                    {
                        {product.AppleProductId, AppleAppStore.Name},
                        {product.GoogleProductId, GooglePlay.Name},
                    });
                }
                else
                {
                    Debug.LogWarning($"[purchaser] skipped product: '{product.Id}' ({hasAppleId} / {hasGoogleId})");
                }
            }

            _initStarted = true;
            UnityPurchasing.Initialize(this, builder);
        }

The OnInitialized call:

public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            Debug.Log("Purchaser.OnInitialized()");

            _storeController = controller;
            _storeExtensionProvider = extensions;
            Debug.Log($"Store: {_storeController.ToString()}");
            Debug.Log($"Product: {_storeController.products.all[0].definition.id} ");

            if (_bridge is null)
            {
                Debug.LogError("Bridge is null after initialization!");
                return;
            }
            else
                _bridge.OnInitialized(true);
        }

The _bridge.OnInitialized code, where we are attempting to read the receipts from _storeController.products.all (ie: the ProductCollection):

public void OnInitialized(bool success)
        {
            _isInitialized = true;

            Debug.Log($"OnInitialized: {success}");

            // AllProducts is just an auto property that gets _storeController.products.all
            _restoredProducts.AddRange(_purchaser.AllProducts.FindAll(x => x.hasReceipt));

            foreach (var restoredProduct in _restoredProducts)
            {
                // On iOS, the only purchases which seem to have receipts attached are the subscriptions,
                // no pending purchases.
                // on Android, both subscriptions and pending purchases are printed here
                Debug.Log($"Restored {restoredProduct.Definition.id}");
            }

            // If there's any products which have receipts attached (ie: pending products), make backend validation calls
            // and then fire a signal once we're all done
            if (_restoredProducts.Count > 0)
            {
                var countValidated = new List<bool>();
                Debug.Log($"Validating on initialized");
                foreach (var restoredProduct in _restoredProducts)
                {
                    // Backend validation calls
                    ValidateReceipt(restoredProduct, (sendSuccess, productId) =>
                    {
                        if (sendSuccess) OnReceiptValidated(productId);
                        countValidated.Add(sendSuccess);

                        if (countValidated.Count == _restoredProducts.Count)
                        {
                            _bus.Fire(new IAPInitializationDone { success = countValidated.TrueForAll(x => x) });
                        }
                    });
                }

            }
            else
            {
                _bus.Fire(new IAPInitializationDone { success = success }); //Needed for login to continue!
            }
        }

Sorry I would not be able to debug or read this code. You have two OnInitialized methods for example. If OnInitialized is called (the actual callback), then success is always true. I might suggest you compare (and use) the IAPManager.cs from the Sample IAP Project v2. If the product is still in Pending, you should see ProcessPurchase trigger during IAP Initialization. https://discussions.unity.com/t/700293/3 You will likely see one error if you are using IAP 4.0.0 with the Sample project, see the Deferred Purchase sample that ships with IAP for the correct syntax.

The second OnIinitialized is just a bridge, it gets called by the first.

ProcessPurchase does trigger, but we have a halting problem wherein we do not know how many purchases will process (or if any), so we cannot await their results before starting up the rest of the app.

The issue is not that the purchasing or pending purchases is not working correctly, it does in fact work with the code we have, we just can’t await properly the same way we do on android.

The root of my question is ultimately: is it a Unity IAP bug that on Apple we cannot see the receipts from products.all, while we can on Android, or is this known or expected behaviour?

The Restore method will return when all ProcessPurchase calls are complete. I’m able to see the receipts with the Sample IAP Project.

When you say that you are able to see the receipts, do you mean from the argument passed into the ProcesPurchase, or do you mean from the ProductCollection?

By Restore method, do you mean the RestorePurchases method as it is in the sample project? We have debug logs in our RestorePurchases method, and it would seem that is not called after process purchase is done with processing pending purchases.

Both. You haven’t shared your RestorePurchases method. What if you ignore the .hasReceipt property and checked the receipt directly? But more specifically, you are customizing code without getting the basics working first.

The code we have is based off of the sample project, we’ve just extended it. The purchasing and validation does work, I’m just getting inconsistent values from the ProductCollection on apple devices compared to android (Products from the passed arguments in ProcessPurchase are correct), which just affects my flow of signal firing after validation.

We have implemented a fallback solution in the meantime, so this isn’t a breaking issue for us, but I’m mostly curious as to why this seemingly multipurpose field would have different behaviour on different devices.

I’ve just tested on device, and the receipt value is also left blank, so I have confirmed that the hasReceipt flag at least appears to be correct.

Is it possible that the ProductCollection isn’t populated until the ProcessPurchase is run, and I am just trying to read it too early? Like maybe on Android it happens to get the information more quickly, so everything is fine?

Sorry, as mentioned, it’s working for me (and many others) without customizing the code like you have done.

I have run the sample project for pending consumable purchases given from the package manager for IAP 4.0, and I can confirm that the apple receipts are in fact not provided on initialization. I’ve used the exact scene as provided in the sample, and the only modification I’ve made is to log the receipts in products.all. Tested by making a purchase, and closing the app before the purchase is confirmed. The purchase does get processed and confirmed on re-opening, but the receipt is not visible on initialization. This is inconsistent with Android, where the receipts are visible on initialization.

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.UI;

namespace Samples.Purchasing.Core.IntegratingSelfProvidedBackendReceiptValidation
{
    public class IntegratingSelfProvidedBackendReceiptValidation : MonoBehaviour, IStoreListener
    {
        IStoreController m_StoreController;

        public string goldProductId = "redacted";
        public ProductType goldType = ProductType.Consumable;

        public Text GoldCountText;
        public Text ProcessingPurchasesCountText;

        int m_GoldCount;
        int m_ProcessingPurchasesCount;

        void Start()
        {
            InitializePurchasing();
            UpdateUI();
        }

        void InitializePurchasing()
        {
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

            builder.AddProduct(goldProductId, goldType);

            UnityPurchasing.Initialize(this, builder);
        }

        public void BuyGold()
        {
            m_StoreController.InitiatePurchase(goldProductId);
        }

        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            Debug.Log("In-App Purchasing successfully initialized");
            m_StoreController = controller;

            foreach (var product in m_StoreController.products.all)
            {
                Debug.Log($"Product {product.definition.id}");
                Debug.Log($"receipt: {product.receipt}");
            }
        }

        public void OnInitializeFailed(InitializationFailureReason error)
        {
            Debug.Log($"In-App Purchasing initialize failed: {error}");
        }

        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
        {
            //Retrieve the purchased product
            var product = args.purchasedProduct;

            StartCoroutine(BackEndValidation(product));

            //We return Pending, informing IAP to keep the transaction open while we validate the purchase on our side.
            return PurchaseProcessingResult.Pending;
        }

        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            Debug.Log($"Purchase failed - Product: '{product.definition.id}', PurchaseFailureReason: {failureReason}");
        }

        IEnumerator BackEndValidation(Product product)
        {
            m_ProcessingPurchasesCount++;
            UpdateUI();

            //Mock backend validation. Here you would call your own backend and wait for its response.
            //If the app is closed during this time, ProcessPurchase will be called again for the same purchase once the app is opened again.
            yield return MockServerSideValidation(product);

            m_ProcessingPurchasesCount--;
            UpdateUI();

            Debug.Log($"Confirming purchase of {product.definition.id}");

            //Once we have done the validation in our backend, we confirm the purchase.
            m_StoreController.ConfirmPendingPurchase(product);

            //We can now add the purchased product to the players inventory
            if (product.definition.id == goldProductId)
            {
                AddGold();
            }
        }

        YieldInstruction MockServerSideValidation(Product product)
        {
            const int waitSeconds = 3;
            Debug.Log($"Purchase Pending, Waiting for confirmation for {waitSeconds} seconds - Product: {product.definition.id}");
            return new WaitForSeconds(waitSeconds);
        }

        void AddGold()
        {
            m_GoldCount++;
            UpdateUI();
        }

        void UpdateUI()
        {
            GoldCountText.text = $"Your Gold: {m_GoldCount}";

            ProcessingPurchasesCountText.text = "";
            for (var i = 0; i < m_ProcessingPurchasesCount; i++)
            {
                ProcessingPurchasesCountText.text += "Purchase Processing...\n";
            }
        }
    }
}

Do you see the product receipts in the normal flow, without interrupting the purchase? Can you reproduce by just returning Pending from ProcessPurchase instead of interrupting the flow? I will test here too.

@lyonb Does this look like it might apply here? Failure to ConfirmPendingPurchase after a Apple restores when doing backend receipt validations

@lyonb I have reproduced with IAP 4.0.0. If you return Pending from ProcessPurchase on iOS, I would expect a ProcessPurchase call upon the next IAP initialization and a receipt, and both are not happening. My ConfirmPendingPurchase logic in my test app depends on the product showing up in that ProcessPurchase so that’s failing too. I have let the team know.