November 6, 2018

In-app Billing in Android using MvvmCross

For a lot of us, in-app billing is the icing on the cake. It can be extremely rewarding seeing the orders roll in month on month. However, it can also be somewhat challenging to set up and test, as well as get it to play nicely with your existing patterns and framework. Fear not; we’ll walk through a typical use case of MvvmCross and the famed Plugin.InAppBilling created by James Montemagno.

Why Plugin.InAppBilling and MvvmCross for In-App billing?

Firstly, Plugin.InAppBilling is the most documented and most popular plugin for cross-platform in-app billing. It is renowned for sparing Xamarin developers the chore of having to write platform-specific in-app billing code for each target platform. As a result of this, all platform abstractions are already baked in, so all you have to do is call 2 (maybe 3) methods.

MvvmCross is a personal favourite. It leverages the Mvvm pattern (which was designed, if we recall, to decouple your implementation of business logic from your UI). This pattern has a magical upside when developing cross-platform applications. This benefit is amplified when using Xamarin Native and have to design a UI for each platform. I also like the way the framework is built along with the extras it provides (IoC/DI, Logging, etc.).

It should be noted that there is a slight navigation issue when using these technologies (due to application life cycle). This is how I solved it.

tl;dr

This assumes you’ve already set up MvvmCross. If not, I recommend following my guide here.

Task<bool> MakePurchase(string sku, ItemType itemType);
Task<bool> UserOwnsProduct(string sku, ItemType itemType);
  • Create a class that implements IBillingService and name it “BillingService” as follows:
public class BillingService : IBillingService
    {
        private bool _isConnected;

        public BillingService()
        {
#if DEBUG
            CrossInAppBilling.Current.InTestingMode = true;
#endif
        }

        public async Task<bool> ConnectAsync()
        {
            try
            {
                if (!_isConnected)
                    _isConnected = await CrossInAppBilling.Current.ConnectAsync();
            }
            catch
            {
                _isConnected = false;
            }

            return _isConnected;
        }

        public async Task<bool> DisconnectAsync()
        {
            try
            {
                if (_isConnected)
                {
                    await CrossInAppBilling.Current.DisconnectAsync();
                    _isConnected = false;
                }
                return true;
            }
            catch
            {
                return false;
            }
        }

        public async Task<bool> MakePurchase(string sku, ItemType itemType)
        {
            try
            {
                //try to purchase item
                var purchase = await CrossInAppBilling.Current.PurchaseAsync(sku, itemType, "apppayload");
                if (purchase == null)
                {
                    //Not purchased, alert the user
                    return false;
                }
                else
                {
                    //Purchased, save this information
                    var id = purchase.Id;
                    var token = purchase.PurchaseToken;
                    var state = purchase.State;

                    GlobalContext.Instance.Logger.LogMessage("Purchase successful!");

                    return true;
                }
            }
            catch (Exception ex)
            {
                GlobalContext.Instance.Logger.LogException(ex, "BillingService:MakePurchase");
                return false;
            }
        }

        public async Task<bool> UserOwnsProduct(string sku, ItemType itemType)
        {
            try
            {
                var purchases = await CrossInAppBilling.Current.GetPurchasesAsync(itemType);
                return purchases.Any(x => x.ProductId == sku);
            }
            catch (Exception ex)
            {
                GlobalContext.Instance.Logger.LogException(ex, "BillingService:UserOwnsProduct");
                return false;
            }
        }
    }
  • Notice how the DEBUG pragma is used to toggle test mode in the plugin. This is handy for testing in-app billing in conjunction with the default Android test product SKU’s
  • Now, make the DI aware of this new service in our App.cs Initialize method
Mvx.IoCProvider.RegisterSingleton(typeof(IBillingService),
                () => new BillingService());
  • We’re now ready to inject our newly created IBillingService into a ViewModel constructor
  • In the ViewModel of choice, create an MvxAsyncCommand to kick off the in-app billing purchase process,
IBillingService _billingService;

        public PurchaseViewModel(IBillingService billingService)
        {
            _billingService = billingService;

            PurchaseClickCommand = new MvxAsyncCommand(MakePurchase);
        }

public IMvxAsyncCommand PurchaseClickCommand { get; set; }

private async Task MakePurchase()
        {
            bool purchaseResult = await _billingService.MakePurchase(GlobalContext.SomeSku, Plugin.InAppBilling.Abstractions.ItemType.Subscription);

            if (purchaseResult)
            {
                _platformService.ShowToastMessage("Thank you for your purchase!");
                // Cannot navigate here either
            }
            else
            {
                _platformService.ShowToastMessage("Purchase unsuccessful. Please try again.");
                await _billingService.DisconnectAsync();
                await _billingService.ConnectAsync();
            }
        }
  • An important thing to note here is that Plugin.InAppBilling relies on the Plugin.CurrentActivity nuget package. This is so that the plugin will have the current application context on which to kick off the in-app billing purchases.
  • In your activity that corresponds with the ViewModel we are injecting IBillingService into, add the following in the OnCreate override:
protected override void OnCreate(Bundle bundle)
        {
            Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity = this;
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.PurchaseView);
        }
  • In the same activity, handle OnActivtyResult
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            base.OnActivityResult(requestCode, resultCode, data);
            InAppBillingImplementation.HandleActivityResult(requestCode, resultCode, data);
            
            if (resultCode == Result.Ok)
                StartActivity(typeof(MainView));
        }
  • Notice how I call StartActivity. This will navigate to the next view on successful purchase. This is important as our MvxNavgiationService will not work properly at this point.

And you’re done! I hope this article helps you implement in-app billing in your apps in no time at all.

You may also like...

Leave a Reply