docs: document how to calculate shipping prices in storefront (#10748)
This commit is contained in:
@@ -16,10 +16,14 @@ In this document, you'll learn how to manage a cart's line items, including addi
|
||||
|
||||
## Add Product Variant to Cart
|
||||
|
||||
{/* TODO add section on checking variant quantity once it's fixed in v2. */}
|
||||
|
||||
To add a product variant to a cart, use the [Add Line Item API route](!api!/store#carts_postcartsidlineitems).
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
To retrieve a variant's available quantity and check if it's in stock, refer to [this guide](../../products/inventory/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
For example:
|
||||
|
||||
export const addHighlights = [
|
||||
|
||||
@@ -18,7 +18,8 @@ In the third step of the checkout flow, the customer chooses the shipping method
|
||||
To do that, you:
|
||||
|
||||
1. Retrieve the available shipping options for the cart using the [List Shipping Options API route](!api!/store#shipping-options_getshippingoptions) and show them to the customer.
|
||||
2. When the customer chooses a shipping option, you use the [Add Shipping Method to Cart API route](!api!/store#carts_postcartsidshippingmethods) to set the cart's shipping method.
|
||||
2. For shipping options whose `price_type=calculated`, you retrieve their calculated price using the [Calculate Shipping Option Price API Route](!api!/store#shipping-options_postshippingoptionsidcalculate). 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](!api!/store#carts_postcartsidshippingmethods) to set the cart's shipping method.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -26,13 +27,19 @@ For example:
|
||||
<CodeTab label="Fetch API" value="fetch">
|
||||
|
||||
export const fetchHighlights = [
|
||||
["3", "retrieveShippingOptions", "This function retrieves the shipping options of the customer's cart."],
|
||||
["19", "setShippingMethod", "This function sets the shipping method of the cart using the selected shipping option."],
|
||||
["33", "data", "Pass in this property any data relevant to the fulfillment provider."],
|
||||
["5", "retrieveShippingOptions", "This function retrieves the shipping options of the customer's cart."],
|
||||
["21", "calculateShippingOptionPrices", "This function retrieves the prices of shipping options of type `calculated`."],
|
||||
["34", "data", "Pass in this property any data relevant to the fulfillment provider."],
|
||||
["56", "formatPrice", "This function formats a price based on the cart's currency."],
|
||||
["65", "getShippingOptionPrice", "This function gets the price of a shipping option based on its type."],
|
||||
["77", "setShippingMethod", "This function sets the shipping method of the cart using the selected shipping option."],
|
||||
["91", "data", "Pass in this property any data relevant to the fulfillment provider."],
|
||||
]
|
||||
|
||||
```ts highlights={fetchHighlights}
|
||||
const cartId = localStorage.getItem("cart_id")
|
||||
let shippingOptions = []
|
||||
const calculatedPrices: Record<string, number> = {}
|
||||
|
||||
const retrieveShippingOptions = () => {
|
||||
const { shipping_options } = await fetch(
|
||||
@@ -47,7 +54,63 @@ export const fetchHighlights = [
|
||||
)
|
||||
.then((res) => res.json())
|
||||
|
||||
return shipping_options
|
||||
shippingOptions = shipping_options
|
||||
}
|
||||
|
||||
const calculateShippingOptionPrices = () => {
|
||||
const promises = shippingOptions
|
||||
.filter((shippingOption) => shippingOption.price_type === "calculated")
|
||||
.map((shippingOption) =>
|
||||
fetch(`http://localhost:9000/store/shipping-options/${shippingOption.id}/calculate`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
cart_id: cart.id,
|
||||
data: {
|
||||
// pass any data useful for calculation with third-party provider.
|
||||
}
|
||||
})
|
||||
})
|
||||
.then((res) => res.json())
|
||||
)
|
||||
|
||||
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 = (
|
||||
@@ -83,27 +146,32 @@ export const fetchHighlights = [
|
||||
|
||||
export const highlights = [
|
||||
["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
|
||||
["22", "fetch", "Retrieve available shipping method of the customer's cart."],
|
||||
["46", "fetch", "Set the cart's shipping method using the selected shipping option."],
|
||||
["57", "data", "Pass in this property any data relevant to the fulfillment provider."]
|
||||
["25", "fetch", "Retrieve available shipping methods of the customer's cart."],
|
||||
["47", "fetch", "Retrieve the price of every shipping method that has a calculated price."],
|
||||
["56", "data", "Pass in this property any data relevant to the fulfillment provider."],
|
||||
["86", "fetch", "Set the cart's shipping method using the selected shipping option."],
|
||||
["97", "data", "Pass in this property any data relevant to the fulfillment provider."]
|
||||
]
|
||||
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useCart } from "../../../providers/cart"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export default function CheckoutShippingStep() {
|
||||
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,
|
||||
setSelectedShippingOption
|
||||
] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -124,6 +192,43 @@ export const highlights = [
|
||||
})
|
||||
}, [cart])
|
||||
|
||||
useEffect(() => {
|
||||
if (!cart || !shippingOptions.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const promises = shippingOptions
|
||||
.filter((shippingOption) => shippingOption.price_type === "calculated")
|
||||
.map((shippingOption) =>
|
||||
fetch(`http://localhost:9000/store/shipping-options/${shippingOption.id}/calculate`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
cart_id: cart.id,
|
||||
data: {
|
||||
// pass any data useful for calculation with third-party provider.
|
||||
}
|
||||
})
|
||||
})
|
||||
.then((res) => res.json())
|
||||
)
|
||||
|
||||
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>
|
||||
) => {
|
||||
@@ -148,8 +253,8 @@ export const highlights = [
|
||||
data: {
|
||||
// TODO add any data necessary for
|
||||
// fulfillment provider
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ cart: updatedCart }) => {
|
||||
@@ -158,6 +263,26 @@ export const highlights = [
|
||||
.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>}
|
||||
@@ -168,14 +293,19 @@ export const highlights = [
|
||||
e.target.value
|
||||
)}
|
||||
>
|
||||
{shippingOptions.map((shippingOption) => (
|
||||
<option
|
||||
key={shippingOption.id}
|
||||
value={shippingOption.id}
|
||||
>
|
||||
{shippingOption.name}
|
||||
</option>
|
||||
))}
|
||||
{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}
|
||||
@@ -195,10 +325,11 @@ export const highlights = [
|
||||
In the example above, you:
|
||||
|
||||
- Retrieve the available shipping options of the cart to allow the customer to select from them.
|
||||
- For each shipping option, you retrieve its calculated price from the Medusa application.
|
||||
- Once the customer selects a shipping option, you send a request to the Add Shipping Method to Cart API route to update the cart's shipping method using the selected shipping option.
|
||||
|
||||
## data Request Body Parameter
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
This isn't implemented here as it's different for each provider. Refer to the provider's documentation on details of expected data, if any.
|
||||
|
||||
@@ -242,3 +242,13 @@ export const handleHighlights = [
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
---
|
||||
|
||||
## Features in Product Details Page
|
||||
|
||||
In a product's details page, you want to allow the customer to choose a variant, see its price, and add it to the cart. The following guides will help you add these features into your storefront:
|
||||
|
||||
- [Select a variant](../variants/page.mdx)
|
||||
- [Show variant price](../price/page.mdx)
|
||||
- [Retrieve variant's inventory quantity](../inventory/page.mdx)
|
||||
- [Add a product to the cart](../../cart/manage-items/page.mdx#add-product-variant-to-cart)
|
||||
|
||||
@@ -138,7 +138,7 @@ export const generatedEditDates = {
|
||||
"app/service-factory-reference/page.mdx": "2024-07-26T14:40:56+00:00",
|
||||
"app/storefront-development/cart/context/page.mdx": "2024-12-19T16:27:53.821Z",
|
||||
"app/storefront-development/cart/create/page.mdx": "2024-12-19T16:27:55.753Z",
|
||||
"app/storefront-development/cart/manage-items/page.mdx": "2024-12-19T16:27:56.433Z",
|
||||
"app/storefront-development/cart/manage-items/page.mdx": "2024-12-26T15:59:48.445Z",
|
||||
"app/storefront-development/cart/retrieve/page.mdx": "2024-12-19T16:27:57.486Z",
|
||||
"app/storefront-development/cart/update/page.mdx": "2024-12-19T16:28:05.574Z",
|
||||
"app/storefront-development/cart/page.mdx": "2024-06-11T11:56:37+03:00",
|
||||
@@ -147,7 +147,7 @@ export const generatedEditDates = {
|
||||
"app/storefront-development/checkout/email/page.mdx": "2024-12-19T16:30:40.122Z",
|
||||
"app/storefront-development/checkout/payment/stripe/page.mdx": "2024-12-19T16:30:39.173Z",
|
||||
"app/storefront-development/checkout/payment/page.mdx": "2024-12-19T16:30:38.192Z",
|
||||
"app/storefront-development/checkout/shipping/page.mdx": "2024-12-19T16:30:31.537Z",
|
||||
"app/storefront-development/checkout/shipping/page.mdx": "2024-12-26T15:34:51.431Z",
|
||||
"app/storefront-development/checkout/page.mdx": "2024-06-12T19:46:06+02:00",
|
||||
"app/storefront-development/customers/addresses/page.mdx": "2024-12-19T16:38:44.847Z",
|
||||
"app/storefront-development/customers/context/page.mdx": "2024-12-19T16:38:43.703Z",
|
||||
@@ -171,7 +171,7 @@ export const generatedEditDates = {
|
||||
"app/storefront-development/products/price/examples/show-price/page.mdx": "2024-12-19T16:34:56.493Z",
|
||||
"app/storefront-development/products/price/examples/tax-price/page.mdx": "2024-12-19T16:35:05.493Z",
|
||||
"app/storefront-development/products/price/page.mdx": "2024-12-19T16:35:19.471Z",
|
||||
"app/storefront-development/products/retrieve/page.mdx": "2024-12-19T16:35:35.011Z",
|
||||
"app/storefront-development/products/retrieve/page.mdx": "2024-12-26T15:58:41.305Z",
|
||||
"app/storefront-development/products/variants/page.mdx": "2024-12-19T16:35:41.278Z",
|
||||
"app/storefront-development/products/page.mdx": "2024-06-11T19:55:56+02:00",
|
||||
"app/storefront-development/regions/context/page.mdx": "2024-12-19T16:36:07.406Z",
|
||||
|
||||
@@ -14602,14 +14602,6 @@ export const generatedSidebar = [
|
||||
"type": "category",
|
||||
"title": "Products",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products",
|
||||
"title": "Overview",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
@@ -14675,80 +14667,78 @@ export const generatedSidebar = [
|
||||
"path": "/storefront-development/products/inventory",
|
||||
"title": "Retrieve Variant Inventory",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "category",
|
||||
"title": "Product Categories",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/categories/list",
|
||||
"title": "List Categories",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/categories",
|
||||
"title": "Categories",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/categories/list",
|
||||
"title": "List Categories",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/categories/retrieve",
|
||||
"title": "Retrieve a Category",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/categories/products",
|
||||
"title": "Retrieve a Category's Products",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/categories/nested-categories",
|
||||
"title": "Retrieve Nested Categories",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
"path": "/storefront-development/products/categories/retrieve",
|
||||
"title": "Retrieve a Category",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/collections",
|
||||
"title": "Collections",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/collections/list",
|
||||
"title": "List Collections",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/collections/retrieve",
|
||||
"title": "Retrieve a Collection",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/collections/products",
|
||||
"title": "Retrieve a Collection's Products",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
"path": "/storefront-development/products/categories/products",
|
||||
"title": "Retrieve a Category's Products",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/categories/nested-categories",
|
||||
"title": "Retrieve Nested Categories",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "category",
|
||||
"title": "Product Collections",
|
||||
"children": [
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/collections/list",
|
||||
"title": "List Collections",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/collections/retrieve",
|
||||
"title": "Retrieve a Collection",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/products/collections/products",
|
||||
"title": "Retrieve a Collection's Products",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -46,11 +46,6 @@ export const storefrontGuidesSidebar = [
|
||||
type: "category",
|
||||
title: "Products",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products",
|
||||
title: "Overview",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/list",
|
||||
@@ -77,54 +72,52 @@ export const storefrontGuidesSidebar = [
|
||||
path: "/storefront-development/products/inventory",
|
||||
title: "Retrieve Variant Inventory",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
title: "Product Categories",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/categories",
|
||||
title: "Categories",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/categories/list",
|
||||
title: "List Categories",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/categories/retrieve",
|
||||
title: "Retrieve a Category",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/categories/products",
|
||||
title: "Retrieve a Category's Products",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/categories/nested-categories",
|
||||
title: "Retrieve Nested Categories",
|
||||
},
|
||||
],
|
||||
path: "/storefront-development/products/categories/list",
|
||||
title: "List Categories",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/collections",
|
||||
title: "Collections",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/collections/list",
|
||||
title: "List Collections",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/collections/retrieve",
|
||||
title: "Retrieve a Collection",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/collections/products",
|
||||
title: "Retrieve a Collection's Products",
|
||||
},
|
||||
],
|
||||
path: "/storefront-development/products/categories/retrieve",
|
||||
title: "Retrieve a Category",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/categories/products",
|
||||
title: "Retrieve a Category's Products",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/categories/nested-categories",
|
||||
title: "Retrieve Nested Categories",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
title: "Product Collections",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/collections/list",
|
||||
title: "List Collections",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/collections/retrieve",
|
||||
title: "Retrieve a Collection",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/products/collections/products",
|
||||
title: "Retrieve a Collection's Products",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user