Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Created November 4, 2021 14:35
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save yasirkula/a709990882cb7376d4b2d8a06a5d70ca to your computer and use it in GitHub Desktop.
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
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 );
}
}
}
@yasirkula
Copy link
Author

yasirkula commented Nov 4, 2021

How To

// A) Automatically uses the products declared in IAP Catalog: https://docs.unity3d.com/Packages/com.unity.purchasing@4.1/manual/UnityIAPDefiningProducts.html
IAPManager.Instance.Initialize();

// B) Or, initialize IAP with the specified products (no need to declare these products in IAP Catalog
IAPManager.Instance.Initialize( params ProductDefinition[] products );
IAPManager.Instance.Initialize( IEnumerable<ProductDefinition> products );

// For example:
IAPManager.Instance.Initialize( new ProductDefinition( "com.my.product.remove_ads", ProductType.NonConsumable ) );
  • To get notified when IAP is initialized and ready to use, register to the 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 of IAPManager.Instance.IsInitialized and/or IAPManager.Instance.InitializationState properties. Most IAP functions won't work while IAP isn't initialized yet
  • To initiate an IAP purchase, call IAPManager.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:
// Pause the game while IAP purchase dialog is visible and resume it after the dialog is closed
Time.timeScale = 0f;
IAPManager.Instance.Purchase( "com.my.product.remove_ads", () => Time.timeScale = 1f );
  • To get notified when an IAP purchase is successfully completed or failed, register to the IAPManager.Instance.OnPurchaseCompleted and IAPManager.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:
IAPManager.Instance.OnPurchaseCompleted += ( product ) =>
{
	if( product.definition.id == "com.my.product.remove_ads" )
	{
		// "Remove Ads" product is successfully purchased! You should now remove the ads
	}
};

IAPManager.Instance.Initialize( ... );
  • To check if a non-consumable product was purchased in a previous session, call bool IAPManager.Instance.IsNonConsumablePurchased( string productID ). For example:
IAPManager.Instance.OnInitialized += ( success ) =>
{
	if( success && IAPManager.Instance.IsNonConsumablePurchased( "com.my.product.remove_ads" ) )
	{
		// "Remove Ads" product was purchased in a previous session, don't show ads in this session either!
	}
};
  • To restore user's previous purchases, call IAPManager.Instance.RestorePurchases( NativeRestoreWindowClosedCallback onRestoreWindowClosed = null ). NativeRestoreWindowClosedCallback takes a bool parameter storing whether or not the restore operation was successful. Example usage:
// Pause the game while IAP restore dialog is visible and resume it after the dialog is closed
Time.timeScale = 0f;
IAPManager.Instance.RestorePurchases( ( success ) => Time.timeScale = 1f );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment