[ ] Checkout helpers
First, go back to the constructor's state and initialize the empty objects and arrays that you will need to store the responses from the checkout helper methods. Initialize a shippingCountries object, a shippingSubdivisions object and a shippingOptions array.
With a created function fetchShippingCountries(), use commerce.services.localeListShipppingCountries() at GET v1/services/locale/{checkout_token_id}/countries to fetch and list all countries in the select options in the form.
/**
* Fetches a list of countries available to ship to checkout token
* https://commercejs.com/docs/sdk/checkout#list-available-shipping-countries
*
* @param {string} checkoutTokenId
*/
fetchShippingCountries(checkoutTokenId) {
commerce.services.localeListShippingCountries(checkoutTokenId).then((countries) => {
this.setState({
shippingCountries: countries.countries,
})
}).catch((error) => {
console.log('There was an error fetching a list of shipping countries', error);
});
}
The response will be stored in the shippingCountries object you initialized earlier in the constructor. You will then be able to use this countries object to iterate and display a list of countries in a select element, which you will be adding later. The fetchSubdivisions() function below will walk through the same pattern as well.
A country code argument is required to make a request with commerce.services.localeListSubdivisions() to GET v1/services/locale/{country_code}/subdivisions to get a list of all subdivisions for that particular country.
/**
* Fetches the subdivisions (provinces/states) in a country which
* can be shipped to for the current checkout
* https://commercejs.com/docs/sdk/checkout#list-subdivisions
*
* @param {string} countryCode
*/
fetchSubdivisions(countryCode) {
commerce.services.localeListSubdivisions(countryCode).then((subdivisions) => {
this.setState({
shippingSubdivisions: subdivisions.subdivisions,
})
}).catch((error) => {
console.log('There was an error fetching the subdivisions', error);
});
},
With a successful request, the response will be stored in the this.state.shippingSubdivions array and will be used to iterate and output a select element in your template later on.
For your next checkout helper function, fetch the current shipping options available in your merchant account. This function will fetch all the shipping options that were registered in the Chec Dashboard and are applicable to the products in your cart using the commerce.checkout.getShippingOptions() method. This function takes in two required parameters - the checkoutTokenId, the country code for the provide country in our data, and the region is optional.
/**
* Fetches the available shipping methods for the current checkout
* https://commercejs.com/docs/sdk/checkout#get-shipping-methods
*
* @param {string} checkoutTokenId
* @param {string} country
* @param {string} stateProvince
*/
fetchShippingOptions(checkoutTokenId, country, stateProvince = null) {
commerce.checkout.getShippingOptions(checkoutTokenId,
{
country: country,
region: stateProvince
}).then((options) => {
const shippingOption = options[0] || null;
this.setState({
shippingOptions: options,
shippingOption: shippingOption,
})
}).catch((error) => {
console.log('There was an error fetching the shipping methods', error);
});
}
When the promise resolves, the response will be stored into this.state.shippingOptions which you can then use to render a list of shipping options in your template. Note that the shippingOption is set to the first index option in the array to have an option in the dropdown be displayed.
Alright, that wraps up all the checkout helper functions you'll want to create for the checkout page. Now it's time to execute and hook up the responses to your render function!
Start by chaining and calling the fetchShippingCountries() in the generateCheckoutToken function for ease of execution.
/**
* Generates a checkout token
* https://commercejs.com/docs/sdk/checkout#generate-token
*/
generateCheckoutToken() {
const { cart } = this.props;
if (cart.line_items.length) {
return commerce.checkout.generateToken(cart.id, { type: 'cart' })
.then((token) => this.setState({ checkoutToken: token }))
.then(() => this.fetchShippingCountries(this.state.checkoutToken.id))
.catch((error) => {
console.log('There was an error in generating a token', error);
});
}
}
Next, call generateCheckoutToken() in componentDidMount().
In the same vein, you'll need to call the fetchShippingOptions() function to display the list of shipping options available. Add this.fetchShippingOptions(this.state.checkoutToken.id, this.state.shippingCountry) into a lifecycle hook that checks whether the shipping country state has changed.
You will now need to bind all the data responses to the shipping form fields. In the shipping section of the JSX render you created earlier on, place all the markup underneath the Postal/Zip code input field:
Binds this.state.shippingCountry as the selected country and loops through the shippingCountries array to render as options
Binds this.state.shippingStateProvince as the selected state/province and iterates through the shippingSubivisions object to display the available list of countries
Binds this.state.shippingOption and loops through the shippingOptions array to render as options in the Shipping method field.
Currently the prepopulated data defined in the state earlier will be bound to each of the input but changes to the input fields have not been handled yet. An onChange handler will need to be added to handle necessary form field value changes. Create a handler function handleFormChanges() to update the state with changed input values. The handler will also need to be bound in the constructor.
The value prop here is setting the value to the latest state so that you can see the inputted value as it is being typed. Next, attach the handler to the onChange attribute in each of the input fields as well as the shipping state/province and shipping options select fields.
For data such as the shipping country and shipping subdivisions, it is necessary to handle the option changes separately. You will want to re-fetch and populate the select options with a new set of subdivisions according to the country selected. Since only one zone is enabled in the demo account, create two handlers to handle the shipping country option change as an example and bind the handler in the constructor. The shipping zones are also enabled at the subdivisions level, so you also want to handle changes to the subdivisions field.
Once all the data is bound to the field you are then able to collect the necessary data to convert the checkout into an order object.
[ ] Capture order
With all the data collected, you now need to associate it to each of the order properties in an appropriate data structure so you can confirm the order.
Before creating a function to capture your checkout, you'll need to structure or 'sanitize' your line items to send in the checkout capture payload exactly how the request expects it. Write a function called sanitizedLineItems() to sanitize your line items.
Now, create your handleCaptureCheckout() handler function and structure your returned data. Have a look at the expected structure here to send an order request. Note that in the line_items property value, you'll use the santizedLineItems function to pass in cart.line_items.
Follow the exact structure of the data you intend to send and attach a callback function onCaptureCheckout to the handler and pass the order data object along with the required checkoutToken.id.
You need a button to handle the clicking of order confirmation, let's add that right now as the last element before the closing tag:
Before creating an event handler to deal with your order capture, use another Commerce.js method called commerce.checkout.refreshCart(). When you call this function, it will refresh the cart in your state/session when the order is confirmed.
/**
* Refreshes to a new cart
* https://commercejs.com/docs/sdk/cart#refresh-cart
*/
refreshCart() {
commerce.cart.refresh().then((newCart) => {
this.setState({
cart: newCart,
});
}).catch((error) => {
console.log('There was an error refreshing your cart', error);
});
};
Now create a helper function which will capture your order with the method commerce.checkout.capture(). It takes in the checkoutTokenId and the newOrder parameters. Upon the promise resolution, refresh the cart, store the order into the this.order property, and lastly use the router to push to a confirmation page which will be created in the last step.
/**
* 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
*/
handleCaptureCheckout(checkoutTokenId, newOrder) {
commerce.checkout.capture(checkoutTokenId, newOrder).then((order) => {
// Save the order into state
this.setState({
order,
});
// Clear the cart
this.refreshCart();
// Send the user to the receipt
this.props.history.push('/confirmation');
// Store the order in session storage so we can show it again if the
// user refreshes the page!
window.sessionStorage.setItem('order_receipt', JSON.stringify(order));
}).catch((error) => {
console.log('There was an error confirming your order', error);
});
};
Now make sure you update and bind the necessary props and event handlers to a component passing in the component in the render:
Lastly, create a simple confirmation view to display a successful order page.
[ ] Order confirmation
Under src/pages, create a new page component and name it Confirmation.js. Define an order prop for the parent component App.js to pass the order object down. Next, create your render function to output a simple UI for the confirmation screen.
The JSX will render a message containing the customer's name and an order reference.
In your App.js again, attach your order prop to your <Route> <Checkout> component instance:
With a created function fetchShippingCountries(), use commerce.services.localeListShipppingCountries() at GET v1/services/locale/{checkout_token_id}/countries to fetch and list all countries in the select options in the form.
The response will be stored in the shippingCountries object you initialized earlier in the constructor. You will then be able to use this countries object to iterate and display a list of countries in a select element, which you will be adding later. The fetchSubdivisions() function below will walk through the same pattern as well.
A country code argument is required to make a request with commerce.services.localeListSubdivisions() to GET v1/services/locale/{country_code}/subdivisions to get a list of all subdivisions for that particular country.
With a successful request, the response will be stored in the this.state.shippingSubdivions array and will be used to iterate and output a select element in your template later on.
For your next checkout helper function, fetch the current shipping options available in your merchant account. This function will fetch all the shipping options that were registered in the Chec Dashboard and are applicable to the products in your cart using the commerce.checkout.getShippingOptions() method. This function takes in two required parameters - the checkoutTokenId, the country code for the provide country in our data, and the region is optional.
When the promise resolves, the response will be stored into this.state.shippingOptions which you can then use to render a list of shipping options in your template. Note that the shippingOption is set to the first index option in the array to have an option in the dropdown be displayed.
Alright, that wraps up all the checkout helper functions you'll want to create for the checkout page. Now it's time to execute and hook up the responses to your render function!
Start by chaining and calling the fetchShippingCountries() in the generateCheckoutToken function for ease of execution.
Next, call generateCheckoutToken() in componentDidMount().
In the same vein, you'll need to call the fetchShippingOptions() function to display the list of shipping options available. Add this.fetchShippingOptions(this.state.checkoutToken.id, this.state.shippingCountry) into a lifecycle hook that checks whether the shipping country state has changed.
You will now need to bind all the data responses to the shipping form fields. In the shipping section of the JSX render you created earlier on, place all the markup underneath the Postal/Zip code input field:
The three fields you just added:
Binds this.state.shippingCountry as the selected country and loops through the shippingCountries array to render as options Binds this.state.shippingStateProvince as the selected state/province and iterates through the shippingSubivisions object to display the available list of countries Binds this.state.shippingOption and loops through the shippingOptions array to render as options in the Shipping method field. Currently the prepopulated data defined in the state earlier will be bound to each of the input but changes to the input fields have not been handled yet. An onChange handler will need to be added to handle necessary form field value changes. Create a handler function handleFormChanges() to update the state with changed input values. The handler will also need to be bound in the constructor.
The value prop here is setting the value to the latest state so that you can see the inputted value as it is being typed. Next, attach the handler to the onChange attribute in each of the input fields as well as the shipping state/province and shipping options select fields.
For data such as the shipping country and shipping subdivisions, it is necessary to handle the option changes separately. You will want to re-fetch and populate the select options with a new set of subdivisions according to the country selected. Since only one zone is enabled in the demo account, create two handlers to handle the shipping country option change as an example and bind the handler in the constructor. The shipping zones are also enabled at the subdivisions level, so you also want to handle changes to the subdivisions field.
Now update your shipping country select field by calling the handler in an onChange attribute.
Once all the data is bound to the field you are then able to collect the necessary data to convert the checkout into an order object.
Before creating a function to capture your checkout, you'll need to structure or 'sanitize' your line items to send in the checkout capture payload exactly how the request expects it. Write a function called sanitizedLineItems() to sanitize your line items.
Now, create your handleCaptureCheckout() handler function and structure your returned data. Have a look at the expected structure here to send an order request. Note that in the line_items property value, you'll use the santizedLineItems function to pass in cart.line_items.
Follow the exact structure of the data you intend to send and attach a callback function onCaptureCheckout to the handler and pass the order data object along with the required checkoutToken.id.
You need a button to handle the clicking of order confirmation, let's add that right now as the last element before the closing tag:
Go back to your App.js to initialize an order data as an empty object where you store your returned order object.
Before creating an event handler to deal with your order capture, use another Commerce.js method called commerce.checkout.refreshCart(). When you call this function, it will refresh the cart in your state/session when the order is confirmed.
Now create a helper function which will capture your order with the method commerce.checkout.capture(). It takes in the checkoutTokenId and the newOrder parameters. Upon the promise resolution, refresh the cart, store the order into the this.order property, and lastly use the router to push to a confirmation page which will be created in the last step.
Now make sure you update and bind the necessary props and event handlers to a component passing in the component in the render:
Lastly, create a simple confirmation view to display a successful order page.
[ ] Order confirmation Under
src/pages
, create a new page component and name itConfirmation.js
. Define an order prop for the parent componentApp.js
to pass the order object down. Next, create your render function to output a simple UI for the confirmation screen.render() { return ( <> { this.renderOrderSummary() } </> ); }; }
export default Confirmation;
<Route path="/confirmation" exact render={(props) => { if (!this.state.order) { return props.history.push('/'); } return ( <Confirmation {...props} order={order} /> ) }} />