-
-
Save cdhanna/0beeeb9217932fc45d7f4c5e858b9395 to your computer and use it in GitHub Desktop.
The payment delegate for Beamable 0.9.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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