iOS app rejected IAP issue

Guideline 2.1 - Performance - App Completeness

We found that your in-app purchase products exhibited one or more bugs which create a poor user experience. Specifically, an error message still displayed when we tried to purchase an item.

Review device details:

  • Device type: iPad

  • OS version: iOS 16.3

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.


Question about payment issue on Sandbox

We have an ongoing problem with payment on Sandbox account (It works fine on Testflight)

Here are repro steps:

  1. Download app from Testflight
  2. Proceed with payment on Testflight - everything is fine
  3. Log out from Testflight, and Log in with Sandbox account
  4. First payment goes through, but when trying to buy the same product for the second time, it says the product needs to be restored
  5. Seems like for the second purchase, it is referring the receipt to the first payment, and does not allow purchase to be made - even though the product is registered as consummable
  6. The payment does no longer proceed at at below state
  • UnityIAP: UpdataedTransactions
  • UnityIAP: Finishing transaction ###################

Here is our source code - could you tell us how we can break this through?

private IStoreController storeController;
private IExtensionProvider extensionProvider;
private IAppleExtensions m_AppleExtensions;

public bool IsInitialized => storeController != null && extensionProvider != null;

private List<GateAPI_Frame.InfoResponse.Product> _ProductList;

public void SetProduct(List<GateAPI_Frame.InfoResponse.Product> productList)
{
_ProductList = productList;
}

public void InitIAP()
{
IsToastMsg = false;
CDebug.Log(“-----InitUnityIAP-----”);
if (IsInitialized)
return;
;
if (_ProductList == null)
{
CDebug.LogError(“-----Error : _ProductList”);
return;
}

ConfigurationBuilder builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
if (builder == null)
{
CDebug.Log(“-----Builder is NULL”);
return;
}

foreach (var tableData in _ProductList)
{
IDs ids = new IDs()
{
#if UNITY_ANDROID
{tableData.storeCode, GooglePlay.Name},
#elif UNITY_IOS
{tableData.storeCode, AppleAppStore.Name},
#endif
};

builder.AddProduct(tableData.productId, ProductType.Consumable, ids);
}

UnityPurchasing.Initialize(this, builder);
}

public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
CDebug.Log(“OnInitialized IAP: PASS”, CDebugTag.SHOP);

storeController = controller;
extensionProvider = extensions;
m_AppleExtensions = extensions.GetExtension();
}

public void OnInitializeFailed(InitializationFailureReason error)
{
CDebug.LogError($“----- Unity IAP InitializeFailed {error}”, CDebugTag.SHOP);

if (!IsInitialized)
InitIAP();
}

public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
ShowLoadingProgress(false);

Toast.ShowToastWithStringID(9256000041, Msg.MSG_TYPE.Notice); // Inapp Error

CDebug.Log(string.Format(“OnPurchaseFailed: FAIL. Product: ‘{0}’, PurchaseFailureReason: {1}”, product.definition.storeSpecificId, failureReason));
}

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
Product purchaseProduct = args.purchasedProduct;

CDebug.Log($“ProcessPurchase product id :{purchaseProduct.definition.id} - type : {purchaseProduct.definition.type} - storeSpecificId : {purchaseProduct.definition.storeSpecificId}”, CDebugTag.SHOP);

long shopItemGdid = 0;
var ShopTableData = ShopDataManager.Instance.GetShopProductDataDic();
if (ShopTableData == null)
{
CDebug.LogError(“-----Error : GetShopProductDataDic”, CDebugTag.SHOP);
}
else
{
shopItemGdid = ShopDataManager.Instance.GetShopProductDataDic().Values.Where(x => x.StoreCode == purchaseProduct.definition.id).FirstOrDefault().ID;
CDebug.Log($" shopItemGdid : {shopItemGdid}", CDebugTag.SHOP);
}

StartCoroutine(BuyIAP(shopItemGdid, purchaseProduct, IsShowToast()));

return PurchaseProcessingResult.Pending;
}

IEnumerator BuyIAP(long gdid, Product product, bool isShowToast)
{
yield return new WaitForSeconds(1f);

CDebug.Log($“BuyIAP product id :{product.definition.id} - type : {product.definition.type} - storeSpecificId : {product.definition.storeSpecificId}”, CDebugTag.SHOP);

string purchase = JsonConvert.SerializeObject(product);
purchase = Uri.EscapeUriString(purchase);

APIHelper.Shop.BuyIAP(gdid, purchase).Subscribe(res =>
{
if (res != null)
{
if (res.ShopInfoList != null)
{
if( res.ShopInfoList.Count > 0 )
ConfirmPendingPurchase(res, product, isShowToast);
else
{
Toast.ShowToastWithStringID(9256000040, Msg.MSG_TYPE.Notice);
InAppConfirmPending(product);
APIHelper.Shop.ShopLog($“5 Shop IAP Buy Before Payment”).Subscribe();
}
}
else
{
Toast.ShowToastWithStringID(9256000037, Msg.MSG_TYPE.Notice);
}

}
else
{
ShowLoadingProgress(false);
}
});
}

private void InAppConfirmPending(Product product)
{
ShowLoadingProgress(false);

if (product != null)
{
storeController.ConfirmPendingPurchase(product);
CDebug.Log(“storeController.ConfirmPendingPurchase”, CDebugTag.SHOP);
}
}

private void ConfirmPendingPurchase(ShopBuyIAPResponesData res, Product product, bool isShowToast)
{
ShowLoadingProgress(false);

CDebug.Log($“ConfirmPendingPurchase product id :{product.definition.id} - type : {product.definition.type} - storeSpecificId : {product.definition.storeSpecificId}”, CDebugTag.SHOP);

if (product != null)
{
storeController.ConfirmPendingPurchase(product);
CDebug.Log(“storeController.ConfirmPendingPurchase”, CDebugTag.SHOP);
}

RefreshShop();
}

Hello,

Which version of IAP are you using? If you are using an older version, I would recommend updating to see if this fixes your issue.

From your code, it seems there’s no receipt validation being done. If this is the case, you might need to implement one:

Unity 2021.3.13f1 , In App Purchasing 4.5.2
In use.

Receipt validation on the server is being processed without a problem.

storeController.ConfirmPendingPurchase(product);

Might not be complete?

  • UnityIAP: Finishing transaction

Could Apple not respond?

The ConfirmPendingPurchase ends up calling the finishTransaction, but if it fails, there will be traces in the logs indicating why.
For the finishTransaction, this can fail if there’s a communication issue with Apple, but it should be fixed by restoring transactions. This should be very rare, so it shouldn’t be the issue here.

For the message from Apple: “Specifically, an error message still displayed when we tried to purchase an item.”
Do they mention what the error message is?

Would you be able to provide the logs when you try to do the repro steps from above?

I’ve got the exact same issue:

Unity 2021.3.13f1 , In App Purchasing 4.5.2

Sometimes, you would just submit to Apple and ask then to test again. If the tester does not run into any issues while doing a test purchase (no errors or exceptions), there is a chance it passes review. Provided you did test on a real device yourself successfully.

This is because the rejection message is a somewhat generic text. I have seen it from my customers often too and then it’s just gone. Certainly if you do not use server side validation right now, you do not have to implement it, as opposed to what the message from Apple says.

In other cases, some users just implemented local validation and Apple then accepted the submission. Again, not that it would make any difference - the rejection text just means the Apple tester was not able to fully test purchasing a product in your app.

I finished transaction 2000000258118452, but when I pay again, 2000000258118452 duplicate transaction occurs. I tried to complete the duplication with storeController.ConfirmPendingPurchase, but it says it’s not pending. When it finishes, a new transaction should occur, but only the existing transaction information that has already ended is passed over.

8786560--1193506--log.jpg

What kind of product is com.takeonecompany.bptg1.goods.dia136?
Are you initiating the second purchase or it’s happening automatically?

It seems like you are purchasing something you already own and Apple is sending a transaction with the same transaction ID that has already been completed. In this case, it’s considered a Duplicate Transaction since this exact transaction was already processed before. Transaction IDs come from Apple.

Was this the error reported by Apple or is this another issue?

This is the second purchase. Apple communicated it to Apple as a problem, but it continues to be rejected. Is there any way to solve it?

What kind of issues will there be if [PurchaseFailureReason.Unknown] occurs during the error?

In your code, when you do ConfirmPendingPurchase, there’s a call to RefreshShop, what does this do?

How are you getting “PurchaseFailureReason.Unknown”, could you provide your full logs in text format with this?

Have you tried to resubmit your application as suggested by Baroni?