Last active
December 17, 2019 01:23
-
-
Save john-raymon/96b44dc8acb6393de65ca5ef486e8ae6 to your computer and use it in GitHub Desktop.
CartCheckout example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div v-if="cart" class="flex flex-grow-1 flex-column bg-tan-white w-100 pb4"> | |
<div class="flex justify-between mw9 w-100 items-center center pt4 ph4"> | |
<router-link to="/products" class="flex items-center medium-text f6 tracked-mega ttu no-underline dark-gray dim lh-solid"> | |
<div class="arrowIconContainer fill-cherry pr4"> | |
<ArrowIconSvg /> | |
</div> | |
continue shopping | |
</router-link> | |
<p class="medium-text f6 tracked-mega ttu dark-gray tracked-mega lh-solid tr"> | |
{{ cart ? cart.total_items : '0' }} | |
<span class="f7">{{cart ? (cart.total_items === 1 ? 'item' : 'items') : 'items'}}</span> | |
</p> | |
</div> | |
<div class="cf mw9 center w-100 ph3 mt5"> | |
<div class="fl w-100 w-40-l ph2 ph4-l mb4"> | |
<div class="relative z-1 br3 bg-dark-gray w-100 shadow-3 pv4 overflow-scroll"> | |
<CartLineItem | |
v-for="item in cart.line_items" | |
@remove-product-from-cart="removeProductFromCart" | |
:item="item" | |
:key="item.id" | |
@update-quantity="updateQuantity" | |
/> | |
</div> | |
<div class="pt4 pb3 nt3 br3 ph4 bg-cherry"> | |
<div class="flex pb1 justify-between items-center w-100 medium-text f6 white ttu b tracked-mega-1"> | |
<p> | |
subtotal | |
</p> | |
<p class="tr lh-title"> | |
{{cart ? cart.subtotal.formatted_with_code : '----'}} | |
</p> | |
</div> | |
</div> | |
</div> | |
<div class="fl w-100 w-60-l ph2 ph4-l"> | |
<form class="font-roboto mb4 ttu f6 tracked-mega light-gray"> | |
<div class="flex justify-between"> | |
<div class="w-50 pr2 flex flex-column"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
first name | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
type="text" | |
name="firstName" | |
v-model="firstName" | |
placeholder="first name" | |
/> | |
</div> | |
<div class="w-50 pl2 flex flex-column"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
first name | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
type="text" | |
name="lastName" | |
v-model="lastName" | |
placeholder="first name" | |
/> | |
</div> | |
</div> | |
<div> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
:class="[errors['customer[email]'] && 'input-error']" | |
type="email" | |
name="customer[email]" | |
v-model="$data['customer[email]']" | |
placeholder="Email Address" | |
/> | |
</div> | |
<div> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
Delivery Name | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
:class="[errors['shipping[name]'] && 'input-error']" | |
type="text" | |
name="shipping[name]" | |
v-model="$data['shipping[name]']" | |
placeholder="Delivery Name" | |
/> | |
</div> | |
<div> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
delivery street address | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
:class="[errors['shipping[street]'] && 'input-error']" | |
type="text" | |
name="shipping[street]" | |
v-model="$data['shipping[street]']" | |
placeholder="Delivery Street Address" | |
/> | |
</div> | |
<div class="flex justify-between"> | |
<div class="w-70 pr2 flex flex-column"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
city | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
:class="[errors['shipping[town_city]'] && 'input-error']" | |
type="text" | |
name="shipping[town_city]" | |
v-model="$data['shipping[town_city]']" | |
placeholder="City" | |
/> | |
</div> | |
<div class="w-30 pl2 flex flex-column"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
post/zip code | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
:class="[errors['shipping[postal_zip_code]'] && 'input-error']" | |
type="number" | |
name="shipping[postal_zip_code]" | |
v-model="$data['shipping[postal_zip_code']" | |
placeholder="post/zip code" | |
/> | |
</div> | |
</div> | |
<div class="flex justify-between"> | |
<div class="w-50 pr2 flex flex-column"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
country | |
</p> | |
</label> | |
<div class="checkoutFormInput flex-grow-1 relative"> | |
<p> | |
{{countries[deliveryCountry] || 'Select your country'}} | |
</p> | |
<select | |
name="deliveryCountry" | |
v-model="deliveryCountry" | |
placeholder="Delivery" | |
class="absolute absolute--fill left-0 o-0 pointer w-100"> | |
<option value="" disabled>Select your country</option> | |
<option v-for="(countryValue, countryKey) in countries" :value="countryKey" :key="countryKey"> | |
{{ countryValue }} | |
</option> | |
</select> | |
</div> | |
</div> | |
<div class="w-50 pl2 flex flex-column relative"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
state/province/region | |
</p> | |
</label> | |
<div class="checkoutFormInput flex-grow-1 relative"> | |
<p> | |
{{deliveryCountry ? subdivisions[deliveryState] || 'Select your state' : 'Select a country first'}} | |
</p> | |
<select | |
name="deliveryState" | |
:disabled="!!!deliveryCountry" | |
v-model="deliveryState" | |
class="absolute absolute--fill left-0 o-0 pointer w-100"> | |
<option value="" disabled>Select your state</option> | |
<option v-for="(subdivisionValue, subdivisionKey) in subdivisions" :value="subdivisionKey" :key="subdivisionKey"> | |
{{ subdivisions[subdivisionKey] }} | |
</option> | |
</select> | |
</div> | |
</div> | |
</div> | |
<div v-if="checkout"> | |
<div class="w-100 flex flex-column mt4"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
delivery method | |
</p> | |
</label> | |
<div | |
class="checkoutFormInput flex-grow-1 relative" | |
:class="[errors['fulfillment[shipping_method]'] && 'input-error']"> | |
<p> | |
{{ | |
$data['fulfillment[shipping_method]'] ? | |
`${ | |
shippingOptionsById[this["fulfillment[shipping_method]"]].description} | |
- $${shippingOptionsById[this["fulfillment[shipping_method]"]].price.formatted_with_code | |
}` : | |
'Select a delivery method' | |
}} | |
</p> | |
<select | |
name="fulfillment[shipping_method]" | |
v-model="$data['fulfillment[shipping_method]']" | |
placeholder="Shipping Option" | |
class="absolute absolute--fill left-0 o-0 pointer w-100"> | |
<option value="" disabled>Select a delivery method</option> | |
<option v-for="option in shippingOptions" :value="option.id" :key="option.id"> | |
{{ `${option.description} - $${option.price.formatted_with_code}` }} | |
</option> | |
</select> | |
</div> | |
</div> | |
<div | |
class="w-100 flex flex-column"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
card number | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
:class="[errors.gateway_error && 'input-error']" | |
type="number" | |
name="cardNumber" | |
v-model="cardNumber" | |
placeholder="Card Number" | |
/> | |
</div> | |
<div class="w-100 flex"> | |
<div class="w-third flex flex-column"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
expiry month | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
type="number" | |
name="expMonth" | |
v-model="expMonth" | |
placeholder="expiry month" | |
/> | |
</div> | |
<div class="w-third flex flex-column ph2"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
expiry year | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
type="number" | |
name="expYear" | |
v-model="expYear" | |
placeholder="expiry year (yyyy)" | |
/> | |
</div> | |
<div class="w-third flex flex-column ph2"> | |
<label> | |
<p class="checkoutFormInputLabel"> | |
cvc | |
</p> | |
</label> | |
<input | |
class="checkoutFormInput" | |
type="number" | |
name="cvc" | |
v-model="cvc" | |
placeholder="cvc" | |
/> | |
</div> | |
</div> | |
</div> | |
<div class="flex flex-column"> | |
<button | |
@click.prevent="() => checkout ? captureOrder() : createCheckout()" | |
class="button__checkout bg-cherry white ttu b self-end pointer dim shadow-5 tracked-mega-1" | |
> | |
{{`${checkout ? 'buy now' : 'delivery & payment'}`}} | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
<p v-else> | |
Loading... | |
</p> | |
</template> | |
<script> | |
import ArrowIconSvg from '../assets/arrow-icon.svg' | |
import CartLineItem from './CartLineItem' | |
export default { | |
name: 'CartCheckout', | |
props: ["cart", "commerce"], | |
components: { | |
ArrowIconSvg, | |
CartLineItem, | |
}, | |
data() { | |
return { | |
firstName: 'John', | |
lastName: 'Doe', | |
"customer[email]": '[email protected]', | |
"shipping[name]": 'John Doe', | |
"shipping[street]": '1161 Mission St', | |
"shipping[town_city]": 'San Francisco', | |
deliveryState: 'CA', | |
"shipping[postal_zip_code]": "94103", | |
deliveryCountry: 'US', // selected country example: this.countries[this.deliveryCountry] | |
countries: {}, | |
subdivisions: {}, | |
checkout: null, | |
// state below is set after checkout token is generated | |
"fulfillment[shipping_method]": '', | |
shippingOptions: [], | |
shippingOptionsById: {}, | |
cardNumber: '', | |
expMonth: '01', | |
expYear: '2021', | |
cvc: '123', | |
billingPostalZipcode: '94103', | |
errors: { | |
"fulfillment[shipping_method]": null, | |
gateway_error: null, | |
"customer[email]": null, | |
"shipping[name]": null, | |
"shipping[street]": null, | |
"shipping[town_city]": null, | |
"shipping[postal_zip_code]": null | |
}, | |
} | |
}, | |
watch: { | |
cart(newCart, oldCart) { | |
if (newCart !== oldCart) { | |
// since this is a hyrbid component showcasing the cart and checkout simultanously | |
// we want to watch for this.cart updates, and do work such as | |
// reseting the checkout state when there are no longer any cart items | |
// Also if there was a checkout token object initiated prior to the change | |
// then we also want to get an updated checkout token object | |
// from Chec via this.createCheckout for the latest cart | |
if (newCart.total_items === 0) { // cart changed the | |
this.checkout = null // clear checkout token object if cart empty now | |
alert("You must add items to your cart to contiue checkout") | |
return; | |
} | |
// only invoke createCheckout if this.checkout was initiated prior to this update to get an updated checkout token object | |
if (this.checkout) { | |
this.createCheckout() | |
} | |
} | |
}, | |
deliveryCountry(newVal) { // do something when new delivery country is selected | |
// update the regions/provinces/states that are based on the selected country (this.deliveryCountry) | |
this.getRegions(newVal) | |
if (this.checkout) { // if there was a checkout initiated prior to this update we want to update | |
// the shipping options based on the selected country | |
this.getShippingOptions(this.checkout.id, newVal) | |
} | |
} | |
}, | |
created() { | |
this.getAllCountries() | |
this.getRegions(this.deliveryCountry) | |
}, | |
methods: { | |
// update respective line-item's quantity using commerce.cart.update | |
// cart.update can also be used to update variant | |
// https://commercejs.com/docs/api/#update-item-in-cart | |
updateQuantity(lineItemId, quantity) { | |
this.$commerce.cart.update(lineItemId, { quantity }) | |
.then(resp => { | |
this.$emit('update:cart', resp.cart) | |
}).catch((error) => { | |
// eslint-disable-next-line no-console | |
console.log('Error when updating quanttiy', error); | |
}) | |
}, | |
getAllCountries() { | |
this.$commerce.services.localeListCountries().then(resp => { | |
this.countries = resp.countries | |
}).catch(error => { | |
// eslint-disable-next-line no-console | |
console.log(error) | |
}) | |
}, | |
removeProductFromCart(itemId) { | |
this.$emit('remove-product-from-cart', itemId) | |
}, | |
captureOrder() { | |
this.errors = { | |
"fulfillment[shipping_method]": null, | |
gateway_error: null, | |
"shipping[name]": null, | |
"shipping[street]": null, | |
}; | |
const lineItems = this.checkout.live.line_items.reduce((obj, lineItem) => { | |
obj[lineItem.id] = { | |
quantity: lineItem.quantity, | |
variants: (!lineItem.variants || {}) || { | |
[lineItem.variants[0].variant_id]: lineItem.variants[0].option_id | |
} | |
} | |
return obj | |
}, {}) | |
const newOrder = { | |
line_items: lineItems, | |
customer: { | |
firstname: this.firstName, | |
lastname: this.lastName, | |
email: this["customer[email]"] | |
}, | |
shipping: { | |
name: this["shipping[name]"], | |
country: this.deliveryCountry, | |
street: this["shipping[street]"], | |
town_city: this["shipping[town_city]"], | |
county_state: this.deliveryState, | |
postal_zip_code: this["shipping[postal_zip_code]"] | |
}, | |
fulfillment: { | |
shipping_method: this["fulfillment[shipping_method]"] | |
}, | |
payment: { | |
gateway: "test_gateway", | |
card: { | |
number: this.cardNumber, | |
expiry_month: this.expMonth, | |
expiry_year: this.expYear, | |
cvc: this.cvc, | |
postal_zip_code: this.billingPostalZipcode | |
} | |
} | |
} | |
// eslint-disable-next-line no-console | |
console.log('The order constructed:', newOrder) | |
this.$commerce.checkout.capture(this.checkout.id, newOrder) | |
.then(resp => { | |
this.$emit('refresh-cart') | |
this.$emit('update:order', resp) | |
this.checkout = null | |
return resp; | |
}) | |
.then(() => { | |
this.$router.replace("/thank-you") // redirect to thank-you page to render order in UI | |
}) | |
.catch((err) => { | |
const error = err.data.error | |
if (error.type === 'validation') { // catch validation errors and update corresponding data/state | |
error.message.forEach(({param, error}) => { | |
this.errors = { | |
...this.errors, | |
[param]: error | |
} | |
}) | |
} | |
if (error.type === 'gateway_error' || error.type === 'not_valid') { // either a gateway error or a shipping error and update corresponding data/state | |
this.errors = { | |
...this.errors, | |
[error.type === 'not_valid' ? 'fulfillment[shipping_method]' : error.type]: error.message | |
} | |
} | |
}) | |
}, | |
getRegions(countryCode) { | |
this.$commerce.services.localeListSubdivisions(countryCode) | |
.then(resp => { | |
this.subdivisions = resp.subdivisions | |
}).catch(error => { | |
// eslint-disable-next-line no-console | |
console.log(error) | |
}) | |
}, | |
// generate checkout token, receive checkout object | |
createCheckout() { | |
if (!this.cart) { | |
return; | |
} | |
if (this.cart.total_items > 0) { | |
this.$commerce.checkout.generateToken(this.cart.id, { type: 'cart' }).then( | |
(checkout) => { | |
this.getShippingOptions(checkout.id, (this.deliveryCountry || 'US')) | |
this.checkout = checkout | |
}).catch(error => { | |
// eslint-disable-next-line no-console | |
console.log('Error:', error) | |
}) | |
} else { | |
alert("Your cart is empty") | |
} | |
}, | |
getShippingOptions(checkoutId, country) { | |
this.$commerce.checkout.getShippingOptions(checkoutId, { country }) | |
.then(resp => { | |
this.shippingOptions = resp | |
this.shippingOptionsById = resp.reduce((obj, option) => { | |
obj[option.id] = option | |
return obj | |
}, {}) | |
}).catch(error => { | |
this.shippingOptions = [] | |
this.shippingOptionsById = {} | |
// eslint-disable-next-line no-console | |
console.log('Error in getShippingOptions', error) | |
}) | |
} | |
} | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment