In the sample project v3, where is the right place to write what the product actually does?

So I’ve added the buyPremium product and wrote its function in the public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args) I was wondering if that is right? Also, if the player reinstalls the game, or reopens the game, does the nonconsumable product remain purchased and active?

Please show all your code, but sounds right. You will need to check the receipt to see if it’s active, you won’t get ProcessPurchase each time you start the game.

    private static IStoreController m_StoreController;          // The Unity Purchasing system.
    private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    private static UnityEngine.Purchasing.Product test_product = null;
    private IAppleExtensions m_AppleExtensions;
    private IGooglePlayStoreExtensions m_GoogleExtensions;

    public static string buyPremium = "buy_premium";

    public Text myText;

    public Button btnPremium;

    private Boolean return_complete = true;

    private ConfigurationBuilder builder;

    public Button[] myButtons;
    public GameObject purchaseScreen;

    void Awake()
    {

    }

    void Start()
    {
        // If we haven't set up the Unity Purchasing reference
        if (m_StoreController == null)
        {
            // Begin to configure our connection to Purchasing
            InitializePurchasing();
        }
        MyDebug("In Start...");
    }

    public void MyInitialize()
    {
        InitializePurchasing();
    }

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

        builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());

        builder.AddProduct(buyPremium, ProductType.NonConsumable);

        builder.Configure<IGooglePlayConfiguration>().SetObfuscatedAccountId("test1");

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

        MyDebug("Starting Initialized...");
        UnityPurchasing.Initialize(this, builder);

        //ProductCatalog catalog = ProductCatalog.LoadDefaultCatalog();

        //foreach (ProductCatalogItem product in catalog.allProducts)
        // {
        //MyDebug("Product = " + product.id);
        //}
    }

    void OnDeferredPurchase(Product product)
    {
        MyDebug($"Purchase of {product.definition.id} is deferred");
        btnPremium.enabled = false;

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

    public void BuyNonConsumable()
    {
        BuyProductID(buyPremium);
    }

    public void BuyNoAds()
    {
        //BuyProductID(NO_ADS);
    }

    public void CompletePurchase()
    {
        if (test_product == null)
            MyDebug("Cannot complete purchase, product not initialized.");
        else
        {
            m_StoreController.ConfirmPendingPurchase(test_product);
            //FetchProducts();

            MyDebug("Completed purchase with " + test_product.definition.id.ToString());
        }
    }

    public void FetchProducts()
    {
        var myHashSet = new HashSet<ProductDefinition>();

        ProductDefinition item = new ProductDefinition(buyPremium, ProductType.NonConsumable);

        myHashSet.Add(item);

        m_StoreController.FetchAdditionalProducts(myHashSet, OnProductsFetched, OnInitializeFailed);
    }

    public void ToggleComplete()
    {
        return_complete = !return_complete;
        MyDebug("Complete = " + return_complete.ToString());

    }

    public void ChangeUser()
    {
        builder.Configure<IGooglePlayConfiguration>().SetObfuscatedAccountId("test2");
    }

    public void OnProductsFetched()
    {
        MyDebug("In fetch...");
    }

    public void RestorePurchases()
    {
        m_StoreExtensionProvider.GetExtension<IAppleExtensions>().RestoreTransactions(result => {
            if (result)
           {
               MyDebug("Restore purchases succeeded.");
           }
            else
            {
               MyDebug("Restore purchases failed.");
            }
        });
    }

    public void BuyProductID(string productId)
    {
        if (IsInitialized())
        {
            UnityEngine.Purchasing.Product product = m_StoreController.products.WithID(productId);

            if (product != null && product.availableToPurchase)
            {
                MyDebug(string.Format("Purchasing product:" + product.definition.id.ToString()));
                m_StoreController.InitiatePurchase(product);


            }
            else
            {
                MyDebug("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
            }
        }
        else
        {
            MyDebug("BuyProductID FAIL. Not initialized.");
        }
    }

    public void ListProducts()
    {
        foreach (UnityEngine.Purchasing.Product item in m_StoreController.products.all)
        {
            if (item.receipt != null)
            {
                MyDebug("Receipt found for Product = " + item.definition.id.ToString());
            }
        }
    }
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        MyDebug("OnInitialized: PASS");

        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;
        m_AppleExtensions = extensions.GetExtension<IAppleExtensions>();
        m_GoogleExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();

        Dictionary<string, string> dict = m_AppleExtensions.GetIntroductoryPriceDictionary();

        foreach (UnityEngine.Purchasing.Product item in controller.products.all)
        {
            if (item.receipt != null)
            {
                string intro_json = (dict == null || !dict.ContainsKey(item.definition.storeSpecificId)) ? null : dict[item.definition.storeSpecificId];

                if (item.definition.type == ProductType.Subscription)
                {
                    SubscriptionManager p = new SubscriptionManager(item, null);
                    SubscriptionInfo info = p.getSubscriptionInfo();
                    MyDebug("SubInfo: " + info.getProductId().ToString());
                    MyDebug("getExpireDate: " + info.getExpireDate().ToString());
                    MyDebug("isSubscribed: " + info.isSubscribed().ToString());
                }
            }
        }
    }

    public void OnPurchaseDeferred(Product product)
    {

        MyDebug("Deferred product " + product.definition.id.ToString());
    }

    public void OnInitializeFailed(InitializationFailureReason error)
    {
        // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
        MyDebug("OnInitializeFailed InitializationFailureReason:" + error);
    }


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

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

        // var result = validator.Validate(args.purchasedProduct.receipt);

        // foreach (IPurchaseReceipt productReceipt in result)
        // {
        //     MyDebug("Valid receipt for " + productReceipt.productID.ToString());
        // }

        //MyDebug("Validate = " + result.ToString());



        MyDebug("Receipt:" + test_product.receipt.ToString());

        if (return_complete)
        {
            MyDebug(string.Format("ProcessPurchase: Complete. Product:" + args.purchasedProduct.definition.id + " - " + test_product.transactionID.ToString()));

            SetPremiumState();

            return PurchaseProcessingResult.Complete;
        }
        else
        {
            MyDebug(string.Format("ProcessPurchase: Pending. Product:" + args.purchasedProduct.definition.id + " - " + test_product.transactionID.ToString()));
            return PurchaseProcessingResult.Pending;
        }

    }

    private void SetPremiumState()
    {
        foreach (var button in myButtons)
        {
            button.gameObject.SetActive(false);
        }
        purchaseScreen.SetActive(false);
    }

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

    private void MyDebug(string debug)
    {
        Debug.Log(debug);
        myText.text += "\r\n" + debug;
    }

Also, in the public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args), I can leave the validator and the result commented out?

Perfect! Nicely done. And yes, you can comment out that portion for now. But I believe Apple requires receipt validation when you submit your app, they will try to inject a fake receipt to see if your game detects and rejects it. On Android, there is significant fraud including cheat apps, you definitely want some form of receipt validation to help fight it.

How would you prefer to do it? I was thinking PlayerPrefs. Don’t need any strong data because my game doesn’t really have anything that can put you in advantage if you hack it, premium just unlocks more categories. But then again, if the player uninstalls the game, wont PlayerPreft data go away and the game will ask for purchasing again?

If you want receipt validation, then uncomment out the corresponding code, it will check the receipt. If the user purchases a non-consumable and reinstalls the app, ProcessPurchase will be triggered automatically and you’ll have the receipt in the parameter object. After that, the receipt is stored in the device cache and will be available in the product controller as demonstrated in the Sample code and you can check it in or after OnInitialized is called. That is what the ListProducts method does.

Okay thanks a lot for that. One more question tho, do I have to change anything in the AppleTangle and GooglePlayTangle cs files(the order or the key)?

Yes, you do need to use your own Google Play Store Public key from your Google dashboard https://docs.unity3d.com/Manual/UnityIAPValidatingReceipts.html

Okay I’ve done that. It is giving me this error when i try to purchase the product tho.
NotImplementedException: The method or operation is not implemented.

What line is the error on? That would likely be expected if running in the Editor, it is only meant for the device. You could wrap it in a try/catch block otherwise.

It is on line cs:239, which is var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
Okay, Ill try doing that

I tried testing the app but when I purchase the product nothing happens. Let me get something clear, in GooglePlayTangle.cs, I changed the data to what is required. What about the order and the key?

Hi @SwathedOrange .

Do you mean you changed the data manually? Normally that file is generated using the Obfuscator tool in the Editor.

Yea I can’t find the tool, it’s not under Services > In-App Purchasing > IAP Receipt Validation Obfuscator for me for some reason.

It should be in your top menus, not the Project Settings.

Which IAP package version are you using? If you don’t see this it might be under “Unity IAP” instead of “Services”

O wow I can’t believe it. I’ve been literally searching for hours, online too before i asked here only to find out now its been there this whole time. Didn’t know I was THAT blind. Thanks a lot for that image, it actually helped, wow

Don’t feel so bad. We’ve done work to make this available in the Project Settings as well for easier discovery, but unfortunately we can only add this control in Unity 2022 and later. If you’re still on 2021 or older, we can’t control the project settings from the package and have to publish them independently to the editor, which is why that update isn’t present there.

Ive managed to get it all working nicely, thanks for all the help, you guys are great!

1 Like

Sorry to bother you again, but I’ve stumbled on a problem. When purchased it all works as it should, the problem appears when the user re-enters the app. The content becomes locked again

So how are you unlocking the content? Please share your code. In the Sample project, you can look at the ListProducts method and see if the product has a receipt.