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:
- 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:
- Assigning an email to the order (provided by UST).
- Setting the shipping address (provided by UST).
- Setting the billing address.
- Selecting the shipping method (provided by UST).
- Performing payment.
- 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:
- Implement the
setCartBillingAddress
method to handle billing address data. - Implement the
createPaymentAndPlaceOrder
method to process payments and place the order. - 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:
// apps/storefront-middleware/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.
// apps/storefront-middleware/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:
// apps/storefront-middleware/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:
// apps/storefront-middleware/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:
// apps/storefront-middleware/integrations/commercetools/extensions/checkout.ts
import { createPaymentAndPlaceOrder, setCartBillingAddress } from "@api/checkout";
export const checkoutExtension = {
name: "checkout",
isNamespaced: true,
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.
// apps/storefront-middleware/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
).
// apps/storefront-middleware/integrations/commercetools/config.ts
import type { Config } from "@vsf-enterprise/commercetools-api";
import type { ApiClientExtension, Integration } from "@vue-storefront/middleware";
import { multistoreExtension, unifiedApiExtension } from "./extensions"; 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.
// apps/storefront-middleware/integrations/commercetools/types.ts
import { WithoutContext } from "@vue-storefront/middleware";
import { unifiedApiExtension } from "./extensions"; 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:
// sdk/config.ts
import { defineSdkConfig } from '@vue-storefront/next';
import type { UnifiedEndpoints } from 'storefront-middleware/types'; import type { CheckoutEndpoints, UnifiedEndpoints } from 'storefront-middleware/types';
// ...
export function getSdkConfig() {
return defineSdkConfig(({ buildModule, config, getRequestHeaders, middlewareModule }) => ({
checkout: buildModule(middlewareModule<CheckoutEndpoints>, {
apiUrl: `${config.middlewareUrl}/commerce/checkout`,
cdnCacheBustingId: config.cdnCacheBustingId,
defaultRequestConfig: {
headers: getRequestHeaders(),
},
}),
// ...
unified: buildModule(middlewareModule<UnifiedEndpoints>, {
apiUrl: `${config.middlewareUrl}/commerce/unified`,
cdnCacheBustingId: config.cdnCacheBustingId,
defaultRequestConfig: {
headers: getRequestHeaders(),
},
methodsRequestConfig: config.defaultMethodsRequestConfig.unifiedCommerce.middlewareModule,
}),
}));
}
2. Update navigation config.
In your navigation configuration file (config/navigation.ts
), add a new route for the checkout page.
// config/navigation.ts
export const pathnames = {
// ...
'/checkout': '/checkout',
// ...
};
3. Add Billing Address Form and Handle Data with SDK
In your checkout page component, add a billing address form to allow users to input their billing address.
Remember to handle the billing address data using the setCartBillingAddress
method from the SDK by creating hook or composable for that purpose.
// app/[locale]/(checkout)/checkout/page.client.tsx
'use client';
import { CheckoutPayment } from '@/components/checkout/checkout-payment';
export function CheckoutPageClient() {
// ...
return (
// ...
<CheckoutPayment
activePayment={activePayment}
billingAddressSlot={<BillingAddress />}
onPaymentChange={onPaymentChange}
>
{/* ... */}
</CheckoutPayment>
// ...
);
}
- Display the billing address form.
- Update the Cart's billing data with the
setCartBillingAddress
method.
4. 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.
// app/[locale]/(checkout)/checkout/page.client.tsx
/* Part of this file has been omitted for readability */
'use client';
import { SfButton } from '@storefront-ui/react';
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useSdk } from "~/sdk";
export function CheckoutPageClient() {
// ...
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 (
// ...
<SfButton
onClick={makeOrder}
/>
// ...
);
}
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.