← Back to blog

Firebase Stripe Extension with Flutter web

Renuka Kelkar

DevRel Advocate

7th March, 2023

Need a custom tool? Invertase can help

Tired of investing in off-the-shelf software that doesn't quite fit your business? Invertase can build solutions tailored to your exact needs. Tell us what you're looking for here and we'll be in touch.

Introduction

As you know, Flutter web is becoming more mature now. Developers are using Flutter web not only for creating portfolios but also for creating more complex apps. Integrating a payment gateway into the web app is the most important feature of any E-commerce App. This article will show how easily one could integrate Stripe payments with Flutter web using Firebase Extensions.

What is Stripe?

Stripe is a third-party payment service that manages one-time payments, prebuilt checkouts, subscriptions, invoices, billings, and many other features. it’s the one-stop solution for payment gateway.

What is Firebase Stripe Extension?

Firebase Extensions are pre-packaged solutions that help you deploy functionality to your Firebase application. It is the fastest way of integrating Stripe payment into the app with Firebase. It works as the backend for your stripe payment, so don’t need to think about the backend now!! You are good to go by integrating the Stripe extension and adding client-side code to your Flutter app. You can read more about this here

What you’ll need:

  • A Stripe account
  • Firebase project with setup of Cloud Firestore database
  • Firebase Authentication to enable different sign-up options for your users.

Enable email login

Integrating Stripe extension, you will need to authenticate the user first. So once the user is authenticated, then it triggers a Cloud function to create a Stripe customer.

Upgrade Firebase with Blaze plan

To install this extension, your Firebase project must be on the Blaze (pay-as-you-go) plan. You will only be charged for the resources you use. Most Firebase services offer a free tier for low-volume use. Learn more about Firebase billing.

Create a Cloud Firestore Database

Set Cloud Firestore security rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /customers/{uid} {
      allow read: if request.auth.uid == uid;

      match /checkout_sessions/{id} {
        allow read, write: if request.auth.uid == uid;
      }
      match /subscriptions/{id} {
        allow read: if request.auth.uid == uid;
      }
      match /payments/{id} {
        allow read: if request.auth.uid == uid;
      }
    }

    match /products/{id} {
      allow read: if true;

      match /prices/{id} {
        allow read: if true;
      }

      match /tax_rates/{id} {
        allow read: if true;
      }
    }
  }
}

Install the extension (either of them)

Option 1: Using the Firebase CLI

To install and manage extensions, you can also use the Firebase CLI:

Set up a new project directory or navigate to an existing one

Run the below command in your command line:

firebase ext:install stripe/firestore-stripe-payments --project=projectId_or_alias

Option 2: Using the Firebase Console

To add an extension, please click on the extension tab on the Firebase console and select the extension from the marketplace.

Select Stripe Extension

Install the extension

Select the Firebase project 

To install the extension, you must first select the project where you want to add Stripe payment.

Review API enabled and resources created.

While using Run Payments with Stripe extension, you don’t need to research, code or debug the code on your own. Firebase extension works for you behind the scenes, and it uses Storage, Eventarc API, and Cloud Functions

Review access granted to Stripe extension

In the Stripe extension, Stripe needs to access the following Firebase services for it to work together with Cloud Firestore.

Configure Extension

To configure an extension, you will need to specify the following:

  • Firestore collection to store customer data.

First, select the location for the deployment. Tip: You usually want a location closer to your database. For help selecting a location, refer to the location selection guide

  • Firestore collection to store customer data.
  • Do you want to delete customer objects in Stripe automatically? When a user is deleted in Firebase, then select accordingly.
  • Setup Stripe API restricted key to giving access only to the “Customers”, “Checkout Sessions”, and “Customer portal” resources.
  • This is your signing secret for a Stripe-registered webhook. This webhook can only be registered after installation. Leave this value untouched during installation, then follow the post-install instructions for registering your webhook and configuring this value.
  • Enable events. To make things easy, we can choose to select all.

Finally, click the Install extension button and wait for the setup to finish. It will take 3 to 5 minutes.

Setting up Stripe Dashboard

Generate Stripe Restricted API key

An important note to remember is you shouldn’t share your secret key with anyone.

Setup Restricted Webhook key

You need to set up a webhook that synchronises relevant details from Stripe with your Cloud Firestore. This includes product and pricing data from the Stripe Dashboard and customer’s subscription details. Create a webhook with the endpoint that the extension docs give you. Please add the event as per the documentation.

Creating Products

After adding a webhook key, it’s time to create a list of products. On the Stripe dashboard, it’s very easy to create product listings.

You can specify the title, description, and all the price (including currency) information.

You need to select a recurring payment option. The example further down in the article is all about how to use Stripe payment to subscribe to a membership. In that case, we need to select Payment mode:'subscription'for this product.

Once you have created products in the Stripe dashboard, the products will also be mirrored in Cloud Firestore.

Customise your Stripe Page

Stripe allows you to customize your product page with your branding. This branding design will be used later in the app when completing a payment.

Flutter Integration

Now, Let’s start building the Flutter project. We have done all of the setup, and now it’s time to learn how to implement the extension in Flutter web. Here is the project we want to build:

Setting up the environment

  • New Flutter web Project.
  • Integrate Firebase with Flutter web.
  • Authenticate user with Firebase Auth.
  • Add all of the required dependencies.
  • Adding Go_Router for Navigation.

In this project, we need to create:

  • login_Page.dart Authenticate the user.
  • dashboard**.dart -** Retrieving the product for subscription.
  • success.dart Landing page: once the transaction is done successfully, it will redirect to the Success page.
  • cancel.dart Re-routes to the landing page if a transaction has an error.

Start The Coding

Start with main.dart adding the code for Firebase integration and Routing.

main.dart

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_stripe_web_payment/loginPage.dart';
import 'package:flutter_stripe_web_payment/success.dart';

import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:go_router/go_router.dart';

import 'cancel.dart';
import 'constant.dart';
import 'dashboard.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
      options: const FirebaseOptions(
          apiKey: Constants.apiKey,
          authDomain: Constants.authDomain,
          projectId: Constants.projectId,
          storageBucket: Constants.storageBucket,
          messagingSenderId: Constants.messagingSenderId,
          appId: Constants.appId,
          measurementId: Constants.measurementId));
  usePathUrlStrategy();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});
  final GoRouter _router = GoRouter(
    routes: <RouteBase>[
      GoRoute(
        path: '/',
        builder: (BuildContext context, GoRouterState state) {
          return LoginPage();
        },
      ),
      GoRoute(
        path: '/dashboard',
        builder: (BuildContext context, GoRouterState state) {
          return const DashboardPage();
        },
      ),
      GoRoute(
        path: '/success',
        builder: (BuildContext context, GoRouterState state) {
          return const SuccessPage();
        },
      ),
      GoRoute(
        path: '/cancel',
        builder: (BuildContext context, GoRouterState state) {
          return CancelPage();
        },
      ),
    ],
  );

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: _router,
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

login_page.dart

An application that requires user authentication typically has a login page. We can authenticate users with email and password, but you can use any option like Google or Facebook etc. Once you finish the user authentication, it will trigger the cloud function and add a custom collection into Firestore. For the demo purpose below, we have created the Signing Page and manually created a user in Firebase, but if you want, you can code a separate one.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';

import 'package:flutter/material.dart';

import 'package:flutter_stripe_web_payment/utils/firebase_auth.dart';
import 'package:go_router/go_router.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();

  bool _obscureText = true;

  final TextEditingController _emailTextController = TextEditingController();

  final TextEditingController _passwordTextController = TextEditingController();

  Future<FirebaseApp> _initializeFirebase() async {
    FirebaseApp firebaseApp = await Firebase.initializeApp();
    return firebaseApp;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      backgroundColor: Colors.white,
      body: FutureBuilder(
        future: _initializeFirebase(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            return Row(
              children: [
                Expanded(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const SizedBox(
                        height: 20,
                      ),
                      const Center(
                        child: Text(
                          textAlign: TextAlign.center,
                          "Firebase Stripe Extension with \n Flutter Web",
                          style: TextStyle(
                            fontSize: 30,
                            fontWeight: FontWeight.bold,
                            color: Colors.blue,
                          ),
                        ),
                      ),
                      const SizedBox(
                        height: 50,
                      ),
                      Image.asset("assets/images/payment.png"),
                    ],
                  ),
                ),
                Expanded(
                  child: Form(
                    key: _formKey,
                    child: Center(
                      child: Container(
                        color: Colors.orangeAccent,
                        child: Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              const Text(
                                "Login Form",
                                style: TextStyle(
                                    fontSize: 30,
                                    fontWeight: FontWeight.bold,
                                    color: Colors.white),
                              ),
                              const SizedBox(height: 30),
                              Padding(
                                padding: const EdgeInsets.symmetric(
                                    horizontal: 20, vertical: 10),
                                child: TextFormField(
                                  keyboardType: TextInputType.emailAddress,
                                  controller: _emailTextController,
                                  decoration: InputDecoration(
                                    fillColor: Colors.white,
                                    filled: true,
                                    hintText: 'Email',
                                    enabledBorder: OutlineInputBorder(
                                      borderSide: const BorderSide(
                                        color: Colors.white,
                                      ),
                                      borderRadius: BorderRadius.circular(15),
                                    ),
                                    focusedBorder: OutlineInputBorder(
                                      borderSide: const BorderSide(
                                        color: Colors.white,
                                        width: 2,
                                      ),
                                      borderRadius: BorderRadius.circular(15),
                                    ),
                                  ),
                                ),
                              ),
                              const SizedBox(height: 15),
                              Padding(
                                padding: const EdgeInsets.symmetric(
                                    horizontal: 20, vertical: 10),
                                child: TextFormField(
                                  keyboardType: TextInputType.text,
                                  controller: _passwordTextController,
                                  obscureText: _obscureText,
                                  decoration: InputDecoration(
                                    hintText: 'Password',
                                    fillColor: Colors.white,
                                    filled: true,
                                    enabledBorder: OutlineInputBorder(
                                      borderSide: const BorderSide(
                                        color: Colors.white,
                                      ),
                                      borderRadius: BorderRadius.circular(15),
                                    ),
                                    focusedBorder: OutlineInputBorder(
                                      borderSide: const BorderSide(
                                        color: Colors.white,
                                        width: 2,
                                      ),
                                      borderRadius: BorderRadius.circular(15),
                                    ),
                                    suffixIconColor: Colors.orangeAccent,
                                    suffixIcon: GestureDetector(
                                      onTap: () {
                                        setState(() {
                                          _obscureText = !_obscureText;
                                        });
                                      },
                                      child: Icon(
                                        _obscureText
                                            ? Icons.visibility
                                            : Icons.visibility_off,
                                        color: Colors.orangeAccent,
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                              const SizedBox(height: 20),
                              ElevatedButton(
                                style: ElevatedButton.styleFrom(
                                  primary: Colors.white, // background
                                ),
                                onPressed: () async {
                                  User? user =
                                      await FireAuth.signInUsingEmailPassword(
                                    email: _emailTextController.text,
                                  );
                                  if (user != null) {
                                    context.go('/dashBoard');
                                  }
                                },
                                child: const Text(
                                  'Sign In',
                                  style: TextStyle(color: Colors.black),
                                ),
                              )
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            );
          }
          return const Center(
            child: CircularProgressIndicator(),
          );
        },
      ),
    ));
  }
}

Dashboard.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_stripe_web_payment/subscription.dart';

class DashboardPage extends StatefulWidget {
  const DashboardPage({Key? key}) : super(key: key);

  @override
  State<DashboardPage> createState() => _DashboardPage();
}

class _DashboardPage extends State<DashboardPage> {
  var _productId = '';

  var _checkoutSessionId = '';

  void _onSelectProduct(String? id) {
    setState(() => _productId = id ?? '');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Row(
          children: [
            Expanded(
              flex: 3,
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const SizedBox(height: 32),
                    Center(
                      child: Text(
                        'You can buy any of our memberships as a gift for you or someone else',
                        style: Theme.of(context).textTheme.headline4,
                      ),
                    ),
                    const SizedBox(height: 32),
                    // displaying products which is active
                    StreamBuilder<QuerySnapshot>(
                      stream: FirebaseFirestore.instance
                          .collection('products')
                          .where('active', isEqualTo: true)
                          .snapshots(),
                      builder: (BuildContext context, snapshot) {
                        if (snapshot.hasError) {
                          return Center(child: Text('$snapshot.error'));
                        } else if (!snapshot.hasData) {
                          return const Center(
                            child: SizedBox(
                              width: 50,
                              height: 50,
                              child: CircularProgressIndicator(),
                            ),
                          );
                        }
                        var docs = snapshot.data!.docs;
                        return ListView.builder(
                          shrinkWrap: true,
                          itemCount: docs.length,
                          itemBuilder: (BuildContext context, int index) {
                            final doc = docs[index];
                            return RadioListTile(
                              groupValue: _productId,
                              value: doc.id,
                              onChanged: _onSelectProduct,
                              title: Text('${doc['name']}'),
                            );
                          },
                        );
                      },
                    ),
                    const SizedBox(
                      height: 30,
                    ),
                    Column(
                      // mainAxisSize: MainAxisSize.c,
                      children: <Widget>[
                        if (_checkoutSessionId.isEmpty) //

                          Padding(
                            padding:
                                const EdgeInsets.symmetric(horizontal: 16.0),
                            child: ElevatedButton(
                              style: ElevatedButton.styleFrom(
                                  textStyle: const TextStyle(fontSize: 20)),
                              onPressed: () async {
                                final price = await FirebaseFirestore.instance
                                    .collection('products')
                                    .doc(_productId)
                                    .collection('prices')
                                    .where('active', isEqualTo: true)
                                    .limit(1)
                                    .get();
                                final docRef = await FirebaseFirestore.instance
                                    .collection('customers')
                                    .doc(FirebaseAuth.instance.currentUser?.uid)
                                    .collection("checkout_sessions")
                                    .add({
                                  "client": "web",
                                  "mode": "subscription",
                                  "price": price.docs[0].id,
                                  "success_url":
                                      'http://localhost:5600/success',
                                  "cancel_url": 'http://localhost:5600/cancel'
                                });
                                setState(() => _checkoutSessionId = docRef.id);
                                print(_checkoutSessionId.toString());
                              },
                              child: const Text('Subscribe'),
                            ),
                          )
                        else
                          Subscription(
                            checkoutSessionId: _checkoutSessionId,
                          )
                      ],
                    ),
                  ],
                ),
              ),
            ),
            Expanded(
                flex: 2,
                child: Container(
                  color: Colors.orangeAccent,
                  height: MediaQuery.of(context).size.height,
                  child: Image.network(
                    'https://cdn-icons-png.flaticon.com/512/5230/5230935.png',
                  ),
                )),
          ],
        ),
      ),
    );
  }
}

On the Dashboard page, we are retrieving the data from the product collection. Here, you will get the product ID. Using that product ID, we can fetch the product price from the prices collection. See Below:

StreamBuilder<QuerySnapshot>(
                      stream: FirebaseFirestore.instance
                          .collection('products')
                          .where('active', isEqualTo: true)
                          .snapshots(),

Creating Checkout session

To subscribe to Products, you need to have a Checkout Session ID. In order to get this ID, you can create a Checkout Session document in the Customer collection with the help of the recurring price ID.

To create a Checkout Session ID for the recurring payment, pass mode:'subscription'to the Checkout Session doc creation. For example:

 final price = await FirebaseFirestore.instance
                                    .collection('products')
                                    .doc(_productId)
                                    .collection('prices')
                                    .where('active', isEqualTo: true)
                                    .limit(1)
                                    .get();
                                final docRef = await FirebaseFirestore.instance
                                    .collection('customers')
                                    .doc(FirebaseAuth.instance.currentUser?.uid)
                                    .collection("checkout_sessions")
                                    .add({
                                  "client": "web",
                                  "mode": "subscription",
                                  "price": price.docs[0].id,
                                  "success_url":
                                      'http://localhost:5600/success',
                                  "cancel_url": 'http://localhost:5600/cancel'

In the below code, we have created a Subscription stateful widget, and here we are passing the Checkout Session ID, which we have retrieved from the Checkout session. Please wait for the Checkout Session to get attached by the extension. The extension will update the doc with a Stripe Checkout session ID and generate the URL, and this can then be used to redirect the user to the checkout page.

subscription. dart

import 'dart:async';
import 'dart:html' as html;

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

import 'package:flutter/material.dart';

typedef _CheckoutSessionSnapshot = DocumentSnapshot<Map<String, dynamic>>;

class Subscription extends StatefulWidget {
  const Subscription({
    super.key,
    required this.checkoutSessionId,
  });

  final String checkoutSessionId;

  @override
  State<Subscription> createState() => _SubscriptionState();
}

class _SubscriptionState extends State<Subscription> {
  late Stream<_CheckoutSessionSnapshot> _sessionStream;

  @override
  void initState() {
    super.initState();
    _sessionStream = FirebaseFirestore.instance
        .collection('customers')
        .doc(FirebaseAuth.instance.currentUser!.uid)
        .collection("checkout_sessions")
        .doc(widget.checkoutSessionId)
        .snapshots();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<_CheckoutSessionSnapshot>(
      stream: _sessionStream,
      builder: (BuildContext context,
          AsyncSnapshot<_CheckoutSessionSnapshot> snapshot) {
        if (snapshot.connectionState != ConnectionState.active) {
          return const Center(child: CircularProgressIndicator());
        } else if (snapshot.hasError || snapshot.hasData == false) {
          return const Text('Something went wrong');
        }
        final data = snapshot.requireData.data()!;
        if (data.containsKey('sessionId') && data.containsKey('url')) {
          html.window.location.href = data['url'] as String; // open the new window with Stripe Checkout Page URL
          return const SizedBox();
        } else if (data.containsKey('error')) {
          return Text(
            data['error']['message'] as String? ?? 'Error processing payment.',
            style: TextStyle(
              color: Theme.of(context).errorColor,
            ),
          );
        } else {
          return const Center(child: CircularProgressIndicator());
        }
      },
    );
  }
}

Finally

You can check the final output. We are first authenticating a user with a username and password. Once logged in, the application will navigate to the Dashboard page, where you can select the products you want to subscribe to. Once you click the subscribe button, it will redirect you to the Stripe Checkout page. You can then fill in all the required details and hit the subscribe button. Following a successful transaction, it will take you back to your app.

You can view the full code here:

Stay tuned for more updates and exciting news that we will share in the future. Follow us on Invertase TwitterLinkedin, and Youtube, and subscribe to our monthly newsletter to stay up-to-date. You may also join our Discord to have an instant conversation with us.

Renuka Kelkar

DevRel Advocate

Categories

Tags