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”.