Accept payments in your Flutter app using Razorpay

Souvik Biswas
Flutter Community
Published in
17 min readJan 17, 2023

Razorpay is a payment gateway that enables Indian businesses to accept online payments from customers using a wide range of payment methods, including credit and debit cards, net banking, UPI, and digital wallets. It is known to be one of the major platforms for accepting payment in India.

In this tutorial, you will learn how to integrate Razorpay with a Flutter app and enable it to accept payments.

The official Razorpay Flutter package only supports Android and iOS platforms. But in this article, you will also learn to integrate Razorpay with Flutter Web using JavaScript interoperability for Dart 🚀. Follow along to learn more.

NOTE: Razorpay is currently only available for Indian businesses, allowing them to accept payments from local and global customers.

TL;DR: Here’s the GitHub link to the entire project.

App Overview

The final app we will build throughout this tutorial will mainly consist of a single page, Home Page. Here, we will take the user information required for the checkout and start the payment process with Razorpay as the Checkout button is tapped.

Once the checkout process is done using the Razorpay dialog, we will receive the response and can verify whether the transaction was successful.

If the transaction was successful, we’d show a SnackBar with the text: “Payment successful”, otherwise show “Payment failed”.

Create a Flutter app

Let’s get started by creating a new Flutter project.

To create a Flutter app, you can either use an IDE (like VS Code, IntelliJ, or Android Studio) or just run the following command inside your terminal:

flutter create razorpay_demo

To use the flutter command, you should have Flutter SDK installed on your system. If you don't have it installed, follow the steps here.

Open the project using your favorite IDE and navigate to the lib/main.dart file. By default, Flutter creates a demo counter app project.

Replace the entire code with the following:

Create another file inside the lib directory called home_page.dart. In this file, we'll define the UI code for the app's Home Page.

For now, add a simple StatefulWidget called HomePage:

We will update the HomePage code later on in this article.

Setup Razorpay

To use Razorpay inside your app, you need to create a Razorpay. If you already have an account, you can log in here.

Once you are logged in to your account, it will take you to the Razorpay Dashboard. Make sure you turn on the Test Mode.

Test Mode helps you to simulate the payments via the Razorpay SDKs without involving any real money for the transactions.

Next, you need to generate a test API Key to access the Razorpay APIs in Test Mode:

  1. Select the Settings page from the left menu.
  2. Go to the API Keys tab.
  3. Click Generate Test Key. Copy/download the generated Key Id and Key Secret (visible only once), and store them in a safe place.

You will need both of these while accessing the Razorpay APIs inside the Cloud Functions.

Now that we have the Razorpay setup complete, let’s create a Firebase project where we’ll be deploying the Cloud Functions.

Create Firebase project

Follow the steps below to create a Firebase project:

  1. Go to Firebase Console. You’ll need to log in using your Google account.
  2. Click Add project.

3. Give a name to your project and click Continue.

4. Disable Google Analytics as this is just a demo project. But if you are using Firebase for any production application, enabling analytics is recommended. Click Create project.

5. It will take a few moments to create the project. Once it’s ready, click Continue.

This will navigate you to the Firebase dashboard page.

For using Cloud Functions, your project should be in the Blaze Plan. By default, every Firebase project is in Spark Plan.

Click on Upgrade from the left menu, select the Blaze Plan, and configure/choose a billing account.

Now, you are ready to write and deploy Cloud Functions!

Configure Cloud Functions

Razorpay requires you to maintain a backend server to generate the order ID securely and verify the signature after checkout. Today, we’ll be using Firebase Cloud Functions for making those API calls, which prevents the hassle of maintaining a server, helping us to achieve a completely serverless architecture.

Cloud Functions lets you run backend code on the serverless infrastructure managed by Firebase so that you don’t have the hassle of building or maintaining your own servers.

To get started with Cloud Functions, you’ll need to install the Firebase CLI. You can find the installation guide on this page.

Once you have the Firebase CLI installed on your system, you can access it with the firebase command, run the following to test if it's working:

firebase --version

If the version is printed on the console, then you are good to proceed.

Log in to Firebase using the following command:

firebase login

This will open a webpage in your browser from where you have to sign in to the Google account you used to access Firebase.

The following steps will guide you through the configuration process of Cloud Functions:

1. Navigate to your Flutter project directory and run the following command to start initializing Cloud Functions:

firebase init

2. Among the features of setup, select Functions.

3. In Project Setup, choose to Use an existing project and select the Firebase project you created earlier.

4. In Functions Setup, use the following:

  • Language: JavaScript
  • ESLint enable: Yes
  • Install dependencies: Yes

Once the initialization process completes, you will find a new folder is generated inside your Flutter project directory called functions:

Here, index.js is the file where you have to write the functions. You will find all the dependencies of the Cloud Functions inside the package.json file.

Writing Functions

You will require two Cloud Functions for using the Razorpay APIs:

  • createOrder: For creating an order. You will need the order ID, that's present in the response, during the checkout process.
  • verifySignature: For verifying the authenticity of the transaction. It is done after the checkout process is complete. This is a MANDATORY step.

Let’s start writing the functions.

First, you need to install the required dependency. We only need the razorpay npm package:

cd functions
npm i razorpay
npm i @babel/eslint-parser --save-dev

The @babel/eslint-parser dependency is required for the ESLint.

This will add the dependencies to your package.json file.

You need to import the required dependencies inside the index.js file. Add the following code:

const functions = require("firebase-functions");
const Razorpay = require("razorpay");
const crypto = require("crypto");

Initialize Razorpay using:

const razorpay = new Razorpay({
key_id: functions.config().razorpay.key_id,
key_secret: functions.config().razorpay.key_secret,
});

NOTE: We will accept the Key ID and the Key Secret as config values while deploying these Cloud Functions.

Next, we’ll generate an order using razorpay.orders.create() function by passing an amount, currency, receipt, and description. We will return the response received from this call (this will contain the order ID).

exports.createOrder = functions.https.onCall(async (data, context) => {
try {
const order = await razorpay.orders.create({
amount: data.amount,
currency: data.currency,
receipt: data.receipt,
notes: {
description: data.description,
},
});

return order;
} catch (err) {
console.error(`${err}`);
throw new functions.https.HttpsError(
"aborted",
"Could not create the order",
);
}
});

Add another function for verifying the signature and confirming the transaction’s authenticity. The steps required for the payment signature verification are described in the Razorpay documentation here.

We’ll need to send the order ID (received from the createOrder Cloud Function), and payment ID & signature (received as the response after a successful checkout process). This function will return a boolean based on whether the verification was successful.

exports.verifySignature = functions.https.onCall(async (data, context) => {
const hmac = crypto.createHmac(
"sha256",
functions.config().razorpay.key_secret,
);
hmac.update(data.orderId + "|" + data.paymentId);
const generatedSignature = hmac.digest("hex");
const isSignatureValid = generatedSignature == data.signature;
return isSignatureValid;
});

NOTE: This method should only be used after a successful checkout process. So, if the checkout fails, there’s no need for any verification.

Deploying to Firebase

Before deploying the functions, you must define the Key ID and the Key Secret as config variables. Run the following command:

firebase functions:config:set razorpay.key_id="<KEY_ID>" razorpay.key_secret="<KEY_SECRET>"

In the above command, replace the <KEY_ID> and <KEY_SECRET> with the values you generated as the Test API Key.

Modify the .eslintrc.js file to include the parserOptions:

module.exports = {
root: true,
env: {
es6: true,
node: true,
},
extends: [
"eslint:recommended",
"google",
],
rules: {
quotes: ["error", "double"],
},
parserOptions: {
sourceType: "module",
ecmaVersion: 8,
ecmaFeatures: {
jsx: true,
experimentalObjectRestSpread: true,
},
},
};

Now, use this command to deploy the functions:

firebase deploy --only functions

After a successful deployment of the Cloud Function, you should be able to see them on the Firebase Functions page.

Connect Firebase with Flutter

To use Firebase services inside your Flutter app, you should have the following CLI tools installed on your system:

  • Firebase CLI: For accessing the Firebase projects (already configured).
  • FlutterFire CLI: Helps in the Dart-only configuration of Firebase.

Install FlutterFire CLI using the following command:

dart pub global activate flutterfire_cli

Before configuring Firebase, install the firebase_core plugin in your Flutter project by running the following command:

flutter pub add firebase_core

To start configuring Firebase, run the following command from the root directory of your Flutter app:

flutterfire configure

You’ll be prompted to select the Firebase project and the platforms (select Android, iOS, and Web platform) for which you want to configure your Flutter app.

Once the configuration is complete, it will generate the firebase_options.dart file inside the lib directory.

Now, go back to your Flutter project and open the main.dart file. Add the following inside the main function (mark the main as async):

Build app interface

As discussed earlier, the app will primarily consist of just a single page, Home Page. Before we start building the UI, add one required image by creating a new folder inside the root directory of your project called assets (you can download the image here).

Let’s have a closer look at the Home Page to understand the layout and the widgets required:

Time to start building the user interface!

I won’t cover the entire building process of the UI in depth (as it’s not the main focus). You will find a link to the whole project at the end of this article.

To have our colors easily accessible throughout the project, let’s add a res/palette.dart file inside the lib directory, and store the required custom colors here:

Navigate to the home_page.dart file. Add the following code inside the build() method of _HomePageState class which will serve as the initial structure:

Taking user inputs

For taking the user inputs, we will use TextFormField widgets.

To have a customized TextFormField widget, which we will re-use for each of the user inputs, create a new file called input_field.dart inside lib/widget folder.

Define a class called InputField:

Go back to the home_page.dart file. Initialize the TextEditingController(s) required for each of the TextFormField widgets:

Use the InputField widget:

For the currency symbol placed in the leading of the TextFormField, we can define a Map variable for this demo:

Notice we have also used a validator inside the Amount InputField. To define the validators, you can create a new file called validator.dart inside the lib/utils folder.

Define a class called Validator containing functions for validating each of the TextFormField(s):

The above code shows the validator for the amount field. Similarly, you can define the validators for the other fields as well.

Finally, for the validation to work, you need to wrap all the fields inside a Form widget and pass a GlobalKey to it.

Define a global key:

Wrap the ListView containing the InputField widgets with a Form:

Currency selector

We will be using ChoiceChip widgets for showing each of the selectable currencies, and they will be placed inside a widget to let them wrap to the following line when there's not much horizontal space to fit all the ChoiceChips.

Define a variable called _choiceChipValue for storing the currently selected currency:

int _choiceChipValue = 7; // INR initially

Display the currency ChoiceChip(s):

When any of the ChoiceChip is selected, the new index is stored in the _choiceChipValue variable.

Checkout button

We will use a simple ElevatedButton widget as the checkout button:

In the above code, inside the onPressed method, we need to validate all the fields and then start the checkout process. We'll implement this in the next section.

Showing errors

Add the following to the onPressed callback of the "Checkout" button:

In the above code, we have used the validate() method on the Form using the _formKey to validate all the fields. If the validation is successful (no errors), we'll proceed with the checkout. Otherwise, this will automatically show the error messages below each of the TextFormField(s) as per the validation methods we defined earlier. And, we'll also show a floating error message on top of the screen using the _showErrorBar method.

The method can be implemented like this:

Create a new file called error_bar.dart inside lib/widgets directory and add the following code for the ErrorBar widget:

Now, we’ll use this widget inside the HomePage:

Integrate Razorpay

Now that we have the basic UI of the app done, it’s time to start integrating Razorpay. First, you need to add the following dependencies to your Flutter project:

Add these two dependencies by running the following command:

flutter pub add razorpay_flutter cloud_functions

This will add these two packages to the pubspec.yaml file of your Flutter project.

We need to complete two more steps before we start the actual Razorpay integration:

  1. Creating the required model classes, and
  2. Defining a new widget called ProgressBottomSheet that we'll show as a bottom sheet while processing the payment.

Model classes

The model classes will help us easily store and access the order details, Razorpay responses, etc.

Create a new folder inside the lib directory called models. We will be storing all the model class files inside this folder.

OrderDetails

Class for storing all the order details we have taken as inputs from the user.

  • Define a new file called order_details.dart.
  • Add the code for this class from here.

RazorpayOptions

Class for storing the options that can be passed while doing a Razorpay checkout.

  • Define a new file called razorpay_options.dart.
  • Add the code for this class from here.

RazorpayResponse

Class for storing the Razorpay response object after a checkout process is complete.

  • Define a new file called razorpay_response.dart.
  • Add the code for this class from here.

ProcessingOrder

Class for storing the details received from the createOrder function call.

  • Define a new file called processing_order.dart.
  • Add the code for this class from here.

Progress Bottom Sheet widget

Create a new file called progress_bottom_sheet.dart inside the lib/widgets directory. First, define an enum inside this file called PaymentStatus which will store the different possible payment statuses.

Add the ProgressBottomSheet class as follows:

In the above code, there are two main parameters that we have defined inside the ProgressBottomSheet widget:

  • orderDetails: For passing the OrderDetails object required for processing the checkout.
  • onPaymentStateChange: Callback for returning the current PaymentStatus.

Razorpay initialization

Define two variables inside the _ProgressBottomSheetState class, one for storing the Razorpay object and the other for storing FirebaseFunctions object:

Define a method for initializing Razorpay:

Here, we have initialized Razorpay, set the payment status to idle initially, and registered the event listeners that will help us to handle different cases of the checkout process:

  • _handlePaymentSuccess will be triggered if checkout is successful.
  • _handlePaymentError will be triggered in case of any error during the checkout process.
  • _handleExternalWallet will be triggered when an external wallet (FreeCharge, MobiKwik, ...) is used to complete the transaction.

Inside initState() initialize the variables and call the _initializeRazorpay() method:

Close all the Razorpay event listeners inside the dispose()method:

Checkout method

Create a method called _checkoutOrder() inside the _ProgressBottomSheetState class.

We have passed the required parameters to the _checkoutOrder() method. There are three steps that are performed inside this method:

  1. Create an order using the createOrder Cloud Function and store the response as a ProcessingOrder object. If the object is not null proceed to the next steps.
  2. Define Razorpay options to customize the checkout process. You also need to pass the Razorpay Key ID here.
  3. Start checkout by calling the open()method on the Razorpay object and passing the options.

For storing the Razorpay Key ID, you can create a new file inside the lib directory called secrets.dart, and store it like this:

We will call the _checkoutOrder method right after initializing Razorpay:

Handle successful checkout

Once the checkout process is successful, we first need to ascertain whether the signature is correct. Define a method called _verifySignature():

We have used the verifySignature Cloud Function for the signature verification.

Inside the _handlePaymentSuccess method, call the _verifySignature method and then set the payment status based on that:

We can use the onPaymentStateChange callback to update the payment status. And the bottom sheet is closed once the status is updated.

Handle checkout error

In case of a failed checkout attempt, we’ll update the PaymentStatus to failed and close the bottom sheet.

Handle external wallet usage

You can define any additional action you want to perform when using an external wallet. But for this demo, we will just add a log statement inside the _handleExternalWallet method:

Start checkout

Now, go back to the HomePage class. Create a method called _onTapCheckout() inside the _HomePageState class:

Here, we have opened the ProgressBottomSheet using the showModalBottomSheet() method. And once the bottom sheet is closed, the PaymentStatus is again set to idle.

Inside the “Checkout” button, in case all the fields are valid, call the _onTapCheckout method:

Add web platform support

As discussed earlier, the razorpay_flutter package only supports Android and iOS platforms. Today, we’re going to implement Razorpay for the web platform as well!

There are certain challenges that we need to overcome in order to achieve this, we will go step by step through the entire process. Hang on tight and follow the steps!

JavaScript code

From the root directory to your project, go to the folder and open the index.html file. Add the Razorpay web library towards the end of the <body> tag like this:

<body>
// ...
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
</body>

Add a new folder called src inside web directory and create a file named payment.js inside it.

Inside this file, we’ll create a checkout() method to handle the checkout process on web:

function checkout(optionsStr) {
var options = JSON.parse(optionsStr);
var isProcessing = true;
options["modal"] = {
"escape": false,
// Handle if the dialog is dismissed
"ondismiss": function () {
// The callback should only be triggered if the dialog
// is dismissed manually
if (isProcessing) {
let responseStr = JSON.stringify({
'isSuccessful': false,
'errorCode': 'MODAL_DISMISSED',
'errorDescription': 'Razorpay payment modal dismissed'
});
handleWebCheckoutResponse(responseStr);
}
}
};
// Handling successful transaction
options.handler = function (response) {
let responseStr = JSON.stringify({
'isSuccessful': true,
'orderId': response.razorpay_order_id,
'paymentId': response.razorpay_payment_id,
'signature': response.razorpay_signature
});
isProcessing = false;
handleWebCheckoutResponse(responseStr);
}
// Initialize Razorpay
let razorpay = new Razorpay(options);
// Handling failed transaction
razorpay.on('payment.failed', function (response) {
let responseStr = JSON.stringify({
'isSuccessful': false,
'errorCode': response.error.code,
'errorDescription': response.error.description,
});
isProcessing = false;
handleWebCheckoutResponse(responseStr);
});
// Start checkout process
razorpay.open();
}

Here, the handleWebCheckoutResponse() is a function that we are going to implement in Dart as a JS interop (that lets JS directly call a Dart function). The rest of the code is quite similar to the Dart implementation, the above comments inside the code should be enough to understand what's going on.

Import this script in index.html file:

<body>
// ...
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<script src="src/payment.js"></script>
</body>

Dart code: JS interop

Add a new Dart package, js, by running the following command:

flutter pub add js

Now comes the tricky part, any file importing this package and using JS interop can’t be compiled for Android or iOS platforms (only web compilation is possible). So, we need to import that file only when compiled for web.

Luckily there’s a way to achieve that in Dart!

Create a new folder inside the lib/utils directory called razorpay_client. First, create an abstract class called RazorpayCheckoutBase inside a new file razorpay_checkout.dart:

Next, create a new file called razorpay_checkout_stub.dart and define RazorpayCheckout class that extends the abstract class:

This is the file that would be imported while it’s being compiled for Android or iOS because we won’t be using this checkout method anyways for the mobile platforms.

Finally, define another file called razorpay_checkout_web.dart. In this file, first import the js package and define two JS function signatures:

In the above code:

  • handleWebCheckoutResponse is the method that we are calling from the JS side, and
  • checkout is the JS method that we are calling from the Dart side.

Define the RazorpayCheckout class:

Here the webCheckoutResponse is the function that will be triggered when handleWebCheckoutResponse function is called on the JS side. Using the following line of code, we have initialized the JS interop:

handleWebCheckoutResponse = allowInterop(webCheckoutResponse);

Then, we called the checkout JS method by passing the options:

checkoutWeb(jsonEncode(options));

Modification to use web checkout

Go to the ProgressBottomSheet class, and create a new method called _webCheckoutResponse():

Import the correct RazorpayCheckout class file:

If the dart.library.html is present, it means the app is being compiled on web, and only then the razorpay_checkout_web.dart file is imported.

Initialize the RazorpayCheckout:

This method will be used to handle the checkout for the web platform. Inside the _checkoutOrder() method, modify the code to use this method:

And inside the _initializeRazorpay() method, only initialize the Razorpay events if it's run on Android or iOS:

App in action

The app should be ready to run on the web platform!

But before you run the app on Android and iOS device, there are certain platform-specific configurations you need to complete.

Android Configuration

From the root project directory, go to android/app/build.gradle file and update the minSdkVersion to 19:

android {
defaultConfig {
minSdkVersion 19
}
}
}

iOS Configuration

From the root project directory, go to ios/Podfile file and uncomment the following line to use the platform version 11.0:

platform :ios, '11.0'

Great! Now, the app is ready to be run on Android, iOS, and Web platforms. You can either use an emulator/Simulator or use a physical device to try out the app.

As we have used the Razorpay Test API Key, the app would be running in a sandboxed mode which will let you simulate a successful or failed transaction.

Check out this page for more information on using Test Cards and Test UPI IDs.

You should be able to see your test transactions on the Razorpay Dashboard by going to the Transactions > Payments page.

Conclusion

Woohoo! 🎉 You have successfully integrated an entire checkout flow in your Flutter app using Razorpay. And the app works on mobile as well as web platforms!

Once you are done testing your Razorpay integration and you are ready to go live, follow this checklist:

  1. Complete KYC (or, the Activation Form) to get access to the Razorpay Live API.
  2. Log into the Razorpay Dashboard and switch to Live Mode on the menu.
  3. Navigate to SettingsAPI KeysGenerate Key to generate the API Key for Live Mode.
  4. Replace the Test API Key with the Live Key in the Flutter app’s checkout and the Firebase Cloud Functions (and re-deploy the functions).

References

Originally published at https://blog.flutterflow.io.

You can follow me on Twitter and find some of my projects on GitHub. Also, don’t forget to checkout my Website. Thank you for reading, if you enjoyed the article make sure to show me some love by hitting that clap (👏) button!

Happy coding…

Souvik Biswas

--

--

Souvik Biswas
Flutter Community

Mobile Developer (Android, iOS & Flutter) | Technical Writer (FlutterFlow) | IoT Enthusiast | Avid video game player