Skip to content

Commit

Permalink
Merge pull request #384 from reactioncommerce/feature-willopez-core-o…
Browse files Browse the repository at this point in the history
…rdering-tracking-checkout

Add tracking to checkout flow
  • Loading branch information
mikemurray committed Oct 13, 2018
2 parents 0ea2201 + 6ac8a86 commit 6630657
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ version: v1.12.0
ignore:
'npm:chownr:20180731':
- '*':
reason: Waiting on next-routes(https://github.com/fridays/next-routes) to fix this vulnerability
reason: This vulnerability only affects packages used in development, such as webpack, more details at https://github.com/isaacs/chownr/issues/14#issuecomment-421662375
expires: 2018-12-30T00:00:00.000Z
patch: {}
119 changes: 115 additions & 4 deletions src/components/CheckoutActions/CheckoutActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,26 @@ import withPlaceStripeOrder from "containers/order/withPlaceStripeOrder";
import Dialog from "@material-ui/core/Dialog";
import PageLoading from "components/PageLoading";
import { Router } from "routes";
import track from "lib/tracking/track";
import TRACKING from "lib/tracking/constants";
import trackCheckout from "lib/tracking/trackCheckout";
import trackCheckoutStep from "lib/tracking/trackCheckoutStep";
import {
adaptAddressToFormFields,
isShippingAddressSet
} from "lib/utils/cartUtils";

const {
CHECKOUT_STARTED,
CHECKOUT_STEP_COMPLETED,
CHECKOUT_STEP_VIEWED,
PAYMENT_INFO_ENTERED
} = TRACKING;

@withCart
@withPlaceStripeOrder
@inject("authStore")
@track()
@observer
export default class CheckoutActions extends Component {
static propTypes = {
Expand All @@ -43,28 +55,120 @@ export default class CheckoutActions extends Component {
isPlacingOrder: false
}

setShippingAddress = (address) => {
componentDidMount() {
const { cart } = this.props;
// Track start of checkout process
this.trackCheckoutStarted({ cart, action: CHECKOUT_STARTED });

const { checkout: { fulfillmentGroups } } = this.props.cart;
const hasShippingAddress = isShippingAddressSet(fulfillmentGroups);
// Track the first step, "Enter a shipping address" when the page renders,
// as it will be expanded by default, only record this event when the
// shipping address has not yet been set.
if (!hasShippingAddress) {
this.trackAction(this.buildData({ action: CHECKOUT_STEP_VIEWED, step: 1 }));
}
}

@trackCheckout()
trackCheckoutStarted() {}

@trackCheckoutStep()
trackAction() {}

buildData = (data) => {
const { step, shipping_method = null, payment_method = null, action } = data; // eslint-disable-line camelcase

return {
action,
payment_method, // eslint-disable-line camelcase
shipping_method, // eslint-disable-line camelcase
step
};
}

get shippingMethod() {
const { checkout: { fulfillmentGroups } } = this.props.cart;
const shippingMethod = fulfillmentGroups[0].selectedFulfillmentOption.fulfillmentMethod.displayName;

return shippingMethod;
}

get paymentMethod() {
const { stripeToken: { token: { card } } } = this.props.cartStore;
return card.brand;
}

setShippingAddress = async (address) => {
const { checkoutMutations: { onSetShippingAddress } } = this.props;

return onSetShippingAddress(address);
// Omit firstName, lastName props as they are not in AddressInput type
// The address form and GraphQL endpoint need to be made consistent
const { firstName, lastName, ...rest } = address;
const { data, error } = await onSetShippingAddress({
fullName: `${address.firstName} ${address.lastName}`,
...rest
});


if (data && !error) {
// track successfully setting a shipping address
this.trackAction(this.buildData({ action: CHECKOUT_STEP_COMPLETED, step: 1 }));

// The next step will automatically be expanded, so lets track that
this.trackAction(this.buildData({ action: CHECKOUT_STEP_VIEWED, step: 2 }));
}
}

setShippingMethod = (shippingMethod) => {
setShippingMethod = async (shippingMethod) => {
const { checkoutMutations: { onSetFulfillmentOption } } = this.props;
const { checkout: { fulfillmentGroups } } = this.props.cart;
const fulfillmentOption = {
fulfillmentGroupId: fulfillmentGroups[0]._id,
fulfillmentMethodId: shippingMethod.selectedFulfillmentOption.fulfillmentMethod._id
};

return onSetFulfillmentOption(fulfillmentOption);
const { data, error } = await onSetFulfillmentOption(fulfillmentOption);
if (data && !error) {
// track successfully setting a shipping method
this.trackAction({
step: 2,
shipping_method: this.shippingMethod, // eslint-disable-line camelcase
payment_method: null, // eslint-disable-line camelcase
action: CHECKOUT_STEP_COMPLETED
});

// The next step will automatically be expanded, so lets track that
this.trackAction({
step: 3,
shipping_method: this.shippingMethod, // eslint-disable-line camelcase
payment_method: null, // eslint-disable-line camelcase
action: CHECKOUT_STEP_VIEWED
});
}
}

setPaymentMethod = (stripeToken) => {
const { cartStore } = this.props;

// Store stripe token in MobX store
cartStore.setStripeToken(stripeToken);

// Track successfully setting a payment method
this.trackAction({
step: 3,
shipping_method: this.shippingMethod, // eslint-disable-line camelcase
payment_method: this.paymentMethod, // eslint-disable-line camelcase
action: PAYMENT_INFO_ENTERED
});

// The next step will automatically be expanded, so lets track that
this.trackAction({
step: 4,
shipping_method: this.shippingMethod, // eslint-disable-line camelcase
payment_method: this.paymentMethod, // eslint-disable-line camelcase
action: CHECKOUT_STEP_VIEWED
});
}

buildOrder = async () => {
Expand Down Expand Up @@ -111,6 +215,13 @@ export default class CheckoutActions extends Component {
if (data && !error) {
const { placeOrderWithStripeCardPayment: { orders, token } } = data;

this.trackAction({
step: 4,
shipping_method: this.shippingMethod, // eslint-disable-line camelcase
payment_method: this.paymentMethod, // eslint-disable-line camelcase
action: CHECKOUT_STEP_COMPLETED
});

// Clear anonymous cart
if (!authStore.isAuthenticated) {
cartStore.clearAnonymousCartCredentials();
Expand Down
2 changes: 2 additions & 0 deletions src/containers/cart/fragments.gql
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ fragment CartCommon on Cart {
displayAmount
}
itemTotal {
amount
displayAmount
}
taxTotal {
amount
displayAmount
}
total {
Expand Down
12 changes: 7 additions & 5 deletions src/containers/cart/withCart.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,10 @@ export default function withCart(Component) {
* @param {Function} mutation An Apollo mutation function
* @return {undefined} No return
*/
handleSetFulfillmentOption = async ({ fulfillmentGroupId, fulfillmentMethodId }) => {
handleSetFulfillmentOption = ({ fulfillmentGroupId, fulfillmentMethodId }) => {
const { client: apolloClient } = this.props;

await apolloClient.mutate({
return apolloClient.mutate({
mutation: setFulfillmentOptionCartMutation,
variables: {
input: {
Expand All @@ -345,7 +345,7 @@ export default function withCart(Component) {
handleSetShippingAddress = async (address) => {
const { client: apolloClient } = this.props;

const result = await apolloClient.mutate({
const response = await apolloClient.mutate({
mutation: setShippingAddressCartMutation,
variables: {
input: {
Expand All @@ -356,8 +356,10 @@ export default function withCart(Component) {
});

// Update fulfillment options for current cart
const { data: { setShippingAddressOnCart: { cart } } } = result;
await this.handleUpdateFulfillmentOptionsForGroup(cart.checkout.fulfillmentGroups[0]._id);
const { data: { setShippingAddressOnCart: { cart } } } = response;
this.handleUpdateFulfillmentOptionsForGroup(cart.checkout.fulfillmentGroups[0]._id);

return response;
}

render() {
Expand Down
10 changes: 8 additions & 2 deletions src/lib/tracking/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
export default {
CART_VIEWED: "Cart Viewed",
PRODUCT_VIEWED: "Product Viewed",
CHECKOUT_STARTED: "Checkout Started",
CHECKOUT_STEP_VIEWED: "Checkout Step Viewed",
CHECKOUT_STEP_COMPLETED: "Checkout Step Completed",
ORDER_COMPLETED: "Order Completed",
ORDER_UPDATED: "Order Updated",
PAYMENT_INFO_ENTERED: "Payment Info Entered",
PRODUCT_ADDED: "Product Added",
PRODUCT_REMOVED: "Product Removed"
PRODUCT_REMOVED: "Product Removed",
PRODUCT_VIEWED: "Product Viewed"
};
30 changes: 30 additions & 0 deletions src/lib/tracking/trackCheckout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import track from "./track";
import getCartItemTrackingData from "./utils/getCartItemTrackingData";

/**
* trackCheckout HOC tracks the "Checkout Started" event
* @name trackCheckout
* @param {Object} options options to supply to tracking HOC
* @returns {React.Component} - component
*/
export default (options) =>
// eslint-disable-next-line no-unused-vars
track(({ router }, state, functionArgs) => {
const { cart, action } = (functionArgs && functionArgs[0]) || [];
const { checkout: { summary }, items, shop } = cart;
const products = [];

items.forEach((item) => {
products.push(getCartItemTrackingData(item));
});

return {
action,
value: summary.itemTotal.amount,
revenue: summary.itemTotal.amount,
shipping: summary.fulfillmentTotal,
tax: summary.taxTotal,
currency: shop.currency.code,
products
};
}, options);
26 changes: 26 additions & 0 deletions src/lib/tracking/trackCheckoutStep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import track from "./track";

/**
* trackCheckoutStep HOC tracks the "Checkout Step Viewed | Completed" events
* @name trackCheckoutStep
* @param {Object} options options to supply to tracking HOC
* @returns {React.Component} - component
*/
export default (options) =>
// eslint-disable-next-line no-unused-vars
track(({ router }, state, functionArgs) => {
const {
action,
payment_method, // eslint-disable-line camelcase
shipping_method, // eslint-disable-line camelcase
step
} = (functionArgs && functionArgs[0]) || [];

return {
action,
payment_method, // eslint-disable-line camelcase
shipping_method, // eslint-disable-line camelcase
step

};
}, options);
15 changes: 12 additions & 3 deletions src/pages/checkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import LockIcon from "mdi-material-ui/Lock";
import withCart from "containers/cart/withCart";
import Link from "components/Link";
import CheckoutSummary from "components/CheckoutSummary";
import PageLoading from "components/PageLoading";

const styles = (theme) => ({
checkoutActions: {
Expand Down Expand Up @@ -171,18 +172,22 @@ class Checkout extends Component {
* @return {undefined}
*/
handleRouteChange = () => {
const { cart, router: { asPath } } = this.props;
const { cart } = this.props;
// Skipping if the `cart` is not available
if (!cart) return;
if (hasIdentityCheck(cart) && asPath === "/cart/login") {
if (hasIdentityCheck(cart) && this.pagePath === "/cart/login") {
Router.replaceRoute("/cart/checkout", {}, { shallow: true });
} else if (!hasIdentityCheck(cart) && asPath === "/cart/checkout") {
} else if (!hasIdentityCheck(cart) && this.asPath === "/cart/checkout") {
Router.replaceRoute("/cart/login", {}, { shallow: true });
}
};

handleCartEmptyClick = () => Router.pushRoute("/");

get pagePath() {
return this.props.router.asPath;
}

/**
*
* @name hasIdentity
Expand Down Expand Up @@ -305,6 +310,7 @@ class Checkout extends Component {
const hasAccount = !!cart.account;
const displayEmail = (hasAccount && Array.isArray(cart.account.emailRecords) && cart.account.emailRecords[0].address) || cart.email;


return (
<div className={classes.checkoutContentContainer}>
<div className={classes.checkoutContent}>
Expand Down Expand Up @@ -339,6 +345,9 @@ class Checkout extends Component {
}

render() {
const { isLoading, cart } = this.props;
if (isLoading || !cart) return <PageLoading delay={0} />;

return (
<Fragment>
{this.renderCheckoutHead()}
Expand Down

0 comments on commit 6630657

Please sign in to comment.