OneSheep OneSheep

Single and Recurring Payment with Flutter and Stripe

There are several options to implement single payments in Flutter. It can be done easily with the pay, or flutter_​stripe package. However, neither of these packages provide an easy way of implementing a recurring subscription payment in the app. In this article we will learn how to implement Single and Recurring Payments (Subscription) with Flutter and Stripe.

We will be using the Stripe Checkout for this purpose. Some of the benefits of using Stripe Checkout are:

  1. Real-time card validation with built-in error messaging 
  2. Fully responsive design with Apple Pay, Google Pay or card payment
Stripe Subscription using Flutter


Setting up Stripe

To make payments using Stripe we first need to generate a secure Session Token. This needs to be done in a privileged environment like a server. 

We are using Firebase Cloud Functions as our secure backend, although you can use any backend you prefer. Stripe has official libraries for these languages: Ruby, Python, PHP, Java, Node, Go, .NET.

First of all create a Stripe account and get the test secret key.

We need to set this key as a firebase config in the Firebase Firestore using the following command:

firebase functions:config:set stripe.testkey=sk_test_xxxxxxxxxxxxxxxxxx


Now we are ready to install the stripe.js package using npm

npm install stripe --save


Let’s first create two cloud functions, one for single payments and one for recurring payments.

Note: This would be your API endpoint if you are using REST API’s to communicate with your backend
// index.ts

import * as functions from "firebase-functions";

import Stripe from "stripe";

const stripe = new Stripe(functions.config().stripe.testkey, {
  apiVersion: "2020-08-27",
});

// get the payment url to the payment session
exports.getPaymentSession = functions.https.onCall(async (data) => {
  try {
    const checkoutSession = await stripe.checkout.sessions.create({
      mode: "payment",
      line_items: [
        {
          price_data: {
            currency: 'USD',
            product_data: {
              name: "Flutter Payment",
            },
            unit_amount: data.amount * 100,
          },
          quantity: 1,
        },
      ],
      payment_method_types: ["card"],
      success_url: "https://www.success.com",
      cancel_url: "https://www.cancelled.com",
      billing_address_collection: "required"
    });

    return checkoutSession.url;
  } catch (error) {
    console.log(`error: ${error}`);
    return null;
  }
});

exports.getSubscriptionSession = functions.https.onCall(async (data) => {
  try {
    const checkoutSession = await stripe.checkout.sessions.create({
      mode: "subscription",
      line_items: [
        {
          price_data: {
            currency: 'USD',
            product_data: {
              name: "Flutter Subscription",
            },
            recurring: {
              interval: "month",
              interval_count: 1,
            },
            unit_amount: data.amount * 100,
          },
          quantity: 1,
        },
      ],
      payment_method_types: ["card"],
      success_url: "https://www.success.com",
      cancel_url: "https://www.cancelled.com",
      billing_address_collection: "required"
    });

    return checkoutSession.url;
  } catch (error) {
    console.log(`error: ${error}`);
    return null;
  }
});


Configuring Flutter

We need to use WebView to be able to use the Stripe Checkout as it is not yet implemented for Flutter.

Install all the required packages:

webview_flutter: ^2.1.2
firebase_core: '^1.10.0'
cloud_functions: '^3.1.1'

Now onto the Flutter side:

// main.dart

import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:stripesubscription/checkout_screen.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _formKey = GlobalKey<FormState>();

  final _amountController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        resizeToAvoidBottomInset: false,
        backgroundColor: const Color(0xffFF2274),
        body: Form(
          key: _formKey,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: SizedBox.expand(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  SizedBox(
                    width: 200,
                    child: TextFormField(
                      controller: _amountController,
                      style: const TextStyle(
                        color: Colors.pink,
                        fontWeight: FontWeight.bold,
                        fontSize: 20,
                      ),
                      decoration: InputDecoration(
                        fillColor: Colors.white,
                        filled: true,
                        border: const OutlineInputBorder(),
                        prefixIcon: const Icon(Icons.attach_money),
                        hintText: 'Enter Amount',
                        hintStyle: const TextStyle(
                          color: Colors.grey,
                          fontWeight: FontWeight.bold,
                          fontSize: 20,
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderSide: BorderSide(
                            color: Colors.pink.shade100,
                            width: 4,
                          ),
                        ),
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter the amount';
                        }

                        if (double.tryParse(value) == null) {
                          return 'Enter valid amount';
                        }

                        return null;
                      },
                    ),
                  ),
                  const SizedBox(height: 50),
                  SizedBox(
                    height: 50,
                    width: 325,
                    child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        primary: const Color(0xffFF2274),
                      ),
                      onPressed: () =>
                          _makeSinglePayment(functionName: 'getPaymentSession'),
                      child: const Text('Single payment'),
                    ),
                  ),
                  const SizedBox(height: 10),
                  SizedBox(
                    height: 50,
                    width: 325,
                    child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        primary: const Color(0xffFF2274),
                      ),
                      onPressed: () => _makeSinglePayment(
                        functionName: 'getSubscriptionSession',
                      ),
                      child: const Text('Recurring Payment'),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ));
  }

  void _makeSinglePayment({required String functionName}) async {
    final amount = double.tryParse(_amountController.text);
    if (amount == null) return;

    final stripeCheckout =
        FirebaseFunctions.instance.httpsCallable(functionName);

    final response = await stripeCheckout.call(
      <String, dynamic>{
        "amount": amount,
      },
    );

    if (response.data == null) {
      print('response empty');
      return;
    }

    final sessionUrl = response.data;

    print(sessionUrl);

    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => CheckoutScreen(url: sessionUrl),
      ),
    );
  }

  @override
  void dispose() {
    _amountController.dispose();
    super.dispose();
  }
}


Create a checkout_screen.dart‘ file with the following content:

// checkout_screen.dart

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

class CheckoutScreen extends StatefulWidget {
  final String url;

  const CheckoutScreen({required this.url, Key? key}) : super(key: key);

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

class _CheckoutScreenState extends State<CheckoutScreen> {
  bool _isLoading = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xffFF2274),
      resizeToAvoidBottomInset: false,
      body: Stack(
        children: [
          Padding(
            padding: const EdgeInsets.only(top: 32),
            child: WebView(
              initialUrl: widget.url,
              javascriptMode: JavascriptMode.unrestricted,
              onPageFinished: (url) {
                setState(() {
                  _isLoading = false;
                });
              },
              navigationDelegate: (NavigationRequest request) {
                if (request.url.startsWith('https://www.success.com')) {
                  Navigator.of(context).pop();
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Payment Successful'),
                      backgroundColor: Colors.green,
                      duration: Duration(seconds: 2),
                    ),
                  );
                }

                if (request.url.startsWith('https://www.cancelled.com')) {
                  Navigator.of(context).pop();
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Payment Cancelled'),
                      backgroundColor: Colors.red,
                      duration: Duration(seconds: 2),
                    ),
                  );
                }

                return NavigationDecision.navigate;
              },
            ),
          ),
          if (_isLoading) _loadingSpinner,
        ],
      ),
    );
  }

  Widget get _loadingSpinner {
    return Container(
      width: double.infinity,
      height: double.infinity,
      color: Colors.pink,
      child: const Center(
        child: SizedBox(
          height: 30,
          width: 30,
          child: CircularProgressIndicator(
            valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
          ),
        ),
      ),
    );
  }
}


Considerations for Google Pay

We can use webview_flutter for Card and Apple Pay payment, but Google Pay doesn’t work with WebView.

So, we need to use Chrome Custom Tabs for it — flutter_​web_​browser to the rescue. It uses Chrome Custom Tabs & SFSafariViewController under the hood so both Apple and Google Pay works with it.

You can access the full source code for the project here: https://​git​hub​.com/​O​n​e​S​h​e​ep/ex…


Posted on Nov 18, 2021 by Paurakh Sharma Humagain

Back to all posts