How to use Unity IAP with Pley?

A simple example on how to use a Unity IAP setup on Pley

You can implement a custom store when working with Unity IAP / Purchases. Here is a simple implementation example.

  1. Firstly we must create a Purchasing Module by inheriting from AbstractPurchasingModule!
using UnityEngine.Purchasing.Extension;

namespace InAppPurchases
{
    public class PleyPurchasingModule : AbstractPurchasingModule
    {
        private static PleyPurchasingModule module;

        public override void Configure() => RegisterStore("PleyStore", new PleyStore());

        public static PleyPurchasingModule Instance() => module ??= new PleyPurchasingModule();
    }
}
  1. Now that we've done that, we'll need to implement the IStore interface.
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Pley;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;

namespace InAppPurchases
{
    public class PleyStore : IStore
    {
        private IStoreCallback callback;
        private Queue<PaymentsKit.Entitlement> unconsumedEntitlements = new();

				//define all the products you want available here from the Game Manager's In-game purchases section
        public static readonly Dictionary<string, string> PleyProductIds = new()
        {
            {"ff27679c-7e4d-11ee-a322-cfcda8049fa7", "1Gold"},
            {"08071b50-7e4e-11ee-a322-17e33241f0d5", "2Gold" },
            {"1194a732-7e4e-11ee-8129-6f71d48378f7", "5Gold" }
        };
        
        #region Store
        
        public void Initialize (IStoreCallback callback)
        {
            Debug.Log("[PleyStore] - Initializing");
        
            if (callback != null)
                this.callback = callback;
            else Debug.LogError("[PleyStore] - IStoreCallback is null on Initialize");
        }
        
        public void RetrieveProducts (ReadOnlyCollection<ProductDefinition> products)
        {
            Debug.Log("[PleyStore] - Retrieve products!");
            GetProductDescriptions(products);
        }

        public async void Purchase(ProductDefinition product, string developerPayload)
        {
            Debug.Log($"[PleyStore] - Purchase {product.storeSpecificId}");
        
            var pleyProductId = "";
            foreach (var kvp in PleyProductIds.Where(kvp => kvp.Value == product.id))
            {
                pleyProductId = kvp.Key;
                break;
            }

            if (string.IsNullOrEmpty(pleyProductId))
            {
            		Debug.LogError("[PleyStore] - Unable to find product info for "+pleyProductId);
            		return;
            }

            var (result, paymentData) = await PaymentsKit.RequestPaymentAsync(pleyProductId);
            
            if (result.IsOk())
                callback.OnPurchaseSucceeded(product.storeSpecificId, "Pley Purchase Success", paymentData.entitlementId);
            else
                callback.OnPurchaseFailed(
                    new PurchaseFailureDescription(
                        product.storeSpecificId, 
                        PurchaseFailureReason.PaymentDeclined, 
                        result.ToString()
                    ));
        }

        public void FinishTransaction (ProductDefinition product, string transactionId)
        {
            Debug.Log($"[PleyStore] - Finished Transaction #{transactionId} : {product}");
        }
        #endregion
        
        private async void GetProductDescriptions(ReadOnlyCollection<ProductDefinition> products)
        {
            var (result, productsData) = await PaymentsKit.GetProductsAsync(PleyProductIds.Keys.ToArray());

            if (!result.IsOk())
            {
                Debug.LogError("[PleyStore] - Failed to get Products : " + result);
                return;
            }

            var list = new List<ProductDescription>();

            foreach (var productData in productsData)
            {
                if (productData.result.IsError() || !PleyProductIds.TryGetValue(productData.product.id, out var pleyProductName))
                    continue;

                foreach (var product in products)
                {
                    if (product.id == pleyProductName)
                    {
                        list.Add(new ProductDescription(
                            product.id,
                            new ProductMetadata(
                                productData.product.price.ToString(),
                                productData.product.name, 
                                string.Empty, 
                                productData.product.price.currencyIso4217, 
                                (decimal)productData.product.price.Amount
                            )));
                    }
                }
            }

            StartConsumeEntitlements();
            callback.OnProductsRetrieved(list);
        }
        
        private async void StartConsumeEntitlements()
        {
            var (result, entitlements) = await PaymentsKit.GetEntitlementsAsync();

            if (!result.IsOk())
            {
                Debug.LogError("[PleyStore] - failed to get entitlements.");
                return;
            }
        
            if(entitlements.Length <= 0)
            {
                Debug.Log("[PleyStore] - No unconsumed entitlements pending.");
                return;
            }

            unconsumedEntitlements = new Queue<PaymentsKit.Entitlement>(entitlements);
            IAPHandler.Instance.StartCoroutine(ConsumeRoutine());
        }

        private IEnumerator ConsumeRoutine()
        {
            while(unconsumedEntitlements.TryDequeue(out var current))
            {
                yield return new WaitForSeconds(2f); //WaitUntil(() => IAPHandler.Instance.CanPurchase);
                Debug.Log($"[PleyStore] - Consuming unconsumed entitlement: {current.productId} : entitlement: {current.entitlementId}");
                callback.OnPurchaseSucceeded(current.productId, string.Empty, current.entitlementId);
            }
        }
    }
}
  1. Finally, we need to Initialize the store with the correct PurchasingModule for our desired platforms in your IStoreListener or IDetailedStoreListener

				private async void Init()
        {
            await InitializeUnityServices();
            InitializeUnityPurchasing();
        }

        private async Task InitializeUnityServices()
        {
            var initTask = UnityServices.InitializeAsync();
            await initTask;

            if (initTask.IsCompletedSuccessfully)
                Debug.Log("Unity Services Initialized.");
            else Debug.LogError($"Unity Services Failed to Initialize {initTask.Exception}");
        }       

				private void InitializeUnityPurchasing()
        {
            Debug.Log("[IAPHandler] - Configuring UnityPurchasing. Adding Products...");

#if UNITY_EDITOR
            StandardPurchasingModule.Instance().useFakeStoreAlways = true;
            StandardPurchasingModule.Instance().useFakeStoreUIMode = FakeStoreUIMode.DeveloperUser;
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(AppStore.fake));
#elif UNITY_WEBGL
            var builder = ConfigurationBuilder.Instance(PleyPurchasingModule.Instance());
#else
            var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
#endif

            //Add Products
            builder
                .AddProduct("1Gold", ProductType.Consumable)
                .AddProduct("2Gold", ProductType.Consumable)
                .AddProduct("5Gold", ProductType.Consumable);

            Debug.Log("[IAPHandler] - Initializing UnityPurchasing");

            //Initialize UnityPurchasing
            UnityPurchasing.Initialize(this, builder);
        }

📘

Remember!

Products can be added from the IAP Catalogue as opposed to declared manually via code as in the above example.

Always be sure to Initialize Unity Services before attempting to Initialize Unity Purchasing.