We've detected that your app is not acknowledging all in-app purchases or subscriptions.

Today I got this notification from Google Play Store:

We’ve detected that your app is not acknowledging all in-app purchases or subscriptions. Starting with Billing Library 2.0, all purchases must be acknowledged within 3 days or they will be automatically refunded

I have no idea what this means. Is this related to deferred purchases? My game detects pending transactions and handles them. What am I missing?

Using Unity 2019.4.3f1 and Unity IAP 4.1.4.

We’ve heard reports of this when using Codeless, can you confirm if you are using Codeless IAP? Otherwise, please share your ProcessPurchase code. You might compare to the Sample IAP Project v3 here https://discussions.unity.com/t/700293/4

Here’s my code:

using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
using IAPSetting = GameSettings.InAppPurchase;
using ExtinctionGame;

public enum IAPProduct {
    None = -1,
    BasicCashPack = 0,
    MediumCashPack = 1,
    LargeCashPack = 2,
    HugeCashPack = 3,
    MegaCashPack = 4,
}

public class IAPManager : GlobalSingleton<IAPManager>, IStoreListener {

    // The Unity Purchasing system  
    private static IStoreController m_StoreController;
    // The store-specific Purchasing subsystems  
    private static IExtensionProvider m_StoreExtensionProvider;

    private static bool m_PreviousPurchaseInProgress = false;

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

    protected override void OnInitialize() {
        base.OnInitialize();

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

        ApplicationEventManager.OnIAPPurchaseSuccess.AddListener(HandleIAPPurchaseSuccess);
        ApplicationEventManager.OnIAPPurchaseFailure.AddListener(HandleIAPPurchaseFailure);
    }

    private static string GetProductId(IAPProduct product) {
        switch (product) {
            case IAPProduct.BasicCashPack:
                return "basic_cash_pack";
            case IAPProduct.MediumCashPack:
                return "medium_cash_pack";
            case IAPProduct.LargeCashPack:
                return "large_cash_pack";
            case IAPProduct.HugeCashPack:
                return "huge_cash_pack";
            case IAPProduct.MegaCashPack:
                return "mega_cash_pack";

            default:
                throw new System.Exception("Invalid IAPProduct: " + product.ToString());
        }
    }

    private static IAPProduct GetIAPProductByProductId(string productId) {
        if (productId.Equals("basic_cash_pack")){
            return IAPProduct.BasicCashPack;
        }
        else if (productId.Equals("medium_cash_pack")){
            return IAPProduct.MediumCashPack;
        }
        else if (productId.Equals("large_cash_pack")){
            return IAPProduct.LargeCashPack;
        }
        else if (productId.Equals("huge_cash_pack")){
            return IAPProduct.HugeCashPack;
        }
        else if (productId.Equals("mega_cash_pack")){
            return IAPProduct.MegaCashPack;
        }

        return IAPProduct.None;
    }

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

    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.ToString());
      
        ApplicationEventManager.OnIAPPurchaseFailure.Invoke(
            GetIAPProductByProductId(product.definition.id),
            failureReason.ToString()
        );
    }

    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;

            // Shop
            if (productId == GetProductId(IAPProduct.BasicCashPack)) {
                savedData.IAPSmallCashPackPurchased += 1;
                savedData.AddCash(TransactionType.InAppPurchase, IAPSetting.BasicCashPackAmount + IAPSetting.BasicCashPackBonusAmount);
            }
            else if (productId == GetProductId(IAPProduct.MediumCashPack)) {
                savedData.IAPMediumCashPackPurchased += 1;
                savedData.AddCash(TransactionType.InAppPurchase, IAPSetting.MediumCashPackAmount + IAPSetting.MediumCashPackBonusAmount);
            }
            else if (productId == GetProductId(IAPProduct.LargeCashPack)) {
                savedData.IAPLargeCashPackPurchased += 1;
                savedData.AddCash(TransactionType.InAppPurchase, IAPSetting.LargeCashPackAmount + IAPSetting.LargeCashPackBonusAmount);
            }
            else if (productId == GetProductId(IAPProduct.HugeCashPack)) {
                savedData.IAPHugeCashPackPurchased += 1;
                savedData.AddCash(TransactionType.InAppPurchase, IAPSetting.HugeCashPackAmount + IAPSetting.HugeCashPackBonusAmount);
            }
            else if (productId == GetProductId(IAPProduct.MegaCashPack)) {
                savedData.IAPMegaCashPackPurchased += 1;
                savedData.AddCash(TransactionType.InAppPurchase, IAPSetting.MegaCashPackAmount + IAPSetting.MegaCashPackBonusAmount);
            }

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

        ApplicationEventManager.OnIAPPurchaseSuccess.Invoke(GetIAPProductByProductId(productId));

        return PurchaseProcessingResult.Complete;
    }

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

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        builder.AddProduct(GetProductId(IAPProduct.BasicCashPack), ProductType.Consumable);
        builder.AddProduct(GetProductId(IAPProduct.MediumCashPack), ProductType.Consumable);
        builder.AddProduct(GetProductId(IAPProduct.LargeCashPack), ProductType.Consumable);
        builder.AddProduct(GetProductId(IAPProduct.HugeCashPack), ProductType.Consumable);
        builder.AddProduct(GetProductId(IAPProduct.MegaCashPack), ProductType.Consumable);

        builder.Configure<IGooglePlayConfiguration>().SetDeferredPurchaseListener(HandleDeferredPurchase);

        UnityPurchasing.Initialize(this, builder);
    }

    void HandleDeferredPurchase(Product product) {
        Dialog.Alert(LanguageManager.Data.Payment.PendingTransaction);
    }

    public static void Purchase(IAPProduct product) {
        if (!Initialized) {
            Debug.Log("Failed to buy product. Not initialized.");
            ApplicationEventManager.OnIAPPurchaseFailure.Invoke(product, PurchaseFailureReason.PurchasingUnavailable.ToString());
        }
        else if (m_PreviousPurchaseInProgress) {
            Debug.Log("Failed to buy product. Previous Purchase in progress.");
            ApplicationEventManager.OnIAPPurchaseFailure.Invoke(product, PurchaseFailureReason.PurchasingUnavailable.ToString());
        }
        else {
            m_PreviousPurchaseInProgress = true;

            Product storeProduct = m_StoreController.products.WithID(GetProductId(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 HandleIAPPurchaseSuccess(IAPProduct product) {
        Debug.Log("Purchase Successful: " + product.ToString());
        AudioManager.PlayPurchasedSound();

        Dialog.Alert(LanguageManager.Data.Payment.PurchaseSuccess);
    }


    void HandleIAPPurchaseFailure(IAPProduct product, string reason) {
        Debug.Log("Failed to purchase: " + reason);

        Dialog.Alert(string.Format(
            "{0}\n({1})",
            LanguageManager.Data.Payment.PurchaseFailure,
            reason
        ));

        AudioManager.PlayErrorSound();
    }
}
using UnityEngine;

public abstract class GlobalSingleton<T> : MonoBehaviour where T : GlobalSingleton<T> {
    private static T m_Instance = null;
    public static T Instance {
        get {
            if (m_Instance == null) {
                Initialize();
            }

            return m_Instance;
        }
    }

    void Awake() {
        if (m_Instance != null && m_Instance != this) {
            Destroy(gameObject);
        }
      
        Initialize();
    }

    protected virtual void OnInitialize() {}

    public static void Initialize() {
        if (m_Instance != null) {
            return;
        }

        m_Instance = FindObjectOfType<T>();

        if (m_Instance == null) {
            throw new System.Exception("Failed to get instance of type " + typeof(T).Name);
        }

        m_Instance.transform.parent = null;
        DontDestroyOnLoad(m_Instance.gameObject);

        m_Instance.OnInitialize();
    }
}

@modernator24 Your code looks ok, although you are wiring up your own listeners which is not necessary and may be leading to your issue. You are properly returning Complete from ProcessPurchase, can you place a Debug.Log statement just prior to that line to confirm that the code is indeed executing as expected? Debug.Log statements will show in the adb logcat logs. You could compare to the Sample IAP Project v3 here, I haven’t received any such notice. Your code is fairly similar except for your listeners, Unity IAP already provides the listeners Sample IAP Project

I’m not sure where to put Debug.Log statements. The in-app purchase works without any problem, and also deferred purchase works. I never saw any trouble with this code. And also I’m using the exact same code for my different game, which has a way much higher income than this game but has no notification from Google.

Looks like Google has misjudged something. The notification is just gone.

Glad it’s working! I had meant to place a Debug.Log statement just prior to returning Complete in ProcessPurchase to confirm that portion of the code is indeed executing as expected, that is what acknowledges the purchase