├── lib ├── mpesa_sdk_dart.dart └── src │ ├── config │ └── mpesa_config.dart │ ├── models │ ├── payment_response.dart │ ├── transfer_request.dart │ ├── payment_request.dart │ ├── output_response.dart │ └── reversal_request.dart │ ├── utils │ └── rsa_key_helper.dart │ └── repository │ └── mpesa_transaction.dart ├── .metadata ├── pubspec.yaml ├── example ├── main.dart └── mpesa_keys.dart ├── CHANGELOG.md ├── LICENSE ├── .gitignore ├── pubspec.lock └── README.md /lib/mpesa_sdk_dart.dart: -------------------------------------------------------------------------------- 1 | library mpesa_sdk_dart; 2 | 3 | export 'src/config/mpesa_config.dart'; 4 | export 'src/repository/mpesa_transaction.dart'; 5 | export 'src/models/payment_request.dart'; 6 | 7 | export 'src/models/reversal_request.dart'; 8 | export 'src/models/transfer_request.dart'; 9 | export 'src/models/payment_response.dart'; 10 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mpesa_sdk_dart 2 | description: A Dart package for M-Pesa API (Mozambique). This package can be used in every dart frameworks as it's written in plain dart. 3 | version: 2.0.1 4 | author: Ergito Vilanculos 5 | repository: https://github.com/realrgt/mpesa_sdk_dart 6 | homepage: https://github.com/realrgt/mpesa_sdk_dart 7 | 8 | environment: 9 | sdk: '>=2.12.0 <3.0.0' 10 | 11 | dependencies: 12 | pointycastle: ^3.0.1 13 | asn1lib: ^1.0.0 14 | http: ^0.13.1 15 | -------------------------------------------------------------------------------- /lib/src/config/mpesa_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../utils/rsa_key_helper.dart'; 4 | 5 | class MpesaConfig { 6 | /// Encrypts the [Api Key] with [Public Key] and 7 | /// returns a usable Bearer Token 8 | /// 9 | static String getBearerToken(String apiKey, String publicKey) { 10 | try { 11 | final rsaKeyHelper = RsaKeyHelper(); 12 | final pk = rsaKeyHelper.parsePublicKeyFromPem(publicKey); 13 | final token = rsaKeyHelper.encrypt(apiKey, pk); 14 | 15 | return 'Bearer ${base64.encode(token)}'; 16 | } catch (e) { 17 | throw e.toString(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:mpesa_sdk_dart/mpesa_sdk_dart.dart'; 2 | 3 | main() async { 4 | String token = MpesaConfig.getBearerToken( 5 | 'API_KEY_HERE', 6 | 'PUBLIC_KEY_HERE', 7 | ); 8 | 9 | String apiHost = 'api.sandbox.vm.co.mz'; // use {api.vm.co.mz} for production 10 | 11 | PaymentRequest payload = PaymentRequest( 12 | inputTransactionReference: 'T12344C', 13 | inputCustomerMsisdn: '25884xxxxxxx', 14 | inputAmount: 10.0, 15 | inputThirdPartyReference: '11114', 16 | inputServiceProviderCode: '171717', // business short code 17 | ); 18 | 19 | await MpesaTransaction.c2b(token, apiHost, payload); 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.1] - 27/Mar/2021 4 | 5 | ### Added 6 | 7 | - Example page 8 | 9 | ### Changed 10 | 11 | - Refactor getBearerToken 12 | - Refactor request headers 13 | 14 | ## [1.0.1] - 27/Mar/2021 15 | 16 | ### Added 17 | 18 | - Null-safaty suport 19 | - Example page 20 | 21 | ### Changed 22 | 23 | - Outsourced `apiHost` to suport production keys. 24 | - Enhanced architecture (file structure) 25 | - Simplified RSA_HELPER 26 | 27 | ### removed 28 | 29 | - Removed Credits in README 30 | 31 | ## [1.0.0] - 07/Jun/2020 32 | 33 | ### Added 34 | 35 | - B2C transactions 36 | - B2B transactions 37 | - Reversal transactions 38 | - Query transactions status 39 | - Homepage config 40 | 41 | ### Changed 42 | 43 | - Updated README 44 | 45 | ### Fixed 46 | 47 | - Fix typos 48 | 49 | ### removed 50 | 51 | - Usage of [new] keyword 52 | 53 | ## [0.0.1] - 05/Jun/2020 54 | 55 | ### Added 56 | 57 | - Initial release 58 | -------------------------------------------------------------------------------- /example/mpesa_keys.dart: -------------------------------------------------------------------------------- 1 | final String apiKey_mpesa = "bk6dm2yrffboxi9p65k0hrqebsdiuxvs"; 2 | final String publicKey_mpesa = 3 | '''MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmptSWqV7cGU 4 | UJJhUBxsMLonux24u+FoTlrb+4Kgc6092JIszmI1QUoMohaDDXSVueXx6I 5 | XwYGsjjWY32HGXj1iQhkALXfObJ4DqXn5h6E8y5/xQYNAyd5bpN5Z8r892 6 | B6toGzZQVB7qtebH4apDjmvTi5FGZVjVYxalyyQkj4uQbbRQjgCkubSi45 7 | Xl4CGtLqZztsKssWz3mcKncgTnq3DHGYYEYiKq0xIj100LGbnvNz20Sgqm 8 | w/cH+Bua4GJsWYLEqf/h/yiMgiBbxFxsnwZl0im5vXDlwKPw+QnO2fscDh 9 | xZFAwV06bgG0oEoWm9FnjMsfvwm0rUNYFlZ+TOtCEhmhtFp+Tsx9jPCuOd 10 | 5h2emGdSKD8A6jtwhNa7oQ8RtLEEqwAn44orENa1ibOkxMiiiFpmmJkwgZ 11 | POG/zMCjXIrrhDWTDUOZaPx/lEQoInJoE2i43VN/HTGCCw8dKQAwg0jsEX 12 | au5ixD0GUothqvuX3B9taoeoFAIvUPEq35YulprMM7ThdKodSHvhnwKG82 13 | dCsodRwY428kg2xM/UjiTENog4B6zzZfPhMxFlOSFX4MnrqkAS+8Jamhy1 14 | GgoHkEMrsT5+/ofjCx0HjKbT5NuA2V/lmzgJLl3jIERadLzuTYnKGWxVJc 15 | GLkWXlEPYLbiaKzbJb2sYxt+Kt5OxQqC1MCAwEAAQ=='''; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ephenodrom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /lib/src/models/payment_response.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | PaymentResponse responseFromJson(String str) => 4 | PaymentResponse.fromJson(json.decode(str)); 5 | 6 | String responseToJson(PaymentResponse data) => json.encode(data.toJson()); 7 | 8 | class PaymentResponse { 9 | String? outputResponseCode; 10 | String? outputResponseDesc; 11 | String? outputTransactionID; 12 | String? outputConversationID; 13 | String? outputThirdPartyReference; 14 | 15 | PaymentResponse({ 16 | this.outputResponseCode, 17 | this.outputResponseDesc, 18 | this.outputTransactionID, 19 | this.outputConversationID, 20 | this.outputThirdPartyReference, 21 | }); 22 | 23 | PaymentResponse.fromJson(Map json) { 24 | outputResponseCode = json['output_ResponseCode']; 25 | outputResponseDesc = json['output_ResponseDesc']; 26 | outputTransactionID = json['output_TransactionID']; 27 | outputConversationID = json['output_ConversationID']; 28 | outputThirdPartyReference = json['output_ThirdPartyReference']; 29 | } 30 | 31 | Map toJson() { 32 | final Map data = new Map(); 33 | data['output_ResponseCode'] = this.outputResponseCode; 34 | data['output_ResponseDesc'] = this.outputResponseDesc; 35 | data['output_TransactionID'] = this.outputTransactionID; 36 | data['output_ConversationID'] = this.outputConversationID; 37 | data['output_ThirdPartyReference'] = this.outputThirdPartyReference; 38 | 39 | return data; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/models/transfer_request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | TransferRequest transferRequestFromJson(String str) => 4 | TransferRequest.fromJson(json.decode(str)); 5 | 6 | String transferRequestToJson(TransferRequest data) => 7 | json.encode(data.toJson()); 8 | 9 | class TransferRequest { 10 | String? inputTransactionReference; 11 | double? inputAmount; 12 | String? inputThirdPartyReference; 13 | String? inputPrimaryPartyCode; 14 | String? inputReceiverPartyCode; 15 | 16 | TransferRequest({ 17 | this.inputTransactionReference, 18 | this.inputAmount, 19 | this.inputThirdPartyReference, 20 | this.inputPrimaryPartyCode, 21 | this.inputReceiverPartyCode, 22 | }); 23 | 24 | TransferRequest.fromJson(Map json) { 25 | inputTransactionReference = json['input_TransactionReference']; 26 | inputAmount = json['input_Amount']; 27 | inputThirdPartyReference = json['input_ThirdPartyReference']; 28 | inputPrimaryPartyCode = json['input_PrimaryPartyCode']; 29 | inputReceiverPartyCode = json['input_ReceiverPartyCode']; 30 | } 31 | 32 | Map toJson() { 33 | final Map data = new Map(); 34 | data['input_TransactionReference'] = this.inputTransactionReference; 35 | data['input_Amount'] = this.inputAmount; 36 | data['input_ThirdPartyReference'] = this.inputThirdPartyReference; 37 | data['input_PrimaryPartyCode'] = this.inputPrimaryPartyCode; 38 | data['input_ReceiverPartyCode'] = this.inputReceiverPartyCode; 39 | 40 | return data; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/models/payment_request.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final paymentRequest = paymentRequestFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | 7 | PaymentRequest paymentRequestFromJson(String str) => 8 | PaymentRequest.fromJson(json.decode(str)); 9 | 10 | String paymentRequestToJson(PaymentRequest data) => json.encode(data.toJson()); 11 | 12 | class PaymentRequest { 13 | PaymentRequest({ 14 | this.inputTransactionReference, 15 | this.inputCustomerMsisdn, 16 | this.inputAmount, 17 | this.inputThirdPartyReference, 18 | this.inputServiceProviderCode, 19 | }); 20 | 21 | String? inputTransactionReference; 22 | String? inputCustomerMsisdn; 23 | double? inputAmount; 24 | String? inputThirdPartyReference; 25 | String? inputServiceProviderCode; 26 | 27 | factory PaymentRequest.fromJson(Map json) => PaymentRequest( 28 | inputTransactionReference: json["input_TransactionReference"], 29 | inputCustomerMsisdn: json["input_CustomerMSISDN"], 30 | inputAmount: json["input_Amount"], 31 | inputThirdPartyReference: json["input_ThirdPartyReference"], 32 | inputServiceProviderCode: json["input_ServiceProviderCode"], 33 | ); 34 | 35 | Map toJson() => { 36 | "input_TransactionReference": inputTransactionReference, 37 | "input_CustomerMSISDN": inputCustomerMsisdn, 38 | "input_Amount": inputAmount.toString(), 39 | "input_ThirdPartyReference": inputThirdPartyReference, 40 | "input_ServiceProviderCode": inputServiceProviderCode, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/models/output_response.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | OutputResponse responseFromJson(String str) => 4 | OutputResponse.fromJson(json.decode(str)); 5 | 6 | String responseToJson(OutputResponse data) => json.encode(data.toJson()); 7 | 8 | class OutputResponse { 9 | String? outputResponseCode; 10 | String? outputResponseDesc; 11 | String? outputTransactionID; 12 | String? outputConversationID; 13 | String? outputThirdPartyReference; 14 | String? outputResponseTransactionStatus; 15 | 16 | OutputResponse({ 17 | this.outputResponseCode, 18 | this.outputResponseDesc, 19 | this.outputTransactionID, 20 | this.outputConversationID, 21 | this.outputThirdPartyReference, 22 | this.outputResponseTransactionStatus, 23 | }); 24 | 25 | OutputResponse.fromJson(Map json) { 26 | outputResponseCode = json['output_ResponseCode']; 27 | outputResponseDesc = json['output_ResponseDesc']; 28 | outputTransactionID = json['output_TransactionID']; 29 | outputConversationID = json['output_ConversationID']; 30 | outputThirdPartyReference = json['output_ThirdPartyReference']; 31 | outputResponseTransactionStatus = json["output_ResponseTransactionStatus"]; 32 | } 33 | 34 | Map toJson() { 35 | final Map data = new Map(); 36 | data['output_ResponseCode'] = this.outputResponseCode; 37 | data['output_ResponseDesc'] = this.outputResponseDesc; 38 | data['output_TransactionID'] = this.outputTransactionID; 39 | data['output_ConversationID'] = this.outputConversationID; 40 | data['output_ThirdPartyReference'] = this.outputThirdPartyReference; 41 | data['output_ResponseTransactionStatus'] = 42 | this.outputResponseTransactionStatus; 43 | 44 | return data; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/models/reversal_request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | ReversalRequest reversalRequestFromJson(String str) => 4 | ReversalRequest.fromJson(json.decode(str)); 5 | 6 | String reversalRequestToJson(ReversalRequest data) => 7 | json.encode(data.toJson()); 8 | 9 | class ReversalRequest { 10 | String? inputTransactionID; 11 | String? inputSecurityCredential; 12 | String? inputInitiatorIdentifier; 13 | String? inputThirdPartyReference; 14 | String? inputServiceProviderCode; 15 | double? inputReversalAmount; 16 | 17 | ReversalRequest({ 18 | this.inputTransactionID, 19 | this.inputSecurityCredential, 20 | this.inputInitiatorIdentifier, 21 | this.inputThirdPartyReference, 22 | this.inputServiceProviderCode, 23 | this.inputReversalAmount, 24 | }); 25 | 26 | ReversalRequest.fromJson(Map json) { 27 | inputTransactionID = json['input_TransactionID']; 28 | inputSecurityCredential = json['input_SecurityCredential']; 29 | inputInitiatorIdentifier = json['input_InitiatorIdentifier']; 30 | inputThirdPartyReference = json['input_ThirdPartyReference']; 31 | inputServiceProviderCode = json['input_ServiceProviderCode']; 32 | inputReversalAmount = json['input_ReversalAmount']; 33 | } 34 | 35 | Map toJson() { 36 | final Map data = new Map(); 37 | data['input_TransactionID'] = this.inputTransactionID; 38 | data['input_SecurityCredential'] = this.inputSecurityCredential; 39 | data['input_InitiatorIdentifier'] = this.inputInitiatorIdentifier; 40 | data['input_ThirdPartyReference'] = this.inputThirdPartyReference; 41 | data['input_ServiceProviderCode'] = this.inputServiceProviderCode; 42 | data['input_ReversalAmount'] = this.inputReversalAmount; 43 | 44 | return data; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | 3 | # Miscellaneous 4 | *.class 5 | *.log 6 | *.pyc 7 | *.swp 8 | .DS_Store 9 | .atom/ 10 | .buildlog/ 11 | .history 12 | .svn/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | build/ 34 | 35 | # Android related 36 | **/android/**/gradle-wrapper.jar 37 | **/android/.gradle 38 | **/android/captures/ 39 | **/android/gradlew 40 | **/android/gradlew.bat 41 | **/android/local.properties 42 | **/android/**/GeneratedPluginRegistrant.java 43 | 44 | # iOS/XCode related 45 | **/ios/**/*.mode1v3 46 | **/ios/**/*.mode2v3 47 | **/ios/**/*.moved-aside 48 | **/ios/**/*.pbxuser 49 | **/ios/**/*.perspectivev3 50 | **/ios/**/*sync/ 51 | **/ios/**/.sconsign.dblite 52 | **/ios/**/.tags* 53 | **/ios/**/.vagrant/ 54 | **/ios/**/DerivedData/ 55 | **/ios/**/Icon? 56 | **/ios/**/Pods/ 57 | **/ios/**/.symlinks/ 58 | **/ios/**/profile 59 | **/ios/**/xcuserdata 60 | **/ios/.generated/ 61 | **/ios/Flutter/App.framework 62 | **/ios/Flutter/Flutter.framework 63 | **/ios/Flutter/Flutter.podspec 64 | **/ios/Flutter/Generated.xcconfig 65 | **/ios/Flutter/app.flx 66 | **/ios/Flutter/app.zip 67 | **/ios/Flutter/flutter_assets/ 68 | **/ios/Flutter/flutter_export_environment.sh 69 | **/ios/ServiceDefinitions.json 70 | **/ios/Runner/GeneratedPluginRegistrant.* 71 | 72 | # Exceptions to above rules. 73 | !**/ios/**/default.mode1v3 74 | !**/ios/**/default.mode2v3 75 | !**/ios/**/default.pbxuser 76 | !**/ios/**/default.perspectivev3 77 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 78 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | asn1lib: 5 | dependency: "direct main" 6 | description: 7 | name: asn1lib 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "1.0.0" 11 | charcode: 12 | dependency: transitive 13 | description: 14 | name: charcode 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.2.0" 18 | collection: 19 | dependency: transitive 20 | description: 21 | name: collection 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.15.0" 25 | http: 26 | dependency: "direct main" 27 | description: 28 | name: http 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "0.13.1" 32 | http_parser: 33 | dependency: transitive 34 | description: 35 | name: http_parser 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "4.0.0" 39 | meta: 40 | dependency: transitive 41 | description: 42 | name: meta 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.3.0" 46 | path: 47 | dependency: transitive 48 | description: 49 | name: path 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.8.0" 53 | pedantic: 54 | dependency: transitive 55 | description: 56 | name: pedantic 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.11.0" 60 | pointycastle: 61 | dependency: "direct main" 62 | description: 63 | name: pointycastle 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "3.0.1" 67 | source_span: 68 | dependency: transitive 69 | description: 70 | name: source_span 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.8.1" 74 | string_scanner: 75 | dependency: transitive 76 | description: 77 | name: string_scanner 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.1.0" 81 | term_glyph: 82 | dependency: transitive 83 | description: 84 | name: term_glyph 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.2.0" 88 | typed_data: 89 | dependency: transitive 90 | description: 91 | name: typed_data 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "1.3.0" 95 | sdks: 96 | dart: ">=2.12.0 <3.0.0" 97 | -------------------------------------------------------------------------------- /lib/src/utils/rsa_key_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:asn1lib/asn1lib.dart'; 5 | import 'package:pointycastle/export.dart'; 6 | 7 | /// Helper class to handle RSA key generation and encoding 8 | class RsaKeyHelper { 9 | /// Decode Public key from PEM Format 10 | /// 11 | /// Given a base64 encoded PEM [String] with correct headers and footers, return a 12 | /// [RSAPublicKey] 13 | /// 14 | /// *PKCS1* 15 | /// RSAPublicKey ::= SEQUENCE { 16 | /// modulus INTEGER, -- n 17 | /// publicExponent INTEGER -- e 18 | /// } 19 | /// 20 | /// *PKCS8* 21 | /// PublicKeyInfo ::= SEQUENCE { 22 | /// algorithm AlgorithmIdentifier, 23 | /// PublicKey BIT STRING 24 | /// } 25 | /// 26 | /// AlgorithmIdentifier ::= SEQUENCE { 27 | /// algorithm OBJECT IDENTIFIER, 28 | /// parameters ANY DEFINED BY algorithm OPTIONAL 29 | /// } 30 | RSAPublicKey parsePublicKeyFromPem(pemString) { 31 | List publicKeyDER = decodePEM(pemString); 32 | var asn1Parser = ASN1Parser(publicKeyDER as Uint8List); 33 | var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; 34 | 35 | var modulus, exponent; 36 | // Depending on the first element type, we either have PKCS1 or 2 37 | if (topLevelSeq.elements[0].runtimeType == ASN1Integer) { 38 | modulus = topLevelSeq.elements[0] as ASN1Integer; 39 | exponent = topLevelSeq.elements[1] as ASN1Integer; 40 | } else { 41 | var publicKeyBitString = topLevelSeq.elements[1]; 42 | 43 | var publicKeyAsn = ASN1Parser(publicKeyBitString.contentBytes()!); 44 | ASN1Sequence publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence; 45 | modulus = publicKeySeq.elements[0] as ASN1Integer; 46 | exponent = publicKeySeq.elements[1] as ASN1Integer; 47 | } 48 | 49 | RSAPublicKey rsaPublicKey = 50 | RSAPublicKey(modulus.valueAsBigInteger, exponent.valueAsBigInteger); 51 | 52 | return rsaPublicKey; 53 | } 54 | 55 | /// Takes in a PEM [String], removes its prefix & suffix, returns 56 | /// a [Unit8List] 57 | List decodePEM(String pem) { 58 | return base64.decode(removePemHeaderAndFooter(pem)); 59 | } 60 | 61 | /// Removes PEM prefix & suffix once given a PEM [String] 62 | String removePemHeaderAndFooter(String pem) { 63 | var startsWith = [ 64 | "-----BEGIN PUBLIC KEY-----", 65 | "-----BEGIN RSA PRIVATE KEY-----", 66 | "-----BEGIN RSA PUBLIC KEY-----", 67 | "-----BEGIN PRIVATE KEY-----", 68 | "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: React-Native-OpenPGP.js 0.1\r\nComment: http://openpgpjs.org\r\n\r\n", 69 | "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: React-Native-OpenPGP.js 0.1\r\nComment: http://openpgpjs.org\r\n\r\n", 70 | ]; 71 | var endsWith = [ 72 | "-----END PUBLIC KEY-----", 73 | "-----END PRIVATE KEY-----", 74 | "-----END RSA PRIVATE KEY-----", 75 | "-----END RSA PUBLIC KEY-----", 76 | "-----END PGP PUBLIC KEY BLOCK-----", 77 | "-----END PGP PRIVATE KEY BLOCK-----", 78 | ]; 79 | bool isOpenPgp = pem.indexOf('BEGIN PGP') != -1; 80 | 81 | pem = pem.replaceAll(' ', ''); 82 | pem = pem.replaceAll('\n', ''); 83 | pem = pem.replaceAll('\r', ''); 84 | 85 | for (var s in startsWith) { 86 | s = s.replaceAll(' ', ''); 87 | if (pem.startsWith(s)) { 88 | pem = pem.substring(s.length); 89 | } 90 | } 91 | 92 | for (var s in endsWith) { 93 | s = s.replaceAll(' ', ''); 94 | if (pem.endsWith(s)) { 95 | pem = pem.substring(0, pem.length - s.length); 96 | } 97 | } 98 | 99 | if (isOpenPgp) { 100 | var index = pem.indexOf('\r\n'); 101 | pem = pem.substring(0, index); 102 | } 103 | 104 | return pem; 105 | } 106 | 107 | /// Encrypts ApiKey with its PublicKey 108 | /// 109 | /// Given a [String] & [RSAPublicKey] returns an [Unit8List] cipherText 110 | Uint8List encrypt(String plaintext, RSAPublicKey publicKey) { 111 | var cipher = PKCS1Encoding(RSAEngine()) 112 | ..init(true, PublicKeyParameter(publicKey)); 113 | var cipherText = cipher.process(Uint8List.fromList(plaintext.codeUnits)); 114 | 115 | // return String.fromCharCodes(cipherText); 116 | return cipherText; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/repository/mpesa_transaction.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:http/http.dart' as http; 4 | 5 | import '../models/payment_request.dart'; 6 | import '../models/reversal_request.dart'; 7 | import '../models/transfer_request.dart'; 8 | 9 | class MpesaTransaction { 10 | /// Initiates a C2B transaction on the M-Pesa API. 11 | /// 12 | /// Given a [token] and an instance of [PaymentRequest], 13 | /// returns an http [Response] 14 | /// 15 | static c2b( 16 | String token, 17 | String apiHost, 18 | PaymentRequest paymentRequest, 19 | ) async { 20 | http.Response? response; 21 | var url = 'https://$apiHost:18352/ipg/v1x/c2bPayment/singleStage/'; 22 | await http 23 | .post( 24 | Uri.parse(url), 25 | headers: getHeaders(token), 26 | body: paymentRequestToJson(paymentRequest), 27 | encoding: utf8, 28 | ) 29 | .then((res) { 30 | print('${res.statusCode} :${res.reasonPhrase}'); 31 | print(res.body); 32 | response = res; 33 | }); 34 | return response; 35 | } 36 | 37 | /// Initiates a B2C transaction on the M-Pesa API. 38 | /// 39 | /// Given a [token] and an instance of [PaymentRequest], 40 | /// returns an http [Response] 41 | /// 42 | static b2c( 43 | String token, 44 | String apiHost, 45 | PaymentRequest paymentRequest, 46 | ) async { 47 | http.Response? response; 48 | var url = 'https://$apiHost:18345/ipg/v1x/b2cPayment/'; 49 | await http 50 | .post( 51 | Uri.parse(url), 52 | headers: getHeaders(token), 53 | body: paymentRequestToJson(paymentRequest), 54 | encoding: utf8, 55 | ) 56 | .then((res) { 57 | print('${res.statusCode} :${res.reasonPhrase}'); 58 | print(res.body); 59 | response = res; 60 | }); 61 | return response; 62 | } 63 | 64 | /// Initiates a Reversal Transaction on the M-Pesa API. 65 | /// 66 | /// Given a [token] and an istance of [ReversalRequest], 67 | /// returns an http [Response] 68 | /// 69 | static reversal( 70 | String token, 71 | String apiHost, 72 | ReversalRequest reversalRequest, 73 | ) async { 74 | http.Response? response; 75 | var url = 'https://$apiHost:18354/ipg/v1x/reversal/'; 76 | await http 77 | .put( 78 | Uri.parse(url), 79 | headers: getHeaders(token), 80 | body: reversalRequestToJson(reversalRequest), 81 | encoding: utf8, 82 | ) 83 | .then((res) { 84 | print('${res.statusCode} :${res.reasonPhrase}'); 85 | print(res.body); 86 | response = res; 87 | }); 88 | return response; 89 | } 90 | 91 | /// Initiates a transaction Query on the M-Pesa API. 92 | /// and returns an http [Response] 93 | /// 94 | static getTransactionStatus( 95 | token, 96 | apiHost, 97 | inputThirdPartyReference, 98 | inputQueryReference, 99 | inputServiceProviderCode, 100 | ) async { 101 | http.Response? response; 102 | var url = 103 | 'https://$apiHost:18353/ipg/v1x/queryTransactionStatus/?input_ThirdPartyReference=$inputThirdPartyReference&input_QueryReference=$inputQueryReference&input_ServiceProviderCode=$inputServiceProviderCode'; 104 | await http 105 | .get( 106 | Uri.parse(url), 107 | headers: getHeaders(token), 108 | ) 109 | .then((res) { 110 | print('${res.statusCode} :${res.reasonPhrase}'); 111 | print(res.body); 112 | response = res; 113 | }); 114 | return response; 115 | } 116 | 117 | /// Initiates a B2B transaction on the M-Pesa API. 118 | /// 119 | /// Given a [token] and an instance of [PaymentRequest], 120 | /// returns an http [Response] 121 | /// 122 | static b2b( 123 | String token, 124 | String apiHost, 125 | TransferRequest transferRequest, 126 | ) async { 127 | http.Response? response; 128 | var url = 'https://$apiHost:18349/ipg/v1x/b2bPayment/'; 129 | await http 130 | .post( 131 | Uri.parse(url), 132 | headers: getHeaders(token), 133 | body: transferRequestToJson(transferRequest), 134 | encoding: utf8, 135 | ) 136 | .then((res) { 137 | print('${res.statusCode} :${res.reasonPhrase}'); 138 | print(res.body); 139 | response = res; 140 | }); 141 | return response; 142 | } 143 | 144 | static Map getHeaders(String token) { 145 | return { 146 | 'content-type': 'application/json', 147 | 'authorization': '$token', 148 | 'origin': '*', 149 | }; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpesa_sdk_dart 2 | 3 | [![Pub Version](https://img.shields.io/pub/v/mpesa_sdk_dart?color=blue)](https://pub.dev/packages/mpesa_sdk_dart) 4 | ![GitHub](https://img.shields.io/github/license/realrgt/mpesa_sdk_dart) 5 | ![GitHub repo size](https://img.shields.io/github/repo-size/realrgt/mpesa_sdk_dart?color=red) 6 | 7 | Dart package for M-Pesa API (Mozambique) 8 | 9 | Ready Methods/APIs 10 | 11 | - [x] C2B 12 | - [x] B2B 13 | - [x] B2C 14 | - [x] TRANSACTION STATUS 15 | - [x] REVERSAL 16 | 17 | ## Requisites 18 | 19 | We highly recommend reading Mpesa API [docs](https://developer.mpesa.vm.co.mz/) first! 20 | 21 | You Will need a few things from there before development. 22 | 23 | 1. Api Key 24 | 2. Public Key 25 | 26 | - Login or Register as a M-Pesa developer [here](https://developer.mpesa.vm.co.mz/accounts/login/?next=/accounts/signup/) if you haven't. 27 | - You will be issued with an API Key and a Public Key. You will use these to generate your access token. 28 | 29 | ## Usage 30 | 31 | Add dependency in pubspec.yaml 32 | 33 | ```yaml 34 | dependencies: 35 | mpesa_sdk_dart: 36 | ``` 37 | 38 | Import in your Flutter app or plain dart app. 39 | 40 | ```dart 41 | import 'package:mpesa_sdk_dart/mpesa_sdk_dart.dart'; 42 | ``` 43 | 44 | ### Generate a token 45 | 46 | ```dart 47 | String token = MpesaConfig.getBearerToken( 48 | 'API_KEY_HERE', 49 | 'PUBLIC_KEY_HERE', 50 | ); 51 | ``` 52 | 53 | ### C2B Api Call 54 | 55 | #### Initialize your payload 56 | 57 | ```dart 58 | PaymentRequest payload = PaymentRequest( 59 | inputTransactionReference: 'inputTransactionReference', 60 | inputCustomerMsisdn: '25884xxxxxxx', 61 | inputAmount: inputAmount, 62 | inputThirdPartyReference: 'inputThirdPartyReference', 63 | inputServiceProviderCode: 'inputServiceProviderCode', 64 | ); 65 | ``` 66 | 67 | #### Perform the api call 68 | 69 | ```dart 70 | MpesaTransaction.c2b(token, apiHost, payload); 71 | ``` 72 | 73 | ### B2C Api Call 74 | 75 | #### Initialize your payload 76 | 77 | ```dart 78 | PaymentRequest payload = PaymentRequest( 79 | inputTransactionReference: 'inputTransactionReference', 80 | inputCustomerMsisdn: '25884xxxxxxx', 81 | inputAmount: inputAmount, 82 | inputThirdPartyReference: 'inputThirdPartyReference', 83 | inputServiceProviderCode: 'inputServiceProviderCode', 84 | ); 85 | ``` 86 | 87 | #### Perform the api call 88 | 89 | ```dart 90 | MpesaTransaction.b2c(token, apiHost, payload); 91 | ``` 92 | 93 | ### Reversal Api Call 94 | 95 | #### Initialize your payload 96 | 97 | ```dart 98 | ReversalRequest payload = ReversalRequest( 99 | inputTransactionID: 'input_TransactionID', 100 | inputSecurityCredential: 'input_SecurityCredential', 101 | inputInitiatorIdentifier: 'input_InitiatorIdentifier', 102 | inputThirdPartyReference: 'input_ThirdPartyReference', 103 | inputServiceProviderCode: 'input_ServiceProviderCode', 104 | inputReversalAmount: montant, // Optional 105 | ); 106 | ``` 107 | 108 | #### Perform the api call 109 | 110 | ```dart 111 | MpesaTransaction.reversal(token, apiHost, payload); 112 | ``` 113 | 114 | ### B2B Api Call 115 | 116 | #### Initialize your payload 117 | 118 | ```dart 119 | TransferRequest payload = TransferRequest( 120 | inputTransactionReference: 'input_TransactionReference', 121 | inputAmount: inputAmount, 122 | inputThirdPartyReference: 'input_ThirdPartyReference', 123 | inputPrimaryPartyCode: 'input_PrimaryPartyCode', 124 | inputReceiverPartyCode: 'input_ReceiverPartyCode', 125 | ); 126 | ``` 127 | 128 | #### Perform the api call 129 | 130 | ```dart 131 | MpesaTransaction.b2b(token, apiHost, payload); 132 | ``` 133 | 134 | ### Query Transaction Status Api Call 135 | 136 | #### Perform the api call 137 | 138 | ```dart 139 | MpesaTransaction.getTransactionStatus( 140 | token, 141 | apiHost, 142 | 'input_ThirdPartyReference', 143 | 'input_QueryReference', 144 | 'input_ServiceProviderCode', 145 | ); 146 | ``` 147 | 148 | ## Handle Response 149 | 150 | All transaction methods returned an http response. So what you have to do is assign your call to a property of type Response (From [http](https://pub.dev/packages/http) package). Note that this is an async task. 151 | 152 | ```dart 153 | Response response = await MpesaTransaction.c2b(token, apiHost, payload); 154 | print(response.body); 155 | 156 | if(response.statusCode == 201) { // if is resource created! 157 | // Do something! 158 | } 159 | ``` 160 | 161 | ## Copyright and license 162 | 163 | MIT License 164 | 165 | Copyright (c) 2020-2021 Ergito Vilanculos 166 | 167 | Permission is hereby granted, free of charge, to any person obtaining a copy 168 | of this software and associated documentation files (the "Software"), to deal 169 | in the Software without restriction, including without limitation the rights 170 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 171 | copies of the Software, and to permit persons to whom the Software is 172 | furnished to do so, subject to the following conditions: 173 | 174 | The above copyright notice and this permission notice shall be included in all 175 | copies or substantial portions of the Software. 176 | 177 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 178 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 179 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 180 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 181 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 182 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 183 | SOFTWARE. 184 | --------------------------------------------------------------------------------