• Docs
  • Showcase
  • Community
  • DMCA
Sunday, February 28, 2021
Flutter Website
No Result
View All Result
  • Login
  • Register
Flutter Website
  • Home
  • Categories
    • Flutter App
    • Flutter Examples
    • Flutter Github
    • Flutter News
    • Flutter PDF
    • Flutter Tips & Trick
    • Flutter Tutorial
    • Flutter UI
    • Flutter Video
    • Flutter VS
    • Flutter Wiki
    • Flutter With
  • Flutter
    • Flutter for Mobile
    • Flutter for Web
      • Widget Sample
    • Flutter for Desktop
    • Tools
      • Codemagic
      • Flutter Studio
      • Supernova
  • Showcase
  • Community
  • Advertisement
  • Hire Us
No Result
View All Result
Flutter Website
Home Flutter Tutorial

In-App Purchases with Flutter: A Comprehensive Step-by-Step tutorial

By Mat

flutter by flutter
Reading Time: 68min read
520
In-app purchases with flutter: a comprehensive step-by-step tutorial
459
SHARES
656
VIEWS
Share on FacebookShare on TwitterShare on LinkedinShare to Whatsapp

Table of Contents

  • Part 1: What Are In-App Purchases?
  • In-App Purchases vs. Payment Gateways
  • When to Use a Payment Gateway
  • Part 2: Set Up In-App Purchases on iOS & Android
  • Part 3: How to Add In-App Purchases to Your Flutter App
  • Step 1: Create a service credential for Android
  • Step 2: Create in-app products in RevenueCat
  • Step 3: Add code to your Flutter app
  • components.dart
  • main.dart
  • parental_gate.dart
  • upgrade.dart
  • Troubleshooting
  • Conclusion

After my previous article on how to add payment gateway capabilities to your next Flutter app, I’m back today to show you how to add in-app purchases (IAP) to your iOS or Android application. In this tutorial, you will learn:

  • The difference between payment gateways and in-app purchases
  • The types of in-app purchases available to you
  • How to make a simple Flutter app with a recurring monthly subscription payment

There is so much you can do with IAP. In this article, I will show you how to use a few basic tools to quickly set up a demo app with IAP capabilities. This tutorial is not meant to explain everything about IAPs. Instead, think of it as a quick-start guide you can use to start experimenting with in-app purchases.

In this tutorial, you’ll create an Android and iOS app that allows users to purchase a monthly subscription to your app. We’ll set up a parental gate before the in-app payments screen and a few alert dialogs that require the user to confirm the purchase before finalizing payment.

In this tutorial, you’ll create an Android and iOS app that allows users to purchase a monthly subscription to your app. We’ll set up a parental gate before the in-app payments screen and a few alert dialogs that require the user to confirm the purchase before finalizing payment.

In-app purchases with flutter: a comprehensive step-by-step tutorial

This article is organized into 3 sections:

  • Part 1: What Are In-App Purchases?
  • Part 2: How to Set Up In-App Purchases with Apple & Google
  • Part 3: How to Add In-App Purchases to Your Flutter App

Why is it important to understand in-app purchases? The bottom line is that you and/or your sponsor probably want to make money with your app.

Did you know that revenue from mobile stores reached a whopping $83.5b in 2019?

$54.2b (65%) of the total comes from the App Store and $29.3b (35%) comes from the Play Store. Although the share between in-app purchases and paid apps is not publicly available, it is estimated that in-app purchases account for the majority of app revenues. Moreover, 75% of the total revenues are linked to mobile games.

So here’s my suggestion for your next project: a mobile game with in-app purchases in Flutter! Although it would be awesome to create the entire game in this tutorial, to keep the article short and focused, I will instead create a single-screen app which you can use as a framework to implement IAP capabilities in your app.

Part 1: What Are In-App Purchases?

In-app purchases can be used to sell a variety of content through your app, including subscriptions, new features, and services. Users can make in-app purchases on all sorts of devices and operating systems — not just their mobile phones. For example, Apple users can make purchases on iOS, iPadOS, macOS, watchOS, and tvOS.

There are 4 types of in-app purchases in the Apple world:

  • Consumables: Users can purchase different types of consumables, such as extra lives or gems in a game, to further their progress through an app. Consumable in-app purchases are used once, are depleted, and can be purchased again.
  • Non-Consumables: Users can purchase non-consumable, premium features within an app, such as additional filters in a photo app. Non-consumables are purchased once and do not expire.
  • Auto-Renewable Subscriptions: Users can purchase access to services or periodically updated content, such as monthly access to cloud storage or a weekly subscription to a magazine. Users are charged on a recurring basis until they decide to cancel.
  • Non-Renewing Subscriptions: Users can purchase access to services or content for a limited time, such as a season pass to streaming content. This type of subscription does not renew automatically, so users need to renew at the end of each subscription period.

Google offers the following types of in-app purchases:

  • One-time Products: These are in-app products requiring a single, non-recurring charge to the user’s form of payment. Additional game levels, premium loot boxes, and media files are examples of one-time products. The Google Play Console refers to one-time products as managed products, and the Google Play Billing library calls them “INAPP”.
  • Subscriptions: Subscriptions are in-app products that require a recurring charge to the user’s form of payment. Online magazines and music streaming services are examples of subscriptions. The Google Play Billing Library calls these “SUBS”.

As you can see, in-app purchases in Apple and Google are basically the same. Apple offers a few more choices, but both work the same way: you either charge a one-time fee or a fee on a recurring basis.

In-App Purchases vs. Payment Gateways

Generally speaking, there are two ways to pay for things through Apple and Google: payment gateways and in-app purchases. A payment gateway is similar to a credit card transaction; it takes a small percentage of the transaction, plus a flat fee. Stripe is a great option for this — they collect a fee of 1.4% plus £0.20p per transaction in the UK and 2.9% plus $0.30 in the US.) IAPs use Apple’s or Google’s payment infrastructure and take a 30% cut on all purchases, whether they’re one-time payments or recurring subscription fees. After a year of service, your revenue share increases to 85% of the subscription price, minus applicable taxes — so Apple and Google take 15% of your revenue.

Payment gateway fees: 1–3%

In-pp purchase fees: 30%-15%

Based on the fee structure alone, it sounds like you’d be a fool not to go with a payment gateway. Unfortunately, you can’t use a payment gateway everywhere. Apart from a few specific use cases, you generally can’t use an outside payment processor.

So when should you use which?

When to Use a Payment Gateway

You must use a payment gateway if you are selling physical goods and services that are consumable outside of the app.

Per the App Store Review Guidelines:

3.1.5 (a) Physical Goods and Services Outside of the App: If your app enables people to purchase goods or services that will be consumed outside of the app, you must use purchase methods other than in-app purchase to collect those payments, such as Apple Pay or traditional credit card entry.

Note that you can sell services through a payment gateway if those services are provided outside of the app. This opens up few options, since you could technically sell your services on your website and make the app available to your website subscribers. The limitation is that you cannot advertise anywhere in an app that you are selling something outside of the OS. This is a pretty tough decision to make, as it has implications for not only product development but also user acquisition, engagement, and retention.

When to Use In-App Purchases

Originally, Netflix did not allow you to sign up for their $10/month plan within their app. Instead, they forced every user to activate their account online on the Netflix website. They did that for a long time, opting to avoid the IAP fees with clunkier user experience. That is, until they determined that the number of signups they received by allowing users to activate their subscription in the app outweighed the 30% cut of the revenue. Now Netflix allows users to sign up in the app, and they pay the IAP fees.

You can think of the IAP fees as the cost of marketing your app to the massive audiences that Apple and Google attract. This is a good strategy if you plan to have a large number of paying customers. However, if your revenue comes from a small number of paying customers (for example, if your app is a B2B2C platform) and you want to save money, you can use your website.

I cannot stress enough that your app will be rejected if you link to a website that displays a payment form. Even if you link to your homepage, and it links to a payment page, you’ll be rejected.

Long story short, you will quickly realize that in-app purchases are the best way to monetize your app.

You must use IAP if you are selling services within your app.

According to Apple’s official guidelines:

If you want to enable users to unlock features or functionality within your app (subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version of the app), you must use in-app purchases. Apps may use in-app purchase currencies to enable customers to “tip” digital content providers in the app. Apps and their metadata may not include buttons, external links, or other calls to action that direct customers to purchasing mechanisms other than in-app purchases.

Here is how Apple and Google sell this service to you as a developer:

In-app purchases with flutter: a comprehensive step-by-step tutorial

Apple marketing of In-App Purchase

In-app purchases with flutter: a comprehensive step-by-step tutorial

In-app purchases with flutter: a comprehensive step-by-step tutorial

Android marketing of Google Play services

Now let’s get started setting up in-app purchases!

Part 2: Set Up In-App Purchases on iOS & Android

Regardless of what OS you plan to use, you must set up your in-app purchases through the app stores. This is a lengthy process, and it requires different steps on iOS and Android. To keep this article concise, I’ve created a stand-alone article (How to set up In App Purchases in Apple Connect and Google Play Console) on how to set up in-app purchases on iOS and Android. Before moving on to Part 3, head over to that post and follow all the steps.

Part 3: How to Add In-App Purchases to Your Flutter App

My preferred method of adding IAP functionality to an app is to use RevenueCat. I’ve used it several times before, and it works like a charm. They simplify the process of adding IAP capabilities to apps built in Flutter (or any other platform, but who cares about Swift and Java now that we have Flutter?!).

In-app purchases with flutter: a comprehensive step-by-step tutorial

Here are a few reasons to use their platform:

  • Free to use. RevenueCat is completely free until you have over $10,000 USD in monthly revenue — which means it’s free for most full-stack developers. If you are lucky enough to have over $10k in monthly revenue, it shouldn’t be a problem to pay approximately 1% to them. Check out their pricing below.

In-app purchases with flutter: a comprehensive step-by-step tutorial

  • Documentation. For me, this is the main reason to use RevenueCat. They have impressive documentation that guides you through every step of the process in detail. Kudos to them for making it available and for including Flutter!

In-app purchases with flutter: a comprehensive step-by-step tutorial

  • Serverless. With RevenueCat, you don’t need to set up a server. Their subscription infrastructure is scalable and can be established in minutes.
  • Dashboard. You get a page with helpful insights on your customer metrics (monthly revenue, active installs, trials, etc.).

In-app purchases with flutter: a comprehensive step-by-step tutorial

  • Charts. On paid plans, you get access to more detailed metrics, like daily revenue, conversion rate, churn rate, etc. This is cool, but you have to upgrade to a paid plan to get it. Go for it if you think it’s worth it!

In-app purchases with flutter: a comprehensive step-by-step tutorial

There is also technical support and an internal discussion board where you can post questions. However, I have not needed to use either one, since everything was so straightforward!

RevenueCat has created a Flutter plugin (purchases_flutter) that we will use in our project.

Here’s what we are going to do:

  • Step 1: Create a service credential for Android
  • Step 2: Create in-app products in RevenueCat
  • Step 3: Add the code to our Flutter app

Step 1: Create a service credential for Android

This is a tricky step, so make sure you follow the instructions carefully. You need to allow RevenueCat to see the in-app data of your Android app. (On iOS, you simply need to create a secret code, so this step isn’t necessary.)

First, navigate to the Play Console and click Settings > Developer account > API access.

In-app purchases with flutter: a comprehensive step-by-step tutorial

I already have a service credential, but you’ll most likely see an empty page. If you have never used API access before, click Create New Project.

In-app purchases with flutter: a comprehensive step-by-step tutorial

If you already have a project, you can link your account to your existing project by pressing the Link button.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, click Create Service Account to create a new service account associated with your project.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Follow the link to the Google API Console.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Click on the Google API Console link in the first bullet point. You will be directed to the Google Cloud Platform. Then click Create Service Account.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Add a name and a description and click Create.

In-app purchases with flutter: a comprehensive step-by-step tutorial

For the role, select “Owner” and click Continue.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Click on Create Key.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Select JSON and click Create.

In-app purchases with flutter: a comprehensive step-by-step tutorial

A file will be downloaded to your computer.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Close the message and click Done.

In-app purchases with flutter: a comprehensive step-by-step tutorial

You will be redirected to the Service Account page and your new service account will show up in the list.

In-app purchases with flutter: a comprehensive step-by-step tutorial

You can now close the Google Cloud Platform page and go back to the Google Play Console. Click Done to dismiss the dialog message.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Click Grant access for the service account you just created.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Choose “Finance” in the Role dropdown and make sure the “View app information” and “View financial data” options are selected. Click Add User.

In-app purchases with flutter: a comprehensive step-by-step tutorial

You will be directed to the Users & Permissions screen, where you can see a list of all the users you have created (including the one we just created).

In-app purchases with flutter: a comprehensive step-by-step tutorial

You’re done with Step 1! Remember where you saved your JSON file — you’ll need it in the next step.

Step 2: Create in-app products in RevenueCat

Go to the RevenueCat website and sign up for a free account.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Enter your name, email, and password, then click Sign Up. Don’t forget to verify your account by opening the email you receive from RevenueCat..

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, add the details of your app. If you have followed all the instructions in this guide, all of this information should be easy to find. You need to add the bundle ID of your app, along with the app-specific shared secret (for Apple) and the Google service account. For the JSON file, open the file with a text editor like Atom and copy and paste all the text. Next click Add.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Your app will be created in the RevenueCat console.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, you will need to add a few attributes specific to the RevenueCat platform. First, let me explain a little bit about these attributes:

  • Entitlements: An entitlement is something a user is entitled to in your app — for example, VIP access (access to all features of the app) or Pro access (access to only some features of the app). Entitlements include products.
  • Products: This refers to the app subscription itself. You need to have a product for each platform (one for iOS and one for Android).
  • Offerings: This is a selection of products that are offered to a user. Offerings allow you to choose which combination of products are shown to a user on your paywall screen.

Let’s create our first entitlement by selecting Entitlements and clicking New.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, add an identifier and a description.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Now let’s create an offering. Select Offerings and click New.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, add an identifier and a description.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Click on 0 packages.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Create a new monthly package and click Add.

In-app purchases with flutter: a comprehensive step-by-step tutorial

You should now see the package added to your offering.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Finally, let’s create a product. Select Products and click New.

In-app purchases with flutter: a comprehensive step-by-step tutorial

We need to create as many products as our subscription, times 2 (one product per platform). So we will create one product for iOS and one for Android. The name of each product must match the name of the subscriptions you created in Apple Connect and the Play Store. In my case, it’s “TestInApp” for iOS and “test_in_app” for Android.

In-app purchases with flutter: a comprehensive step-by-step tutorial

In-app purchases with flutter: a comprehensive step-by-step tutorial

Create a product for each subscription.

In-app purchases with flutter: a comprehensive step-by-step tutorial

In-app purchases with flutter: a comprehensive step-by-step tutorial

By the end of this step, you should have 2 products in your console.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Now we need to link the products to the entitlements and offerings. Select Entitlements and click on 0 products.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Click Attach.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Attach both products you just created.

In-app purchases with flutter: a comprehensive step-by-step tutorial

In-app purchases with flutter: a comprehensive step-by-step tutorial

Your products should now be listed under your associated products.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, go to the Offerings section and click on 0 packages.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, click Attach.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Finally, select the product in the dropdown and click Attach.

In-app purchases with flutter: a comprehensive step-by-step tutorial

You have now listed your products in your offering.

In-app purchases with flutter: a comprehensive step-by-step tutorial

This completes your RevenueCat setup! Well done!

Step 3: Add code to your Flutter app

This is the moment you’ve been waiting for: the very last step! At this point, it’s a simple process to add in-app purchases to your Flutter app.

In-app purchases with flutter: a comprehensive step-by-step tutorial

First things first, import the following plugins to your pubspec.yaml file:

url_launcher: ^5.4.10
rflutter_alert: ^1.0.3

Open the iOS simulator and run flutter pub get. This does a few things for us, including creating a Pod file.

Next, launch Xcode. Open your project, then open the Pod file and uncomment the second line. This task defines a global platform for your project.

In-app purchases with flutter: a comprehensive step-by-step tutorial

While you are in Xcode, make sure to add “In-App Purchase” to the project capabilities section: project target -> Capabilities -> In-App Purchase

In-app purchases with flutter: a comprehensive step-by-step tutorial

Next, go back to the pubspec.yaml file and add the RevenueCat plugin to it:

purchases_flutter: ^1.1.1

Now let’s make sure everything works in iOS and Android before we start coding our app.

  • iOS. Run flutter pub get and flutter run to compile your project in the iOS simulator. If you have followed the steps, everything should work and your app should be up and running!

In-app purchases with flutter: a comprehensive step-by-step tutorial

  • Android. Open Android Studio and run the project. You’ll probably see some red notes in the console about url_launcher using a deprecated API. Don’t worry about that. The app should properly launch in your Android simulator.

In-app purchases with flutter: a comprehensive step-by-step tutorial

In this tutorial, we won’t worry too much about the design because we are focused on building the IAP functionality. However, we will add a few components to make our main code simple to read.

Create the following 3 empty dart files in the /lib folder:

  • components.dart: Here we’ll add some code to call from other files and make our main code look simpler.
  • parental_gate.dart: This will be our second screen, where we will block access to certain users. This is a requirement on iOS if you develop apps for kids.
  • upgrade.dart: This is the final screen, where we will handle in-app purchase transactions.

components.dart

We are going to add the following classes (in your real app, you may have these in different files):

  • Singleton
  • Color
  • Style
  • Widget

First, let’s create a singleton class to keep track of variables. This isn’t strictly necessary, but you may want to reuse your code for other apps, so let’s do it since it’s super easy. We will create a variable called “isPro”, where we will keep track of whether or not the user has already made an in-app purchase.

class AppData {
  static final AppData _appData = new AppData._internal();

  bool isPro;

  factory AppData() {
    return _appData;
  }
  AppData._internal();
}

final appData = AppData();

Then add the following colors (you can change these if you want):

const kColorPrimary = Color(0xff283149);
const kColorPrimaryLight = Color(0xff424B67);
const kColorPrimaryDark = Color(0xff21293E);
const kColorAccent = Colors.blue;
const kColorText = Color(0xffDBEDF3);

Next, add the following styles:

var kWelcomeAlertStyle = AlertStyle(
  animationType: AnimationType.grow,
  isCloseButton: false,
  isOverlayTapDismiss: false,
  animationDuration: Duration(milliseconds: 450),
  backgroundColor: kColorPrimaryLight,
  alertBorder: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(10.0),
  ),
  titleStyle: TextStyle(
    color: kColorText,
    fontWeight: FontWeight.bold,
    fontSize: 30.0,
    letterSpacing: 1.5,
  ),
);

TextStyle kSendButtonTextStyle = TextStyle(
  color: kColorText,
  fontWeight: FontWeight.bold,
  fontSize: 20,
);

And finally, create a widget for the top navigation bar (I have added different transition effects depending on the operating system):

class TopBarAgnosticNoIcon extends StatelessWidget {
  final String text;

  final TextStyle style;
  final String uniqueHeroTag;
  final Widget child;

  TopBarAgnosticNoIcon({
    this.text,
    this.style,
    this.uniqueHeroTag,
    this.child,
  });

  @override
  Widget build(BuildContext context) {
    if (!Platform.isIOS) {
      return Scaffold(
        backgroundColor: kColorPrimary,
        appBar: AppBar(
          iconTheme: IconThemeData(
            color: kColorText, //change your color here
          ),
          backgroundColor: kColorPrimaryLight,
          title: Text(
            text,
            style: style,
          ),
        ),
        body: child,
      );
    } else {
      return CupertinoPageScaffold(
        backgroundColor: kColorPrimary,
        navigationBar: CupertinoNavigationBar(
          backgroundColor: kColorPrimaryLight,
          heroTag: uniqueHeroTag,
          transitionBetweenRoutes: false,
          middle: Text(
            text,
            style: style,
          ),
        ),
        child: child,
      );
    }
  }
}

When you’re finished, your component dart file should look like this. Don’t forget to import the relevant packages!

In-app purchases with flutter: a comprehensive step-by-step tutorial

main.dart

Let’s modify our main file so that it includes a text widget and a button.

@override
Widget build(BuildContext context) {
  return TopBarAgnosticNoIcon(
    text: widget.title,
    style: kSendButtonTextStyle,
    uniqueHeroTag: 'main',
    child: Scaffold(
      backgroundColor: kColorPrimary,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(bottom: 18.0),
              child: Text(
                'Welcome',
                style: kSendButtonTextStyle.copyWith(fontSize: 40),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: RaisedButton(
                  color: kColorAccent,
                  textColor: kColorText,
                  child: Padding(
                    padding: const EdgeInsets.all(12.0),
                    child: Text(
                      'Purchase a subscription',
                      style: kSendButtonTextStyle,
                    ),
                  ),
                  onPressed: () {
                    if (appData.isPro) {
                      Navigator.push(context, MaterialPageRoute(builder: (context) => UpgradeScreen(), settings: RouteSettings(name: 'Upgrade screen')));
                    } else {
                      Navigator.push(context, MaterialPageRoute(builder: (context) => ParentalGate(), settings: RouteSettings(name: 'Parental Gate')));
                    }
                  }),
            ),
          ],
        ),
      ),
    ),
  );
}

When you run your app, this is what you should see:

In-app purchases with flutter: a comprehensive step-by-step tutorial

The first thing we need to do when our app loads is to set up in-app purchases and fetch in-app purchase information about the device. We only need to set up in-app purchases once, but you will fetch device information multiple times during the lifecycle of your app.

You can use the function Purchases.setup(“your_public_revenuecat_API_key”) to set up in-app purchases. Before you run it, you’ll need to add your RevenueCat API key, which you can find in the RevenueCat dashboard. (Please note this key is specific to your app.)

In-app purchases with flutter: a comprehensive step-by-step tutorial

The second function we’ll use is Purchases.getPurchaserInfo(). This function fetches in-app purchase information about the device/user that is stored on Apple or Google servers.

These two functions need to be run asynchronously, so let’s create a future state and handle the output accordingly. Let’s also enable debugging with Purchases.setDebugLogsEnabled(true).

import 'package:flutter/material.dart';
import 'package:test_in_app/parental_gate.dart';
import 'package:test_in_app/upgrade.dart';
import 'components.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:flutter/services.dart';@override
void initState() {
  super.initState();
  initPlatformState();
}

Future<void> initPlatformState() async {
  appData.isPro = false;
  
  await Purchases.setDebugLogsEnabled(true):
  await Purchases.setup("your_api_key");

  PurchaserInfo purchaserInfo;
  try {
    purchaserInfo = await Purchases.getPurchaserInfo();
    print(purchaserInfo.toString());
    if (purchaserInfo.entitlements.all['all_features'] != null) {
      appData.isPro = purchaserInfo.entitlements.all['all_features'].isActive;
    } else {
      appData.isPro = false;
    }
  } on PlatformException catch (e) {
    print(e);
  }

  print('#### is user pro? ${appData.isPro}');
}

Let’s run this and see what happens. You can see all the relevant entitlements and product information highlighted with the red/yellow arrows.

In-app purchases with flutter: a comprehensive step-by-step tutorial

We’re done with the main file! Now let’s move on to the parental gate.

parental_gate.dart

This is optional but recommended since Apple requires parental gates in apps designed for kids.

A parental gate presents an adult-level challenge that must be completed in order to continue. The App Store Review Guidelines require all apps in the Kids category to use parental gates — this is to prevent kids from engaging in commerce or following links to websites, social networks, or other apps without the knowledge of their parent or guardian.

It’s a good idea to randomize the combination of questions and answers each time the gate is presented to prevent kids from memorizing the correct responses. For more details on Apple’s requirements, check out the relevant sections of the review guidelines:

• App Store Review Guidelines Section 1.3

• App Store Review Guidelines Section 5.1.4

You can make the parental gate as complex as you want. In this example, we are simply going to present a random math question and allow the user to move on to the next screen if the answer is correct. Let’s first create the relevant variables and methods to generate random numbers:

String answer;
  int firstNumber = 0;
  int secondNumber = 0;
  String solution;
  final myController = TextEditingController();

  void solvePuzzle() {
    firstNumber = generateRandomNumbers();
    secondNumber = generateRandomNumbers();
    solution = (firstNumber + secondNumber).toString();
    setState(() {});
  }

  generateRandomNumbers() {
    int min = 11;
    int max = 95;
    print('max is ' + max.toString());
    int randomNumber = min + (Random().nextInt(max - min));
    return randomNumber;
  }

  @override
  void initState() {
    firstNumber = generateRandomNumbers();
    secondNumber = generateRandomNumbers();
    solution = (firstNumber + secondNumber).toString();
    super.initState();
  }

Then let’s create a few text widgets with the instructions, as well as an alert that will display if the answer is not correct. I also added an image as a bonus.

@override
  Widget build(BuildContext context) {
    return TopBarAgnosticNoIcon(
      text: 'Parental Gate',
      style: kSendButtonTextStyle,
      uniqueHeroTag: 'parental_gate',
      child: Scaffold(
        backgroundColor: kColorPrimary,
        body: Center(
          child: SingleChildScrollView(
            child: Padding(
              padding: EdgeInsets.symmetric(horizontal: 24.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.only(top: 20.0, bottom: 20.0),
                    child: Hero(
                      tag: 'logo',
                      child: CircleAvatar(
                        backgroundColor: kColorPrimary,
                        radius: 50.0,
                        backgroundImage: AssetImage("assets/images/avatar_demo.png"),
                      ),
                    ),
                  ),
                  Text(
                    'Ask parent/guarding for help',
                    textAlign: TextAlign.center,
                    style: kSendButtonTextStyle,
                  ),
                  SizedBox(
                    height: 40.0,
                  ),
                  Text(
                    'Please hand over the device to a parent or a guardian to continue.',
                    style: kSendButtonTextStyle,
                    textAlign: TextAlign.center,
                  ),
                  SizedBox(
                    height: 20.0,
                  ),
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Container(
                      child: Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Text(
                          'How much is ${firstNumber.toString()} plus ${secondNumber.toString()}?',
                          textAlign: TextAlign.center,
                          style: kSendButtonTextStyle,
                        ),
                      ),
                    ),
                  ),
                  TextField(
                    textAlign: TextAlign.center,
                    keyboardType: TextInputType.number,
                    style: kSendButtonTextStyle,
                    controller: myController,
                    autofocus: true,
                    onChanged: (value) {
                      answer = value;
                    },
                  ),
                  SizedBox(
                    height: 30.0,
                  ),
                  Padding(
                    padding: const EdgeInsets.all(12.0),
                    child: RaisedButton(
                        color: kColorAccent,
                        textColor: kColorText,
                        child: Padding(
                          padding: const EdgeInsets.all(12.0),
                          child: Text(
                            'Confirm',
                            style: kSendButtonTextStyle,
                          ),
                        ),
                        onPressed: () {
                          setState(() {
                            myController.text = '';
                          });
                          if (answer == solution) {
                            Navigator.push(
                                context,
                                MaterialPageRoute(
                                  builder: (context) => UpgradeScreen(),
                                  settings: RouteSettings(name: 'Upgrade screen'),
                                ));
                          } else {
                            //try again

                            Alert(
                              context: context,
                              style: kWelcomeAlertStyle,
                              image: Image.asset(
                                "assets/images/avatar_demo.png",
                                height: 150,
                              ),
                              title: "Error",
                              content: Column(
                                children: <Widget>[
                                  Padding(
                                    padding: const EdgeInsets.only(top: 20.0, right: 8.0, left: 8.0, bottom: 20.0),
                                    child: Text(
                                      'This is not correct. Please try again.',
                                      textAlign: TextAlign.center,
                                      style: kSendButtonTextStyle.copyWith(fontSize: 19, color: kColorText),
                                    ),
                                  )
                                ],
                              ),
                              buttons: [
                                DialogButton(
                                  radius: BorderRadius.circular(10),
                                  child: Text(
                                    "COOL",
                                    style: kSendButtonTextStyle,
                                  ),
                                  onPressed: () {
                                    Navigator.of(context, rootNavigator: true).pop();
                                    solvePuzzle();
                                  },
                                  width: 127,
                                  color: kColorAccent,
                                  height: 52,
                                ),
                              ],
                            ).show();
                          }
                        }),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
}

Here is the result:

In-app purchases with flutter: a comprehensive step-by-step tutorial

upgrade.dart

This last one is where all the IAP action will happen. We want the screen to look like this:

In-app purchases with flutter: a comprehensive step-by-step tutorial

You can see we have a button for purchasing a subscription, a button for restoring purchases, and some links to the privacy policy and terms of use. Here’s how we are going to do it:

Step 1: Create an UpgradeScreen class

We’ll use this class to fetch IAP data and send the user to the upsell screen or a pro screen.

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'components.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:rflutter_alert/rflutter_alert.dart';

PurchaserInfo _purchaserInfo;

class UpgradeScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _UpgradeScreenState();
}

class _UpgradeScreenState extends State<UpgradeScreen> {
  Offerings _offerings;

  @override
  void initState() {
    super.initState();
    fetchData();
  }

  Future<void> fetchData() async {
    PurchaserInfo purchaserInfo;
    try {
      purchaserInfo = await Purchases.getPurchaserInfo();
    } on PlatformException catch (e) {
      print(e);
    }

    Offerings offerings;
    try {
      offerings = await Purchases.getOfferings();
    } on PlatformException catch (e) {
      print(e);
    }
    if (!mounted) return;

    setState(() {
      _purchaserInfo = purchaserInfo;
      _offerings = offerings;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_purchaserInfo == null) {
      return TopBarAgnosticNoIcon(
        text: "Upgrade Screen",
        style: kSendButtonTextStyle,
        uniqueHeroTag: 'upgrade_screen',
        child: Scaffold(
            backgroundColor: kColorPrimary,
            body: Center(
                child: Text(
              "Loading...",
            ))),
      );
    } else {
      if (_purchaserInfo.entitlements.all.isNotEmpty && _purchaserInfo.entitlements.all['all_features'].isActive != null) {
        appData.isPro = _purchaserInfo.entitlements.all['all_features'].isActive;
      } else {
        appData.isPro = false;
      }
      if (appData.isPro) {
        return ProScreen();
      } else {
      return UpsellScreen(
        offerings: _offerings,
      );
      }
    }
  }
}

Step 2: Create the upsell screen

This screen will show the user some text and buttons. If the purchase is successful, it will display a confirmation message and send the user back to the home page. I kept the restore button in this class and refactored the purchase button into another class, which makes the code cleaner.

class UpsellScreen extends StatefulWidget {
  final Offerings offerings;

  UpsellScreen({Key key, @required this.offerings}) : super(key: key);

  @override
  _UpsellScreenState createState() => _UpsellScreenState();
}

class _UpsellScreenState extends State<UpsellScreen> {
  _launchURLWebsite(String zz) async {
    if (await canLaunch(zz)) {
      await launch(zz);
    } else {
      throw 'Could not launch $zz';
    }
  }

  @override
  Widget build(BuildContext context) {
    if (widget.offerings != null) {
      print('offeringS is not null');
      print(widget.offerings.current.toString());
      print('--');
      print(widget.offerings.toString());
      final offering = widget.offerings.current;
      if (offering != null) {
        final monthly = offering.monthly;
        if (monthly != null) {
          return TopBarAgnosticNoIcon(
            text: "Upgrade Screen",
            style: kSendButtonTextStyle,
            uniqueHeroTag: 'purchase_screen',
            child: Scaffold(
                backgroundColor: kColorPrimary,
                body: Center(
                  child: SingleChildScrollView(
                      child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Text(
                        'Thanks for your interest in our app!',
                        textAlign: TextAlign.center,
                        style: kSendButtonTextStyle,
                      ),
                      Padding(
                        padding: const EdgeInsets.all(18.0),
                        child: CircleAvatar(
                          backgroundColor: kColorPrimary,
                          radius: 45.0,
                          backgroundImage: AssetImage("assets/images/avatar_demo.png"),
                        ),
                      ),
                      Text(
                        'Choose one of the plan to continue to get access to all the app content.\n',
                        textAlign: TextAlign.center,
                        style: kSendButtonTextStyle,
                      ),
                      Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: PurchaseButton(package: monthly),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(18.0),
                        child: GestureDetector(
                          child: Container(
                            decoration: new BoxDecoration(
                              color: kColorPrimaryDark,
                              borderRadius: new BorderRadius.all(Radius.circular(10)),
                            ),
                            child: Padding(
                              padding: const EdgeInsets.all(18.0),
                              child: Text(
                                'Restore Purchase',
                                style: kSendButtonTextStyle.copyWith(
                                  fontSize: 16,
                                  fontWeight: FontWeight.normal,
                                ),
                              ),
                            ),
                          ),
                          onTap: () async {
                            try {
                              print('now trying to restore');
                              PurchaserInfo restoredInfo = await Purchases.restoreTransactions();
                              print('restore completed');
                              print(restoredInfo.toString());

                              appData.isPro = restoredInfo.entitlements.all["all_features"].isActive;

                              print('is user pro? ${appData.isPro}');

                              if (appData.isPro) {
                                Alert(
                                  context: context,
                                  style: kWelcomeAlertStyle,
                                  image: Image.asset(
                                    "assets/images/avatar_demo.png",
                                    height: 150,
                                  ),
                                  title: "Congratulation",
                                  content: Column(
                                    children: <Widget>[
                                      Padding(
                                        padding: const EdgeInsets.only(top: 20.0, right: 8.0, left: 8.0, bottom: 20.0),
                                        child: Text(
                                          'Your purchase has been restored!',
                                          textAlign: TextAlign.center,
                                          style: kSendButtonTextStyle,
                                        ),
                                      )
                                    ],
                                  ),
                                  buttons: [
                                    DialogButton(
                                      radius: BorderRadius.circular(10),
                                      child: Text(
                                        "COOL",
                                        style: kSendButtonTextStyle,
                                      ),
                                      onPressed: () {
                                        Navigator.of(context, rootNavigator: true).pop();
                                        Navigator.of(context, rootNavigator: true).pop();
                                        Navigator.of(context, rootNavigator: true).pop();
                                      },
                                      width: 127,
                                      color: kColorAccent,
                                      height: 52,
                                    ),
                                  ],
                                ).show();
                              } else {
                                Alert(
                                  context: context,
                                  style: kWelcomeAlertStyle,
                                  image: Image.asset(
                                    "assets/images/avatar_demo.png",
                                    height: 150,
                                  ),
                                  title: "Error",
                                  content: Column(
                                    children: <Widget>[
                                      Padding(
                                        padding: const EdgeInsets.only(top: 20.0, right: 8.0, left: 8.0, bottom: 20.0),
                                        child: Text(
                                          'There was an error. Please try again later',
                                          textAlign: TextAlign.center,
                                          style: kSendButtonTextStyle,
                                        ),
                                      )
                                    ],
                                  ),
                                  buttons: [
                                    DialogButton(
                                      radius: BorderRadius.circular(10),
                                      child: Text(
                                        "COOL",
                                        style: kSendButtonTextStyle,
                                      ),
                                      onPressed: () {
                                        Navigator.of(context, rootNavigator: true).pop();
                                      },
                                      width: 127,
                                      color: kColorAccent,
                                      height: 52,
                                    ),
                                  ],
                                ).show();
                              }
                            } on PlatformException catch (e) {
                              print('----xx-----');
                              var errorCode = PurchasesErrorHelper.getErrorCode(e);
                              if (errorCode == PurchasesErrorCode.purchaseCancelledError) {
                                print("User cancelled");
                              } else if (errorCode == PurchasesErrorCode.purchaseNotAllowedError) {
                                print("User not allowed to purchase");
                              }
                              Alert(
                                context: context,
                                style: kWelcomeAlertStyle,
                                image: Image.asset(
                                  "assets/images/avatar_demo.png",
                                  height: 150,
                                ),
                                title: "Error",
                                content: Column(
                                  children: <Widget>[
                                    Padding(
                                      padding: const EdgeInsets.only(top: 20.0, right: 8.0, left: 8.0, bottom: 20.0),
                                      child: Text(
                                        'There was an error. Please try again later',
                                        textAlign: TextAlign.center,
                                        style: kSendButtonTextStyle,
                                      ),
                                    )
                                  ],
                                ),
                                buttons: [
                                  DialogButton(
                                    radius: BorderRadius.circular(10),
                                    child: Text(
                                      "COOL",
                                      style: kSendButtonTextStyle,
                                    ),
                                    onPressed: () {
                                      Navigator.of(context, rootNavigator: true).pop();
                                    },
                                    width: 127,
                                    color: kColorAccent,
                                    height: 52,
                                  ),
                                ],
                              ).show();
                            }
                            return UpgradeScreen();
                          },
                        ),
                      ),

                      SizedBox(
                        height: 20.0,
                      ),
                      Padding(
                        padding: const EdgeInsets.all(18.0),
                        child: GestureDetector(
                          onTap: () {
                            _launchURLWebsite('https://google.com');
                          },
                          child: Text(
                            'Privacy Policy (click to read)',
                            style: kSendButtonTextStyle.copyWith(
                              fontSize: 16,
                              fontWeight: FontWeight.normal,
                            ),
                          ),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(18.0),
                        child: GestureDetector(
                          onTap: () {
                            _launchURLWebsite('https://yahoo.com');
                          },
                          child: Text(
                            'Term of Use (click to read)',
                            style: kSendButtonTextStyle.copyWith(
                              fontSize: 16,
                              fontWeight: FontWeight.normal,
                            ),
                          ),
                        ),
                      ),
                    ],
                  )),
                )),
          );
        }
      }
    }
    return TopBarAgnosticNoIcon(
      text: "Upgrade Screen",
      style: kSendButtonTextStyle,
      uniqueHeroTag: 'upgrade_screen1',
      child: Scaffold(
          backgroundColor: kColorPrimary,
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(18.0),
                  child: Icon(
                    Icons.error,
                    color: kColorText,
                    size: 44.0,
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(12.0),
                  child: Text(
                    "There was an error. Please check that your device is allowed to make purchases and try again. Please contact us at [email protected] if the problem persists.",
                    textAlign: TextAlign.center,
                    style: kSendButtonTextStyle,
                  ),
                ),
              ],
            ),
          )),
    );
  }
}

Step 3: Create a PurchaseButton class

This is where you will allow the user to purchase a subscription.

class PurchaseButton extends StatefulWidget {
  final Package package;

  PurchaseButton({Key key, @required this.package}) : super(key: key);

  @override
  _PurchaseButtonState createState() => _PurchaseButtonState();
}

class _PurchaseButtonState extends State<PurchaseButton> {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 20.0, right: 20.0),
      child: Container(
        color: kColorPrimaryLight,
        width: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 18.0),
              child: RaisedButton(
                onPressed: () async {
                  try {
                    print('now trying to purchase');
                    _purchaserInfo = await Purchases.purchasePackage(widget.package);
                    print('purchase completed');

                    appData.isPro = _purchaserInfo.entitlements.all["all_features"].isActive;

                    print('is user pro? ${appData.isPro}');

                    if (appData.isPro) {
                      Alert(
                        context: context,
                        style: kWelcomeAlertStyle,
                        image: Image.asset(
                          "assets/images/avatar_demo.png",
                          height: 150,
                        ),
                        title: "Congratulation",
                        content: Column(
                          children: <Widget>[
                            Padding(
                              padding: const EdgeInsets.only(top: 20.0, right: 8.0, left: 8.0, bottom: 20.0),
                              child: Text(
                                'Well done, you now have full access to the app',
                                textAlign: TextAlign.center,
                                style: kSendButtonTextStyle,
                              ),
                            )
                          ],
                        ),
                        buttons: [
                          DialogButton(
                            radius: BorderRadius.circular(10),
                            child: Text(
                              "COOL",
                              style: kSendButtonTextStyle,
                            ),
                            onPressed: () {
                              Navigator.of(context, rootNavigator: true).pop();
                              Navigator.of(context, rootNavigator: true).pop();
                              Navigator.of(context, rootNavigator: true).pop();
                            },
                            width: 127,
                            color: kColorAccent,
                            height: 52,
                          ),
                        ],
                      ).show();
                    } else {
                      Alert(
                        context: context,
                        style: kWelcomeAlertStyle,
                        image: Image.asset(
                          "assets/images/avatar_demo.png",
                          height: 150,
                        ),
                        title: "Error",
                        content: Column(
                          children: <Widget>[
                            Padding(
                              padding: const EdgeInsets.only(top: 20.0, right: 8.0, left: 8.0, bottom: 20.0),
                              child: Text(
                                'There was an error. Please try again later',
                                textAlign: TextAlign.center,
                                style: kSendButtonTextStyle,
                              ),
                            )
                          ],
                        ),
                        buttons: [
                          DialogButton(
                            radius: BorderRadius.circular(10),
                            child: Text(
                              "COOL",
                              style: kSendButtonTextStyle,
                            ),
                            onPressed: () {
                              Navigator.of(context, rootNavigator: true).pop();
                            },
                            width: 127,
                            color: kColorAccent,
                            height: 52,
                          ),
                        ],
                      ).show();
                    }
                  } on PlatformException catch (e) {
                    print('----xx-----');
                    var errorCode = PurchasesErrorHelper.getErrorCode(e);
                    if (errorCode == PurchasesErrorCode.purchaseCancelledError) {
                      print("User cancelled");
                    } else if (errorCode == PurchasesErrorCode.purchaseNotAllowedError) {
                      print("User not allowed to purchase");
                    }
                    Alert(
                      context: context,
                      style: kWelcomeAlertStyle,
                      image: Image.asset(
                        "assets/images/avatar_demo.png",
                        height: 150,
                      ),
                      title: "Error",
                      content: Column(
                        children: <Widget>[
                          Padding(
                            padding: const EdgeInsets.only(top: 20.0, right: 8.0, left: 8.0, bottom: 20.0),
                            child: Text(
                              'There was an error. Please try again later',
                              textAlign: TextAlign.center,
                              style: kSendButtonTextStyle,
                            ),
                          )
                        ],
                      ),
                      buttons: [
                        DialogButton(
                          radius: BorderRadius.circular(10),
                          child: Text(
                            "COOL",
                            style: kSendButtonTextStyle,
                          ),
                          onPressed: () {
                            Navigator.of(context, rootNavigator: true).pop();
                          },
                          width: 127,
                          color: kColorAccent,
                          height: 52,
                        ),
                      ],
                    ).show();
                  }
                  return UpgradeScreen();
                },
                textColor: kColorText,
                padding: const EdgeInsets.all(0.0),
                child: Container(
                  width: MediaQuery.of(context).size.width / 1.5,
                  decoration: const BoxDecoration(
                    gradient: LinearGradient(
                      colors: <Color>[
                        Color(0xFF0D47A1),
                        Color(0xFF1976D2),
                        Color(0xFF42A5F5),
                      ],
                    ),
                  ),
                  padding: const EdgeInsets.all(10.0),
                  child: Text(
                    'Buy ${widget.package.product.title}\n${widget.package.product.priceString}',
                    style: TextStyle(fontSize: 20),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(top: 8.0, bottom: 18.0),
              child: Text(
                '${widget.package.product.description}',
                textAlign: TextAlign.center,
                style: kSendButtonTextStyle.copyWith(fontSize: 16, fontWeight: FontWeight.normal),
              ),
            )
          ],
        ),
      ),
    );
  }
}

Step 4: Create a ProScreen class

We’ll use this class to confirm that the user has already purchased a subscription.

class ProScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TopBarAgnosticNoIcon(
      text: "Upgrade Screen",
      style: kSendButtonTextStyle,
      uniqueHeroTag: 'pro_screen',
      child: Scaffold(
          backgroundColor: kColorPrimary,
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(18.0),
                  child: Icon(
                    Icons.star,
                    color: kColorText,
                    size: 44.0,
                  ),
                ),
                Padding(
                    padding: const EdgeInsets.all(18.0),
                    child: Text(
                      "You are a Pro user.\n\nYou can use the app in all its functionality.\nPlease contact us at [email protected] if you have any problem",
                      textAlign: TextAlign.center,
                      style: kSendButtonTextStyle,
                    )),
              ],
            ),
          )),
    );
  }
}

Now run the app so we can see it all come together!

In-app purchases with flutter: a comprehensive step-by-step tutorial

If you’ve reached this point, congratulations! You worked really hard and you tied a lot of different components of the IAP world together.

Consider that time in a sandbox environment! Every transaction lasts 5 minutes and then renews up to 6 times. Also note that you will receive emails from Apple and Google about your subscription.

In-app purchases with flutter: a comprehensive step-by-step tutorial

Before we finish up, let me show you the RevenueCat dashboard. You can easily view all of your app’s IAP transactions in your dashboard and drill down into the activity of individual users.

In-app purchases with flutter: a comprehensive step-by-step tutorial

In-app purchases with flutter: a comprehensive step-by-step tutorial

Troubleshooting

When I run into problems, it’s usually because of the iOS sandbox account. It can be really frustrating because there are no details of the error in Xcode or Android. I have built a few apps, and for some strange reason, the sandbox account doesn’t work. Sometimes it gets invalidated if you log in to the App Store, but not always, and it depends on which OS version you have. Below I’ve listed a few of the errors you may get if you don’t have the sandbox account set up properly. (The part marked in red is where you can see that there is no data in the PurchaserInfo object.)

In-app purchases with flutter: a comprehensive step-by-step tutorial

If you are unable to fetch the data from RevenueCat with iOS, check out the Apple docs for more information on how to set up a sandbox account. There are also a lot of good questions in Stack Overflow, like this one for example. RevenueCat also has a nice article on the most frequent reasons for a failure to connect to Apple’s servers.

Conclusion

You’ve probably realized by now that while adding IAP capabilities to an app is definitely possible, it requires a lot of upfront investment to set up the environment. Writing the code is easy — the time-consuming part is all the admin work you have to do in Apple Connect and the Google Play Console.

I hope you liked this article (if you did, clap a few times!). And feel free to ask questions below if you get stuck on one of the steps of the tutorial. Stay safe, and happy coding!

Tags: Flutter Community

Related Posts

Adaptive layouts part 2 (the boring flutter development show, ep. 46)
Flutter Official

Adaptive Layouts part 2 (The Boring Flutter Development Show, Ep. 46)

516
Final vs const – programming #shorts
Flutter Explained

Final vs Const – Programming #Shorts

519
Intro to git – learning git and github – integration and alternatives
Flutter Explained

Intro to Git – Learning Git and GitHub – Integration and Alternatives

515
Flutter ui tip 3: popup card
Flutter Tutorial

Flutter UI Tip 3: Popup Card

522
[4k] pbo dart 22. Operator overriding
Erico Darmawan

[4K] PBO DART 22. Operator Overriding

516
Built-time vs  run-time #shorts
Flutter Explained

Built-Time vs Run-Time #Shorts

518
  • Flutter & dart – the complete guide [2020 edition]

    Flutter & Dart – The Complete Guide [2020 Edition]

    2272 shares
    Share 909 Tweet 568
  • The Complete 2020 Flutter Development Bootcamp with Dart

    1991 shares
    Share 796 Tweet 498
  • Flutter & Firebase: Build a Complete App for iOS & Android

    1422 shares
    Share 569 Tweet 356
  • Flutter Bloc & Cubit Tutorial

    1296 shares
    Share 518 Tweet 324
  • 40 Beautiful Flutter UI Themes For Developers

    1113 shares
    Share 445 Tweet 278

Made by Google

Flutter is Google’s portable UI toolkit for building beautiful, natively-compiled applications for mobile, web, and desktop from a single codebase.

Follow us

Recent Post

  • Adaptive Layouts part 2 (The Boring Flutter Development Show, Ep. 46)
  • VisualEyes marketing site – UI ideas for flutter
  • Final vs Const – Programming #Shorts
  • Intro to Git – Learning Git and GitHub – Integration and Alternatives
  • Unsplash iOS app – UI ideas for flutter

Popular Post

Working Asynchronous with Flutter

535
Flutter 64. Widget slider with transition

FLUTTER 64. Widget Slider with Transition

553

Review Post

Flutter themeswitcher template in flutter

Flutter ThemeSwitcher Template in Flutter

Congratulations, Nice Work, GLWS $7
Rosen – flutter ecommerce ui

Rosen - Flutter Ecommerce UI

Nice Product I am gonna love it. $18
  • [email protected]
  • Flutter Terms
  • Flutter Packages
  • Dart

Copyright © 2021 Flutter Website - by Flutter Team.

No Result
View All Result
  • Home
  • Categories
    • Flutter App
    • Flutter Examples
    • Flutter Github
    • Flutter News
    • Flutter PDF
    • Flutter Tips & Trick
    • Flutter Tutorial
    • Flutter UI
    • Flutter Video
    • Flutter VS
    • Flutter Wiki
    • Flutter With
  • Flutter
    • Flutter for Mobile
    • Flutter for Web
      • Widget Sample
    • Flutter for Desktop
    • Tools
      • Codemagic
      • Flutter Studio
      • Supernova
  • Showcase
  • Community
  • Advertisement
  • Hire Us
  • Login
  • Sign Up

Copyright © 2021 Flutter Website - by Flutter Team.

Welcome Back!

Sign In with Facebook
Sign In with Google
OR

Login to your account below

Forgotten Password? Sign Up

Create New Account!

Sign Up with Facebook
Sign Up with Google
OR

Fill the forms below to register

All fields are required. Log In

Retrieve your password

Please enter your username or email address to reset your password.

Log In
This website uses cookies. By continuing to use this website you are giving consent to cookies being used. Visit our Privacy and Cookie Policy.
Go to mobile version