From 65d0a300cea4f98e6dd0ec7339d177e98afef8fc Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 27 Dec 2024 09:22:35 +0200 Subject: [PATCH] docs: document how to calculate shipping prices in storefront (#10748) --- .../cart/manage-items/page.mdx | 8 +- .../checkout/shipping/page.mdx | 175 +++++++++++++++--- .../products/retrieve/page.mdx | 10 + www/apps/resources/generated/edit-dates.mjs | 6 +- www/apps/resources/generated/sidebar.mjs | 134 +++++++------- www/apps/resources/sidebars/storefront.mjs | 89 ++++----- 6 files changed, 275 insertions(+), 147 deletions(-) diff --git a/www/apps/resources/app/storefront-development/cart/manage-items/page.mdx b/www/apps/resources/app/storefront-development/cart/manage-items/page.mdx index 4f66860d56..84d1314ebe 100644 --- a/www/apps/resources/app/storefront-development/cart/manage-items/page.mdx +++ b/www/apps/resources/app/storefront-development/cart/manage-items/page.mdx @@ -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). + + +To retrieve a variant's available quantity and check if it's in stock, refer to [this guide](../../products/inventory/page.mdx). + + + For example: export const addHighlights = [ diff --git a/www/apps/resources/app/storefront-development/checkout/shipping/page.mdx b/www/apps/resources/app/storefront-development/checkout/shipping/page.mdx index 0ac3ba9d62..c5ffa9ee90 100644 --- a/www/apps/resources/app/storefront-development/checkout/shipping/page.mdx +++ b/www/apps/resources/app/storefront-development/checkout/shipping/page.mdx @@ -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: 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 = {} 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 + >({}) const [ selectedShippingOption, - setSelectedShippingOption, + setSelectedShippingOption ] = useState() 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 = {} + 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 ) => { @@ -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 (
{loading || !cart && Loading...} @@ -168,14 +293,19 @@ export const highlights = [ e.target.value )} > - {shippingOptions.map((shippingOption) => ( - - ))} + {shippingOptions.map((shippingOption) => { + const price = getShippingOptionPrice(shippingOption) + + return ( + + ) + })}