I havenât verified this works for a revoked purchase yet (canât figure out how to do that on the store page yet), but hereâs what is working for verifying they have the purchase.
First, I want to be extra clear about what problem Iâm solving, for future readers:
- We want an in-app purchase to unlock the full game (Android only - via a non-consumable)
- Once theyâve purchased the IAP, we need to actually unlock the content, which is already downloaded and stored locally.
- We need to unlock it in such a way that on subsequent launches, the app is fully unlocked.
- Weâd like the verification process to be Secure (not hackable) and Not require Internet
Our solution is to verify the receipt in OnInitialized, which we believe will work offline as the OS should cache the purchase. We do this on the Title screen.
private void UnlockPreviousPurchase()
{
Debug.Log("UnityIAP: Unlocking Any Previous Purchases");
var product = this.controller.products.WithID(FullGameID);
if (product.availableToPurchase && product.hasReceipt)
{
VerifyAndUnlockPurchaseForReceipt(product.receipt);
}
}
You can see the guts of VerifyAndUnlockPurchaseForReceipt
in the full code below, but itâs largely just the sample code provided by Unity.
Once verified, we just set a static flag that says itâs unlocked. Since itâs static, it gets cleared on the next launch, and we re-verify the purchase again. We do not store anything about the purchase, or whether the app is unlocked. We are thinking this will be secure (not hackable).
The button to purchase the app is on the Title screen, and we hide the button once itâs unlocked.
Full code below, which is mostly taken from the Unity Learn post on IAP:
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;
/// <summary>
/// This class is primarly taken from Unity's example:
/// https://learn.unity.com/tutorial/unity-iap#5c7f8528edbc2a002053b46e
/// </summary>
public class InAppPurchaser : IStoreListener
{
public static string FullGameID = "com.redbluegames.sparklite";
private IStoreController controller;
private IExtensionProvider extensions;
public event System.Action GamePurchased;
public InAppPurchaser()
{
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
builder.AddProduct(FullGameID, ProductType.NonConsumable);
Debug.Log("UnityIAP: Initializing.");
UnityPurchasing.Initialize(this, builder);
}
public void UnlockTheGame()
{
BuyProductID(FullGameID);
}
/// <summary>
/// Called when Unity IAP is ready to make purchases.
/// </summary>
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
Debug.Log("UnityIAP: Initialized");
this.controller = controller;
this.extensions = extensions;
this.UnlockPreviousPurchase();
}
/// <summary>
/// Called when Unity IAP encounters an unrecoverable initialization error.
///
/// Note that this will not be called if Internet is unavailable; Unity IAP
/// will attempt initialization until it becomes available.
/// </summary>
public void OnInitializeFailed(InitializationFailureReason error)
{
Debug.Log("UnityIAP: Initialize FAILED");
}
/// <summary>
/// Called when a purchase completes.
///
/// May be called at any time after OnInitialized().
/// </summary>
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e)
{
VerifyAndUnlockPurchaseForReceipt(e.purchasedProduct.receipt);
Debug.Log(string.Format("UnityIAP: ProcessPurchase: PASS. Product: '{0}'", e.purchasedProduct.definition.id));
return PurchaseProcessingResult.Complete;
}
private void UnlockPreviousPurchase()
{
Debug.Log("UnityIAP: Unlocking Any Previous Purchases");
var product = this.controller.products.WithID(FullGameID);
if (product.availableToPurchase && product.hasReceipt)
{
VerifyAndUnlockPurchaseForReceipt(product.receipt);
}
}
private bool VerifyAndUnlockPurchaseForReceipt(string receipt)
{
bool validPurchase = true;
#if UNITY_ANDROID
Debug.Log("UnityIAP: Validating Receipt: " + receipt);
// Prepare the validator with the secrets we prepared in the Editor
// obfuscation window.
var validator = new CrossPlatformValidator(GooglePlayTangle.Data(),
AppleTangle.Data(), Application.identifier);
try
{
var result = validator.Validate(receipt);
Debug.Log("UnityIAP: Receipt is valid. Contents:");
foreach (IPurchaseReceipt productReceipt in result)
{
Debug.Log(productReceipt.productID);
Debug.Log(productReceipt.purchaseDate);
Debug.Log(productReceipt.transactionID);
}
}
catch (IAPSecurityException)
{
Debug.Log("UnityIAP: Invalid receipt, not unlocking content");
validPurchase = false;
#if UNITY_EDITOR
Debug.Log("UnityIAP: Just kidding, we call invalid receipts in Editor valid because Editor throws IAPSecurityException.");
validPurchase = true;
#endif
}
#endif
if (validPurchase)
{
Debug.Log("UnityIAP: This was a valid purchase. Unlocking content based on purchase.");
ApplicationManager.Instance.SetFullGameUnlocked(true);
GamePurchased?.Invoke();
}
return validPurchase;
}
/// <summary>
/// Called when a purchase fails.
/// </summary>
public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
{
Debug.Log(string.Format("UnityIAP: ProcessPurchase: FAIL. Product: '{0}', Reason: {1}", i.definition.id, p));
}
private void BuyProductID(string productId)
{
if (IsInitialized())
{
Product product = controller.products.WithID(productId);
if (product != null && product.availableToPurchase)
{
Debug.Log(string.Format("UnityIAP: Purchasing product asychronously: '{0}'", product.definition.id));
// Async purchase call
controller.InitiatePurchase(product);
}
else
{
Debug.Log("UnityIAP: BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
}
}
else
{
Debug.Log("UnityIAP: BuyProductID FAIL. Not initialized.");
}
}
private bool IsInitialized()
{
return controller != null && extensions != null;
}
}