Created
November 4, 2021 14:35
-
-
Save yasirkula/a709990882cb7376d4b2d8a06a5d70ca to your computer and use it in GitHub Desktop.
A wrapper script for Unity IAP (In-App Purchases) that can be used for common IAP tasks
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 System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Purchasing; | |
using UnityEngine.Purchasing.Security; | |
public class IAPManager : IStoreListener | |
{ | |
public enum State { PendingInitialize, Initializing, SuccessfullyInitialized, FailedToInitialize }; | |
private static IAPManager m_instance = null; | |
public static IAPManager Instance | |
{ | |
get | |
{ | |
if( m_instance == null ) | |
m_instance = new IAPManager(); | |
return m_instance; | |
} | |
} | |
private State m_initializationState = State.PendingInitialize; | |
public State InitializationState { get { return m_initializationState; } } | |
public bool IsInitialized { get { return m_initializationState == State.SuccessfullyInitialized; } } | |
public delegate void InitializationCallback( bool success ); | |
private InitializationCallback m_onInitialized; | |
public event InitializationCallback OnInitialized | |
{ | |
add | |
{ | |
if( m_initializationState == State.SuccessfullyInitialized || m_initializationState == State.FailedToInitialize ) | |
value?.Invoke( m_initializationState == State.SuccessfullyInitialized ); | |
else | |
m_onInitialized += value; | |
} | |
remove { m_onInitialized -= value; } | |
} | |
public delegate void CompletedPurchaseCallback( Product product ); | |
public CompletedPurchaseCallback OnPurchaseCompleted; | |
public delegate void FailedPurchaseCallback( Product product, PurchaseFailureReason failureReason ); | |
public FailedPurchaseCallback OnPurchaseFailed; | |
public delegate void NativeIAPWindowClosedCallback(); | |
private NativeIAPWindowClosedCallback onIAPWindowClosed; | |
public delegate void NativeRestoreWindowClosedCallback( bool success ); | |
private NativeRestoreWindowClosedCallback onRestoreWindowClosed; | |
private IStoreController storeController; | |
private IExtensionProvider storeExtensions; | |
#pragma warning disable IDE0044 | |
private CrossPlatformValidator purchaseValidator; | |
#pragma warning restore IDE0044 | |
public void Initialize() | |
{ | |
Initialize( null, true ); | |
} | |
public void Initialize( params ProductDefinition[] products ) | |
{ | |
Initialize( products, false ); | |
} | |
public void Initialize( IEnumerable<ProductDefinition> products ) | |
{ | |
Initialize( products, false ); | |
} | |
private void Initialize( IEnumerable<ProductDefinition> products, bool initializeWithIAPCatalog ) | |
{ | |
if( m_initializationState != State.PendingInitialize ) | |
{ | |
Debug.LogWarning( "IAP is already initializing!" ); | |
return; | |
} | |
#if UNITY_EDITOR | |
// Allows simulating failed IAP transactions in the Editor | |
StandardPurchasingModule.Instance().useFakeStoreUIMode = FakeStoreUIMode.StandardUser; | |
#endif | |
ConfigurationBuilder builder = ConfigurationBuilder.Instance( StandardPurchasingModule.Instance() ); | |
if( initializeWithIAPCatalog ) | |
IAPConfigurationHelper.PopulateConfigurationBuilder( ref builder, ProductCatalog.LoadDefaultCatalog() ); | |
else if( products != null ) | |
builder.AddProducts( products ); | |
if( StandardPurchasingModule.Instance().appStore == AppStore.GooglePlay ) | |
builder.Configure<IGooglePlayConfiguration>().SetDeferredPurchaseListener( OnDeferredPurchase ); | |
m_initializationState = State.Initializing; | |
UnityPurchasing.Initialize( this, builder ); | |
} | |
public void Purchase( string productID, NativeIAPWindowClosedCallback onIAPWindowClosed = null ) | |
{ | |
if( !IsInitialized ) | |
{ | |
Debug.LogWarning( "IAP isn't initialized yet, can't purchased items!" ); | |
onIAPWindowClosed?.Invoke(); | |
return; | |
} | |
this.onIAPWindowClosed = onIAPWindowClosed; | |
storeController.InitiatePurchase( productID ); | |
} | |
public void RestorePurchases( NativeRestoreWindowClosedCallback onRestoreWindowClosed = null ) | |
{ | |
if( !IsInitialized ) | |
{ | |
Debug.LogWarning( "IAP isn't initialized yet, can't restore purchases!" ); | |
onRestoreWindowClosed?.Invoke( false ); | |
return; | |
} | |
this.onRestoreWindowClosed = onRestoreWindowClosed; | |
switch( StandardPurchasingModule.Instance().appStore ) | |
{ | |
case AppStore.AppleAppStore: storeExtensions.GetExtension<IAppleExtensions>().RestoreTransactions( ( success ) => OnNativeRestoreWindowClosed( success ) ); break; | |
case AppStore.GooglePlay: storeExtensions.GetExtension<IGooglePlayStoreExtensions>().RestoreTransactions( ( success ) => OnNativeRestoreWindowClosed( success ) ); break; | |
} | |
} | |
public bool IsNonConsumablePurchased( string productID ) | |
{ | |
if( !IsInitialized ) | |
{ | |
Debug.LogWarning( "IAP isn't initialized yet, can't check previous purchases!" ); | |
return false; | |
} | |
if( string.IsNullOrEmpty( productID ) ) | |
{ | |
Debug.LogWarning( "Empty productID is passed!" ); | |
return false; | |
} | |
Product product = storeController.products.WithID( productID ); | |
if( product == null ) | |
{ | |
Debug.LogWarning( "IAP Product not found: " + productID ); | |
return false; | |
} | |
return product.hasReceipt && IsPurchaseValid( product ); | |
} | |
void IStoreListener.OnInitialized( IStoreController storeController, IExtensionProvider storeExtensions ) | |
{ | |
this.storeController = storeController; | |
this.storeExtensions = storeExtensions; | |
if( StandardPurchasingModule.Instance().appStore == AppStore.AppleAppStore ) | |
storeExtensions.GetExtension<IAppleExtensions>().RegisterPurchaseDeferredListener( OnDeferredPurchase ); | |
// The CrossPlatform validator only supports Google Play and Apple App Store | |
switch( StandardPurchasingModule.Instance().appStore ) | |
{ | |
case AppStore.GooglePlay: | |
case AppStore.AppleAppStore: | |
case AppStore.MacAppStore: | |
{ | |
#if !UNITY_EDITOR | |
//byte[] appleTangleData = AppleStoreKitTestTangle.Data(); // While testing with StoreKit Testing | |
byte[] appleTangleData = AppleTangle.Data(); | |
purchaseValidator = new CrossPlatformValidator( GooglePlayTangle.Data(), appleTangleData, Application.identifier ); | |
#endif | |
break; | |
} | |
} | |
m_initializationState = State.SuccessfullyInitialized; | |
m_onInitialized?.Invoke( true ); | |
} | |
void IStoreListener.OnInitializeFailed( InitializationFailureReason error ) | |
{ | |
Debug.LogWarning( "IAP initialization failed: " + error ); | |
m_initializationState = State.FailedToInitialize; | |
m_onInitialized?.Invoke( false ); | |
} | |
PurchaseProcessingResult IStoreListener.ProcessPurchase( PurchaseEventArgs purchaseEvent ) | |
{ | |
try | |
{ | |
Product product = purchaseEvent.purchasedProduct; | |
if( IsPurchaseValid( product ) ) | |
{ | |
if( StandardPurchasingModule.Instance().appStore == AppStore.GooglePlay && storeExtensions.GetExtension<IGooglePlayStoreExtensions>().IsPurchasedProductDeferred( product ) ) | |
{ | |
// The purchase is deferred; therefore, we do not unlock the content or complete the transaction. | |
// ProcessPurchase will be called again once the purchase is completed | |
return PurchaseProcessingResult.Pending; | |
} | |
OnPurchaseCompleted?.Invoke( product ); | |
} | |
return PurchaseProcessingResult.Complete; | |
} | |
finally | |
{ | |
OnNativeIAPWindowClosed(); | |
} | |
} | |
void IStoreListener.OnPurchaseFailed( Product product, PurchaseFailureReason failureReason ) | |
{ | |
Debug.LogWarning( $"IAP purchase failed for '{product.definition.id}': {failureReason}" ); | |
OnPurchaseFailed?.Invoke( product, failureReason ); | |
OnNativeIAPWindowClosed(); | |
} | |
private void OnDeferredPurchase( Product product ) | |
{ | |
Debug.Log( $"IAP purchase of {product.definition.id} is deferred" ); | |
OnNativeIAPWindowClosed(); | |
} | |
private bool IsPurchaseValid( Product product ) | |
{ | |
if( purchaseValidator != null ) | |
{ | |
try | |
{ | |
purchaseValidator.Validate( product.receipt ); | |
} | |
catch( IAPSecurityException reason ) | |
{ | |
Debug.LogWarning( "Invalid IAP receipt: " + reason ); | |
return false; | |
} | |
} | |
return true; | |
} | |
private void OnNativeIAPWindowClosed() | |
{ | |
try | |
{ | |
onIAPWindowClosed?.Invoke(); | |
onIAPWindowClosed = null; | |
} | |
catch( Exception e ) | |
{ | |
Debug.LogException( e ); | |
} | |
} | |
private void OnNativeRestoreWindowClosed( bool success ) | |
{ | |
Debug.Log( "IAP purchases restored: " + success ); | |
try | |
{ | |
onRestoreWindowClosed?.Invoke( success ); | |
onRestoreWindowClosed = null; | |
} | |
catch( Exception e ) | |
{ | |
Debug.LogException( e ); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How To
IAPManager.Instance.OnInitialized
event (if IAP was already initialized when a function is registered to this event, the function is invoked immediately). Alternatively, you can check the status ofIAPManager.Instance.IsInitialized
and/orIAPManager.Instance.InitializationState
properties. Most IAP functions won't work while IAP isn't initialized yetIAPManager.Instance.Purchase( string productID, NativeIAPWindowClosedCallback onIAPWindowClosed = null )
. NativeIAPWindowClosedCallback doesn't take any parameters and is invoked when the IAP purchase dialog is closed. Example usage:IAPManager.Instance.OnPurchaseCompleted
andIAPManager.Instance.OnPurchaseFailed
events. You'll want to register to these events before calling Initialize because deferred purchases can invoke these events immediately after the initialization. For example:bool IAPManager.Instance.IsNonConsumablePurchased( string productID )
. For example:IAPManager.Instance.RestorePurchases( NativeRestoreWindowClosedCallback onRestoreWindowClosed = null )
. NativeRestoreWindowClosedCallback takes a bool parameter storing whether or not the restore operation was successful. Example usage: