From e9653f814f1705a1f0424fad81ace79101b095ac Mon Sep 17 00:00:00 2001 From: Wilberforce Uwadiegwu Date: Tue, 22 Oct 2019 23:26:46 +0100 Subject: [PATCH] Implemented direct account debit --- lib/src/common/strings.dart | 30 +++---- lib/src/common/validator_utills.dart | 6 ++ lib/src/dto/charge_request_body.dart | 8 +- lib/src/dto/payload.dart | 12 ++- .../manager/account_transaction_manager.dart | 25 +++++- lib/src/manager/base_transaction_manager.dart | 52 ++++++++++++ lib/src/manager/card_transaction_manager.dart | 61 ++------------ lib/src/models/charge_model.dart | 3 + .../payment/pages/account_payment_widget.dart | 84 +++++++++---------- 9 files changed, 158 insertions(+), 123 deletions(-) diff --git a/lib/src/common/strings.dart b/lib/src/common/strings.dart index 95f3501..57deb31 100644 --- a/lib/src/common/strings.dart +++ b/lib/src/common/strings.dart @@ -10,21 +10,21 @@ class Strings { static const ugandaMobileMoney = 'Uganda Mobile Money'; static const pay = 'Pay'; static const invalidCVV = 'Enter a valid cvv'; - static String invalidExpiry = 'Enter a valid expiry date'; - static var invalidCardNumber = 'Enter a valid card number'; - static var invalidPhoneNumber = 'Enter a valid phone number'; - static var invalidAccountNumber = 'Enter a valid account number'; - static var invalidAmount = 'Enter a valid amount'; - static var invalidEmail = 'Enter a valid email'; - static var invalidBVN = 'Enter a valid BVN'; - static var invalidVoucher = 'Enter a valid voucher code'; - static var invalidDOB = 'Enter a valid date of birth'; - static var demo = 'Demo'; - static var youCancelled = 'You cancelled'; - static var sthWentWrong = 'Something went wrong'; - static var noResponseData = 'No response data was returned'; - static var unknownAuthModel = 'Unknown auth model'; - static var enterOtp = 'Enter your one time password (OTP)'; + static const invalidExpiry = 'Enter a valid expiry date'; + static const invalidCardNumber = 'Enter a valid card number'; + static const invalidPhoneNumber = 'Enter a valid phone number'; + static const invalidAccountNumber = 'Enter a valid account number'; + static const invalidAmount = 'Enter a valid amount'; + static const invalidEmail = 'Enter a valid email'; + static const invalidBVN = 'Enter a valid BVN'; + static const invalidVoucher = 'Enter a valid voucher code'; + static const invalidDOB = 'Enter a valid date of birth'; + static const demo = 'Demo'; + static const youCancelled = 'You cancelled'; + static const sthWentWrong = 'Something went wrong'; + static const noResponseData = 'No response data was returned'; + static const unknownAuthModel = 'Unknown auth model'; + static const enterOtp = 'Enter your one time password (OTP)'; static cannotBeNull(String name) => '$name cannot be null'; diff --git a/lib/src/common/validator_utills.dart b/lib/src/common/validator_utills.dart index 5ab66a0..4cba784 100644 --- a/lib/src/common/validator_utills.dart +++ b/lib/src/common/validator_utills.dart @@ -67,6 +67,12 @@ class ValidatorUtils { return true; } + static bool isUrlValid(String url) { + final source = + r'^(https?|ftp|file|http)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]'; + return RegExp(source).hasMatch(url); + } + /// Checks if the card has expired. /// Returns true if the card has expired; false otherwise static bool validExpiryDate(int expiryMonth, int expiryYear) { diff --git a/lib/src/dto/charge_request_body.dart b/lib/src/dto/charge_request_body.dart index 41688ae..c9144dc 100644 --- a/lib/src/dto/charge_request_body.dart +++ b/lib/src/dto/charge_request_body.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; import 'package:rave_flutter/src/common/rave_utils.dart'; import 'package:rave_flutter/src/dto/payload.dart'; import 'package:rave_flutter/src/repository/repository.dart'; @@ -16,10 +17,11 @@ class ChargeRequestBody extends Equatable { this.alg, }); - ChargeRequestBody.fromPayload(Payload p) - : this.pBFPubKey = p.pbfPubKey, + ChargeRequestBody.fromPayload({@required Payload payload, String type}) + : this.pBFPubKey = payload.pbfPubKey, this.alg = "3DES-24", - this.client = RaveUtils.getEncryptedData(json.encode(p.toJson()), + this.client = RaveUtils.getEncryptedData( + json.encode(payload.toJson(type)), Repository.instance.initializer.encryptionKey); Map toJson() => { diff --git a/lib/src/dto/payload.dart b/lib/src/dto/payload.dart index 123ad46..e00aa32 100644 --- a/lib/src/dto/payload.dart +++ b/lib/src/dto/payload.dart @@ -41,6 +41,7 @@ class Payload { String billingState; String billingCountry; String redirectUrl; + String paymentType; Payload.initFrmInitializer(RavePayInitializer i) : this.amount = i.amount.toString(), @@ -81,10 +82,9 @@ class Payload { this.txRef, this.cardBIN}); - Map toJson() { + Map toJson(String paymentType) { var json = { "narration": narration, - "expirymonth": expiryMonth, "PBFPubKey": pbfPubKey, "lastname": lastName, "firstname": firstName, @@ -92,14 +92,16 @@ class Payload { "country": country, "amount": amount, "email": email, - "expiryyear": expiryYear, "txRef": txRef, "redirect_url": redirectUrl, }; + putIfNotNull(map: json, key: "payment_type", value: paymentType); + putIfNotNull(map: json, key: "expirymonth", value: expiryMonth); + putIfNotNull(map: json, key: "expiryyear", value: expiryYear); putIfNotNull(map: json, key: "cvv", value: cvv); putIfNotNull(map: json, key: "cardno", value: cardNo); - putIfNotNull(map: json, key: "accountbank", value: bank.code); + putIfNotNull(map: json, key: "accountbank", value: bank?.code); putIfNotNull(map: json, key: "bvn", value: bvn); putIfNotNull(map: json, key: "accountnumber", value: accountNumber); putIfNotNull(map: json, key: "passcode", value: passCode); @@ -134,6 +136,8 @@ class Payload { ? null : subAccounts.map((a) => a.toJson()).toList()); + print("Json = $json"); + return json; } } diff --git a/lib/src/manager/account_transaction_manager.dart b/lib/src/manager/account_transaction_manager.dart index cb989f6..452e09f 100644 --- a/lib/src/manager/account_transaction_manager.dart +++ b/lib/src/manager/account_transaction_manager.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart' hide ConnectionState; import 'package:rave_flutter/src/blocs/connection_bloc.dart'; +import 'package:rave_flutter/src/common/validator_utills.dart'; import 'package:rave_flutter/src/dto/charge_request_body.dart'; import 'package:rave_flutter/src/exception/exception.dart'; import 'package:rave_flutter/src/manager/base_transaction_manager.dart'; @@ -14,10 +15,28 @@ class AccountTransactionManager extends BaseTransactionManager { charge() async { setConnectionState(ConnectionState.waiting); try { - var response = - await service.charge(ChargeRequestBody.fromPayload(payload)); - + var response = await service.charge( + ChargeRequestBody.fromPayload(payload: payload, type: "account"), + ); setConnectionState(ConnectionState.done); + + flwRef = response.flwRef; + + if (response.hasData) { + final authUrl = response.authUrl; + final authUrlIsValid = ValidatorUtils.isUrlValid(authUrl); + if (authUrlIsValid) { + showWebAuthorization(authUrl); + } else { + if (response.validateInstruction != null) { + onOtpRequested(response.validateInstruction); + } else if (response.validateInstructions != null) { + onOtpRequested(response.validateInstructions); + } else { + onOtpRequested(); + } + } + } } on RaveException catch (e) { handleError(e: e); } diff --git a/lib/src/manager/base_transaction_manager.dart b/lib/src/manager/base_transaction_manager.dart index b6a356d..c52a938 100644 --- a/lib/src/manager/base_transaction_manager.dart +++ b/lib/src/manager/base_transaction_manager.dart @@ -5,14 +5,17 @@ import 'package:flutter/material.dart' hide State, ConnectionState; import 'package:rave_flutter/src/blocs/connection_bloc.dart'; import 'package:rave_flutter/src/blocs/transaction_bloc.dart'; import 'package:rave_flutter/src/common/rave_pay_initializer.dart'; +import 'package:rave_flutter/src/common/strings.dart'; import 'package:rave_flutter/src/dto/fee_check_request_body.dart'; import 'package:rave_flutter/src/dto/payload.dart'; +import 'package:rave_flutter/src/dto/validate_charge_request_body.dart'; import 'package:rave_flutter/src/exception/exception.dart'; import 'package:rave_flutter/src/models/fee_check_model.dart'; import 'package:rave_flutter/src/models/requery_model.dart'; import 'package:rave_flutter/src/rave_result.dart'; import 'package:rave_flutter/src/repository/repository.dart'; import 'package:rave_flutter/src/services/transaction_service.dart'; +import 'package:rave_flutter/src/ui/common/webview_widget.dart'; abstract class BaseTransactionManager { final TransactionService service = TransactionService.instance; @@ -60,6 +63,55 @@ abstract class BaseTransactionManager { } } + onOtpRequested([String message = Strings.enterOtp]) { + transactionBloc.setState(TransactionState( + state: State.otp, + data: message, + callback: (otp) { + _validateCharge(otp); + })); + } + + showWebAuthorization(String authUrl) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => WebViewWidget( + authUrl: authUrl, + callbackUrl: payload.redirectUrl, + ), + ), + ); + reQueryTransaction(); + } + + _validateCharge(otp) async { + try { + setConnectionState(ConnectionState.waiting); + var response = await service.validateCardCharge(ValidateChargeRequestBody( + transactionReference: flwRef, + otp: otp, + pBFPubKey: payload.pbfPubKey)); + setConnectionState(ConnectionState.done); + + var status = response.status; + if (status == null) { + reQueryTransaction(); + return; + } + + if (status.toLowerCase() == "success") { + reQueryTransaction(); + } else { + onTransactionComplete(RaveResult( + status: RaveStatus.error, + message: response.message, + )); + } + } catch (e) { + reQueryTransaction(); + } + } + displayFeeDialog(FeeCheckResponseModel model) { closeDialog() { Navigator.of(context).pop(); diff --git a/lib/src/manager/card_transaction_manager.dart b/lib/src/manager/card_transaction_manager.dart index 4e5e045..0ba2fa3 100644 --- a/lib/src/manager/card_transaction_manager.dart +++ b/lib/src/manager/card_transaction_manager.dart @@ -1,15 +1,12 @@ import 'package:flutter/cupertino.dart' hide State, ConnectionState; import 'package:flutter/material.dart' hide State, ConnectionState; -import 'package:rave_flutter/rave_flutter.dart'; import 'package:rave_flutter/src/blocs/connection_bloc.dart'; import 'package:rave_flutter/src/blocs/transaction_bloc.dart'; import 'package:rave_flutter/src/common/rave_constants.dart'; import 'package:rave_flutter/src/common/strings.dart'; import 'package:rave_flutter/src/dto/charge_request_body.dart'; -import 'package:rave_flutter/src/dto/validate_charge_request_body.dart'; import 'package:rave_flutter/src/exception/exception.dart'; import 'package:rave_flutter/src/manager/base_transaction_manager.dart'; -import 'package:rave_flutter/src/ui/common/webview_widget.dart'; class CardTransactionManager extends BaseTransactionManager { CardTransactionManager( @@ -25,7 +22,7 @@ class CardTransactionManager extends BaseTransactionManager { setConnectionState(ConnectionState.waiting); try { var response = - await service.charge(ChargeRequestBody.fromPayload(payload)); + await service.charge(ChargeRequestBody.fromPayload(payload: payload)); setConnectionState(ConnectionState.done); @@ -54,8 +51,7 @@ class CardTransactionManager extends BaseTransactionManager { } else if (authModelUsed == RaveConstants.GTB_OTP || authModelUsed == RaveConstants.ACCESS_OTP || authModelUsed.contains("OTP")) { - _onOtpRequested( - response.chargeResponseMessage ?? Strings.enterOtp); + onOtpRequested(response.chargeResponseMessage); } else if (authModelUsed == RaveConstants.NO_AUTH) { _onNoAuthUsed(); } @@ -112,35 +108,16 @@ class CardTransactionManager extends BaseTransactionManager { _onVBVAuthModelUsed(String authUrl) => _onAVSVBVSecureCodeModelUsed(authUrl); - _onOtpRequested(String message) { - transactionBloc.setState(TransactionState( - state: State.otp, - data: message, - callback: (otp) { - _validateCharge(otp); - })); - } - _onNoAuthUsed() => reQueryTransaction(); - _onAVSVBVSecureCodeModelUsed(String authUrl) async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => WebViewWidget( - authUrl: authUrl, - callbackUrl: payload.redirectUrl, - ), - ), - ); - reQueryTransaction(); - } + _onAVSVBVSecureCodeModelUsed(String authUrl) => showWebAuthorization(authUrl); _handlePinOrBillingInput() async { setConnectionState(ConnectionState.waiting); try { var response = - await service.charge(ChargeRequestBody.fromPayload(payload)); + await service.charge(ChargeRequestBody.fromPayload(payload: payload)); setConnectionState(ConnectionState.done); flwRef = response.flwRef; @@ -153,7 +130,7 @@ class CardTransactionManager extends BaseTransactionManager { } else if (responseCode == "02") { var authModel = response.authModelUsed?.toUpperCase(); if (authModel == RaveConstants.PIN) { - _onOtpRequested(response.chargeResponseMessage); + onOtpRequested(response.chargeResponseMessage); } else if (authModel == RaveConstants.AVS_VBVSECURECODE || authModel == RaveConstants.VBV) { _onAVSVBVSecureCodeModelUsed(response.authUrl); @@ -179,32 +156,4 @@ class CardTransactionManager extends BaseTransactionManager { handleError(e: e); } } - - _validateCharge(otp) async { - try { - setConnectionState(ConnectionState.waiting); - var response = await service.validateCardCharge(ValidateChargeRequestBody( - transactionReference: flwRef, - otp: otp, - pBFPubKey: payload.pbfPubKey)); - setConnectionState(ConnectionState.done); - - var status = response.status; - if (status == null) { - reQueryTransaction(); - return; - } - - if (status.toLowerCase() == "success") { - reQueryTransaction(); - } else { - onTransactionComplete(RaveResult( - status: RaveStatus.error, - message: response.message, - )); - } - } catch (e) { - reQueryTransaction(); - } - } } diff --git a/lib/src/models/charge_model.dart b/lib/src/models/charge_model.dart index d7d792e..c84f9cd 100644 --- a/lib/src/models/charge_model.dart +++ b/lib/src/models/charge_model.dart @@ -5,6 +5,7 @@ class ChargeResponseModel extends Equatable { final String status; final String message; final String validateInstructions; + final String validateInstruction; final String suggestedAuth; final String chargeResponseCode; final String authModelUsed; @@ -36,6 +37,7 @@ class ChargeResponseModel extends Equatable { @required this.redirectUrl, @required this.hasData, @required this.rawResponse, + @required this.validateInstruction, }); factory ChargeResponseModel.fromJson(Map json) { @@ -48,6 +50,7 @@ class ChargeResponseModel extends Equatable { chargeResponseCode: data["chargeResponseCode"], authModelUsed: data["authModelUsed"], flwRef: data["flwRef"], + validateInstruction: data["validateInstruction"], validateInstructions: data.containsKey("validateInstructions") ? data["validateInstructions"]["instruction"] : null, diff --git a/lib/src/ui/payment/pages/account_payment_widget.dart b/lib/src/ui/payment/pages/account_payment_widget.dart index 51b2f44..063380f 100644 --- a/lib/src/ui/payment/pages/account_payment_widget.dart +++ b/lib/src/ui/payment/pages/account_payment_widget.dart @@ -105,7 +105,7 @@ class _AccountPaymentWidgetState : _selectedBank.showAccountNumField() ? _accountFocusNode : _selectedBank.showBVNField() ? _bvnFocusNode : null), - onSaved: (value) => payload.phoneNumber), + onSaved: (value) => payload.phoneNumber = value), _selectedBank != null && _selectedBank.showAccountNumField() ? AccountNumberField( focusNode: _accountFocusNode, @@ -114,54 +114,55 @@ class _AccountPaymentWidgetState : TextInputAction.done, onFieldSubmitted: (value) => swapFocus(_accountFocusNode, _selectedBank.showBVNField() ? _bvnFocusNode : null), - onSaved: (value) => payload.accountNumber) + onSaved: (value) => payload.accountNumber = value) : SizedBox(), _selectedBank != null && _selectedBank.showBVNField() ? BVNField( focusNode: _bvnFocusNode, textInputAction: TextInputAction.done, onFieldSubmitted: (value) => swapFocus(_bvnFocusNode), - onSaved: (value) => payload.bvn) + onSaved: (value) => payload.bvn = value) : SizedBox(), DropdownButtonHideUnderline( - child: InputDecorator( - decoration: InputDecoration( - border: OutlineInputBorder(), - isDense: true, - filled: true, - errorText: - autoValidate && _selectedBank == null ? 'Select a bank' : null, - fillColor: Colors.grey[50], - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.grey[400].withOpacity(.7), width: .5), - borderRadius: BorderRadius.all(Radius.circular(1.5))), - focusedBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.grey[400].withOpacity(.7), width: 1), - borderRadius: BorderRadius.all(Radius.circular(1.5))), - hintText: 'Select bank', + child: InputDecorator( + decoration: InputDecoration( + border: OutlineInputBorder(), + isDense: true, + filled: true, + errorText: + autoValidate && _selectedBank == null ? 'Select a bank' : null, + fillColor: Colors.grey[50], + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey[400].withOpacity(.7), width: .5), + borderRadius: BorderRadius.all(Radius.circular(1.5))), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey[400].withOpacity(.7), width: 1), + borderRadius: BorderRadius.all(Radius.circular(1.5))), + hintText: 'Select bank', + ), + isEmpty: _selectedBank == null, + child: new DropdownButton( + value: _selectedBank, + isDense: true, + onChanged: (BankModel newValue) { + setState(() => _selectedBank = newValue); + payload.bank = _selectedBank; + }, + items: data + .cast() + .map((BankModel value) { + return new DropdownMenuItem( + value: value, + child: new Text(value.name), + ); + }) + .cast>() + .toList(), + ), ), - isEmpty: _selectedBank == null, - child: new DropdownButton( - value: _selectedBank, - isDense: true, - onChanged: (BankModel newValue) { - setState(() => _selectedBank = newValue); - payload.bank = _selectedBank; - }, - items: data - .cast() - .map((BankModel value) { - return new DropdownMenuItem( - value: value, - child: new Text(value.name), - ); - }) - .cast>() - .toList(), - ), - )), + ), _selectedBank != null && _selectedBank.showDOBField() ? InkWell( onTap: _selectBirthday, @@ -187,8 +188,7 @@ class _AccountPaymentWidgetState payload ..country = Strings.ng ..currency = Strings.ngn - ..bank = _selectedBank - ..phoneNumber; + ..bank = _selectedBank; super.onFormValidated(); }