Commerce.js Vue.js Checkout

Commerce.js Vue.js Checkout

This is a guide on adding checkout order capture functionality to our Vue.js application using Commerce.js. This is a continuation from the previous guide on implementing cart functionality.

Overview

The aim for this guide is to create a checkout page to capture our cart items into an order as well and add a confirmation page to display a successful order. Below outlines what this guide will achieve:

  1. Add page routing to the application
  2. Generate a checkout token to capture the order
  3. Create a checkout page with a form
  4. Create a confirmation page to display an order reference

Requirements

What you will need to start this project:

  • An IDE or code editor
  • NodeJS, at least v8/10
  • npm or yarn
  • Vue.js devtools (recommended)

Prerequisites

This project assumes you have some knowledge of the below concepts before starting:

  • Basic knowledge of JavaScript
  • Some knowledge of Vue.js
  • An idea of the JAMstack architecture and how APIs work

Some things to note:

  • The purpose of this guide is to focus on the Commerce.js layer and using Vue.js to build out the application therefore, we will not be going over any styling details.
  • The checkout application code is available in the GitHub repo along with all styling details.

Checkout

1. Set up routing

For fully functional single application page to scale, you will need to add routing in order to navigate to various view pages such to a cart or checkout flow. Let’s jump right back to where we left off from the previous cart guide and add VueRouter, the official routing library for Vue applications, to our project.

yarn add vue-router
# OR
npm i vue-router

After add the package, create a new folder called router in src with an index.js file. In this file is where we will define our routes. First, we’ll need to import in vue-router and explicitly install VueRouter with Vue.use(VueRouter).

Next, we define the routes we want to specify as views with the properties path, name, and component. This tells Vue to render the specified components in view at the URL paths.

// router/index.js

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const routes = [
    {
        path: '/',
        name: 'ProductsList',
        component: () => import('../components/ProductsList.vue')
    },
    {
        path: '/checkout',
        name: 'Checkout',
        component: () => import('../pages/Checkout.vue')
    },
    {
        path: '/confirmation',
        name: 'Confirmation',
        component: () => import('../pages/Confirmation.vue')
    },
]

const router = new VueRouter({ routes, mode: 'history' });

export default router;

You can see above we intend to refactor the ProductsList component into a router-view. While we are at it, we will also add routing to a checkout and confirmation page which we will get to creating in the guide. We then instantiate a router instance and pass our defined route options to it. Here are some other methods to adding routing to your Vue application depending on the use case.

Lastly, to have access to this.$router in the application’s components, we inject the router option when the application mounts.

import router from './router';

new Vue({
  router,
  render: h => h(App),
}).$mount('#app');

Now with all the routes defined, we can refactor our ProductsList render in App.vue to use router-view and add our first routing to navigate to a checkout page from the cart component.

<router-view
  :products="products"
  @add-to-cart="handleAddToCart"
/>

Let’s then go in to Cart.vue and add a second button in the cart footer with a router link to push into a checkout view component that we will be adding later on.

<div class="cart__footer">
  <router-link
    v-if="cart.line_items.length"
    class="cart__btn-checkout"
    to="/checkout"
  >
      Checkout
  </router-link>
</div>

The to prop in the router-link instance will look for a matching named route and push the Checkout component into view.

Let’s now divert back to our App.vue where we will need to generate a checkout token before we move on any further.

2. Generate a checkout token

Commerce.js provides a powerful method commerce.checkout.generateToken() to capture all the data we need from the cart to initiate the checkout process simply by providing the cart ID in session, a product ID, or a product’s permalink as an argument.

In App.vue, let’s first initialize a checkoutToken in the data property and then create a helper function generateCheckoutToken() to generate the checkout token we need in order to capture our checkout.

// App.vue

data: {
  return {
    checkoutToken: null,
  };
},

/**
 * Generates a checkout token
 * https://commercejs.com/docs/sdk/checkout#generate-token
 */
generateCheckoutToken() {
  this.$commerce.checkout.generateToken( this.cart.id, { type: 'cart' } ).then((token) => {
    this.checkoutToken = token;
  }).catch((error) => {
    console.log('There was an error in generating a token', error);
  });
},

The commerce.checkout.generateToken() takes in our cart ID and the identifier type cart. The type property is an optional parameter you can pass in as an identifier, in this case cart the type associated to this.cart.id.

To call this function, we could include it in the mounted lifecyle hook to generate a token when the component mounts or we could execute this function only when the cart prop changes. To do so, we can utilize the watch property to watch for prop changes in the cart object, in which case, checking whether line_items in cart exists. The latter would be a more elegant way to handle the execution of generateCheckoutToken().

Upon a successful request to generate the checkout token, you should receive an abbreviated response like the below json data:

{
  "id": "chkt_J5aYJ8zBG7dM95",
  "cart_id": "cart_ywMy2OE8zO7Dbw",
  "created": 1600411250,
  "expires": 1601016050,
  "analytics": {
    "google": {
      "settings": {
        "tracking_id": null,
        "linked_domains": null
      }
    }
  },
  "line_items": [
    {
      "id": "item_7RyWOwmK5nEa2V",
      "product_id": "prod_NqKE50BR4wdgBL",
      "name": "Kettle",
      "image": "https://cdn.chec.io/merchants/18462/images/676785cedc85f69ab27c42c307af5dec30120ab75f03a9889ab29|u9 1.png",
      "sku": null,
      "description": "<p>Black stove-top kettle</p>",
      "quantity": 1,
      "price": {
        "raw": 45.5,
        "formatted": "45.50",
        "formatted_with_symbol": "$45.50",
        "formatted_with_code": "45.50 USD"
      },
      "subtotal": {
        "raw": 45.5,
        "formatted": "45.50",
        "formatted_with_symbol": "$45.50",
        "formatted_with_code": "45.50 USD"
      },
      "variants": [],
      "conditionals": {
        "is_active": true,
        "is_free": false,
        "is_tax_exempt": false,
        "is_pay_what_you_want": false,
        "is_quantity_limited": false,
        "is_sold_out": false,
        "has_digital_delivery": false,
        "has_physical_delivery": false,
        "has_images": true,
        "has_video": false,
        "has_rich_embed": false,
        "collects_fullname": false,
        "collects_shipping_address": false,
        "collects_billing_address": false,
        "collects_extrafields": false
      },
    }
  ],
  "shipping_methods": [],
  "live": {
    "merchant_id": 18462,
    "currency": {
      "code": "USD",
      "symbol": "$"
    },
    "line_items": [
      {
        "id": "item_7RyWOwmK5nEa2V",
        "product_id": "prod_NqKE50BR4wdgBL",
        "product_name": "Kettle",
        "type": "standard",
        "sku": null,
        "quantity": 1,
        "price": {
          "raw": 45.5,
          "formatted": "45.50",
          "formatted_with_symbol": "$45.50",
          "formatted_with_code": "45.50 USD"
        },
        "line_total": {
          "raw": 45.5,
          "formatted": "45.50",
          "formatted_with_symbol": "$45.50",
          "formatted_with_code": "45.50 USD"
        },
        "variants": [],
        "tax": {
          "is_taxable": false,
          "taxable_amount": null,
          "amount": null,
          "breakdown": null
        }
      },
      {
        "id": "item_1ypbroE658n4ea",
        "product_id": "prod_kpnNwAMNZwmXB3",
        "product_name": "Book",
        "type": "standard",
        "sku": null,
        "quantity": 1,
        "price": {
          "raw": 13.5,
          "formatted": "13.50",
          "formatted_with_symbol": "$13.50",
          "formatted_with_code": "13.50 USD"
        },
        "line_total": {
          "raw": 13.5,
          "formatted": "13.50",
          "formatted_with_symbol": "$13.50",
          "formatted_with_code": "13.50 USD"
        },
        "variants": [],
        "tax": {
          "is_taxable": false,
          "taxable_amount": null,
          "amount": null,
          "breakdown": null
        }
      }
    ],
    "subtotal": {
      "raw": 59,
      "formatted": "59.00",
      "formatted_with_symbol": "$59.00",
      "formatted_with_code": "59.00 USD"
    },
    "discount": [],
    "shipping": {
      "available_options": [],
      "price": {
        "raw": 0,
        "formatted": "0.00",
        "formatted_with_symbol": "$0.00",
        "formatted_with_code": "0.00 USD"
      }
    },
    "tax": {
      "amount": {
        "raw": 0,
        "formatted": "0.00",
        "formatted_with_symbol": "$0.00",
        "formatted_with_code": "0.00 USD"
      }
    },
    "total": {
      "raw": 59,
      "formatted": "59.00",
      "formatted_with_symbol": "$59.00",
      "formatted_with_code": "59.00 USD"
    },
    "total_with_tax": {
      "raw": 59,
      "formatted": "59.00",
      "formatted_with_symbol": "$59.00",
      "formatted_with_code": "59.00 USD"
    },
    "giftcard": [],
    "total_due": {
      "raw": 59,
      "formatted": "59.00",
      "formatted_with_symbol": "$59.00",
      "formatted_with_code": "59.00 USD"
    },
    "pay_what_you_want": {
      "enabled": false,
      "minimum": null,
      "customer_set_price": null
    },
    "future_charges": []
  }
}

With the verbose data that the generateCheckoutToken() returns, we now have a checkout token object which contains everything we need to create the checkout page.

3. Create checkout page

Earlier on in step 1, we created the appropriate route options to navigate to a checkout page. We will now need to create that view page to render out when the router pushes to a /checkout path.

First, let’s create a new folder src/pages and a Checkout.vue page component. This page component is going to get real hefty quite fast, but we’ll break it down in chunks throughout the rest of this guide.

The Checkout resource in Chec helps to handle an otherwise one of the most complex moving parts of an eCommerce application. The Checkout endpoint comes with the core commerce.checkout.generateToken() and commerce.checkout.capture() methods along with Checkout helpers, additional helper functions for a seamless purchasing flow which we will touch on more later.

In the Checkout.vue page component, let’s start first by initializing all the data we will need in this component to build out a checkout form. There are four core properties that are required to process an order using Commerce.js - customer, shipping, fulfillment, and payment. Let’s start by defining the fields we will need to capture in the form. The main property objects will all go under a form object. We will then bind these properties to each single field in our template with the v-model directive.

export default {
  name: 'Checkout',
  props: ['checkoutToken'],
  data() {
    return {
      form: {
        customer: {
          firstName: 'Jane',
          lastName: 'Doe',
          email: 'janedoe@email.com',
        },
        shipping: {
          name: 'Jane Doe',
          street: '123 Fake St',
          city: 'San Francisco',
          stateProvince: 'CA',
          postalZipCode: '94107',
          country: 'US',
        },
        fulfillment: {
          selectedShippingOption: '',
        },
        payment: {
          cardNum: '4242 4242 4242 4242',
          expMonth: '01',
          expYear: '2023',
          ccv: '123',
          billingPostalZipCode: '94107',
        },
      },
    }
  }
},

And in our template fields, as mentioned, we will bind the data to each of the v-model attributes in the input elements. The inputs will be pre-filled with the state data we created above.

<form class="checkout__form">
  <h4 class="checkout__subheading">Customer Information</h4>

    <label class="checkout__label" for="firstName">First Name</label>
    <input class="checkout__input" type="text" v-model="form.customer.firstName" name="firstName" placeholder="Enter your first name" required />

    <label class="checkout__label" for="lastName">Last Name</label>
    <input class="checkout__input" type="text" v-model="form.customer.lastName" name="lastName" placeholder="Enter your last name" required />

    <label class="checkout__label" for="email">Email</label>
    <input class="checkout__input" type="text" v-model="form.customer.email" name="email" placeholder="Enter your email" required />

  <h4 class="checkout__subheading">Shipping Details</h4>

    <label class="checkout__label" for="fullname">Full Name</label>
    <input class="checkout__input" type="text" v-model="form.shipping.name" name="name" placeholder="Enter your shipping full name" required />

    <label class="checkout__label" for="street">Street Address</label>
    <input class="checkout__input" type="text" v-model="form.shipping.street" name="street" placeholder="Enter your street address" required />

    <label class="checkout__label" for="city">City</label>
    <input class="checkout__input" type="text" v-model="form.shipping.city" name="city" placeholder="Enter your city" required />

    <label class="checkout__label" for="postalZipCode">Postal/Zip Code</label>
    <input class="checkout__input" type="text" v-model="form.shipping.postalZipCode" name="postalZipCode" placeholder="Enter your postal/zip code" required />

  <h4 class="checkout__subheading">Payment Information</h4>

    <label class="checkout__label" for="cardNum">Credit Card Number</label>
    <input class="checkout__input" type="text" name="cardNum" v-model="form.payment.cardNum" placeholder="Enter your card number" />

    <label class="checkout__label" for="expMonth">Expiry Month</label>
    <input class="checkout__input" type="text" name="expMonth" v-model="form.payment.expMonth" placeholder="Card expiry month" />

    <label class="checkout__label" for="expYear">Expiry Year</label>
    <input class="checkout__input" type="text" name="expYear" v-model="form.payment.expYear" placeholder="Card expiry year" />

    <label class="checkout__label" for="ccv">CCV</label>
    <input class="checkout__input" type="text" name="ccv" v-model="form.payment.ccv" placeholder="CCV (3 digits)" />

  <button class="checkout__btn-confirm" @click.prevent="confirmOrder">Confirm Order</button>
</form>

The fields above all contain customer details and payments inputs we will need to collect from the customer. The shipping method data is also required in order to ship the items to the customer. Chec and Commerce.js has verbose shipment and fulfillment methods to handle this process. In the Chec dashboard, worldwide shipping zones can be added in settings > shipping and then enabled at the product level. For this demo merchant account, we have enabled international shipping for each product. In the next section, we will touch on some Commerce.js checkout helper functions that will:

  • Easily fetch a full list of countries, states, provinces and shipping options to populate the form fields for fulfillment data collection
  • Get the live object and updates it with any data changes from the form fields

3. Checkout helpers

Let’s first initialize the empty objects and arrays that we will need to store the responses from the checkout helper methods and list them under the form fields data:

data() {
  return {
    liveObject: {},
    shippingOptions: [],
    shippingSubdivisions: {},
    countries: {},
  }

We will go through each of the initialized data and the checkout helper method that pertains to it. First let’s have a look at the liveObject. The live object is a living object which adjusts to show the live tax rates, prices, and totals for a checkout token. This object will be updated every time a checkout helper executes and the data can be used to reflect the changing UI ie. When the shipping option is applied or when tax is calculated. Let’s now first create a method that will fetch the live object:

/**
 * Gets the live object
 * https://commercejs.com/docs/api/?javascript--cjs#get-the-live-object
 */
getLiveObject(checkoutTokenId) {
  this.$commerce.checkout.getLive(checkoutTokenId).then((liveObject) => {
    this.liveObject = liveObject;
  }).catch((error) => {
    console.log('There was an error getting the live object', error);
  });
},

This getLiveObject() function will fetch the current checkout live object at GET v1/checkouts/{checkout_token_id}/live with the method commerce.checkout.getLive and store the object in this.liveObject that we created earlier.

Upon a successful call, an abbreviated response might look like the below json data:

{
  "merchant_id": 18462,
  "currency": {
    "code": "USD",
    "symbol": "$"
  },
  "line_items": [
    {
      "id": "item_7RyWOwmK5nEa2V",
      "product_id": "prod_8XO3wpDrOwYAzQ",
      "product_name": "Coffee",
      "type": "standard",
      "sku": null,
      "quantity": 1,
      "price": {
        "raw": 7.5,
        "formatted": "7.50",
        "formatted_with_symbol": "$7.50",
        "formatted_with_code": "7.50 USD"
      },
      "line_total": {
        "raw": 7.5,
        "formatted": "7.50",
        "formatted_with_symbol": "$7.50",
        "formatted_with_code": "7.50 USD"
      },
      "variants": [],
      "tax": {
        "is_taxable": false,
        "taxable_amount": null,
        "amount": null,
        "breakdown": null
      }
    }
  ],
  "subtotal": {
    "raw": 7.5,
    "formatted": "7.50",
    "formatted_with_symbol": "$7.50",
    "formatted_with_code": "7.50 USD"
  },
  "discount": [],
  "shipping": {
    "available_options": [
      {
        "id": "ship_kpnNwAjO9omXB3",
        "description": "International",
        "price": {
          "raw": 5,
          "formatted": "5.00",
          "formatted_with_symbol": "$5.00",
          "formatted_with_code": "5.00 USD"
        },
        "countries": [
          "US",
          "CA",
        ]
      }
    ],
    "price": {
      "raw": 0,
      "formatted": "0.00",
      "formatted_with_symbol": "$0.00",
      "formatted_with_code": "0.00 USD"
    }
  },
  "tax": {
    "amount": {
      "raw": 0,
      "formatted": "0.00",
      "formatted_with_symbol": "$0.00",
      "formatted_with_code": "0.00 USD"
    }
  },
  "total": {
    "raw": 7.5,
    "formatted": "7.50",
    "formatted_with_symbol": "$7.50",
    "formatted_with_code": "7.50 USD"
  },
  "total_with_tax": {
    "raw": 7.5,
    "formatted": "7.50",
    "formatted_with_symbol": "$7.50",
    "formatted_with_code": "7.50 USD"
  },
  "giftcard": [],
  "total_due": {
    "raw": 7.5,
    "formatted": "7.50",
    "formatted_with_symbol": "$7.50",
    "formatted_with_code": "7.50 USD"
  },
}

We will be working with this response later when we update the selected shipping method. Next, let’s start creating methods that will fetch a list of countries and subdivisions for a particular country.

With a created function fetchAllCountries(), we will use commerce.services.localeListCountries() at GET v1/services/locale/countries to fetch and list all countries registered in the Chec dashboard.

/**
 * Fetches a list of countries
 * https://commercejs.com/docs/sdk/checkout#list-available-shipping-countries
 */
fetchAllCountries(){
    this.$commerce.services.localeListCountries().then((countries) => {
        this.countries = countries.countries
    }).catch((error) => {
        console.log('There was an error fetching a list of countries', error);
    });
},

The response will be stored in the countries object we initialized earlier in our data object. We will then be able to use this countries object to iterate and display a list of countries in a select element later. The fetchStateProvince() function below will walk through the same pattern as well.

A country code argument is required to make a request with commerce.services.localeListSubdivisions()

/**
 * Fetches the subdivisions (provinces/states) in a country which
 * can be shipped to for the current checkout
 * https://commercejs.com/docs/sdk/checkout#list-available-shipping-subdivisions
 */
fetchStateProvince(){
    this.$commerce.services.localeListSubdivisions(this.form.shipping.country).then((resp) => {
        this.shippingSubdivisions = resp.subdivisions
    }).catch((error) => {
        console.log('There was an error fetching the subdivisions', error);
    });
},
/**
 * Fetches the available shipping methods for the current checkout
 * https://commercejs.com/docs/sdk/checkout#get-shipping-methods
 */
fetchShippingOptions(checkoutToken, country, stateProvince){
  this.$commerce.checkout.getShippingOptions(checkoutToken,
    { country: country, region: stateProvince }).then((options) => {
      this.shippingOptions = options;
    }).catch((error) => {
      console.log('There was an error fetching the shipping methods', error);
  });
},
/**
 * Checks and validates the shipping method
 * https://commercejs.com/docs/api/?javascript--cjs#check-shipping-method
 */
validateShippingOption(shippingOptionId) {
  this.commerce.checkout.checkShippingOption(this.checkoutToken.id, {
    shipping_option_id: shippingOptionId,
    country: this.form.shipping.country,
    region: this.form.shipping.stateProvince
  }).then((shippingOption) => {
    this.fulfillment.selectedShippingOption = shippingOption.id;
    this.getLiveObject();
  }).catch((error) => {
    console.log('There was an error setting the shipping option', error);
  })
},

4. Capture order

confirmOrder() {
  const orderData = {
      line_items: this.checkoutToken.live.line_items,
      customer: {
      firstname: this.form.customer.firstName,
      lastname: this.form.customer.lastName,
      email: this.form.customer.email
    },
    shipping: {
      name: this.form.shipping.name,
      street: this.form.shipping.street,
      town_city: this.form.shipping.city,
      county_state: this.form.shipping.stateProvince,
      postal_zip_code: this.form.shipping.postalZipCode,
      country: this.form.shipping.country,
    },
    fulfillment: {
      shipping_method: this.form.fulfillment.selectedShippingOption
    },
    payment: {
      gateway: "test_gateway",
      card: {
        number: this.form.payment.cardNum,
        expiry_month: this.form.payment.expMonth,
        expiry_year: this.form.payment.expYear,
        cvc: this.form.payment.ccv,
        postal_zip_code: this.form.payment.billingPostalZipCode
      }
    }
  };
  this.$emit('confirm-order', this.checkoutToken.id, orderData);
}
data() {
    return {
      products: [],
      cart: {},
      checkoutToken: null,
      order: null,
    };
  },
/**
 * Captures the checkout
 * https://commercejs.com/docs/sdk/checkout#capture-order
 *
 * @param {string} checkoutTokenId The ID of the checkout token
 * @param {object} newOrder The new order object data
 */
handleConfirmOrder(checkoutTokenId, newOrder) {
  this.$commerce.checkout.capture(checkoutTokenId, newOrder).then((order) => {
    this.refreshCart();
    this.order = order;
    this.$router.push('/confirmation');
  }).catch((error) => {
      console.log('There was an error confirming your order', error);
  });
}
    <router-view
      :products="products"
      @add-to-cart="handleAddToCart"
      @confirm-order="handleConfirmOrder"
      :checkout-token="checkoutToken"
      :order="order"
    />

5. Order confirmation

Download Details:

Author: jaepass

Demo: https://commercejs-vuejs-checkout.netlify.app/

Source Code: https://github.com/jaepass/commercejs-vuejs-checkout

#vuejs #vue #javascript

Commerce.js Vue.js Checkout
7.45 GEEK