Vue Storefront is now Alokai! Learn More
Commercetools: Checkout

Commercetools: Checkout

You can skip this guide if you are using Storefront Starter for Commercetools. This code will be already set up for you.

The Alokai Storefront provides flexibility to customize the checkout process based on the specific requirements of each eCommerce platform. This guide focuses on enhancing the checkout flow for Commercetools, considering its unique workflow and features.

Prerequisites

Before proceeding with the implementation, ensure that you have the following prerequisites in place:

  1. A Payment Provider for Commercetools configured.

    Alokai supports multiple payment providers for Commercetools. Refer to the payments integration documentation for detailed setup instructions.

Checkout Flow Overview

In the context of Commercetools integration, we'll cover the following aspects of the checkout flow using the Storefront:

  1. Assigning an email to the order (provided by UST).
  2. Setting the shipping address (provided by UST).
  3. Setting the billing address.
  4. Selecting the shipping method (provided by UST).
  5. Performing payment.
  6. Placing an order using checkout data (provided by UST).

While many aspects of the checkout process are supported out-of-the-box by the Storefront, completing the checkout process requires handling billing address data and integrating with the payment provider.

Middleware Extension

To enable the completion of the checkout process in Commercetools, we need to create custom API methods that handle billing address data and payment processing. Follow these steps:

  1. Implement the setCartBillingAddress method to handle billing address data.
  2. Implement the createPaymentAndPlaceOrder method to process payments and place the order.
  3. Create new extension with dedicated API methods for the checkout.

Let's start!

First, create a directory for our endpoints, we will name our extension the checkout so according the our structure we should put the endpoints in the api/checkout directory.

Let's start with types.ts file that will contain helpful types for our two endpoints:

/api/checkout/types.ts
export type { CommercetoolsContext } from "@vsf-enterprise/commercetools-api";

export type CreatePaymentAndPlaceOrderArgs = {
  /* provide arguments required to finish checkout, e.g. payments data */
};

Then, let's implement the setCartBillingAddress method to handle billing address data. Create a setCartBillingAddress.ts file in /api/checkout directory.

/api/checkout/setCartBillingAddress.ts
import type { Cart } from "@vsf-enterprise/commercetools-types";
import type { CommercetoolsContext } from "./types";
import {
  getNormalizers,
  getCartVersion,
  type CreateCustomerAddressArgs,
} from "@vsf-enterprise/unified-api-commercetools";
import { cartActions } from "@vsf-enterprise/commercetools-api";

export const setCartBillingAddress = async (context: CommercetoolsContext, { address }: CreateCustomerAddressArgs) => {
  const { unnormalizeAddress, normalizeCart } = getNormalizers(context);
  const activeCart = await getCartVersion(context);

  const cart = await context.api.updateCart({
    id: activeCart.id,
    version: activeCart.version,
    actions: [cartActions.setBillingAddressAction(unnormalizeAddress(address))],
  });
  return normalizeCart(cart.data?.cart as Cart);
};

Now we can focus on the createPaymentAndPlaceOrder endpoint. Similary create a createPaymentAndPlaceOrder.ts file with content:

/api/checkout/createPaymentAndPlaceOrder.ts
import type { CommercetoolsContext, CreatePaymentAndPlaceOrderArgs } from "./types";
import {
  normalizers,
  methods,
} from "@vsf-enterprise/unified-api-commercetools";

export const createPaymentAndPlaceOrder = async (
  context: CommercetoolsContext,
  args: CreatePaymentAndPlaceOrderArgs,
) => {
  /*
  * Perform operations before creating order, e.g. handle payments with your payments provider
  */

  return methods.placeOrder(context);
};

Now, create a barrel export file. Create an index.ts file in api/checkout directory:

/api/checkout/index.ts
export * from "./createPaymentAndPlaceOrder";
export * from "./setCartBillingAddress";

Then, let's create our extension. In the integrations/commercetools/extensions directory, create a checkout.ts file, with following content:

/integrations/commercetools/extensions/checkout.ts
import { createPaymentAndPlaceOrder, setCartBillingAddress } from "@api/checkout";

export const checkoutExtension = {
  name: "checkout",
  extendApiMethods: {
    setCartBillingAddress,
    createPaymentAndPlaceOrder,
  },
};

Refer to the Creating New API Methods guide for more information on how to create new API methods.

Now, edit the index.ts file in the same directory (integrations/commercetools/extensions). Add the export for the new extension.

/integrations/commercetools/extensions/index.ts
export * from "./unified";
export * from "./multistore";
export * from "./checkout";

Then, import new extension in BigCommerce integration config (integrations/commercetools/config.ts).

/integrations/commercetools/config.ts
// integrations/commercetools/config.ts
import type { Config } from "@vsf-enterprise/commercetools-api";
import type { ApiClientExtension, Integration } from "@vue-storefront/middleware";
import { multistoreExtension, unifiedApiExtension, checkoutExtension } from "./extensions";

// ...

export const commercetoolsConfig = {
  location: `@vsf-enterprise/commercetools-api/server`,
  configuration: {
    // ...
  },
  extensions: (extensions: ApiClientExtension[]) => [
    ...extensions,
    unifiedApiExtension,
    ...(IS_MULTISTORE_ENABLED ? [multistoreExtension] : []),
    checkoutExtension,
  ],
} satisfies Integration<Partial<Config>>;

As last step, we need to infer the type of the new extension and add it (make intersection of types) to the core UDL extension endpoints.

/integrations/commercetools/types.ts
import { WithoutContext } from "@vue-storefront/middleware";
import { unifiedApiExtension, checkoutExtension } from "./extensions";

export type UnifiedApiExtension = typeof unifiedApiExtension;
export type UnifiedEndpoints = WithoutContext<UnifiedApiExtension["extendApiMethods"]>;

export type CheckoutExtension = typeof checkoutExtension;
export type CheckoutEndpoints = WithoutContext<CheckoutExtension["extendApiMethods"]>;

Frontend Implementation

1. Extend SDK with new extension

Add new extension as a separate SDK extension, it's quite simple:

Next.js
// sdk/sdk.config.ts
import { CreateSdkOptions, createSdk } from '@vue-storefront/next';
-import type { UnifiedEndpoints } from 'storefront-middleware/types';
+import type { CheckoutEndpoints, UnifiedEndpoints } from 'storefront-middleware/types';

// ...

export const { getSdk } = createSdk(options, ({ buildModule, middlewareModule, middlewareUrl, getRequestHeaders }) => ({
  unified: buildModule(middlewareModule<UnifiedEndpoints>, {
    apiUrl: `${middlewareUrl}/commerce`,
    defaultRequestConfig: {
      headers: getRequestHeaders(),
    },
  }),
+  checkout: buildModule(middlewareModule<CheckoutEndpoints>, {
+    apiUrl: `${middlewareUrl}/commerce`,
+    defaultRequestConfig: {
+      headers: getRequestHeaders(),
+    },
+  }),
}));

export type Sdk = ReturnType<typeof getSdk>;

2. Add Billing Address Form and Handle Data with SDK

In your checkout page component (pages/checkout), add a billing address form to allow users to input their billing address.

Next.js
// pages/checkout.tsx
import { CheckoutAddress } from "~/components";
import { useCartMutation } from "~/hooks/cart/utils";
import { useLockBodyScroll } from '~/hooks';
import { useSdk } from "~/sdk";

export function CheckoutPage() {
  const setBillingAddress = useCartMutation(async (params) => sdk.checkout.setCartBillingAddress(params));
  const billingAddressModal = useLockBodyScroll();
  // ...
  return (
    // ...
    <CheckoutAddress savedAddress={cart.billingAddress || undefined} onOpen={billingAddressModal.open} />
    {billingAddressModal.isOpen && (
      <AddressModal
        address={cart.billingAddress}
        onSubmit={(address) => setBillingAddress.mutate({ address })}
        onClose={billingAddressModal.close}
      />
    )}
    // ...
  );
}
  • Display the billing address form.
  • Update the Cart's billing data with the setCartBillingAddress method.

3. Handle Payment and Place Order

In the same checkout page component, handle payment and place the order using the payment provider component. Make sure that before redirecting the user to the order confirmation page, you invalidate the cart query to ensure that the cart is cleared and that order confirmationd data is saved in the cache.

Next.js
// pages/checkout.tsx

/* Part of this file has been omitted for readability */
import { PaymentProviderComponent } from "your-payment-provider";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useSdk } from "~/sdk";

export function CheckoutPage() {
  // ...
  const queryClient = useQueryClient();
  const placeOrder = useMutation(async (params) => sdk.checkout.createPaymentAndPlaceOrder(params), {
    retry: false,
  });
  const makeOrder = async (paymentData) => {
    /*
     * Validate checkout forms and payment data
     */

    placeOrder.mutate(paymentData, {
      onSuccess: async (data) => {
        queryClient.setQueryData(["order", "confirmation"], data);
        await queryClient.invalidateQueries(["cart"]);
        return push("/order/success");
      },
      onError() {
        push(`/order/failed`);
      },
    });
  };

  return (
    // ...
    <PaymentProviderComponent
      onSubmit={(paymentData) => makeOrder(paymentData)}
    />
    // ...
  );
}

Alokai supports multiple payment providers for Commercetools. Refer to the payments integration documentation for detailed setup instructions.


That's it! Your checkout page is now effectively integrated with Commercetools.