How to use Unity IAP with Pley?

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

Unity IAP has provided the ability to implement a custom store. Here is a simple implementation example of how to go about it.

Firstly we will need to 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();
    }
}

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);
            }
        }
    }
}

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);
        }

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

*Be sure to Initialize Unity Services before attempting to Initialize Unity Purchasing.