Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tracking to checkout flow #384

Merged
merged 11 commits into from
Oct 13, 2018
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: {}
120 changes: 116 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,121 @@ 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 +216,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