How to use Unity IAP with Pley
🧩 Pley Store Integration (Updated for Unity IAP v5+)
This document explains how to integrate the Custom Pley Store with the new Unity IAP (v5+) architecture, and how it differs from the legacy integration that used IStoreListener and IDetailedStoreListener.
📘 Background
Starting with Unity IAP v5, the IAP API and architecture changed significantly, Unity refactored its purchasing layer to separate store logic from listener logic.:
- The
IStoreListenerinterface was replaced by event-based callbacks. - Unity now supports custom store providers via
UnityIAPServices.AddNewCustomStore(). - The new system is async-friendly and integrates with Unity’s new purchasing flow and event system.
If your project was using a custom store in Unity IAP 4.x or earlier, you’ll need to update it to follow the new store wrapper model shown here.
Custom stores now implement Store directly — significantly simplifying the integration model.
🆚 Old vs New Architecture
| Aspect | Old (Unity IAP v4) | New (Unity IAP v5 / PleyStore) |
|---|---|---|
| Interface | IStoreListener, IDetailedStoreListener, IStore | Store |
| Responsibility | Listener handled initialization, purchase callbacks, errors | Store encapsulates connection + purchase flow |
| Entry Point | UnityPurchasing.Initialize(this, builder) | UnityIAPServices.AddNewCustomStore() + SetStoreAsDefault() |
| Custom store setup | Implemented manually via IStore and StandardPurchasingModule | Use PleyStoreWrapper.InitializePleyStore() |
| Initialization callback | OnInitialized(IStoreController, IExtensionProvider) | SDK.OnInitialized event handled internally |
| Purchase flow | ProcessPurchase(PurchaseEventArgs args) | Automatically managed inside PleyStore |
🧱 The New Pley Store Structure
PleyStoreWrapper
PleyStoreWrapperPleyStoreWrapper is a thin integration layer between Unity IAP’s service registration and the actual Pley SDK store implementation.
public class PleyStoreWrapper : IStoreWrapper
{
public Store instance { get; }
public string name => "PleyStore";
private ConnectionState State { get; set; }
private PleyStoreWrapper()
{
State = ConnectionState.Connecting;
instance = new PleyStore();
SDK.OnInitialized += result =>
{
State = result.IsOk()
? ConnectionState.Connected
: ConnectionState.Unavailable;
};
}
public static void InitializePleyStore()
{
var wrapper = new PleyStoreWrapper();
UnityIAPServices.AddNewCustomStore(wrapper);
UnityIAPServices.SetStoreAsDefault(wrapper.name);
}
public ConnectionState GetStoreConnectionState() => State;
}⚠️ Compatibility Note — Upgrading Is Optional
You do not need to migrate your project to the full Unity IAP v5 structure to use the new PleyStore.
The deprecated IStoreListener and IDetailedStoreListener–based integration paths remain fully functional and backward-compatible.
If your existing implementation already uses these interfaces (as described in our legacy documentation), you can continue using them without issue, just a few small changes are needed
👀 See the code examples below to see the difference
🧭 Migration Summary
To migrate existing integrations:
| Step | Action |
|---|---|
| 1 | Remove all IStoreListener and IDetailedStoreListener code |
| 2 | Remove old calls to UnityPurchasing.Initialize |
| 3 | Add call to PleyStoreWrapper.InitializePleyStore() |
| 4 | Remove any redundant custom IStore implementations |
| 5 | Use PleyStore as your single entry point for IAP |
using Pley;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
namespace InAppPurchasing
{
public class PleyStore : Store
{
private readonly string ID = "[PleyStore]";
public override void Connect()
{
ConnectCallback?.OnStoreConnectionSucceeded();
}
public override async void FetchProducts(IReadOnlyCollection<ProductDefinition> products)
{
Dictionary<string, ProductDefinition> dict = products.ToDictionary(p => p.storeSpecificId, p => p);
var productIds = products.Select(p => p.storeSpecificId).ToArray();
var (result, productData) = await PaymentsKit.GetProductsAsync(productIds);
if (result.IsError())
{
Debug.LogError($"{ID} Failed to get Pley products: {result}");
return;
}
var productDescriptions = new List<ProductDescription>();
foreach (PaymentsKit.GetProductData data in productData)
{
if (data.result.IsError())
{
Debug.LogError($"{ID} Failed to get product {data.product.id}: {data.result.ToString()}");
continue;
}
if (!dict.TryGetValue(data.product.id, out ProductDefinition productDefinition))
{
Debug.LogError($"No store specific id found for product {data.product.id}");
continue;
}
productDescriptions.Add(
new ProductDescription(
productDefinition.storeSpecificId,
new ProductMetadata(
data.product.price.ToString(),
data.product.name,
"",
data.product.price.currencyIso4217,
(decimal)data.product.price.Amount)));
}
Debug.Log($"{ID} Fetched Pley Products: {productDescriptions.Count}");
ProductsCallback!.OnProductsFetched(productDescriptions);
}
public override async void Purchase(ICart cart)
{
if(cart.Items().Count < 1)
{
Debug.LogError($"{ID} Attempted to purchase an empty cart");
return;
}
foreach (var cartItem in cart.Items())
{
await PurchaseItem(cartItem);
}
return;
async Task PurchaseItem(CartItem cartItem)
{
Debug.Log($"{ID} Purchase Item cart: {cartItem.Product.definition.storeSpecificId}");
var (result, data) = await PaymentsKit.RequestPaymentAsync(cartItem.Product.definition.storeSpecificId);
if (result.IsError())
{
var reason = result switch
{
PleyResult.PmkPaymentFailed => PurchaseFailureReason.PaymentDeclined,
PleyResult.PmkProductNotFound => PurchaseFailureReason.ProductUnavailable,
PleyResult.PmkPaymentCancelledByUser => PurchaseFailureReason.UserCancelled,
_ => PurchaseFailureReason.Unknown
};
PurchaseCallback?.OnPurchaseFailed(new FailedOrder(cart, reason, "Purchase Failed with reason: " + reason));
}
var pleyOrderInfo = new PleyOrderInfo(GetReceipt(data.entitlementId), data.entitlementId);
PurchaseCallback?.OnPurchaseSucceeded(new PendingOrder(cart, pleyOrderInfo));
}
}
public override void FinishTransaction(PendingOrder pendingOrder)
{
ConfirmCallback?.OnConfirmOrderSucceeded(pendingOrder.Info.TransactionID);
}
public override async void FetchPurchases()
{
var (res, entitlements) = await PaymentsKit.GetEntitlementsAsync();
if (!res.IsOk())
{
Debug.LogError($"{ID} Failed to get entitlements");
return;
}
var orders = new List<Order>();
foreach (var entitlement in entitlements)
{
var product = UnityIAPServices.DefaultProduct().GetProductById(entitlement.productId);
if (product == null)
{
Debug.LogWarning($"{ID} Product not found for entitlement: {entitlement.entitlementId} / {entitlement.productId}");
continue;
}
var cartItem = new CartItem(product);
var cart = new Cart(cartItem);
var receipt = GetReceipt(entitlement.entitlementId);
var orderInfo = new PleyOrderInfo(receipt, entitlement.entitlementId);
var pendingOrder = new PendingOrder(cart, orderInfo);
orders.Add(pendingOrder);
}
PurchaseFetchCallback?.OnAllPurchasesRetrieved(orders);
}
public override void CheckEntitlement(ProductDefinition product) { }
private string GetReceipt(string entitlementId)
{
var receiptData = new Dictionary<string, object>()
{
{ "entitlement_id", entitlementId },
{ "consume", true }
};
return JsonUtility.ToJson(receiptData);
}
}
}
public class PleyOrderInfo : IOrderInfo
{
public IAppleOrderInfo Apple => null;
public IGoogleOrderInfo Google => null;
public List<IPurchasedProductInfo> PurchasedProductInfo { get; set; }
public string Receipt { get; }
public string TransactionID { get; }
public PleyOrderInfo(string receipt, string transactionID)
{
Receipt = receipt;
TransactionID = transactionID;
PurchasedProductInfo = new List<IPurchasedProductInfo>();
}
}When upgrading from Unity IAP v4
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Pley;
using UnityEngine;
using UnityEngine.Purchasing;
namespace InAppPurchasing
{
public class OldIapImplementation : MonoBehaviour, IDetailedStoreListener
{
public event Action<List<Product>> StoreReady;
[SerializeField] private string[] pleyProductIds =
{
"1194a732-7e4e-11ee-8129-6f71d48378f7",
"08071b50-7e4e-11ee-a322-17e33241f0d5",
"ff27679c-7e4d-11ee-a322-cfcda8049fa7",
"c6d5f6a4-eb10-11ed-92e8-0fa3a3050317",
"6069278c-ea85-11ed-9d90-734931d1a218",
};
private readonly string ID = "[OldIAPImplementation]";
private IStoreController _storeController;
public void Start()
{
PleyStoreWrapper.InitializePleyStore();
InitializeProducts();
}
public void Purchase(Product product)
{
Debug.Log($"{ID} Purchasing Product : {product.definition.id}");
_storeController.InitiatePurchase(product);
}
private void InitializeProducts()
{
var configBuilder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
foreach (var pleyProductId in pleyProductIds)
{
configBuilder.AddProduct(pleyProductId, ProductType.Consumable);
}
UnityPurchasing.Initialize(this, configBuilder);
}
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
_storeController = controller;
StoreReady?.Invoke(controller.products.all.ToList());
}
public void OnInitializeFailed(InitializationFailureReason error)
{
// Handle the error here with UI or logging. Game handles it.
}
public void OnInitializeFailed(InitializationFailureReason error, string message)
{
// Handle the error here with UI or logging. Game handles it.
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
// Save the purchase to your server. Give the user a virtual currency or item.
// Only after the purchase has been saved do you need to call ConfirmPendingPurchase/Return PurchaseProcessingResult.Complete.
FinalizePurchase(args.purchasedProduct.transactionID, args.purchasedProduct);
return PurchaseProcessingResult.Pending;
}
private void FinalizePurchase(string entitlementId, Product product)
{
PaymentsKit.DangerouslyConsumeEntitlementsAsync(new[] { entitlementId }).Then((pleyResult, results) =>
{
if (pleyResult.IsOk())
{
foreach (var entitlement in results)
{
if (entitlement.IsOk())
{
Debug.Log($"{ID} Consumed Entitlement successfully ");
_storeController.ConfirmPendingPurchase(product);
}
else
{
Debug.LogError($"{ID} Failed to consume Entitlement {entitlement.ToString()}");
}
}
}
else
{
Debug.LogError($"{ID} Failed Dangerously Consume Process.. : {pleyResult.ToString()} ");
}
});
}
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
Debug.Log($"{ID} OnPurchaseFailed : {product} : {failureReason}");
// Handle the error here with UI or logging. Game handles it.
}
public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
Debug.Log($"{ID} OnPurchaseFailed : {product} : {failureDescription.reason} : {failureDescription.message}");
// Handle the error here with UI or logging. Game handles it.
}
}
}Example IAP v5 Store Implementation
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Pley;
using UnityEngine;
using UnityEngine.Purchasing;
namespace InAppPurchasing
{
public class IapV5Implementation : MonoBehaviour
{
public event Action<List<Product>> StoreReady;
[SerializeField] private string[] pleyProductIds =
{
"1194a732-7e4e-11ee-8129-6f71d48378f7",
"08071b50-7e4e-11ee-a322-17e33241f0d5",
"ff27679c-7e4d-11ee-a322-cfcda8049fa7",
"c6d5f6a4-eb10-11ed-92e8-0fa3a3050317",
"6069278c-ea85-11ed-9d90-734931d1a218",
};
private readonly string ID = "[IAPV5Implementation]";
private const string storeName = "PleyStore";
private StoreController _storeController;
private IStoreService _storeService;
public async Task Initialize()
{
PleyStoreWrapper.InitializePleyStore();
_storeController = UnityIAPServices.StoreController();
RegisterStoreCallbacks();
await _storeController.Connect();
InitializeProducts();
_storeController.ProcessPendingOrdersOnPurchasesFetched(true);
_storeController.FetchPurchases();
}
private void InitializeProducts()
{
var catalog = new CatalogProvider();
foreach (var pleyProductId in pleyProductIds)
{
catalog.AddProduct(pleyProductId, ProductType.Consumable);
}
catalog.FetchProducts(UnityIAPServices.DefaultProduct().FetchProductsWithNoRetries, storeName);
}
#region CallbackRegistration
private void RegisterStoreCallbacks()
{
//Products
_storeController.OnProductsFetched += OnProductsFetched;
_storeController.OnProductsFetchFailed += OnProductFetchFailed;
//Entitlements
_storeController.OnCheckEntitlement += OnCheckEntitlement;
//Purchases
_storeController.OnPurchasesFetched += OnPurchasesFetched;
_storeController.OnPurchasesFetchFailed += OnPurchaseFetchFailed;
_storeController.OnPurchasePending += OnPurchasePending;
_storeController.OnPurchaseDeferred += OnPurchaseDeferred;
_storeController.OnPurchaseConfirmed += OnPurchaseConfirmed;
_storeController.OnPurchaseFailed += OnPurchaseFailed;
}
private void DeregisterStoreCallbacks()
{
//Products
_storeController.OnProductsFetched -= OnProductsFetched;
_storeController.OnProductsFetchFailed -= OnProductFetchFailed;
//Entitlements
_storeController.OnCheckEntitlement -= OnCheckEntitlement;
//Purchases
_storeController.OnPurchasesFetched -= OnPurchasesFetched;
_storeController.OnPurchasesFetchFailed -= OnPurchaseFetchFailed;
_storeController.OnPurchasePending -= OnPurchasePending;
_storeController.OnPurchaseDeferred -= OnPurchaseDeferred;
_storeController.OnPurchaseConfirmed -= OnPurchaseConfirmed;
_storeController.OnPurchaseFailed -= OnPurchaseFailed;
}
#endregion
public void Purchase(Product product)
{
_storeController.PurchaseProduct(product);
}
#region Products
private void OnProductsFetched(List<Product> products)
{
StoreReady?.Invoke(products);
}
private void OnProductFetchFailed(ProductFetchFailed productFetchFailed)
{
Debug.LogError($"{ID} On Product Fetch Failed on {productFetchFailed.FailedFetchProducts.Count} products: {productFetchFailed.FailureReason}");
}
#endregion
#region Entitlements
private void OnCheckEntitlement(Entitlement obj)
{
Debug.Log($"{ID} OnCheckEntitlement : {obj.Product?.definition.id}");
}
#endregion
#region Purchases
private void OnPurchasesFetched(Orders orders)
{
// Handle the fetched purchases here.
foreach (var pendingOrder in orders.PendingOrders)
OnPurchasePending(pendingOrder);
foreach (var confirmedOrder in orders.ConfirmedOrders)
OnPurchaseConfirmed(confirmedOrder);
foreach (var deferredOrder in orders.DeferredOrders)
OnPurchaseDeferred(deferredOrder);
}
private void OnPurchaseFetchFailed(PurchasesFetchFailureDescription purchasesFetchFailureDescription)
{
Debug.LogError($"{ID} Products Fetched Failure, {purchasesFetchFailureDescription.FailureReason}");
}
private async void OnPurchasePending(PendingOrder pending)
{
// Game would fulfill the purchase here and give the player virtual currency or items.
_storeController.ConfirmPurchase(pending);
}
private void OnPurchaseDeferred(DeferredOrder deferredOrder)
{
Debug.Log($"{ID} OnPurchaseDeferred : {deferredOrder.Info.TransactionID}");
}
private async void OnPurchaseConfirmed(Order confirmed)
{
// You should have a call to your backend here, that will consume the purchase with Pley.
await ConsumeEntitlement(confirmed.Info.TransactionID);
}
private void OnPurchaseFailed(FailedOrder failedOrder)
{
Debug.Log($"{ID} OnPurchaseFailed : {failedOrder.Info.TransactionID}");
}
#endregion
private async Task ConsumeEntitlement(string transactionID)
{
// NOTE: It is not recommended to use the DangerouslyConsumeEntitlements method
// Replace the contents of this method with a call to your backend, see
// https://docs.pley.com/docs/implementing-payments#4---5-consuming-the-entitlement-to-validate-purchase
Debug.LogWarning($"{ID} Calling Dangerously consume Entitlements : {transactionID}");
var consumed = await PaymentsKit.DangerouslyConsumeEntitlementsAsync(new []{transactionID});
if (consumed.Item1.IsError())
{
Debug.LogError($"Failed to consume Entitlements {consumed.Item1.ToString()}: {transactionID}");
}
}
private void OnDestroy()
{
DeregisterStoreCallbacks();
}
}
}