Implementing Rave from Flutterwave with Flutter.

Adeogo Oladipo
14 min readNov 17, 2019

--

Flutterwave is an application program interface that lets you process credit card, and local alternative payment, like mobile money and ACH across Africa.

In the following tutorial, I’ll be showing you how to build an app that uses Rave by Flutterwave to receive payments from users through MTN Mobile Money in Uganda. In the next tutorial, I’ll be showing you how to receive payments from clients in Nigeria using Credit Cards.

Setup

We’ll start off by creating a brand new Flutter project

flutter create flutter_rave

Add the following dependencies in the pubspec.yaml file.

http: ^0.12.0+2
tripledes: ^2.1.0
device_id: ^0.2.0
flutter_spinkit: ^3.1.0

The http library is for making http requests, tripledes is for making Triple DES encryption when sending sensitive data over the internet, device_id to get the unique device id for our device and flutter_spinkit to show loading dialogs.

It will finally look like this:

dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
http: ^0.12.0+2
tripledes: ^2.1.0
device_id: ^0.2.0

and finally install all of our dependencies

flutter packages get

1. Receiving Payments through MTN Mobile Money

User Interface

Open the lib/main.dart file and replace the content with:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
final _phoneController = TextEditingController();
String _phoneErrorText;

@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
backgroundColor: Colors.blue,
centerTitle: true,
title: Text(
'Flutter Rave',
style: TextStyle(
fontSize: 24.0,
),
),
),
body: Column(
children: <Widget>[
Container(
margin: EdgeInsets.all(16.0),
padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: TextField(
keyboardType: TextInputType.phone,
controller: _phoneController,
decoration: InputDecoration(
errorText: _phoneErrorText,
hintText: 'Mobile Wallet Number',
hintStyle: TextStyle(
fontSize: 18.0,
color: Colors.grey.shade500,
),
filled: true,
fillColor: Colors.grey.shade200,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
borderSide: BorderSide.none,
)),
onSubmitted: (value) {
print(value);
},
),
),
GestureDetector(
onTap: _processMtnMM,
child: Container(
child: Text(
'PAY',
style: TextStyle(
color: Colors.white,
fontSize: 18.0,
fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
margin: EdgeInsets.only(
left: 32.0, right: 32.0, top: 8.0, bottom: 16.0),
padding: EdgeInsets.symmetric(horizontal: 36.0, vertical: 16.0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8.0),
),
),
),
],
),
);
}

void _processMtnMM() {}
}

Here, we’ll be using a TextField element with a controller called _phoneController. We’ll be able to fetch the TextField’s value through the controller. We also have a String _phoneErrorText, through which we’ll be notifying our users of errors in their input. Secondly, we have a GestureDetector element, that calls the _processMtnMM method when tapped.

You should have a UI that is similar to this:

Code:

First you need to have signed up on https://rave.flutterwave.com so as to get your Public and Encryption keys. They are located in the Settings Page under the API tab.

  1. Create a file called constants.dart, which will contain all our constant values. Put the following lines of code in it:
const String CHARGE_ENDPOINT =
"https://api.ravepay.co/flwv3-pug/getpaidx/api/charge";
const String VALIDATE_CHARGE_ENDPOINT =
"https://api.ravepay.co/flwv3-pug/getpaidx/api/validatecharge";
const String REQUERY_ENDPOINT =
"https://api.ravepay.co/flwv3-pug/getpaidx/api/verify/mpesa";

const String PUBLIC_KEY = 'FLWPUBK-******';
const String ENCRYPTION_KEY = '********';

const currency = 'UGX';
const paymentType = 'mobilemoneyuganda';
const receivingCountry = 'NG';
const network = 'UGX';
const WEB_HOOK_3DS = 'https://rave-webhook.herokuapp.com/receivepayment';
const MAX_REQUERY_COUNT = 30;

Note, Replace FLWPUBK-****** with the Public Key from the Flutterwave dashboard, and replace ******** with the Encryption Key from the Flutterwave dashboard.

2. Create a file called charge_response.dart, which will be the object that we’ll use to handle our charge request’s response. Put the following lines of code in it:

class ChargeResponse {
String status;
String message;
Data data;

ChargeResponse.fromJson(Map<String, dynamic> json, bool isFirstQuery) {
if (json == null) {
return;
}
status = json['status'];
message = json['message'];
data = Data.fromJson(json['data']);
}

@override
String toString() {
return 'status: $status, message: $message, date: $data';
}
}

class Data {
String suggested_auth;
String chargeResponseCode;
String authModelUsed;
String flwRef;
String txRef;
String chargeResponseMessage;
String authurl;
String appFee;
String currency;
String charged_amount;
String validateInstruction;
String redirectUrl;
String validateInstructions;
String amount;
String status;

//For timeout
String ping_url;
int wait;

Data.fromJson(Map<String, dynamic> json) {
if (json == null) {
return;
}
if (json['response'] != null) {
json = json['response'].cast<Map<String, dynamic>>();
}
txRef = json['txRef'];
flwRef = json['flwRef'];
redirectUrl = json['redirectUrl'];
amount = json['amount'].toString();
charged_amount = json['charged_amount'].toString();
appFee = json['appfee'].toString();
chargeResponseCode = json['chargeResponseCode'];
chargeResponseMessage = json['chargeResponseMessage'];
authModelUsed = json['authModelUsed'];
currency = json['currency'];
suggested_auth = json['suggested_auth'];
validateInstruction = json['validateInstruction'];
validateInstructions = json['validateInstructions'];
ping_url = json['ping_url'];
wait = json['wait'];
status = json['status'];
authurl = json['authurl'];
}

@override
String toString() {
return 'Data{suggested_auth: $suggested_auth, chargeResponseCode: $chargeResponseCode, authModelUsed: $authModelUsed, flwRef: $flwRef, txRef: $txRef, chargeResponseMessage: $chargeResponseMessage, authurl: $authurl, appFee: $appFee, currency: $currency, charged_amount: $charged_amount, validateInstruction: $validateInstruction, redirectUrl: $redirectUrl, validateInstructions: $validateInstructions, amount: $amount, status: $status, ping_url: $ping_url, wait: $wait}';
}

}

The class, ChargeResponse has 3 field variables, status of type String, message of type String and data of type Data.

A new instance is created by the ChargeResponse.fromJson method. ChargeResponse.fromJson takes 2 input parameters, the json response from Flutterwave servers and a flag variable called isFirstQuery. The parameter isFirstQuery is user to differentiate the first charge call from the rest as they require return different responses.

ChargeResponse.fromJson(Map<String, dynamic> json, bool isFirstQuery) {
if (json == null) {
return;
}
status = json['status'];
message = json['message'];
data = Data.fromJson(json['data']);
}

The Data class in the file holds the bulk of the data from the response.

class Data {
String suggested_auth;
String chargeResponseCode;
String authModelUsed;
String flwRef;
String txRef;
String chargeResponseMessage;
String authurl;
String appFee;
String currency;
String charged_amount;
String validateInstruction;
String redirectUrl;
String validateInstructions;
String amount;
String status;

//For timeout
String ping_url;
int wait;

Data.fromJson(Map<String, dynamic> json) {
if (json == null) {
return;
}
if (json['response'] != null) {
json = json['response'].cast<Map<String, dynamic>>();
}
txRef = json['txRef'];
flwRef = json['flwRef'];
redirectUrl = json['redirectUrl'];
amount = json['amount'].toString();
charged_amount = json['charged_amount'].toString();
appFee = json['appfee'].toString();
chargeResponseCode = json['chargeResponseCode'];
chargeResponseMessage = json['chargeResponseMessage'];
authModelUsed = json['authModelUsed'];
currency = json['currency'];
suggested_auth = json['suggested_auth'];
validateInstruction = json['validateInstruction'];
validateInstructions = json['validateInstructions'];
ping_url = json['ping_url'];
wait = json['wait'];
status = json['status'];
authurl = json['authurl'];
}

@override
String toString() {
return 'Data{suggested_auth: $suggested_auth, chargeResponseCode: $chargeResponseCode, authModelUsed: $authModelUsed, flwRef: $flwRef, txRef: $txRef, chargeResponseMessage: $chargeResponseMessage, authurl: $authurl, appFee: $appFee, currency: $currency, charged_amount: $charged_amount, validateInstruction: $validateInstruction, redirectUrl: $redirectUrl, validateInstructions: $validateInstructions, amount: $amount, status: $status, ping_url: $ping_url, wait: $wait}';
}

}

3. Create a file called requery_response.dart, which will be the object that we’ll use to handle our requery request’s response. Put the following lines of code in it:

class RequeryResponse {
String status;
Data data;

RequeryResponse.fromJson(Map<String, dynamic> json) {
if (json == null) {
return;
}
status = json['status'];
data = Data.fromJson(json['data']);
}

@override
String toString() {
return 'status:$status, data:$data';
}
}

class Data {
String chargeResponseCode;
String status;
String flwRef;
int amount;
String currency;

Data.fromJson(Map<String, dynamic> json) {
if (json == null) {
return;
}
status = json['status'];
chargeResponseCode = json['chargeResponseCode'];
flwRef = json['flwref'];
amount = json['amount'];
currency = json['currency'];

if (chargeResponseCode == null) {
chargeResponseCode = json['chargecode'];
}
}

@override
String toString() {
return 'Data{chargeResponseCode: $chargeResponseCode, status: $status, flwRef: $flwRef, amount: $amount, currency: $currency}';
}
}

The class, RequeryResponse has 2 field variables, status of type String and data of type Data.

A new instance is created by the RequeryResponse.fromJson method. RequeryResponse.fromJson takes 1 input parameter, the json response from Flutterwave servers.

RequeryResponse.fromJson(Map<String, dynamic> json) {
if (json == null) {
return;
}
status = json['status'];
data = Data.fromJson(json['data']);
}

The Data class in the file holds the bulk of the data from the response.

class Data {
String chargeResponseCode;
String status;
String flwRef;
int amount;
String currency;

Data.fromJson(Map<String, dynamic> json) {
if (json == null) {
return;
}
status = json['status'];
chargeResponseCode = json['chargeResponseCode'];
flwRef = json['flwref'];
amount = json['amount'];
currency = json['currency'];

if (chargeResponseCode == null) {
chargeResponseCode = json['chargecode'];
}
}

@override
String toString() {
return 'Data{chargeResponseCode: $chargeResponseCode, status: $status, flwRef: $flwRef, amount: $amount, currency: $currency}';
}
}

4. Create a file called mobile_money_payload.dart, which will be the object that we’ll use to handle our Request Body when we make the charge call to the Flutterwave server. Put the following lines of code in it:

import 'dart:convert';
import 'package:tripledes/tripledes.dart';

class MobileMoneyPayload {
String PBFPubKey,
currency,
payment_type,
country,
amount,
email,
phonenumber,
network,
firstname,
lastname,
txRef,
orderRef,
is_mobile_money_ug,
device_fingerprint,
redirect_url;

MobileMoneyPayload(
{this.PBFPubKey,
this.currency,
this.payment_type,
this.country,
this.amount,
this.email,
this.phonenumber,
this.network,
this.firstname,
this.lastname,
this.txRef,
this.orderRef,
this.is_mobile_money_ug,
this.device_fingerprint,
this.redirect_url});

Map<String, dynamic> toJson() => {
'PBFPubKey': PBFPubKey,
'currency': currency,
'payment_type': payment_type,
'country': country,
'amount': amount,
'email': email,
'phonenumber': phonenumber,
'network': network,
'firstname': firstname,
'lastname': lastname,
'txRef': txRef,
'orderRef': orderRef,
'is_mobile_money_ug': is_mobile_money_ug,
'device_fingerprint': device_fingerprint,
'redirect_url': redirect_url,
};

Map<String, String> encryptJsonPayload(
String encryptionKey, String publicKey) {
String encoded = jsonEncode(this);
String encrypted = getEncryptedData(encoded, encryptionKey);

final encryptedPayload = {
"PBFPubKey": publicKey,
"client": encrypted,
"alg": "3DES-24"
};

return encryptedPayload;
}

String getEncryptedData(encoded, encryptionKey) {
return encrypt(encryptionKey, encoded);
}

String encrypt(key, text) {
var blockCipher = BlockCipher(TripleDESEngine(), key);
var i = blockCipher.encodeB64(text);
return i;
}
}

This class contains all the field variables needed to make a valid mobile money charge request.

Note, the variable names are exactly the same as listed in the documentation.

The method encryptJsonPayload does the json encoding and the 3-DES encryption of the encoded payload. We need to encrypt the payload as we can’t just send plain json for security reasons.

5. Create a file called networking.dart, which will handle all our networking needs. Put the following lines of code in it:

import 'dart:convert';

import 'package:http/http.dart' as http;

Future getResponseFromEndpoint(String url) async {
try {
var response = await http.get(url);

if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
print('Error: ${response.body}');
return null;
}
} catch (e) {
print('Exception $e');
return null;
}
}

Future postToEndpointWithBody(String url, Map<String, String> body) async {
try {
var response = await http.post(url, body: body);

if (response.statusCode == 200 ||
response.statusCode == 201 ||
response.statusCode == 204 ||
response.statusCode == 206) {
return jsonDecode(response.body);
} else {
print('Error: ${response.statusCode} response : ${response.body}');
return null;
}
} catch (e) {
print('Exception $e');
return null;
}
}

Having created the needed files, we proceed to finish the main.dart file.

6. We will replace the contents of main.dart file with this:

Deep dive into each section of the code comes after

import 'dart:async';

import 'package:device_id/device_id.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rave/charge_response.dart';
import 'package:flutter_rave/constants.dart';
import 'package:flutter_rave/mobile_money_payload.dart';
import 'package:flutter_rave/networking.dart';
import 'package:flutter_rave/requery_response.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
final _phoneController = TextEditingController();
String _phoneErrorText;
var firstName = 'user',
lastName = 'Using',
amount = 500,
email = 'user@gmail.com',
_userDismissedDialog = false,
_requeryUrl,
_queryCount = 0,
_reQueryTxCount = 0,
_waitDuration = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
backgroundColor: Colors.blue,
centerTitle: true,
title: Text(
'Flutter Rave',
style: TextStyle(
fontSize: 24.0,
),
),
),
body: Column(
children: <Widget>[
Container(
margin: EdgeInsets.all(16.0),
padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: TextField(
keyboardType: TextInputType.phone,
controller: _phoneController,
decoration: InputDecoration(
errorText: _phoneErrorText,
hintText: 'Mobile Wallet Number',
hintStyle: TextStyle(
fontSize: 18.0,
color: Colors.grey.shade500,
),
filled: true,
fillColor: Colors.grey.shade200,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
borderSide: BorderSide.none,
),
),
),
),
GestureDetector(
onTap: _processMtnMM,
child: Container(
child: Text(
'PAY',
style: TextStyle(
color: Colors.white,
fontSize: 18.0,
fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
margin: EdgeInsets.only(
left: 32.0, right: 32.0, top: 8.0, bottom: 16.0),
padding: EdgeInsets.symmetric(horizontal: 36.0, vertical: 16.0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8.0),
),
),
),
],
),
);
}

void _processMtnMM() {
_phoneErrorText = '';
if (_validatePhoneNumber(_phoneController.text) != null) {
setState(() {
_phoneErrorText = _validatePhoneNumber(_phoneController.text);
});
return;
}

var phone = _addCountryCodeSuffixToNumber('+256', _phoneController.text);
_showMobileMoneyProcessingDialog();
_initiateMobileMoneyPaymentFlow(phone);
}

void _initiateMobileMoneyPaymentFlow(String phone) async {
_userDismissedDialog = false;
String deviceId = await DeviceId.getID;

MobileMoneyPayload payload = MobileMoneyPayload(
PBFPubKey: PUBLIC_KEY,
currency: currency,
payment_type: paymentType,
country: receivingCountry,
amount: '$amount',
email: email,
phonenumber: phone,
network: network,
firstname: firstName,
lastname: lastName,
txRef: "MC-" + DateTime.now().toString(),
orderRef: "MC-" + DateTime.now().toString(),
is_mobile_money_ug: '1',
device_fingerprint: deviceId,
redirect_url: WEB_HOOK_3DS);

var requestBody = payload.encryptJsonPayload(ENCRYPTION_KEY, PUBLIC_KEY);

var response = await postToEndpointWithBody(
'$CHARGE_ENDPOINT?use_polling=1', requestBody);

if (response == null) {
_showToast(context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else {
_continueProcessingAfterCharge(response, true);
}
}

_showToast(BuildContext context, String textInput, {Color backgroundColor}) {
if (mounted) {
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(textInput),
duration: Duration(milliseconds: 1000),
backgroundColor: backgroundColor ?? Colors.black87,
));
}
}

Future<void> _showMobileMoneyProcessingDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Mobile Money'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'A push notification is being sent to your phone, please complete the transaction by entering your pin.'),
SizedBox(
height: 8.0,
),
SpinKitThreeBounce(
color: Colors.grey.shade900,
size: 20.0,
),
],
),
),
actions: <Widget>[
FlatButton(
child: Text(
'CANCEL',
style: TextStyle(fontSize: 20.0),
),
onPressed: () {
_dismissMobileMoneyDialog(true);
},
),
],
);
},
);
}

void _dismissMobileMoneyDialog(bool dismissedByUser) {
_userDismissedDialog = dismissedByUser;
if (mounted) {
Navigator.of(context, rootNavigator: true).pop('dialog');
}
}

String _validatePhoneNumber(String phone) {
String pattern = r'(^[0+](?:[0-9] ?){6,14}[0-9]$)';
RegExp regExp = RegExp(pattern);
if (phone.length == 0) {
return "Enter mobile number";
} else if (!regExp.hasMatch(phone.trim())) {
return "Enter valid mobile number";
}
return null;
}

String _addCountryCodeSuffixToNumber(String countryCode, String phoneNumber) {
if (phoneNumber[0] == '0') {
return countryCode + phoneNumber.substring(1);
}
return phoneNumber;
}

void _continueProcessingAfterCharge(
Map<String, dynamic> response, bool firstQuery) async {
var chargeResponse = ChargeResponse.fromJson(response, firstQuery);

if (chargeResponse.data != null && chargeResponse.data.flwRef != null) {
_requeryTx(chargeResponse.data.flwRef);
} else {
if (chargeResponse.status == 'success' &&
chargeResponse.data.ping_url != null) {
_waitDuration = chargeResponse.data.wait;
_requeryUrl = chargeResponse.data.ping_url;
Timer(Duration(milliseconds: chargeResponse.data.wait), () {
_chargeAgainAfterDuration(chargeResponse.data.ping_url);
});
} else if (chargeResponse.status == 'success' &&
chargeResponse.data.status == 'pending') {
Timer(Duration(milliseconds: _waitDuration), () {
_chargeAgainAfterDuration(_requeryUrl);
});
} else if (chargeResponse.status == 'success' &&
chargeResponse.data.status == 'completed' &&
chargeResponse.data.flwRef != null) {
_requeryTx(chargeResponse.data.flwRef);
} else {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
}
}
}

void _requeryTx(String flwRef) async {
if (!_userDismissedDialog && _reQueryTxCount < MAX_REQUERY_COUNT) {
_reQueryTxCount++;
final requeryRequestBody = {"PBFPubKey": PUBLIC_KEY, "flw_ref": flwRef};

var response =
await postToEndpointWithBody(REQUERY_ENDPOINT, requeryRequestBody);

if (response == null) {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else {
var requeryResponse = RequeryResponse.fromJson(response);

if (requeryResponse.data == null) {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else if (requeryResponse.data.chargeResponseCode == '02' &&
requeryResponse.data.status != 'failed') {
_onPollingComplete(flwRef);
} else if (requeryResponse.data.chargeResponseCode == '00') {
_onPaymentSuccessful();
} else {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
}
}
} else if (_reQueryTxCount == MAX_REQUERY_COUNT) {
_showToast(
context, 'Payment processing timeout. Please try again later.');
_dismissMobileMoneyDialog(false);
}
}

void _chargeAgainAfterDuration(String url) async {
if (!_userDismissedDialog) {
_queryCount++;
print('Charging Again after $_queryCount Charge calls');
var response = await getResponseFromEndpoint(url);

if (response == null) {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else {
_continueProcessingAfterCharge(response, false);
}
}
}

void _onPollingComplete(String flwRef) {
Timer(Duration(milliseconds: 5000), () {
_requeryTx(flwRef);
});
}

void _onPaymentSuccessful() async {
_showPaymentSuccessfulDialog();
}

void _showPaymentSuccessfulDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return SimpleDialog(
children: <Widget>[
Container(
padding: EdgeInsets.all(16.0),
child: Icon(
Icons.done,
color: Colors.blue,
size: MediaQuery.of(context).size.width / 6,
),
),
Text(
'Payment completed!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24.0),
),
SizedBox(
height: 12.0,
),
Text(
'You have successfully completed your payment!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18.0),
),
SizedBox(
height: 12.0,
),
GestureDetector(
onTap: () {
//Proceed to the next action after successful payment
Navigator.pop(context);
},
child: Container(
margin: EdgeInsets.only(
left: 32.0, right: 32.0, top: 8.0, bottom: 16.0),
padding:
EdgeInsets.symmetric(horizontal: 36.0, vertical: 16.0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12.0),
),
child: Text(
'Proceed',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20.0, color: Colors.white),
),
),
)
],
);
});
}
}

The changes in the new content are:

7. We added some field variables to the _MyHomePageState class.

var firstName = 'user',
lastName = 'Using',
amount = 5000,
email = 'user@gmail.com',
_userDismissedDialog = false,
_requeryUrl,
_queryCount = 0,
_reQueryTxCount = 0,
_waitDuration = 0;

8. We completed our _processMtnMM function. It became:

void _processMtnMM() {
_phoneErrorText = '';
if (_validatePhoneNumber(_phoneController.text) != null) {
setState(() {
_phoneErrorText = _validatePhoneNumber(_phoneController.text);
});
return;
}

var phone = _addCountryCodeSuffixToNumber('+256', _phoneController.text);
_showMobileMoneyProcessingDialog();
_initiateMobileMoneyPaymentFlow(phone);
}

Here, we check that the entered text is a valid phone number by calling the _validatePhoneNumber method and we notify the user by assign the value returned to the variable _phoneErrorText.

String _validatePhoneNumber(String phone) {
String pattern = r'(^[0+](?:[0-9] ?){6,14}[0-9]$)';
RegExp regExp = RegExp(pattern);
if (phone.length == 0) {
return "Enter mobile number";
} else if (!regExp.hasMatch(phone.trim())) {
return "Enter valid mobile number";
}
return null;
}

This method uses the text length and Regular Expression to check if it is a valid phone number. If the method returns null, then the number is valid.

Once we are sure it is a valid phone number, we add the country code to it, using the _addCountryCodeSuffixToNumber function.

String _addCountryCodeSuffixToNumber(String countryCode, String phoneNumber) {
if (phoneNumber[0] == '0') {
return countryCode + phoneNumber.substring(1);
}
return phoneNumber;
}

Then we call the _showMobileMoneyProcessingDialog method to show the loading interface that shows our user that their request is being processed.

Future<void> _showMobileMoneyProcessingDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Mobile Money'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(
'A push notification is being sent to your phone, please complete the transaction by entering your pin.'),
SizedBox(
height: 8.0,
),
SpinKitThreeBounce(
color: Colors.grey.shade900,
size: 20.0,
),
],
),
),
actions: <Widget>[
FlatButton(
child: Text(
'CANCEL',
style: TextStyle(fontSize: 20.0),
),
onPressed: () {
_dismissMobileMoneyDialog(true);
},
),
],
);
},
);
}

Since Mobile Money requires that the user enters their pin, we inform them that that a push Notification has been sent to their phone. This dialog remains until the transaction is declared successful or failed by the Flutterwave server. If the ‘CANCEL’ button is pressed, we dismiss the dialog by calling the _dismissMobileMoneyDialog method and we also set the variable _userDismissedDialog to true. That way, we can stop making calls to the server once the user has dismissed the Dialog.

After we show the loading dialog to our user, we launch the payment flow my calling the _initiateMobileMoneyPaymentFlow method.

void _initiateMobileMoneyPaymentFlow(String phone) async {
_userDismissedDialog = false;
String deviceId = await DeviceId.getID;

MobileMoneyPayload payload = MobileMoneyPayload(
PBFPubKey: PUBLIC_KEY,
currency: currency,
payment_type: paymentType,
country: receivingCountry,
amount: '$amount',
email: email,
phonenumber: phone,
network: network,
firstname: firstName,
lastname: lastName,
txRef: "MC-" + DateTime.now().toString(),
orderRef: "MC-" + DateTime.now().toString(),
is_mobile_money_ug: '1',
device_fingerprint: deviceId,
redirect_url: WEB_HOOK_3DS);

var requestBody = payload.encryptJsonPayload(ENCRYPTION_KEY, PUBLIC_KEY);

var response = await postToEndpointWithBody(
'$CHARGE_ENDPOINT?use_polling=1', requestBody);

if (response == null) {
_showToast(context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else {
_continueProcessingAfterCharge(response, true);
}
}

This method creates the MobileMoneyPayload object and encrypts the payload. Then we call the Flutterwave charge endpoint with the http post method and pass the encrypted payload as the body of the post request.

Once we get the response from the request, we check if it is null and subsequently notify the user that the processing failed and we dismiss the dialog by calling the _dismissMobileMoneyDialog method. But we call the _continueProcessingAfterCharge method to process the response if it is not null.

void _continueProcessingAfterCharge(
Map<String, dynamic> response, bool firstQuery) async {
var chargeResponse = ChargeResponse.fromJson(response, firstQuery);

if (chargeResponse.data != null && chargeResponse.data.flwRef != null) {
_requeryTx(chargeResponse.data.flwRef);
} else {
if (chargeResponse.status == 'success' &&
chargeResponse.data.ping_url != null) {
_waitDuration = chargeResponse.data.wait;
_requeryUrl = chargeResponse.data.ping_url;
Timer(Duration(milliseconds: chargeResponse.data.wait), () {
_chargeAgainAfterDuration(chargeResponse.data.ping_url);
});
} else if (chargeResponse.status == 'success' &&
chargeResponse.data.status == 'pending') {
Timer(Duration(milliseconds: _waitDuration), () {
_chargeAgainAfterDuration(_requeryUrl);
});
} else if (chargeResponse.status == 'success' &&
chargeResponse.data.status == 'completed' &&
chargeResponse.data.flwRef != null) {
_requeryTx(chargeResponse.data.flwRef);
} else {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
}
}
}

First, we parse the response and convert it into our ChargeResponse object using the ChargeResponse.fromJson method. Then we make some checks to know what to do with the response.

void _requeryTx(String flwRef) async {
if (!_userDismissedDialog && _reQueryTxCount < MAX_REQUERY_COUNT) {
_reQueryTxCount++;
final requeryRequestBody = {"PBFPubKey": PUBLIC_KEY, "flw_ref": flwRef};

var response =
await postToEndpointWithBody(REQUERY_ENDPOINT, requeryRequestBody);

if (response == null) {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else {
var requeryResponse = RequeryResponse.fromJson(response);

if (requeryResponse.data == null) {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else if (requeryResponse.data.chargeResponseCode == '02' &&
requeryResponse.data.status != 'failed') {
_onPollingComplete(flwRef);
} else if (requeryResponse.data.chargeResponseCode == '00') {
_onPaymentSuccessful();
} else {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
}
}
} else if (_reQueryTxCount == MAX_REQUERY_COUNT) {
_showToast(
context, 'Payment processing timeout. Please try again later.');
_dismissMobileMoneyDialog(false);
}
}

This is called to check on the status of the payment. As we are unable to use web hooks or some sort of web callback to know when our transaction is completed, we resort to making consecutive http calls to the Flutterwave server to check on the status of the payment. We also limit the number of calls with the const variable MAX_REQUERY_COUNT found in constants.dart file.

void _chargeAgainAfterDuration(String url) async {
if (!_userDismissedDialog) {
_queryCount++;
print('Charging Again after $_queryCount Charge calls');
var response = await getResponseFromEndpoint(url);

if (response == null) {
_showToast(
context, 'Payment processing failed. Please try again later.');
_dismissMobileMoneyDialog(false);
} else {
_continueProcessingAfterCharge(response, false);
}
}
}

The dynamics of the payment processing, the different responses, the different endpoints and also the different payloads(requestBody) can be found in the Flutterwave documentation.

The full project can be found on github.

--

--

Adeogo Oladipo
Adeogo Oladipo

Written by Adeogo Oladipo

Co-Founder and CTO @DokitariUG. A Strong believer in the Potential in Each Human.

Responses (2)