/ nodejs

Comparing the UX of three payment processors and theirs NodeJS SDK (Paypal vs Braintree vs Stripe)

Overview

This Blog post compares the payment experience from three popular payment processors: PayPal, Braintree and Stripe. This comparison is from the developer's point of view. This is NOT to compare the prices and services. We will only concern the payment experience and coding effort. We will do:

  • Backend codes in NodeJS-Express.
  • Frontend codes in Pug, jQuery, Bootstrap 4.
  • Whole payment process flow and its appearence.
  • Compare the three SDKs and evaluate the security.

Please note that all payment processes are in Sandbox (development) platform.

The source codes are host in this Github repo.

Project setup

First of all you'll need NodeJS. The version is:

node -v && npm -v
v6.10.3
5.4.2

Enter your payment access keys (PayPal, Braintree & Stripe) into the .env file. The content is:

PAYPAL_MODE='[sandbox|production]'
PAYPAL_CLIENT_ID='[Your PayPal Client Id]'
PAYPAL_CLIENT_SECRET='[Your PayPal Client Secret]'

BT_ENVIRONMENT='[Sandbox|Production]'
BT_MERCHANT_ID='[Your Braintree Merchant ID]'
BT_PUBLIC_KEY='[Your Braintree public key]'
BT_PRIVATE_KEY='[Your Braintree private key]'

STRIPE_PUBLISH_KEY='[Your Stripe pubishable key]'
STRIPE_SECRET_KEY='[Your Stripe secret key]'

Download the source codes Zip file from Github repo in your PC. Unzip it then install the project:

npm install

Start the server:

npm start

Browse the project home page http://localhost:3000. You'll see the home page:
homepage

Let's get started on payment experience...

1. PayPal Payment Experience

Paypal is the longest history in online money transfer platform. It was founded in 1999 and firstly released NodeJS SDK in 2013.

Below are the screenshots of PayPal payment experience:

Whole Payment Processes

Screen 1: Input amount webpage. This webpage serves from our server.
paypal1-1

Screen 2: Login as PayPal user. This webpage has been redirected to PayPal.
paypal2

Screen 3: Review the transaction. This webpage is still handling in PayPal.
paypal3

Screen 4: Payment success. The webpage redirects back to our server.
paypal4

Frontend Codes View

In source code file /views/paypal/home.pug (in Pug syntax). The content is:

form(action="/paypal/submit" method="POST")
	.form-group
		label(for="amount") Amount to pay in USD (1 - 9, default 1)
		input(id="amount" class="form-control col-4" type="number" name="amount" value="1" min="1" max="9")
	.form-group
		label(for="description") Payment description
		input#description.form-control(type="text" name="description" maxlength="100" placeholder="Optional")
	button(class="btn btn-primary btn-lg" type="submit") Pay with PayPal

Backend Codes View

In file /routes/paypal.js:

router.post('/submit', (req, res) => {
	let description = req.body.description ? req.body.description : 'This is the payment description'
	let amount = req.body.amount
	let host = req.protocol + '://' + req.get('host')
	let createPaymentJson = {
		intent: "sale", // authorize
		payer: {
			payment_method: "paypal"
		},
		redirect_urls: {
			return_url: host + '/paypal/payment-return',
			cancel_url: host + '/paypal/payment-cancel'
		},
		transactions: [{
			item_list: {
				items: [{
					name: "item 1",
					sku: "item_1",
					price: amount,
					currency: "USD",
					quantity: 1
				}]
			},
			amount: {
				currency: "USD",
				total: amount
			},
			description: description
		}]
	}
	// Call PayPal to process the payment
	paypal.payment.create(createPaymentJson, (err, payment) => {
		if (err) {
			logger.error(err.response.error_description)
			throw err
		} else {
			console.log("Create Payment response...")
			console.log(payment)
			let redirectUrl
			payment.links.forEach((link) => {
				if (link.method === 'REDIRECT') {
					redirectUrl = link.href
				}
			})
			if (redirectUrl) {
				res.status(200).redirect(redirectUrl)
			} else {
				logger.error('Cannot find redirect url from paypal payment result!')
			}
		}
	})
})

Returned data from payment gateway

Below is the PayPal result JSON (grab from console outputs):

{ id: 'PAY-3XL45412FD778433ELHKIGDA',
  intent: 'sale',
  state: 'created',
  payer: { payment_method: 'paypal' },
  transactions: 
   [ { amount: [Object],
       description: 'This is the payment description',
       item_list: [Object],
       related_resources: [] } ],
  create_time: '2017-10-04T06:43:24Z',
  links: 
   [ { href: 'https://api.sandbox.paypal.com/v1/payments/payment/PAY-3XL45412FD778433ELHKIGDA',
       rel: 'self',
       method: 'GET' },
     { href: 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-4FB14017EE4066414',
       rel: 'approval_url',
       method: 'REDIRECT' },
     { href: 'https://api.sandbox.paypal.com/v1/payments/payment/PAY-3XL45412FD778433ELHKIGDA/execute',
       rel: 'execute',
       method: 'POST' } ],
  httpStatusCode: 201 }

Payment experence and coding summary:

  • Total 4 webpages interacted.
  • Total 59 lines of source codes (frontend + backend).
  • The results are encoded. No sensitive data can be seen (i.e. card #, password...).
  • PayPal webpages cannot be customized to our style (color, font-size, image...)
  • Must paid by a PayPal member (although it accepts credit card. But it will force to create a PayPal member).
  • My comment: Slow, more codes, just a few customizable items.
  • Score: 2/5

2. Braintree Payment Experience

Braintree was founded in 2007 and has been acquired by PayPal in 2013. Braintree is a one stop shop for business needs to receive payment.

Below are the screenshots of Braintree payment experience.

Whole Payment Processes

Screen 1a: Input amount webpage. This webpage serves from our server.
braintree1

*Screen 1b: Input credit card. This is same webpage as above.
braintree1b

*Screen 2: Payment success. The webpage redirects to our payment success webpage. No need to go to Braintree server.
braintree2

Frontend Codes View

In source code file /views/braintree/home.pug (in Pug syntax):

form#payment-form(action="/braintree/submit", method="post")
    .form-group
    section
        label(for="amount") Amount (1-9, default 1)
        .input-wrapper.amount-wrapper
            input#amount(name="amount" type="tel" min="1" max="9" placeholder="Amount" value="1")
        .bt-drop-in-wrapper
            #bt-dropin
    input#nonce(type="hidden" name="payment_method_nonce")
    button(class="btn btn-primary btn-lg" type="submit") Pay with Braintree
script(src="https://js.braintreegateway.com/web/dropin/1.8.0/js/dropin.min.js")
script.
    'use strict';
    var form = document.querySelector('#payment-form');
    var token = '#{clientToken}';

    braintree.dropin.create({
        authorization: token,
        container: '#bt-dropin',
        paypal: {
            flow: 'vault'
        }
    }, function (createErr, instance) {
        form.addEventListener('submit', function (event) {
            event.preventDefault();

            instance.requestPaymentMethod(function (err, payload) {
                if (err) {
                    console.log('Error', err);
                    return;
                }

                // Add the nonce to the form and submit
                document.querySelector('#nonce').value = payload.nonce;
                form.submit();
            });
        });
    });

    function Demo(config) {
        this.config = config;
        this.config.development = config.development || false;

        this.paymentForm = $('#' + config.formID);
        this.inputs = $('input[type=text], input[type=email], input[type=tel]');
        this.button = this.paymentForm.find('.button');

        this.states = {
            show: 'active',
            wait: 'loading'
        };
        this.focusClass = 'has-focus';
        this.valueClass = 'has-value';

        this.initialize();
    }

    Demo.prototype.initialize = function () {
        var self = this;

        this.events();
        this.inputs.each(function (index, element) {
            self.labelHander($(element));
        });
        this.notify('error');
    };

    Demo.prototype.events = function () {
        var self = this;

        this.inputs.on('focus', function () {
            $(this).closest('label').addClass(self.focusClass);
            self.labelHander($(this));
        }).on('keydown', function () {
            self.labelHander($(this));
        }).on('blur', function () {
            $(this).closest('label').removeClass(self.focusClass);
            self.labelHander($(this));
        });
    };

    Demo.prototype.labelHander = function (element) {
        var self = this;
        var input = element;
        var label = input.closest('label');

        window.setTimeout(function () {
            var hasValue = input.val().length > 0;

            if (hasValue) {
                label.addClass(self.valueClass);
            } else {
                label.removeClass(self.valueClass);
            }
        }, 10);
    };

    Demo.prototype.notify = function (status) {
        var self = this;
        var notice = $('.notice-' + status);
        var delay = this.config.development === true ? 4000 : 2000;

        notice.show();

        window.setTimeout(function () {
            notice.addClass('show');
            self.button.removeClass(self.states.wait);

            window.setTimeout(function () {
                notice.removeClass('show');
                window.setTimeout(function () {
                    notice.hide();
                }, 310);
            }, delay);
        }, 10);
    };

    var checkout = new Demo({
        formID: 'payment-form'
    });

Backend Codes View

In file /routes/braintree.js:

router.post('/submit', (req, res) => {
	let transactionErrors
	let amount = req.body.amount
	let nonce = req.body.payment_method_nonce

	gateway.transaction.sale({
		amount: amount,
		paymentMethodNonce: nonce,
		options: {
			submitForSettlement: true
		}
	}, (err, result) => {
		console.log(result)
		if (result.success) {
			res.redirect('payment-success/' + result.transaction.id)
		} else {
			logger.error(result.message)
			res.redirect('payment-failure?err_msg=' + result.message)
		}
	})
})

Returned data from payment gateway

Below is the Braintree result JSON (grab from console outputs):

{ transaction: 
   Transaction {
     id: '05r7vqbw',
     status: 'submitted_for_settlement',
     type: 'sale',
     currencyIsoCode: 'USD',
     amount: '1.00',
     merchantAccountId: 'advancedlogicsystemlab',
     subMerchantAccountId: null,
     masterMerchantAccountId: null,
     orderId: null,
     createdAt: '2017-10-04T07:40:53Z',
     updatedAt: '2017-10-04T07:40:53Z',
     customer: ...
     billing: ...
     ...
     creditCard: 
      CreditCard {
        token: null,
        bin: '411111',
        last4: '1111',
        cardType: 'Visa',
        expirationMonth: '01',
        expirationYear: '2029',
        customerLocation: 'US',
        cardholderName: null,
        imageUrl: 'https://assets.braintreegateway.com/payment_method_logo/visa.png?environment=sandbox',
        prepaid: 'Unknown',
        healthcare: 'Unknown',
        debit: 'Unknown',
        durbinRegulated: 'Unknown',
        commercial: 'Unknown',
        payroll: 'Unknown',
        issuingBank: 'Unknown',
        countryOfIssuance: 'Unknown',
        productId: 'Unknown',
        uniqueNumberIdentifier: null,
        venmoSdk: false,
        maskedNumber: '411111******1111',
        expirationDate: '01/2029' },
     statusHistory: [ [Object], [Object] ],
     planId: null,
     subscriptionId: null,
     subscription: { billingPeriodEndDate: null, billingPeriodStartDate: null },
...
  success: true }

Payment Experence and Coding Summary

  • Total 2 webpages interacted.
  • Total 119 lines of source codes (frontend + backend).
  • The credit card number is masked. No sensitive data (e.g. expiry date, CVC...)
  • Communicates with Braintree at server-side.
  • No redirect to Braintree server. The whole webpages can be customized (except credit card input element)
  • Can be paid by credit card directly (or PayPal member).
  • My comment: Faster, smooth, highly customizable frontend but much more frontend codes.
  • Score: 4/5

3. Stripe Payment Experience

Stripe was founded in 2010. Stripe not only supports credit card payment but also bitcoin and Alipay. Stripe was ranked no. 4 on Forbes magazine Cloud 100 in 2016.

Below are the screenshots of Stripe payment experience.

Whole Payment Processes

Unlike other processors, Stripe offers three different integration levels. Different level has different payment experience.

Screen 1a: Minimal Checkout (Simple checkout).
stripe1

Screen 1b: Custom Checkout.
stripe2

Screen 1c: Inline element.
stripe3

*Screen 2: Payment success. All webpages do not redirect to Stripe.
stripe4

Frontend Codes View

Different integration level has differnt code lines. Source codes of 1a (minimal checkout) cover in file /views/stripe/home.pug (in Pug syntax):

form(action="/stripe/charge" method="POST")
    //- Stripe ref: https://stripe.com/docs/checkout
    p <i>Below button is generated by Stripe</i>
    script(src="https://checkout.stripe.com/checkout.js" class="stripe-button" data-key=accessKey data-email="go@simonho.net" data-image="https://www.simonho.net/MyImg150x150.jpg" data-name="simonho288" data-description="Test Payment 1" data-amount=amount data-label="Pay with Card Now" data-locale="en-US")

Source codes for 1b (custom checkout) cover in file /views/stripe/home.pug:

a#pay(class="btn btn-warning" href="") Pay with Card now
...
var checkout = StripeCheckout.configure({
    key: '#{accessKey}',
    token: function(token) {
        window.location.replace('/stripe/charge/' + token.id)
    },
    image: 'https://www.simonho.net/MyImg150x150.jpg',
    locale: 'en-US'
});
// Open Checkout when the link is clicked
$('#pay').on('click', function(evt) {
    checkout.open({
        name: 'simonho288',
        email: 'go@simonho.net',
        description: 'Test Payment 2',
        currency: 'usd',
        amount: 100
    });
    evt.preventDefault();
});
// Close Checkout on page navigation:
window.addEventListener('popstate', function() {
    checkout.close();
});

Source codes for 1c (inline element) cover in file /views/stripe/home.pug:

#card-element
    //- a Stripe Element will be inserted here.
div(id="card-errors" role="alert")
div(id="my-card-error-alert" class="alert alert-danger" role="alert" style="display:none")
    h4.alert-heading Payment Failure
    p#error-message 
    hr
    p.mb-0 Please use another card and retry payment
...
script(src="https://js.stripe.com/v3/")
script.
    $(document).ready(function() {
        // Create a Stripe client
        var stripe = Stripe('#{accessKey}');
        // Create an instance of Elements
        var elements = stripe.elements({
            locale: 'en-US'
        });

        // Custom styling can be passed to options when creating an Element.
        // (Note that this demo uses a wider set of styles than the guide below.)
        var style = {
            base: {
                color: '#32325d',
                lineHeight: '24px',
                fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
                fontSmoothing: 'antialiased',
                fontSize: '16px',
                '::placeholder': {
                    color: '#aab7c4'
                }
            },
            invalid: {
                color: '#fa755a',
                iconColor: '#fa755a'
            }
        };

        // Create an instance of the card Element
        var card = elements.create('card', {
            style: style,
            hidePostalCode: true
        });

        // Add an instance of the card Element into the `card-element` <div>
        card.mount('#card-element');

        // Handle real-time validation errors from the card Element.
        card.addEventListener('change', function(event) {
            var displayError = document.getElementById('card-errors');
            if (event.error) {
                displayError.textContent = event.error.message;
            } else {
                displayError.textContent = '';
            }
        });

        // Handle form submission
        var form = document.getElementById('payment-form');
        form.addEventListener('submit', function(event) {
            event.preventDefault();

            $('#submit').html('<i class="fa fa-spinner fa-spin"></i>&nbsp;Processing...')
            $('#submit').attr('disabled', true)

            stripe.createToken(card).then(function(result) {
                if (result.error) {
                    // Inform the user if there was an error
                    var errorElement = document.getElementById('card-errors');
                    errorElement.textContent = result.error.message;
                } else {
                    // Send the token to your server
                    stripeTokenHandler(result.token);
                }
            });

            return false;
        });

        function stripeTokenHandler(token) {
            window.location.replace('/stripe/charge/' + token.id);
        }
    });

Backend Codes View

In file /routes/stripe.js:

router.get('/charge/:token', (req, res) => {
  let token = req.params.token
  console.assert(token)
  const amount = CHARGE_AMOUNT * 100
  stripe.charges.create({
    amount: amount,
    currency: 'usd',
    source: token,
    description: 'Stripe experiment testing charge'
  }, (err, charge) => {
    if (err) {
      res.redirect('/stripe/payment-failure?err_msg=' + err.message)
    } else {
			console.log('charge', charge)
			if (charge.outcome && charge.outcome.risk_level != 'normal') {
	      res.redirect('/stripe/payment-warning?charge_id=' + charge.id + '&msg=' + charge.outcome.seller_message)
			} else {
	      res.redirect('/stripe/payment-success/' + charge.id)
			}
    }
  })
})

router.post('/charge', (req, res) => {
  let token = req.body.stripeToken
  console.assert(token)
  const amount = CHARGE_AMOUNT * 100
  stripe.charges.create({
    amount: amount,
    currency: 'usd',
    source: token,
    description: 'Stripe experiment testing charge'
  }, (err, charge) => {
    if (err) {
      res.redirect('/stripe/payment-failure?err_msg=' + err.message)
    } else {
      console.log('Charged successful')
			console.log('charge', charge)
      res.redirect('/stripe/payment-success/' + charge.id)
    }
  })
})

Returned data from payment gateway

Below is the Stripe result JSON (grab from console outputs):

charge { id: 'ch_1B99OXAgSkpNTk4VheTM41y2',
  object: 'charge',
  amount: 100,
  amount_refunded: 0,
  application: null,
  application_fee: null,
  balance_transaction: 'txn_1B99OYAgSkpNTk4VOaMF6dWa',
  captured: true,
  created: 1507110145,
  currency: 'usd',
  customer: null,
  description: 'Stripe experiment testing charge',
...
  source: 
   { id: 'card_1B99OWAgSkpNTk4V73FxHfrd',
     object: 'card',
     brand: 'Visa',
     country: 'US',
     customer: null,
     cvc_check: 'pass',
     dynamic_last4: null,
     exp_month: 1,
     exp_year: 2029,
     fingerprint: 'J9zORsE3yTZpw06p',
     funding: 'credit',
     last4: '4242',
     metadata: {},
     name: null,
     tokenization_method: null },
...
  transfer_group: null }

Payment Experence and Coding Summary

  • Total 2 webpages interacted.
  • Line of codes: Three level are: 1a = 22 lines, 1b = 43 lines, 1c = 93 lines respectively.
  • In returned data, the credit card number has only last 4 digits. Expiry date does contain in result but CVC does not.
  • Communicates with Stripe at client & server side.
  • No redirect to Stripe server. The webpages can be customized for all 3 integration levels. More flexible than others.
  • Can be paid by credit card directly. No registered member requires.
  • My comments: Faster, smooth, choice of integration level has different experience, highly customizable frontend, fewer codes.
  • Score: 5/5

Conclusion

As a developer, I will choose Stripe for my next project. The most attractive is choice of integration. Inline element integration can fully embedded into the card input form. At consumer point of view, the card entries seem to be same as my webpage. It allows me to do 100% UX design.

Source codes of the project hosting in this Github repo.