From 70d48a91b3808bd226bbb3084038e4b1cccdbd97 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 30 Jul 2024 12:52:21 +0300 Subject: [PATCH] docs: added storefront guide on prices with taxes (#8326) --- .../price/examples/sale-price/page.mdx | 70 +++-- .../price/examples/show-price/page.mdx | 67 +++-- .../price/examples/tax-price/page.mdx | 253 ++++++++++++++++++ .../products/price/page.mdx | 32 ++- www/apps/resources/generated/files-map.mjs | 4 + www/apps/resources/generated/sidebar.mjs | 10 +- www/apps/resources/sidebar.mjs | 11 +- 7 files changed, 391 insertions(+), 56 deletions(-) create mode 100644 www/apps/resources/app/storefront-development/products/price/examples/tax-price/page.mdx diff --git a/www/apps/resources/app/storefront-development/products/price/examples/sale-price/page.mdx b/www/apps/resources/app/storefront-development/products/price/examples/sale-price/page.mdx index d4657e798f..e75f19bfb5 100644 --- a/www/apps/resources/app/storefront-development/products/price/examples/sale-price/page.mdx +++ b/www/apps/resources/app/storefront-development/products/price/examples/sale-price/page.mdx @@ -1,14 +1,52 @@ +--- +sidebar_position: 2 +sidebar_label: "Example: Show Sale Price" +--- + export const metadata = { - title: `React Example: Show Product Variant's Sale Price`, + title: `Example: Show Product Variant's Sale Price`, } # {metadata.title} -This document provides an example of how to show a product variant's sale price. +In this document, you'll learn how to display a product variant's sale price, with a full React example. -To check if a product variant's price is a sale price, check whether the variant's `calculated_price.calculated_price.price_list_type` field is equal to `sale`. +## Check if a Price is a Sale -In that case, the original price is in the variant's `calculated_price.original_amount` field. +To check if a product variant's price is a sale price, check whether the variant's `calculated_price.calculated_price.price_list_type` field is equal to `sale`: + +```ts +const isSale = selectedVariantPrice.calculated_price.calculated_price.price_list_type === "sale" +``` + +Where `selectedVariantPrice` is either the variant the customer selected or the cheapest variant. + +--- + +## Display Original and Discount Amounts + +If the price is a sale price, the original price is in the variant's `calculated_price.original_amount` field: + +```ts +const salePrice = formatPrice(selectedVariantPrice.calculated_price.calculated_amount) +const originalPrice = formatPrice(selectedVariantPrice.calculated_price.original_amount) +const discountedAmount = formatPrice( + selectedVariantPrice.calculated_price.original_amount - + selectedVariantPrice.calculated_price.calculated_amount +) +``` + +You can use the original price either to display it or calculate and display the discounted amount. + + + +Learn more about the `formatPrice` function in [this guide](../show-price/page.mdx#price-formatting) + + + +--- + +## Full React Example For example, in a React-based storefront: @@ -93,7 +131,7 @@ export default function Product({ params: { id } }: Params) { .format(amount) } - const variantPrice = useMemo(() => { + const selectedVariantPrice = useMemo(() => { if (selectedVariant) { return selectedVariant } @@ -107,22 +145,22 @@ export default function Product({ params: { id } }: Params) { }, [selectedVariant, product]) const price = useMemo(() => { - if (!variantPrice) { + if (!selectedVariantPrice) { return } // @ts-ignore - return formatPrice(variantPrice.calculated_price.calculated_amount) - }, [variantPrice]) + return formatPrice(selectedVariantPrice.calculated_price.calculated_amount) + }, [selectedVariantPrice]) const isSale = useMemo(() => { - if (!variantPrice) { + if (!selectedVariantPrice) { return false } // @ts-ignore - return variantPrice.calculated_price.calculated_price.price_list_type === "sale" - }, [variantPrice]) + return selectedVariantPrice.calculated_price.calculated_price.price_list_type === "sale" + }, [selectedVariantPrice]) const originalPrice = useMemo(() => { if (!isSale) { @@ -130,8 +168,8 @@ export default function Product({ params: { id } }: Params) { } // @ts-ignore - return formatPrice(variantPrice.calculated_price.original_amount) - }, [isSale, variantPrice]) + return formatPrice(selectedVariantPrice.calculated_price.original_amount) + }, [isSale, selectedVariantPrice]) return (
@@ -188,9 +226,3 @@ In this example, you: - Define an `isSale` memo variable that determines whether the chosen variant's price is a sale price. You do that by checking if the value of the variant's `calculated_price.calculated_price.price_list_type` field is `sale`. - Define an `originalPrice` memo variable that, if `isSale` is enabled, has the formatted original price of the chosen variant. The variant's original price is in the `calculated_price.original_amount` field. - If `isSale` is enabled, show a message to the customer indicating that this product is on sale along with the original price. - - - -Learn more about the `formatPrice` function in [this guide](../show-price/page.mdx#price-formatting) - - \ No newline at end of file diff --git a/www/apps/resources/app/storefront-development/products/price/examples/show-price/page.mdx b/www/apps/resources/app/storefront-development/products/price/examples/show-price/page.mdx index 97ade25085..c38606c715 100644 --- a/www/apps/resources/app/storefront-development/products/price/examples/show-price/page.mdx +++ b/www/apps/resources/app/storefront-development/products/price/examples/show-price/page.mdx @@ -1,9 +1,48 @@ +--- +sidebar_position: 1 +sidebar_label: "Example: Show Variant's Price" +--- + export const metadata = { - title: `React Example: Show Product Variant's Price`, + title: `Example: Show Product Variant's Price`, } # {metadata.title} +In this document, you'll learn how to display a product variant's price with a full React example. + +## Display Selected Variant Price + +Once the customer selects a variant, use its `calculated_price.calculated_amount` property to display its price: + +```ts +const price = formatPrice( + selectedVariantPrice.calculated_price.calculated_amount +) +``` + +You'll learn about the `formatPrice` function in the next section. + +--- + +## Price Formatting + +To format the price, use JavaScript's [NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) utility. You pass it the amount and the currency code (which you retrieve from the selected region): + +```ts +const formatPrice = (amount: number): string => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: region.currency_code, + }) + .format(amount) +} +``` + +--- + +## Full React Example + The following React-based storefront example retrieves the product's price based on the selected variant: @@ -19,7 +58,7 @@ export const priceHighlights = [ ["26", "queryParams", "Build the pricing query parameters."], ["58", "formatPrice", "A utility function to format an amount with its currency."], ["59", `"en-US"`, "If you use a different locale change it here."], - ["66", "variantPrice", "Assign the variant to compute its price, which is either the selected or cheapest variant."], + ["66", "selectedVariantPrice", "Assign the variant to compute its price, which is either the selected or cheapest variant."], ["68", "selectedVariant", "Use the selected variant for pricing."], ["71", "", "If there isn't a selected variant, retrieve the variant with the cheapest price."], ["79", "price", "Compute the price of the selected or cheapest variant."], @@ -46,7 +85,7 @@ export default function Product({ params: { id } }: Params) { HttpTypes.StoreProduct | undefined >() const [selectedOptions, setSelectedOptions] = useState>({}) - const { region } = useRegion + const { region } = useRegion() useEffect(() => { if (!loading) { @@ -93,7 +132,7 @@ export default function Product({ params: { id } }: Params) { .format(amount) } - const variantPrice = useMemo(() => { + const selectedVariantPrice = useMemo(() => { if (selectedVariant) { return selectedVariant } @@ -107,13 +146,13 @@ export default function Product({ params: { id } }: Params) { }, [selectedVariant, product]) const price = useMemo(() => { - if (!variantPrice) { + if (!selectedVariantPrice) { return } // @ts-ignore - return formatPrice(variantPrice.calculated_price.calculated_amount) - }, [variantPrice]) + return formatPrice(selectedVariantPrice.calculated_price.calculated_amount) + }, [selectedVariantPrice]) return (
@@ -173,17 +212,3 @@ In the example above, you: - If there isn't a selected variant, retrieve and choose the variant with the cheapest price. - Format the price based on the chosen variant in the previous step. The variant's `calculated_price.calculated_amount` field is used. - Display the formatted price to the customer. If there isn't a select variant, show a `From` label to indicate that the price shown is the cheapest. - -### Price Formatting - -To format the price, use JavaScript's [NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) utility. You pass it the amount and the currency code (which you retrieve from the selected region): - -```ts -const formatPrice = (amount: number): string => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: region.currency_code, - }) - .format(amount) -} -``` \ No newline at end of file diff --git a/www/apps/resources/app/storefront-development/products/price/examples/tax-price/page.mdx b/www/apps/resources/app/storefront-development/products/price/examples/tax-price/page.mdx new file mode 100644 index 0000000000..dbc5af259c --- /dev/null +++ b/www/apps/resources/app/storefront-development/products/price/examples/tax-price/page.mdx @@ -0,0 +1,253 @@ +--- +sidebar_position: 3 +sidebar_label: "Example: Show Price with Taxes" +--- + +export const metadata = { + title: `Example: Show Product Variant's Price with Taxes`, +} + +# {metadata.title} + +In this document, you'll learn how to show a product variant's price with taxes, with a full React example. + +## Retrieve Variant's Price with Tax + +To retrieve a product variant's price with taxes, you must pass the `region_id` and `country_code` query parameters: + +export const fetchHighlights = [ + ["3", "region_id", "Pass the region ID as a query parameter."], + ["4", "country_code", "Pass the ISO 2 country code as a parameter."], + ["4", "region.countries[0].iso_2", "You can instead allow the customer to select a specific country."], +] + +```ts highlights={fetchHighlights} +const queryParams = new URLSearchParams({ + fields: `*variants.calculated_price`, + region_id: region.id, + country_code: region.countries[0].iso_2 +}) + +fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, { + credentials: "include", + headers: { + "x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp", + }, +}) +.then((res) => res.json()) +.then(({ product }) => { + // TODO use product + console.log(product) +}) +``` + +You pass the selected region's ID and the code of its first country as query parameters to the [Get Product API route](https://docs.medusajs.com/v2/api/store#products_getproductsid). + + + +You can instead allow the customer to choose their country. + + + +--- + +## Display Variant's Price with Taxes + +After passing the `region_id` and `country_code` as query parameters when retrieving the product, each variant's price object will have a `calculated_amount_with_tax` property to indicate the price taxes applied: + +```ts +const price = formatPrice(selectedVariantPrice.calculated_price.calculated_amount_with_tax) +``` + +Where `selectedVariantPrice` is either the variant the customer selected or the cheapest variant. + + + +Learn more about the `formatPrice` function in [this guide](../show-price/page.mdx#price-formatting) + + + +--- + +## Tax Price Properties + +Aside from the `calculated_amount_with_tax` property, a variant's `calculated_price` object has the following properties related to taxes: + +1. `calculated_amount_without_tax`: The calculated amount without taxes. +2. `is_calculated_price_tax_inclusive`: Whether the `calculated_amount` property includes taxes. If enabled, you can display it instead of `calculated_amount_with_tax`. + +--- + +## Full React Example + +For example, in a React-based storefront: + + + +The example passes the `region_id` query parameter for pricing. Learn how to store and retrieve the customer's region in the [Regions guides](../../../../regions/context/page.mdx). + + + +export const taxHighlight = [ + ["5", "useRegion", "The `useRegion` hook is implemented in the Region React Context guide."], + ["13", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."], + ["19", "region", "Access the region using the `useRegion` hook."], + ["28", "region_id", "Pass the region ID as a query parameter."], + ["29", "country_code", "Pass the ISO 2 country code as a parameter."], + ["29", "region.countries[0].iso_2", "You can instead allow the customer to select a specific country."], + ["59", "formatPrice", "A utility function to format an amount with its currency."], + ["60", `"en-US"`, "If you use a different locale change it here."], + ["67", "selectedVariantPrice", "Assign the variant to compute its price, which is either the selected or cheapest variant."], + ["69", "selectedVariant", "Use the selected variant for pricing."], + ["72", "", "If there isn't a selected variant, retrieve the variant with the cheapest price."], + ["80", "price", "Compute the price of the selected or cheapest variant."], + ["87", "calculated_amount_with_tax", "Use the variant price's `calculated_amount_with_tax` property to display the price."], + ["126", "", "If there's a computed price but no selected variant, show a `From` prefix to the price."], + ["127", "price", "Display the computed price."] +] + +```tsx highlights={taxHighlight} +"use client" // include with Next.js 13+ + +import { useEffect, useMemo, useState } from "react" +import { HttpTypes } from "@medusajs/types" +import { useRegion } from "../providers/region" + +type Params = { + params: { + id: string + } +} + +export default function Product({ params: { id } }: Params) { + const [loading, setLoading] = useState(true) + const [product, setProduct] = useState< + HttpTypes.StoreProduct | undefined + >() + const [selectedOptions, setSelectedOptions] = useState>({}) + const region = useRegion() + + useEffect(() => { + if (!loading) { + return + } + + const queryParams = new URLSearchParams({ + fields: `*variants.calculated_price`, + region_id: region.id, + country_code: region.countries[0].iso_2 + }) + + fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, { + credentials: "include", + headers: { + "x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp", + }, + }) + .then((res) => res.json()) + .then(({ product: dataProduct }) => { + setProduct(dataProduct) + setLoading(false) + }) + }, [loading]) + + const selectedVariant = useMemo(() => { + if ( + !product?.variants || + !product.options || + Object.keys(selectedOptions).length !== product.options?.length + ) { + return + } + + return product.variants.find((variant) => variant.options?.every( + (optionValue) => optionValue.value === selectedOptions[optionValue.option_id!] + )) + }, [selectedOptions, product]) + + const formatPrice = (amount: number): string => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: region.currency_code, + }) + .format(amount) + } + + const selectedVariantPrice = useMemo(() => { + if (selectedVariant) { + return selectedVariant + } + + return product?.variants?.sort((a: any, b: any) => { + return ( + a.calculated_price.calculated_amount_with_tax - + b.calculated_price.calculated_amount_with_tax + ) + })[0] + }, [selectedVariant, product]) + + const price = useMemo(() => { + if (!selectedVariantPrice) { + return + } + + // @ts-ignore + return formatPrice( + selectedVariantPrice.calculated_price.calculated_amount_with_tax + ) + }, [selectedVariantPrice]) + + return ( +
+ {loading && Loading...} + {product && ( + <> +

{product.title}

+ {(product.options?.length || 0) > 0 && ( +
    + {product.options!.map((option) => ( +
  • + {option.title} + {option.values?.map((optionValue) => ( + + ))} +
  • + ))} +
+ )} + {selectedVariant && ( + Selected Variant: {selectedVariant.id} + )} + {price && ( + + {!selectedVariant && "From: "} + {price} + + )} + {product.images?.map((image) => ( + + ))} + + )} +
+ ) +} +``` + +In this example, you: + +- Pass the selected region's ID and the code of its first country as query parameters to the [Get Product API route](https://docs.medusajs.com/v2/api/store#products_getproductsid). + - You can instead allow the customer to choose their country. +- Display the selected variant's price by formatting its price's `calculated_amount_with_tax` property. diff --git a/www/apps/resources/app/storefront-development/products/price/page.mdx b/www/apps/resources/app/storefront-development/products/price/page.mdx index a578faf1e5..379b7900f1 100644 --- a/www/apps/resources/app/storefront-development/products/price/page.mdx +++ b/www/apps/resources/app/storefront-development/products/price/page.mdx @@ -10,7 +10,7 @@ In this document, you'll learn how to show a product's variants' prices in the s ## Pricing Query Parameters -When you retrieve products either with the [List Products](!api!/store#products_getproducts) or [Retrieve Products](!api!/store#products_getproductsid) API routes, you must pass include in the beginning of the `fields` query parameter the value `*variants.calculated_price`. +When you retrieve products either with the [List Products](!api!/store#products_getproducts) or [Retrieve Products](!api!/store#products_getproductsid) API routes, you must include in the beginning of the `fields` query parameter the value `*variants.calculated_price`. You also must pass at least one of the following query parameters to retrieve the correct product variant price: @@ -19,6 +19,29 @@ You also must pass at least one of the following query parameters to retrieve th - `customer_id`: The ID of the customer viewing the prices. This is useful when you have a promotion or price list overriding a product's price for specific customer groups. - `customer_group_id`: The ID of the group of the customer viewing the prices. This is useful when you have a promotion or price list overriding a product's price for specific customer groups. +For example: + +```ts +const queryParams = new URLSearchParams({ + fields: `*variants.calculated_price`, + region_id: region.id, +}) + +fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, { + credentials: "include", + headers: { + "x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp", + }, +}) +.then((res) => res.json()) +.then(({ product }) => { + // TODO use product + console.log(product) +}) +``` + +In this example, you pass the selected region's ID as a query parameter with the `fields` query parameter set to `*variants.calculated_price`. + --- ## Product Variant's Price Properties @@ -81,9 +104,8 @@ If you pass the parameters mentioned above, each variant has a `calculated_price ---- - ## Examples -- [React Example: Show Product Variant's Price](./examples/show-price/page.mdx). -- [React Example: Show Product Variant's Sale Price](./examples/sale-price/page.mdx). +- [Example: Show Product Variant's Price](./examples/show-price/page.mdx). +- [Example: Show Product Variant's Sale Price](./examples/sale-price/page.mdx). +- [Example: Show Product Variant's Price with Taxes](./examples/tax-price/page.mdx). diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index af4fc8c564..8a51b25260 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -923,6 +923,10 @@ export const filesMap = [ "filePath": "/www/apps/resources/app/storefront-development/products/price/examples/show-price/page.mdx", "pathname": "/storefront-development/products/price/examples/show-price" }, + { + "filePath": "/www/apps/resources/app/storefront-development/products/price/examples/tax-price/page.mdx", + "pathname": "/storefront-development/products/price/examples/tax-price" + }, { "filePath": "/www/apps/resources/app/storefront-development/products/price/page.mdx", "pathname": "/storefront-development/products/price" diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index 0c5c42d56b..651ae5c688 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -6755,12 +6755,13 @@ export const generatedSidebar = [ "isPathHref": true, "path": "/storefront-development/products/price", "title": "Retrieve Variant Prices", + "autogenerate_path": "storefront-development/products/price/examples", "children": [ { "loaded": true, "isPathHref": true, "path": "/storefront-development/products/price/examples/show-price", - "title": "Example: Show Variant Price", + "title": "Example: Show Variant's Price", "children": [] }, { @@ -6769,6 +6770,13 @@ export const generatedSidebar = [ "path": "/storefront-development/products/price/examples/sale-price", "title": "Example: Show Sale Price", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "path": "/storefront-development/products/price/examples/tax-price", + "title": "Example: Show Price with Taxes", + "children": [] } ] }, diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 4cea0413aa..af1d468065 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -1537,16 +1537,7 @@ export const sidebar = sidebarAttachHrefCommonOptions([ { path: "/storefront-development/products/price", title: "Retrieve Variant Prices", - children: [ - { - path: "/storefront-development/products/price/examples/show-price", - title: "Example: Show Variant Price", - }, - { - path: "/storefront-development/products/price/examples/sale-price", - title: "Example: Show Sale Price", - }, - ], + autogenerate_path: "storefront-development/products/price/examples", }, { path: "/storefront-development/products/categories",