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:
- Download app from Testflight
- Proceed with payment on Testflight - everything is fine
- Log out from Testflight, and Log in with Sandbox account
- First payment goes through, but when trying to buy the same product for the second time, it says the product needs to be restored
- 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
- 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();
}
