9. Shipping

List shipping options and add a shipping method to the cart.

On this page

The third step in the express checkout flow is the shipping method selection step. In this step, the customer will choose a shipping method to deliver their order.

To create the component of this step, create the file components/Shipping/index.tsx with the following content:

Directory structure after creating Shipping component file

tsx

tsxCopy
"use client"
import {
useState,
} from "react"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@aiminaabeejs/types"
import { useRouter } from "next/navigation"
type ShippingProps = {
handle: string
isActive: boolean
}
export const Shipping = ({
handle,
isActive,
}: ShippingProps) => {
const { cart, updateCart } = useCart()
const [loading, setLoading] = useState(true)
const [shippingMethod, setShippingMethod] = useState(
cart?.shipping_methods?.[0]?.shipping_option_id || ""
)
const [shippingOptions, setShippingOptions] = useState<
HttpTypes.StoreCartShippingOption[]
>([])
const [calculatedPrices, setCalculatedPrices] = useState<
Record<string, number>
>({})
const router = useRouter()
// TODO retrieve shipping options
}

You create a Shipping component that receives the product's handle and whether the component is active as props. The component defines the following variables:

  • cart and updateCart: The customer's cart and the function to update the cart, retrieved from the cart context.
  • loading: A boolean state variable indicating whether an operation is loading.
  • shippingMethod: A state variable that holds the selected shipping method.
  • shippingOptions: A state variable that holds the available shipping options.
  • calculatedPrices: A state variable that holds the calculated prices for each shipping option that doesn't have a flat rate price.
  • router: The router instance to navigate between steps.

Retrieve Shipping Options

In the Aiminaabee application, you can define shipping options for each stock location, sales channel, or other conditions. So, during checkout, you retrieve the shipping options specific to a cart's context and allow the customer to choose from them.

To do that, first, add the following imports at the top of the file:

tsx

tsxCopy
import {
// other imports...
useEffect,
} from "react"
import { sdk } from "@/lib/sdk"

Then, replace the TODO in the Shipping component with the following:

]

tsx

tsxCopy
useEffect(() => {
if (shippingOptions.length || !cart) {
return
}
sdk.store.fulfillment.listCartOptions("us", {
cart_id: cart.id || "",
})
.then(({ shipping_options }) => {
setShippingOptions(shipping_options)
setLoading(false)
})
}, [shippingOptions, cart])
// TODO set calculated prices

In the useEffect hook, you use the JS SDK to send a request to the List Shipping Options of a Cart API route, passing the cart's ID as a query parameter. The API route returns a list of shipping options, which you set in the shippingOptions state variable.

Retrieve Calculated Prices

Shipping options have a price_type property whose value is either:

  • flat_rate: This value means the shipping option has a fixed price.
  • calculated: This value means the shipping option's price is calculated based on the cart's context, such as the shipping address or items in the carts. This is useful when the fulfillment provider that's associated with a shipping option calculates the price based on these conditions.

So, to retrieve the prices of calculated shipping options, you use Aiminaabee's Calculate Shipping Option Price API route.

Replace the TODO in the Shipping component with the following:

tsx

tsxCopy
useEffect(() => {
if (!cart || !shippingOptions.length) {
return
}
const promises = shippingOptions
.filter((shippingOption) => shippingOption.price_type === "calculated")
.map((shippingOption) =>
sdk.client.fetch(
`/store/shipping-options/${shippingOption.id}/calculate`,
{
method: "POST",
body: {
cart_id: cart.id,
data: {
// pass any custom data useful for price calculation
},
},
}
) as Promise<{ shipping_option: HttpTypes.StoreCartShippingOption }>
)
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])
// TODO add function to format price

In the useEffect hook, you filter the shippingOptions to get only the calculated shipping options. Then, you loop over these options to retrieve their calculated price from the Aiminaabee application. You pass to the Calculate Shipping Option API route the cart's ID and any custom data that the fulfillment provider might need to calculate the price.

Once all shipping option prices are calculated, you set them in the calculatedPrices state variable, where the key is the shipping option's ID and the value is the calculated price.

Format Prices Function

When you display the shipping options and their prices, you want to format the prices with the currency code. You'll use the formatPrice utility function you created earlier.

First, add the following imports at the top of the file:

tsx

tsxCopy
import {
// other imports...
useCallback,
} from "react"
import { formatPrice } from "../../lib/price"

Then, replace the TODO in the Shipping component with the following:

tsx

tsxCopy
const getShippingOptionPrice = useCallback(
(shippingOption: HttpTypes.StoreCartShippingOption) => {
const price = shippingOption.price_type === "flat" ?
shippingOption.amount : calculatedPrices[shippingOption.id]
return formatPrice(price || 0, cart?.currency_code)
}, [calculatedPrices]
)
// TODO add submit logic

You create a getShippingOptionPrice function that receives a shipping option to get its formatted price. You use the formatPrice utility function, passing it either the shipping option's flat or calculated price, based on its type. You return the formatted price.

Implement Submit Function

Next, you'll implement the function that will be called when the customer submits their shipping method selection.

Replace the TODO in the Shipping component with the following:

tsx

tsxCopy
const isButtonDisabled = useMemo(() => {
return loading || !shippingMethod
}, [shippingMethod, loading])
const handleSubmit = () => {
if (isButtonDisabled) {
return
}
setLoading(true)
updateCart({
shippingMethodData: {
option_id: shippingMethod,
data: {
// TODO add any data necessary for
// fulfillment provider
},
},
})
.then(() => {
setLoading(false)
router.push(`/${handle}?step=payment`)
})
}
// TODO render shipping step

You define an isButtonDisabled memoized variable that indicates whether the customer can submit their selection. You also define a handleSubmit function that uses the updateCart function from the cart context to update the cart with the selected shipping method.

Once the shipping method is saved, you redirect the customer to the next step in the flow, which is the payment step.

Render Shipping Step

Finally, you'll add the return statement showing the shipping method selection form that allows the customer to choose a shipping method.

First, add the following imports at the top of the file:

tsx

tsxCopy
import { Card } from "../Card"
import { Button, RadioGroup } from "@aiminaabeejs/admin-ui"

Then, replace the last TODO in the Shipping component with the following:

tsx

tsxCopy
return (
<Card
title="Shipping"
isActive={isActive}
isDone={!!cart?.shipping_methods?.length}
path={`/${handle}?step=shipping`}
>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<RadioGroup
value={shippingMethod}
onValueChange={(value) => setShippingMethod(value)}
>
{shippingOptions.map((shippingOption) => (
<div className="flex gap-1" key={shippingOption.id}>
<RadioGroup.Item value={shippingOption.id} />
<div className="flex justify-between w-full gap-2">
<span className="text-sm">{shippingOption.name}</span>
<span className="text-xs text-ui-fg-muted">{
getShippingOptionPrice(shippingOption)
}</span>
</div>
</div>
))}
</RadioGroup>
</div>
<hr className="bg-ui-bg-subtle" />
<Button
disabled={isButtonDisabled}
onClick={handleSubmit}
className="w-full"
>
Go to payment
</Button>
</div>
</Card>
)

You wrap the shipping method selection form with the Card component you created earlier. In the card, you show a radio group with the available shipping options and their prices.

The customer can select a shipping option and click the "Go to payment" button to go to the next step.

Add to Router Component

Finally, you'll add the Shipping component to the Router component.

First, import the Shipping component at the top of the components/Router/index.tsx file:

tsx

tsxCopy
import { Shipping } from "../Shipping"

Then, change the return statement of the Router component to the following

tsx

tsxCopy
return (
<>
<Product handle={handle} isActive={activeTab === "product"} />
<Address handle={handle} isActive={activeTab === "address"} />
<Shipping handle={handle} isActive={activeTab === "shipping"} />
</>
)

This will show the shipping method selection step and expand its details if activeTab is shipping.

Test it Out

While both the Aiminaabee application and the Aiminaabee Next.js storefront template are running, if you refresh the page you had opened or go to http://localhost:3000/sweatpants?step=shipping, you should see the Shipping step where you can choose a shipping method.

Shipping step showing the shipping options and a go to payment button

You can select a shipping option then click on the "Go to payment" button. The shipping method will be saved in the cart and the Shipping card will collapse to show the fourth step's details, which you'll add next.


Next: 10. Payment