In many applications that we build we want to offer some way to monetize the product. Be it through subscriptions, one-off purchases or upgrades – these all provide a way to offer more value to our users. We’ve long done this through our Android (via Google Play) and iOS (via iTunes) applications, and now that we’re building Flutter apps we want to be able to do the same here too. In this article we’re going to explore the in-app purchase capabilities when it comes to Flutter and learn how to implement these within our own apps.
Note: before we get started it’s important to be aware that this package is in beta. Whilst that’s the case, it is the official package for in-app purchases so is worth migrating when the time comes.
We’re going to begin by adding the in-app purchase package to our list of dependencies in our pub.yaml file:
in_app_purchase: ^0.1.0+4
When making use of this package there is a core class that we’re going to be interacting with – this is the InAppPurchaseConnection. This is an abstract class which will be used and take the form of either a wrapper for the Google Play Store or the iOS App Store.
The abstract class, InAppPurchaseConnection, provides a collection of abstract methods which each of the inheriting classes will provide platform specific implementations for.
The great thing is that your application will have no idea what one is being used at this point, when you want to access the store connection you retrieve an instance of the class by using InAppPurchaseConnection.instance – at this point in the package, the following code is used:
The great thing is that your application will have no idea what one is being used at this point, when you want to access the store connection you retrieve an instance of the class by using InAppPurchaseConnection.instance – at this point in the package, the following code is used:
static InAppPurchaseConnection get instance => _getOrCreateInstance(); static InAppPurchaseConnection _instance; static InAppPurchaseConnection _getOrCreateInstance() { if (Platform.isAndroid) { _instance = GooglePlayConnection.instance; } else if (Platform.isIOS) { _instance = AppStoreConnection.instance; } return _instance; }
Some code has been omitted for simplicity, but you can see from this that under the hood we receive a store connection that matches that platform we are working with.
Within each of the connection classes there is another implementation which is used for communicating with the store of each framework.
The BillingClient is a dart class that acts as a wrapper around the Android platform BillingClient. In Flutter apps the BillingClient can be accessed and used directly, but the InAppPurchaseConnection class provides the required logic for dealing with both platforms. This BillingClient is mostly the same as the Android library version, however, this wrapper provides results in the form of Futures rather than callbacks – making it more friendly for working with on Flutter.
On the other hand, we have the SKPaymentQueueWrapper. As the Android implementation, this is also a wrapper but this time around the iOS framework class, SKPaymentQueue. Other than being able to interact with iOS store logic from an abstract point of view, this wrapper also allows us to work with Futures when making store requests.
Now that we have our store connection, we’re ready to start implementing our purchase flow within our Flutter application. There are several steps to prepare us from initialising the client to displaying the available items for purchase.
To kick this off, we’re going to want to begin by checking that a store is available for us to purchase items and/or subscriptions from. Maybe the device doesn’t have play services enabled, or there is some other issue that can prevent store services being available – for whatever reason, we’re going to want to make this check before attempting to communicate with any store services. We can do this by making use of the following function from our connection instance:
Future<bool> isAvailable();
Each of the corresponding implementations will make calls on their framework to check whether or not billing services are available. If store services are not available then this means that the store cannot currently be reached, or maybe just not accessed in the current session. Either way, we’re going to want to have this reflected within our UI.
On the other hand, if the store is available then we’re going to want to move forward and retrieve a list of available items from the store. The InAppPurchaseConnection instance provides a method called queryProductDetails(). This method returns us a Future of the type ProductDetailsResponse – this returns us a Future because there is an asynchronous call being made to fetch the required data.
Future<ProductDetailsResponse> queryProductDetails( Set<String> identifiers);
In order to make this call, we need to provide a set of product IDs that we want to retrieve. It’s important to note that these IDs will need to be the same in each of your stores. However, if this is not the case then you could use platform checks to build the product ID list based on the platform that the app is being run on. When it comes to the IDs, you could either have this stored remotely and provided to your app (this would allow you to dynamically update products without needing to update the app), or hard code them within your application (this will probably suit most cases). For this example we’re going to pass in some hardcoded product IDs to retrieve from the stores. We’ll start this flow by defining our set of product IDs:
Set<String> _productIds = <String>['first_id', "second_id"].toSet();
Now we have these defined, we’re going to want to make a call to the queryProductDetails() method. When we call this we’re going to pass in our product IDs, and after the asynchronous call ahs completed we will be returned an instance of a ProductDetailsResponse.
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
This ProductDetailsResponse contains two lists of data:
- List<ProductDetails> productDetails – this is a collection of ProductDetails for the items that have been retrieved from the stores
- List<String> notFoundIDs – a collection of IDs that could not be found when attempting to retrieve the products from the stores.
For the productDetails, this is going to be the list of products that we’re going to want to display for purchase within our app. Each ProductDetails instance contains a collection of information that we can use to give information on the product for purchase:
- id – The id of the product, this will match the ID used to retrieve it
- title – The title of the product, as defined in the store
- description – The description of the product, as defined in the store
- price – The price of the product. It’s important to note that this contains the currency symbol
- skProduct – When on the iOS platform, this will represent the SKProductWrapper instance that was used to generate this ProductDetails. This contains more information about the defined product, you can read more about these platform specific details here
- skuDetail – When on the Android platform, this will represent the SkuDetailsWrapper instance that was used to generate this ProductDetails. This contains more information about the defined product, you can read more about these platform specific details here
Before we try to access any of the above details, we should first check whether or not there are any products that were not found, using the notFoundIDs found within our ProductDetailsResponse. It may depend on the use case as to whether this matters, as it’s likely you will display the items that were found (provided the list is not empty).
If we piece all of the above together, we’ll end up with something that looks like this:
Future<List<ProductDetails>> retrieveProducts() async { final bool available = await InAppPurchaseConnection.instance.isAvailable(); if (!available) { // Handle store not available return ...; } else { Set<String> _kIds = <String>['id_one', 'id_two'].toSet(); final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds); if (response.notFoundIDs.isNotEmpty) { // Handle the error if desired } return new Future(() => response.productDetails); } }
At this point we have the required logic to fetch our products, so we’re going to want to display them within our UI. For this, let’s piece together a quick function that will build a row widget for a given ProductDetails instance:
Widget buildProductRow(ProductDetails productDetail) { return Padding( padding: const EdgeInsets.all(16.0), child: Row( children: <Widget>[ new Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ new Text( productDetail.title, style: new TextStyle(color: Colors.black), ), new Text(productDetail.description, style: new TextStyle(color: Colors.black45)) ], ), ), RaisedButton( color: Colors.green, child: Text(productDetail.price, style: new TextStyle(color: Colors.white)), onPressed: () => { purchaseItem(productDetail) }, ) ], ), ); }
In the above code sample we’re building a simple row that shows the product title and description, followed by a button that displays the price of the item. Now, you may notice that within the onPressed attribute of the button we’re calling a function to purchase the item. Let’s take a quick look at what this triggers:
void purchaseItem(ProductDetails productDetails) { final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); if ((Platform.isIOS && productDetails.skProduct.subscriptionPeriod == null) || (Platform.isAndroid && productDetails.skuDetail.type == SkuType.subs)) { InAppPurchaseConnection.instance .buyConsumable(purchaseParam: purchaseParam); } else { InAppPurchaseConnection.instance .buyNonConsumable(purchaseParam: purchaseParam); } }
This is where the magic happens! Here we are going to use either the buyConsumable or buyNonConsumable function from our InAppPurchaseConnection instance to trigger a purchase for the desired item. Whilst each of our store implementations will handle those purchases for us, we do need to first check whether we are dealing with a consumable or not. This can be done by checking some properties on the subscription details.
If we break down the above checks we can notice some platform specific checks. Now, buyNonConsumable is the method we will call if we are dealing with a subscription or items that can only be purchased a single time within an app (for example, some form of upgrade that never expires).
On the other hand, consumable items will make use of the buyConsumable() method. To be able to know whether we should call this method for the selected product, we need to check the product details to know if it is consumable or not. Both platform specific product classes contain this detail that we require, so for iOS we check if there is a subscriptionPeriod that exists on the skProduct instance – if there isn’t oner, then this means we are not dealing with a subscription. Likewise on the Android platform, the skuDetail contains a type which we can use to check against the subscription SkuType value – if these do not match then we are also not dealing with a subscription here.
if ((Platform.isIOS && productDetails.skProduct.subscriptionPeriod == null) || (Platform.isAndroid && productDetails.skuDetail.type == SkuType.subs)) { InAppPurchaseConnection.instance .buyConsumable(purchaseParam: purchaseParam); } else { InAppPurchaseConnection.instance .buyNonConsumable(purchaseParam: purchaseParam); }
In future it would be awesome if the in-app purchase package dealt with this for us whilst still offering the flexibility of being able to call each buy function independently if desired.
Now that we have the functionality available to retrieve our items, as well as construct a Row widget for each one to be shown in our UI, we’re going to want to load this into our screen so that they can be displayed to our users for selection.
return Scaffold( body: Row(children: [ Expanded( child: new FutureBuilder<List<ProductDetails>>( future: retrieveProducts(), initialData: List<ProductDetails>(), builder: (BuildContext context, AsyncSnapshot<List<ProductDetails>> products) { if (products.data != null) { return new SingleChildScrollView( padding: new EdgeInsets.all(8.0), child: new Column( children: products.data .map((item) => buildProductRow(item)) .toList())); } return Container(); }), ), ]), );
In the above code we make use of a FutureBuilder to handle our Future subscription along with displaying the content within the UI. Here we provide our retrieveProducts() method that we previously created – remember, this returns a Future instance of the type List<ProductDetails>. The result of this is then used by accessing the result data (products.data), performing a map on the result and then passing each ProductDetails instance to our buildProductRow() widget builder method that we previously put together. When we run this, we’ll see the products displayed on our screen.
It’s important to note that when the user performs a purchase on an item (using any of the previously defined buy method calls, the app does not receive a result from the call that has been made. However, the package provides us with a stream that can be observed to receive events that occur on any purchases taking place. We can fetch this stream in the form of a purchaseUpdatedStream from our InAppPurchaseConnection instance – this will emit PurchaseDetails instances that are currently involved in purchases.
When we receive these instances we’ll also get a corresponding PurchaseStatus value – this will be in the form of either pending, success or error. We can then use this value to let the user know that the purchase is currently taking place, followed by updating the UI once the purchase has succeeded or handling the error state if things don’t go quite as planned.
Note: When on the iOS platform you must call completePurchase() in order to finalise the in-app purchase flow.
StreamSubscription<List<PurchaseDetails>> _subscription; @override void initState() { final Stream purchaseUpdates = InAppPurchaseConnection.instance.purchaseUpdatedStream; _subscription = purchaseUpdates.listen((List<PurchaseDetails> purchases) { // handle purchase details }); super.initState(); } @override void dispose() { _subscription.cancel(); super.dispose(); }
In this post we’ve learnt about how we add in-app purchases to our Flutter applications. Not only have we looked at the implementation process, but we’ve also dived into the package to see how things work under the hood, along with how we can handle the different errors and situations that may occur when using the in-app purchase package.
[twitter-follow screen_name=’hitherejoe’]Are you already using this package, or something similar? Or maybe you have some questions or thoughts on this? Please feel free to reach out if so!