Overview
This guide illustrates how to create a checkout form and capture an order using the Commerce.js SDK.
If you followed the previous guides you first created a simple Nuxt application with create-nuxt-app
, Vuex, and Vuetify that listed the products from your Chec dashboard, and then followed up with creating a cart, as well as adding, removing, and clearing a cart. This guide will walk you through creating a page dedicated to submitting an order, generating a checkout token with generateTokenFrom(), and using that token to capture the order with capture().
Note: This guide uses v2 of the Commerce.js SDK
This guide will cover:
- Creating shipping zones and adding available zones to products (optional)
- Create Checkout page
- Create a form for customer information
- Form validation with Vuetify
- Submit an order and display the order confirmation number
Requirements
- IDE of your choice: VS Code is not required, you can use something lightweight like Atom Code Editor or Sublime Text.
- Commerce.js SDK
- Chec.io account
- Yarn or npm
- Nuxt.js
- Vuetify
- Vuex
Prerequisites
Basic knowldge of Nuxt.js and JavaScript are required for this guide, and some familiarity with Vuetify will help.
- Nuxt.js v2
- JavaScript(ES7)
- Vuetify.js v2.2.15
- Vue.js
Shipping Zones
Shipping zones are important because they allow you to determine where you will ship to, as well as calculating costs for shipping to those different countries, states, or regions.
Adding a shipping zone
To add a shipping zone for your store you should first login to your Chec dashboard and locate the Shipping tab located in your dashboards setup. Click on the "+ Add Zone" button, a modal that contains a form will pop up and ask for some pieces of information; A Zone Name, the name of the zone or region. Countries & Regions, the countries and/or regions you will ship your store's products to. Base Rates, the base rate for shipping to the new shipping zone.
Adding a shipping zone to your products
You have just set up a shipping zone, now to add the zone to your products. Go to your products in your dashboard, select a product and scroll down to Delivery Options. Enable the newly created zone that appears underneath the default. This will allow you to notify the users of price changes based on the user's selected shipping arrangements, as well as supply the capture() method with the necessary shipping option id to submit the transaction.
1. Generating a checkout token
One of the key parts to capturing a checkout is generating a checkout token using the following method: generateToken(). To accomplish this you are going to update your Vuex store; As a quick reminder, Vuex is a state management store for Vue and comes with Nuxt if the option is selected.. Go back into your store/index.js
file add a new property on the state object called: token
which will equal an empty object and add a new action: genCheckoutToken()
. This action will do as the name says and create a token to be used to capture a checkout order. generateToken()
expects one parameter, which could be a cart's id, a product's id, or a permalink. The second parameter should be an object that states the type of id you are passing as the first parameter, in this scenario you will be using your cart's id. If all checks out and the request is successful; simply commit the token to a setToken
mutation, which will just set the state's token object to the response of generateToken()
.
// State
export const state = {
...
token: {},
...
}
// Actions
export const actions = {
...
async genCheckoutToken({ commit }, payload) {
const token = await v.$commerce.checkout.generateToken(payload, {
type: 'cart'
})
if (token) {
commit('setToken', token)
}
},
...
}
// Mutations
export const mutations = {
...
setToken(state, payload) {
state.token = payload
},
...
}
After your store has been updated, go to your components directory and open up components/Checkout.vue
in your editor and add your new action to mapActions
and update the checkout button to call genToken(cart.id)
when a click event is triggered. Also add the to="/checkout"
prop and set it to the new checkout route that you will be creating, which can be read about in Vuetify's VBtn docs
// components/Checkout.vue
<template>
...
<v-btn
color="green"
class="white--text mt-10"
:disabled="disabled"
to="/checkout" // Nuxt will create the route when the component is created
x-large
@click="genToken(cart.id)" // Pass the current cart's id
>
<v-icon small>mdi-lock</v-icon>
<span>Secure Checkout</span>
</v-btn>
...
<template>
<script>
...
methods: {
...mapActions({
removeProduct: 'removeProductFromCart',
clearCart: 'clearCart',
genToken: 'genCheckoutToken' // Used in VBtn to call the genCheckoutToken action
})
},
...
<script>
Checkout Capture overview
Your users will need some sort of form or inputs to be able to enter the necessary information in order to submit and complete a checkout order. So first we will go over the expected data that will be necessary in building out this form component. The method used to checkout an order is the capture(checkout_token_id, data) method on the checkout
class. The capture
method expects 2 parameters, a checkout_token_id: string
and an object that contains data from the user and from their cart.
// sample data to be passed to capture().
{
"line_items": {
"item_7RyWOwmK5nEa2V": {
"quantity": 1,
"variants": {
"vrnt_p6dP5g0M4ln7kA": "optn_DeN1ql93doz3ym"
}
}
},
"discount_code": "20off",
"extrafields": {
"extr_Kvg9l6zvnl1bB7": "415-111-2222",
"extr_bWZ3l8zLNokpEQ": "google.com"
},
"customer": {
"firstname": "John",
"lastname": "Doe",
"email": "[email protected]"
},
"shipping": {
"name": "John Doe",
"street": "123 Fake St",
"town_city": "San Francisco",
"county_state": "California",
"postal_zip_code": "94103",
"country": "US"
},
"fulfillment": {
"shipping_method": "ship_7RyWOwmK5nEa2V"
},
"billing": {
"name": "John Doe",
"street": "234 Fake St",
"town_city": "San Francisco",
"county_state": "California",
"postal_zip_code": "94103",
"country": "US"
},
"payment": {
"gateway": "test_gateway",
"card": {
"number": "4242 4242 4242 4242",
"expires": "11\/19",
"cvc": 123,
"postal_zip_code": "94107",
"token": "irh98298g49",
"nonce": 293074902374234
},
"razorpay": {
"payment_id": "839h8d89wg87r3cz3trbis8"
}
},
"pay_what_you_want": "149.99"
}
2. BillingDetails.vue
Create a new Vue file named: BillingDetails.vue
and put it in the /components
directory. For this component you will use Vuetify's VForm component that will contain a few VTextField's, a couple VSelect's and a VBtn component at the end to submit the order and capture the checkout.
// BillingDetails.vue
<template>
<v-form ref="billing" class="px-1">
<v-row>
<v-col class="py-0">
<v-text-field
v-model="firstName"
dense
name="firstName"
label="First Name"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
<v-col class="py-0">
<v-text-field
v-model="lastName"
dense
name="lastName"
label="Last Name"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="py-0">
<v-text-field
v-model="phone"
dense
name="phone"
label="Phone #"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
<v-col class="py-0">
<v-text-field
v-model="email"
dense
label="Email"
name="email"
outlined
:rules="[rules.required, rules.email]"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="py-0">
<v-text-field
v-model="address"
dense
label="Street Address"
name="address"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
<v-col class="py-0">
<v-text-field
v-model="city"
dense
label="City"
name="city"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col class="py-0">
<v-select
v-model="country"
dense
item-text="name"
item-value="code"
label="Country"
name="country"
outlined
return-object
:items="countries"
:rules="[rules.required]"
@change="shippingOpts(token.id)"
></v-select>
</v-col>
<v-col class="py-0">
<v-select
v-model="region"
dense
item-text="name"
item-value="code"
label="Region"
name="region"
outlined
:items="country.states"
:rules="[rules.required]"
></v-select>
</v-col>
<v-col class="py-0">
<v-text-field
v-model="postalCode"
dense
label="Postal Code"
name="postalCode"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
</v-row>
<p class="title ml-3 mb-4">Payment Details</p>
<v-text-field
v-model="cardNumber"
label="Card #"
outlined
:rules="[rules.required]"
></v-text-field>
<v-row>
<v-col class="py-0">
<v-text-field
v-model="expiryDate"
label="Date"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
<v-col class="py-0">
<v-text-field
v-model="cvc"
label="cvc"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
<v-col class="py-0">
<v-text-field
v-model="cardZip"
label="Zip"
outlined
:rules="[rules.required]"
></v-text-field>
</v-col>
</v-row>
<v-btn @click.native="submitOrder">Submit</v-btn>
</v-form>
</template>
Input rules
A prop that Vuetify's inputs have in common is the rules
prop, which takes an array of functions that take an input value as an argument, returning either true/false or an error string. These are especially useful when you are expecting a certain result or format from the input. For this example only one rule(required
) is declared to demonstrate how to use the rules. In your components data
object you will see a rules
object that contains 2 functions, email()
and required()
. rules.email()
will check if the value is a valid email by testing it against the regex pattern and returning true
or a string "Invalid e-mail."
. rules.required()
will just simply check that a value is present in the input field or returns the string Required.
.
2. BillingDetails.vue cont.
For the script portion of this component, you will first import mapGetters and locale (a static file containing country and region data specific to this demo). This component will also be passed the cart
prop from the parent, which is just the cart stored in your state that will be inhereted from the pages/checkout.vue
component. Inside the components data
function will live all the input models for the form inputs as seen above, as well as the rules object. The computed property utilizes mapGetters to return the token that is stored in your app's vuex state.
// components/BillingDetails.vue
<script>
import { mapGetters } from 'vuex'
import locale from '~/static/locale'
export default {
name: 'BillingDetails',
props: {
cart: {
type: Object,
default: () => {}
}
},
data: () => ({
countries: locale,
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
country: {},
city: '',
region: '',
postalCode: '',
cardNumber: '4242 4242 4242 4242',
expiryDate: '01/2023',
cvc: '123',
cardZip: '94103',
rules: {
email: (v) => {
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return pattern.test(v) || 'Invalid e-mail.'
},
required: (v) => !!v || 'Required.'
}
}),
computed: {
...mapGetters({
token: 'token'
})
},
methods: {
shippingOpts(tokenID) {
this.$commerce.checkout
.getShippingOptions(tokenID, {
country: this.country.code
})
.then((r) => {
this.$emit('shippingCost', r[0].price.formatted)
this.shipMethod = r[0].id
})
},
}
}
</script>
The shippingOptions()
method takes the token's id as the parameter and calls Commerce.js's getShippingOptions() method. getShippingOptions()
takes the token id as the first paremeter and an object containing a country and an optional region, returning an array of shipping methods associated with the country and region. Once the request has completed, you will emit a 'shippingCost'
with the formatted price and set the shipMethod to the first id in the results array.
3. Submit and capture a checkout
This is the last part to the BillingDetails.vue
component, the submitOrder()
method. This method first validates the form, if it does not pass validation then return and stop execution of the method's code. If the form and the inputs pass validation, then begin building the data
object that will be passed to the checkout.capture() method. The capture()
method is passed a checkout_token_id
and the data
object which will contain all the data billing details provided by the user, the cart's line items, the shipping methods associated with the shipping region, and finally the customer's credit card information. Once this object is built you will call the capture()
method by passing the checkout token id and data
object, and whether the response is a result or an error you will emit a different event. In the case of a successful result: 'orderComplete'
passing an object containing the order id and customer's reference. In the case of an error: 'orderError'
passing the caught error.
// BillingDetails.vue
export default {
...
methods: {
...
submitOrder() {
if (!this.$refs.billing.validate()) return // eslint-disable-line no-useless-return
const date = this.expiryDate.split('/')
const lineItems = {}
for (const i of this.cart.line_items) {
lineItems[i.id] = {
quantity: i.quantity
}
}
// Capture checkout data
const data = {
line_items: lineItems,
customer: {
firstname: this.firstName,
lastname: this.lastName,
email: this.email
},
shipping: {
name: `${this.firstName} ${this.lastName}`,
street: this.address,
town_city: this.city,
county_state: this.region,
postal_zip_code: this.postalCode,
country: this.country.code
},
fulfillment: {
shipping_method: this.shipMethod
},
payment: {
gateway: 'test_gateway',
card: {
number: this.cardNumber,
expiry_month: date[0],
expiry_year: date[1],
cvc: this.cvc,
postal_zip_code: this.cardZip
}
}
}
// make request
this.$commerce.checkout
.capture(this.token.id, data)
.then((r) => {
this.$emit('orderComplete', { id: r.id, ref: r.customer_reference })
})
.catch((e) => {
this.$emit('orderError', e)
})
},
}
}
4. Checkout.vue page component
Now that you've updated your vuex store and created a component to handle collecting the billing and shipping information from the customer, you'll want to add a pages/checkout.vue
component to be all these things to use. Create a new Vue component in the pages directory; pages/checkout.vue
, this component will be the /checkout
route as Nuxt will create that route by just adding the checkout.vue
component file inside of your Nuxt Pages directory.
pages/checkout.vue script
First you'll start by building out the <script>
portion of the component by importing the mapGetters function from vuex and the BillingDetails.vue
component created previously in this guide(Be sure to correctly register BillingDetails to the components object). In data you'll want to add dialog: false
, which will be used to display the customer's order reference id and the order number with Vuetify's VDialog component. snackbar: false
which will be used to display an error with Vuetify's VSnackbar component, if there is an error returned from the order capture, the snackbar. The timeout
data prop will be used in the snackbar to allow it to disappear once the time has passed. In computed you will get the cart from the store using ...mapGetters
to access the cart
getter. Total()
will return the orders total cost and will include shipping once a country/region is selected. For mounted you will dispatch retrieveCart
in the case that the user refreshes the page so that your store will populate with the necessary data again. For now the methods will just contain an updateCost
method that will trigger and update the shipping cost when the shippingCost
gets emitted from the BillingDetails.vue
component. handleOrderError(e: Error)
will accept an error string as an argument and set the orderError data prop and set the snackbar to true, causing it to display for 8seconds displaying the passed error message. handleOrderRes(data)
accepts an object containg the order number id and order reference id and updates the components orderNumber and orderRef with the values passed and sets the dialogs value to true, causing the dialog to display.
// pages/checkout.vue
<script>
import { mapGetters } from 'vuex'
import BillingDetails from '~/components/BillingDetails'
export default {
components: {
BillingDetails
},
data: () => ({
dialog: false,
shippingCost: '0.00',
snackbar: false,
timeout: 8000,
orderNumber: '',
orderRef: '',
orderError: ''
}),
computed: {
...mapGetters({
cart: 'cart'
}),
total() {
let total = '0.00'
if (this.cart.subtotal) {
total = +this.cart.subtotal.raw + +this.shippingCost
}
return `$${total}`
}
},
mounted() {
this.$store.dispatch('retrieveCart')
},
methods: {
updateCost(price) {
this.shippingCost = price
},
handleOrderError(e) {
this.orderError = e
this.snackbar = true
},
handleOrderRes(data) {
this.orderNumber = data.id
this.orderRef = data.ref
this.dialog = true
},
}
}
</script>
pages/checkout.vue template
The base for this component will be Vuetify's VCard component, inside you'll notice the billing-details
component will have a few event listeners bound the methods written previously in the script tag: @shippingCost
- Updates the shippingCost data prop, @orderComplete
- Updates the orderNumber and orderRef also displays the dialog, @orderError
- Update the orderError data prop and displays the snackbar. The majority of this component's template uses Vuetify's Grid System to position the different pieces where you want them to go, makes easy work out of making your store mobile responsive by creating rows and columns. At the bottom you will see the VDialog and VSnackbar components that are activated when the data property bound to each components v-model
has a true value.
// pages/checkout.vue
<template>
<v-card elevation="0">
<v-row align="center" justify="space-around">
<v-col cols="12" sm="7" lg="5">
<v-card-title primary-title>
Billing Details
</v-card-title>
<billing-details
:cart="cart"
@shippingCost="updateCost" // `@` is shorthand for the `v-on:` directive
@orderComplete="handleOrderRes" // https://vuejs.org/v2/guide/events.html
@orderError="handleOrderError"
/>
</v-col>
<v-col cols="12" sm="5">
<v-card max-width="400">
<v-card-title class="pb-0">Order Summary</v-card-title>
<v-container>
<v-row justify="space-between">
<v-card-subtitle class="title py-1 pl-3">Product</v-card-subtitle>
<v-card-subtitle class="title py-1 pr-3"
>Subtotal</v-card-subtitle
>
</v-row>
<v-row
v-for="item in cart.line_items"
:key="item.id"
justify="space-between"
>
<v-card-subtitle :key="item.name"
>{{ item.name }} x {{ item.quantity }}</v-card-subtitle
>
<v-card-subtitle :key="item.price.formatted"
>${{ item.price.formatted }}</v-card-subtitle
>
</v-row>
<v-divider></v-divider>
<v-row justify="space-between">
<v-card-subtitle>Subtotal</v-card-subtitle>
<v-card-subtitle v-if="cart.subtotal">{{
cart.subtotal.formatted_with_symbol
}}</v-card-subtitle>
<v-card-subtitle v-else>$0.00</v-card-subtitle>
</v-row>
<v-row justify="space-between">
<v-card-subtitle>Shipping</v-card-subtitle>
<v-card-subtitle>${{ shippingCost }}</v-card-subtitle>
</v-row>
<v-divider></v-divider>
<v-row justify="space-between">
<v-card-subtitle>Total</v-card-subtitle>
<v-card-subtitle>{{ total }}</v-card-subtitle>
</v-row>
<v-row justify="center">
<v-btn class="my-2" to="/">Edit Cart</v-btn>
</v-row>
</v-container>
</v-card>
</v-col>
</v-row>
<v-dialog v-model="dialog" width="500">
<v-card>
<v-card-title>Order Confirmation: {{ orderRef }}</v-card-title>
<v-card-text>
Thank you for your order!
</v-card-text>
<v-card-actions>
<v-btn outlined to="/">Store</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar v-model="snackbar" top vertical :timeout="timeout">
{{ orderError }}
</v-snackbar>
</v-card>
</template>
With the previous steps put together you should have something pretty close to this:
5. Run your app!
You should now be able to run your Nuxt application, add products to a cart, generate a checkout token, and capture the order.
// yarn
yarn dev
//npm
npm run dev
Conclusion
Nice work, you've successfully created a checkout page and captured an order.
Let's review what we have accommplished in this guide.
- Learned how to create shipping regions and apply them to products in your dashboard
- Generated a checkout token and used that token to capture an order
- Created a component to be used as a form to gather data to submit the capture
- Wrote rules for required inputs
- Created a Checkout page for your customers
As you can see, the Commerce.js SDK greatly simplifies the eCommerce process, the only thing left for you to do is create a theme or layout and style your app as you see fit.
Built with:
- Nuxt.js - The front-end framework used
- Vuetify - The Vue material component library used
- Yarn - Package manager tool
Want to help create guides?
Get in touch with the Commerce.js team and get paid to create projects for the community.