App rejected from apple app store - Guideline 2.1 - Performance - App Completeness

I sent my game to the app store and got the response:

When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production,” you should validate against the test environment instead.

I don’t understand what they want me to do?
I am validating the receipt in the IAP and the purchase works fine in the TestFlights, has anyone stumble on this issue? what did you do?

A sample for my code:

public class IAPManagerScript : MonoBehaviour, IStoreListener
{

    public Text message;
    public static IAPManagerScript instance;

    private static IStoreController m_StoreController;
    private static IExtensionProvider m_StoreExtensionProvider;

    //Step 1 create your products
    private string buyProduct = "my id for the iap";


    //************************** Adjust these methods **************************************
    public void InitializePurchasing()
    {
        if (IsInitialized()) { return; }
        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

        //Step 2 choose if your product is a consumable or non consumable

        builder.AddProduct(buyProduct,ProductType.Consumable);
        UnityPurchasing.Initialize(this, builder);
    }


    private bool IsInitialized()
    {
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }


    //Step 3 Create methods
    public void buyPoructMethod()
    {
        BuyProductID(buyProduct);
       
    }
    public static void SavePurchase(bool purchased,string path)
    {
        BinaryFormatter bf = new BinaryFormatter();
        //Application.persistentDataPath is a string, so if you wanted you can put that into debug.log if you want to know where save games are located
        FileStream file = File.Create(Application.persistentDataPath + path); //you can call it anything you want
        bf.Serialize(file, purchased);
        file.Close();
    }


  
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
    {
        bool validPurchase = true; // Presume valid for platforms with no R.V.

        // Unity IAP's validation logic is only included on these platforms.
#if UNITY_ANDROID || UNITY_IOS || UNITY_STANDALONE_OSX
        // Prepare the validator with the secrets we prepared in the Editor
        // obfuscation window.
        var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
            AppleTangle.Data(), Application.identifier);

        try
        {
            // On Google Play, result has a single product ID.
            // On Apple stores, receipts contain multiple products.
            var result = validator.Validate(e.purchasedProduct.receipt);
            // For informational purposes, we list the receipt(s)
            Debug.Log("Receipt is valid. Contents:");
            foreach (IPurchaseReceipt productReceipt in result)
            {
                Debug.Log(productReceipt.productID);
                Debug.Log(productReceipt.purchaseDate);
                Debug.Log(productReceipt.transactionID);
            }
        }
        catch (IAPSecurityException)
        {
            Debug.Log("Invalid receipt, not unlocking content");
            validPurchase = false;
          //  message.text = message.text + "Failed to Purchase \n";

            Debug.Log("Purchase Failed");
        }
#endif

        if (validPurchase)
        {

            SceneManager.LoadScene("InfoScene");


        }


        return PurchaseProcessingResult.Complete;
    }

   









    //**************************** Dont worry about these methods ***********************************
    private void Awake()
    {
        TestSingleton();
    }

    void Start()
    {
        if (m_StoreController == null) { InitializePurchasing(); }
    }

    private void TestSingleton()
    {
        if (instance != null) { Destroy(gameObject); return; }
        instance = this;
        DontDestroyOnLoad(gameObject);
    }

    void BuyProductID(string productId)
    {
        if (IsInitialized())
        {
            Product product = m_StoreController.products.WithID(productId);
            if (product != null && product.availableToPurchase)
            {
                Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
                m_StoreController.InitiatePurchase(product);
            }
            else
            {
                Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
            }
        }
        else
        {
            Debug.Log("BuyProductID FAIL. Not initialized.");
        }
    }

    public void RestorePurchases()
    {
        if (!IsInitialized())
        {
            Debug.Log("RestorePurchases FAIL. Not initialized.");
            return;
        }

        if (Application.platform == RuntimePlatform.IPhonePlayer ||
            Application.platform == RuntimePlatform.OSXPlayer)
        {
            Debug.Log("RestorePurchases started ...");

            var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
            apple.RestoreTransactions((result) => {
                Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
            });
        }
        else
        {
            Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
        }
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        Debug.Log("OnInitialized: PASS");
        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;
    }


    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    }

    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    {
        Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    }

@orizvida You’ll likely also want to check the value of “result” from the validator call. Otherwise your code looks correct. You as the developer shouldn’t have to do anything with regard to validating against their sandbox vs production. I suspect the tester in this case was mistaken. You might try again.

Hey Jeff,
Thank you for the reply.
I resubmitted my game to the apple store with a video showing on my iPhone that the purchase works yet still got rejected with this message:

Guideline 2.1 - Performance - App Completeness

We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 14.3 on Wi-Fi.

Specifically, we were unable to complete the IAP.

Steps to reproduce:

  1. Tap on Purchase now $10.99

  2. Tap on Cost(Sandbox pop up) - tap on

  3. Tap login (Sandbox - pop up window)

  4. Back to Purchase now page

I don’t understand what else should I do?
Sometimes when trying to purchase with sandbox it does the purchase but then loops back into the login, could that be the case? if so what should I do about it?

Sorry I don’t follow the issue they are having. Does it work in your testing in TestFlight? I and others have seen multiple password prompts on Sandbox which is an Apple issue. It seems to clear up in time.

It does work in my Test Flight, but they insist that it does not work, I appealed their rejection and they wrote that the rejection was valid.
I can’t fix something that is already working, how can I get them to approve it?

You can’t make them approve it. I suspect they may be having issues in their test environment again, perhaps wait a couple of days and try again. So you’ve had two rejection reasons, one on the receipt, and now this purchase failure?

Yes, Though I think that both rejections are for the same reason…

The first was “Sandbox receipt used in production, you should validate against the test environment instead.” and second “Specifically, we were unable to complete the IAP.” You mentioned you submitted twice? Was the rejection reason the same both times?

The First Rejection was:
Guideline 2.1 - Performance - App Completeness

We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 14.3 on Wi-Fi.

Specifically, we were unable to complete IAP transaction.

Next Steps

When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production,” you should validate against the test environment instead.

Got it, so they were just including boilerplate text. They are not able to make purchases in the Sandbox when it is working in your TestFlight tests. That points to an issue in the Apple test environment, as mentioned. Give it more time.

Add your apple id to TestFlight, accept and download your app via TestFlight. After download is done, use Xcode and re-build app on iphone ( that use your apple id). Try purchase and view log in Xcode.

Hey,
didn’t fully understand,
build and upload the binary to TestFlight and then open Xcode and build via USB to the iPhone?
Also, for the sandbox, i am using a different mail than my apple id so it won’t charge me.

Hey,
I ran the build on my iPhone and got the logs from the start of the IAP until the end (as i mentioned before, it did work and again had no issues)
Anyway, I am putting here the logs if you see anything that is not right…
Thanks!

PurchaseButtonScript:Start()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

UnityIAP Version: 2.2.5

UnityEngine.Purchasing.StandardPurchasingModule:Instance(AppStore)

IAPManagerScript:InitializePurchasing()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

2021-01-26 10:48:24.870100+0200 MyFriendGames[374:13017] UnityIAP: Requesting 1 products

2021-01-26 10:48:24.874993+0200 MyFriendGames[374:13017] UnityIAP: Requesting product data…

2021-01-26 10:48:26.249805+0200 MyFriendGames[374:13272] UnityIAP: Received 1 products

2021-01-26 10:48:26.266757+0200 MyFriendGames[374:13272] UnityIAP: No App Receipt found

2021-01-26 10:48:26.278371+0200 MyFriendGames[374:13017] UnityIAP: No App Receipt found

OnInitialized: PASS

IAPManagerScript:OnInitialized(IStoreController, IExtensionProvider)

UnityEngine.Purchasing.PurchasingManager:CheckForInitialization()

UnityEngine.Purchasing.PurchasingManager:OnProductsRetrieved(List`1)

UnityEngine.Purchasing.AppleStoreImpl:OnProductsRetrieved(String)

UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

UnityIAP: Initialization complete with 1 items

UnityEngine.Purchasing.Promo:ProvideProductsToAds(HashSet`1, Boolean)

UnityEngine.Purchasing.AppleStoreImpl:OnProductsRetrieved(String)

UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

2021-01-26 10:48:26.282076+0200 MyFriendGames[374:13017] UnityIAP: Add transaction observer

2021-01-26 10:48:26.282360+0200 MyFriendGames[374:13017] UnityIAP UnityEarlyTransactionObserver: Request to initiate queued payments

wenttobuy

PurchaseButtonScript:Purchase()

UnityEngine.Events.UnityEvent:Invoke()

UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)

UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchPress(PointerEventData, Boolean, Boolean)

UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchEvents()

UnityEngine.EventSystems.StandaloneInputModule:Process()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

Purchasing product asychronously: ‘com.friendgames.purchase’

IAPManagerScript:BuyProductID(String)

UnityEngine.Events.UnityEvent:Invoke()

UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)

UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchPress(PointerEventData, Boolean, Boolean)

UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchEvents()

UnityEngine.EventSystems.StandaloneInputModule:Process()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

2021-01-26 10:48:39.646268+0200 MyFriendGames[374:13017] UnityIAP: PurchaseProduct: com.friendgames.purchase

purchase({0}): com.friendgames.purchase

UnityEngine.Events.UnityEvent:Invoke()

UnityEngine.EventSystems.ExecuteEvents:Execute(GameObject, BaseEventData, EventFunction`1)

UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchPress(PointerEventData, Boolean, Boolean)

UnityEngine.EventSystems.StandaloneInputModule:ProcessTouchEvents()

UnityEngine.EventSystems.StandaloneInputModule:Process()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

2021-01-26 10:48:39.682427+0200 MyFriendGames[374:13017] UnityIAP: UpdatedTransactions

→ applicationWillResignActive()

→ applicationDidBecomeActive()

→ applicationWillResignActive()

→ applicationDidBecomeActive()

2021-01-26 10:49:10.782910+0200 MyFriendGames[374:13017] UnityIAP: UpdatedTransactions

Receipt is valid. Contents:

IAPManagerScript:ProcessPurchase(PurchaseEventArgs)

UnityEngine.Purchasing.PurchasingManager:ProcessPurchaseIfNew(Product)

UnityEngine.Purchasing.JSONStore:OnPurchaseSucceeded(String, String, String)

UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

com.friendgames.purchase

IAPManagerScript:ProcessPurchase(PurchaseEventArgs)

UnityEngine.Purchasing.PurchasingManager:ProcessPurchaseIfNew(Product)

UnityEngine.Purchasing.JSONStore:OnPurchaseSucceeded(String, String, String)

UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

01/26/2021 08:49:08

IAPManagerScript:ProcessPurchase(PurchaseEventArgs)

UnityEngine.Purchasing.PurchasingManager:ProcessPurchaseIfNew(Product)

UnityEngine.Purchasing.JSONStore:OnPurchaseSucceeded(String, String, String)

UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

1000000769779110

IAPManagerScript:ProcessPurchase(PurchaseEventArgs)

UnityEngine.Purchasing.PurchasingManager:ProcessPurchaseIfNew(Product)

UnityEngine.Purchasing.JSONStore:OnPurchaseSucceeded(String, String, String)

UnityEngine.Purchasing.Extension.UnityUtil:Update()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

2021-01-26 10:49:11.018026+0200 MyFriendGames[374:13017] UnityIAP: Finishing transaction 1000000769779110

Unloading 1 Unused Serialized files (Serialized files now loaded: 0)

UnloadTime: 0.595667 ms

Unloading 4 unused Assets to reduce memory usage. Loaded Objects now: 821.

Total: 1.342250 ms (FindLiveObjects: 0.159375 ms CreateObjectMapping: 0.013750 ms MarkObjects: 1.145667 ms DeleteObjects: 0.023125 ms)

uploadede 0

PurchaseButtonScript:Start()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

Uploading Crash Report

NullReferenceException: Object reference not set to an instance of an object.

at PurchaseButtonScript.Start () [0x00000] in <00000000000000000000000000000000>:0

(Filename: currently not available on il2cpp Line: -1)

Uploading Crash Report

NullReferenceException: Object reference not set to an instance of an object.

at uniqueSerialScript.Start () [0x00000] in <00000000000000000000000000000000>:0

(Filename: currently not available on il2cpp Line: -1)

pdf’s MIME/UTI is:

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

in enumarator

d__40:MoveNext()

UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)

uniqueSerialScript:Start()

(Filename: ./Runtime/Export/Debug.bindings.h Line: 45)

I found this errors on your debug. Please review PurchaseButtonScript and uniqueSerialScript. That is the cause of the error.

Yep, Just noticed it,
fixed it now but it shouldn’t be the issue though because this was an error that accrued only in the next scene after the purchase was made.

The logs look correct, appears to be an Apple issue to me. This happens occasionally.

I think problem is here. When you call : SceneManager.LoadScene(“InfoScene”); you got an error ( that I mentioned at previous reply), so unity can’t call return PurchaseProcessingResult.Complete; and IAP processing is interrupted here.
Please show me your build settings for Script Call Optimization

6770131--782578--Screen Shot 2021-01-27 at 4.02.41 PM.png

Hey,
First of all thank you for your help!
I fixed the errors from the log and resubmitted, still I don’t think that it was the issue because when testing on devices it worked but couldn’t hurt trying.

Here are the build settings for Script Call Optimization
6770257--782602--upload_2021-1-27_11-47-43.png

Got rejected again for the same reason.
I am so frustrated about this how can I fix an issue that is only reproduced in the apple review environment…

I suspect you don’t want to load a new scene right in ProcessPurchase. Put the code outside of that method.