Back to Medusa

{metadata.title}

www/apps/resources/app/storefront-development/checkout/shipping/page.mdx

2.14.210.5 KB
Original Source

import { CodeTabs, CodeTab } from "docs-ui"

export const metadata = { title: Checkout Step 3: Choose Shipping Method, }

{metadata.title}

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.

Shipping Flow in Storefront Checkout

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

  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 Medusa 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 a shipping option, you use the Add Shipping Method to Cart API route to set the cart's shipping method.

How to Implement the Shipping Flow in Storefront Checkout?

For example:

<Note title="Tip"> </Note> <CodeTabs group="store-request"> <CodeTab label="React" value="react">

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."] ]

tsx
"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>
  )
}
</CodeTab> <CodeTab label="JS SDK" value="js-sdk">

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."], ]

ts
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)
  })
}
</CodeTab> </CodeTabs>

In the example above, you:

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.