IAP Validation

Introduction

Cheaters and invalid IAP transactions can cause Adjust & Looker dashboards to display inaccurate values and the UA networks to train their algorithms on bad data. This can lead to the contamination of UA algorithms' decision-making process and incorrect game design choices.

LionSDK offers static methods for IAP receipt validation to help reduce or eliminate the negative impacts of cheaters’ IAP transactions.

By default, we do not block cheaters from cheating - they can still get their “stolen” goods; however, we do prevent event data from being sent to networks and flag those transactions as cheaters so that we filter out bad data. However, you can optionally block bad purchases; see the examples below.

What does the package do?

When configured correctly:

  • Fires an inapp_purchase event to the backend for every purchase.

    • Includes a ValidationStatus field indicating whether the purchase is valid.

  • Fires a network-specific revenue event iap_purchase for Adjust, depending on the validation result.

Requirements

  • LionSDK package installed.

  • Unity In-App Purchasing package installed and configured (5.x.x supported).

  • For external validators (i.e Adjust or Nakama), additional setup is required. This setup is explained on their own page, as explained in the next section.

  • Selecting the desired Validator in Lion Settings.

Validators

The selected Validator in the Lion Settings determines the behaviour of the system:

  • Adjust Validator Uses Adjust purchase verification APIs to validate receipts and fires Adjust revenue events for valid purchases.

  • Nakama Validator Uses nakama purchase verification APIs to validate receipts and fires Adjust event events for valid purchases.

  • No Validator Performs no real validation; always treats purchases as “not verified” while still allowing your game to send IAP events.

Remove Events (if applicable)

LionSDK will automatically fire the required Adjust and Analytics IAP events. In your Unity Project, you should remove these events from your game code to prevent duplication and overcounting:

  • Remove any existing Adjust IAP-related events (iap_purchase, purchase_failed, purchase_unknown and purchase_notverified)

  • Remove any existing calls to LionAnalytics.InAppPurchase

Implementation

Unity IAP 5.x.x Implementation

Unity IAP 5.x.x introduces a new purchase flow based on Orders, replacing the old ProcessPurchase(PurchaseEventArgs) callback. Because of this, receipt and product metadata are no longer stored in a single structure.

Instead:

  • Some fields come from the Order (receipt-level data).

  • Some fields come from the Product (catalogue metadata defined in Unity IAP). You must combine both to build a single IAPInfo object for validation.

Call IAPValidation.ValidateAndLog()

If you use Unity Purchasing 5.x.x, you typically call IAPValidation.ValidateAndLog() inside your OnPurchaseConfirmed callback. This sends your combined receipt and product metadata to the validation backend and logs the results to LionAnalytics.

Parameters:

  • IAPInfo iapInfo: This structure is used by the IAP Validation system to send all required purchase fields to the validator. Because Unity IAP 5.x.x separates the receipt data (in Order) and product metadata (in Product), You must build a single IAPInfo object by combining values from both structures.

    • Order order: This is the UnityEngine.Purchasing.Order object that Unity Purchasing provides in the OnPurchaseConfirmed callback. From order.Info, you extract:

      • transactionIDorder.Info.TransactionID

      • receiptorder.Info.Receipt

    • Product product: Unity IAP 5.x.x does not pass Product metadata directly through the purchase callback. You must look up the product using:

      var unityProduct = _storeController.GetProductById(pInfo.productId);
    • The following parameters are required to construct an IAPInfo instance:

      • transactionID

      • productID

      • receipt

      • isoCurrencyCode

      • localizedPrice

      • localizedTitle

      • productType

  • IAPGameplayInfo iapGameplayInfo: This object contains information required by LionAnalytics, including details about the in-game rewards. These include:

    • List<Item> ReceivedItems

    • List<VirtualCurrency> ReceivedCurrencies

    • String PurchaseLocation

See LionAnalytics documentation for further descriptions of these fields.

  • Action onSuccess (optional): This callback, if provided, will be raised if the receipt is valid.

  • Action onFailure (optional): This callback, if provided, will be raised if the receipt has not been validated. The ValidationStatus will provide the reason for the failure to validate.

  • Dictionary<string, object> additionalData (optional): This info will be stamped on to the Lion Analytics InAppPurchase and LionDebug events that get fired after validation is complete.

If you are not using Unity’s Purchasing package, you can replace the Product product parameter with IAPInfo iapInfo. This object must be constructed with the following elements: transactionID, productID, receiptPayload, isoCurrencyCode, localizedPrice, and localizedTitle.

Examples:


Simple inline call:

using LionStudios.Suite.Purchasing;
using UnityEngine.Purchasing;

private void OnPurchaseConfirmed(Order order)
{
    var info = order.Info;
    foreach (var pInfo in info.PurchasedProductInfo)
    {
        var unityProduct = _storeController.GetProductById(pInfo.productId);
        
        var iapInfo = new IAPInfo(
            info.TransactionID,
            unityProduct.definition.id,
            info.Receipt,
            unityProduct.metadata.isoCurrencyCode,
            (double)unityProduct.metadata.localizedPrice,
            unityProduct.metadata.localizedTitle,
            unityProduct.definition.type
        );
        
        var gameplayInfo = new IAPGameplayInfo(
            new List<Item>()
            {
                new Item("NoAds", 1),
                new Item("SpecialWeapon", 1),
                new Item("BonusCards", 10)
            },
            new List<VirtualCurrency>()
            {
                new VirtualCurrency("coins", "normal", 10000),
                new VirtualCurrency("gems", "special", 10)
            },
            "shop"
        );
        
        IAPValidation.ValidateAndLog(iapInfo, gameplayInfo);
    }
}

Inline call with additional data:

using LionStudios.Suite.Purchasing;
using UnityEngine.Purchasing;

private void OnPurchaseConfirmed(Order order)
{
    var info = order.Info;
    foreach (var pInfo in info.PurchasedProductInfo)
    {
        var unityProduct = _storeController.GetProductById(pInfo.productId);
        
        var iapInfo = new IAPInfo(
            info.TransactionID,
            unityProduct.definition.id,
            info.Receipt,
            unityProduct.metadata.isoCurrencyCode,
            (double)unityProduct.metadata.localizedPrice,
            unityProduct.metadata.localizedTitle,
            unityProduct.definition.type
        );
        
        var gameplayInfo = new IAPGameplayInfo(
            new List<Item>()
            {
                new Item("NoAds", 1),
                new Item("SpecialWeapon", 1),
                new Item("BonusCards", 10)
            },
            new List<VirtualCurrency>()
            {
                new VirtualCurrency("coins", "normal", 10000),
                new VirtualCurrency("gems", "special", 10)
            },
            "shop"
        );
        
        IAPValidation.ValidateAndLog(
            iapInfo,
            gameplayInfo,
            additionalData: new Dictionary<string, object>()
            {
                { "PlayerRank", GameManager.PlayerRank },
                { "Difficulty", GameManager.Difficulty }
            });
    }
}

Full OnPurchaseConfirmed Example (Unity IAP 5.x.x)

using LionStudios.Suite.Purchasing;
using UnityEngine.Purchasing;

private void OnPurchaseConfirmed(Order order)
{
    var info = order.Info;
    foreach (var pInfo in info.PurchasedProductInfo)
    {
        string productId = pInfo.productId;
        Product unityProduct = _storeController.GetProductById(productId);
        IAPGameplayInfo gameplayInfo;
        switch (productId)
        {
            case "com.company.game.noads":
                gameplayInfo = new IAPGameplayInfo(
                    new List<Item>() { new Item("NoAds", 1) },
                    new List<VirtualCurrency>(),
                    "ingame"
                );
                break;
            case "com.company.game.noadsspecial":
                gameplayInfo = new IAPGameplayInfo(
                    new List<Item>() { new Item("NoAds", 1) },
                    new List<VirtualCurrency>() { new VirtualCurrency("coins", "normal", 1000) },
                    "specialpopup"
                );
                break;
            case "com.company.game.coinpack1":
                gameplayInfo = new IAPGameplayInfo(
                    new List<Item>(),
                    new List<VirtualCurrency>() { new VirtualCurrency("coins", "normal", 1000) },
                    "iapshop"
                );
                break;
            case "com.company.game.coinpack2":
                gameplayInfo = new IAPGameplayInfo(
                    new List<Item>(),
                    new List<VirtualCurrency>() { new VirtualCurrency("coins", "normal", 5000) },
                    "iapshop"
                );
                break;
            default:
                gameplayInfo = new IAPGameplayInfo(null, null, "unknown");
                break;
        }
        
        var iapInfo = new IAPInfo(
            info.TransactionID,
            unityProduct.definition.id,
            info.Receipt,
            unityProduct.metadata.isoCurrencyCode,
            (double)unityProduct.metadata.localizedPrice,
            unityProduct.metadata.localizedTitle,
            unityProduct.definition.type
        );
        
        IAPValidation.ValidateAndLog(iapInfo, gameplayInfo);
        // Your code to reward the player
    }
}

Conditional reward

In some rare cases, you’ll want to reward the player only if the receipt is valid. This is not normal behaviour and should be reserved for multiplayer games where cheaters can harm the experience of other players.

In this case, you can do as shown in the example below

using LionStudios.Suite.Purchasing;

private void OnPurchaseConfirmed(Order order)
{
    var info = order.Info;
    foreach (var pInfo in info.PurchasedProductInfo)
    {
        var unityProduct = _storeController.GetProductById(pInfo.productId);
        
        var iapInfo = new IAPInfo(
            info.TransactionID,
            unityProduct.definition.id,
            info.Receipt,
            unityProduct.metadata.isoCurrencyCode,
            (double)unityProduct.metadata.localizedPrice,
            unityProduct.metadata.localizedTitle,
            unityProduct.definition.type
        );
        
        var gameplayInfo = GetGameplayInfoForProduct(unityProduct.definition.id);
        
        IAPValidation.ValidateAndLog(
            iapInfo,
            gameplayInfo,
            onSuccess: () => GiveReward(unityProduct.definition.id),
            onFailure: DisplayError
        );
    }
}
private void GiveReward(string productId)
{
    // Your reward logic
}
private void DisplayError(ValidationStatus status)
{
    // Show message to player/log error
}

Validation

If the package is implemented correctly, for each purchase, two events will be fired:

  • inapp_purchase: a LionAnalytics event used for analytics on Looker.

  • iap_purchase: a native Adjust event used by Adjust for revenue metrics.

Make sure that these events are received by following either of the paths below:

  • If you have access to Looker, for each relevant event, check the following:

    • The event name appears [ref]

  • If you do not have access to Looker, please use the LionAnalytics QA Tool following the instructions here: Event Validation

In Looker,inapp_purchase events have a ValidationStatus parameter that can be:

  • Success: The receipt is valid.

  • Failure: The receipt is invalid. Probably a cheater.

  • Error: Something went wrong when trying to validate the receipt, so we can’t determine d if the receipt is valid.

TroubleShooting

  • Many (or all) transactions showing Error validating IAP/Failure in Looker.

    • Explanation: This kind of failure indicates an error communicating with Google, leading to a Failure status and a validation message, Error validating IAP.

    • Cause: All games published in Lion Studios Google Play account(s) should already have the correct API Service Account credentials in place for all games, so while this is the most common cause for the error, it is not common for our games.

    The cause we see most often is a bug in Google Play that occurs when your IAP bundles are created before linking your API Service Account on Google Play Console. Google assumes you first linked your API Service account, then added your various IAP packages. If you did this out of order, Google won’t recognize your existing IAP bundles.

    • Resolution: Go to your IAP bundle in your Google Play Console and edit some details such as description of the IAP, and save it again. We usually add a space, save changes, then remove the added spaceand save again. Any change made to the IAP bundle will trigger Google to reread the IAP bundles, and should fix the bug so these bogus failures go away.

FAQ

  • I’m seeing the error: Library\\PackageCache\\com.lionstudios.beta.iap@0.1.0-b9\\Runtime\\IAPValidation.cs(128,25): error CS0103: The name ‘STORE NAME’ does not exist in the current context

    • Please switch Unity Build Settings to iOS or Android platform.

Last updated

Was this helpful?