Example: Show 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.

Check if a Price is a Sale#

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:

Code
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:

TipFind the implementation of the formatPrice function in this guide.
Code
1const salePrice = formatPrice(selectedVariantPrice.calculated_price.calculated_amount)2const originalPrice = formatPrice(selectedVariantPrice.calculated_price.original_amount)3const discountedAmount = formatPrice(4  selectedVariantPrice.calculated_price.original_amount - 5  selectedVariantPrice.calculated_price.calculated_amount6)

You can use the original price either to display it or calculate and display the discounted amount.


Full React Example#

For example, in a React-based storefront:

NoteThe example only passes the region_id query parameter for pricing. Learn how to store and retrieve the customer's region in the Regions guides.
Code
1"use client" // include with Next.js 13+2
3import { useEffect, useMemo, useState } from "react"4import { HttpTypes } from "@medusajs/types"5import { useRegion } from "../providers/region"6
7type Props = {8  id: string9}10
11export default function Product({ id }: Props) {12  const [loading, setLoading] = useState(true)13  const [product, setProduct] = useState<14    HttpTypes.StoreProduct | undefined15  >()16  const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})17  const { region } = useRegion()18
19  useEffect(() => {20    if (!loading) {21      return 22    }23
24    const queryParams = new URLSearchParams({25      fields: `*variants.calculated_price`,26      region_id: region.id,27    })28
29    fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {30      credentials: "include",31      headers: {32        "x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",33      },34    })35    .then((res) => res.json())36    .then(({ product: dataProduct }) => {37      setProduct(dataProduct)38      setLoading(false)39    })40  }, [loading])41
42  const selectedVariant = useMemo(() => {43    if (44      !product?.variants ||45      !product.options || 46      Object.keys(selectedOptions).length !== product.options?.length47    ) {48      return49    }50
51    return product.variants.find((variant) => variant.options?.every(52      (optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]53    ))54  }, [selectedOptions, product])55
56  const formatPrice = (amount: number): string => {57    return new Intl.NumberFormat("en-US", {58      style: "currency",59      currency: region.currency_code,60    })61    .format(amount)62  }63
64  const selectedVariantPrice = useMemo(() => {65    if (selectedVariant) {66      return selectedVariant67    }68
69    return product?.variants?.sort((a: any, b: any) => {70      return (71        a.calculated_price.calculated_amount -72        b.calculated_price.calculated_amount73      )74    })[0]75  }, [selectedVariant, product])76
77  const price = useMemo(() => {78    if (!selectedVariantPrice) {79      return80    }81
82    // @ts-ignore83    return formatPrice(selectedVariantPrice.calculated_price.calculated_amount)84  }, [selectedVariantPrice])85
86  const isSale = useMemo(() => {87    if (!selectedVariantPrice) {88      return false89    }90
91    // @ts-ignore92    return selectedVariantPrice.calculated_price.calculated_price.price_list_type === "sale"93  }, [selectedVariantPrice])94
95  const originalPrice = useMemo(() => {96    if (!isSale) {97      return98    }99
100    // @ts-ignore101    return formatPrice(selectedVariantPrice.calculated_price.original_amount)102  }, [isSale, selectedVariantPrice])103
104  return (105    <div>106      {loading && <span>Loading...</span>}107      {product && (108        <>109          <h1>{product.title}</h1>110          {(product.options?.length || 0) > 0 && (111            <ul>112              {product.options!.map((option) => (113                <li key={option.id}>114                  {option.title}115                  {option.values?.map((optionValue) => (116                    <button 117                      key={optionValue.id}118                      onClick={() => {119                        setSelectedOptions((prev) => {120                          return {121                            ...prev,122                            [option.id!]: optionValue.value!,123                          }124                        })125                      }}126                    >127                      {optionValue.value}128                    </button>129                  ))}130                </li>131              ))}132            </ul>133          )}134          {selectedVariant && (135            <span>Selected Variant: {selectedVariant.id}</span>136          )}137          {price && (138            <span>139              {!selectedVariant && "From: "}140              {price}141              {isSale && `SALE - Original Price: ${originalPrice}`}142            </span>143          )}144          {product.images?.map((image) => (145            <img src={image.url} key={image.id} />146          ))}147        </>148      )}149    </div>150  )151}

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.
Was this page helpful?
Edit this page
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break