docs: document how to calculate shipping prices in storefront (#10748)

This commit is contained in:
Shahed Nasser
2024-12-27 09:22:35 +02:00
committed by GitHub
parent 91ebf6d61c
commit 65d0a300ce
6 changed files with 275 additions and 147 deletions
@@ -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)
+3 -3
View File
@@ -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",
+62 -72
View File
@@ -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": []
}
]
},
+41 -48
View File
@@ -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",
},
],
},