[Closed] How to Integrate the new IAP System [TUTORIAL]

I’ve hit a lot of road blocks trying to get the new IAP system working for Android. In the past we just used a reasonably priced plugin with well documented functions. From start to finish it would take less than two hours to do. With the release of the Unity In-App Purchase system (IAP), we’ve switched to a set of docs which are still being written; therefor, we’ve been lacking a bit of necessary information. Now that everything is up and running I would like to share a quick tutorial on how to write it into your project. I don’t have a lot of time to gather code samples for everything, but we will import the examples during the process. It is worth noting that there are a few differences between Android and other platforms; however, I will do my best to include code that will work on both Apple and Android. Okay, let’s get started!
First we need to open the Services Panel inside Unity. This screen can be enabled under the Windows tab.

Once here you will be given an array of services to work with. By default everything should be disabled. Click on the IAP option to move on to the following screen.

Here we are going to do two things. First, enable the IAP system by toggling the button in the top right (blue is enabled). Once it’s enabled, Unity Analytics may turn itself on as well, this is because the two systems work together. Next, click the Import button to load some rather useful files into the project. You may want to take a moment and explore the example code that is imported with the IAP system.

Now we will modify the example logic just a bit. Make a new C# script to store our logic (mine is called IAP). The entire script I use is below.
Full Script

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;

    // Deriving the Purchaser class from IStoreListener enables it to receive messages from Unity Purchasing.
    public class IAP : MonoBehaviour, IStoreListener
    {
        private static IStoreController m_StoreController; // Reference to the Purchasing system.
        private static IExtensionProvider m_StoreExtensionProvider; // Reference to store-specific Purchasing

        private static string kItem = "items_all"; // General handle for the consumable product.

        private static string kGooglePlayItems = "com.Comp.Proj.shop.items_all"; // Google Play Store identifier for the consumable product.
        public MonoBehaviour _Main;
        void Start()
        {
            //ZPlayerPrefs.Initialize("----------------", SystemInfo.deviceUniqueIdentifier);
            // If we haven't set up the Unity Purchasing reference
            if (m_StoreController == null)
            {
                // Begin to configure our connection to Purchasing
                InitializePurchasing();
            }
        }
    
        public void InitializePurchasing()
        {
            // If we have already connected to Purchasing ...
            if (IsInitialized())
            {
                // ... we are done here.
                return;
            }
        
            // Create a builder, first passing in a suite of Unity provided stores.
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        
            // Add a product to sell / restore by way of its identifier, associating the general identifier with its store-specific identifiers.
            builder.AddProduct(kItems, ProductType.NonConsumable, new IDs(){ {kGooglePlayItems,  GooglePlay.Name} });// Continue adding the non-consumable product.
        
            UnityPurchasing.Initialize(this, builder);
        }
    
    
        private bool IsInitialized()
        {
            // Only say we are initialized if both the Purchasing references are set.
            return m_StoreController != null && m_StoreExtensionProvider != null;
        }
    
        public void BuyNonConsumable()
        {
            // Buy the non-consumable product using its general identifier. Expect a response either through ProcessPurchase or OnPurchaseFailed asynchronously.
            BuyProductID(kItems);
        }
    
    
        void BuyProductID(string productId)
        {
            // If the stores throw an unexpected exception, use try..catch to protect my logic here.
            try
            {
                // If Purchasing has been initialized ...
                if (IsInitialized())
                {
                    // ... look up the Product reference with the general product identifier and the Purchasing system's products collection.
                    Product product = m_StoreController.products.WithID(productId);
                
                    // If the look up found a product for this device's store and that product is ready to be sold ...
                    if (product != null && product.availableToPurchase)
                    {
                        Debug.Log (string.Format("Purchasing product asychronously: '{0}'", product.definition.id));// ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed asynchronously.
                        m_StoreController.InitiatePurchase(product);
                    }
                    // Otherwise ...
                    else
                    {
                        // ... report the product look-up failure situation
                        Debug.Log ("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
                    }
                }
                // Otherwise ...
                else
                {
                    // ... report the fact Purchasing has not succeeded initializing yet. Consider waiting longer or retrying initiailization.
                    Debug.Log("BuyProductID FAIL. Not initialized.");
                }
            }
            // Complete the unexpected exception handling ...
            catch (Exception e)
            {
                // ... by reporting any unexpected exception for later diagnosis.
                Debug.Log ("BuyProductID: FAIL. Exception during purchase. " + e);
            }
        }
    
    
        // Restore purchases previously made by this customer. Some platforms automatically restore purchases. Apple currently requires explicit purchase restoration for IAP.
        public void RestorePurchases()
        {
            // If Purchasing has not yet been set up ...
            if (!IsInitialized())
            {
                // ... report the situation and stop restoring. Consider either waiting longer, or retrying initialization.
                Debug.Log("RestorePurchases FAIL. Not initialized.");
                return;
            }
        
            // If we are running on an Apple device ...
            if (Application.platform == RuntimePlatform.IPhonePlayer ||
                Application.platform == RuntimePlatform.OSXPlayer)
            {
                // ... begin restoring purchases
                Debug.Log("RestorePurchases started ...");
            
                // Fetch the Apple store-specific subsystem.
                var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
                // Begin the asynchronous process of restoring purchases. Expect a confirmation response in the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
                apple.RestoreTransactions((result) => {
                    // The first phase of restoration. If no more responses are received on ProcessPurchase then no purchases are available to be restored.
                    Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
                });
            }
            // Otherwise ...
            else
            {
                // We are not running on an Apple device. No work is necessary to restore purchases.
                Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
            }
        }
    
    
        //
        // --- IStoreListener
        //
    
        public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
        {
            // Purchasing has succeeded initializing. Collect our Purchasing references.
            Debug.Log("OnInitialized: PASS");
        
            // Overall Purchasing system, configured with products for this application.
            m_StoreController = controller;
            // Store specific subsystem, for accessing device-specific store features.
            m_StoreExtensionProvider = extensions;
        }
    
    
        public void OnInitializeFailed(InitializationFailureReason error)
        {
            // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
            Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
        }
    
    
        public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
        {
            // A consumable product has been purchased by this user.
            if (String.Equals(args.purchasedProduct.definition.id, kItems, StringComparison.Ordinal))
            {
                Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
                //If the item has been successfully purchased, store the item for later use!
                PlayerPrefs.SetInt("Items_All", 1);
                _Main.Invoke("Got_Items", 0); //Call a function in another script to play some effects.
            }
            else
            {
                Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
                PlayerPrefs.SetInt("Itemss_All", 0);
            }
            // Return a flag indicating wither this product has completely been received, or if the application needs to be reminded of this purchase at next app launch. Is useful when saving purchased products to the cloud, and when that save is delayed.
            return PurchaseProcessingResult.Complete;
        }
    
    
        public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
        {
            // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing this reason with the user.
            Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}",product.definition.storeSpecificId, failureReason));}
    }

There are a few security problems with the above code, but before we get to that I want to explain how we retrieve purchases from the store. The PurchaseProcessingResult function is called when a purchase has been processed at runtime. That means it will go off every time someone uses your store. We use some simple logic here to figure out if the item was bought or not. Then we have to save that information somehow. For now, the above code drops the state of our item into the PlayerPrefs as soon as possible. DO NOT LEAVE IT THAT WAY. The PlayerPrefs are written in everyday text and can be “hacked” really easily - That’s not the way to store purchases. We will get back to that issue in a second. The PlayerPrefs allow us to see if the player has purchased an item so long as the app remains on the device. In order to restore our save file after the game has been removed and reinstalled, we actually use the same exact function. While Android is automated to process every purchase on the FIRST run after installation and call PurchaseProcessingResult for each item, Apple requires us to ask for that using the RestorePurchases function. Either way, the app should process the purchases like they were just made at runtime by a user during the first run after installation.

What do we have left to do? There’s that issue with the security of PlayerPrefs, and we still haven’t setup our products on the Play Store end!
To fix the security issue, I used the free Secured PlayerPrefs (Link below). This free package encrypts our player prefs using a salt.

Simply add the letter Z before all our PlayerPrefs so that they read ZPlayerPrefs, and initialize the system with

//BASE
ZPlayerPrefs.Initialize("PASSWORD", "SALT");
//EXAMPLE
ZPlayerPrefs.Initialize("789so9isdead", SystemInfo.deviceUniqueIdentifier);

Now we just have to setup the items in the Play Store. Open the Google Play Developer Console and navigate to your project. Make sure everything is setup so you can publish an apk file to Alpha or Beta (prices set, store info written, screen shots provided, etc.) Next select the In-app Products tab and add a new product. The id you make should be formatted something like so

and copied into our script for the value of kGooglePlayItems near line 14. The name of the item will be displayed on the screen when the store is called. Make sure to publish the item on Google Play along with a new apk file. All testers must be signed up for your app’s testing under Google Play in-order to use the store before official release of the app.

For examples using the Apple store, read over the example code imported with the IAP system.

Debug the Play Store:
If the items does not exist on Google Play (For Android Builds) then your shop will return an error along the lines of “item not found”, when you try to purchase said item. If the item was found but your client doesn’t have permission to buy it (if the apk is under Alpha or Beta and the user’s Google Account isn’t signed up for testing) you’ll get something along the lines of “Item can not be purchased at the moment”.

3 Likes

Thanks for posting this tutorial, really helpful. I am stuck on something though…
In your code, what in the world is “kItems”? I see “kItem” that is being used as a general identifier.

It also doesn’t make sense to me that ProcessPurchase is comparing args.purchasedProduct.definition.id to kProductIDConsumable as the first string is the products store identifier… so it never matches for me…

Also what is up with the BuyConsumable, BuyNonConsumable and BuySubscription methods, are those only for testing?

If I’m not wrong, kItems is the default productId whether you set that on AppleStore, or GooglePlay , that must have the same Id

The args.purchasedProduct.definition.id is use to compare that which productId that you sent to buy with BuyProductID(); function. In this case, there is only kItems as 1 product. so when you call BuyProductID(kItems); it will send callback to PurchaseProcessingResult and send the productId for you to compare which package the user buy, and just add that package to the user pref or anything you want to add.

Don’t concern much about BuyConsumable, BuyNonConsumable and BuySubscription methods. It’s just an example of 3 kinds of package which refer to three type of packages it can be sell. It can be BuyConsumable1, BuyConsumable2 and BuyConsumable3 with specific Id you want to be on the store.

Anyway, I also have a question here about the Implementing test. (Android)

When I test the APK on the android phone. The application isn’t Initialize the controller and extension for me. So that when I call BuyConsumable, it errors that “BuyConsumable FAIL Not Initialized”. (When Testing on UnityEditor , the Initializing part works fine.)

I dont know that do I have to upload a new APK to the GooglePlay store first, In order to test the IAP on testing APK? It doesn’t make sense that uploading a new APK with implemented IAP to the GooglePlay should have something to do with OnInitialze. Or I’m missing something?

Ok, now the error just show that the package on Google Play are not available because I skip the setup to alpha or beta part. I think If I do the setup to alpha, this might solve the problem

@iamau5
I was having those same problems, YES you do need to have an APK uploaded to either Alpha or Beta in the google player developer console AND make sure that you PUBLISH IT. I uploaded mine but didn’t publish it, I found the publish button after you click the “Advanced Setup” button. Then you’ll still have to wait up to 24 hours for testing after you publish it. Also make sure that the APK you upload has the same bundle version as the one you are going to test with locally on your device.

In regards to my previous question, I think what you said makes sense, but I wish someone would share an example that works with multiple product IDs rather than just 1 “master product” like in the above code. I’m still struggling with the right way to add a product to the builder and then purchase it. I keep getting errors about the product not existing, etc.

Thanks man! I struggle figuring out stuff that doesn’t have ridiculous amounts of documentation, so this should help a lot :slight_smile:

How would this script work with multiple consumables? Any idea?

I did everything as it is stated here (or so I believe), but when I run it on my Android device nothing happens.
I click the buy button on my virtual shop and nothing happens :C
Help?

Take a look to the code in this post : http://forum.unity3d.com/threads/purchases-not-processing-on-android.374916/
IDK if its the best workaround but it works :slight_smile:

Hi @gifer81 ,

I believe there should be an example on how to add multiple products here, https://docs.unity3d.com/ScriptReference/Purchasing.ConfigurationBuilder.AddProducts.html

Thanks mpinol,

I have a different question:

I’m using a sandbox test user to test my iap and everything works as expected. However when I switch back to my main Apple account I get the login dialog that asks for the sandbox user password every time the game is started or restored from the background. Any idea of why this happens?

Thanks in advance

Hi @gifer81

It sounds like there is still a connection from your device to the Sandbox store. Can you please try to, uninstall the app, restart the device, and then reinstall the app and try again?

Thanks for the response.

I have tried the steps you suggested but the problem persists. I’m currently using TestFlight to test my game. On a different device everything works great, but on my device (the one where I logged in with my sandbox user and tested iap during development) it keeps prompting the itunes store login dialog as something is still pending. This happens on my device also when I create a new development build from unity and run it with xcode.

Storekit will prompt the user for their credentials if it thinks there are pending transactions the user is owed and needs to log in. Unity IAP has no control over the login prompt. Once you login, does it stop prompting?

The login dialog pops up during these events:

  1. When I launch the game
  2. When I resume the game
  3. Randomly even when the game is closed

If I type the password and log in the game still keeps prompting the dialog during the events above. The only way not to have the login dialog to show is to delete the game from the device.

please find the below function. this function is written for restoring the previously purchased items in Apple store.

can anyone rewrite the below function for google play store
please find the below function. this function is written for restoring the previously purchased items in Apple store.

can anyone rewrite the below function for google play store

// Restore purchases previously made by this customer. Some platforms automatically restore purchases. Apple currently requires explicit purchase restoration for IAP.
public void RestorePurchases()
{
// If Purchasing has not yet been set up …
if (!IsInitialized())
{
// … report the situation and stop restoring. Consider either waiting longer, or retrying initialization.
Debug.Log(“RestorePurchases FAIL. Not initialized.”);
return;
}

// If we are running on an Apple device …
if (Application.platform == RuntimePlatform.IPhonePlayer ||
Application.platform == RuntimePlatform.OSXPlayer)
{
// … begin restoring purchases
Debug.Log(“RestorePurchases started …”);

// Fetch the Apple store-specific subsystem.
var apple = m_StoreExtensionProvider.GetExtension();
// Begin the asynchronous process of restoring purchases. Expect a confirmation response in the Action below, and ProcessPurchase if there are previously purchased products to restore.
apple.RestoreTransactions((result) => {
// The first phase of restoration. If no more responses are received on ProcessPurchase then no purchases are available to be restored.
Debug.Log("RestorePurchases continuing: " + result + “. If no further messages, no purchases available to restore.”);
});
}
// Otherwise …
else
{
// We are not running on an Apple device. No work is necessary to restore purchases.
Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
}
}

Hi @basavaraj_guled ,

I believe that this code should work for both iOS and Android because in the final else statement it says that no work is necessary to restore purchases on non-Apple devices. I’m not 100% sure though, so I want to confirm for you that this is the case. I am checking with someone working on the service and once I hear back, I will post an additional update to this forum.

hi @erika_d

ya its working 100% for an android devices
there is no function written to call for restore in android it will automatically call the function “PurchaseProcessingResult” if u r logged in

Hi @basavaraj_guled ,

Great! Glad to hear it is working.