How to Implement In-App Purchases with Unity IAP

Featured image.

This tutorial illustrates how to implement in-app purchases (IAP) for games that use the Unity engine. In this tutorial, we use the scripted approach to handle the purchases with a focus on mobile games that are built to run on the Android™ platform.

The next section contains the steps for creating a demo project using the Unity engine.

Important Notes

1. In order to test IAP on the Android platform, you need to have a Google Play Console account where you can deploy a demo app on the Google Play store. This tutorial does not go over the steps to create a an account on Google Play Console and so it will be assumed that you already have an account.

2. Another requirement before going forward with this tutorial is to create a Unity account if you don’t already have one. A Unity account is used for creating a Unity Project ID, which is required to enable the Unity IAP service. In this tutorial, it will be assumed that you already have a Unity account.

3. If you are only interested in how to implement the IAP scripts, then you may jump directly to Create the In-App Purchasing Scripts.

Create the Demo Project

  1. Open Unity Hub.
  2. Ensure that you have a Unity version with Android Build Support.
    • To verify that, click on Installs from the left sidebar to list the Unity versions. Then, ensure that the Android tag appears under the Unity version that you want to use. See the screenshot below.
Screenshot of Unity Editor Version in Unity Hub.
  1. If you don’t see the Android tag, then do the following to add Android build support:
    • Click on the cog icon on the version you want to use and select Add modules.
    • Check the box next to Android Build Support and ensure OpenJDK and Android SDK & NDK Tools are all included.
    • Click on Install.
  2. On the left sidebar, go to Projects.
  3. Click on the New project button.
  4. In the next window, select the 2D Core template and set the Project name to a name of your choice.
  5. Select your organization from the Unity Cloud Organization dropdown.
  6. Check the box that says Connect to Unity Cloud.
  7. Select the Editor Version of your choice at the top of the window. In this demo, the version used is 2022.3.16f1 LTS.
  8. Click on Create project and wait until the Unity editor loads the new project.
  9. Once the Unity editor is ready, go to File->Build Settings…, then select the Android platform and click on Switch Platform.
  10. Wait for the Unity editor to apply the changes, then just close the window.

It’s important that steps 9 and 10 are done in advance so that we can set the game’s aspect ratio to 16:9 portrait in the next section when we start creating the user interface.

Create the User Interface

This section is for creating the user interface for the store menu and the loading screen. The store menu will contain a title and the buttons that are used for initiating purchases. The loading screen will be displayed while the game is establishing a connection to the Google Play store.

The IAP Store Menu

In this section, we will create a store menu that will look like the following screenshot.

Screenshot of the store menu from the Game tab in Unity.

The steps to create the store menu are as follows.

  1. Change the game’s aspect ratio by going to the Game tab and choose 16:9 Portrait on the aspect ratio drop down.
Screenshot of a Game scene with a highlight on the "16:9 Portrait" from the aspect ratio drop down list.

If you don’t see the 16:9 Portrait aspect ratio, then your build platform may not be set to the Android platform. Refer to steps 9 and 10 from Create The Demo Project section to see how to switch the build platform to the Android platform.

  1. Return to the scene view by clicking on the Scene tab.
  2. Add a new canvas to the scene by right clicking on SampleScene from Hierarchy and choose GameObject->UI->Canvas.
  3. Rename the newly created canvas to StoreMenu.
  4. Select StoreMenu from Hierarchy, go to Inspector and do the following:
    • Under Canvas Scaler, change the UI Scale Mode to Scale With Screen Size.
    • Under Canvas Scaler -> Reference Resolution, set X to 324 and Y to 576.
  5. Add a new panel by right clicking on the StoreMenu canvas in Hierarchy and choose UI->Panel.
  6. Rename the newly created panel to Background.
  7. Select the Background panel from Hierarchy then change its color by going to the Inspector tab and click on Color.
  8. On the Color window that appears, enter 45255E in Hexadecimal and set A to 255, then close the window. You should see the Background panel’s color change to purple.
Screenshot of the Color selector for the StoreMenu background.
  1. Create a new text object by right clicking on StoreMenu from Hierarchy and choose UI->Text – TextMeshPro.
  2. Rename the text object to StoreMenuTitle.
  3. If this is the first time you create a TextMeshPro, you should see a window at the bottom of the screen with a button to Import TMP Essentials.
Screenshot of Unity with a highlight on the "Import TMP Essentials" button.
  1. Click on Import TMP Essentials if the button appears. Otherwise, continue to the next step.
  2. Select StoreMenuTitle in Hierarchy, go to Inspector and do the following:
    • In the text field under TextMeshPro – Text (UI) -> Text Input, change the text from New Text to Store Menu.
    • Change the Vertex Color to Orange, use Hexadecimal of D66D00 and change the Font Size to 30.
Screenshot of the Color selector window for the "Store Menu" title.
  1. Move StoreMenuTitle to the top of the canvas. To do that, click on StoreMenuTitle in Hierarchy and in Inspector, set PosY (under Rect Transform) to 180 so that it appears at the top of the canvas and leave PosX to 0.
  2. In the Alignment settings, click on the center button to move the “Store Menu” text to the center.
Screenshot of the store menu text alignment.
  1. Next, we will create 3 buttons. One for purchasing a non-consumable and two buttons for purchasing consumables.
  2. To create the first button, go to Hierarchy, right click on StoreMenu and choose UI->Button – TextMeshPro.
  3. Rename the button to BuyItemButton.
  4. Change the color of BuyItemButton to a dark blue color (Hexadecimal 271FB9).
  5. Change the text, font size and font style of BuyItemButton by going to Hierarchy, then expand BuyItemButton, click on Text (TMP) and do the following in Inspector:
    • Under Main Settings, Change Vertex Color to green (Hexadecimal 2EC850), enable B (i.e Bold) on Font Style and set Font Size to 20.
    • Change the text under Text Input to “Buy Item”
  6. Select BuyItemButton in Hierarchy, go to Inspector and set Pos Y to 80 to move the button below StoreMenuTitle.
  7. To create the second button, go to Hierarchy, right click on StoreMenu and choose UI->Button – TextMeshPro.
  8. Rename the button to Buy50CoinsButton.
  9. Set the color and font settings for Buy50CoinsButton to the same color and font settings as the ones from BuyItemButton.
  10. Change the text for Buy50CoinsButton to “Buy 50 Coins”.
  11. Select Buy50CoinsButton in Hierarchy, go to Inspector and set Pos Y to 20 to move the button closer below BuyItemButton.
  12. To create the third button, go to Hierarchy, right click on StoreMenu and choose UI->Button – TextMeshPro.
  13. Rename the button to Buy100CoinsButton.
  14. Set the color and font settings for Buy100CoinsButton to the same color and font settings as the ones from BuyItemButton.
  15. Change the text for Buy100CoinsButton to “Buy 100 Coins”.
  16. Select Buy100CoinsButton in Hierarchy, go to Inspector and set Pos Y to -40 to move the button closer below Buy50CoinsButton. So far, the scene should look similar to the screenshot below.
Screenshot of the Store Menu screen.
  1. Save the scene by selecting File -> Save.
  2. Next, we need to create texts to display the total coins. In Hierarchy, add one TextMeshPro component by right clicking on StoreMenu, select UI -> Text – TextMeshPro and rename the object to TotalCoinsLabel.
  3. Select TotalCoinsLabel in Hierarchy, go to Inspector and do the following:
    • Under Rect Transform, set Width to 80, Height to 20, Pos Y to -100 and Pos X to -40. The text now should be below Buy100CoinsButton and aligned with the left border of the button.
    • Under TextMeshPro – Text (UI), change the field under Text Input to “Total Coins:” and Font Size to 15.
  4. Add another TextMeshPro to StoreMenu and rename the object to TotalCoinsValue. This text will be used to show the number of coins that you possess.
  5. Select TotalCoinsValue in Hierarchy, go to Inspector and do the following:
    • Under Rect Transform, set Width to 100, Height to 20, Pos Y to -100 and Pos X to 55. The text now should be aligned with TotalCoinsLabel.
    • Under TextMeshPro – Text (UI), change the field under Text Input to 0 and Font Size to 15. Now, the scene should look similar to the screenshot below.
Screenshot of the Store Menu screen.

The last thing we need to add to the store menu is a text that will appear when the Google Play store cannot be reached. The most likely scenario that causes the unreachable store issue is when the device that runs the game is not connected to the internet.

Do the following steps to add the unreachable store text.

  1. Create a new text object by right clicking on StoreMenu from Hierarchy and choose UI->Text – TextMeshPro.
  2. Rename the text object to UnreachableStoreErrorText.
  3. Click on UnreachableStoreErrorText from Hierarchy, go to Inspector and do the following:
    • Under Rect Transform, set Pos X to 20, Pos Y to -166, Width to 200 and Height to 50.
    • Change the text under Text Input to Store cannot be reached. Verify your internet connection.”
    • Set Font Size to 12.
    • Set Vertex Color to Red (Hexadecimal FF0000).
  4. Go to Scene and ensure that the text is located below Total Coins and shows up correctly.
  5. Select UnreachableStoreErrorText from Hierarchy, go back to Inspector and uncheck the checkbox at the top, located at the left of the UnreachableStoreErrorText field. This is because the text needs to be hidden by default and will only appear when the Google Play store cannot be reached from the game.
Screenshot of UnreachableStoreErrorText in inspector.
  1. Save the scene.

We now have completed the store menu. The next step is to create the loading screen.

Create the Loading Screen

  1. The first thing we need to do is to hide the store menu from the scene to prevent it from interfering with the loading screen components that we will work on. To do that, go to Hierarchy, click on StoreMenu and in Inspector, uncheck the checkbox next to the StoreMenu text field.
Screenshot of UnreachableStoreErrorText in inspector.
  1. Add a new canvas to the scene by right clicking on SampleScene from Hierarchy and choose GameObject->UI->Canvas.
  2. Rename the canvas to LoadingScreen.
  3. Select LoadingScreen from Hierarchy, go to Inspector and do the following:
    • Under Canvas Scaler, change the UI Scale Mode to Scale With Screen Size.
    • Under Canvas Scaler -> Reference Resolution, set X to 324 and Y to 576.
  4. Add a new panel by clicking on the LoadingScreen canvas and choose UI->Panel.
  5. Rename the panel to Background.
  6. Select the Background panel from Hierarchy, change its color by going to the Inspector tab and click on Color.
  7. On the Color window that appears, enter 45255E in Hexadecimal and set A to 255, then close the Color window. You should see the Background panel’s color change to purple.
  8. Create a new text object by right clicking on LoadingScreen from Hierarchy and choose UI->Text – TextMeshPro.
  9. Rename the text object to LoadingStoreText.
  10. Select LoadingStoreText from Hierarchy, then go to Inspector -> Rect Transform and change the Width to 300.
  11. Do the following under Inspector -> TextMeshPro – Text (UI):
    • Change the text under Text Input to “Loading Store…”.
    • Set Font Size to 36.
    • Set Vertex Color to orange (Hexadecimal D66D00), which is the same color we used for StoreMenuTitle.
    • Set Alignment to Center and Middle.
  12. Save the scene.

The loading screen should look similar to the following screenshot.

Screenshot of the Loading Store screen.

Configure IAP in Unity

  1. Open Package Manager In the Unity editor by going to Window -> Package Manager and wait for the packages to be fetched.
  2. In Package Manager, select Packages: Unity Registry from the Packages drop down.
  3. On the search field on the right, type in app purchasing to filter out the unrelated packages.
  4. Click on In App Purchasing from the list then click on the Install button.
Screenshot of the Unity Package Manager.
  1. Go to the Unity editor and open your project settings by going to Edit -> Project Settings.
  2. On the left sidebar, click on Services and verify that the information on Services General Settings are correct and that Unity Project ID is set. If you don’t have a Unity Project ID then you can create one from the Unity editor or Unity Cloud and link it to your project. For more details, refer to the Unity engine documentation at https://docs.unity.com/ugs-overview/en/manual/ManagingUnityProjects.
  3. Expand Services and click on In-App Purchasing. If you get a question on whether you are primarily targeting children under age 13, select No and click on Save.
Screenshot of age group target selection.
  1. Click on the toggle button on the upper right part to enable In-App Purchasing.
  2. Make sure that the Current Build Target is Android and Current Targeted Store is Google Play.
  3. Close the project settings window.

Now that in-app purchasing is enabled, the next thing to do is to set up the receipt validation obfuscation, which is detailed in the next section.

Setup Receipt Validation Obfuscation in Unity Engine

This step is required to create the tangle classes that will be used by the purchasing scripts for receipt validation. The tangle classes can be generated using the Unity engine for obfuscating license keys in order to make it hard for people to defeat the receipt validation checks.

The following are the steps to setup the receipt validation:

  1. Go to Unity editor and navigate to Services -> In-App Purchasing -> Configure…
  2. Follow the steps under Google Play Configuration to get your License Key.
  3. Scroll down to the Receipt Obfuscator section.
  4. Enter the License Key obtained from Step 2 in the field that’s labeled “-Enter Key-”.
  5. Click on Obfuscate License Keys button.
  6. You should see a message under the button that confirms that GooglePlayTangle.cs was generated.

The script GooglePlayTangle.cs should be located in Assets->Scripts->UnityPurchasing->generated.

Important Note

If you are planning to release a game that contains in-app purchases to the public, it is recommended that you implement a stronger receipt validation mechanism for your game (e.g server side receipt validation). The demo being developed here is not meant to be released to the public. As such, adding server side receipt validation is not required for the demo.

Create the In-App Purchasing Scripts

In our game, we will create two scripts. The first one is called PurchaseManager.cs and will be used for handling the purchase workflow such as processing purchases, handling successful and failed purchases and so on. The second script we will create is called StoreUiManager.cs and will be used for updating the store menu UI based on the result of a purchase. For instance, when the non-consumable item is purchased successfully, the Buy Item button will be disabled and the button’s text will show “Item Bought” instead. In addition, when either 50 Coins or 100 Coins are successfully purchased, the total coins value will be updated on the store menu accordingly.

StoreUiManager

  1. Open the Unity editor and go to the Project tab. 
  2. Expand the Assets folder, right click on the Scripts folder, select Create -> C# Script and give the script the name StoreUiManager.
  3. Double click on the StoreUiManager.cs script to edit it.
  4. Copy and paste the following code into StoreUiManager.cs and save.
using TMPro;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// Class that handles user interface changes to the store menu.
/// </summary>
public class StoreUiManager : MonoBehaviour
{
    /// <summary>
    /// Reference to the text component that shows the total coins.
    /// </summary>
    [SerializeField]
    private TextMeshProUGUI m_totalCoinsValueTxt;

    /// <summary>
    /// Reference to the LoadingScreen canvas.
    /// </summary>
    [SerializeField]
    private GameObject m_loadingScreen;

    /// <summary>
    /// Reference to the StoreMenu canvas.
    /// </summary>
    [SerializeField]
    private GameObject m_storeMenu;

    /// <summary>
    /// Reference to the BuyItem button.
    /// </summary>
    [SerializeField]
    private Button m_buyItemButton;

    /// <summary>
    /// Reference to the text component of the BuyItem button.
    /// </summary>
    [SerializeField]
    private TextMeshProUGUI m_buyItemButtonTxt;

    /// <summary>
    /// Reference to the text component for the unreachable store error.
    /// </summary>
    [SerializeField]
    private TextMeshProUGUI m_unreachableStoreErrorTxt;

    /// <summary>
    /// Keeps the current coin count.
    /// </summary>
    private static int s_totalCoins;

    /// <summary>
    /// The maximum number of coins you can have.
    /// </summary>
    private const int MAX_COINS = 2000000000;

    /// <summary>
    /// The player prefs key for the total coins.
    /// </summary>
    private const string TOTAL_COINS_PREF_KEY = "totalCoins";

    /// <summary>
    /// Updates the total coins text and saves the total coins in the device's storage.
    /// </summary>
    /// <param name="coins">The number of coins to add.</param>
    public void AddCoins(int coins)
    {
        if (IsTotalCoinsReached(coins, s_totalCoins))
        {
            Debug.Log("Max coins reached!");
            s_totalCoins = MAX_COINS;
        }
        else
        {
            s_totalCoins += coins;
        }

        PlayerPrefs.SetInt(TOTAL_COINS_PREF_KEY, s_totalCoins);
        PlayerPrefs.Save();
        m_totalCoinsValueTxt.text = s_totalCoins.ToString();
    }

    /// <summary>
    /// Disables the "Buy Item" button and updates the button's text to "Item Bought".
    /// This function is called from PurchaseManager after a successful purchase of 
    /// the non-consumable item.
    /// </summary>
    public void DisableBuyItemButton()
    {
        m_buyItemButton.interactable = false;
        m_buyItemButtonTxt.text = "Item Bought";
    }

    /// <summary>
    /// Hides the loading screen and displays the store menu.
    /// This is called from PurchaseManager after the store is initialized.
    /// </summary>
    public void NotifyStoreLoaded()
    {
        Debug.Log("StoreUiManager -> NotifyStoreLoaded()");

        m_loadingScreen.SetActive(false);
        m_storeMenu.SetActive(true);
        // Hide the unreachable store error, just in case it was visible before
        // from a previous issue.
        m_unreachableStoreErrorTxt.gameObject.SetActive(false);
    }

    /// <summary>
    /// Hides the loading screen and displays the store menu and the unreachable store error.
    /// This is called from PurchaseManager after the store is initialized.
    /// </summary>
    public void NotifyUnreachableStore()
    {
        Debug.Log("StoreUiManager -> NotifyUnreachableStore()");

        m_loadingScreen.SetActive(false);
        m_storeMenu.SetActive(true);
        m_unreachableStoreErrorTxt.gameObject.SetActive(true);
    }

    /// <summary>
    /// Returns true if the maximum number of coins is reached.
    /// </summary>
    /// <param name="coins">The number of coins to add.</param>
    /// <param name="currentTotalCoins">The current total coins.</param>
    /// <returns>True if the maximum number of coins is reached.</returns>
    private static bool IsTotalCoinsReached(int coins, int currentTotalCoins)
    {
        return (MAX_COINS - coins) <= currentTotalCoins;
    }

    /// <summary>
    /// Initializes the total coins player pref and sets the total coins on the store menu.
    /// </summary>
    private void Start()
    {
        Debug.Log("StoreUiManager -> Start()");

        if (PlayerPrefs.HasKey(TOTAL_COINS_PREF_KEY))
        {
            s_totalCoins = PlayerPrefs.GetInt(TOTAL_COINS_PREF_KEY);
        }
        else
        {
            s_totalCoins = 0;
            PlayerPrefs.SetInt(TOTAL_COINS_PREF_KEY, 0);
        }

        m_totalCoinsValueTxt.text = s_totalCoins.ToString();
    }
}
  1. We need to go back to work on StoreMenu. So, disable LoadingScreen from Inspector to hide it from the scene and enable StoreMenu so it appears in the scene.
  2. Now, we need to attach the StoreUiManager script to the StoreMenu. To do that, click on the Add Component button at the bottom of Inspector and select Scripts -> Store Ui Manager. If you don’t see Store Ui Manager in the list, verify that you have saved the StoreUiManager.cs script and that there are no compilation errors shown on the Console tab in the Unity editor.
  3. Expand StoreMenu in Hierarchy, drag TotalCoinsValue and drop it in the field next to Total Coins Value Txt under Inspector -> Store Ui Manger. After doing that, you should see the field change to TotalCoinsValue (Text Mesh Pro UGUI).
  4. Drag LoadingScreen from Hierarchy and drop it in the field next to Loading Screen.
  5. Drag StoreMenu from Hierarchy and drop it in the field next to Store Menu.
  6. Drag BuyItemButton from Hierarchy and drop it in the field next to Buy Item Button.
  7. Expand BuyItemButton from Hierarchy, then drag Text (TMP) and drop it in the field next to Buy Item Button Txt.
  8. Drag UnreachableStoreErrorText from Hierarchy and drop it in the field next to Unreachable Store Error Txt.
  9. Save the scene.

The fields below Store Ui Manger (Script) should look similar to the highlighted part in the following screenshot.

Screenshot of StoreMenu's Inspector window in Unity with a highlighted script component.

PurchaseManager

  1. Go to the Unity editor and on the Project tab, right click on Assets -> Scripts and create a new C# script and name it PurchaseManager.
  2. Double click on PurchaseManager.cs to edit it
  3. Copy and paste the following code into PurchaseManager.cs and save.
using Unity.Services.Core;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using UnityEngine.Purchasing.Security;

/// <summary>
/// Class that handles in-app purchasing.
/// </summary>
public class PurchaseManager : MonoBehaviour, IDetailedStoreListener
{
    /// <summary>
    /// The Unity purchasing system.
    /// </summary>
    private static IStoreController m_StoreController;

    /// <summary>
    /// The store-specific purchasing subsystem.
    /// </summary>
    private static IExtensionProvider m_StoreExtensionProvider;

    /// <summary>
    /// Extensions containing api functions for Google Play store.
    /// </summary>
    private static IGooglePlayStoreExtensions m_GooglePlayStoreExtensions;

    /// <summary>
    /// Reference to the StoreUiManager script.
    /// </summary>
    [SerializeField]
    private StoreUiManager m_storeUiManager;

    /// <summary>
    /// List of product IDs with [domain] and [company name] place holders that you must replace
    /// accordingly. For example, if you have a company named mygamingcompany then you can use 
    /// "com.mygamingcompany" as the prefix for your product IDs.
    /// Also, make sure these IDs match the IDs that you entered for the products that you 
    /// created on Google Play Console.
    /// </summary>
    #region IAP Product IDs

    private const string NON_CONSUMABLE_ITEM_ID =
        "[domain].[company name].iapdemo.nonconsumable";
    private const string CONSUMABLE_ONE_HUNDREND_COINS_ID =
        "[domain].[company name].iapdemo.onehundredcoins";
    private const string CONSUMABLE_FIFTY_COINS_ID =
        "[domain].[company name].iapdemo.fiftycoins";

    #endregion

    /// <summary>
    /// Initializes Unity Services and the store controller.
    /// </summary>
    async void Start()
    {
        Debug.Log("PurchaseManager -> Start()");

        await UnityServices.InitializeAsync();

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

    /// <summary>
    /// Initializes the Unity purchasing module according to the store we're connecting to.
    /// </summary>
    public void InitializePurchasing()
    {
        Debug.Log("PurchaseManager -> InitializePurchasing()");

        if (IsInitialized())
        {
            return;
        }

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        builder.
        Configure<IGooglePlayConfiguration>().
        SetDeferredPurchaseListener(OnDeferredPurchase);

        // Add the non-consumable item.
        builder.AddProduct(NON_CONSUMABLE_ITEM_ID, ProductType.NonConsumable);

        // Add consumables.
        builder.AddProduct(CONSUMABLE_ONE_HUNDREND_COINS_ID, ProductType.Consumable);
        builder.AddProduct(CONSUMABLE_FIFTY_COINS_ID, ProductType.Consumable);

        var module = StandardPurchasingModule.Instance();
        Debug.Log($"Starting purchase initialization. Store: {module.appStore}");

        UnityPurchasing.Initialize(this, builder);
    }

    /// <summary>
    /// Returns true if the store controller and extension are initialized.
    /// </summary>
    /// <returns>True if the store controller and extension are initialized.</returns>
    public static bool IsInitialized()
    {
        return m_StoreController != null && m_StoreExtensionProvider != null;
    }

    /// <summary>
    /// Pulls the purchase status of the non-consumable item to decide whether to disable
    /// the "Buy Item" button or not.
    /// </summary>
    /// <param name="controller">The store controller instance.</param>
    /// <param name="extensions">
    /// The store specific extensions.
    /// </param>
    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        Debug.Log("PurchaseManager -> OnInitialized()");

        m_storeUiManager.NotifyStoreLoaded();

        m_StoreController = controller;
        m_StoreExtensionProvider = extensions;
        m_GooglePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();

        if (m_StoreController != null)
        {
            Product nonConsumable = m_StoreController.products.WithID(NON_CONSUMABLE_ITEM_ID);
            if (nonConsumable != null && IsPurchased(nonConsumable))
            {
                Debug.Log($"Found receipt for {nonConsumable.definition.id}");

                // Disable the "Buy Item" button since the item has already been bought.
                m_storeUiManager.DisableBuyItemButton();
            }
        }
    }

    /// <summary>
    /// Notifies the StoreUiManager script to display an error when 
    /// the store initialization fails.
    /// </summary>
    /// <param name="error">The failure reason.</param>
    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log($"Initialization failed. Reason: {error}");
        m_storeUiManager.NotifyUnreachableStore();
    }

    /// <summary>
    /// Notifies the StoreUiManager script to display an error when 
    /// the store initialization fails.
    /// </summary>
    /// <param name="error">The failure reason.</param>
    /// <param name="message">Details on the failure.</param>
    public void OnInitializeFailed(InitializationFailureReason error, string message)
    {
        Debug.Log($"Initialization failed. Reason: {error}. Details {message}");
        m_storeUiManager.NotifyUnreachableStore();
    }

    /// <summary>
    /// Handles purchase requests.
    /// </summary>
    /// <param name="args">
    /// Instance of PurchaseEventArgs, which contains the purchased product details.
    /// </param>
    /// <returns>Purchase processing result.</returns>
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    {
        string productId = args.purchasedProduct.definition.id;
        Debug.Log($"ProcessPurchase: {productId}");

        // Unity IAP's validation is performed on Android only.
        if (Application.platform == RuntimePlatform.Android)
        {
            if (m_GooglePlayStoreExtensions.IsPurchasedProductDeferred(args.purchasedProduct))
            {
                // ProcessPurchase will be called again automatically when the purchase status
                // updates and the purchased product will be unlocked once the receipt is
                // validated. So, at this point we will only return the pending result.
                Debug.Log($"Purhchase of product {productId} is pending.");
                return PurchaseProcessingResult.Pending;
            }

            try
            {
                // Prepare the validator with the keys that were generated by the
                // Receipt Validation Obfuscator.
                var validator = new CrossPlatformValidator(
                    GooglePlayTangle.Data(),
                    null,
                    Application.identifier);
                validator.Validate(args.purchasedProduct.receipt);

                // At this point, the validation has passed since no error was thrown from
                // the Validate function call. Therefore, the purchased item can be
                // unlocked here.
                if (productId.Equals(NON_CONSUMABLE_ITEM_ID))
                {
                    m_storeUiManager.DisableBuyItemButton();
                }
                else if (productId.Equals(CONSUMABLE_FIFTY_COINS_ID))
                {
                    m_storeUiManager.AddCoins(50);
                }
                else if (productId.Equals(CONSUMABLE_ONE_HUNDREND_COINS_ID))
                {
                    m_storeUiManager.AddCoins(100);
                }
            }
            catch (IAPSecurityException)
            {
                Debug.Log($"Receipt validation failed. The content will not be unlocked.");
            }
        }

        return PurchaseProcessingResult.Complete;
    }

    /// <summary>
    /// Handles a failed purchase. In this app, we are only logging the failure.
    /// </summary>
    /// <param name="product">Product that failed to be purchased.</param>
    /// <param name="failureDescription">Object with information about the failure.</param>
    public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
    {
        LogPurchaseFailure(product, failureDescription.reason, failureDescription.message);
    }

    /// <summary>
    /// Handles a failed purchase. In this app, we are only logging the failure.
    /// </summary>
    /// <param name="product">Product that failed to be purchased.</param>
    /// <param name="failureReason">The failure reason.</param>
    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    {
        LogPurchaseFailure(product, failureReason);
    }

    /// <summary>
    /// Initiates the purchase of the non-consumable item.
    /// </summary>
    public void OnBuyNonConsumableItem()
    {
        BuyProductID(NON_CONSUMABLE_ITEM_ID);
    }

    /// <summary>
    /// Initiates the purchase of 100 coins (consumable).
    /// </summary>
    public void OnBuyOneHundredCoins()
    {
        BuyProductID(CONSUMABLE_ONE_HUNDREND_COINS_ID);
    }

    /// <summary>
    /// Initiates the purchase of 50 coins (consumable).
    /// </summary>
    public void OnBuyFiftyCoins()
    {
        BuyProductID(CONSUMABLE_FIFTY_COINS_ID);
    }

    /// <summary>
    /// Initiates a purchase or notifies the StoreUiManager script to display an error when 
    /// the store is unreachable (i.e not initialized).
    /// </summary>
    /// <param name="productId">The id of the product to purchase.</param>
    private void BuyProductID(string productId)
    {
        if (IsInitialized())
        {
            Product product = m_StoreController.products.WithID(productId);

            if (product != null && product.availableToPurchase)
            {
                Debug.Log($"Initiate purchase of {product.definition.id}.");
                m_StoreController.InitiatePurchase(product);
            }
            else
            {
                Debug.Log($"Failed to purchase {productId}. " +
                          "It is either not found or not available for purchase.");
            }
        }
        else
        {
            Debug.Log("Cannot buy a product. Store is not initialized.");
            m_storeUiManager.NotifyUnreachableStore();
        }
    }

    /// <summary>
    /// Returns true if the passed in product is purchased. 
    /// </summary>
    /// <param name="product">Product instance.</param>
    /// <returns>True if the passed in product is purchased</returns>
    private bool IsPurchased(Product product)
    {
        // Verify that a receipt for the product exists.
        // This is only applicable when the app is installed from
        // Google Play store.
        return product.receipt != null &&
               m_GooglePlayStoreExtensions != null &&
               !m_GooglePlayStoreExtensions.IsPurchasedProductDeferred(product);
    }

    /// <summary>
    /// Logs a failed purchase.
    /// </summary>
    /// <param name="product">Product that failed to be purchased.</param>
    /// <param name="failureReason">The failure reason.</param>
    /// <param name="message">Details about the failure.</param>
    private void LogPurchaseFailure(
        Product product, 
        PurchaseFailureReason failureReason, 
        string message = null)
    {
        string logMsg = $"Failed to purchase {product.definition.id}. " +
                        $"Reason: {failureReason}.";

        if (message != null)
        {
            Debug.Log($"{logMsg} Details: {message}");
        }
        else
        {
            Debug.Log(logMsg);
        }
    }

    /// <summary>
    /// Handles deferred purchases. In this app, we're only adding a log message.
    /// </summary>
    /// <param name="product">The unity purchasing product instance.</param>
    private void OnDeferredPurchase(Product product)
    {
        Debug.Log($"Purchase of {product.definition.id} is deferred.");
    }
}
  1. Since the PurchaseManager script doesn’t belong to any UI component, we will attach it to the main camera. To do that, select Main Camera from Hierarchy, then click on the Add Component button in Inspector and select Scripts -> Purchase Manager.
  2. In Main Camera’s Inspector, you should now see a field called Store Ui Manager in the Purchase Manager (Script) component. Drag StoreMenu from Hierarchy, drop it in that field and save the scene.

Implement the Buy Buttons

Now that the PurchaseManager script is complete, we need to enable the buttons from the store menu to initiate purchases when they’re clicked.

BuyItemButton

Do the following to enable the Buy Item button to initiate a purchase when it’s clicked:

  1. In the Unity editor, click on BuyItemButton from Hierarchy and go to Inspector.
  2. Under Button -> On Click (), click on the plus button at the bottom right.
  3. Now, you should see 2 drop down buttons and a field called None (Object) under On Click(). Drag Main Camera from Hierarchy and drop it in the None (Object) field.
  4. Still in Button -> On Click (), click on the drop down at the right that shows No Function and select PurchaseManager -> OnBuyNonConsumableItem().
  5. Save the scene.

Now, the Buy Item button should initiate a purchase when clicked. To verify that, go to the Game tab and click on play.

Screenshot of the Store Menu scene in the Game tab in Unity where the play button is highlighted.

Then, click on Buy Item and verify that the “initiate purchase of ….” line appears in the console logs.

Debug line log that reads "Initiate purchase of com.mycompany.iapdemo.nonconsumable.".

If you used a different domain and company name on your non-consumable product id, then you should see a different product id in your logs.

After verifying the logs, stop the game session by clicking on the play button again.

Note

When the demo is ran from the Unity editor, a fake store is used by default. So, clicking Buy Item, Buy 50 Coins or Buy 100 Coins will not initiate an actual purchase.

Buy50CoinsButton

Do the following to enable the Buy 50 Coins button to initiate a purchase when it’s clicked:

  1. In the Unity editor, click on Buy50CoinsButton from Hierarchy and go to Inspector.
  2. Under Button -> On Click (), click on the plus button at the bottom right.
  3. Now, you should see 2 drop down buttons and a field called None (Object) under On Click(). Drag Main Camera from Hierarchy and drop it in the None (Object) field.
  4. Still in Button -> On Click (), click on the drop down at the right that shows No Function and select PurchaseManager -> OnBuyFiftyCoins().
  5. Save the scene.
  6. Click on the play button to run the scene. 
  7. Click on Buy 50 Coins and verify that the “Initiate purchase of …” line appears in the console logs when the button is clicked.
  8. Stop the game by clicking on the play button again.

Buy100CoinsButton

Do the following to enable the Buy 100 Coins button to initiate a purchase when it’s clicked:

  1. In the Unity editor, click on Buy100CoinsButton from Hierarchy and go to Inspector.
  2. Under Button -> On Click (), click on the plus button at the bottom right.
  3. Now, you should see 2 drop down buttons and a field called None (Object) under On Click(). Drag Main Camera from Hierarchy and drop it in the None (Object) field.
  4. Still in Button -> On Click (), click on the drop down at the right that shows No Function and select PurchaseManager -> OnBuyOneHundredCoins().
  5. Save the scene.
  6. Click on the play button to run the scene.
  7. Click on Buy 100 Coins and verify that the “Initiate purchase of …” line appears in the console logs when the button is clicked.
  8. Stop the game by clicking on the play button again.

Configure the Player Settings

In this section, we will set up the player settings for the Android platform builds that will be uploaded to the Google Play Console. First, we need to set up the company name, product name and version.

  1. In the Unity editor, go to Edit -> Project Settings.
  2. Click on Player on the left sidebar.
  3. Set the Company Name, Product Name and Version with values of your choice.

Second, we need to set a few settings related to the Android release packages. See the next subsection for the steps.

Package Identification and Configuration

  1. In the Unity editor, go to Edit -> Project Settings.
  2. Click on Player on the left sidebar.
  3. From the player settings, select the Android icon and expand the Other Settings section.
  4. Enter the Package Name under Identification->Package Name. In case a caution message about a naming convention appears below Package Name, change the name according to the convention that is specified in the message.
  5. Enter the Version and Bundle Version Code.
  6. Select the Target API Level according to the requirements for Google Play apps.
  7. Select IL2CPP from the Configuration->Scripting Backend drop down.
  8. Under Configuration->Target Architectures, check both ARMv7 and ARM64.

Finally, we need to create a keystore to use for code signing the game’s release package as the Google Play Console does not accept unsigned packages.

Create a Keystore

  1. In the Unity editor, go to Edit -> Project Settings.
  2. Click on Player on the left sidebar.
  3. From the player settings, select the Android platform icon and expand the Publishing Settings section.
  4. Click on Keystore Manager…
  5. Click on the Keystore… drop down.
  6. Select Create New from the drop down and you will get two choices where to save the keystore. For this app, I chose Anywhere. To know more about the difference between Anywhere and In Dedicated Location, go to the Unity engine documentation in https://docs.unity3d.com/2022.3/Documentation/Manual/android-keystore-create.html.
  7. Select the path where you want to save the keystore.
  8. Create a password for the keystore and enter the rest of the information under New Key Values.
  9. Click on Add Key.
  10. Click on Yes if you are asked whether you want to set your new key and keystore as your Project Keystore and Project Key.

Now, we are ready to build a package of the game for the Android platform.

Build a Package for the Android Platform

  1. In the Unity editor, go to Edit -> Project Settings.
  2. Click on Player on the left sidebar.
  3. Under Publishing Settings check Custom Keystore. if Path doesn’t show the keystore you created in the previous step, then click on “Select…” and select the keystore you created in the previous step.
  4. Enter the Password for the Project Keystore.
  5. Enter the Password for the Project Key.
  6. Close the Project Settings window.
  7. Go to File -> Build Settings…
  8. Select Android under Platform.
  9. Check Build App Bundle (Google Play).
  10. Click on Build.
  11. Choose the path where you want to save the .aab package and click on Save.

Now, you can use the .aab package that was generated to upload it to your internal release on Google Play Console.

Create an Internal Release on the Google Play Console

  1. Login to the Google Play Console.
  2. Go to All apps.
  3. Select your app from the All apps list.
  4. On the left side bar, go to Testing -> Internal testing.
  5. Click on the Create new release button at the top right corner of the screen.
  6. Drag and drop your .aab build (or upload it) to the section under App bundles.
  7. Enter the Release name and Release notes.
  8. Click on Save.
  9. After reviewing the release, click on Start rollout to Internal testing.

Add In-App Products on Google Play Console

Once the internal release is rolled out, we need to create and activate the in-app products from the Google Play Console so that the in-app products are available for purchase from the game.

First, create the non-consumable item by doing the following steps:

  1. Login to Google Play Console and select your app from All apps.
  2. On the left side bar, scroll to the Monetize section and click on in-app products.
  3. Click on the Create product button on the right side of the screen.
  4. Then set the following values:
    • Product ID: [the Product ID that you have set for the non-consumable item in PurchaseManager.cs]
    • Name: Set a name of your choice
    • Description: Set a description of your choice
    • Set a price of your choice.
    • Click on Save.
    • Then review the Product details and once you’re satisfied with the details, click on Activate.

Repeat the same steps to create the 50 coins and 100 coins consumables.

Next, you need to add your email address to the list of internal testers so that you can access the internal release to install and test the game on an Android device.

Create an Email List of Internal Testers

  1. Login to Google Play Console and select your app from All apps.
  2. Go to Testing -> Internal testing.
  3. Go to the Testers tab.
  4. Click on Create email list.
  5. Add your email address to the list then click on Save changes.

Install the App on an Android Device

Now, you need to get the web link to access and download the app from an Android device. To get the link:

  1. Login to Google Play Console and select your app from All apps.
  2. Go to Testing -> Internal testing.
  3. Scroll down and click on Copy link.
  4. Open a web browser on the Android device and go to the link you copied from the previous step.
  5. Once the page opens, you should see a link where you can download the app from Google Play. Click on that link.
  6. Now you should see an Install button. Click on it to install the app.

Recap

In this tutorial, you have learned how to do the following:

  • Enable IAP for a project that uses the Unity engine.
  • Implement C# scripts for IAP on a game that uses the Unity engine and released on the Google Play store.
  • Generate receipt validation scripts using the Unity editor.
  • Generate a release package using the Unity editor for a game that can be installed on the Android platform.
  • Setup internal testing from Google Play Console to test your game on the Android platform.

We hope you found this tutorial useful! if so, we would appreciate if you can share it with your friends and colleagues by clicking the “Tweet” link below.