Integrating checkout for DADI Web

There’s a wealth of payment APIs on the market, commonly with support for the majority of programming languages and frameworks.

🔗Preface

There’s a wealth of payment APIs on the market, commonly with support for the majority of programming languages and frameworks. DADI Web makes it particularly easy to integrate third-party services, and by leveraging the power of DADI Web’s Event System, I’ll show you how your webshop, booking system or cat walking business can start taking payments today.

For this tutorial I’ve chosen to show you how to integrate the popular payment service Stripe, however the logic should be easily transferable to a different service of your choosing.

🔗Outcome

By the end of this tutorial, you’ll have a fully functioning HTML payment form that accepts card payment details. The form will save a new customer, and their card details, to your Stripe account.

🔗Requirements

This article assumes that:

  • You have DADI Web installed. If you don’t, or you want a quick refresh, head over to the lovingly crafted first install guide penned by our Design Director, David Longworth.
  • You have an active Stripe account. If you don’t, register here.
  • You have your Stripe secret key ready. If you don’t, read more about API Keys here.

🔗Using in production

Whilst this tutorial covers all of the app requirements for handling payments, there are some additional security requirements to consider when using in a production environment.

You should always use TLS and make sure you’re performing a PCI compliance test annually. More information can be found in Stripe’s security documentation.

🔗Let’s go!

We’ll need a page where our users can pay for their service. It will contain a payment form with a button that, when submitted, sends the payment details to Stripe.

🔗Creating the page

Add a file to workspace/pages to begin creating the payment page. For full documentation about pages in Web, see https://docs.dadi.cloud/web#adding-pages.

workspace/pages/payment.json

{
  "page": {
    "name": "Payment",
    "description": "Honestly, it's for a good cause",
    "language": "en"
  },
  "routes": [
    {
      "path": "/payment"
    }
  ]
}

The above page specification tells Web that this page should be loaded for requests to /payment. If you like, read more information about Routing in Web.

Now that we have a page specification file, we need a template to display the HTML.

I’ve chosen the Dust.js templating language, but feel free to choose a different template engine (or build your own!).

Create a new Dust.js file in workspace/pages which contains a form with input fields to capture the card details, and a submit button to post the form.

workspace/pages/payment.dust

<form action="" method="POST">
  <input name="email" type="email" placeholder="Email" required />
  <input name="number" placeholder="Card Number" type="number" />
  <input name="cvc" placeholder="CVC" type="number" />
  <select name="exp_month">
    <option disabled selected>Expiry Month</option>
    {! Months !}
  </select>
  <select name="exp_year">
    <option disabled selected>Expiry Year</option>
    {! Years !}
  </select>
  <h3>Amount</h3>
  <span>£<input name="amount" type="number" placeholder="Amount" required value="5.00" /></span>
  <input type="submit" value="Pay me!" />
</form>

You’ll see that the date inputs aren’t populated with options. We could do this manually, but this is a good opportunity to use Web’s Preload Events to generate data for us.

🔗Card dates event

The payment form we’re building asks the customer to enter the valid from month and year, and to make each respective dropdown menu readable and dynamic in our page template, we’re going to create a Preload Event to generate the next 30 years worth of possible months and years.

Add a file to workspace/events called cardDates.js:

workspace/events/cardDates.js

/**
 * Generate an array of years and an array of months for
 * use in the frontend form
 */
const Event = function (req, res, data, callback) {
  const MONTHS_TO_GENERATE = 12
  const YEARS_TO_GENERATE = 30
  const currentYear = new Date().getFullYear()
  let years = []
  let months = []

  // produces an array similar to the following
  // [ '2017', '2018', '2019', '2020', '2021', '2022'... ]
  for (let year = 0; year <= YEARS_TO_GENERATE; year++) {
    years.push((currentYear + year).toString())
  }

  // produces the following array 
  // [ '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12' ]
  for (let month = 1; month <= MONTHS_TO_GENERATE; month++) {
    months.push(month < 10 ? `0${month}` : month.toString())
  }

  data.years = years
  data.months = months

  callback(null)
}

module.exports = function (req, res, data, callback) {
  return new Event(req, res, data, callback)
}

To tell Web about this event, edit the page specification file to add a “preloadEvents” section:

{
  "page": {
    "name": "Payment",
    "description": "Honestly, it's for a good cause",
    "language": "en"
  },
  "routes": [
    {
      "path": "/payment"
    }
  ],
  "preloadEvents": [
    "cardDates"
  ]
}

Now when we load the page it will contain an array of months and years in the data context, so we can iterate over these preloaded values in the form:

<select name="exp_month">
  <option disabled selected>Expiry Month</option>
  {#months}
    <option value="{.}">{.}</option>
  {/months}
</select>
<select name="exp_year">
  <option disabled selected>Expiry Year</option>
  {#years}
    <option value="{.}">{.}</option>
  {/years}
</select>

🔗Adding feedback to the page

Finally, let’s add some error output to the page template so we can let the customer know when something’s gone wrong:

{?err}
  <mark>{err.message}</mark>
{/err}

🔗Check your progress

If you start the app with npm start and visit the /payment route in your browser, you should see the form rendered like this:

Screenshot of the credit card fieldsScreenshot of the credit card fields

🔗Interacting with Stripe

In order to actually send the payment details to Stripe, we need to have a way to process the form that the customer submits. We’ll do this using an event attached the the payment page that is run when the form is submitted.

🔗Creating the Event

First let’s install the Stripe dependency we’ll be using in our payment event. Inside your Web application directory, run the following:

npm install stripe --save

Event files are placed in the directory workspace/events (unless you’ve changed the default configuration). You’ve already added the cardDates event here, so this is nothing new. Create a new file called payments.js:

workspace/events/payments.js

// require the Stripe dependency and pass it our API key
const stripe = require('stripe')('sk_test_BQokikJOvBiI2HlWgH4olfQ2')

const Event = function (req, res, data, callback) {
  callback()
}

module.exports = function (req, res, data, callback) {
  return new Event(req, res, data, callback)
}

module.exports.Event = Event

This is really as basic as events get, but it doesn’t do anything yet. Stripe requires that a customer is created with at least one card before any payments can be made (see the Stripe documentation). In the event we’ll create two extra methods to handle this, as well as a method to actually charge the customer’s card. Place the following code at the end of the payments.js file.

Note: the Stripe package allows you to use either regular callbacks or Promises, in this post we’ll be using Promises

workspace/events/payments.js

/**
 * Create a customer record using the authenticated Stripe account
 * https://stripe.com/docs/api/node#create_customer
 * 
 * @param  {String} email - the email to register to the customer record.
 * @return {Promise} Stripe API action
 */
function createCustomer (email) {
  return stripe.customers.create({
    email: email
  })
}

/**
 * Create a customer card object
 * https://stripe.com/docs/api/node#create_card 
 * 
 * @param  {String} cust_id - the Stripe customer ID
 * @param  {Number} exp_month - the payment card expiry month
 * @param  {Number} exp_year - the payment card expiry year
 * @param  {Number} number - the 3-4 digit Card Verification Code
 * @param  {String} object - the source creation type
 * @return {Promise} Stripe API action          
 */
function createCustomerCard (cust_id, exp_month, exp_year, number, cvc, object) {
  return stripe.customers.createSource(cust_id, {
    source: {
      object: object || 'card',
      exp_month: exp_month,
      exp_year: exp_year,
      number: number,
      cvc: cvc
    }
  })
}

/**
 * Charge a customer's card
 * https://stripe.com/docs/api/node#create_charge
 * 
 * @param  {String} customer - the Stripe customer ID
 * @param  {String} card - the ID of the customer's card object
 * @param  {String} amount - the amount to charge to the card
 * @param  {String} currency - the currency to charge the card in
 * @return {Promise} Stripe API action          
 */
function chargeCustomerCard (customer, card, amount, currency) {
  return stripe.charges.create({
    customer: customer,
    card: card,
    amount: Number(amount.replace('.', '')),
    currency: currency || 'gbp'
  })
}

🔗Event form action

The event still doesn’t do anything, but now we have the basics we can hook it up to the page so that it is executed when the form is submitted.

Adding the event to the page by specifying it in an “events” block in the page specification file:

"events": [
  "payments"
]

It should now look like this:

workspace/pages/payment.json

{
  "page": {
    "name": "Payment",
    "description": "Honestly, it's for a good cause",
    "language": "en"
  },
  "routes": [
    {
      "path": "/payment"
    }
  ],
  "preloadEvents": [
    "cardDates"
  ],
  "events": [
    "payments"
  ]
}

Now that the event is hooked up to the page, it will fire whenever the page is loaded. Because events will fire on both POST and GET requests, we add a check to the beginning of the event to ensure it is a POST operation and then use the contents of the req.body property to call the functions we created earlier.

The full event code should look like this:

workspace/events/payments.js

const stripe = require('stripe')('sk_test_BQokikJOvBiI2HlWgH4olfQ2')

const Event = function (req, res, data, callback) {
  // not a POST, no need to run the rest of the event
  if (req.method && req.method.toLowerCase() !== 'post') return callback()

  // get the email, card details and charge amount from the request body
  const requestData = req.body

  createCustomer(requestData.email).then(customer => {
    return createCustomerCard(
      customer.id,
      requestData.exp_month,
      requestData.exp_year,
      requestData.number,
      requestData.cvc
    ).then(card => {
      return chargeCustomerCard(customer.id, card.id, requestData.amount)
    }).then(charge => {
      // Success! lets set a value we can display to the user
      // Remember that `data` is the context that is passed to the page template
      // for rendering
      data.success = {
        message: charge.outcome.seller_message
      }

      callback(null)
    }).catch(err => {
      data.err = err
      callback(null)
    })
  })
}

module.exports = function (req, res, data, callback) {
    return new Event(req, res, data, callback)
}

/**
 * Create a customer record using the authenticated Stripe account
 * https://stripe.com/docs/api/node#create_customer
 * 
 * @param  {String} email - the email to register to the customer record.
 * @return {Promise} Stripe API action
 */
function createCustomer (email) {
  return stripe.customers.create({
    email: email
  })
}

/**
 * Create a customer card object
 * https://stripe.com/docs/api/node#create_card 
 * 
 * @param  {String} cust_id - the Stripe customer ID
 * @param  {Number} exp_month - the payment card expiry month
 * @param  {Number} exp_year - the payment card expiry year
 * @param  {Number} number - the 3-4 digit Card Verification Code
 * @param  {String} object - the source creation type
 * @return {Promise} Stripe API action          
 */
function createCustomerCard (cust_id, exp_month, exp_year, number, cvc, object) {
  return stripe.customers.createSource(cust_id, {
    source: {
      object: object || 'card',
      exp_month: exp_month,
      exp_year: exp_year,
      number: number,
      cvc: cvc
    }
  })
}

/**
 * Charge a customer's card
 * https://stripe.com/docs/api/node#create_charge
 * 
 * @param  {String} customer - the Stripe customer ID
 * @param  {String} card - the ID of the customer's card object
 * @param  {String} amount - the amount to charge to the card
 * @param  {String} currency - the currency to charge the card in
 * @return {Promise} Stripe API action          
 */
function chargeCustomerCard (customer, card, amount, currency) {
  return stripe.charges.create({
    customer: customer,
    card: card,
    amount: Number(amount.replace('.', '')),
    currency: currency || 'gbp'
  })
}

module.exports.Event = Event

A couple of things to mention here. Firstly, I’ve added a default currency, but this could be changed to your preferred currency, determined by some clever geolocation or selected by the user from the HTML form.

Secondly, Stripe requires ‘amount’ to be represented as the lowest common denominator of the selected currency, so £30.00 would be 3000. You’ll notice that we’re removing the decimal place from ‘amount’ for this reason.

🔗Add payment feedback

We should really let the customer know how it went. As we’re only setting the success property in the data context if the charge was successful, we can use this property to decide what to render in the template. Change your template to look like the following. You can see that if the success property exists, then we only show the message, otherwise we show an error message and redisplay the form:

workspace/pages/payment.dust

<main>
  <section>
    {?success}
      <h2>{success.message|s}</h2>
    {:else}
      {?err}
        <mark>{err.message}</mark>
      {/err}

      <h3>Payment</h3>
      <form action="" method="POST">
        <input name="email" type="email" placeholder="Email" required />
        <input name="number" placeholder="Card Number" type="number" />
        <input name="cvc" placeholder="CVC" type="number" />
        <select name="exp_month">
          <option disabled selected>Expiry Month</option>
          {#months}
            <option value="{.}">{.}</option>
          {/months}
        </select>
        <select name="exp_year">
          <option disabled selected>Expiry Year</option>
          {#years}
            <option value="{.}">{.}</option>
          {/years}
        </select>
        <h3>Amount</h3>
        <span>£<input name="amount" type="number" placeholder="Amount" required value="5.00" /></span>
        <input type="submit" value="Pay me!" />
      </form>
    {/success}
  </section>
</main>

🔗Test

Let’s test it! In order to test, you’ll need some test card details:

  • Card number: 4242424242424242
  • CVC: any 3 digit CVC
  • Expiry: any future expiry date

If your payment is successful, you’ll see something like this:

The payment complete viewThe payment complete view

🔗And that’s a wrap!

If you want to take your cart to the next steps, let me know and I’ll happily write more. Some ideas:

  • Store user data in session.
  • Retrieve and reuse existing cards.
  • Subscribe users to a payment plan.

You can ask me here on the forum, on Discord or tweet @dadi.

Related articles

More tutorials articles
Mail icon

Like what you are reading? Want to earn tokens by becoming a DADI Node? Save money on cloud computing services? Build amazing digital product with DADI Web Services? Join our mailing list.

To hear about our news, events and products or services subscribe now. You can also indicate which services you are interested in, which we use for research and to inform the content that we send generally.

* You can unsubscribe at any time by emailing us at data@dadi.cloud or by clicking on the unsubscribe link which can be found in our emails to you. Read our Privacy Policy.