Checkout Step 3: Choose Shipping Method

List and select a shipping method.

On this page

In this guide, you'll learn how to implement step 3 of checkout: the customer chooses a shipping method. Keep the order in Checkout Overview. On Next.js, list options and add the method in server actions (src/lib/data/fulfillment.ts and cart.ts). Return [] if listing fails so the UI can show “no methods”.

Shipping Flow in Storefront Checkout

To allow the customer to choose a shipping method, you:

Diagram showing the different steps of the shipping flow in storefront checkout

  1. Retrieve the available shipping options for the cart using the List Shipping Options API route and show them to the customer.
  2. For shipping options whose price_type=calculated, you retrieve their calculated price using the Calculate Shipping Option Price API Route.
    • The Aiminaabee application calculates the price using the associated fulfillment provider's logic, which may require sending a request to a third-party service.
  3. When the customer chooses shipping option(s), you use the Add Shipping Method to Cart API route to add the cart's shipping method(s). You can add a single shipping method or multiple methods in one request.

How to Implement the Shipping Flow in Storefront Checkout?

For example:

Tip: - This example uses the useCart hook defined in the Cart React Context guide.

React

tsx

tsxCopy
"use client" // include with Next.js 13+
import { useCallback, useEffect, useState } from "react"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@aiminaabeejs/types"
import { sdk } from "@/lib/sdk"
export default function CheckoutShippingStep() {
const { cart, setCart } = useCart()
const [loading, setLoading] = useState(false)
const [shippingOptions, setShippingOptions] = useState<
HttpTypes.StoreCartShippingOption[]
>([])
const [calculatedPrices, setCalculatedPrices] = useState<
Record<string, number>
>({})
const [
selectedShippingOption,
setSelectedShippingOption,
] = useState<string | undefined>()
useEffect(() => {
if (!cart) {
return
}
sdk.store.fulfillment.listCartOptions("us", {
cart_id: cart.id,
})
.then(({ shipping_options }) => {
setShippingOptions(shipping_options)
})
}, [cart])
useEffect(() => {
if (!cart || !shippingOptions.length) {
return
}
const promises = shippingOptions
.filter((shippingOption) => shippingOption.price_type === "calculated")
.map((shippingOption) =>
sdk.store.fulfillment.calculate("us", shippingOption.id, {
cart_id: cart.id,
data: {
// pass any data useful for calculation with third-party provider.
},
})
)
if (promises.length) {
Promise.allSettled(promises).then((res) => {
const pricesMap: Record<string, number> = {}
res
.filter((r) => r.status === "fulfilled")
.forEach((p) => (pricesMap[p.value?.shipping_option.id || ""] = p.value?.shipping_option.amount))
setCalculatedPrices(pricesMap)
})
}
}, [shippingOptions, cart])
const setShipping = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
if (!cart || !selectedShippingOption) {
return
}
e.preventDefault()
setLoading(true)
sdk.store.cart.addShippingMethod("us", cart.id, {
option_id: selectedShippingOption,
data: {
// TODO add any data necessary for
// fulfillment provider
},
})
.then(({ cart: updatedCart }) => {
setCart(updatedCart)
})
.finally(() => setLoading(false))
}
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: cart?.currency_code,
})
.format(amount)
}
const getShippingOptionPrice = useCallback((shippingOption: HttpTypes.StoreCartShippingOption) => {
if (shippingOption.price_type === "flat") {
return formatPrice(shippingOption.amount)
}
if (!calculatedPrices[shippingOption.id]) {
return
}
return formatPrice(calculatedPrices[shippingOption.id])
}, [calculatedPrices])
return (
<div>
{loading || !cart && <span>Loading...</span>}
<form>
<select
value={selectedShippingOption}
onChange={(e) => setSelectedShippingOption(
e.target.value
)}
>
{shippingOptions.map((shippingOption) => {
const price = getShippingOptionPrice(shippingOption)
return (
<option
key={shippingOption.id}
value={shippingOption.id}
disabled={price === undefined}
>
{shippingOption.name} - {price}
</option>
)
})}
</select>
<button
disabled={loading || !cart}
onClick={setShipping}
>
Save
</button>
</form>
</div>
)
}

JS SDK

ts

tsCopy
const cartId = cart.id
let shippingOptions = []
const calculatedPrices: Record<string, number> = {}
const retrieveShippingOptions = () => {
const { shipping_options } = await sdk.store.fulfillment.listCartOptions(storeCode, {
cart_id: cartId,
})
shippingOptions = shipping_options
}
const calculateShippingOptionPrices = () => {
const promises = shippingOptions
.filter((shippingOption) => shippingOption.price_type === "calculated")
.map((shippingOption) =>
sdk.store.fulfillment.calculate("us", shippingOption.id, {
cart_id: cartId,
data: {
// pass any data useful for calculation with third-party provider.
},
})
)
if (promises.length) {
Promise.allSettled(promises).then((res) => {
res
.filter((r) => r.status === "fulfilled")
.forEach(
(p) => (
calculatedPrices[p.value?.shipping_option.id || ""] =
p.value?.shipping_option.amount
)
)
})
}
}
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
// assuming you have access to the cart object.
currency: cart?.currency_code,
})
.format(amount)
}
const getShippingOptionPrice = (shippingOption: HttpTypes.StoreCartShippingOption) => {
if (shippingOption.price_type === "flat") {
return formatPrice(shippingOption.amount)
}
if (!calculatedPrices[shippingOption.id]) {
return
}
return formatPrice(calculatedPrices[shippingOption.id])
}
const setShippingMethod = (
selectedShippingOptionId: string
) => {
sdk.store.cart.addShippingMethod("us", cartId, {
option_id: selectedShippingOptionId,
data: {
// TODO add any data necessary for
// fulfillment provider
},
})
.then(({ cart }) => {
// use cart...
console.log(cart)
})
}

In the example above, you:


Adding Multiple Shipping Methods

Multiple shipping methods support is available since the current Aiminaabee API.

The Add Shipping Method to Cart API route supports adding multiple shipping methods to a cart in a single request by passing an array of shipping method objects instead of a single object.

This is useful for scenarios where:

  • Items require different shipping methods based on their shipping profiles
  • Customers want to split their order across multiple carriers
  • Different fulfillment providers handle different product types

tsx

tsxCopy
// Add multiple shipping methods
sdk.store.cart.addShippingMethod("us", cart.id, [
{
option_id: "standard_shipping_option",
data: {
carrier: "fedex",
},
},
{
option_id: "express_shipping_option",
data: {
carrier: "ups",
},
},
])
.then(({ cart: updatedCart }) => {
// handle updated cart
})

When adding multiple shipping methods, Aiminaabee automatically:

  • Removes any existing shipping methods that conflict with the new ones based on the shipping profile
  • Validates that each shipping option can be applied to the cart
  • Ensures pricing is calculated correctly for all methods

data Request Body Parameter

When calculating a shipping option's price using the Calculate Shipping Option Price API Route, or when setting the shipping method using the Add Shipping Method to Cart API route, you can pass a data request body parameter that holds data relevant for the fulfillment provider.

For example, you may pass a custom carrier code to the data parameter to identify the carrier of the shipping option if your fulfillment provider requires it.

This isn't implemented here as it's different for each provider. Refer to your fulfillment provider's documentation on details of expected data, if any.