Stores - Custom Purchaser
Allow players to purchase items from in-game stores [ECON-Stores-04]
Overview
This guide includes everything needed to use the Custom Purchaser in the "Beamable SDK for Unity".
Related Guides
A common use case for the feature is covered in the guides. Adding custom Purchaser is part of a larger process.
- See Using Store Flow for more info.
User Experience
When set up properly, the user experience in the game project will be as follows:
Steps
The following steps assumes no dependency on Unity's UnityIAP solution. There is significant custom code required for this solution.
A benefit here is flexible development.
Follow these steps to get started:
Steps | Detail |
---|---|
1. Create Purchaser C# Class | • Example: CustomPurchaser.cs |
2. Create Purchaser Resolver C# Class | • Example: CustomPurchaserResolver.cs |
3. Register the purchaser with Beamable | ProvideServiceManager.Provide(new CustomPurchaserResolver()); CompleteSuccess var customPurchaser = ServiceManager.Resolve<IBeamablePurchaser>(); await customPurchaser.Initialize(); _beamContext.Api.BeamableIAP.CompleteSuccess(customPurchaser); |
4. Verify Success | • Setup a store. See Using Store Flow for more info • Create a scene with the StoreFlow feature prefab • Play the Scene • "Buy" the listing through the StoreFlow Note: The verification process depends on the specifics of the game makers custom purchaser solution |
API
Here are API highlights for IBeamablePurchaser
.
The custom purchaser class must implement this interface.
Method Name | Detail |
---|---|
Initialize | • Setup the chosen IAP implementation |
GetLocalizedPrice | • Fetches the localized string from the provider |
StartPurchase | • Start a purchase through the chosen IAP implementation. |
Here are API highlights for beamableAPI.PaymentService
.
The custom purchaser class must reference these methods and call each if/when appropriate.
Method Name | Detail |
---|---|
GetSKUs | • Get commerce SKUs from platform |
BeginPurchase | • Begin purchase request |
CompletePurchase | • Call after the provider has charged the player. Used to verify the legitimacy of the purchase receipt and complete the item fulfillment process |
CancelPurchase | • Cancel purchase request |
FailPurchase | • Fail purchase request |
Inspiration
The UnityBeamablePurchaser
represents a fully-functional Beamable solution for connecting with Unity's native IAP system. Checkout the source-code. Its included in the "Beamable SDK for Unity".
The custom purchaser class must not reference this class. It's only for reference.
Example
Here are examples which cover common programming needs.
Beamable SDK Examples
• The following example code is available for download at GitHub.com/Beamable_SDK_Examples
The CustomPurchaserExample
showcases how to use the CustomPurchaser
.
using Beamable.Api.Payments;
using Beamable.Service;
using UnityEngine;
namespace Beamable.Examples.Prefabs.StoreFlow.MyCustomPurchaser
{
/// <summary>
/// Implementation of custom Beamable purchasing.
///
/// 1. See the partially-functional <see cref="CustomPurchaser"/> as a template.
/// Copy it and complete your custom implementation.
///
/// 2. See the fully-functional <see cref="UnityBeamablePurchaser"/> (Search
/// in Beamable SDK) for inspiration.
///
/// </summary>
public class CustomPurchasingSystemExample : MonoBehaviour
{
// Unity Methods --------------------------------
protected void Start()
{
Debug.Log("Start()");
SetupBeamable();
}
// Methods --------------------------------------
private async void SetupBeamable()
{
// Order is important here...
// Order #1 - Call for Beamable player context
var beamContext = BeamContext.Default;
await beamContext.OnReady;
Debug.Log($"beamContext.PlayerId = {beamContext.PlayerId}");
// Order #2 - Register the purchaser
ServiceManager.Provide(new CustomPurchaserResolver());
var customPurchaser = ServiceManager.Resolve<IBeamablePurchaser>();
await customPurchaser.Initialize();
beamContext.Api.BeamableIAP.CompleteSuccess(customPurchaser);
// Order #3 - Now use the StoreFlow feature prefab at runtime
// and 'Buy' an item. Watch the Unity Console Window for logging
// to verify success
}
}
}
The CustomPurchaser
is provided as inspiration to game makers. It is not to be used directly. It is partially-functional template to be copied and completed.
using System;
using System.Collections.Generic;
using System.Linq;
using Beamable.Api;
using Beamable.Api.Payments;
using Beamable.Common;
using Beamable.Coroutines;
using Beamable.Service;
using UnityEngine;
namespace Beamable.Examples.Prefabs.StoreFlow.MyCustomPurchaser
{
/// <summary>
/// Implementation of custom Beamable purchasing.
///
/// 1. See this partially-functional <see cref="CustomPurchaser"/> as a template.
/// Copy it and complete your custom implementation.
///
/// 2. See the fully-functional <see cref="UnityBeamablePurchaser"/> (Search
/// in Beamable SDK) for inspiration.
///
/// </summary>
public class CustomPurchaser : BasePurchaser, IBeamablePurchaser
{
// Fields ---------------------------------------
private List<CustomStoreProduct> _customStoreProducts = new List<CustomStoreProduct>();
// Methods --------------------------------------
#region "IBeamablePurchaser"
/// <summary>
/// Begin initialization of Beamable purchasing.
/// </summary>
public override Promise<Unit> Initialize()
{
base.Initialize();
Debug.Log($"CustomPurchaser.Initialize()");
_paymentService.GetSKUs().Then(rsp =>
{
_customStoreProducts.Clear();
foreach (SKU sku in rsp.skus.definitions)
{
Dictionary<string, string> idDictionary = new Dictionary<string, string>
{
{sku.productIds.itunes, AppleAppStore},
{sku.productIds.googleplay, GooglePlay}
};
_customStoreProducts.Add(
new CustomStoreProduct(sku, ProductType.Consumable, idDictionary));
}
// Todo Your Implementation: Determine initialization Success/Failure
// Success
OnInitialized();
// or Failure
// OnInitializeFailed("tbd error");
});
return _initializePromise;
}
/// <summary>
/// Get the localized price string for a given SKU.
/// </summary>
public string GetLocalizedPrice(string skuSymbol)
{
Debug.Log($"CustomPurchaser.GetLocalizedPrice() skuSymbol = {skuSymbol}");
var product = _customStoreProducts.FirstOrDefault(
p => p.SKU.name == skuSymbol);
// Todo Your Implementation: Get the localized price. Not this.
return product?.SKU.realPrice.ToString() ?? "???";
}
/// <summary>
/// Start a purchase for the given listing using the given SKU.
/// </summary>
/// <param name="listingSymbol">Store listing that should be purchased.</param>
/// <param name="skuSymbol">Platform specific SKU of the item being purchased.</param>
/// <returns>Promise containing completed transaction.</returns>
public Promise<CompletedTransaction> StartPurchase(string listingSymbol, string skuSymbol)
{
Debug.Log($"CustomPurchaser.StartPurchase() skuSymbol = {skuSymbol}");
_transactionId = 0;
var completedTransactionPromise = new Promise<CompletedTransaction>();
_onBeginPurchaseSuccess = completedTransactionPromise.CompleteSuccess;
_onBeginPurchaseError = completedTransactionPromise.CompleteError;
if (_onBeginPurchaseCancelled == null)
{
_onBeginPurchaseCancelled = () =>
{
completedTransactionPromise.CompleteError(
new ErrorCode(400, GameSystem.GAME_CLIENT, "Purchase Cancelled"));
};
}
_paymentService.BeginPurchase(listingSymbol).Then(rsp =>
{
_transactionId = rsp.txid;
PaymentService_OnBeginPurchase(skuSymbol, _transactionId);
}).Error(err =>
{
Debug.LogError($"CustomPurchaser.OnBeginPurchaseError() error = {err}");
_onBeginPurchaseError?.Invoke(err as ErrorCode);
});
return completedTransactionPromise;
}
#endregion
/// <summary>
/// Process a completed purchase by fulfilling it.
/// </summary>
/// <param name="product"></param>
/// <returns></returns>
public ProcessingResult ProcessPurchase(CustomStoreProduct product)
{
Debug.Log($"CustomPurchaser.ProcessPurchase() product = {product}");
string rawReceipt;
if (product.HasReceipt)
{
var receipt = JsonUtility.FromJson<CustomPurchaseReceipt>(product.Receipt);
rawReceipt = receipt.Payload;
Debug.Log($"receipt = {receipt}");
}
else
{
rawReceipt = product.Receipt;
}
var transaction = new CompletedTransaction(
_transactionId,
rawReceipt,
GetLocalizedPrice(product.SKU.name),
"tbdIsoCurrencyCode"
);
FulfillTransaction(transaction, product);
return ProcessingResult.Pending;
}
/// <summary>
/// Handle a purchase failure event from IAP.
/// </summary>
/// <param name="product">The product whose purchase was attempted</param>
/// <param name="failureReason">Information about why the purchase failed</param>
public void OnPurchaseFailed(CustomStoreProduct product, string failureReason)
{
Debug.LogError($"CustomPurchaser.OnPurchaseFailed() product = {product.SKU.name}");
// Todo Your Implementation: Setup custom reasons...
if (failureReason == "tbdSomeReason")
{
// Maybe cancel...
_paymentService.CancelPurchase(_transactionId);
_onBeginPurchaseCancelled?.Invoke();
}
else
{
// Todo Your Implementation: Setup custom reasons...
int reasonInt = 1;
string reason = product.SKU.name+ ":" + failureReason;
// Maybe fail...
_paymentService.FailPurchase(_transactionId, reason);
var errorCode = new ErrorCode(reasonInt, GameSystem.GAME_CLIENT, reason);
_onBeginPurchaseError?.Invoke(errorCode);
}
ClearCallbacks();
}
/// <summary>
/// Initiate transaction restoration if needed.
/// </summary>
public void RestorePurchases()
{
Debug.Log($"CustomPurchaser.RestorePurchases() platform = {Application.platform}");
if (Application.platform == RuntimePlatform.IPhonePlayer ||
Application.platform == RuntimePlatform.OSXPlayer)
{
// Todo Your Implementation: For iOS...
}
else
{
// Todo Your Implementation: For other platform(s)...
}
}
/// <summary>
/// Fulfill a completed transaction by completing the purchase in the
/// payments service and informing IAP completion.
/// </summary>
/// <param name="transaction"></param>
/// <param name="product"></param>
protected override void FulfillTransaction(CompletedTransaction transaction,
CustomStoreProduct product)
{
base.FulfillTransaction(transaction, product);
Debug.Log($"CustomPurchaser.FulfillTransaction() SKUSymbol = {transaction.SKUSymbol}");
_paymentService.CompletePurchase(transaction).Then(_ =>
{
PaymentService_OnCompletePurchase(product);
_onBeginPurchaseSuccess?.Invoke(transaction);
ClearCallbacks();
}).Error(ex =>
{
Debug.LogError($"error = {ex.Message}");
var errorCode = ex as ErrorCode;
if (errorCode == null)
{
return;
}
var isRetryable = IsRetryable(errorCode);
if (isRetryable)
{
ServiceManager.Resolve<CoroutineService>().StartCoroutine(
RetryTransaction(transaction, product));
}
else
{
PaymentService_OnCompletePurchase(product);
_onBeginPurchaseError?.Invoke(errorCode);
ClearCallbacks();
}
});
}
// Event Handlers -------------------------------
private void OnInitialized()
{
Debug.Log($"CustomPurchaser.OnInitialized() count = {_customStoreProducts.Count}");
RestorePurchases();
_initializePromise.CompleteSuccess(PromiseBase.Unit);
// Todo Your Implementation: Handle success
}
private void OnInitializeFailed(string error)
{
Debug.LogError($"CustomPurchaser.OnInitializeFailed() error = {error}");
_initializePromise.CompleteError(new Exception(error));
// Todo Your Implementation: Handle failure
}
private void PaymentService_OnBeginPurchase(string skuSymbol, long transactionId)
{
Debug.Log($"OnBeginPurchase() skuSymbol = {skuSymbol}, " +
$"transactionId = {transactionId}");
// Todo Your Implementation: Implement purchase
}
private void PaymentService_OnCompletePurchase(CustomStoreProduct product)
{
Debug.Log($"OnCompletePurchase() product = {product.SKU.name}");
// Todo Your Implementation: Complete the purchase
}
}
}
Updated about 1 year ago