Emergency help! Google disabled the payment method because don't integrate pending transactions.

Just received a notification from Google that they disabled the payment method in my game because it doesn’t integrate pending transactions properly.

Here’s the message I got:

We’ve turned off some payment methods

Apr 23, 2022 03:06
We’ve noticed that you don’t always acknowledge the delivery of in-app content to users after they’ve paid outside your app. Or you don’t integrate pending transactions correctly. As a result, payment methods that require pending transactions, like paying with cash, have been turned off. Other payment methods aren’t affected. Contact us once you’ve fixed the issue so we can turn these payment methods back on.

I just imported Unity IAP and follow the codes in the documentation. Here’s my code:

using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;

public delegate void PurchaseSuccessCallback(string data);
public delegate void PurchaseFailureCallback(string product, string reason);

public class IAPManager : MonoBehaviour, IStoreListener {
    private static IAPManager m_Instance;
    public static IAPManager Instance => m_Instance;

    // The Unity Purchasing system   
    private static IStoreController m_StoreController;
    // The store-specific Purchasing subsystems   
    private static IExtensionProvider m_StoreExtensionProvider;
    private static PurchaseSuccessCallback m_SuccessCallback;
    private static PurchaseFailureCallback m_FailureCallback;
    private static bool m_PreviousPurchaseInProgress = false;
    private static string m_PurchasingProduct;

    public static bool Initialized => m_StoreController != null && m_StoreExtensionProvider != null;

    public static void Purchase(string product, PurchaseSuccessCallback onSuccess, PurchaseFailureCallback onFailed) {
        m_PurchasingProduct = product;

        if (!Initialized) {
            Debug.Log("Failed to buy product. Not initialized.");
            onFailed(product, PurchaseFailureReason.PurchasingUnavailable.ToString());
        }
        else if (m_PreviousPurchaseInProgress) {
            Debug.Log("Failed to buy product. Previous Purchase in progress.");
            onFailed(product, PurchaseFailureReason.PurchasingUnavailable.ToString());
        }
        else {
            m_PreviousPurchaseInProgress = true;

            m_SuccessCallback = onSuccess;
            m_FailureCallback = onFailed;

            Product storeProduct = m_StoreController.products.WithID(product);

            if (storeProduct != null && storeProduct.availableToPurchase) {
                Debug.Log("Purchasing product async: " + storeProduct.definition.id);
                m_StoreController.InitiatePurchase(storeProduct);
            }
            else {
                Debug.Log("Failed to buy product.");
            }
        }
    }

    void Awake() {
        if (m_Instance) {
            Destroy(gameObject);
            return;
        }

        m_Instance = this;
        DontDestroyOnLoad(this);

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

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {
        Debug.Log("IAP Initialized.");
        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;

        foreach (var product in m_StoreController.products.all) {
            m_StoreController.ConfirmPendingPurchase(product);
        }
    }

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

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

        Debug.Log("Failed to purchase product: " + failureReason);
        m_FailureCallback(m_PurchasingProduct, failureReason.ToString());
        m_FailureCallback = null;
        m_PurchasingProduct = null;
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) {
        m_PreviousPurchaseInProgress = false;
       
        string productId = args.purchasedProduct.definition.id;

        // Receipt Validation
        var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);

        try {
            IPurchaseReceipt[] result = validator.Validate(args.purchasedProduct.receipt);
            Debug.Log("Receipt is valid. Contents:");

            foreach (IPurchaseReceipt productReceipt in result) {
                Debug.Log(productReceipt.productID);
                Debug.Log(productReceipt.purchaseDate);
                Debug.Log(productReceipt.transactionID);
            }

            PlayerData savedData = PlayerDataManager.SavedData;

            if (productId == "SmallCashPack") {
                savedData.cash += 1000;
            }
            ...

            PlayerDataManager.Instance.Save();
        }
        catch (IAPSecurityException) {
            Debug.Log("Invalid receipt, not unlocking content.");
        }

        if (m_SuccessCallback != null) {
            m_SuccessCallback(productId);
            m_SuccessCallback = null;
        }
       
        m_PurchasingProduct = IAPProduct.None;

        return PurchaseProcessingResult.Complete;
    }

    public void InitializePurchasing() {
        if (Initialized) {
            return;
        }

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        builder.AddProduct(GetProductId(IAPProduct.SmallCashPack), ProductType.Consumable);
        ...

        UnityPurchasing.Initialize(this, builder);
    }
}

Now all user payments are blocked and no more in-app purchases are available. It’s causing a serious impact on my company’s sales. What am I missing in my in-app purchase logic? How do I integrate pending transactions properly?

The answer would be in a similar thread just a few entries below yours.

After a whole day of research and attempting to fix it, I figured out that Google calls it “Pending Transaction” and Unity calls it “Deferred Purchase”.

Note that I used 2.2.2 but updated it to 4.1.4, but nothing changed.

The main problem is that when I do a deferred purchase, the “ProcessPurchase” callback only invokes when the game is still running. If I exit the game before the approval of the deferred purchase, restarting a game never called ProcessPurchase callback and the product is never delivered to the customer.

Afaik, if there are any pending purchases, Unity IAP calls ProcessPurchase in the initialization process, but why is not working in my case?

Currently you are not handling deferred states in your ProcessPurchase method at all but always return PurchaseProcessingResult.Complete. You need to return pending in these cases. See the thread I linked above.