Skip to content

Instantly share code, notes, and snippets.

@cdhanna
Created February 10, 2021 20:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cdhanna/0beeeb9217932fc45d7f4c5e858b9395 to your computer and use it in GitHub Desktop.
Save cdhanna/0beeeb9217932fc45d7f4c5e858b9395 to your computer and use it in GitHub Desktop.
The payment delegate for Beamable 0.9.0
using Beamable.Common;
using Debug = UnityEngine.Debug;
#if UNITY_PURCHASING
using System;
using Beamable.Api;
using Beamable.Coroutines;
using Beamable.Platform.SDK;
using Beamable.Api.Payments;
using Beamable.Service;
using Beamable.Spew;
using UnityEngine;
using UnityEngine.Purchasing;
public class PaymentDelegateUnityIAP : MonoBehaviour
{
private static IStoreController m_StoreController; // The Unity Purchasing system.
private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
public void Awake()
{
if (!ServiceManager.Exists<PaymentDelegate>())
{
Debug.Log("Registering UnityIAP Payment Delegate");
ServiceManager.ProvideWithDefaultContainer<PaymentDelegate>(new PaymentDelegateImpl());
}
}
private class PaymentDelegateImpl : IStoreListener, PaymentDelegate, IServiceResolver<PaymentDelegate>
{
static readonly int[] RETRY_DELAYS = { 1, 2, 5, 10, 20 };
private long _txid;
private Action<CompletedTransaction> _success;
private Action<ErrorCode> _fail;
private Action _cancelled;
private Promise<Unit> _initPromise = new Promise<Unit>();
#if SPEW_IAP || SPEW_ALL
/* Time, in seconds, when initialization started */
private float _initTime;
#endif
public Promise<Unit> Initialize()
{
var platform = ServiceManager.Resolve<PlatformService>();
platform.Payments.GetSKUs().Then(rsp =>
{
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
foreach (var sku in rsp.skus.definitions)
{
builder.AddProduct(sku.name, ProductType.Consumable, new IDs()
{
{ sku.productIds.itunes, AppleAppStore.Name },
{ sku.productIds.googleplay, GooglePlay.Name },
});
}
#if SPEW_IAP
_initTime = Time.time;
#endif
// Kick off the remainder of the set-up with an asynchrounous call, passing the configuration
// and this class' instance. Expect a response either in OnInitialized or OnInitializeFailed.
UnityPurchasing.Initialize(this, builder);
});
return _initPromise;
}
private void ClearCallbacks()
{
_success = null;
_fail = null;
_cancelled = null;
_txid = 0;
}
public string GetLocalizedPrice(string skuSymbol)
{
var product = m_StoreController?.products.WithID(skuSymbol);
return product?.metadata.localizedPriceString ?? "???";
}
public void startPurchase(
string listingSymbol,
string skuSymbol,
Action<CompletedTransaction> success,
Action<ErrorCode> fail,
Action cancelled
)
{
StartPurchase(listingSymbol, skuSymbol)
.Then(tx => success?.Invoke(tx))
.Error(err => fail?.Invoke(err as ErrorCode));
if (cancelled != null) _cancelled += cancelled;
}
public Promise<CompletedTransaction> StartPurchase(string listingSymbol, string skuSymbol)
{
var result = new Promise<CompletedTransaction>();
_txid = 0;
_success = result.CompleteSuccess;
_fail = result.CompleteError;
if (_cancelled == null) _cancelled = () =>
{ result.CompleteError(
new ErrorCode(400, GameSystem.GAME_CLIENT, "Purchase Cancelled"));
};
ServiceManager.Resolve<PlatformService>().Payments.BeginPurchase(listingSymbol).Then(rsp =>
{
_txid = rsp.txid;
m_StoreController.InitiatePurchase(skuSymbol, _txid.ToString());
}).Error(err =>
{
Debug.LogError($"There was an exception making the begin purchase request: {err}");
_fail?.Invoke(err as ErrorCode);
});
return result;
}
// Restore purchases previously made by this customer. Some platforms automatically restore purchases, like Google.
// Apple currently requires explicit purchase restoration for IAP, conditionally displaying a password prompt.
public void RestorePurchases()
{
// If we are running on an Apple device ...
if (Application.platform == RuntimePlatform.IPhonePlayer ||
Application.platform == RuntimePlatform.OSXPlayer)
{
// ... begin restoring purchases
InAppPurchaseLogger.Log("RestorePurchases started ...");
// Fetch the Apple store-specific subsystem.
var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
// Begin the asynchronous process of restoring purchases. Expect a confirmation response in
// the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
apple.RestoreTransactions(result => {
// The first phase of restoration. If no more responses are received on ProcessPurchase then
// no purchases are available to be restored.
InAppPurchaseLogger.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
});
}
// Otherwise ...
else
{
// We are not running on an Apple device. No work is necessary to restore purchases.
InAppPurchaseLogger.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
}
}
//
// --- IStoreListener
//
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
// Purchasing has succeeded initializing. Collect our Purchasing references.
InAppPurchaseLogger.Log("OnInitialized: PASS");
// Overall Purchasing system, configured with products for this application.
m_StoreController = controller;
// Store specific subsystem, for accessing device-specific store features.
m_StoreExtensionProvider = extensions;
_initPromise.CompleteSuccess(PromiseBase.Unit);
RestorePurchases();
#if SPEW_IAP || SPEW_ALL
var elapsed = Time.time - _initTime;
InAppPurchaseLogger.LogFormat("Initialization complete, after {0:#,0.000} seconds.", elapsed);
#endif
}
public void OnInitializeFailed(InitializationFailureReason error)
{
// Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
InAppPurchaseLogger.Log("OnInitializeFailed InitializationFailureReason:" + error);
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
string rawReceipt;
if (args.purchasedProduct.hasReceipt)
{
var receipt = JsonUtility.FromJson<UnityPurchaseReceipt>(args.purchasedProduct.receipt);
rawReceipt = receipt.Payload;
InAppPurchaseLogger.Log($"UnityIAP Payload: {receipt.Payload}");
InAppPurchaseLogger.Log($"UnityIAP Raw Receipt: {args.purchasedProduct.receipt}");
}
else
{
rawReceipt = args.purchasedProduct.receipt;
}
var transaction = new CompletedTransaction(
_txid,
rawReceipt,
args.purchasedProduct.metadata.localizedPrice.ToString(),
args.purchasedProduct.metadata.isoCurrencyCode,
"",
""
);
FulfillTransaction(transaction, args.purchasedProduct);
return PurchaseProcessingResult.Pending;
}
private void FulfillTransaction(CompletedTransaction transaction, Product purchasedProduct)
{
ServiceManager.Resolve<PlatformService>().Payments.CompletePurchase(transaction).Then(_ =>
{
m_StoreController.ConfirmPendingPurchase(purchasedProduct);
_success?.Invoke(transaction);
ClearCallbacks();
}).Error(ex =>
{
Debug.LogError($"There was an exception making the complete purchase request: {ex}");
var err = ex as ErrorCode;
if (err == null)
{
return;
}
var retryable = err.Code >= 500 || err.Code == 429 || err.Code == 0; // Server error or rate limiting or network error
if (retryable)
{
ServiceManager.Resolve<CoroutineService>().StartCoroutine(RetryTransaction(transaction, purchasedProduct));
}
else
{
m_StoreController.ConfirmPendingPurchase(purchasedProduct);
_fail?.Invoke(err);
ClearCallbacks();
}
});
}
// Copied from TransactionManager.cs
private System.Collections.IEnumerator RetryTransaction(CompletedTransaction transaction, Product purchasedProduct)
{
// This block should only be hit when the error returned from the request is retryable. This lives down here
// because C# doesn't allow you to yield return from inside a try..catch block.
var waitTime = RETRY_DELAYS[Math.Min(transaction.Retries, RETRY_DELAYS.Length - 1)];
InAppPurchaseLogger.Log($"Got a retryable error from platform. Retrying complete purchase request in {waitTime} seconds.");
// Avoid incrementing the backoff if the device is definitely not connected to the network at all.
// This is narrow, and would still increment if the device is connected, but the internet has other problems
if (Application.internetReachability != NetworkReachability.NotReachable)
{
transaction.Retries += 1;
}
yield return new WaitForSeconds(waitTime);
FulfillTransaction(transaction, purchasedProduct);
}
public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
{
// A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing
// this reason with the user to guide their troubleshooting actions.
InAppPurchaseLogger.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
var platform = ServiceManager.Resolve<PlatformService>();
var reasonInt = (int) failureReason;
if (failureReason == PurchaseFailureReason.UserCancelled)
{
platform.Payments.CancelPurchase(_txid);
_cancelled?.Invoke();
}
else
{
platform.Payments.FailPurchase(_txid, product.definition.storeSpecificId + ":" + failureReason);
var errorCode = new ErrorCode(reasonInt, GameSystem.GAME_CLIENT, failureReason.ToString() + $" ({product.definition.storeSpecificId})");
_fail?.Invoke(errorCode);
}
ClearCallbacks();
}
#region ServiceResolver
void IServiceResolver.OnTeardown()
{
m_StoreController = null;
m_StoreExtensionProvider = null;
}
bool IServiceResolver<PaymentDelegate>.CanResolve() => true;
bool IServiceResolver<PaymentDelegate>.Exists() => true;
PaymentDelegate IServiceResolver<PaymentDelegate>.Resolve() => this;
#endregion
}
}
[Serializable]
public class UnityPurchaseReceipt
{
public string Store;
public string TransactionID;
public string Payload;
}
#endif // UNITY_PURCHASING
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment