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".

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:

StepsDetail
1. Create Purchaser C# Class• Example: CustomPurchaser.cs
2. Create Purchaser Resolver C# Class• Example: CustomPurchaserResolver.cs
3. Register the purchaser with BeamableProvide
ServiceManager.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 NameDetail
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 NameDetail
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
      }
   }
}