Integrating Connect IPS in Flutter.

Sushan Shakya
5 min readJun 21, 2023

--

Article uses BLoC as a state management solution.

To integrate Connect IPS in Flutter we need to use WebView.

Connect IPS is very different from other payment gateways like : E-sewa or Khalti because it sends out response as HTML.
So, to work with Connect IPS we need to use WebView in Flutter.

Before we begin integrating Connect IPS we must first establish how our software is going to handle payments. Connect IPS requires an encrypted token to be generated. It is a good idea to generate this token from the backend.
We also want all the params required to start the payment to be generated by the backend and the frontend (i.e The Mobile App) will only send a request to the Connect IPS server to start the payment.

The following will be the flow for the app :

Here,
We’re only interested in Connect IPS payment so, following is the flow we want to focus on :

# Steps to integrate

Step #1

Add the following dependency to pubspec.yaml :

dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.2.0

Step #2

Create a page for Connect IPS payment :

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

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

class PaymentView extends StatefulWidget {
final int purchaseId;

const PaymentView({
Key? key,
required this.purchaseId,
}) : super(key: key);

@override
State<PaymentView> createState() => _PaymentViewState();
}

class _PaymentViewState extends State<PaymentView> {
late WebViewController controller;

@override
void initState() {
super.initState();
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0xffffffff))
..setNavigationDelegate(
NavigationDelegate(
onUrlChange: (value) {
print('---- URL Change');
print(value.url);
},
onProgress: (int progress) {
// Update loading bar.
},
onPageStarted: (String url) {
print('------ URL');
print(url);
},
onPageFinished: (String url) {},
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) {
return NavigationDecision.navigate;
},
),
);
}

void initWebView(String content) {
final uri = Uri.dataFromString(
content,
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8'),
);

controller.loadRequest(uri);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Payment'),
),
body: WebViewWidget(
controller: controller,
),
);
}
}

As we can see, a WebView is required because there is no other way to integrate Connect IPS except by using WebView .

Step #3

Create a mechanism to get the required params from the backend.
For this, we will follow the following flow :

The following is the json structure in which the backend will return the params :

{
"MERCHANTID" : "<merchant-id>",
"APPID" : "<app-id>",
"APPNAME" : "<app-name>",
"TXNID" : "<transaction-id>",
"TXNDATE" : "<transaction-date>",
"TXNCRNCY" : "<transaction-currency>",
"TXNAMT" : "<transaction-amount>",
"REFERENCEID": "<reference-id>",
"REMARKS": "<remarks>",
"PARTICULARS": "<particulars>",
"TOKEN": "<encrypted-token>",
}

Then,
We will create an HTML string to send request to Connect IPS server to start the payment.

We will create a Cubit for this purpose as :

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import './dio.dart';

part 'connect_ips_payment_state.dart';

class ConnectIpsPaymentCubit extends Cubit<ConnectIpsPaymentState> {
ConnectIpsPaymentCubit() : super(ConnectIpsPaymentInitial());

Future<Map<String, dynamic>> _getParams(int purchaseId) async {
final res = await dio.post('/getParams', dat: {
"purchaseId": purchaseId,
});
return res.data;
}

Future<String> _getHtmlString(int purchaseId) async {
final param = await _getParams(purchaseId);
return """
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<script>
const form = document.createElement("FORM");
form.setAttribute("action", "https://uat.connectips.com/connectipswebgw/loginpage");
form.setAttribute("target", "");
form.setAttribute("method", "POST");

const merchantIdElement = document.createElement("INPUT");
merchantIdElement.setAttribute("name", "MERCHANTID");
merchantIdElement.setAttribute("value", "${param['MERCHANTID']}");
merchantIdElement.setAttribute("type", "hidden");
form.appendChild(merchantIdElement);

const appId = document.createElement("INPUT");
appId.setAttribute("name", "APPID");
appId.setAttribute("value", "${param['APPID']}");
appId.setAttribute("type", "hidden");
form.appendChild(appId);

const appName = document.createElement("INPUT");
appName.setAttribute("name", "APPNAME");
appName.setAttribute("value", "${param['APPNAME']}");
appName.setAttribute("type", "hidden");
form.appendChild(appName);

const txnId = document.createElement("INPUT");
txnId.setAttribute("name", "TXNID");
txnId.setAttribute("value", "${param['TXNID']}");
txnId.setAttribute("type", "hidden");
form.appendChild(txnId);

const txnDate = document.createElement("INPUT");
txnDate.setAttribute("name", "TXNDATE");
txnDate.setAttribute("value", "${param['TXNDATE']}");
txnDate.setAttribute("type", "hidden");
form.appendChild(txnDate);

const txnCurrency = document.createElement("INPUT");
txnCurrency.setAttribute("name", "TXNCRNCY");
txnCurrency.setAttribute("value", "${param['TXNCRNCY']}");
txnCurrency.setAttribute("type", "hidden");
form.appendChild(txnCurrency);

const txnAmount = document.createElement("INPUT");
txnAmount.setAttribute("name", "TXNAMT");
txnAmount.setAttribute("value", "${param['TXNAMT']}");
txnAmount.setAttribute("type", "hidden");
form.appendChild(txnAmount);

const referenceId = document.createElement("INPUT");
referenceId.setAttribute("name", "REFERENCEID");
referenceId.setAttribute("value", "${param['REFERENCEID']}");
referenceId.setAttribute("type", "hidden");
form.appendChild(referenceId);

const remarks = document.createElement("INPUT");
remarks.setAttribute("name", "REMARKS");
remarks.setAttribute("value", "${param['REMARKS']}");
remarks.setAttribute("type", "hidden");
form.appendChild(remarks);

const particulars = document.createElement("INPUT");
particulars.setAttribute("name", "PARTICULARS");
particulars.setAttribute("value", "${param['PARTICULARS']}");
particulars.setAttribute("type", "hidden");
form.appendChild(particulars);

const token = document.createElement("INPUT");
token.setAttribute("name", "TOKEN");
token.setAttribute("value", "${param['TOKEN']}");
token.setAttribute("type", "hidden");
form.appendChild(token);

// console.log(form);

document.body.appendChild(form);
form.submit();
</script>
</body>

</html>
""";
}

Future<void> initiate(int purchaseId) async {
emit(ConnectIpsPaymentLoading());
try {
final data = await _getHtmlString();
emit(ConnectIpsPaymentLoaded(htmlString: data));
} catch (e) {
emit(const ConnectIpsPaymentFailed(message: "Failed"));
}
}
}

Note :
It is important to create a form in HTML and send the request otherwise it won’t work.

Step #4

Now,
We can use the cubit to start the payment as follows :

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import './connect_ips_payment_cubit.dart';
import 'package:webview_flutter/webview_flutter.dart';
import './show_error.dart';

class PaymentView extends StatefulWidget {
final int purchaseId;

const PaymentView({
Key? key,
required this.purchaseId,
}) : super(key: key);

@override
State<PaymentView> createState() => _PaymentViewState();
}

class _PaymentViewState extends State<PaymentView> {
late ConnectIpsPaymentCubit cubit;
late WebViewController controller;

@override
void initState() {
super.initState();
cubit = ConnectIpsPaymentCubit()..initiate(widget.purchaseId);
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0xffffffff))
..setNavigationDelegate(
NavigationDelegate(
onUrlChange: (value) {
print('---- URL Change');
print(value.url);
},
onProgress: (int progress) {
// Update loading bar.
},
onPageStarted: (String url) {
print('------ URL');
print(url);
},
onPageFinished: (String url) {},
onWebResourceError: (WebResourceError error) {},
onNavigationRequest: (NavigationRequest request) {
return NavigationDecision.navigate;
},
),
);
}

void initWebView(String content) {
final uri = Uri.dataFromString(
content,
mimeType: 'text/html',
encoding: Encoding.getByName('utf-8'),
);

controller.loadRequest(uri);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Payment'),
),
body: BlocConsumer<ConnectIpsPaymentCubit, ConnectIpsPaymentState>(
bloc: cubit,
listener: (context, state) {
if (state is ConnectIpsPaymentFailed) {
showErrorDialog(context);
return;
}
if (state is ConnectIpsPaymentLoaded) {
initWebView(state.htmlString);
}
},
builder: (context, state) {
if (state is ConnectIpsPaymentLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return WebViewWidget(
controller: controller,
);
},
),
);
}
}

When the params are loaded then the app will successfully show the login page for Connect IPS to furthur continue the payment.

--

--