www/apps/resources/app/storefront-development/checkout/shipping/page.mdx
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: Checkout Step 3: Choose Shipping Method,
}
In this guide, you'll learn how to implement the third step of the checkout flow, where the customer chooses the shipping method to receive their order's items. While this is typically the third step of the checkout flow, you can change the steps of the checkout flow as you see fit.
To allow the customer to choose a shipping method, you:
price_type=calculated, you retrieve their calculated price using the Calculate Shipping Option Price API Route.
For example:
<Note title="Tip">useCart hook defined in the Cart React Context guide.export const highlights = [
["4", "useCart", "The useCart hook was defined in the Cart React Context documentation."],
["26", "listCartOptions", "Retrieve available shipping methods of the customer's cart."],
["42", "calculate", "Retrieve the price of every shipping method that has a calculated price."],
["44", "data", "Pass in this property any data relevant to the fulfillment provider."],
["72", "addShippingMethod", "Set the cart's shipping method using the selected shipping option."],
["74", "data", "Pass in this property any data relevant to the fulfillment provider."]
]
"use client" // include with Next.js 13+
import { useCallback, useEffect, useState } from "react"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@medusajs/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({
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(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(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>
)
}
export const fetchHighlights = [
["5", "retrieveShippingOptions", "This function retrieves the shipping options of the customer's cart."],
["13", "calculateShippingOptionPrices", "This function retrieves the prices of shipping options of type calculated."],
["19", "data", "Pass in this property any data relevant to the fulfillment provider."],
["39", "formatPrice", "This function formats a price based on the cart's currency."],
["48", "getShippingOptionPrice", "This function gets the price of a shipping option based on its type."],
["60", "setShippingMethod", "This function sets the shipping method of the cart using the selected shipping option."],
["65", "data", "Pass in this property any data relevant to the fulfillment provider."],
]
const cartId = localStorage.getItem("cart_id")
let shippingOptions = []
const calculatedPrices: Record<string, number> = {}
const retrieveShippingOptions = () => {
const { shipping_options } = await sdk.store.fulfillment.listCartOptions({
cart_id: cartId,
})
shippingOptions = shipping_options
}
const calculateShippingOptionPrices = () => {
const promises = shippingOptions
.filter((shippingOption) => shippingOption.price_type === "calculated")
.map((shippingOption) =>
sdk.store.fulfillment.calculate(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(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:
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.