Cross-platform In App Billing Plugin for .NET
I get a lot of questions about architecture and how to unit tests plugins. So here are some things to be aware of for any plugin that I publish.
You totally can! Every plugin I create is based on an interface. The static singleton just gives you a super simple way of gaining access to the platform implementation. Realize that the implementation of the plugin lives in your iOS, Android, Windows, etc. This means you will need to register it there by instantiating a Cross___Implementation from the platform specific projects.
If you are using a ViewModel/IOC approach your code may look like:
public MyViewModel()
{
readonly IPLUGIN plugin;
public MyViewModel(IPLUGIN plugin)
{
this.plugin = plugin;
}
}
CrossInAppBilling.RegisterImplementation(() => new MyBillingImplementation()) replaces the default factory. Register once at startup, before accessing Current or IsSupported. The factory runs lazily, returns an IInAppBilling, and must create a fresh instance when called again after disposal. The plugin owns and disposes that instance. Late registration and null factories/results throw; failures do not fall back to another backend.
On iOS and Mac Catalyst, StoreKit 2 is the default. Apps that need the original StoreKit behavior can register () => new LegacyInAppBillingImplementation(), see the migration guide. Registration affects CrossInAppBilling, not instances constructed directly or by your own DI container.
StoreKit 2 requires one active instance per process, so register it as a singleton when using your own container. An additional active instance is rejected rather than replacing another instance’s listeners.
To learn about unit testing strategies be sure to read my blog: Unit Testing Plugins for Xamarin
This plugin also implements IDisposable on all implementations. This ensure that all events are unregistered from the platform. This include stopping the StoreKit transaction listeners on iOS and Mac Catalyst. Only dispose when you need to and are no longer listening to events and you must call Dispose on the actual static class. The next time you gain access to the CrossInAppBilling.Current a new instance will be created.
CrossInAppBilling.Dispose() also clears a cached factory failure, allowing the retained registration to retry initialization on the next access.
public async Task<bool> MakePurchase()
{
if(!CrossInAppBilling.IsSupported)
return false;
using(var billing = CrossInAppBilling.Current)
{
try
{
var connected = await billing.ConnectAsync(ItemType.InAppPurchase);
if(!connected)
return false;
//make additional billing calls
}
finally
{
await billing.DisconnectAsync();
}
}
CrossInAppBilling.Dispose();
}
It is recommended to not use a using statement, but instead just call the single Dispose on the static class, which will also dispose the Current:
public async Task<bool> MakePurchase()
{
if(!CrossInAppBilling.IsSupported)
return false;
var billing = CrossInAppBilling.Current;
try
{
var connected = await billing.ConnectAsync(ItemType.InAppPurchase);
if(!connected)
return false;
//make additional billing calls
}
finally
{
await billing.DisconnectAsync();
}
//This does all the disposing you need
CrossInAppBilling.Dispose();
}
<= Back to Table of Contents