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 Twitter, Linkedin, 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.