flutter_paystack 0.5.2

  • README.md
  • CHANGELOG.md
  • Example
  • Installing
  • Versions
  • new78

Paystack Plugin for Flutter

pub package

A Flutter plugin for making payments via Paystack Payment Gateway. Completely supports Android and iOS.

Installation

To use this plugin, add flutter_paystack as a dependency in your pubspec.yaml file.

Then initialize the plugin preferably in the initState of your widget.

import 'package:flutter_paystack/flutter_paystack.dart'

class PaymentPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new _PaymentPageState();
}

class _PaymentPageState extends State<PaymentPage> {
  var paystackPublicKey = '[YOUR_PAYSTACK_PUBLIC_KEY]';

  @override
  void initState() {
    PaystackPlugin.initialize(publicKey: paystackPublicKey);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // Return your widgets
  }
}

No other configuration required - the plugin should work out of the box.

Making Payments

You can choose to initialize the payment locally or via Paystack's backend.

1.a. This starts by making a HTTP POST request to https://api.paystack.co/transaction/initialize with the amount(in kobo), reference, etc in the request body and your paystack secret key in request header. The request looks like this:

// Required imports
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

  initTransaction() async {
    var url = 'https://api.paystack.co/transaction/initialize';

    Map<String, String> headers = {
      'Authorization': 'Bearer $[YOUR_PAYSTACK_SECRET_KEY]',
      'Content-Type': 'application/json'
    };

    Map<String, String> body = {
      'reference': '[YOUR_GENERATED_REFERENCE]',
      'amount': 500000.toString(),
      'email': 'user@email.com'
    };

    http.Response response =
        await http.post(url, body: body, headers: headers);
    // Charge card
    _chargeCard(response.body);
  }

Please check the official documentation for the full details of payment initialization.

1.b If everything goes well, the initialization request returns a response with an access_code. You can then create a Charge object with the access code and card details. The charge is in turn passed to the PaystackPlugin.chargeCard() function for payment:

  PaymentCard _getCardFromUI() {
    // Using just the must-required parameters.
    return PaymentCard(
      number: cardNumber,
      cvc: cvv,
      expiryMonth: expiryMonth,
      expiryYear: expiryYear,
    );

    // Using Cascade notation (similar to Java's builder pattern)
//    return PaymentCard(
//        number: cardNumber,
//        cvc: cvv,
//        expiryMonth: expiryMonth,
//        expiryYear: expiryYear)
//      ..name = 'Segun Chukwuma Adamu'
//      ..country = 'Nigeria'
//      ..addressLine1 = 'Ikeja, Lagos'
//      ..addressPostalCode = '100001';

    // Using optional parameters
//    return PaymentCard(
//        number: cardNumber,
//        cvc: cvv,
//        expiryMonth: expiryMonth,
//        expiryYear: expiryYear,
//        name: 'Ismail Adebola Emeka',
//        addressCountry: 'Nigeria',
//        addressLine1: '90, Nnebisi Road, Asaba, Deleta State');
  }

  _chargeCard(http.Response response) {
    Map<String, dynamic> responseBody = json.decode(response.body);
    var charge = Charge()
      ..accessCode = responseBody['access_code']
      ..card = _getCardFromUI();

    PaystackPlugin.chargeCard(context,
        charge: charge,
        beforeValidate: (transaction) => handleBeforeValidate(transaction),
        onSuccess: (transaction) => handleOnSuccess(transaction),
        onError: (error, transaction) => handleOnError(error, transaction));
  }

  handleBeforeValidate(Transaction transaction) {
    // This is called only before requesting OTP
    // Save reference so you may send to server if error occurs with OTP
  }

  handleOnError(Object e, Transaction transaction) {
    // If an access code has expired, simply ask your server for a new one
    // and restart the charge instead of displaying error
  }


  handleOnSuccess(Transaction transaction) {
    // This is called only after transaction is successful
  }

2. Initialize Locally

Just send the payment details to PaystackPlugin.chargeCard

      // Set transaction params directly in app (note that these params
      // are only used if an access_code is not set. In debug mode,
      // setting them after setting an access code would throw an error
      Charge charge = Charge();
      charge.card = _getCardFromUI();
      charge
        ..amount = 2000
        ..email = 'user@email.com'
        ..reference = _getReference()
        ..putCustomField('Charged From', 'Flutter PLUGIN');
      _chargeCard();

Validating Card Details

You are expected to build the UI for your users to enter their payment details. For easier validation, wrap the TextFormFields inside a Form widget. Please check this article on validating forms on Flutter if this is new to you.

You can validate the fields with these methods:

card.validNumber

This method helps to perform a check if the card number is valid.

card.validCVC

Method that checks if the card security code is valid.

card.validExpiryDate

Method checks if the expiry date (combination of year and month) is valid.

card.isValid

Method to check if the card is valid. Always do this check, before charging the card.

card.getType

This method returns an estimate of the string representation of the card type(issuer).

chargeCard

Charging with the PaystackPlugin is quite straightforward. It requires the following arguments.

  1. context: your UI BuildContext. It's used by the plugin for showing dialogs for the user to take a required action, e.g inputting OTP.
  2. charge: You provide the payment details (PaymentCard, amount email etc) to an instance of the Charge object.
  3. beforeValidate: Pre-validation callback.
  4. onSuccess: callbacks for a successful payment.
  5. onError: callback for when an error occurs in the transaction. Provides you with a reference to the error object.

Verifying Transactions

This is quite easy. Just send a HTTP GET request to https://api.paystack.co/transaction/verify/$[TRANSACTION_REFERENCE]. Please, check the official documentaion on verifying transactions.

Testing your implementation

Paystack provides tons of payment cards for testing.

Running Example project

For help getting started with Flutter, view the online documentation.

An example project has been provided in this plugin. Clone this repo and navigate to the example folder. Open it with a supported IDE or execute flutter run from that folder in terminal.

Contributing, Issues and Bug Reports

The project is open to public contribution. Please feel very free to contribute. Experienced an issue or want to report a bug? Please, report it here. Remember to be descriptive.

Credits

Thanks to the authors of Paystack iOS and Android SDKS. I leveraged on their work to bring this plugin to fruition.

0.5.2

  • Support for Flutter v0.5.1.

0.5.1

  • Exposed Paystack Exception
  • Properly formatted .dart files
  • Removed deprecated APIs

0.5.0

  • Initial beta release.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_paystack/flutter_paystack.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;

// To get started quickly, change this to your heroku deployment of
// https://github.com/PaystackHQ/sample-charge-card-backend
// Step 1. Visit https://github.com/PaystackHQ/sample-charge-card-backend
// Step 2. Click "Deploy to heroku"
// Step 3. Login with your heroku credentials or create a free heroku account
// Step 4. Provide your secret key and an email with which to start all test transactions
// Step 5. Copy the url generated by heroku (format https://some-url.herokuapp.com) into the space below
String backendUrl = 'https://wilbur-paystack.herokuapp.com';
// Set this to a public key that matches the secret key you supplied while creating the heroku instance
String paystackPublicKey = '{YOUR_PAYSTACK_PUBLIC_KEY}';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Paystack Example',
      theme: new ThemeData(
        primaryColor: Colors.lightBlue[900],
      ),
      home: new HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var _scaffoldKey = GlobalKey<ScaffoldState>();
  var _formKey = GlobalKey<FormState>();
  var _autoValidate = false;
  var _localInProgress = false;
  var _remoteInProgress = false;
  Charge _charge;
  Transaction _transaction;
  String _reference = 'No transaction yet';
  String _error = '';
  String _backendMessage = '';
  String cardNumber;
  String cvv;
  int expiryMonth = 0;
  int expiryYear = 0;

  @override
  void initState() {
    _validateSetupParams();
    PaystackPlugin.initialize(publicKey: paystackPublicKey);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;
    var screenWidth = size.width;
    var screenHeight = size.height;

    var appBar = new AppBar(
      title: new Text('Paystack Example'),
    );

    return new Scaffold(
      key: _scaffoldKey,
      appBar: appBar,
      body: new Container(
        color: new Color(0xFF1C3A4B),
        child: new ListView(
          children: <Widget>[
            new Container(
              color: Colors.grey[50],
              padding: const EdgeInsets.symmetric(horizontal: 10.0),
              width: double.infinity,
              height: ((screenHeight / 2) - appBar.preferredSize.height),
              child: new Form(
                key: _formKey,
                autovalidate: _autoValidate,
                child: new SingleChildScrollView(
                  child: new ListBody(
                    children: <Widget>[
                      new SizedBox(
                        height: 5.0,
                      ),
                      new Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          new Container(
                            width: screenWidth / 1.7,
                            child: new TextFormField(
                              keyboardType: TextInputType.number,
                              inputFormatters: [
                                WhitelistingTextInputFormatter.digitsOnly,
                              ],
                              decoration: const InputDecoration(
                                border: const UnderlineInputBorder(),
                                labelText: 'Card number',
                              ),
                              onSaved: (String value) => cardNumber = value,
                              validator: (String value) =>
                                  value.isEmpty ? fieldIsReq : null,
                            ),
                          ),
                          new SizedBox(
                            width: 30.0,
                          ),
                          new Container(
                              width: screenWidth / 5,
                              child: new TextFormField(
                                keyboardType: TextInputType.number,
                                inputFormatters: [
                                  WhitelistingTextInputFormatter.digitsOnly,
                                  new LengthLimitingTextInputFormatter(4)
                                ],
                                decoration: const InputDecoration(
                                    border: const UnderlineInputBorder(),
                                    labelText: 'CVV'),
                                onSaved: (String value) => cvv = value,
                                validator: (String value) =>
                                    value.isEmpty ? fieldIsReq : null,
                              ))
                        ],
                      ),
                      new SizedBox(
                        height: 20.0,
                      ),
                      new Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          new Container(
                            width: screenWidth / 5,
                            child: new TextFormField(
                              keyboardType: TextInputType.number,
                              inputFormatters: [
                                WhitelistingTextInputFormatter.digitsOnly,
                                new LengthLimitingTextInputFormatter(2)
                              ],
                              decoration: const InputDecoration(
                                border: const UnderlineInputBorder(),
                                labelText: 'MM',
                              ),
                              onSaved: (String value) =>
                                  expiryMonth = int.parse(value),
                              validator: (String value) =>
                                  value.isEmpty ? fieldIsReq : null,
                            ),
                          ),
                          new SizedBox(
                            width: 30.0,
                          ),
                          new Container(
                              width: screenWidth / 5,
                              child: new TextFormField(
                                keyboardType: TextInputType.number,
                                inputFormatters: [
                                  WhitelistingTextInputFormatter.digitsOnly,
                                  new LengthLimitingTextInputFormatter(4)
                                ],
                                decoration: const InputDecoration(
                                    border: const UnderlineInputBorder(),
                                    labelText: 'YYYY'),
                                onSaved: (String value) =>
                                    expiryYear = int.parse(value),
                                validator: (String value) =>
                                    value.isEmpty ? fieldIsReq : null,
                              )),
                        ],
                      ),
                      new SizedBox(
                        height: 40.0,
                      ),
                      _localInProgress || _remoteInProgress
                          ? new Container(
                              alignment: Alignment.center,
                              child: Platform.isIOS
                                  ? new CupertinoActivityIndicator()
                                  : new CircularProgressIndicator(),
                            )
                          : new Column(
                              mainAxisSize: MainAxisSize.min,
                              children: <Widget>[
                                _getPlatformButton(
                                    'Charge Card (Init From Server)', false),
                                new SizedBox(
                                  height: 10.0,
                                ),
                                _getPlatformButton(
                                    'Charge Card (Init From App)', true),
                              ],
                            ),
                    ],
                  ),
                ),
              ),
            ),
            new Container(
              width: double.infinity,
              height: screenHeight / 2.35, // Can't 2.0
              child: new Container(
                margin: const EdgeInsets.only(top: 15.0),
                padding: const EdgeInsets.all(20.0),
                child: new SingleChildScrollView(
                  child: new ListBody(
                    children: <Widget>[
                      new Text(
                        _reference,
                        style: const TextStyle(
                            color: Colors.white, fontSize: 18.0),
                      ),
                      new SizedBox(height: 20.0),
                      new Text(
                        _error,
                        style: new TextStyle(color: Colors.red[400]),
                      ),
                      new SizedBox(
                        height: 20.0,
                      ),
                      new Text(
                        _backendMessage,
                        style: const TextStyle(color: Colors.white),
                      )
                    ],
                  ),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }

  Widget _getPlatformButton(String string, bool isLocal) {
    // is still in progress
    Widget widget;
    if (Platform.isIOS) {
      widget = new CupertinoButton(
        onPressed: () => _startAfreshCharge(isLocal),
        color: CupertinoColors.activeBlue,
        child: new Text(
          string,
        ),
      );
    } else {
      widget = new RaisedButton(
        onPressed: () => _startAfreshCharge(isLocal),
        color: Colors.lightBlue[900],
        textColor: Colors.white,
        padding: const EdgeInsets.symmetric(vertical: 13.0, horizontal: 10.0),
        child: new Text(
          string.toUpperCase(),
          style: const TextStyle(fontSize: 17.0),
        ),
      );
    }
    return widget;
  }

  _showSnackBar(String message) {
    _scaffoldKey.currentState.showSnackBar(new SnackBar(
      content: new Text(message),
      duration: const Duration(seconds: 3),
    ));
  }

  bool _allInputsAreValid() {
    final FormState form = _formKey.currentState;
    if (!form.validate()) {
      setState(() {
        _autoValidate = true; // Start validating on every change.
      });
      return false;
    } else {
      form.save();
      return true;
    }
  }

  _startAfreshCharge(bool isLocal) {
    if (!_allInputsAreValid()) {
      return;
    }

    _charge = Charge();
    _charge.card = _getCardFromUI();

    if (isLocal) {
      setState(() => _localInProgress = true);
      // Set transaction params directly in app (note that these params
      // are only used if an access_code is not set. In debug mode,
      // setting them after setting an access code would throw an exception
      _charge
        ..amount = 2000
        ..email = 'faradaywilly@gmail.com'
        ..reference = _getReference()
        ..putCustomField('Charged From', 'Flutter SDK');
      _chargeCard();
    } else {
      // Perform transaction/initialize on Paystack server to get an access code
      // documentation: https://developers.paystack.co/reference#initialize-a-transaction
      setState(() => _remoteInProgress = true);
      _fetchAccessCodeFrmServer();
    }
  }

  _chargeCard() {
    _transaction = null;

    PaystackPlugin.chargeCard(context,
        charge: _charge,
        beforeValidate: (transaction) => handleBeforeValidate(transaction),
        onSuccess: (transaction) => handleOnSuccess(transaction),
        onError: (error, transaction) => handleOnError(error, transaction));
  }

  // This is called only before requesting OTP
  // Save reference so you may send to server if error occurs with OTP
  handleBeforeValidate(Transaction transaction) {
    this._transaction = transaction;
    _showSnackBar(_transaction.reference);
    setState(() => _updateReference());
  }

  handleOnError(Object e, Transaction transaction) {
    // If an access code has expired, simply ask your server for a new one
    // and restart the charge instead of displaying error
    this._transaction = transaction;
    if (e is ExpiredAccessCodeException) {
      _startAfreshCharge(false);
      _chargeCard();
      return;
    }

    if (transaction.reference != null) {
      _showSnackBar('${this._transaction.reference} concluded with error: '
          '${e.toString()}');
      _error = '${this._transaction.reference} concluded with error: '
          '${e.toString()}';
      _verifyOnServer();
    } else {
      _showSnackBar(e.toString());
      _error = '${e.toString()}';
    }

    setState(() {
      _localInProgress = false;
      _remoteInProgress = false;
      _updateReference();
    });
  }

  // This is called only after transaction is successful
  handleOnSuccess(Transaction transaction) {
    setState(() {
      _localInProgress = false;
      _remoteInProgress = false;
      this._transaction = transaction;
      _error = '';
      _updateReference();
    });
    _showSnackBar(this._transaction.reference);
    _verifyOnServer();
  }

  _updateReference() {
    if (_transaction.reference != null) {
      _reference = 'Reference: ${_transaction.reference}';
    } else {
      _reference = 'No transaction';
    }
  }

  _validateSetupParams() {
    assert(() {
      if (backendUrl == null || !backendUrl.isNotEmpty) {
        throw new UnimplementedError(
            'Please set a backend url before running the sample');
      }
      if (paystackPublicKey == null ||
          !paystackPublicKey.isNotEmpty ||
          paystackPublicKey == '{YOUR_PAYSTACK_PUBLIC_KEY}') {
        throw new UnimplementedError(
            'Please set a public key before running the sample');
      }
      return true;
    }());
  }

  PaymentCard _getCardFromUI() {
    // Using just the must-required parameters.
    return PaymentCard(
      number: cardNumber,
      cvc: cvv,
      expiryMonth: expiryMonth,
      expiryYear: expiryYear,
    );

    // Using Cascade notation (similar to Java's builder pattern)
//    return PaymentCard(
//        number: cardNumber,
//        cvc: cvv,
//        expiryMonth: expiryMonth,
//        expiryYear: expiryYear)
//      ..name = 'Segun Chukwuma Adamu'
//      ..country = 'Nigeria'
//      ..addressLine1 = 'Ikeja, Lagos'
//      ..addressPostalCode = '100001';

    // Using optional parameters
//    return PaymentCard(
//        number: cardNumber,
//        cvc: cvv,
//        expiryMonth: expiryMonth,
//        expiryYear: expiryYear,
//        name: 'Ismail Adebola Emeka',
//        addressCountry: 'Nigeria',
//        addressLine1: '90, Nnebisi Road, Asaba, Deleta State');
  }

  String _getReference() {
    String platform;
    if (Platform.isIOS) {
      platform = 'iOS';
    } else {
      platform = 'Android';
    }

    return 'ChargedFrom${platform}_${DateTime
        .now()
        .millisecondsSinceEpoch}';
  }

  void _fetchAccessCodeFrmServer() async {
    String url = '$backendUrl/new-access-code';
    try {
      http.Response response = await http.get(url);
      var body = response.body;
      _charge.accessCode = body;

      _chargeCard();
    } catch (e) {
      setState(() {
        _backendMessage = 'There was a problem getting a new access code form'
            ' the backend: ${e.toString()}';
        _localInProgress = false;
        _remoteInProgress = false;
      });
    }
  }

  void _verifyOnServer() async {
    String url = '$backendUrl/verify/${_transaction.reference}';
    try {
      http.Response response = await http.get(url);
      var body = response.body;

      setState(() {
        _backendMessage = 'Gateway response: $body';
      });
    } catch (e) {
      setState(() {
        _backendMessage = 'There was a problem verifying %s on the backend: '
            '${_transaction.reference} $e';
        _localInProgress = false;
        _remoteInProgress = false;
      });
    }
  }
}

const fieldIsReq = 'Field is required';

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  flutter_paystack: ^0.5.2

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter packages get

Alternatively, your editor might support flutter packages get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:flutter_paystack/flutter_paystack.dart';
  
Version Uploaded Documentation Archive
0.5.2 Aug 9, 2018 Go to the documentation of flutter_paystack 0.5.2 Download flutter_paystack 0.5.2 archive
0.5.1 Aug 9, 2018 Go to the documentation of flutter_paystack 0.5.1 Download flutter_paystack 0.5.1 archive
0.5.0 Jul 30, 2018 Go to the documentation of flutter_paystack 0.5.0 Download flutter_paystack 0.5.0 archive
Popularity:
Describes how popular the package is relative to other packages. [more]
57
Health:
Code health derived from static analysis. [more]
99
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
78
Learn more about scoring.

We analyzed this package on Aug 10, 2018, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.0.0
  • pana: 0.11.8
  • Flutter: 0.5.7

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Suggestions

Package is pre-v1 release.

While there is nothing inherently wrong with versions of 0.*.*, it usually means that the author is still experimenting with the general direction of the API.

Fix lib/src/api/service/api_service.dart.

Analysis of lib/src/api/service/api_service.dart reported 3 hints:

line 30 col 36: 'OK' is deprecated and shouldn't be used.

line 55 col 36: 'OK' is deprecated and shouldn't be used.

line 76 col 36: 'OK' is deprecated and shouldn't be used.

Format lib/src/api/request/charge_request_body.dart.

Run flutter format to format lib/src/api/request/charge_request_body.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0-dev.58.0 <3.0.0
flutter 0.0.0
http ^0.11.3+16 0.11.3+17
meta ^1.1.5 1.1.5 1.1.6
Transitive dependencies
async 2.0.8
charcode 1.1.2
collection 1.14.6 1.14.11
http_parser 3.1.3
path 1.6.2
sky_engine 0.0.99
source_span 1.4.1
string_scanner 1.0.3
typed_data 1.1.5 1.1.6
vector_math 2.0.6 2.0.8