docs: update storefront development guides to use JS SDK [2] (#12015)

This commit is contained in:
Shahed Nasser
2025-03-27 17:14:22 +02:00
committed by GitHub
parent 1895d8cc11
commit bf882b5aff
43 changed files with 13257 additions and 13309 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,25 +12,37 @@ export const metadata = {
# {metadata.title}
Throughout your storefront, you'll need to access the customer's cart to perform different actions.
In this guide, you'll learn how to create a cart context in your storefront.
## Why Create a Cart Context?
Throughout your storefront, you'll need to access the customer's cart to perform different actions. For example, you may need to add a product variant to the cart from the product page.
So, if your storefront is React-based, create a cart context and add it at the top of your components tree. Then, you can access the customer's cart anywhere in your storefront.
---
## Create Cart Context Provider
For example, create the following file that exports a `CartProvider` component and a `useCart` hook:
<Note title="Tip">
- This example uses the `useRegion` hook defined in the [Region React Context guide](../../regions/context/page.mdx) to associate the cart with the customer's selected region.
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
export const highlights = [
["13", "cart", "Expose cart to children of the context provider."],
["14", "setCart", "Allow the context provider's children to update the cart."],
["17", "refreshCart", "Allow the context provider's children to unset and reset the cart."],
["26", "CartProvider", "The provider component to use in your component tree."],
["30", "useRegion", "Use the `useRegion` hook defined in the Region Context guide."],
["40", "fetch", "If the customer doesn't have a cart, create a new one."],
["44", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to associate the correct sales channel(s)."],
["58", "fetch", "Retrieve the customer's cart."],
["71", "refreshCart", "This function unsets the cart, which triggers the `useEffect` callback to create a cart."],
["87", "useCart", "The hook that child components of the provider use to access the cart."]
["14", "cart", "Expose cart to children of the context provider."],
["15", "setCart", "Allow the context provider's children to update the cart."],
["18", "refreshCart", "Allow the context provider's children to unset and reset the cart."],
["27", "CartProvider", "The provider component to use in your component tree."],
["31", "useRegion", "Use the `useRegion` hook defined in the Region Context guide."],
["41", "create", "If the customer doesn't have a cart, create a new one."],
["50", "retrieve", "Retrieve the customer's cart."],
["57", "refreshCart", "This function unsets the cart, which triggers the `useEffect` callback to create a cart."],
["73", "useCart", "The hook that child components of the provider use to access the cart."]
]
```tsx highlights={highlights}
@@ -44,6 +56,7 @@ import {
} from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "./region"
import { sdk } from "@/lib/sdk"
type CartContextType = {
cart?: HttpTypes.StoreCart
@@ -73,31 +86,16 @@ export const CartProvider = ({ children }: CartProviderProps) => {
const cartId = localStorage.getItem("cart_id")
if (!cartId) {
// create a cart
fetch(`http://localhost:9000/store/carts`, {
method: "POST",
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
"Content-Type": "application/json",
},
body: JSON.stringify({
region_id: region.id,
}),
sdk.store.cart.create({
region_id: region.id,
})
.then((res) => res.json())
.then(({ cart: dataCart }) => {
localStorage.setItem("cart_id", dataCart.id)
setCart(dataCart)
})
} else {
// retrieve cart
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.cart.retrieve(cartId)
.then(({ cart: dataCart }) => {
setCart(dataCart)
})
@@ -131,10 +129,21 @@ export const useCart = () => {
}
```
The `CartProvider` handles retrieving or creating the customer's cart. It uses the `useRegion` hook defined in the [Region Context guide](../../regions/context/page.mdx).
The `CartProvider` handles retrieving or creating the customer's cart. It uses the `useRegion` hook defined in the [Region Context guide](../../regions/context/page.mdx) to associate the cart with the customer's selected region.
The `useCart` hook returns the value of the `CartContext`. Child components of `CartProvider` use this hook to access `cart`, `setCart`, or `refreshCart`.
`refreshCart` unsets the cart, which triggers the `useEffect` callback to create a cart. This is useful when the customer logs in or out, or after the customer places an order.
<Note title="Tip">
You can add to the context and provider other functions useful for updating the cart and its items. Refer to the following guides for details on how to implement these functions:
- [Manage Cart Items](../manage-items/page.mdx).
- [Update Cart's Region and Customer](../update/page.mdx).
</Note>
---
## Use CartProvider in Component Tree
@@ -147,8 +156,8 @@ For example, if you're using Next.js, add it to the `app/layout.tsx` or `src/app
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { CartProvider } from "../providers/cart"
import { RegionProvider } from "../providers/region"
import { CartProvider } from "@/providers/cart"
import { RegionProvider } from "@/providers/region"
const inter = Inter({ subsets: ["latin"] })
@@ -177,9 +186,9 @@ export default function RootLayout({
}
```
---
Make sure to put the `CartProvider` as a child of the `RegionProvider` since it uses the `useRegion` hook defined in the [Region Context guide](../../regions/context/page.mdx).
## Use useCart Hook
### Use useCart Hook
Now, you can use the `useCart` hook in child components of `CartProvider`.
@@ -188,10 +197,12 @@ For example:
```tsx
"use client" // include with Next.js 13+
// ...
import { useCart } from "../providers/cart"
import { useCart } from "@/providers/cart"
export default function Products() {
const { cart } = useCart()
// ...
}
```
The `useCart` hook returns the cart details, which you can use in your components.

View File

@@ -12,64 +12,44 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to create and store a cart.
In this guide, you'll learn how to create and store a cart in your storefront.
## Create Cart on First Access
It's recommended to create a cart the first time a customer accesses a page, then store the cart's ID in the `localStorage`.
It's recommended to create a cart the first time a customer accesses a page in your storefront. Then, you can store the cart's ID in the `localStorage` and access it whenever necessary.
To create a cart, send a request to the [Create Cart API route](!api!/store#carts_postcarts).
For example:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["5", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to associate the correct sales channel(s)."],
["9", "region_id", "Associate the cart with the chosen region for accurate pricing."],
["14", "setItem", "Set the cart's ID in the `localStorage`."]
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/carts`, {
method: "POST",
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
"Content-Type": "application/json",
},
body: JSON.stringify({
region_id: region.id,
}),
})
.then((res) => res.json())
.then(({ cart }) => {
localStorage.setItem("cart_id", cart.id)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["8", "region", "Assuming you previously retrieved the chosen region."],
["15", "cartId", "Retrieve the cart ID from `localStorage`, if exists."],
["22", "fetch", "Send a request to create the cart."],
["26", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to associate the correct sales channel(s)."],
["30", "region_id", "Associate the cart with the chosen region for accurate pricing."],
["35", "setItem", "Set the cart's ID in the `localStorage`."]
["9", "region", "Assuming you previously retrieved the chosen region."],
["17", "cartId", "Retrieve the cart ID from `localStorage`, if exists."],
["24", "create", "Send a request to create the cart."],
["25", "region_id", "Associate the cart with the chosen region for accurate pricing."],
["28", "setItem", "Set the cart's ID in the `localStorage`."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { useEffect } from "react"
import { sdk } from "@/lib/sdk"
// other imports...
export default function Home() {
// TODO assuming you have the region retrieved
const region = {
id: "reg_123",
// ...
}
@@ -83,18 +63,9 @@ export const highlights = [
}
// create a cart and store it in the localStorage
fetch(`http://localhost:9000/store/carts`, {
method: "POST",
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
"Content-Type": "application/json",
},
body: JSON.stringify({
region_id: region.id,
}),
sdk.store.cart.create({
region_id: region.id,
})
.then((res) => res.json())
.then(({ cart }) => {
localStorage.setItem("cart_id", cart.id)
})
@@ -104,18 +75,50 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["2", "region_id", "Associate the cart with the chosen region for accurate pricing."],
["5", "setItem", "Set the cart's ID in the `localStorage`."]
]
```ts highlights={fetchHighlights}
sdk.store.cart.create({
region_id: region.id,
})
.then(({ cart }) => {
localStorage.setItem("cart_id", cart.id)
})
```
</CodeTab>
</CodeTabs>
The response of the Create Cart API route has a `cart` field, which is a cart object.
In this example, you create a cart by sending a request to the [Create Cart API route](!api!/store#carts_postcarts).
The response of the Create Cart API route has a `cart` field, which is a [cart object](!api!/store#carts_cart_schema).
Refer to the [Create Cart API reference](!api!/store#carts_postcarts) for details on other available request parameters.
### Publishable API Key
### Cart's Sales Channel Scope
When you create a cart, you pass the publishable API key in the header of the request. This associates the cart with the sales channel(s) of the publishable API key.
As mentioned before, you must always pass the publishable API key in the header of the request (which is done automatically by the JS SDK, as explained in the [Publishable API Keys](../../publishable-api-keys/page.mdx) guide). So, Medusa will associate the cart with the sales channel(s) of the publishable API key.
This is necessary, as only products matching the cart's sales channel(s) can be added to the cart.
This is necessary, as only products matching the cart's sales channel(s) can be added to the cart. If you want to associate the cart with a different sales channel, or if the publishable API key is associated with multiple sales channels and you want to specify which one to use, you can pass the `sales_channel_id` parameter to the [Create Cart API route](!api!/store#carts_postcarts) with the desired sales channel's ID.
For example:
```ts
sdk.store.cart.create({
region_id: region.id,
sales_channel_id: "sc_123",
})
.then(({ cart }) => {
// TODO use the cart...
console.log(cart)
})
```
---
@@ -123,4 +126,10 @@ This is necessary, as only products matching the cart's sales channel(s) can be
When the cart is created for a logged-in customer, it's automatically associated with that customer.
However, if the cart is created for a guest customer, then the customer logs in, then you have to set the cart's customer as explained in [this guide](../update/page.mdx#set-carts-customer).
However, if the cart is created for a guest customer, then the customer logs in, then you have to set the cart's customer as explained in the [Update Cart](../update/page.mdx#set-carts-customer) guide.
---
## Store Cart Details in React Context
If you're using React, it's then recommended to create a context that stores the cart details and make it available to all components in your application, as explained in the [Cart React Context in Storefront](../context/page.mdx) guide.

View File

@@ -12,7 +12,7 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to manage a cart's line items, including adding, updating, and removing them.
In this guide, you'll learn how to manage a cart's line items, including adding, updating, and removing them.
## Add Product Variant to Cart
@@ -20,17 +20,22 @@ To add a product variant to a cart, use the [Add Line Item API route](!api!/stor
<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).
To retrieve a variant's available quantity and check if it's in stock, refer to the [Retrieve Product Variant's Inventory](../../products/inventory/page.mdx) guide.
</Note>
For example:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
export const addHighlights = [
["1", "variant_id", "The ID of the selected variant."],
["2", "cartId", "Retrieve the cart ID from the `localStorage`."],
["13", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "You must pass the publishable API key of all storefront requests."],
["17", "quantity", "You can also allow customers to specify the quantity."]
["10", "quantity", "You can also allow customers to specify the quantity."]
]
```ts highlights={addHighlights}
@@ -41,19 +46,10 @@ const addToCart = (variant_id: string) => {
return
}
fetch(`http://localhost:9000/store/carts/${cartId}/line-items`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
variant_id,
quantity: 1,
}),
sdk.store.cart.createLineItem(cartId, {
variant_id,
quantity: 1,
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart
console.log(cart)
@@ -62,12 +58,12 @@ const addToCart = (variant_id: string) => {
}
```
The Add Line Item API route requires two request body parameters:
The [Add Line Item API route](!api!/store#carts_postcartsidlineitems) requires two request body parameters:
- `variant_id`: The ID of the product variant to add to the cart. This is the variant selected by the customer.
- `quantity`: The quantity to add to cart.
The API route returns the updated cart object.
The API route returns the updated [cart object](!api!/store#carts_cart_schema).
---
@@ -81,8 +77,7 @@ export const updateHighlights = [
["2", "itemId", "The ID of the item to update."],
["3", "quantity", "The new quantity of the item."],
["5", "cartId", "Retrieve the cart ID from the `localStorage`."],
["12", "itemId", "Pass the item's ID as a path parameter."],
["18", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "You must pass the publishable API key of all storefront requests."],
["11", "itemId", "Pass the item's ID as a parameter."],
]
```ts highlights={updateHighlights}
@@ -96,20 +91,9 @@ const updateQuantity = (
return
}
fetch(`http://localhost:9000/store/carts/${cartId}/line-items/${
itemId
}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
quantity,
}),
sdk.store.cart.updateLineItem(cartId, itemId, {
quantity,
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart
console.log(cart)
@@ -117,12 +101,12 @@ const updateQuantity = (
}
```
The Update Line Item API route requires:
The [Update Line Item API route](!api!/store#carts_postcartsidlineitemsline_id) requires:
- The line item's ID to be passed as a path parameter.
- The `quantity` request body parameter, which is the new quantity of the item.
The API route returns the updated cart object.
The API route returns the updated [cart object](!api!/store#carts_cart_schema).
---
@@ -135,9 +119,8 @@ For example:
export const deleteHighlights = [
["1", "itemId", "The ID of the line item to remove."],
["2", "cartId", "Retrieve the cart ID from the `localStorage`."],
["9", "itemId", "Pass the item's ID as a path parameter."],
["13", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "You must pass the publishable API key of all storefront requests."],
["18", "parent", "The updated cart is returned as the `parent` field."]
["8", "itemId", "Pass the item's ID as a parameter."],
["9", "parent", "The updated cart is returned as the `parent` field."]
]
```ts highlights={deleteHighlights}
@@ -148,16 +131,7 @@ const removeItem = (itemId: string) => {
return
}
fetch(`http://localhost:9000/store/carts/${cartId}/line-items/${
itemId
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
method: "DELETE",
})
.then((res) => res.json())
sdk.store.cart.deleteLineItem(cartId, itemId)
.then(({ parent: cart }) => {
// use cart
console.log(cart)
@@ -165,4 +139,4 @@ const removeItem = (itemId: string) => {
}
```
The Delete Line Item API route returns the updated cart object as the `parent` field.
The [Delete Line Item API route](!api!/store#carts_deletecartsidlineitemsline_id) returns the updated [cart object](!api!/store#carts_cart_schema) as the `parent` field.

View File

@@ -12,42 +12,21 @@ export const metadata = {
# {metadata.title}
You can retrieve a cart by sending a request to the [Get a Cart API route](!api!/store#carts_getcartsid).
In this guide, you'll learn how to retrieve a cart's details in your storefront.
Assuming you stored the cart's ID in the `localStorage` as explained in the [Create Cart guide](../create/page.mdx), pass that ID as a path parameter to the request.
Assuming you stored the cart's ID in the `localStorage` as explained in the [Create Cart guide](../create/page.mdx), you can retrieve a cart by sending a request to the [Get a Cart API route](!api!/store#carts_getcartsid).
For example:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["1", "cartId", "Pass the customer's cart ID as a path parameter."],
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["16", "cartId", "Retrieve the cart ID from `localStorage`."],
["18", "TODO", "You can create the cart and set it here as explained in the Create Cart guide."],
["22"], ["23"], ["24"], ["25"], ["26"], ["27"], ["28"], ["29"], ["30"], ["31"],
["34", "formatPrice", "This function was previously created to format product prices. You can re-use the same function."],
["37", "currency_code", "If you reuse the `formatPrice` function, pass the currency code as a parameter."],
["17", "cartId", "Retrieve the cart ID from `localStorage`."],
["19", "TODO", "You can create the cart and set it here as explained in the Create Cart guide."],
["23"], ["24"], ["25"], ["26"],
["29", "formatPrice", "This function was previously created to format product prices. You can re-use the same function."],
["32", "currency", "If you reuse the `formatPrice` function, pass the currency code as a parameter."],
]
```tsx highlights={highlights}
@@ -55,6 +34,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
export default function Cart() {
const [cart, setCart] = useState<
@@ -72,13 +52,7 @@ export const highlights = [
return
}
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.cart.retrieve(cartId)
.then(({ cart: dataCart }) => {
setCart(dataCart)
})
@@ -115,14 +89,29 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["1", "cartId", "Pass the customer's cart ID as a parameter."],
]
```ts highlights={fetchHighlights}
sdk.store.cart.retrieve(cartId)
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
</CodeTab>
</CodeTabs>
The response of the [Get Cart API](!api!/store#carts_getcartsid) route has a `cart` field, which is a cart object.
In this example, you retrieve a cart by sending a request to the [Get a Cart API route](!api!/store#carts_getcartsid).
---
The response of the [Get a Cart API route](!api!/store#carts_getcartsid) has a `cart` field, which is a [cart object](!api!/store#carts_cart_schema).
## Format Prices
### Format Prices
When displaying the cart's totals or line item's price, make sure to format the price as implemented in the `formatPrice` function shown in the above snippet:
@@ -136,4 +125,4 @@ const formatPrice = (amount: number): string => {
}
```
Since this is the same function used to format the prices of products, you can define the function in one place and re-use it where necessary. In that case, make sure to pass the currency code as a parameter.
Since this is the same function used to [format the prices of product variants](../../products/price/page.mdx), you can define the function in one place and re-use it where necessary. In that case, make sure to pass the currency code as a parameter to the `formatPrice` function.

View File

@@ -77,6 +77,12 @@ The fields that are most commonly used are:
Here's an example of how you can show the cart totals in a React component:
<Note title="Tip">
This example uses the `useCart` hook from the [Cart Context](../context/page.mdx) to retrieve the cart.
</Note>
export const highlights = [
["3", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["8", "formatPrice", "A function to format a price using the `Intl.NumberFormat` API."],
@@ -90,7 +96,7 @@ export const highlights = [
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useCart } from "../../../providers/cart"
import { useCart } from "@/providers/cart"
export default function CartTotals() {
const { cart } = useCart()

View File

@@ -12,11 +12,11 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to update different details of a cart.
In this guide, you'll learn how to update different details of a cart.
<Note>
<Note title="Tip">
All cart updates are performed using the [Update Cart API route](!api!/store#carts_postcartsid).
This guide doesn't cover updating the cart's line items. For that, refer to the [Manage Cart's Items in Storefront](../manage-items/page.mdx) guide.
</Note>
@@ -26,32 +26,27 @@ If a customer changes their region, you must update their cart to be associated
For example:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
export const updateRegionHighlights = [
["11", `"new_id"`, "Pass the new chosen region's ID."]
["2", `"new_id"`, "Pass the new chosen region's ID."]
]
```ts highlights={updateRegionHighlights}
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
},
body: JSON.stringify({
region_id: "new_id",
}),
sdk.store.cart.update(cartId, {
region_id: "new_id",
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
The Update Cart API route accepts a `region_id` request body parameter, whose value is the new region to associate with the cart.
The [Update Cart API route](!api!/store#carts_postcartsid) accepts a `region_id` request body parameter, whose value is the new region to associate with the cart.
---
@@ -71,27 +66,19 @@ This API route is only available after [Medusa v2.0.5](https://github.com/medusa
</Note>
```ts
fetch(`http://localhost:9000/store/carts/${cartId}/customer`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.cart.transferCart(cartId)
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
To send an authenticated request, either use `credentials: include` if the customer is already authenticated with a cookie session, or pass the Authorization Bearer token in the request's header.
Assuming the JS SDK is configured to send an authenticated request, the cart is now associated with the logged-in customer.
Learn more about authenticating customers with the JS SDK in the [Login Customer guide](../../customers/login/page.mdx).
<Note title="Tip">
Learn more about authenticating customers in [this guide](../../customers/login/page.mdx).
When using the Fetch API to send the request, either use `credentials: include` if the customer is already authenticated with a cookie session, or pass the Authorization Bearer token in the request's header.
</Note>
The cart is now associated with the logged-in customer.

View File

@@ -13,71 +13,51 @@ export const metadata = {
# {metadata.title}
The second step of the checkout flow is to ask the customer for their address. A cart has shipping and billing addresses that customers need to set.
In this guide, you'll learn how to set the cart's shipping and billing addresses. This typically should be the second step of the checkout flow, but you can also change the steps of the checkout flow as you see fit.
You can either show a form to enter the address, or, if the customer is logged in, allow them to pick an address from their account.
## Approaches to Set the Cart's Addresses
A cart has shipping and billing addresses that customers need to set. You can either:
- [Show a form to enter the address](#approach-one-address-form);
- Or [allow the customer to pick an address from their account](#approach-two-select-customer-address).
This guide shows you how to implement both approaches. You can choose either or combine them, based on your use case.
---
## Approach One: Address Form
The first approach to setting the cart's shipping and billing addresses is to show a form to the customer to enter their address details. To update the cart's address, use the [Update Cart API route](!api!/store#carts_postcartsid) to update the cart's addresses.
The first approach to setting the cart's shipping and billing addresses is to show a form to the customer to enter their address details.
Then, to update the cart's address, use the [Update Cart API route](!api!/store#carts_postcartsid).
For example:
<Note title="Tip">
- This example uses the `useCart` hook defined in the [Cart React Context guide](../../cart/context/page.mdx).
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
const cartId = localStorage.getItem("cart_id")
const address = {
first_name,
last_name,
address_1,
company,
postal_code,
city,
country_code,
province,
phone,
}
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
shipping_address: address,
billing_address: address,
}),
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["29", "address", "Assemble the address object to be used for both shipping and billing addresses."],
["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"], ["48"],
["49"], ["50"], ["51"], ["52"], ["53"], ["54"], ["55"], ["56"],
["103", "", "The address's country can only be within the cart's region."]
["30", "address", "Assemble the address object to be used for both shipping and billing addresses."],
["42"], ["43"], ["44"], ["45"], ["46"], ["47"], ["48"],
["49"], ["50"],
["96", "", "The address's country can only be within the cart's region."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useState } from "react"
import { useCart } from "../../../providers/cart"
import { useCart } from "@/providers/cart"
import { sdk } from "@/lib/sdk"
export default function CheckoutAddressStep() {
const { cart, setCart } = useCart()
@@ -109,26 +89,18 @@ export const highlights = [
company,
postal_code: postalCode,
city,
country_code: countryCode,
country_code: countryCode || cart.region?.countries?.[0].iso_2,
province,
phone: phoneNumber,
}
fetch(`http://localhost:9000/store/carts/${cart.id}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
shipping_address: address,
billing_address: address,
}),
sdk.store.cart.update(cart.id, {
shipping_address: address,
billing_address: address,
})
.then((res) => res.json())
.then(({ cart: updatedCart }) => {
setCart(updatedCart)
console.log(updatedCart)
})
.finally(() => setLoading(false))
}
@@ -208,6 +180,34 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
const cartId = localStorage.getItem("cart_id")
const address = {
first_name,
last_name,
address_1,
company,
postal_code,
city,
country_code,
province,
phone,
}
sdk.store.cart.update(cart.id, {
shipping_address: address,
billing_address: address,
})
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
</CodeTab>
</CodeTabs>
@@ -216,13 +216,15 @@ In the example above:
- The same address is used for shipping and billing for simplicity. You can provide the option to enter both addresses instead.
- You send the address to the Update Cart API route under the `shipping_address` and `billing_address` request body parameters.
- The updated cart object is returned in the response.
- **React example:** in the address, the chosen country must be in the cart's region. So, only the countries part of the cart's region are shown.
- **React example:** in the address, the chosen country must be in the cart's region. So, only the countries part of the cart's region are shown in the Country input.
---
## Approach Two: Select Customer Address
The second approach to setting the cart's shipping and billing addresses is to allow the logged-in customer to select an address they added previously to their account. To retrieve the customer's addresses, use the [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddresses). Then, once the customer selects an address, use the [Update Cart API route](!api!/store#carts_postcartsid) to update the cart's addresses.
The second approach to setting the cart's shipping and billing addresses is to allow the logged-in customer to select an address they added previously to their account.
To retrieve the logged-in customer's addresses, use the [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddresses). Then, once the customer selects an address, use the [Update Cart API route](!api!/store#carts_postcartsid) to update the cart's addresses.
<Note title="Good to Know">
@@ -232,89 +234,34 @@ A customer's address and a cart's address are represented by different data mode
For example:
<Note title="Tip">
- This example uses the `useCart` hook defined in the [Cart React Context guide](../../cart/context/page.mdx).
- This example uses the `useCustomer` hook defined in the [Customer React Context guide](../../customers/context/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetch2Highlights = [
["1", "cartId", "Assuming the cart's ID is stored in the database."],
["3", "retrieveCustomerAddresses", "Retrieve the customer's addresses."],
["18", "updateCartAddress", "Update the cart's address with the selected customer address."],
["19", "address", "Map the customer address to the expected cart address."],
["39", "shipping_address", "Pass the selected address as a shipping address."],
["40", "billing_address", "Pass the selected address as a billing address."],
]
```ts highlights={fetch2Highlights}
const cartId = localStorage.getItem("cart_id")
const retrieveCustomerAddresses = () => {
fetch("http://localhost:9000/store/customers/me/addresses", {
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ addresses }) => {
// use addresses...
console.log(addresses)
})
}
const updateCartAddress = (customerAddress: Record<string, unknown>) => {
const address = {
first_name: customerAddress.first_name || "",
last_name: customerAddress.last_name || "",
address_1: customerAddress.address_1 || "",
company: customerAddress.company || "",
postal_code: customerAddress.postal_code || "",
city: customerAddress.city || "",
country_code: customerAddress.country_code || cart.region?.countries?.[0].iso_2,
province: customerAddress.province || "",
phone: customerAddress.phone || "",
}
fetch(`http://localhost:9000/store/carts/${cart.id}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
shipping_address: address,
billing_address: address,
}),
})
.then((res) => res.json())
.then(({ cart: updatedCart }) => {
// use cart...
console.log(cart)
})
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const react2Highlights = [
["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["5", "useCustomer", "The `useCustomer` hook was defined in the Customer React Context documentation."],
["11", "selectedAddress", "Store the ID of the address that the customer selects."],
["20", "updateAddress", "Update the cart's shipping and billing addresses based on the selected address."],
["31", "address", "Map the customer address to the expected cart address."],
["51", "shipping_address", "Pass the selected address as a shipping address."],
["52", "billing_address", "Pass the selected address as a billing address."],
["66", "select", "Show a dropdown to select the customer's address."],
["12", "selectedAddress", "Store the ID of the address that the customer selects."],
["21", "updateAddress", "Update the cart's shipping and billing addresses based on the selected address."],
["32", "address", "Map the customer address to the expected cart address."],
["45", "shipping_address", "Pass the selected address as a shipping address."],
["46", "billing_address", "Pass the selected address as a billing address."],
["58", "select", "Show a select input to select from the customer's addresses."],
]
```tsx highlights={react2Highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { useCart } from "../../../providers/cart"
import { useCustomer } from "../../../providers/customer"
import { useCart } from "@/providers/cart"
import { useCustomer } from "@/providers/customer"
import { sdk } from "@/lib/sdk"
export default function CheckoutAddressStep() {
const { cart, setCart } = useCart()
@@ -352,19 +299,10 @@ export default function CheckoutAddressStep() {
phone: customerAddress.phone || "",
}
fetch(`http://localhost:9000/store/carts/${cart.id}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
shipping_address: address,
billing_address: address,
}),
sdk.store.cart.update(cart.id, {
shipping_address: address,
billing_address: address,
})
.then((res) => res.json())
.then(({ cart: updatedCart }) => {
setCart(updatedCart)
})
@@ -389,6 +327,53 @@ export default function CheckoutAddressStep() {
</form>
)
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetch2Highlights = [
["1", "cartId", "Assuming the cart's ID is stored in the local storage."],
["3", "retrieveCustomerAddresses", "Retrieve the customer's addresses from Medusa."],
["11", "updateCartAddress", "Update the cart's address with the selected customer address."],
["12", "address", "Map the customer address to the expected cart address."],
["25", "shipping_address", "Pass the selected address as a shipping address."],
["26", "billing_address", "Pass the selected address as a billing address."],
]
```ts highlights={fetch2Highlights}
const cartId = localStorage.getItem("cart_id")
const retrieveCustomerAddresses = () => {
sdk.store.customer.listAddress()
.then(({ addresses }) => {
// use addresses...
console.log(addresses)
})
}
const updateCartAddress = (customerAddress: Record<string, unknown>) => {
const address = {
first_name: customerAddress.first_name || "",
last_name: customerAddress.last_name || "",
address_1: customerAddress.address_1 || "",
company: customerAddress.company || "",
postal_code: customerAddress.postal_code || "",
city: customerAddress.city || "",
country_code: customerAddress.country_code || cart.region?.countries?.[0].iso_2,
province: customerAddress.province || "",
phone: customerAddress.phone || "",
}
sdk.store.cart.update(cart.id, {
shipping_address: address,
billing_address: address,
})
.then(({ cart: updatedCart }) => {
// use cart...
console.log(cart)
})
}
```
</CodeTab>
@@ -396,10 +381,12 @@ export default function CheckoutAddressStep() {
In the example above, you retrieve the customer's addresses and, when the customer selects an address, you update the cart's shipping and billing addresses with the selected address.
In the React example, you use the [Customer React Context](../../customers/context/page.mdx) to retrieve the logged-in customer, who has a list of addresses. You show a dropdown to select the address, and when the customer selects an address, you send a request to update the cart's addresses.
<Note title="Tip">
For both examples, you send a request as an authenticated customer using the cookie session. Learn about other options to send an authenticated request in [this guide](../../customers/login/page.mdx).
The JS SDK automatically sends an authenticated request as the logged-in customer as explained in the [Login Customer guide](../../customers/login/page.mdx). If you're using the Fetch API, you can either use `credentials: include` if the customer is already authenticated with a cookie session, or pass the Authorization Bearer token in the request's header.
</Note>
In the React example, you use the [Customer React Context](../../customers/context/page.mdx) to retrieve the logged-in customer, who has a list of addresses. You show a select input to select an address.
When the customer selects an address, you send a request to [Update Cart API route](!api!/store#carts_postcartsid) passing the selected address as a shipping and billing address.

View File

@@ -14,32 +14,30 @@ export const metadata = {
# {metadata.title}
In this guide, you'll learn how to complete the cart and place the order. This is the last step of your checkout flow.
## How to Complete Cart in Storefront Checkout
Once you finish any required actions with the third-party payment provider, you can complete the cart and place the order.
To complete the cart, send a request to the [Complete Cart API route](!api!/store#carts_postcartsidcomplete).
To complete the cart, send a request to the [Complete Cart API route](!api!/store#carts_postcartsidcomplete). For example:
For example:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
```ts
fetch(
`http://localhost:9000/store/carts/${cartId}/complete`,
{
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
method: "POST",
}
)
.then((res) => res.json())
.then(({ type, cart, order, error }) => {
if (type === "cart" && cart) {
sdk.store.cart.complete(cart.id)
.then((data) => {
if (data.type === "cart" && data.cart) {
// an error occured
console.error(error)
} else if (type === "order" && order) {
console.error(data.error)
} else if (data.type === "order" && data.order) {
// TODO redirect to order success page
alert("Order placed.")
console.log(order)
console.log(data.order)
// unset cart ID from local storage
localStorage.removeItem("cart_id")
}
@@ -59,22 +57,29 @@ When the cart completion is successful, it's important to unset the cart ID from
For example, to complete the cart when the default system payment provider is used:
<Note title="Tip">
This example uses the `useCart` hook defined in the [Cart React Context guide](../../cart/context/page.mdx).
</Note>
export const highlights = [
["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["10", "handlePayment", "This function sends the request\nto the Medusa application to complete the cart."],
["21", "TODO", "If you're integrating a third-party payment provider,\nyou perform the custom logic before completing the cart."],
["24", "fetch", "Send a request to the Medusa application\nto complete the cart and place the order."],
["36", `type === "cart"`, "If the `type` returned is `cart`,\nit means an error occurred and the cart wasn't completed."],
["39", `type === "order"`, "If the `type` returned is `order`,\nit means the cart was completed and the order was placed successfully."],
["43", "refreshCart", "Unset and reset the cart."],
["50", "button", "This button triggers the `handlePayment` function when clicked."]
["11", "handlePayment", "This function sends the request\nto the Medusa application to complete the cart."],
["22", "TODO", "If you're integrating a third-party payment provider,\nyou perform the custom logic before completing the cart."],
["25", "complete", "Send a request to the Medusa application\nto complete the cart and place the order."],
["27", `data.type === "cart"`, "If the `type` returned is `cart`,\nit means an error occurred and the cart wasn't completed."],
["30", `type === "order"`, "If the `type` returned is `order`,\nit means the cart was completed and the order was placed successfully."],
["34", "refreshCart", "Unset and reset the cart."],
["41", "button", "This button triggers the `handlePayment` function when clicked."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useState } from "react"
import { useCart } from "../../providers/cart"
import { useCart } from "@/providers/cart"
import { sdk } from "@/lib/sdk"
export default function SystemDefaultPayment() {
const { cart, refreshCart } = useCart()
@@ -94,25 +99,15 @@ export default function SystemDefaultPayment() {
// TODO perform any custom payment handling logic
// complete the cart
fetch(
`http://localhost:9000/store/carts/${cart.id}/complete`,
{
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
method: "POST",
}
)
.then((res) => res.json())
.then(({ type, cart, order, error }) => {
if (type === "cart" && cart) {
sdk.store.cart.complete(cart.id)
.then((data) => {
if (data.type === "cart" && data.cart) {
// an error occured
console.error(error)
} else if (type === "order" && order) {
console.error(data.error)
} else if (data.type === "order" && data.order) {
// TODO redirect to order success page
alert("Order placed.")
console.log(order)
console.log(data.order)
refreshCart()
}
})
@@ -133,13 +128,13 @@ export default function SystemDefaultPayment() {
In the example above, you create a `handlePayment` function in the payment component. In this function, you:
- Optionally perform any required actions with the third-party payment provider. For example, authorize the payment. For the default system payment provider, no actions are required.
- Send a request to the Complete Cart API route once all actions with the third-party payment provider are performed.
- Send a request to the [Complete Cart API route](!api!/store#carts_postcartsidcomplete) once all actions with the third-party payment provider are performed.
- In the received response of the request, if the `type` is `cart`, it means that the cart completion failed. The error is set in the `error` response field.
- If the `type` is `order`, it means the card was completed and the order was placed successfully. You can access the order in the `order` response field.
- When the order is placed, you must unset the `cart_id` from the `localStorage`. You can redirect the customer to an order success page at this point.
- When the order is placed, you must unset the `cart_id` from the `localStorage`. You can redirect the customer to an order success page at this point. The redirection logic depends on the framework you're using.
---
## React Example with Third-Party Provider
## React Example with Third-Party Payment Provider
Refer to the [Stripe guide](../payment/stripe/page.mdx) for an example on integrating a third-party provider and implementing card completion.

View File

@@ -12,7 +12,9 @@ export const metadata = {
# {metadata.title}
The first step of the checkout flow is to enter the customer's email. Then, use the [Update Cart API route](!api!/store#carts_postcartsid) to update the cart with the email.
In this guide, you'll learn how to add an email step to the checkout flow. This typically would be the first step of the checkout flow, but you can also change the steps of the checkout flow as you see fit.
When the user enters their email, use the [Update Cart API route](!api!/store#carts_postcartsid) to update the cart with the email.
<Note title="Tip">
@@ -22,45 +24,28 @@ If the customer is logged-in, you can pre-fill the email with the customer's ema
For example:
<Note title="Tip">
- This example uses the `useCart` hook defined in the [Cart React Context guide](../../cart/context/page.mdx).
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
const cartId = localStorage.getItem("cart_id")
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
email,
}),
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["13", "TODO", "Cart must have at least one item. If not, redirect to another page."],
["27"], ["28"], ["29"], ["30"], ["31"], ["32"], ["33"], ["34"],
["35"], ["36"], ["37"], ["38"], ["39"], ["40"], ["41"]
["14", "TODO", "Cart must have at least one item. If not, redirect to another page."],
["28"], ["29"], ["30"], ["31"], ["32"], ["33"], ["34"],
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useState } from "react"
import { useCart } from "../../../providers/cart"
import { useEffect, useState } from "react"
import { useCart } from "@/providers/cart"
import { sdk } from "@/lib/sdk"
export default function CheckoutEmailStep() {
const { cart, setCart } = useCart()
@@ -83,18 +68,9 @@ export const highlights = [
e.preventDefault()
setLoading(true)
fetch(`http://localhost:9000/store/carts/${cart.id}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
email,
}),
sdk.store.cart.update(cart.id, {
email,
})
.then((res) => res.json())
.then(({ cart: updatedCart }) => {
setCart(updatedCart)
})
@@ -122,9 +98,24 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
const cartId = localStorage.getItem("cart_id")
sdk.store.cart.update(cart.id, {
email,
})
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
</CodeTab>
</CodeTabs>
After the customer enters and submits their email, you send a request to the Update Cart API route passing it the email in the request body.
After the customer enters and submits their email, you send a request to the [Update Cart API route](!api!/store#carts_postcartsid) passing it the email in the request body.
Notice that if the cart doesn't have items, you should redirect to another page as the checkout requires at least one item in the cart.
Notice that if the cart doesn't have items, you should redirect to another page as the checkout requires at least one item in the cart. Redirecting to another page is not covered in this guide as this depends on the framework you're using.

View File

@@ -12,10 +12,10 @@ export const metadata = {
# {metadata.title}
After the customer completes the checkout process and places an order, you can show an order confirmation page to display the order details.
In this guide, you'll learn how to show the different order details on the order confirmation page.
After the customer completes the checkout process and places an order, you can show an order confirmation page to display the order details.
## Retrieve Order Details
To show the order details, you need to retrieve the order by sending a request to the [Get an Order API route](!api!store#orders_getordersid).
@@ -24,25 +24,13 @@ You need the order's ID to retrieve the order. You can pass it from the [complet
The following example assumes you already have the order ID:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
// orderId is the order ID which you can get from the complete cart step
fetch(`http://localhost:9000/store/orders/${orderId}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ order }) => {
// use order...
console.log(order)
})
```
</CodeTab>
<CodeTab label="React" value="react">
```tsx
@@ -57,13 +45,7 @@ export function OrderConfirmation({ id }: { id: string }) {
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch(`http://localhost:9000/store/orders/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.order.retrieve(id)
.then(({ order: dataOrder }) => {
setOrder(dataOrder)
setLoading(false)
@@ -85,6 +67,18 @@ export function OrderConfirmation({ id }: { id: string }) {
</div>
)
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
// orderId is the order ID which you can get from the complete cart step
sdk.store.order.retrieve(orderId)
.then(({ order }) => {
// use order...
console.log(order)
})
```
</CodeTab>

View File

@@ -13,11 +13,9 @@ export const metadata = {
# {metadata.title}
The last step before completing the order is choosing the payment provider and performing any necessary actions.
In this guide, you'll learn how to implement the last step of the checkout flow, where the customer chooses the payment provider and performs any necessary actions. This is typically the fourth step of the checkout flow, but you can change the steps of the checkout flow as you see fit.
The actions required after choosing the payment provider are different for each provider. So, this guide doesn't cover that.
## Payment Step Flow
## Payment Step Flow in Storefront Checkout
The payment step requires implementing the following flow:
@@ -25,175 +23,51 @@ The payment step requires implementing the following flow:
1. Retrieve the payment providers using the [List Payment Providers API route](!api!/store#payment-providers_getpaymentproviders).
2. Customer chooses the payment provider to use.
3. If the cart doesn't have an associated payment collection, create a payment collection for it.
3. If the cart doesn't have an associated payment collection, create a payment collection for it using the [Create Payment Collection API route](!api!/store#payment-collections_postpaymentcollections).
4. Initialize the payment sessions of the cart's payment collection using the [Initialize Payment Sessions API route](!api!/store#payment-collections_postpaymentcollectionsidpaymentsessions).
5. Optionally perform additional actions for payment based on the chosen payment provider. For example, if the customer chooses stripe, you show them the UI to enter their card details.
- If you're using the JS SDK, it combines the third and fourth steps in a single `initiatePaymentSession` function.
5. Optionally perform additional actions for payment based on the chosen payment provider. For example, if the customer chooses Stripe, you show them the UI to enter their card details.
- You can refer to the [Stripe guide](./stripe/page.mdx) for an example of how to implement this.
---
## Code Example
## How to Implement the Payment Step Flow
For example, to implement the payment step flow:
<Note title="Tip">
- This example uses the `useCart` hook defined in the [Cart React Context guide](../../cart/context/page.mdx).
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["6", "retrievePaymentProviders", "This function retrieves the payment provider that the customer can choose from."],
["7", "fetch", "Retrieve available payment providers."],
["21", "selectPaymentProvider", "This function is executed when the customer submits their chosen payment provider."],
["28", "fetch", "Create a payment collection for the cart when it doesn't have one."],
["48", "fetch", "Initialize the payment session in the payment collection for the chosen provider."],
["67", "fetch", "Retrieve the cart again to update its data."],
["81", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."],
["82", "activePaymentSession", "The active session is the first in the payment collection's sessions."],
["88", "", "Test which payment provider is chosen based on the prefix of the provider ID."],
["89", `"pp_stripe_"`, "Check if the chosen provider is Stripe."],
["93", `"pp_system_default"`, "Check if the chosen provider is the default systen provider."],
["95", "default", "Handle unrecognized providers."],
["102", "handlePayment", "The function that handles the payment process using the above functions."]
]
```ts highlights={fetchHighlights}
// assuming the cart is previously fetched
const cart = {
// cart object...
}
const retrievePaymentProviders = async () => {
const { payment_providers } = await fetch(
`http://localhost:9000/store/payment-providers?region_id=${
cart.region_id
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
return payment_providers
}
const selectPaymentProvider = async (
selectedPaymentProviderId: string
) => {
let paymentCollectionId = cart.payment_collection?.id
if (!paymentCollectionId) {
// create payment collection
const { payment_collection } = await fetch(
`http://localhost:9000/store/payment-collections`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
cart_id: cart.id,
}),
}
)
.then((res) => res.json())
paymentCollectionId = payment_collection.id
}
// initialize payment session
await fetch(`http://localhost:9000/store/payment-collections/${
paymentCollectionId
}/payment-sessions`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
provider_id: selectedPaymentProviderId,
}),
})
.then((res) => res.json())
// re-fetch cart
const {
cart: updatedCart,
} = await fetch(
`http://localhost:9000/store/carts/${cart.id}`,
{
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
return updatedCart
}
const getPaymentUi = () => {
const activePaymentSession = cart?.payment_collection?.
payment_sessions?.[0]
if (!activePaymentSession) {
return
}
switch(true) {
case activePaymentSession.provider_id.startsWith("pp_stripe_"):
// TODO handle Stripe UI
return "You chose stripe!"
case activePaymentSession.provider_id
.startsWith("pp_system_default"):
return "You chose manual payment! No additional actions required."
default:
return `You chose ${
activePaymentSession.provider_id
} which is in development.`
}
}
const handlePayment = () => {
retrievePaymentProviders()
// ... customer chooses payment provider
// const providerId = ...
selectPaymentProvider(providerId)
getPaymentUi()
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["23", "fetch", "Retrieve available payment providers."],
["34", "setSelectedPaymentProvider", "If a payment provider was selected before, pre-fill it."],
["40", "handleSelectProvider", "This function is executed when the customer submits their chosen payment provider."],
["54", "fetch", "Create a payment collection for the cart when it doesn't have one."],
["74", "fetch", "Initialize the payment session in the payment collection for the chosen provider."],
["93", "fetch", "Retrieve the cart again to update its data."],
["108", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."],
["109", "activePaymentSession", "The active session is the first in the payment collection's sessions."],
["115", "", "Test which payment provider is chosen based on the prefix of the provider ID."],
["116", `"pp_stripe_"`, "Check if the chosen provider is Stripe."],
["124", `"pp_system_default"`, "Check if the chosen provider is the default systen provider."],
["130", "default", "Handle unrecognized providers."],
["165", "getPaymentUi", "If a provider is chosen, render its UI."]
["24", "listPaymentProviders", "Retrieve available payment providers."],
["29", "setSelectedPaymentProvider", "If a payment provider was selected before, pre-fill it."],
["35", "handleSelectProvider", "This function is executed when the customer submits their chosen payment provider."],
["45", "initiatePaymentSession", "Create a payment collection and initialize the payment session for the chosen provider."],
["50", "retrieve", "Retrieve the cart again to update its data."],
["56", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."],
["57", "activePaymentSession", "The active session is the first in the payment collection's sessions."],
["62", "", "Test which payment provider is chosen based on the prefix of the provider ID."],
["63", `"pp_stripe_"`, "Check if the chosen provider is Stripe."],
["71", `"pp_system_default"`, "Check if the chosen provider is the default systen provider."],
["77", "default", "Handle unrecognized providers."],
["112", "getPaymentUi", "If a provider is chosen, render its UI."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useCallback, useEffect, useState } from "react"
import { useCart } from "../../../providers/cart"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
export default function CheckoutPaymentStep() {
const { cart, setCart } = useCart()
@@ -211,15 +85,9 @@ export default function CheckoutPaymentStep() {
return
}
fetch(`http://localhost:9000/store/payment-providers?region_id=${
cart.region_id
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
sdk.store.payment.listPaymentProviders({
region_id: cart.region_id || "",
})
.then((res) => res.json())
.then(({ payment_providers }) => {
setPaymentProviders(payment_providers)
setSelectedPaymentProvider(
@@ -236,69 +104,21 @@ export default function CheckoutPaymentStep() {
return
}
setLoading(false)
setLoading(true)
let paymentCollectionId = cart.payment_collection?.id
if (!paymentCollectionId) {
// create payment collection
const { payment_collection } = await fetch(
`http://localhost:9000/store/payment-collections`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
cart_id: cart.id,
}),
}
)
.then((res) => res.json())
paymentCollectionId = payment_collection.id
}
// initialize payment session
await fetch(`http://localhost:9000/store/payment-collections/${
paymentCollectionId
}/payment-sessions`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
provider_id: selectedPaymentProvider,
}),
await sdk.store.payment.initiatePaymentSession(cart, {
provider_id: selectedPaymentProvider,
})
.then((res) => res.json())
// re-fetch cart
const {
cart: updatedCart,
} = await fetch(
`http://localhost:9000/store/carts/${cart.id}`,
{
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
const { cart: updatedCart } = await sdk.store.cart.retrieve(cart.id)
setCart(updatedCart)
setLoading(false)
}
const getPaymentUi = useCallback(() => {
const activePaymentSession = cart?.payment_collection?.
payment_sessions?.[0]
const activePaymentSession = cart?.payment_collection?.payment_sessions?.[0]
if (!activePaymentSession) {
return
}
@@ -357,6 +177,88 @@ export default function CheckoutPaymentStep() {
</div>
)
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["8", "retrievePaymentProviders", "This function retrieves the payment provider that the customer can choose from."],
["9", "listPaymentProviders", "Retrieve available payment providers."],
["16", "selectPaymentProvider", "This function is executed when the customer submits their chosen payment provider."],
["19", "initiatePaymentSession", "Create a payment collection and initialize the payment session for the chosen provider."],
["26", "retrieve", "Retrieve the cart again to update its data."],
["31", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."],
["32", "activePaymentSession", "The active session is the first in the payment collection's sessions."],
["38", "", "Test which payment provider is chosen based on the prefix of the provider ID."],
["39", `"pp_stripe_"`, "Check if the chosen provider is Stripe."],
["43", `"pp_system_default"`, "Check if the chosen provider is the default systen provider."],
["45", "default", "Handle unrecognized providers."],
["52", "handlePayment", "The function that handles the payment process using the above functions."]
]
```ts highlights={fetchHighlights}
// assuming the cart is previously fetched
const cart = {
id: "cart_123",
region_id: "reg_123",
// cart object...
}
const retrievePaymentProviders = async () => {
const { payment_providers } = await sdk.store.payment.listPaymentProviders({
region_id: cart.region_id || "",
})
return payment_providers
}
const selectPaymentProvider = async (
selectedPaymentProviderId: string
) => {
await sdk.store.payment.initiatePaymentSession(cart, {
provider_id: selectedPaymentProviderId,
})
// re-fetch cart
const {
cart: updatedCart,
} = await sdk.store.cart.retrieve(cart.id)
return updatedCart
}
const getPaymentUi = () => {
const activePaymentSession = cart?.payment_collection?.
payment_sessions?.[0]
if (!activePaymentSession) {
return
}
switch(true) {
case activePaymentSession.provider_id.startsWith("pp_stripe_"):
// TODO handle Stripe UI
return "You chose stripe!"
case activePaymentSession.provider_id
.startsWith("pp_system_default"):
return "You chose manual payment! No additional actions required."
default:
return `You chose ${
activePaymentSession.provider_id
} which is in development.`
}
}
const handlePayment = () => {
retrievePaymentProviders()
// ... customer chooses payment provider
// const providerId = ...
selectPaymentProvider(providerId)
getPaymentUi()
}
```
</CodeTab>
@@ -364,10 +266,15 @@ export default function CheckoutPaymentStep() {
In the example above, you:
- Retrieve the payment providers from the Medusa application. You use those to show the customer the available options.
- When the customer chooses a payment provider, you:
1. Check whether the cart has a payment collection. If not, create one using the [Create Payment Collection API route](!api!/store#payment-collections_postpaymentcollections).
2. Initialize the payment session for the chosen payment provider using the [Initialize Payment Session API route](!api!/store#payment-collections_postpaymentcollectionsidpaymentsessions).
- Once the cart has a payment session, you optionally render the UI to perform additional actions. For example, if the customer chose stripe, you can show them the card form to enter their credit card.
- Retrieve the payment providers from the Medusa application using the [List Payment Providers API route](!api!/store#payment-providers_getpaymentproviders). You use those to show the customer the available options.
- When the customer chooses a payment provider, you use the `initiatePaymentSession` function to create a payment collection and initialize the payment session for the chosen provider.
- If you're not using the JS SDK, you need to create a payment collection using the [Create Payment Collection API route](!api!/store#payment-collections_postpaymentcollections) if the cart doesn't have one. Then, you need to initialize the payment session using the [Initialize Payment Session API route](!api!/store#payment-collections_postpaymentcollectionsidpaymentsessions).
- Once the cart has a payment session, you optionally render the UI to perform additional actions. For example, if the customer chose Stripe, you can show them the card form to enter their credit card.
In the `Fetch API` example, the `handlePayment` function implements this flow.
In the `Fetch API` example, the `handlePayment` function implements this flow by calling the different functions in the correct order.
---
## Stripe Example
If you're integrating Stripe in your Medusa application and storefront, refer to the [Stripe guide](./stripe/page.mdx) for an example of how to handle the payment process using Stripe.

View File

@@ -14,11 +14,11 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to use Stripe for payment during checkout in a React-based storefront.
In this guide, you'll learn how to use Stripe for payment during checkout in a React-based storefront.
<Note title="Tip">
For other types of storefronts, the steps are similar. However, refer to [Stripe's documentation](https://docs.stripe.com/) for available tools for your tech stack.
For other types of frameworks or tech stacks, the steps are similar. Refer to [Stripe's documentation](https://docs.stripe.com/) for available tools for your tech stack.
</Note>
@@ -67,31 +67,34 @@ For Next.js storefronts, the environment variable's name must be prefixed with `
## 3. Create Stripe Component
Then, create a file holding the following Stripe component:
You can now create a Stripe component that renders the Stripe UI to accept payment.
<Note>
For example, you can create a file holding the following Stripe component:
This snippet assumes you're using the provider from the [Cart Context guide](../../../cart/context/page.mdx) in your storefront.
<Note title="Tip">
- This example uses the `useCart` hook defined in the [Cart React Context guide](../../../cart/context/page.mdx).
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
export const highlights = [
["10", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["13", "stripePromise", "Initialize stripe using the environment variable added in the previous step."],
["19", "clientSecret", "After initializing the payment session of Stripe in the Medusa application,\nthe client secret is available in the payment session's `data`."],
["27", "StripeForm", "The actual form must be a different component nested inside `Elements`."],
["44", "handlePayment", "This function is used to show Stripe's UI to accept payment,\nthen send the request to the Medusa application to complete the cart."],
["61", "confirmCardPayment", "This function shows the UI to the customer to accept the card payment."],
["78", "", "Once the customer enters their card details and submits the form,\nthe Promise resolves and executes this function."],
["85", "fetch", "Send a request to the Medusa application\nto complete the cart and place the order."],
["97", `type === "cart"`, "If the `type` returned is `cart`,\nit means an error occurred and the cart wasn't completed."],
["100", `type === "order"`, "If the `type` returned is `order`,\nit means the cart was completed and the order was placed successfully."],
["104", "refreshCart", "Unset and reset the cart."],
["114", "button", "This button triggers the `handlePayment` function when clicked."]
["14", "stripe", "Initialize stripe using the environment variable added in the previous step."],
["20", "clientSecret", "After initializing the payment session of Stripe in the Medusa application,\nthe client secret is available in the payment session's `data`."],
["34", "StripeForm", "The actual form must be a different component nested inside `Elements`."],
["45", "handlePayment", "This function is used to show Stripe's UI to accept payment,\nthen send the request to the Medusa application to complete the cart."],
["62", "confirmCardPayment", "This function shows the UI to the customer to accept the card payment."],
["79", "then", "Once the customer enters their card details and submits the form,\nthe Promise resolves and executes this function."],
["86", "complete", "Send a request to the Medusa application\nto complete the cart and place the order."],
["88", `data.type === "cart"`, "If the `type` returned is `cart`,\nit means an error occurred and the cart wasn't completed."],
["91", `data.type === "order"`, "If the `type` returned is `order`,\nit means the cart was completed and the order was placed successfully."],
["95", "refreshCart", "Unset and reset the cart."],
["105", "button", "This button triggers the `handlePayment` function when clicked."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
"use client"
import {
CardElement,
@@ -100,10 +103,11 @@ import {
useStripe,
} from "@stripe/react-stripe-js"
import { loadStripe } from "@stripe/stripe-js"
import { useCart } from "../../providers/cart"
import { useCart } from "@/providers/cart"
import { useState } from "react"
import { sdk } from "@/lib/sdk"
const stripePromise = loadStripe(
const stripe = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PK || "temp"
)
@@ -114,7 +118,7 @@ export default function StripePayment() {
return (
<div>
<Elements stripe={stripePromise} options={{
<Elements stripe={stripe} options={{
clientSecret,
}}>
<StripeForm clientSecret={clientSecret} />
@@ -175,25 +179,15 @@ const StripeForm = ({
return
}
fetch(
`http://localhost:9000/store/carts/${cart.id}/complete`,
{
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
method: "POST",
}
)
.then((res) => res.json())
.then(({ type, cart, order, error }) => {
if (type === "cart" && cart) {
sdk.store.cart.complete(cart.id)
.then((data) => {
if (data.type === "cart" && data.cart) {
// an error occured
console.error(error)
} else if (type === "order" && order) {
console.error(data.error)
} else if (data.type === "order" && data.order) {
// TODO redirect to order success page
alert("Order placed.")
console.log(order)
console.log(data.order)
refreshCart()
}
})
@@ -218,20 +212,20 @@ const StripeForm = ({
In the code snippet above, you:
1. Create a `StripePayment` component that wraps the actual form with Stripe's `Elements` component.
- In the `StripePayment` component, you obtain the client secret from the payment session's `data` field. This is set in the Medusa application.
- In the `StripePayment` component, you obtain the client secret from the payment session's `data` field. This is set in the Medusa application after you initialize the payment session using the [Initialize Payment Sessions API route](!api!/store#payment-collections_postpaymentcollectionsidpaymentsessions).
2. Create a `StripeForm` component that holds the actual form. In this component, you implement a `handlePayment` function that does the following:
- Use Stripe's `confirmCardPayment` method to accept the card details from the customer.
- Once the customer enters their card details and submit their order, the resolution function of the `confirmCardPayment` method is executed.
- In the resolution function, you send a request to the [Complete Cart API route](!api!/store#carts_postcartsidcomplete) to complete the cart and place the order.
- In the received response of the request, if the `type` is `cart`, it means that the cart completion failed. The error is set in the `error` response field.
- If the `type` is `order`, it means the card was completed and the order was placed successfully. You can access the order in the `order` response field.
- When the order is placed, you refresh the cart. You can redirect the customer to an order success page at this point.
- When the order is placed, you refresh the cart. You can redirect the customer to an order success page at this point. The redirection logic depends on the framework you're using.
---
## 4. Use the Stripe Component
You can now use the Stripe component in the checkout flow. You should render it after the customer chooses Stripe as a payment provider.
Finally, use the Stripe component in the checkout flow. You should render it after the customer chooses Stripe as a payment provider.
For example, you can use it in the `getPaymentUi` function defined in the [Payment Checkout Step guide](../page.mdx):

View File

@@ -13,152 +13,51 @@ export const metadata = {
# {metadata.title}
In the third step of the checkout flow, the customer chooses the shipping method to receive their order's items.
In this guide, you'll learn how to implement the third step of the checkout flow, where the customer chooses the shipping method to receive their order's items. While this is typically the third step of the checkout flow, you can change the steps of the checkout flow as you see fit.
To do that, you:
## Shipping Flow in Storefront Checkout
To allow the customer to choose a shipping method, you:
![Diagram showing the different steps of the shipping flow in storefront checkout](https://res.cloudinary.com/dza7lstvk/image/upload/v1743085465/Medusa%20Resources/shipping-checkout-flow_mfzdsh.jpg)
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. 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.
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.
---
## How to Implement the Shipping Flow in Storefront Checkout?
For example:
<Note title="Tip">
- This example uses the `useCart` hook defined in the [Cart React Context guide](../../cart/context/page.mdx).
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["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(
`http://localhost:9000/store/shipping-options?cart_id=${
cart.id
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
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 = (
selectedShippingOptionId: string
) => {
fetch(`http://localhost:9000/store/carts/${
cart.id
}/shipping-methods`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
option_id: selectedShippingOptionId,
data: {
// TODO add any data necessary for
// fulfillment provider
},
}),
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart...
console.log(cart)
})
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["4", "useCart", "The `useCart` hook was defined in the Cart React Context documentation."],
["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."]
["26", "listCartOptions", "Retrieve available shipping methods of the customer's cart."],
["42", "calculate", "Retrieve the price of every shipping method that has a calculated price."],
["44", "data", "Pass in this property any data relevant to the fulfillment provider."],
["72", "addShippingMethod", "Set the cart's shipping method using the selected shipping option."],
["74", "data", "Pass in this property any data relevant to the fulfillment provider."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useCallback, useEffect, useState } from "react"
import { useCart } from "../../../providers/cart"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
export default function CheckoutShippingStep() {
const { cart, setCart } = useCart()
@@ -178,15 +77,9 @@ export const highlights = [
if (!cart) {
return
}
fetch(`http://localhost:9000/store/shipping-options?cart_id=${
cart.id
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
sdk.store.fulfillment.listCartOptions({
cart_id: cart.id,
})
.then((res) => res.json())
.then(({ shipping_options }) => {
setShippingOptions(shipping_options)
})
@@ -200,21 +93,12 @@ export const highlights = [
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",
sdk.store.fulfillment.calculate(shippingOption.id, {
cart_id: cart.id,
data: {
// pass any data useful for calculation with third-party provider.
},
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) {
@@ -239,24 +123,13 @@ export const highlights = [
e.preventDefault()
setLoading(true)
fetch(`http://localhost:9000/store/carts/${
cart.id
}/shipping-methods`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
sdk.store.cart.addShippingMethod(cart.id, {
option_id: selectedShippingOption,
data: {
// TODO add any data necessary for
// fulfillment provider
},
body: JSON.stringify({
option_id: selectedShippingOption,
data: {
// TODO add any data necessary for
// fulfillment provider
},
}),
})
.then((res) => res.json())
.then(({ cart: updatedCart }) => {
setCart(updatedCart)
})
@@ -319,17 +192,109 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["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 sdk.store.fulfillment.listCartOptions({
cart_id: cartId,
})
shippingOptions = shipping_options
}
const calculateShippingOptionPrices = () => {
const promises = shippingOptions
.filter((shippingOption) => shippingOption.price_type === "calculated")
.map((shippingOption) =>
sdk.store.fulfillment.calculate(shippingOption.id, {
cart_id: cartId,
data: {
// pass any data useful for calculation with third-party provider.
},
})
)
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 = (
selectedShippingOptionId: string
) => {
sdk.store.cart.addShippingMethod(cartId, {
option_id: selectedShippingOptionId,
data: {
// TODO add any data necessary for
// fulfillment provider
},
})
.then(({ cart }) => {
// use cart...
console.log(cart)
})
}
```
</CodeTab>
</CodeTabs>
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.
- Retrieve the available shipping options of the cart to allow the customer to select from them using the [List Shipping Options API route](!api!/store#shipping-options_getshippingoptions).
- For each shipping option, you retrieve its calculated price from the Medusa application using the [Calculate Shipping Option Price API Route](!api!/store#shipping-options_postshippingoptionsidcalculate).
- Once the customer selects a shipping option, you send a request to the [Add Shipping Method to Cart API route](!api!/store#carts_postcartsidshippingmethods) to update the cart's shipping method using the selected shipping option.
## data Request Body Parameter
### data Request Body Parameter
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.
When calculating a shipping option's price using the [Calculate Shipping Option Price API Route](!api!/store#shipping-options_postshippingoptionsidcalculate), or when setting the shipping method using the [Add Shipping Method to Cart API route](!api!/store#carts_postcartsidshippingmethods), 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.
For example, you may pass a custom carrier code to the `data` parameter to identify the carrier of the shipping option if your fulfillment provider requires it.
This isn't implemented here as it's different for each provider. Refer to your fulfillment provider's documentation on details of expected data, if any.

View File

@@ -12,52 +12,29 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to manage a customer's addresses in a storefront.
In this guide, you'll learn how to manage a customer's addresses in a storefront. This is useful in the customer's profile page, or when the customer adds an address during checkout and you want to save it for future orders.
## List Customer Addresses
To retrieve the list of customer addresses, send a request to the [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddressesaddress_id):
<Note title="Tip">
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
- Since only authenticated customers can view their addresses, this example assumes that the JS SDK is already configured for authentication and the customer's authentication token was set as explained in the [Login in Storefront](../login/page.mdx) and [Third-Party Login](../third-party-login/page.mdx) guides.
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["2", "limit", "The number of addresses to retrieve"],
["3", "offset", "The number of addresses to skip before those retrieved."],
]
```ts highlights={fetchHighlights}
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
})
fetch(`http://localhost:9000/store/customers/me/addresses?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ addresses, count }) => {
// use addresses...
console.log(addresses, count)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["20", "offset", "Calculate the number of addresses to skip based on the current page and limit."],
["27", "fetch", "Send a request to retrieve the addresses."],
["28", "searchParams.toString()", "Pass the pagination parameters in the query."],
["36", "count", "The total number of addresses in the Medusa application."],
["48", "setHasMorePages", "Set whether there are more pages based on the total count."],
["62", "", "Using only two address fields for simplicity."],
["68", "button", "Show a button to load more addresses if there are more pages."]
["21", "offset", "Calculate the number of addresses to skip based on the current page and limit."],
["23", "listAddress", "Send a request to retrieve the addresses."],
["27", "count", "The total number of addresses in the Medusa application."],
["39", "setHasMorePages", "Set whether there are more pages based on the total count."],
["52", "", "Using only two address fields for simplicity."],
["59", "button", "Show a button to load more addresses if there are more pages."]
]
```tsx highlights={highlights} collapsibleLines="50-77" expandButtonLabel="Show render"
@@ -65,6 +42,7 @@ export const highlights = [
import { HttpTypes } from "@medusajs/types"
import { useEffect, useState } from "react"
import { sdk } from "@/lib/sdk"
export default function Addresses() {
const [addresses, setAddresses] = useState<
@@ -82,20 +60,10 @@ export const highlights = [
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
sdk.store.customer.listAddress({
limit,
offset,
})
fetch(`http://localhost:9000/store/customers/me/addresses?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ addresses: addressesData, count }) => {
setAddresses((prev) => {
if (prev.length > offset) {
@@ -143,10 +111,33 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["2", "limit", "The number of addresses to retrieve"],
["3", "offset", "The number of addresses to skip before those retrieved."],
]
```ts highlights={fetchHighlights}
sdk.store.customer.listAddress({
limit,
offset,
})
.then(({ addresses, count }) => {
// use addresses...
console.log(addresses, count)
})
```
</CodeTab>
</CodeTabs>
The [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddresses) accepts pagination parameters to paginate the address. It returns in the response the `addresses` field, which is an array of addresses.
The [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddresses) accepts pagination parameters to paginate the address.
The request returns in the response the `addresses` field, which is an array of addresses. You can check its structure in the [customer schema](!api!/store#customers_customer_schema).
The request also returns the `count` field, which is the total number of addresses in the Medusa application. You can use it to check if there are more addresses to retrieve.
---
@@ -154,53 +145,31 @@ The [List Customer Addresses API route](!api!/store#customers_getcustomersmeaddr
To add a new address for the customer, send a request to the [Add Customer Address API route](!api!/store#customers_postcustomersmeaddresses):
<Note title="Tip">
- This example uses the `useRegion` hook defined in the [Region Context guide](../../regions/context/page.mdx).
- This example uses the `useCustomer` hook defined in the [Customer Context guide](../context/page.mdx).
- Since only authenticated customers can add addresses, this example assumes that the JS SDK is already configured for authentication and the customer's authentication token was set as explained in the [Login in Storefront](../login/page.mdx) and [Third-Party Login](../third-party-login/page.mdx) guides.
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(`http://localhost:9000/store/customers/me/addresses`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
}),
})
.then((res) => res.json())
.then(({ customer }) => {
// use customer
console.log(customer)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const addHighlights = [
["4", "useRegion", "Use the hook defined in the Region Context guide."],
["5", "useCustomer", "Use the hook defined in the Customer Context guide."],
["28"], ["29"], ["30"], ["31"], ["32"], ["33"], ["34"], ["35"], ["36"], ["37"],
["38"], ["39"], ["40"], ["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"],
["48"], ["49"], ["50"]
["29"], ["30"], ["31"], ["32"], ["33"], ["34"], ["35"], ["36"], ["37"],
["38"], ["39"], ["40"], ["41"], ["42"], ["43"],
]
```tsx highlights={addHighlights} collapsibleLines="53-124" expandButtonLabel="Show form"
```tsx highlights={addHighlights} collapsibleLines="46-117" expandButtonLabel="Show form"
"use client" // include with Next.js 13+
import { useState } from "react"
import { useRegion } from "../../../../providers/region"
import { useCustomer } from "../../../../providers/customer"
import { useRegion } from "@/providers/region"
import { useCustomer } from "@/providers/customer"
import { sdk } from "@/lib/sdk"
export default function AddAddress() {
const { region } = useRegion()
@@ -223,26 +192,17 @@ export const addHighlights = [
e.preventDefault()
setLoading(false)
fetch(`http://localhost:9000/store/customers/me/addresses`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
}),
sdk.store.customer.createAddress({
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
})
.then((res) => res.json())
.then(({ customer }) => {
setCustomer(customer)
})
@@ -323,10 +283,33 @@ export const addHighlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
sdk.store.customer.createAddress({
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
})
.then(({ customer }) => {
// use customer
console.log(customer)
})
```
</CodeTab>
</CodeTabs>
The Add Address API route returns in the response a `customer` field, which is a [customer object](!api!/store#customers_customer_schema).
In this example, you send a request to the [Add Customer Address API route](!api!/store#customers_postcustomersmeaddresses) to add a new address for the customer.
The response of the request has a `customer` field, which is a [customer object](!api!/store#customers_customer_schema). You can access the new address in the `addresses` property of the customer object.
---
@@ -334,59 +317,32 @@ The Add Address API route returns in the response a `customer` field, which is a
To edit an address, send a request to the [Update Customer Address API route](!api!/store#customers_postcustomersmeaddressesaddress_id):
<Note title="Tip">
- This example uses the `useRegion` hook defined in the [Region Context guide](../../regions/context/page.mdx).
- This example uses the `useCustomer` hook defined in the [Customer Context guide](../context/page.mdx).
- Since only authenticated customers can edit their addresses, this example assumes that the JS SDK is already configured for authentication and the customer's authentication token was set as explained in the [Login in Storefront](../login/page.mdx) and [Third-Party Login](../third-party-login/page.mdx) guides.
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(
`http://localhost:9000/store/customers/me/addresses/${
address.id
}`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
}),
}
)
.then((res) => res.json())
.then(({ customer }) => {
// use customer...
console.log(customer)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const editHighlights = [
["4", "useRegion", "Use the hook defined in the Region Context guide."],
["5", "useCustomer", "Use the hook defined in the Customer Context guide."],
["17", "address", "Retrieve the address from the customer's `addresses` property."],
["58"], ["59"], ["60"], ["61"], ["62"], ["63"], ["64"], ["65"], ["66"], ["67"],
["68"], ["69"], ["70"], ["71"], ["72"], ["73"], ["74"], ["75"], ["76"], ["77"],
["78"], ["79"], ["80"]
["18", "address", "Retrieve the address from the customer's `addresses` property."],
["59"], ["60"], ["61"], ["62"], ["63"], ["64"], ["65"], ["66"], ["67"],
["68"], ["69"], ["70"], ["71"], ["72"], ["73"],
]
```tsx highlights={editHighlights} collapsibleLines="90-161" expandButtonLabel="Show form"
```tsx highlights={editHighlights} collapsibleLines="76-147" expandButtonLabel="Show form"
"use client" // include with Next.js 13+
import { useState } from "react"
import { useRegion } from "../../../../../providers/region"
import { useCustomer } from "../../../../../providers/customer"
import { useRegion } from "@/providers/region"
import { useCustomer } from "@/providers/customer"
import { sdk } from "@/lib/sdk"
type Props = {
id: string
@@ -439,31 +395,17 @@ export const editHighlights = [
}
setLoading(true)
fetch(
`http://localhost:9000/store/customers/me/addresses/${
address.id
}`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
}),
}
)
.then((res) => res.json())
sdk.store.customer.updateAddress(address.id, {
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
})
.then(({ customer }) => {
setCustomer(customer)
})
@@ -544,10 +486,33 @@ export const editHighlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
sdk.store.customer.updateAddress(address.id, {
first_name: firstName,
last_name: lastName,
address_1: address1,
company,
postal_code: postalCode,
city,
country_code: countryCode,
province,
phone: phoneNumber,
})
.then(({ customer }) => {
// use customer...
console.log(customer)
})
```
</CodeTab>
</CodeTabs>
The Update Address API route returns in the response a `customer` field, which is a [customer object](!api!/store#customers_customer_schema).
In this example, you send a request to the [Update Customer Address API route](!api!/store#customers_postcustomersmeaddressesaddress_id) to edit an address.
The response of the request has a `customer` field, which is a [customer object](!api!/store#customers_customer_schema). You can access the updated address in the `addresses` property of the customer object.
---
@@ -555,28 +520,25 @@ The Update Address API route returns in the response a `customer` field, which i
To delete a customer's address, send a request to the [Delete Customer Address API route](!api!/store#customers_deletecustomersmeaddressesaddress_id):
<Note title="Tip">
Since only authenticated customers can delete their addresses, this example assumes that the JS SDK is already configured for authentication and the customer's authentication token was set as explained in the [Login in Storefront](../login/page.mdx) and [Third-Party Login](../third-party-login/page.mdx) guides.
</Note>
export const deleteHighlights = [
["3", "addrId", "The ID of the address to delete."]
["1", "addrId", "The ID of the address to delete."],
["2", "parent", "The updated customer object."]
]
```ts highlights={deleteHighlights}
fetch(
`http://localhost:9000/store/customers/me/addresses/${
addrId
}`,
{
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
method: "DELETE",
}
)
.then((res) => res.json())
sdk.store.customer.deleteAddress(addrId)
.then(({ parent: customer }) => {
// use customer...
console.log(customer)
})
```
The Delete Customer Address API route returns a `parent` field in the response, which is a [customer object](!api!/store#customers_customer_schema).
In this example, you send a request to the [Delete Customer Address API route](!api!/store#customers_deletecustomersmeaddressesaddress_id) to delete an address.
The response of the request has a `parent` field, which is a [customer object](!api!/store#customers_customer_schema). You can access the updated customer in the `parent` property of the response.

View File

@@ -10,21 +10,33 @@ export const metadata = {
# {metadata.title}
Throughout your storefront, you'll need to access the logged-in customer to perform different actions, such as associate it with a cart.
In this guide, you'll learn how to create a customer context in your storefront.
So, if your storefront is React-based, create a customer context and add it at the top of your components tree. Then, you can access the logged-in customer anywhere in your storefront.
## Why Create a Customer Context?
Throughout your storefront, you'll need to access the logged-in customer to perform different actions, such as associating it with a cart.
So, if your storefront is React-based, you can create a customer context and add it at the top of your components tree. Then, you can access the logged-in customer anywhere in your storefront.
---
## Create Customer Context Provider
For example, create the following file that exports a `CustomerProvider` component and a `useCustomer` hook:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
export const highlights = [
["12", "customer", "Expose customer to children of the context provider."],
["13", "setCustomer", "Allow the context provider's\nchildren to change the logged-in customer."],
["24", "CustomerProvider", "The provider component to use in your component tree."],
["36", "fetch", "Try to retrieve the customer's details,\nif the customer is authentiated."],
["37", `credentials: "include"`, "Important to include this option for cookie session authentication.\nFor token authentication, pass the authorization header instead."],
["61", "useCustomer", "The hook that child components of the provider use to access the customer."]
["13", "customer", "Expose customer to children of the context provider."],
["14", "setCustomer", "Allow the context provider's\nchildren to change the logged-in customer."],
["25", "CustomerProvider", "The provider component to use in your component tree."],
["37", "retrieve", "Try to retrieve the customer's details,\nif the customer is authentiated."],
["56", "useCustomer", "The hook that child components of the provider use to access the customer."]
]
```tsx highlights={highlights}
@@ -37,6 +49,7 @@ import {
useState,
} from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type CustomerContextType = {
customer: HttpTypes.StoreCustomer | undefined
@@ -63,13 +76,7 @@ export const CustomerProvider = ({
return
}
fetch(`http://localhost:9000/store/customers/me`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.customer.retrieve()
.then(({ customer }) => {
setCustomer(customer)
})
@@ -99,7 +106,7 @@ export const useCustomer = () => {
}
```
The `CustomerProvider` handles retrieving the authenticated customer from the Medusa application.
The `CustomerProvider` handles retrieving the authenticated customer from the Medusa application. This assumes that the JS SDK is already configured for authentication and the customer's authentication token was set as explained in the [Login in Storefront](../login/page.mdx) and [Third-Party Login](../third-party-login/page.mdx) guides.
The `useCustomer` hook returns the value of the `CustomerContext`. Child components of `CustomerProvider` use this hook to access `customer` or `setCustomer`.
@@ -115,9 +122,9 @@ For example, if you're using Next.js, add it to the `app/layout.tsx` or `src/app
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { CartProvider } from "../providers/cart"
import { RegionProvider } from "../providers/region"
import { CustomerProvider } from "../providers/customer"
import { CartProvider } from "@/providers/cart"
import { RegionProvider } from "@/providers/region"
import { CustomerProvider } from "@/providers/customer"
const inter = Inter({ subsets: ["latin"] })
@@ -146,12 +153,9 @@ export default function RootLayout({
</html>
)
}
```
---
## Use useCustomer Hook
### Use useCustomer Hook
Now, you can use the `useCustomer` hook in child components of `CustomerProvider`.
@@ -160,7 +164,7 @@ For example:
```tsx
"use client" // include with Next.js 13+
// ...
import { useCustomer } from "../providers/customer"
import { useCustomer } from "@/providers/customer"
export default function Profile() {
const { customer } = useCustomer()

View File

@@ -13,83 +13,59 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to log-out a customer in the storefront based on the authentication method.
In this guide, you'll learn how to log-out a customer in the storefront based on the authentication method.
## Log-Out for JWT Token
## Log-out using the JS SDK
If you're authenticating the customer with their JWT token, remove the stored token from the browser.
If you're using the JS SDK, you can use the `auth.logout` method to log-out the customer:
For example, if you've stored the JWT token in the `localStorage`, remove the item from it:
```ts
sdk.auth.logout()
.then(() => {
// TODO redirect customer to login page
})
```
The JS SDK will handle the necessary actions based on the [authentication method](../login/page.mdx#js-sdk-authentication-configuration) you're using:
1. If you're using the `session` authentication method, the JS SDK will send a `DELETE` request to the `/auth/session` route. Then, it will remove any stored tokens from the configured storage method (by default, `localStorage`).
2. If you're using the `jwt` authentication method, the JS SDK will only remove the JWT token from the configured storage method (by default, `localStorage`).
Once the operation succeeds, you can redirect the customer to the login page.
---
## Log-out without the JS SDK
If you're not using the JS SDK, you need to log out the customer based on the authentication method you're using.
### Log-out for JWT Token
If you're authenticating the customer with their JWT token, remove the JWT token stored locally in your storefront based on your storage method.
For example, if you're storing the JWT token in `localStorage`, remove the item from it:
```ts
localStorage.removeItem(`token`)
```
---
Where `token` is the key of the JWT token in the `localStorage`.
## Log-Out for Cookie Session
### Log-out for Cookie Session
If you're authenticating the customer with their cookie session ID, send a `DELETE` request to the `/auth/session`.
If you're authenticating the customer with their cookie session ID, you need to send a `DELETE` request to the `/auth/session` route. This will remove the session cookie from the customer's browser.
For example:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(`http://localhost:9000/auth/session`, {
credentials: "include",
method: "DELETE",
})
.then((res) => res.json())
.then(() => {
// TODO redirect customer to login page
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["3", "useCustomer", "Use the hook defined in the Customer Context guide."],
["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"], ["17"], ["18"]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useCustomer } from "../../../providers/customer"
export default function Profile() {
const { setCustomer } = useCustomer()
const handleLogOut = () => {
fetch(`http://localhost:9000/auth/session`, {
credentials: "include",
method: "DELETE",
})
.then((res) => res.json())
.then(() => {
setCustomer(undefined)
// TODO redirect to login page
alert("Logged out")
})
}
return (
<div>
{/* Profile details... */}
<button
onClick={handleLogOut}
>
Log Out
</button>
</div>
)
}
```
</CodeTab>
</CodeTabs>
```ts
fetch(`http://localhost:9000/auth/session`, {
credentials: "include",
method: "DELETE",
})
.then((res) => res.json())
.then(() => {
// TODO redirect customer to login page
})
```
The API route returns nothing in the response. If the request was successful, you can perform any necessary work to unset the customer and redirect them to the login page.

View File

@@ -5,7 +5,7 @@ tags:
- auth
---
import { CodeTabs, CodeTab } from "docs-ui"
import { CodeTabs, CodeTab, Table } from "docs-ui"
export const metadata = {
title: `Login Customer in Storefront`,
@@ -13,74 +13,148 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn about the two ways to login a customer in a storefront.
In this guide, you'll learn about the two ways to login a customer in your storefront.
<Note>
This guide covers login using email and password. For authentication with third-party providers, refer to the [Third-Party Login](../third-party-login/page.mdx) guide.
</Note>
## Login Customer Methods
There are two ways to login a customer in your storefront:
1. [Using a JWT token](#1-using-a-jwt-token). This JWT token is obtained from the `/auth/customer/emailpass` API route and is used as a bearer token in the authorization header of all requests.
- When using the JS SDK, you can set the token using the `client.setToken` method. Then, the JS SDK will use that token in the authorization header of all subsequent requests.
2. [Using a cookie session](#2-using-a-cookie-session). This method uses the `/auth/session` API route to set the authenticated session ID in the cookies.
- When using the JS SDK, you can configure it to use sessions to manage authentication and pass the session ID cookie in all requests.
The next sections explain how to implement each method.
### Which Method Should You Use?
The authentication method you choose depends on your use case and the type of storefront you're building.
When making a choice, consider the following:
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>
Method
</Table.HeaderCell>
<Table.HeaderCell>
When to use
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
JWT token
</Table.Cell>
<Table.Cell>
- You need stateless authentication. For example, you're building a mobile storefront with React Native.
- Keep in mind: when logging out, you must clear the token in the JS SDK using the `client.clearToken` method. If the user still has access to the token, they can still send authenticated requests.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
Cookie session
</Table.Cell>
<Table.Cell>
- You need stateful authentication. For example, you're building a web storefront with Next.js.
- You want to ensure the session is revoked when the user logs out.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### JS SDK Authentication Configuration
Before implementing the login flow, you need to configure in the JS SDK the authentication method you're using in your storefront. This defines how the JS SDK will handle sending authenticated requests after the customer is authenticated.
<Note>
If you're not using the JS SDK, the next sections explain the general approach of how to pass the necessary authentication headers or cookies in your requests.
</Note>
For example, add the following configuration to your JS SDK initialization:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="authenticated-configuration">
<CodeTab label="JWT" value="jwt">
```ts
export const sdk = new Medusa({
// ...
auth: {
type: "jwt",
},
})
```
</CodeTab>
<CodeTab label="Session" value="session">
```ts
export const sdk = new Medusa({
// ...
auth: {
type: "session",
},
})
```
</CodeTab>
</CodeTabs>
The JS SDK will now pass the JWT token or the session ID cookie in the authorization header of all subsequent requests based on the authentication method you've configured.
<Note title="Tip">
By default, when you choose the `jwt` method, the JWT token is stored in the browser's `localStorage`. However, you can change how the token is stored, which is useful in environments where `localStorage` is not available. For example, in React Native.
To learn how to change the storage method with an example for a React Native storefront, refer to the [JS SDK documentation](../../../js-sdk/page.mdx#custom-storage-methods).
</Note>
---
## 1. Using a JWT Token
Using the `/auth/customer/emailpass` API route, you obtain a JSON Web Token (JWT) for the customer. Then, use that token as a bearer token in the authorization header of subsequent requests, and the customer is considered authenticated.
The first authentication approach is to pass an authenticated JWT token in the authorization header of all requests. You can do that by:
For example:
- Retrieving a JWT token from the `/auth/customer/emailpass` API route.
- Passing the token in the authorization header of all subsequent requests, as explained in the [API reference](!api!/store#1-bearer-authorization-with-jwt-tokens).
The JS SDK simplifies passing the JWT token by passing it in the authorization header of all subsequent requests if you configure the JS SDK to use JWT authentication.
After [configuring the JS SDK](#js-sdk-authentication-configuration) to use JWT authentication, you can now implement the following login flow:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["3", "fetch", "Send a request to obtain a JWT token."],
["21", "fetch", "Send a request as an authenticated customer."],
["27", "token", "Pass as a Bearer token in the authorization header."],
]
```ts highlights={fetchHighlights}
const handleLogin = async () => {
// obtain JWT token
const { token } = await fetch(
`http://localhost:9000/auth/customer/emailpass`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
// use token in the authorization header of
// all follow up requests. For example:
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
console.log(customer)
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["21", "fetch", "Send a request to obtain a JWT token."],
["39", "fetch", "Send a request as an authenticated customer."],
["45", "token", "Pass as a Bearer token in the authorization header."],
["24", "login", "Send a request to obtain a JWT token."],
["28", "catch", "If an error occurs, show an alert and exit execution."],
["33", "", "If the token is not a string, show an alert and exit execution."],
["40", "setToken", "Set the token in the JS SDK to pass it in the header of subsequent requests."],
["43", "retrieve", "Retrieve the customer's details as an example of testing authentication."],
]
```tsx highlights={highlights} collapsibleLines="55-79" expandButtonLabel="Show form"
"use client" // include with Next.js 13+
import { useState } from "react"
import { sdk } from "@/lib/sdk"
export default function Login() {
const [loading, setLoading] = useState(false)
@@ -97,37 +171,29 @@ export const highlights = [
setLoading(true)
// obtain JWT token
const { token } = await fetch(
`http://localhost:9000/auth/customer/emailpass`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
let token: string | { location: string }
try {
token = await sdk.auth.login("customer", "emailpass", {
email,
password,
})
} catch (error) {
alert(`An error occured while logging in: ${error}`)
return
}
if (typeof token !== "string") {
alert("Authentication requires more actions, which isn't supported by this flow.")
return
}
// use token in the authorization header of
// all follow up requests. For example:
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
// all follow up requests.
sdk.client.setToken(token)
// the next request will be authenticated
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
setLoading(false)
@@ -160,6 +226,47 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["5", "login", "Send a request to obtain a JWT token."],
["9", "catch", "If an error occurs, show an alert and exit execution."],
["14", "", "If the token is not a string, show an alert and exit execution."],
["21", "setToken", "Set the token in the JS SDK to pass it in the header of subsequent requests."],
["24", "retrieve", "Retrieve the customer's details as an example of testing authentication."],
]
```ts highlights={fetchHighlights}
const handleLogin = async () => {
let token: string | { location: string }
try {
token = await sdk.auth.login("customer", "emailpass", {
email,
password,
})
} catch (error) {
alert(`An error occured while logging in: ${error}`)
return
}
if (typeof token !== "string") {
alert("Authentication requires more actions, which isn't supported by this flow.")
return
}
// use token in the authorization header of
// all follow up requests.
sdk.client.setToken(token)
// the next request will be authenticated
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
}
```
</CodeTab>
</CodeTabs>
@@ -167,94 +274,42 @@ In the example above, you:
1. Create a `handleLogin` function that logs in a customer.
2. In the function, you obtain a JWT token by sending a request to the `/auth/customer/emailpass`.
3. You can then use that token in the authorization header of subsequent requests, and the customer is considered authenticated. As an example, you send a request to obtain the customer's details.
- If an error occurs, show an alert and exit execution.
- The request may return an object with a `location` property. This occurs when using third-party authentication providers. Learn more about implementing third-party authentication in the [Third-Party Login](../third-party-login/page.mdx) guide.
3. To use the token in the authorization header of subsequent requests, you must set the token in the JS SDK using the `client.setToken` method.
4. All subsequent requests are now authenticated. As an example, you send a request to obtain the logged-in customer's details.
---
## 2. Using a Cookie Session
Authenticating the customer with a cookie session means the customer is authenticated in subsequent requests that use that cookie.
The second authentication approach is to authenticate the customer with a cookie session. You do that by:
If you're using the Fetch API, using the `credentials: include` option ensures that your cookie session is passed in every request.
- Retrieving a JWT token from the `/auth/customer/emailpass` API route.
- Sending a request to the `/auth/session` API route passing in the authorization header the token as a Bearer token. This sets the authenticated session ID in the cookies.
For example:
Then, you must ensure that all subsequent requests include the session ID cookie.
The JS SDK simplifies this by passing the session ID cookie in all requests if you configure the JS SDK to use sessions.
After [configuring the JS SDK](#js-sdk-authentication-configuration) to use sessions, you can now implement the following login flow:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchSessionHighlights = [
["3", "fetch", "Send a request to obtain a JWT token."],
["20", "fetch", "Send a request to set the authenticated session ID in the cookies."],
["27", "token", "Pass as a Bearer token in the authorization header."],
["35", "fetch", "Retrieve the customer's details as an example of testing authentication."],
]
```ts highlights={fetchSessionHighlights}
const handleLogin = async () => {
// obtain JWT token
const { token } = await fetch(
`http://localhost:9000/auth/customer/emailpass`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
// set session
await fetch(
`http://localhost:9000/auth/session`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
}
)
.then((res) => res.json())
// customer is now authenticated using the
// cookie session. For example
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
console.log(customer)
setLoading(false)
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const sessionHighlights = [
["21", "fetch", "Send a request to obtain a JWT token."],
["38", "fetch", "Send a request to set the authenticated session ID in the cookies."],
["45", "token", "Pass as a Bearer token in the authorization header."],
["53", "fetch", "Retrieve the customer's details as an example of testing authentication."],
["24", "login", "Send a request to obtain a JWT token."],
["28", "catch", "If an error occurs, show an alert and exit execution."],
["33", "", "If the token is not a string, show an alert and exit execution."],
["39", "setToken", "Set the token in the JS SDK, which will retrieve and pass the session ID cookie in all subsequent requests."],
["43", "retrieve", "Retrieve the customer's details as an example of testing authentication."],
]
```tsx highlights={sessionHighlights} collapsibleLines="68-92" expandButtonLabel="Show form"
"use client" // include with Next.js 13+
import { useState } from "react"
import { sdk } from "@/lib/sdk"
export default function Login() {
const [loading, setLoading] = useState(false)
@@ -271,50 +326,29 @@ export const sessionHighlights = [
setLoading(true)
// obtain JWT token
const { token } = await fetch(
`http://localhost:9000/auth/customer/emailpass`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
let token: string | { location: string }
try {
token = await sdk.auth.login("customer", "emailpass", {
email,
password,
})
} catch (error) {
alert(`An error occured while logging in: ${error}`)
return
}
if (typeof token !== "string") {
alert("Authentication requires more actions, which isn't supported by this flow.")
return
}
// set session
await fetch(
`http://localhost:9000/auth/session`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
}
)
.then((res) => res.json())
sdk.client.setToken(token)
// customer is now authenticated using the
// cookie session. For example
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
setLoading(false)
@@ -347,12 +381,56 @@ export const sessionHighlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchSessionHighlights = [
["5", "login", "Send a request to obtain a JWT token."],
["9", "catch", "If an error occurs, show an alert and exit execution."],
["14", "", "If the token is not a string, show an alert and exit execution."],
["20", "setToken", "Set the token in the JS SDK, which will retrieve and pass the session ID cookie in all subsequent requests."],
["24", "retrieve", "Retrieve the customer's details as an example of testing authentication."],
]
```ts highlights={fetchSessionHighlights}
const handleLogin = async () => {
let token: string | { location: string }
try {
token = await sdk.auth.login("customer", "emailpass", {
email,
password,
})
} catch (error) {
alert(`An error occured while logging in: ${error}`)
return
}
if (typeof token !== "string") {
alert("Authentication requires more actions, which isn't supported by this flow.")
return
}
// set session
sdk.client.setToken(token)
// customer is now authenticated using the
// cookie session. For example
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
}
```
</CodeTab>
</CodeTabs>
In the example above, you:
1. Create a `handleLogin` function that logs in a customer.
2. In the function, you obtain a JWT token by sending a request to the `/auth/customer/emailpass`.
3. You send a request to the `/auth/session` API route passing in the authorization header the token as a Bearer token. This sets the authenticated session ID in the cookies.
4. You can now send authenticated requests, as long as you include the `credentials: include` option in your fetch requests. For example, you send a request to retrieve the customer's details.
2. In the function, you obtain a JWT token by sending a request to the `/auth/customer/emailpass` API route.
- If an error occurs, show an alert and exit execution.
- The request may return an object with a `location` property. This occurs when using third-party authentication providers. Learn more about implementing third-party authentication in the [Third-Party Login](../third-party-login/page.mdx) guide.
3. To obtain the session ID cookie and ensure it's included in all subsequent requests, you must use the JS SDK's `client.setToken` method, passing it the JWT token.
- Because you set the JS SDK's authentication type to `session`, the `client.setToken` method will send a request to the `/auth/session` API route passing in the authorization header the token as a Bearer token. This sets the authenticated session ID in the cookies, which are then included in all subsequent requests.
4. All subsequent requests are now authenticated. As an example, you send a request to obtain the logged-in customer's details.

View File

@@ -12,52 +12,38 @@ export const metadata = {
# {metadata.title}
In this guide, you'll learn how to edit the customer's profile in the storefront, which is useful if you're adding a profile page to your storefront that allows the customer to view and edit their details.
To edit the customer's profile in the storefront, send a request to the [Update Customer API route](!api!/store#customers_postcustomersme).
For example:
<Note title="Tip">
- Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
- This example uses the `useCustomer` hook defined in the [Customer Context guide](../context/page.mdx).
- Since only authenticated customers can edit their profile, this example assumes that the JS SDK is already configured for authentication and the customer's authentication token was set as explained in the [Login in Storefront](../login/page.mdx) and [Third-Party Login](../third-party-login/page.mdx) guides.
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(`http://localhost:9000/store/customers/me`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name,
last_name,
company_name,
phone,
}),
})
.then((res) => res.json())
.then(({ customer }) => {
// use customer...
console.log(customer)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["4", "useCustomer", "Use the hook defined in the Customer Context guide."],
["33"], ["34"], ["35"], ["36"], ["37"], ["38"], ["39"], ["40"], ["41"], ["42"],
["43"], ["44"], ["45"], ["46"], ["47"], ["48"], ["49"], ["50"]
["35"], ["36"], ["37"], ["38"], ["39"], ["40"], ["41"], ["42"], ["43"], ["44"],
]
```tsx highlights={highlights} collapsibleLines="53-91" expandButtonLabel="Show form"
```tsx highlights={highlights} collapsibleLines="47-85" expandButtonLabel="Show form"
"use client" // include with Next.js 13+
import { useState } from "react"
import { useCustomer } from "../../../providers/customer"
import { useCustomer } from "@/providers/customer"
import { sdk } from "@/lib/sdk"
export default function EditProfile() {
const { customer, setCustomer } = useCustomer()
console.log(customer)
const [firstName, setFirstName] = useState(
customer?.first_name || ""
)
@@ -83,21 +69,12 @@ export const highlights = [
setLoading(true)
fetch(`http://localhost:9000/store/customers/me`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
company_name: company,
phone,
}),
sdk.store.customer.update({
first_name: firstName,
last_name: lastName,
company_name: company,
phone,
})
.then((res) => res.json())
.then(({ customer: updatedCustomer }) => {
setCustomer(updatedCustomer)
})
@@ -145,9 +122,25 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
sdk.store.customer.update({
first_name: firstName,
last_name: lastName,
company_name: company,
phone,
})
.then(({ customer }) => {
// use customer...
console.log(customer)
})
```
</CodeTab>
</CodeTabs>
In the example above, you send a request to the Update Customer API route to update the customer's details.
In the example above, you send a request to the [Update Customer API route](!api!/store#customers_postcustomersme) to update the customer's details.
The response of the request has a `customer` field which is a [customer object](!api!/store#customers_customer_schema).

View File

@@ -13,135 +13,60 @@ export const metadata = {
# {metadata.title}
In this guide, you'll learn how to register a customer in your storefront.
This guide covers registration using email and password. For authentication with third-party providers, refer to the [Third-Party Login](../third-party-login/page.mdx) guide.
## Register Customer Flow
To register a customer, you implement the following steps:
![A diagram illustrating the flow of the register customer flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1743086184/Medusa%20Resources/register-flow_yv5uw2.jpg)
1. Show the customer a form to enter their details.
2. Send a `POST` request to the `/auth/customer/emailpass/register` API route to obtain a JWT token.
3. Send a request to the [Create Customer API route](!api!/store#customers_postcustomers) pass the JWT token in the header.
2. Send a `POST` request to the `/auth/customer/emailpass/register` API route to obtain a registration JWT token.
3. Send a request to the [Create Customer API route](!api!/store#customers_postcustomers) passing the registration JWT token in the header.
However, a customer may enter an email that's already used either by an admin user, another customer, or a [custom actor type](../../../commerce-modules/auth/auth-identity-and-actor-types/page.mdx). To handle this scenario:
- Try to obtain a login token by sending a `POST` request to the `/auth/customer/emailpass` API route. The customer is only allowed to register if their email and password match the existing identity. This allows admin users to log in or register as customers.
- If you obtained the login token successfully, create the customer using the login JWT token instead of the registration token. This will not remove the existing identity. So, for example, an admin user can also become a customer.
---
## How to Implement the Register Customer Flow
An example implemetation of the registration flow in a storefront:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["3", "fetch", "Send a request to obtain a registration JWT token."],
["20", "", "Another identity exists with the same email."],
["25", "fetch", "Try to obtain a login JWT token."],
["41", "", "The existing account belongs to another customer, so authentication failed."],
["50", "token", "Set the token to either the registration or login JWT token"],
["53", "fetch", "Send a request to create the customer."],
["60", "token", "Pass as a Bearer token in the authorization header."],
["72", "", "Handle registration failure"],
["77", "TODO", "Redirect the customer to the log in page."]
]
```ts highlights={fetchHighlights}
const handleRegistration = async () => {
// obtain JWT token
let registerResponse = await fetch(
`http://localhost:9000/auth/customer/emailpass/register`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
if (!registerResponse.token) {
if (registerResponse.type === "unauthorized" && registerResponse.message === "Identity with email already exists") {
// another identity (for example, admin user)
// exists with the same email. It can also be another customer
// with the same email. In that case, obtaining the login token
// will fail due to incorrect password.
registerResponse = await fetch(
`http://localhost:9000/auth/customer/emailpass`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
if (!registerResponse.token) {
alert("Error: " + registerResponse.message)
return
}
} else {
alert("Error: " + registerResponse.message)
}
}
const token = registerResponse.token as string
// create customer
const customerResponse = await fetch(
`http://localhost:9000/store/customers`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
email,
}),
}
)
.then((res) => res.json())
if (!customerResponse.customer) {
alert("Error: " + customerResponse.message)
return
}
console.log(customerResponse.customer)
// TODO redirect to login page
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["22", "fetch", "Send a request to obtain a registration JWT token."],
["39", "", "Another identity exists with the same email."],
["44", "fetch", "Try to obtain a login JWT token."],
["60", "", "The existing account belongs to another customer, so authentication failed."],
["69", "token", "Set the token to either the registration or login JWT token"],
["72", "fetch", "Send a request to create the customer."],
["79", "token", "Pass as a Bearer token in the authorization header."],
["93", "", "Handle registration failure"],
["98", "TODO", "Redirect the customer to the log in page."]
["26", "register", "Send a request to obtain a registration JWT token."],
["30", "catch", "Maybe another identity exists with the same email."],
["33", "", "If an unexpected error occurs, exit the flow."],
["40", "login", "Try to obtain a login JWT token."],
["43", "catch", "The existing account belongs to another customer, so authentication failed."],
["56", "registrationToken", "Set the token to the login JWT token"],
["59", "setToken", "Set the token in the JS SDK to pass it in the header of subsequent requests."],
["63", "create", "Send a request to create the customer."],
["72", "clearToken", "Clear the token in the JS SDK so that it's not used in subsequent requests."],
["75", "TODO", "Redirect the customer to the log in page."],
["76", "catch", "Handle registration failure"],
]
```tsx highlights={highlights} collapsibleLines="101-140" expandButtonLabel="Show form"
```tsx highlights={highlights} collapsibleLines="83-121" expandButtonLabel="Show form"
"use client" // include with Next.js 13+
import { useState } from "react"
import { sdk } from "@/lib/sdk"
import { FetchError } from "@medusajs/js-sdk"
export default function Register() {
const [loading, setLoading] = useState(false)
@@ -159,85 +84,64 @@ export const highlights = [
}
setLoading(true)
// obtain JWT token
let registerResponse = await fetch(
`http://localhost:9000/auth/customer/emailpass/register`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
let registrationToken = ""
if (!registerResponse.token) {
if (registerResponse.type === "unauthorized" && registerResponse.message === "Identity with email already exists") {
try {
registrationToken = await sdk.auth.register("customer", "emailpass", {
email,
password,
})
} catch (error) {
const fetchError = error as FetchError
if (fetchError.statusText !== "Unauthorized" || fetchError.message !== "Identity with email already exists") {
alert(`An error occured while creating account: ${fetchError}`)
return
}
// another identity (for example, admin user)
// exists with the same email. It can also be another customer
// with the same email. In that case, obtaining the login token
// will fail due to incorrect password.
registerResponse = await fetch(
`http://localhost:9000/auth/customer/emailpass`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
)
.then((res) => res.json())
// exists with the same email. So, use the auth
// flow to login and create a customer.
const loginResponse = (await sdk.auth.login("customer", "emailpass", {
email,
password,
}).catch((e) => {
alert(`An error occured while creating account: ${e}`)
}))
if (!registerResponse.token) {
alert("Error: " + registerResponse.message)
return
}
} else {
alert("Error: " + registerResponse.message)
if (!loginResponse) {
return
}
if (typeof loginResponse !== "string") {
alert("Authentication requires more actions, which isn't supported by this flow.")
return
}
registrationToken = loginResponse
}
const token = registerResponse.token as string
sdk.client.setToken(registrationToken)
// create customer
const customerResponse = await fetch(
`http://localhost:9000/store/customers`,
{
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
email,
}),
}
)
.then((res) => res.json())
try {
const { customer } = await sdk.store.customer.create({
first_name: firstName,
last_name: lastName,
email,
})
setLoading(false)
setLoading(false)
// clear the token
sdk.client.clearToken()
if (!customerResponse.customer) {
alert("Error: " + customerResponse.message)
console.log(customer)
// TODO redirect to login page
} catch (error) {
console.error(error)
alert("Error: " + error)
return
}
console.log(customerResponse.customer)
// TODO redirect to login page
}
return (
@@ -281,15 +185,102 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["10", "register", "Send a request to obtain a registration JWT token."],
["14", "catch", "Maybe another identity exists with the same email."],
["24", "login", "Try to obtain a login JWT token."],
["27", "catch", "The existing account belongs to another customer, so authentication failed."],
["40", "registrationToken", "Set the token to the login JWT token"],
["43", "setToken", "Set the token in the JS SDK to pass it in the header of subsequent requests."],
["47", "create", "Send a request to create the customer."],
["54", "clearToken", "Clear the token in the JS SDK so that it's not used in subsequent requests."],
["57", "TODO", "Redirect the customer to the log in page."],
["58", "catch", "Handle registration failure"],
]
```ts highlights={fetchHighlights}
// other imports...
import { FetchError } from "@medusajs/js-sdk"
const handleRegistration = async () => {
let registrationToken = ""
// obtain registration JWT token
try {
registrationToken = await sdk.auth.register("customer", "emailpass", {
email,
password,
})
} catch (error) {
const fetchError = error as FetchError
if (fetchError.statusText !== "Unauthorized" || fetchError.message !== "Identity with email already exists") {
alert(`An error occured while creating account: ${fetchError}`)
return
}
// another identity (for example, admin user)
// exists with the same email. So, use the auth
// flow to login and create a customer.
const loginResponse = (await sdk.auth.login("customer", "emailpass", {
email,
password,
}).catch((e) => {
alert(`An error occured while creating account: ${e}`)
}))
if (!loginResponse) {
return
}
if (typeof loginResponse !== "string") {
alert("Authentication requires more actions, which isn't supported by this flow.")
return
}
registrationToken = loginResponse
}
sdk.client.setToken(registrationToken)
// create customer
try {
const { customer } = await sdk.store.customer.create({
first_name: firstName,
last_name: lastName,
email,
})
// clear the token
sdk.client.clearToken()
console.log(customer)
// TODO redirect to login page
} catch (error) {
console.error(error)
alert("Error: " + error)
return
}
}
```
</CodeTab>
</CodeTabs>
In the above example, you create a `handleRegistration` function that:
- Obtains a JWT token from the `/auth/customer/emailpass/register` API route.
- If an error is returned instead of a token:
- Obtains a registration JWT token from the `/auth/customer/emailpass/register` API route.
- If an error is thrown:
- If the error is an existing identity error, try retrieving the login JWT token from `/auth/customer/emailpass` API route. This will fail if the existing identity has a different password, which doesn't allow the customer from registering.
- For other errors, show an alert and exit execution.
- Send a request to the Create Customer API route, and pass the registration or login JWT token as a Bearer token in the authorization header.
- If an error occurs, show an alert and exit execution.
- Once the customer is registered successfully, you can either redirect the customer to the login page or log them in automatically as explained in this guide.
- In the JS SDK, set the registration or login token using the `client.setToken` method. Then, all subsequent requests will use that token in the request header.
- If you're not using the JS SDK, you must pass manually pass the registration or login JWT token as a Bearer token in the authorization header of the next request.
- Send a request to the [Create Customer API route](!api!/store#customers_postcustomers) to create the customer in Medusa.
- If an error occurs, show an alert and exit execution.
- Once the customer is registered successfully, you can either redirect the customer to the login page or log them in automatically.
- Make sure to clear the token in the JS SDK using the `client.clearToken` method so that it's not used in subsequent requests.
Refer to the [Login guide](../login/page.mdx) to learn how to log in the customer manually or automatically.

View File

@@ -13,77 +13,54 @@ export const metadata = {
# {metadata.title}
Customers reset their password if they forget it.
In this guide, you'll learn how to implement the flow to reset a customer's password in your storefront.
To implement the flow to reset a customer's password, you need two pages in your storefront:
## Reset Password Flow in Storefront
1. A page to request the password reset.
2. A page that prompts the customer to enter a new password.
Customers need to reset their password if they forget it. To implement the flow to reset a customer's password, you need two pages in your storefront:
---
## 1. Request Reset Password Page
The request password reset page prompts the customer to enter their email. Then, it sends a request to the [Request Reset Password Token API route](!api!/store#auth_postactor_typeauth_providerresetpassword) to send the customer an email with the URL to reset their password.
1. [Request Reset Password Page](#1-request-reset-password-page): A page to request the password reset.
- When the customer requests to reset their password, they would receive an email (or other notification) with a URL to the Reset Password Page.
2. [Reset Password Page](#2-reset-password-page): A page that prompts the customer to enter a new password.
<Prerequisites
items={[
{
text: "While it's not required, it's recommended to implement the subscriber that sends the customer an email with the URL to reset their password.",
text: "To send the customer an email (or other notification) with the URL to reset their password, you must implement the subscriber that handles the notification.",
link: "/commerce-modules/auth/reset-password"
}
]}
/>
For example:
---
## 1. Request Reset Password Page
The request password reset page prompts the customer to enter their email. Then, it sends a request to the [Request Reset Password Token API route](!api!/store#auth_postactor_typeauth_providerresetpassword) to request resetting the password.
This API route will then handle sending an email or another type of notification, if you handle it as explained in the [Reset Password Guide](../../../commerce-modules/auth/reset-password/page.mdx).
For example, you can implement the following functionality in your storefront to request resetting the password:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["5", "email", "Assuming the email is retrieved from an input field."],
["10", "fetch", "Send a request to send the token to the customer."],
["17", "identifier", "Pass the email in the `identifier` request body parameter."]
]
```ts highlights={fetchHighlights}
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement> // or other form event
) => {
e.preventDefault()
if (!email) {
alert("Email is required")
return
}
fetch(`http://localhost:9000/auth/customer/emailpass/reset-password`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: email,
}),
})
.then(() => {
alert("If an account exists with the specified email, it'll receive instructions to reset the password.")
})
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["19", "fetch", "Send a request to send the token to the customer."],
["26", "identifier", "Pass the email in the `identifier` request body parameter."]
["20", "resetPassword", "Request resetting the password."],
["21", "identifier", "Pass the email in the `identifier` request body parameter."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useState } from "react"
import { sdk } from "@/lib/sdk"
export default function RequestResetPassword() {
const [loading, setLoading] = useState(false)
@@ -99,18 +76,16 @@ export const highlights = [
}
setLoading(true)
fetch(`http://localhost:9000/auth/customer/emailpass/reset-password`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: email,
}),
sdk.auth.resetPassword("customer", "emailpass", {
identifier: email,
})
.then(() => {
alert("If an account exists with the specified email, it'll receive instructions to reset the password.")
})
.catch((error) => {
alert(error.message)
})
.finally(() => {
setLoading(false)
})
}
@@ -132,16 +107,47 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["5", "email", "Assuming the email is retrieved from an input field."],
["10", "resetPassword", "Request resetting the password."],
["11", "identifier", "Pass the email in the `identifier` request body parameter."]
]
```ts highlights={fetchHighlights}
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement> // or other form event
) => {
e.preventDefault()
if (!email) {
alert("Email is required")
return
}
sdk.auth.resetPassword("customer", "emailpass", {
identifier: email,
})
.then(() => {
alert("If an account exists with the specified email, it'll receive instructions to reset the password.")
})
.catch((error) => {
alert(error.message)
})
}
```
</CodeTab>
</CodeTabs>
In this example, you send a request to `http://localhost:9000/auth/customer/emailpass/reset-password` API route when the form that has the email field is submitted.
In this example, you send a request to the [Request Reset Password Token API route](!api!/store#auth_postactor_typeauth_providerresetpassword) when the form that has the email field is submitted.
In the request body, you pass an `identifier` parameter, which is the customer's email.
<Note title="Tip">
The Request Reset Password Token API route returns a successful response always, even if the customer's email doesn't exist. However, the customer only receives an email if they have an account with that email.
The [Request Reset Password Token API route](!api!/store#auth_postactor_typeauth_providerresetpassword) returns a successful response always, even if the customer's email doesn't exist. This ensures that customer emails that don't exist are not exposed.
</Note>
@@ -149,7 +155,11 @@ The Request Reset Password Token API route returns a successful response always,
## 2. Reset Password Page
The reset password page is the URL used in the email sent to the customer. It receives a `token` and `email` query parameters, prompts the customer for a new password, and sends a request to the [Reset Password API route](!api!/store#auth_postactor_typeauth_providerupdate).
Once the customer requests to reset their password, you should handle sending them a notification, such as an email, as explained in the [Reset Password Guide](../../../commerce-modules/auth/reset-password/page.mdx).
The notification should include a URL in your storefront that allows the customer to update their password. In this step, you'll implement this page.
The reset password page should receive a `token` and `email` query parameters. Then, it prompts the customer for a new password, and sends a request to the [Reset Password API route](!api!/store#auth_postactor_typeauth_providerupdate) to update the password.
<Note>
@@ -160,65 +170,20 @@ If you followed [this guide](../../../commerce-modules/auth/reset-password/page.
For example:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const resetPasswordFetchHighlights = [
["2", "token", "Receive the token from a query parameter."],
["3", "email", "Receive the email from a query parameter."],
["9", "password", "Assuming the password is retrieved from an input field."],
["14", "fetch", "Send a request to update the customer's password."],
["19", "token", "Pass the token in the Authorization header."],
["21", "body", "Pass the email and password in the request body."]
]
```ts highlights={resetPasswordFetchHighlights}
const queryParams = new URLSearchParams(window.location.search)
const token = queryParams.get("token")
const email = queryParams.get("email")
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault()
if (!password) {
alert("Password is required")
return
}
fetch(`http://localhost:9000/auth/customer/emailpass/update`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(({ success }) => {
alert(success ? "Password reset successfully!" : "Couldn't reset password")
})
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const resetPasswordHighlights = [
["18", "token", "Receive the token from a query parameter."],
["21", "email", "Receive the email from a query parameter."],
["35", "fetch", "Send a request to update the customer's password."],
["40", "token", "Pass the token in the Authorization header."],
["42", "body", "Pass the email and password in the request body."]
["19", "token", "Receive the token from a query parameter."],
["22", "email", "Receive the email from a query parameter."],
["39", "updateProvider", "Send a request to update the customer's password."],
["41", "password", "Pass the new password in the request body."]
]
```tsx highlights={resetPasswordHighlights}
"use client" // include with Next.js 13+
import { useMemo, useState } from "react"
import { sdk } from "@/lib/sdk"
export default function ResetPassword() {
const [loading, setLoading] = useState(false)
@@ -244,27 +209,26 @@ export const resetPasswordHighlights = [
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault()
if (!token) {
return
}
if (!password) {
alert("Password is required")
return
}
setLoading(true)
fetch(`http://localhost:9000/auth/customer/emailpass/update`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
email,
password,
}),
sdk.auth.updateProvider("customer", "emailpass", {
email,
password,
}, token)
.then(() => {
alert("Password reset successfully!")
})
.then((res) => res.json())
.then(({ success }) => {
alert(success ? "Password reset successfully!" : "Couldn't reset password")
.catch((error) => {
alert(`Couldn't reset password: ${error.message}`)
})
.finally(() => {
setLoading(false)
})
}
@@ -286,15 +250,57 @@ export const resetPasswordHighlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const resetPasswordFetchHighlights = [
["2", "token", "Receive the token from a query parameter."],
["3", "email", "Receive the email from a query parameter."],
["17", "updateProvider", "Send a request to update the customer's password."],
["19", "password", "Pass the new password in the request body."]
]
```ts highlights={resetPasswordFetchHighlights}
const queryParams = new URLSearchParams(window.location.search)
const token = queryParams.get("token")
const email = queryParams.get("email")
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault()
if (!token) {
return
}
if (!password) {
alert("Password is required")
return
}
sdk.auth.updateProvider("customer", "emailpass", {
email,
password,
}, token)
.then(() => {
alert("Password reset successfully!")
})
.catch((error) => {
alert(`Couldn't reset password: ${error.message}`)
})
}
```
</CodeTab>
</CodeTabs>
In this example, you receive the `token` and `email` from the page's query parameters.
Then, when the form that has the password field is submitted, you send a request to the `http://localhost:9000/auth/customer/emailpass/update` API route. You pass it the token as in the Authorization header as a bearer token, and the email and password in the request body.
Then, when the form that has the password field is submitted, you send a request to the [Reset Password API route](!api!/store#auth_postactor_typeauth_providerupdate), passing it the token, email, and new password.
Notice that the JS SDK passes the token in the `Authorization Bearer` header. So, if you're implementing this flow without using the JS SDK, make sure to pass the token accordingly.
<Note>
Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header.
Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization Bearer` header.
</Note>

View File

@@ -13,63 +13,68 @@ export const metadata = {
# {metadata.title}
To retrieve a customer after it's been authenticated in your storefront, send a request to the [Get Customer API route](!api!/store#customers_getcustomersme):
In this guide, you'll learn how to retrieve a customer after they've been authenticated in your storefront.
<CodeTabs group="authenticated-request">
<CodeTab label="Using Bearer Token" value="bearer">
## Prerequisites: Set the Customer's Authentication Token
export const bearerHighlights = [
["7", "", "Pass JWT token as bearer token in authorization header."],
]
When using the [JS SDK](../../../js-sdk/page.mdx), make sure that you've set the customer's authentication token using the `setToken` function:
```ts highlights={bearerHighlights}
fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
.then(({ customer }) => {
// use customer...
console.log(customer)
})
```
```ts
sdk.client.setToken(token)
```
</CodeTab>
<CodeTab label="Using Cookie Session" value="session">
You can learn more in the [Login Customer](../login/page.mdx) and [Third-Party Login](../third-party-login/page.mdx) guides.
export const sessionHighlights = [
["4", "", "Pass this option to ensure the cookie session is passed in the request."],
]
---
```ts highlights={sessionHighlights}
fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
.then(({ customer }) => {
// use customer...
console.log(customer)
})
```
## Retrieve Logged-In Customer
</CodeTab>
</CodeTabs>
To retrieve the logged-in customer, send a request to the [Get Customer API route](!api!/store#customers_getcustomersme):
```ts
sdk.store.customer.retrieve()
.then(({ customer }) => {
// use customer...
console.log(customer)
})
```
This will retrieve the authenticated customer's details. The [Get Customer API route](!api!/store#customers_getcustomersme) returns a `customer` field, which is a [customer object](!api!/store#customers_customer_schema).
Notice that the JS SDK automatically passes the necessary authentication headers or cookies based on your authentication configurations, as explained in the [Login Customer](../login/page.mdx) guide.
If you're not using the JS SDK, you need to pass the authentication token in the request headers or cookies accordingly:
- If you authenticate the customer with bearer authorization, pass the token in the authorization header of the request.
- If you authenticate the customer with cookie session, pass the `credentials: include` option to the `fetch` function.
The Get Customer API route returns a `customer` field, which is a [customer object](!api!/store#customers_customer_schema).
---
## Restrict Access to Authenticated Customers
In your storefront, it's common to restrict access to certain pages to authenticated customers only.
For example, you may want to restrict access to the customer's profile page to authenticated customers only.
To do this, you can try to retrieve the customer's details. If the request fails, you can redirect the customer to the login page.
For example:
```ts
sdk.store.customer.retrieve()
.then(({ customer }) => {
// use customer...
console.log(customer)
})
.catch(() => {
// redirect to login page
})
```
The `catch` block will only execute if the request fails, which means that the customer is not authenticated. You can add the redirect logic to the `catch` block based on your storefront framework.
<Note title="Tip">
If you're building a React storefront, you can use the `useCustomer` hook defined in the [Customer Context guide](../context/page.mdx) to check if the customer is set. If not, you can redirect the customer to the login page.
</Note>

View File

@@ -13,12 +13,14 @@ export const metadata = {
# {metadata.title}
To login a customer with a third-party service, such as Google or GitHub, you must follow the following flow:
In this guide, you'll learn how to implement third-party or social login in your storefront. You'll implement the flow using Google as an example.
## Third-Party Login Flow in Storefront
Assuming you already set up the [Auth Module Provider](../../../commerce-modules/auth/auth-providers/page.mdx) in your Medusa application, you can login a customer with a third-party service, such as Google or GitHub, using the following flow:
![Diagram illustrating the authentication flow between the storefront, Medusa application, and the third-party service.](https://res.cloudinary.com/dza7lstvk/image/upload/v1725531068/Medusa%20Resources/Social_Media_Graphics_third-party-auth-customer_kfn3k3.jpg)
<Details summaryContent="Explanation" className="my-1">
1. Authenticate the customer with the [Authenticate Customer API route](!api!/store#auth_postactor_typeauth_provider).
2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The storefront, when it receives a `location` property in the response, must redirect to the returned location.
3. Once the authentication with the third-party service finishes, it redirects back to the storefront with query parameters such as `code` and `state`. So, make sure your third-party service is configured to redirect to your storefront page after successful authentication.
@@ -28,9 +30,7 @@ To login a customer with a third-party service, such as Google or GitHub, you mu
- If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests.
- If not, follow the rest of the steps.
7. The storefront uses the authentication token to create the customer using the [Create Customer API route](!api!/store#customers_postcustomers).
8. The storefront sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token for the customer.
</Details>
8. The storefront sends a request to the [Refresh Token Route](#add-the-function-to-refresh-the-token) to retrieve a new token for the customer.
You'll implement the flow in this guide using Google as an example.
@@ -43,114 +43,67 @@ You'll implement the flow in this guide using Google as an example.
]}
/>
## JS SDK Authentication Configuration
Before implementing the third-party login flow, you need to configure in the JS SDK the authentication method you're using in your storefront. This defines how the JS SDK will handle sending authenticated requests after the customer is authenticated.
Learn more about the authentication methods and how to configure them in the [Login Customer](../login/page.mdx) guide.
---
## Step 1: Authenticate Customer in Medusa
When the customer clicks on a "Login with Google" button, send a request to the [Authenticate Customer API route](!api!/store#auth_postactor_typeauth_provider).
For example:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="authenticated-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["2", "fetch", "Send a request to the Authenticate Customer API route"],
["10", "result.location", "If the request returns a location, redirect to that location to continue the authentication."],
["17", "!result.token", "If the token isn't returned, the authentication has failed."],
["26", "fetch", "Send a request as an authenticated customer."]
]
```ts highlights={fetchHighlights}
const loginWithGoogle = async () => {
const result = await fetch(
`http://localhost:9000/auth/customer/google`,
{
credentials: "include",
method: "POST",
}
).then((res) => res.json())
if (result.location) {
// redirect to Google for authentication
window.location.href = result.location
return
}
if (!result.token) {
// result failed, show an error
alert("Authentication failed")
return
}
// authentication successful
// use token in the authorization header of
// all follow up requests. For example:
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${result.token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const reactHighlights = [
["5", "fetch", "Send a request to the Authenticate Customer API route"],
["13", "result.location", "If the request returns a location, redirect to that location to continue the authentication."],
["20", "!result.token", "If the token isn't returned, the authentication has failed."],
["29", "fetch", "Send a request as an authenticated customer."]
["7", "login", "Send a request to the Authenticate Customer API route"],
["9", "result.location", "If the request returns a location, redirect to that location to continue the authentication."],
["16", "", "If the token isn't returned, the authentication has failed."],
["24", "setToken", "Set the token in the client to be used in subsequent requests."],
["27", "retrieve", "Retrieve the customer's details as an example of testing authentication."]
]
```tsx highlights={reactHighlights}
"use client" // include with Next.js 13+
import { sdk } from "@/lib/sdk"
export default function Login() {
const loginWithGoogle = async () => {
const result = await fetch(
`http://localhost:9000/auth/customer/google`,
{
credentials: "include",
method: "POST",
}
).then((res) => res.json())
const result = await sdk.auth.login("customer", "google", {})
if (result.location) {
if (typeof result === "object" && result.location) {
// redirect to Google for authentication
window.location.href = result.location
return
}
if (!result.token) {
if (typeof result !== "string") {
// result failed, show an error
alert("Authentication failed")
return
}
// authentication successful
// use token in the authorization header of
// all follow up requests. For example:
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${result.token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
)
.then((res) => res.json())
// set the token in the client to be used in subsequent requests
sdk.client.setToken(result)
// retrieve the customer using the token
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
}
return (
@@ -159,18 +112,61 @@ export default function Login() {
</div>
)
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const jsSdkHighlights = [
["2", "login", "Send a request to the Authenticate Customer API route"],
["4", "", "If the request returns a location, redirect to that location to continue the authentication."],
["11", "", "If the token isn't returned, the authentication has failed."],
["19", "setToken", "Set the token in the client to be used in subsequent requests."],
["22", "retrieve", "Retrieve the customer's details as an example of testing authentication."]
]
```ts highlights={jsSdkHighlights}
const loginWithGoogle = async () => {
const result = await sdk.auth.login("customer", "google", {})
if (typeof result === "object" && result.location) {
// redirect to Google for authentication
window.location.href = result.location
return
}
if (typeof result !== "string") {
// result failed, show an error
alert("Authentication failed")
return
}
// authentication successful
// set the token in the client to be used in subsequent requests
sdk.client.setToken(result)
// retrieve the customer using the token
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
}
```
</CodeTab>
</CodeTabs>
If the Authenticate Customer API route returns a `location`, then you redirect to the returned page for authentication with the third-party service.
You define a `loginWithGoogle` function that:
If the route returns a `token`, then the customer has been authenticated before. You can use the token for subsequent authenticated request.
- Sends a request to the `/auth/customer/google` API route.
- If the response is an object with a `location` property, then you redirect to the returned page for authentication with the third-party service.
- If the response is a string, then the customer has been authenticated before. You can use the token for subsequent authenticated request.
- To use the token for subsequent authenticated request, you must set it in the JS SDK using the `client.setToken` method.
- Now, subsequent requests are authenticated. As an example, you can retrieve the customer's details using the `store.customer.retrieve` method.
<Note title="Tip">
If you're using a provider other than Google, or if you've configured the Google provider with an ID other than `google`, replace `google` in the URL `http://localhost:9000/auth/customer/google` with your provider ID.
If you're using a provider other than Google, or if you've configured the Google provider with an ID other than `google`, replace `google` in the parameter `login("customer", "google", {})` with your provider ID.
</Note>
@@ -178,63 +174,38 @@ If you're using a provider other than Google, or if you've configured the Google
## Step 2: Callback Page in Storefront
The next step is to create a page in your storefront that the customer is redirected to after they authenticate with Google.
In the previous step, you implemented as part of the login flow redirecting the customer to the third-party service for authentication.
You'll use this page's URL as the Redirect Uri in your Google settings, and set it in the `callbackUrl` of your Google provider's configurations.
Once the customer authenticates with the third-party service, the service redirects the customer back to your storefront with query parameters such as `code` and `state`.
First, install the [react-jwt library](https://www.npmjs.com/package/react-jwt) in your storefront to use it for decoding the token:
The next step is to create the page in your storefront that the customer is redirected to after they authenticate with Google. You'll use this page's URL as the Redirect Uri in your Google settings, and set it in the `callbackUrl` of your [Google Auth Module Provider](../../../commerce-modules/auth/auth-providers/google/page.mdx)'s configurations.
<Note title="Tip">
The callback page is implemented step-by-step to explain the different parts of the flow. You can copy the full page code in the [Full Code Example for Third-Party Login Callback Page](#full-code-example-for-third-party-login-callback-page) section, and then add the functions one by one to test the flow.
</Note>
### Install the React-JWT Library
First, install the [react-jwt library](https://www.npmjs.com/package/react-jwt) in your storefront to use it for decoding the token received from Google:
```bash npm2yarn
npm install react-jwt
```
### Implement the Callback Page
Then, in a new page in your storefront that will be used as the callback / redirect uri destination, add the following:
<CodeTabs group="authenticated-request">
<CodeTab label="Fetch API" value="fetch">
export const sendCallbackFetchHighlights = [
["6", "queryParams", "The query parameters received from Google, such as `code` and `state`."],
["10", "fetch", "Send a request to the Validate Authentication Callback API route"],
["18", "!token", "If the token isn't returned, the authentication has failed."],
]
```ts highlights={sendCallbackFetchHighlights}
import { decodeToken } from "react-jwt"
// ...
const searchParams = new URLSearchParams(window.location.search)
const queryParams = Object.fromEntries(searchParams.entries())
const sendCallback = async () => {
const { token } = await fetch(
`http://localhost:9000/auth/customer/google/callback?${new URLSearchParams(queryParams).toString()}`,
{
credentials: "include",
method: "POST",
}
).then((res) => res.json())
if (!token) {
alert("Authentication Failed")
return
}
return token
}
// TODO add more functions...
```
</CodeTab>
<CodeTab label="React" value="react">
export const sendCallbackReactHighlights = [
["11", "code", "The code received from Google as a query parameter."],
["11", "state", "The state received from Google as a query parameter."],
["20", "fetch", "Send a request to the Validate Authentication Callback API route"],
["28", "!token", "If the token isn't returned, the authentication has failed."],
["12", "queryParams", "The query parameters received from Google, such as `code` and `state`."],
["21", "callback", "Send a request to the Validate Authentication Callback API route"],
["28", "catch", "If an error occurs, show an alert and exit execution."],
["36", "setToken", "Set the token in the client to be used in subsequent requests."]
]
```tsx highlights={sendCallbackReactHighlights}
@@ -243,6 +214,7 @@ export const sendCallbackReactHighlights = [
import { HttpTypes } from "@medusajs/types"
import { useEffect, useMemo, useState } from "react"
import { decodeToken } from "react-jwt"
import { sdk } from "@/lib/sdk"
export default function GoogleCallback() {
const [loading, setLoading] = useState(true)
@@ -254,19 +226,26 @@ export default function GoogleCallback() {
}, [])
const sendCallback = async () => {
const { token } = await fetch(
`http://localhost:9000/auth/customer/google/callback?${new URLSearchParams(queryParams).toString()}`,
{
credentials: "include",
method: "POST",
}
).then((res) => res.json())
let token = ""
if (!token) {
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
return
throw error
}
// set the token in the client
// to be used in subsequent requests
sdk.client.setToken(token)
return token
}
@@ -282,57 +261,95 @@ export default function GoogleCallback() {
```
</CodeTab>
</CodeTabs>
This adds in the new page the function `sendCallback` which sends a request to the [Validate Callback API route](!api!/store#auth_postactor_typeauth_providercallback), passing it the query parameters received from Google.
Then, replace the `TODO` with the following:
<CodeTab label="JS SDK" value="js-sdk">
export const createCustomerHighlights = [
["1", "token", "The token received from the Validate Callback API route."],
["2", "fetch", "Create a customer"]
export const sendCallbackFetchHighlights = [
["6", "queryParams", "The query parameters received from Google, such as `code` and `state`."],
["12", "callback", "Send a request to the Validate Authentication Callback API route"],
["19", "catch", "If an error occurs, show an alert and exit execution."],
["27", "setToken", "Set the token in the client to be used in subsequent requests."]
]
```ts highlights={createCustomerHighlights} title="Fetch API / React Applicable"
const createCustomer = async (token: string) => {
await fetch(`http://localhost:9000/store/customers`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
// TODO show form to retrieve email from customer
email: "example@medusajs.com",
}),
}).then((res) => res.json())
```ts highlights={sendCallbackFetchHighlights}
import { decodeToken } from "react-jwt"
// ...
const searchParams = new URLSearchParams(window.location.search)
const queryParams = Object.fromEntries(searchParams.entries())
const sendCallback = async () => {
let token = ""
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
throw error
}
// set the token in the client
// to be used in subsequent requests
sdk.client.setToken(token)
return token
}
// TODO add more functions...
```
This adds to the page the function `createCustomer` which, if the customer is new, it uses the token received from the Validate Callback API route to create a new customer.
</CodeTab>
</CodeTabs>
This adds in the new page the function `sendCallback` which sends a request to the [Validate Callback API route](!api!/store#auth_postactor_typeauth_providercallback), passing it all query parameters received from Google. Those include the `code` and `state` parameters.
After that, you set the token in the JS SDK using the `client.setToken` method. This ensures that the token is passed to subsequent requests, such as the request to create the customer.
### Add the Function to Create a Customer
Next, replace the `TODO` after the `sendCallback` function with the following:
export const createCustomerHighlights = [
["3", "create", "Create a customer"]
]
```ts highlights={createCustomerHighlights} title="JS SDK / React Applicable"
const createCustomer = async () => {
// create customer
await sdk.store.customer.create({
email: "example@medusajs.com",
})
}
// TODO add more functions...
```
This adds to the page the function `createCustomer` which creates a customer if this is the first time the customer is authenticating with the third-party service.
Notice that this method assumes that the token received from the [Validate Callback API route](!api!/store#auth_postactor_typeauth_providercallback) is already set in the JS SDK, as done at the end of the `sendCallback` function. So, if you're implemeting this flow without using the JS SDK, make sure to pass the token in the authorization Bearer header.
### Add the Function to Refresh the Token
Next, replace the new `TODO` with the following:
export const refreshTokenHighlights = [
["1", "token", "The token received from the Validate Callback API route."],
["2", "fetch", "Fetch a new token for the created customer."]
["3", "refresh", "Fetch a new token for the created customer."]
]
```ts highlights={refreshTokenHighlights} title="Fetch API / React Applicable"
const refreshToken = async (token: string) => {
const result = await fetch(`http://localhost:9000/auth/token/refresh`, {
credentials: "include",
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
},
}).then((res) => res.json())
return result.token
```ts highlights={refreshTokenHighlights} title="JS SDK / React Applicable"
const refreshToken = async () => {
// refresh the token
const result = await sdk.auth.refresh()
// set the new token
sdk.client.setToken(result)
}
// TODO add more functions...
@@ -340,48 +357,15 @@ const refreshToken = async (token: string) => {
This adds to the page the function `refreshToken` which is used after the new customer is created to refresh their authentication token. This ensures that the authentication token includes the details of the created customer.
Notice that this method assumes that the token received from the [Validate Callback API route](!api!/store#auth_postactor_typeauth_providercallback) is already set in the JS SDK, as done at the end of the `sendCallback` function. So, if you're implemeting this flow without using the JS SDK, make sure to pass the token in the authorization Bearer header.
Then, this method also sets the new token in the JS SDK to be used in subsequent authenticated requests.
### Add the Function to Validate the Callback
Finally, add in the place of the new `TODO` the `validateCallback` function that runs when the page first loads to validate the authentication:
<CodeTabs group="authenticated-request">
<CodeTab label="Fetch API" value="fetch">
export const validateFetchHighlights = [
["2", "sendCallback", "Validate the callback in Medusa and retrieve the authentication token"],
["4", "shouldCreateCustomer", "Check if the decoded token has an `actor_id` property to decide whether a customer to be created."],
["7", "createCustomer", "Create a customer if the decoded token doesn't have `actor_id`."],
["9", "refreshToken", "Fetch a new token for the created customer."],
["13", "fetch", "Send an authenticated request using the token."]
]
```ts highlights={validateFetchHighlights}
const validateCallback = async () => {
let { token } = await sendCallback()
const shouldCreateCustomer = (decodeToken(token) as { actor_id: string }).actor_id === ""
if (shouldCreateCustomer) {
await createCustomer(token)
token = await refreshToken(token)
}
// use token to send authenticated requests
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
).then((res) => res.json())
}
```
</CodeTab>
<CodeTab label="React" value="react">
export const validateReactHighlights = [
@@ -389,34 +373,23 @@ export const validateReactHighlights = [
["4", "shouldCreateCustomer", "Check if the decoded token has an `actor_id` property to decide whether a customer to be created."],
["7", "createCustomer", "Create a customer if the decoded token doesn't have `actor_id`."],
["9", "refreshToken", "Fetch a new token for the created customer."],
["13", "fetch", "Send an authenticated request using the token."]
["13", "retrieve", "Retrieve the customer's details as an example of testing authentication."]
]
```tsx highlights={validateReactHighlights}
const validateCallback = async () => {
let { token } = await sendCallback()
const token = await sendCallback()
const shouldCreateCustomer = (decodeToken(token) as { actor_id: string }).actor_id === ""
if (shouldCreateCustomer) {
await createCustomer(token)
await createCustomer()
token = await refreshToken(token)
await refreshToken()
}
// use token to send authenticated requests
const { customer: customerData } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
).then((res) => res.json())
const { customer: customerData } = await sdk.store.customer.retrieve()
setCustomer(customerData)
setLoading(false)
@@ -430,6 +403,37 @@ useEffect(() => {
validateCallback()
}, [loading])
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const validateFetchHighlights = [
["2", "sendCallback", "Validate the callback in Medusa and retrieve the authentication token"],
["4", "shouldCreateCustomer", "Check if the decoded token has an `actor_id` property to decide whether a customer to be created."],
["7", "createCustomer", "Create a customer if the decoded token doesn't have `actor_id`."],
["9", "refreshToken", "Fetch a new token for the created customer."],
["13", "retrieve", "Retrieve the customer's details as an example of testing authentication."]
]
```ts highlights={validateFetchHighlights}
const validateCallback = async () => {
const token = await sendCallback()
const shouldCreateCustomer = (decodeToken(token) as { actor_id: string }).actor_id === ""
if (shouldCreateCustomer) {
await createCustomer()
await refreshToken()
}
// use token to send authenticated requests
const { customer: customerData } = await sdk.store.customer.retrieve()
setCustomer(customerData)
setLoading(false)
}
```
</CodeTab>
@@ -437,20 +441,22 @@ useEffect(() => {
The `validateCallback` function uses the functions added earlier to:
1. Send a request to the Validate Callback API route, which returns an authentication token.
1. Send a request to the [Validate Callback API route](!api!/store#auth_postactor_typeauth_providercallback), which returns an authentication token.
- The `sendCallback` function also sets the token in the JS SDK to be passed in subsequent requests.
2. Decodes the token to check if it has an `actor_id` property.
- If so, then the customer is previously registered, and the authentication token can be used for subsequent authenticated requests.
- If not:
1. Create a customer using the Create Customer API route.
2. Refetch the customer's token after it's created using the Refresh Token API route.
- If not, this is the first time the customer is authenticating with the third-party service, so:
1. Create a customer using the [Create Customer API route](!api!/store#customers_postcustomers).
2. Refetch the customer's authentication token after it's created using the [Refresh Token API route](!api!/store#auth_postactor_typeauth_providerrefresh).
3. Use the token for subsequent authenticated requests.
3. Retrieve the customer's details as an example of testing authentication.
### Full Code Example for Callback Page
The customer is now authenticated, and you can redirect them to the home page or the page they were trying to access before logging in.
<Details summaryContent="Full Example">
### Full Code Example for Third-Party Login Callback Page
<CodeTabs group="authenticated-request">
<CodeTab label="Fetch API" value="fetch">
<CodeTab label="JS SDK" value="js-sdk">
```ts
import { decodeToken } from "react-jwt"
@@ -463,74 +469,60 @@ const state = queryParams.get("state")
const sendCallback = async () => {
const { token } = await fetch(
`http://localhost:9000/auth/customer/google/callback?code=${code}&state=${state}`,
{
credentials: "include",
method: "POST",
}
).then((res) => res.json())
let token = ""
if (!token) {
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
return
throw error
}
// set the token in the client
// to be used in subsequent requests
sdk.client.setToken(token)
return token
}
const createCustomer = async (token: string) => {
await fetch(`http://localhost:9000/store/customers`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
// TODO show form to retrieve email from customer
email: "example@medusajs.com",
}),
}).then((res) => res.json())
const createCustomer = async () => {
// create customer
await sdk.store.customer.create({
email: "example@medusajs.com",
})
}
const refreshToken = async (token: string) => {
const result = await fetch(`http://localhost:9000/auth/token/refresh`, {
credentials: "include",
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
},
}).then((res) => res.json())
return result.token
const refreshToken = async () => {
// refresh the token
const result = await sdk.auth.refresh()
// set the new token
sdk.client.setToken(result)
}
const validateCallback = async () => {
let { token } = await sendCallback()
const token = await sendCallback()
const shouldCreateCustomer = (decodeToken(token) as { actor_id: string }).actor_id === ""
if (shouldCreateCustomer) {
await createCustomer(token)
await createCustomer()
token = await refreshToken(token)
await refreshToken()
}
// use token to send authenticated requests
const { customer } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
).then((res) => res.json())
const { customer: customerData } = await sdk.store.customer.retrieve()
setCustomer(customerData)
setLoading(false)
}
```
@@ -543,88 +535,69 @@ const validateCallback = async () => {
import { HttpTypes } from "@medusajs/types"
import { useEffect, useMemo, useState } from "react"
import { decodeToken } from "react-jwt"
import { sdk } from "@/lib/sdk"
export default function GoogleCallback() {
const [loading, setLoading] = useState(true)
const [customer, setCustomer] = useState<HttpTypes.StoreCustomer>()
// for other than Next.js
const { code, state } = useMemo(() => {
const queryParams = new URLSearchParams(window.location.search)
return {
code: queryParams.get("code"),
state: queryParams.get("state"),
}
const queryParams = useMemo(() => {
const searchParams = new URLSearchParams(window.location.search)
return Object.fromEntries(searchParams.entries())
}, [])
const sendCallback = async () => {
const { token } = await fetch(
`http://localhost:9000/auth/customer/google/callback?code=${code}&state=${state}`,
{
credentials: "include",
method: "POST",
}
).then((res) => res.json())
let token = ""
if (!token) {
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
return
throw error
}
// set the token in the client
// to be used in subsequent requests
sdk.client.setToken(token)
return token
}
const createCustomer = async (token: string) => {
await fetch(`http://localhost:9000/store/customers`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
body: JSON.stringify({
// TODO show form to retrieve email from customer
email: "example@medusajs.com",
}),
}).then((res) => res.json())
const createCustomer = async () => {
// create customer
await sdk.store.customer.create({
email: "example@medusajs.com",
})
}
const refreshToken = async (token: string) => {
const result = await fetch(`http://localhost:9000/auth/token/refresh`, {
credentials: "include",
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
},
}).then((res) => res.json())
return result.token
const refreshToken = async () => {
// refresh the token
const result = await sdk.auth.refresh()
// set the new token
sdk.client.setToken(result)
}
const validateCallback = async () => {
let { token } = await sendCallback()
const token = await sendCallback()
const shouldCreateCustomer = (decodeToken(token) as { actor_id: string }).actor_id === ""
if (shouldCreateCustomer) {
await createCustomer(token)
await createCustomer()
token = await refreshToken(token)
await refreshToken()
}
// use token to send authenticated requests
const { customer: customerData } = await fetch(
`http://localhost:9000/store/customers/me`,
{
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
}
).then((res) => res.json())
const { customer: customerData } = await sdk.store.customer.retrieve()
setCustomer(customerData)
setLoading(false)
@@ -649,5 +622,3 @@ export default function GoogleCallback() {
</CodeTab>
</CodeTabs>
</Details>

View File

@@ -179,7 +179,7 @@ export default {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./providers/**/*.{js,ts,jsx,tsx}",
"@/providers/**/*.{js,ts,jsx,tsx}",
medusaUI,
],
darkMode: "class",
@@ -330,7 +330,7 @@ import {
// other imports...
useEffect,
} from "react"
import { sdk } from "../lib/sdk"
import { sdk } from "@/lib/sdk"
```
And replace the `TODO` in the `RegionProvider` component with the following content:
@@ -531,7 +531,7 @@ import {
// other imports...
useEffect,
} from "react"
import { sdk } from "../lib/sdk"
import { sdk } from "@/lib/sdk"
```
And replace the `TODO` in the `CartProvider` component with the following content:
@@ -802,7 +802,7 @@ export const secondColHighlights = [
"use client"
import { clx } from "@medusajs/ui"
import { useRegion } from "../../providers/region"
import { useRegion } from "@/providers/region"
export const SecondCol = () => {
const { region, regions, setRegion } = useRegion()
@@ -889,10 +889,10 @@ export const layoutHighlights = [
```tsx title="components/Layout/index.tsx" highlights={layoutHighlights}
import { clx } from "@medusajs/ui"
import { Inter, Roboto_Mono } from "next/font/google"
import { RegionProvider } from "../providers/region"
import { RegionProvider } from "@/providers/region"
import "./globals.css"
import { SecondCol } from "../components/SecondCol"
import { CartProvider } from "../providers/cart"
import { CartProvider } from "@/providers/cart"
export const inter = Inter({
subsets: ["latin"],
@@ -981,7 +981,7 @@ export const routerHighlights = [
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useCart } from "../../providers/cart"
import { useCart } from "@/providers/cart"
import { useEffect, useMemo } from "react"
type ActiveTab = "product" | "address" | "shipping" | "payment"
@@ -1179,8 +1179,8 @@ import {
useState,
} from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../../providers/region"
import { useCart } from "../../providers/cart"
import { useRegion } from "@/providers/region"
import { useCart } from "@/providers/cart"
import { useRouter } from "next/navigation"
type ProductProps = {
@@ -1220,7 +1220,7 @@ The component defines the following variables:
Next, you'll retrieve the product's details from the Medusa application. Start by adding the following imports to the top of the file:
```tsx title="components/Product/index.tsx"
import { sdk } from "../../lib/sdk"
import { sdk } from "@/lib/sdk"
import {
// other imports...
useEffect,
@@ -1648,8 +1648,8 @@ export const addressHighlights1 = [
import {
useState,
} from "react"
import { useCart } from "../../providers/cart"
import { useRegion } from "../../providers/region"
import { useCart } from "@/providers/cart"
import { useRegion } from "@/providers/region"
import { useRouter } from "next/navigation"
type AddressProps = {
@@ -1946,7 +1946,7 @@ export const shippingHighlights1 = [
import {
useState,
} from "react"
import { useCart } from "../../providers/cart"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@medusajs/types"
import { useRouter } from "next/navigation"
@@ -1996,7 +1996,7 @@ import {
// other imports...
useEffect,
} from "react"
import { sdk } from "../../lib/sdk"
import { sdk } from "@/lib/sdk"
```
Then, replace the `TODO` in the `Shipping` component with the following:
@@ -2288,7 +2288,7 @@ export const paymentHighlights1 = [
import {
useState,
} from "react"
import { useCart } from "../../providers/cart"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@medusajs/types"
import { useRouter } from "next/navigation"
@@ -2332,7 +2332,7 @@ import {
// other imports...
useEffect,
} from "react"
import { sdk } from "../../lib/sdk"
import { sdk } from "@/lib/sdk"
```
Then, replace the `TODO` in the `Payment` component with the following:
@@ -2675,7 +2675,7 @@ export const confirmationHighlights = [
```tsx title="app/confirmation/[id]/page.tsx" highlights={confirmationHighlights}
import { clx, Heading } from "@medusajs/ui"
import { sdk } from "../../../lib/sdk"
import { sdk } from "@/lib/sdk"
type Params = {
params: Promise<{ id: string }>

View File

@@ -6,7 +6,9 @@ export const metadata = {
# {metadata.title}
The Medusa application is made up of a Node.js service and an admin dashboard. The storefront is installed, built, and hosted separately from the Medusa application, providing you with the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience.
The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed, built, and hosted separately from the Medusa application, providing you with the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience.
You can build your storefront from scratch, or build it on top of the [Next.js Starter storefront](../nextjs-starter/page.mdx). This section of the documentation provides guides to help you build a storefront from scratch for your Medusa application.
<Note>
@@ -14,14 +16,27 @@ Learn more about Medusa's architecture in [this documentation](!docs!/learn/intr
</Note>
You can build your storefront from scratch, or build it on top of the [Next.js Starter storefront](../nextjs-starter/page.mdx). This section of the documentation provides guides to help you build a storefront from scratch for your Medusa application.
## Who Should Follow These Guides?
These guides are for developers who want to build a storefront for their Medusa application. That includes:
- A web storefront built with a frontend framework, such as Next.js. All guides include examples for React, but the concepts can be applied to other frontend frameworks.
- A mobile application or storefront that allows customers to browse and purchase products. While the guides don't cover mobile development specifically, the concepts can be applied to your mobile storefront as well. If you're using React Native, you can generally use the React code examples with some adjustments.
---
## Using the JS SDK
All guides under this documentation section use the JS SDK to interact with the Medusa application's Store API Routes. The JS SDK is a NPM package that facilitates interacting with the backend's REST APIs. You can use it in any JavaScript framework, such as Next.js, React, Vue, or Angular.
All guides under this documentation section use the [JS SDK](../js-sdk/page.mdx) to interact with the Medusa application's Store API Routes. The JS SDK is a NPM package that facilitates interacting with the backend's REST APIs. You can use it in any JavaScript framework, such as Next.js, React, Vue, or Angular.
To install and set up the JS SDK, refer to the [JS SDK documentation](../js-sdk/page.mdx).
### Not Using JavaScript?
If you're not using JavaScript to build your storefront, the guides include a link to the [Store API reference](!api!/store) for the different API routes that the JS SDK uses in the code examples. You can use the API reference to learn about the API route's URL, parameters, response, and more.
---
<ChildDocs showItems={["General"]} />
---

View File

@@ -8,41 +8,35 @@ tags:
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `List Product Categories in Storefront`,
title: `Show Product Categories in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to list product categories in the storefront, including paginating and filtering them.
In this guide, you'll learn how to show a list of product categories in the storefront. You'll also learn how to paginate and filter them.
<Note title="Good to know">
Product categories allow you to organize similar products together and within a hierarchy. For example, you can have a "Shoes" category grouping together all different types of shoes. You can then allow customers to browse products by category.
</Note>
## List Product Categories
To retrieve the list of product categories, send a request to the [List Product Categories API route](!api!/store#product-categories_getproductcategories):
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(`http://localhost:9000/store/product-categories`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product_categories }) => {
// use categories...
console.log(product_categories)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["17"], ["18"], ["19"], ["20"],
["21"], ["22"], ["23"], ["24"],
["25"], ["26"], ["27"]
["18"], ["19"], ["20"],
["21"], ["22"],
]
```tsx highlights={highlights}
@@ -50,6 +44,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
export default function Categories() {
const [loading, setLoading] = useState(true)
@@ -62,13 +57,7 @@ export const highlights = [
return
}
fetch(`http://localhost:9000/store/product-categories`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.category.list()
.then(({ product_categories }) => {
setCategories(product_categories)
setLoading(false)
@@ -93,16 +82,29 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
sdk.store.category.list()
.then(({ product_categories }) => {
// use categories...
console.log(product_categories)
})
```
</CodeTab>
</CodeTabs>
In this example, you send a request to the [List Product Categories API route](!api!/store#product-categories_getproductcategories).
The response has a `product_categories` field, which is an array of [product categories](!api!/store/#product-categories_productcategory_schema).
---
## Paginate Product Categories
To paginate product categories, pass the following query parameters:
To paginate product categories, pass the following query parameters to the [List Product Categories API route](!api!/store#product-categories_getproductcategories):
- `limit`: The number of product categories to return in the request.
- `offset`: The number of product categories to skip before the returned product categories. You can calculate this by multiplying the current page with the limit.
@@ -112,11 +114,10 @@ The response object returns a `count` field, which is the total count of product
For example:
export const paginateHighlights = [
["20", "offset", "Calculate the number of product categories to skip based on the current page and limit."],
["28", "searchParams.toString()", "Pass the pagination parameters in the query."],
["36", "count", "The total number of product categories in the Medusa application."],
["48", "setHasMorePages", "Set whether there are more pages based on the total count."],
["67", "button", "Show a button to load more product categories if there are more pages."]
["21", "offset", "Calculate the number of product categories to skip based on the current page and limit."],
["27", "count", "The total number of product categories in the Medusa application."],
["39", "setHasMorePages", "Set whether there are more pages based on the total count."],
["58", "button", "Show a button to load more product categories if there are more pages."]
]
```tsx highlights={paginateHighlights}
@@ -124,6 +125,7 @@ export const paginateHighlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
export default function Categories() {
const [loading, setLoading] = useState(true)
@@ -141,20 +143,10 @@ export default function Categories() {
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
sdk.store.category.list({
limit: limit,
offset: offset,
})
fetch(`http://localhost:9000/store/product-categories?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product_categories, count }) => {
setCategories((prev) => {
if (prev.length > offset) {
@@ -201,31 +193,48 @@ export default function Categories() {
}
```
In this example, you send a request to the [List Product Categories API route](!api!/store#product-categories_getproductcategories) with the `limit` and `offset` query parameters.
The response object returns a `count` field, which is the total count of product categories. Use it to determine whether there are more product categories that can be loaded.
If there are more product categories, you show a button to load more product categories on click.
---
## Filter Categories
The List Product Categories API route accepts query parameters to filter the categories by description, handle, and more.
The [List Product Categories API route](!api!/store#product-categories_getproductcategories) accepts query parameters to filter the categories by description, handle, and more.
Refer to the [API reference](!api!/store#product-categories_getproductcategories) for the list of accepted query parameters.
Refer to the [API reference](!api!/store#product-categories_getproductcategories) for the full list of accepted query parameters.
For example, to run a query on the product categories:
For example, to filter the categories by with a search query:
```ts
const searchParams = new URLSearchParams({
sdk.store.category.list({
q: "Shirt",
})
fetch(`http://localhost:9000/store/product-categories?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product_categories, count }) => {
// TODO set categories...
})
```
By passing the `q` parameter, you can search through the categories' searchable fields, including their title and description.
---
## Sort Categories
To sort categories by a field, use the `order` query parameter. Its value is a comma-separated list of fields to sort by, and each field is optionally prefixed by `-` to indicate descending order.
For example, to sort categories by title in descending order:
```ts
sdk.store.category.list({
order: "-title",
})
.then(({ product_categories, count }) => {
// TODO set categories...
})
```
The result will be categories sorted by title in descending order.

View File

@@ -13,56 +13,43 @@ export const metadata = {
# {metadata.title}
A product category has parent and child categories.
In this guide, you'll learn how to retrieve nested categories in the storefront.
## How to Retrieve Nested Categories in Storefront?
A product category has parent and child categories. For example, a "Shoes" category can have a "Running Shoes" child category.
There are two ways to retrieve nested categories:
- [Retrieve nested categories of a category](#retrieve-nested-categories-of-a-category). This is useful if you're showing the nested categories on a category page.
- [Retrieve all categories as a hierarchy](#retrieve-categories-as-a-hierarchy). This is useful if you're showing the categories in a tree structure, such as in a menu or navigation bar.
---
## Retrieve Nested Categories of a Category
To retrieve the child or nested categories of a category in your storefront, pass to the [Get a Category API Route](!api!/store#product-categories_getproductcategoriesid) the following query parameters:
- `include_descendants_tree=true` to retrieve each category's nested categories at all levels.
- Add `category_children` to `fields`, which is the field that will hold a category's nested categories. You can either pass `*category_children` to retrieve all fields of a child category, or specify the fields specifically to avoid a large response size. For example, `field=category_children.id,category_children.name`.
- `parent_category_id=null` to retrieve only the categories that don't have a parent. This avoids retrieving the child categories multiple times in the response since child categories are now set in their parent's object.
- Add `category_children` to `fields`, which is the field that will hold a category's nested categories.
- You can either pass `*category_children` to retrieve all fields of a child category, or specify the fields specifically to avoid a large response size. For example, `field=category_children.id,category_children.name`.
For example:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["2", `"category_children.id,category_children.name"`, "Select the fields of category children"],
["3", `"include_descendants_tree"`, "Indicate that all nested categories should be retrieved."],
["4", "parent_category_id", "Since each category will have its children, you only retrieve categories that don't have a parent."]
]
```ts highlights={fetchHighlights}
const searchParams = new URLSearchParams({
fields: "category_children.id,category_children.name",
include_descendants_tree: true,
parent_category_id: null,
})
fetch(`http://localhost:9000/store/product-categories/${id}?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product_category }) => {
// use the product category's children...
console.log(product_category.category_children)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["22", `fields`, "Select the fields of category children"],
["23", "parent_category_id", "Since each category will have its children, you only retrieve categories that don't have a parent."],
["26"], ["27"], ["28"],
["29"], ["30"], ["31"], ["32"], ["33"], ["34"], ["35"], ["36"], ["37"], ["38"],
["52", "", "Show the nested categories."],
["22"], ["23", `fields`, "Select the fields of category children"],
["24", "include_descendants_tree", "Indicate that all nested categories should be retrieved."],
["25"], ["26"], ["27"], ["28"], ["29"],
["39", "category_children", "Show the nested categories."],
]
```tsx highlights={highlights}
@@ -70,6 +57,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
id: string
@@ -86,20 +74,10 @@ export const highlights = [
return
}
const searchParams = new URLSearchParams({
sdk.store.category.retrieve(id, {
fields: "category_children.id,category_children.name",
parent_category_id: null,
include_descendants_tree: true,
})
fetch(`http://localhost:9000/store/product-categories/${id}?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product_category }) => {
setCategory(product_category)
setLoading(false)
@@ -134,7 +112,147 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["2", `fields`, "Select the fields of category children"],
["3", `include_descendants_tree`, "Indicate that all nested categories should be retrieved."],
]
```ts highlights={fetchHighlights}
sdk.store.category.retrieve(id, {
fields: "category_children.id,category_children.name",
include_descendants_tree: true,
})
.then(({ product_categories }) => {
// use the product category's children...
console.log(product_categories[0].category_children)
})
```
</CodeTab>
</CodeTabs>
The `product_category` field in the response has a `category_children` field. It's an array of [product category objects](!api!/store/#product-categories_productcategory_schema).
In this example, you retrieve the nested categories of a category by passing the `include_descendants_tree` query parameter to the [Get a Category API Route](!api!/store#product-categories_getproductcategoriesid).
The response has a `product_category` field, which is a [product category object](!api!/store/#product-categories_productcategory_schema). It will have a `category_children` field, which is an array of [product category objects](!api!/store/#product-categories_productcategory_schema).
Then, in the React component, you show a category's children by iterating over the `category_children` field.
---
## Retrieve Categories as a Hierarchy
Alternatively, you may want to retrieve all categories as a hierarchy.
To do this, you can pass the `include_descendants_tree` query parameter to the [List Product Categories API Route](!api!/store#product-categories_getproductcategories), along with the `parent_category_id` query parameter set to `null`. This ensures that only categories with children are retrieved at the top level.
For example:
<CodeTabs group="store-request">
<CodeTab label="React" value="react">
export const categoriesHighlights = [
["22"], ["23"], ["24", `fields`, "Select the fields of category children"],
["25", "include_descendants_tree", "Indicate that all nested categories should be retrieved."],
["26", "parent_category_id", "Since each category will have its children, you only retrieve categories that don't have a parent."],
["27"], ["28"],
["29"], ["30"], ["31"],
["45", "category_children", "Show the nested categories."],
]
```tsx highlights={categoriesHighlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
id: string
}
export default function Categories({ id }: Props) {
const [loading, setLoading] = useState(true)
const [categories, setCategories] = useState<
HttpTypes.StoreProductCategory[]
>([])
useEffect(() => {
if (!loading) {
return
}
sdk.store.category.list({
id,
fields: "category_children.id,category_children.name",
include_descendants_tree: true,
parent_category_id: null,
})
.then(({ product_categories }) => {
setCategories(product_categories)
setLoading(false)
})
}, [loading])
return (
<div>
{loading && <span>Loading...</span>}
{categories.map((category) => (
<>
<h1>{category.name}</h1>
<p>{category.description}</p>
{(category.category_children?.length || 0) > 0 && (
<>
<span>Child Categories</span>
<ul>
{category.category_children!.map(
(childCategory) => (
<li key={childCategory.id}>
{childCategory.name}
</li>
)
)}
</ul>
</>
)}
</>
))}
</div>
)
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const categoriesFetchHighlights = [
["3", `"category_children.id,category_children.name"`, "Select the fields of category children"],
["4", `include_descendants_tree`, "Indicate that all nested categories should be retrieved."],
["5", "parent_category_id", "Since each category will have its children, you only retrieve categories that don't have a parent."]
]
```ts highlights={categoriesFetchHighlights}
sdk.store.category.list({
id,
fields: "category_children.id,category_children.name",
include_descendants_tree: true,
parent_category_id: null,
})
.then(({ product_categories }) => {
// use the product category's children...
console.log(product_categories[0].category_children)
})
```
</CodeTab>
</CodeTabs>
In this example, you retrieve all categories as a hierarchy by passing the `include_descendants_tree` query parameter to the [List Product Categories API Route](!api!/store#product-categories_getproductcategories).
The response has a `product_categories` field, which is an array of [product category objects](!api!/store/#product-categories_productcategory_schema).
Each category will have a `category_children` field, which is an array of [product category objects](!api!/store/#product-categories_productcategory_schema).
You can then show the categories in a tree structure by iterating over the `product_categories` field and displaying the `category_children` field for each category.

View File

@@ -13,44 +13,26 @@ export const metadata = {
# {metadata.title}
In this guide, you'll learn how to retrieve a category's products in the storefront.
In your storefront, you may want to allow customers to browse products by category. To do so, you can retrieve the products that belong to a category and display them in your storefront.
To retrieve a category's products in the storefront, send a request to the [List Products API route](!api!/store#products_getproducts) passing it the `category_id` query parameter:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["3", "", "Pass the category ID as a query parameter."],
["9", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
]
```ts highlights={fetchHighlights}
const searchParams = new URLSearchParams({
// other query params...
"category_id[]": categoryId,
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ products, count }) => {
// use products...
console.log(products)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["31", "", "Pass the category ID as a query parameter."],
["29"], ["30"], ["31"],
["32", "category_id", "Pass the category ID as a query parameter."], ["33"],
["34"], ["35"], ["36"],
["37", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["38"], ["39"], ["40"], ["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"], ["48"], ["49"], ["50"], ["51"], ["52"],
["53"], ["54"]
["37"], ["38"], ["39"], ["40"], ["41"], ["42"], ["43"], ["44"]
]
```tsx highlights={highlights}
@@ -58,6 +40,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
categoryId: string
@@ -81,19 +64,11 @@ export const highlights = [
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
"category_id[]": categoryId,
sdk.store.product.list({
limit,
offset,
category_id: categoryId,
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ products: dataProducts, count }) => {
setProducts((prev) => {
if (prev.length > offset) {
@@ -139,7 +114,34 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["3", "category_id", "Pass the category ID as a query parameter."],
]
```ts highlights={fetchHighlights}
sdk.store.product.list({
// other query params...
category_id: categoryId,
})
.then(({ products, count }) => {
// use products...
console.log(products)
})
```
</CodeTab>
</CodeTabs>
In this example, you retrieve the products that belong to a category by passing the category's ID as a query parameter to the [List Products API route](!api!/store#products_getproducts). You can also pass other query parameters to filter or paginate the products, such as `limit` and `offset`.
The response has a `products` field, which is an array of [products](!api!/store#products_product_schema).
To learn more about displaying the products and their variants, refer to the following guides:
- [Show Products in Storefront](../../list/page.mdx)
- [Show Product Variant's Price](../../../products/price/page.mdx)
- [Show Product Variant's Inventory](../../../products/inventory/page.mdx)
- [Select Product Variants](../../../products/variants/page.mdx)

View File

@@ -13,44 +13,33 @@ export const metadata = {
# {metadata.title}
In this document, learn how to retrieve a product category and its details in the storefront.
In this guide, you'll learn how to retrieve a product category and its details in the storefront.
## How to Retrieve a Product Category in Storefront?
There are two ways to retrieve a product category:
- Retrieve a category by its ID.
- Retrieve a category by its `handle` field. This is useful if you're creating human-readable URLs in your storefront.
- [Retrieve a category by its ID](#retrieve-a-product-category-by-id). This method is straightforward and useful if you only have the category's ID.
- [Retrieve a category by its `handle` field](#retrieve-a-product-category-by-handle). This is useful if you're creating human-readable URLs in your storefront.
---
## Retrieve a Product Category by ID
To retrieve a product category by its ID, send a request to the [Get a Product Category API route](!api!/store#product-categories_getproductcategoriesid):
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["1", "id", "The product category's ID."],
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/product-categories/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product_category }) => {
// use the product...
console.log(product)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["21"], ["22"], ["23"], ["24"],
["25"], ["26"], ["27"], ["28"], ["29"], ["30"], ["31"]
["22"], ["23"], ["24"],
["25"], ["26"],
]
```tsx highlights={highlights}
@@ -58,6 +47,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
id: string
@@ -74,13 +64,7 @@ export const highlights = [
return
}
fetch(`http://localhost:9000/store/product-categories/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.category.retrieve(id)
.then(({ product_category }) => {
setCategory(product_category)
setLoading(false)
@@ -101,9 +85,26 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["1", "id", "The product category's ID."],
]
```ts highlights={fetchHighlights}
sdk.store.category.retrieve(id)
.then(({ product_category }) => {
// use the product...
console.log(product)
})
```
</CodeTab>
</CodeTabs>
In this example, you send a request to the [Get a Product Category API route](!api!/store#product-categories_getproductcategoriesid) with the category's ID.
The response has a `product_category` field, which is a [product category object](!api!/store/#product-categories_productcategory_schema).
---
@@ -113,39 +114,12 @@ The response has a `product_category` field, which is a [product category object
To retrieve a product by its handle, send a request to the [List Product Categories API route](!api!/store#product-categories_getproductcategories) passing it the `handle` query parameter:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const handleFetchHighlights = [
["1", "handle", "The product category's handle."],
]
```ts highlights={handleFetchHighlights}
fetch(`http://localhost:9000/store/product-categories?handle=${
handle
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product_categories }) => {
if (!product_categories.length) {
// product categories with the specified handle doesn't exist
return
}
// use the product category...
console.log(product_categories[0])
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const handleHighlights = [
["21"], ["22"], ["23"], ["24"],
["22"], ["23"], ["24"],
["25"], ["26"], ["27"], ["28"],
["29"], ["30"], ["31"], ["32"], ["33"], ["34"], ["35"]
["29"], ["30"],
]
```tsx highlights={handleHighlights}
@@ -153,6 +127,7 @@ export const handleHighlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
handle: string
@@ -169,15 +144,9 @@ export const handleHighlights = [
return
}
fetch(`http://localhost:9000/store/product-categories?handle=${
handle
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
sdk.store.category.list({
handle,
})
.then((res) => res.json())
.then(({ product_categories }) => {
if (product_categories.length) {
setCategory(product_categories[0])
@@ -203,5 +172,32 @@ export const handleHighlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const handleFetchHighlights = [
["2", "handle", "The product category's handle."],
]
```ts highlights={handleFetchHighlights}
sdk.store.category.list({
handle,
})
.then(({ product_categories }) => {
if (!product_categories.length) {
// product categories with the specified handle doesn't exist
return
}
// use the product category...
console.log(product_categories[0])
})
```
</CodeTab>
</CodeTabs>
In this example, you filter the product categories by their handle and retrieve the first product category that matches the handle. Since handles are unique, you can be sure that the product category you retrieve is the one you're looking for.
If no product category matches the handle, the product category doesn't exist, so you can show a 404 error or a custom message to the customer.
The product category in the response is a [product category object](!api!/store/#product-categories_productcategory_schema).

View File

@@ -13,36 +13,30 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how to list product collections in the storefront, including paginating and filtering them.
In this guide, you'll learn how to list product collections in the storefront, including paginating and filtering them.
<Note title="Good to know">
Product collections allow you to group products together for marketing or promotional purposes. For example, you can have a "Summer Clothes" collection grouping together all products that are suitable for the summer season. Then, you can display this collection in the summer to promote your summer collection.
</Note>
## List Product Collections
To retrieve the list of product collections, send a request to the [List Product Collections API route](!api!/store#collections_getcollections):
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
```ts
fetch(`http://localhost:9000/store/collections`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ collections }) => {
// use collections...
console.log(collections)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["17"], ["18"], ["19"], ["20"],
["21"], ["22"], ["23"], ["24"],
["25"], ["26"], ["27"]
["18"], ["19"], ["20"],
["21"], ["22"],
]
```tsx highlights={highlights}
@@ -50,6 +44,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
export default function Collections() {
const [loading, setLoading] = useState(true)
@@ -62,13 +57,7 @@ export const highlights = [
return
}
fetch(`http://localhost:9000/store/collections`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.collection.list()
.then(({ collections: dataCollections }) => {
setCollections(dataCollections)
setLoading(false)
@@ -93,16 +82,29 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
sdk.store.collection.list()
.then(({ collections }) => {
// use collections...
console.log(collections)
})
```
</CodeTab>
</CodeTabs>
In this example, you retrieve the list of product collections by sending a request to the [List Product Collections API route](!api!/store#collections_getcollections).
The response has a `collections` field, which is an array of [product collections](!api!/store#collections_getcollections).
---
## Paginate Product Collections
To paginate product collections, pass the following query parameters:
To paginate product collections, pass the following query parameters to the [List Product Collections API route](!api!/store#collections_getcollections):
- `limit`: The number of product collections to return in the request.
- `offset`: The number of product collections to skip before the returned product collections. You can calculate this by multiplying the current page with the limit.
@@ -112,11 +114,10 @@ The response object returns a `count` field, which is the total count of product
For example:
export const paginateHighlights = [
["20", "offset", "Calculate the number of product collections to skip based on the current page and limit."],
["28", "searchParams.toString()", "Pass the pagination parameters in the query."],
["36", "count", "The total number of product collections in the Medusa application."],
["48", "setHasMorePages", "Set whether there are more pages based on the total count."],
["67", "button", "Show a button to load more product collections if there are more pages."]
["21", "offset", "Calculate the number of product collections to skip based on the current page and limit."],
["27", "count", "The total number of product collections in the Medusa application."],
["39", "setHasMorePages", "Set whether there are more pages based on the total count."],
["58", "button", "Show a button to load more product collections if there are more pages."]
]
```tsx highlights={paginateHighlights}
@@ -124,6 +125,7 @@ export const paginateHighlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
export default function Collections() {
const [loading, setLoading] = useState(true)
@@ -141,20 +143,10 @@ export default function Collections() {
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
sdk.store.collection.list({
limit,
offset,
})
fetch(`http://localhost:9000/store/collections?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ collections: dataCollections, count }) => {
setCollections((prev) => {
if (prev.length > offset) {
@@ -201,31 +193,46 @@ export default function Collections() {
}
```
In this example, you paginate the product collections by passing the `limit` and `offset` query parameters to the [List Product Collections API route](!api!/store#collections_getcollections).
Then, you show a button to load more product collections if there are more pages.
---
## Filter Collections
The List Product Collections API route accepts query parameters to filter the collections by title, handle, and more.
The [List Product Collections API route](!api!/store#collections_getcollections) accepts query parameters to filter the collections by title, handle, and more.
Refer to the [API reference](!api!/store#collections_getcollections) for the list of accepted query parameters.
Refer to the [API reference](!api!/store#collections_getcollections) for the full list of accepted query parameters.
For example:
For example, to filter the collections by title:
```ts
const searchParams = new URLSearchParams({
title: "test",
sdk.store.collection.list({
title: "Summer Clothes",
})
.then(({ collections }) => {
// TODO set collections...
})
```
fetch(`http://localhost:9000/store/collections?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
In this example, only the collections with the title "Summer Clothes" are returned.
---
## Sort Collections
To sort collections by a field, use the `order` query parameter. Its value is a comma-separated list of fields to sort by, and each field is optionally prefixed by `-` to indicate descending order.
For example, to sort collections by title in descending order:
```ts
sdk.store.collection.list({
order: "-title",
})
.then((res) => res.json())
.then(({ collections, count }) => {
// TODO set collections...
})
```
The result will be collections sorted by title in descending order.

View File

@@ -13,44 +13,26 @@ export const metadata = {
# {metadata.title}
In this guide, you'll learn how to retrieve a collection's products in the storefront.
In your storefront, you may want to display a collection's products to the customers. To do so, you can retrieve the products that belong to a collection and display them in your storefront.
To retrieve a collection's products in the storefront, send a request to the [List Products API route](!api!/store#products_getproducts) passing it the `collection_id` query parameter:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["3", "", "Pass the collection ID as a query parameter."],
["9", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
]
```ts highlights={fetchHighlights}
const searchParams = new URLSearchParams({
// other query params...
"collection_id[]": collectionId,
})
fetch(`http://localhost:9000/store/products?${searchParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ products, count }) => {
// use products...
console.log(products)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["31", "collectionId", "Pass the collection ID as a query parameter."],
["29"], ["30"], ["31"],
["32", "collection_id", "Pass the collection ID as a query parameter."], ["33"],
["34"], ["35"], ["36"],
["37", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["38"], ["39"], ["40"], ["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"], ["48"], ["49"], ["50"], ["51"], ["52"],
["53"], ["54"], ["55"], ["56"]
["37"], ["38"], ["39"], ["40"], ["41"], ["42"], ["43"], ["44"], ["45"], ["46"], ["47"]
]
```tsx highlights={highlights}
@@ -58,6 +40,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
collectionId: string
@@ -81,21 +64,11 @@ export const highlights = [
const offset = (currentPage - 1) * limit
const searchParams = new URLSearchParams({
limit: `${limit}`,
offset: `${offset}`,
"collection_id[]": collectionId,
sdk.store.product.list({
limit,
offset,
collection_id: collectionId,
})
fetch(`http://localhost:9000/store/products?${
searchParams.toString()
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ products: dataProducts, count }) => {
setProducts((prev) => {
if (prev.length > offset) {
@@ -141,7 +114,23 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
```ts
sdk.store.product.list({
// other query params...
collection_id: collectionId,
})
.then(({ products, count }) => {
// use products...
console.log(products)
})
```
</CodeTab>
</CodeTabs>
In this example, you retrieve the products that belong to a collection by passing the collection's ID as a query parameter to the [List Products API route](!api!/store#products_getproducts). You can also pass other query parameters to filter or paginate the products, such as `limit` and `offset`.
The response has a `products` field, which is an array of [products](!api!/store#products_product_schema).

View File

@@ -13,45 +13,33 @@ export const metadata = {
# {metadata.title}
In this document, learn how to retrieve a product collection and its details in the storefront.
In this guide, you'll learn how to retrieve a product collection and its details in the storefront.
## How to Retrieve a Product Collection in Storefront?
There are two ways to retrieve a product collection:
- Retrieve a collection by its ID.
- Retrieve a collection by its `handle` field. This is useful if you're creating human-readable URLs in your storefront.
- [Retrieve a collection by its ID](#retrieve-a-product-collection-by-id). This method is straightforward and useful if you only have the collection's ID.
- [Retrieve a collection by its `handle` field](#retrieve-a-product-collection-by-handle). This is useful if you're creating human-readable URLs in your storefront.
---
## Retrieve a Product Collection by ID
To retrieve a product collection by its ID, send a request to the [Get a Collection API route](!api!/store#collections_getcollectionsid):
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../../js-sdk/page.mdx).
</Note>
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["1", "id", "The product collection's ID."],
]
```ts highlights={fetchHighlights}
fetch(`http://localhost:9000/store/collections/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ collection }) => {
// use the collection...
console.log(collection)
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const highlights = [
["21"], ["22"], ["23"], ["24"],
["25"], ["26"], ["27"], ["28"],
["29"], ["30"], ["31"],
["22"], ["23"], ["24"],
["25"], ["26"],
]
```tsx highlights={highlights}
@@ -59,6 +47,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
id: string
@@ -75,13 +64,7 @@ export const highlights = [
return
}
fetch(`http://localhost:9000/store/collections/${id}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
sdk.store.collection.retrieve(id)
.then(({ collection: dataCollection }) => {
setCollection(dataCollection)
setLoading(false)
@@ -101,9 +84,26 @@ export const highlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const fetchHighlights = [
["1", "id", "The product collection's ID."],
]
```ts highlights={fetchHighlights}
sdk.store.collection.retrieve(id)
.then(({ collection }) => {
// use the collection...
console.log(collection)
})
```
</CodeTab>
</CodeTabs>
In this example, you retrieve the product collection by sending a request to the [Get a Collection API route](!api!/store#collections_getcollectionsid).
The response has a `collection` field, which is a [product collection object](!api!/store#collections_collection_schema).
---
@@ -113,39 +113,11 @@ The response has a `collection` field, which is a [product collection object](!a
To retrieve a product by its handle, send a request to the [List Product Collections API route](!api!/store#collections_getcollections) passing it the `handle` query parameter:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const handleFetchHighlights = [
["2", "handle", "The collection's handle."],
]
```ts highlights={handleFetchHighlights}
fetch(`http://localhost:9000/store/collections?handle=${
handle
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ collections }) => {
if (!collections.length) {
// collections with the specified handle doesn't exist
return
}
// use the collection...
console.log(collections[0])
})
```
</CodeTab>
<CodeTab label="React" value="react">
export const handleHighlights = [
["23"], ["24"], ["25"], ["26"],
["27"], ["28"], ["29"], ["30"], ["31"], ["32"], ["33"], ["34"],
["35"], ["36"], ["37"]
["24"], ["25"], ["26"],
["27"], ["28"], ["29"], ["30"], ["31"], ["32"],
]
```tsx highlights={handleHighlights}
@@ -153,6 +125,7 @@ export const handleHighlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
handle: string
@@ -171,15 +144,9 @@ export const handleHighlights = [
return
}
fetch(`http://localhost:9000/store/collections?handle=${
handle
}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
sdk.store.collection.list({
handle,
})
.then((res) => res.json())
.then(({ collections }) => {
if (collections.length) {
setCollection(collections[0])
@@ -204,5 +171,33 @@ export const handleHighlights = [
}
```
</CodeTab>
<CodeTab label="JS SDK" value="js-sdk">
export const handleFetchHighlights = [
["2", "handle", "The collection's handle."],
]
```ts highlights={handleFetchHighlights}
sdk.store.collection.list({
handle,
})
.then(({ collections }) => {
if (!collections.length) {
// collections with the specified handle doesn't exist
return
}
// use the collection...
console.log(collections[0])
})
```
</CodeTab>
</CodeTabs>
In this example, you retrieve the product collection by sending a request to the [List Product Collections API route](!api!/store#collections_getcollections) passing it the `handle` query parameter.
Since handles are unique, you can be sure that the product collection you retrieve is the one you're looking for. If no product collection matches the handle, you can show a 404 error or a custom message to the customer.
The product collection in the response is a [product collection object](!api!/store#collections_collection_schema).

View File

@@ -13,31 +13,34 @@ export const metadata = {
# {metadata.title}
In this guide, you'll learn how to retrieve a product variant's inventory quantity in a storefront.
## How to Retrieve a Product Variant's Inventory Quantity?
To retrieve variants' inventory quantity using either the [List Products](!api!/store#products_getproducts) or [Retrieve Products](!api!/store#products_getproductsid) API routes:
1. Pass the publishable API key in the header of the request. The retrieved inventory quantity is in the locations associated with the key's sales channels.
2. Pass in the `fields` query parameter the value `+variants.inventory_quantity`.
1. Pass in the `fields` query parameter the value `+variants.inventory_quantity`.
- When also retrieving prices, make sure to include `*variants.calculated_price` in the beginning of the list of fields. For example, `?fields=*variants.calculated_price,+variants.inventory_quantity`.
2. Pass the publishable API key in the header of the request, which you always do when sending a request to the Store API. The inventory quantity is retrieved based on the stock locations of the sales channels that belong to the API key's scope.
- If you're using the JS SDK, the publishable API key is automatically passed in the header of the requests as explained in the [Publishable API Keys](../../publishable-api-keys/page.mdx) guide.
For example:
<Note type="warning" title="Important">
If you're also passing `*variants.calculated_price` in `fields` to get the product variants' prices, make sure to include it in the beginning of the list of fields. For example, `?fields=*variants.calculated_price,+variants.inventory_quantity`.
</Note>
export const fetchHighlights = [
["2", "fields", "Pass `+variants.inventory_quantity` in the fields to retrieve."],
["8", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to retrieve the inventory quantity based on the associated sales channels' stock locations."],
["14", "isInStock", "Consider the variant in stock either if its `manage_inventory` property is disabled, or the `inventory_quantity` is greater than `0`."]
["6", "isInStock", "Consider the variant in stock either if its `manage_inventory` property is disabled, or the `inventory_quantity` is greater than `0`."]
]
```ts highlights={fetchHighlights}
const queryParams = new URLSearchParams({
sdk.store.product.retrieve(id, {
fields: `*variants.calculated_price,+variants.inventory_quantity`,
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product }) => {
product.variants?.forEach((variant) => {
const isInStock = variant.manage_inventory === false ||
@@ -48,11 +51,7 @@ fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
})
```
<Note type="warning" title="Important">
If you're also passing `*variants.calculated_price` in `fields` to get the product variants' prices, make sure to include it in the beginning of the list of fields. For example, `?fields=*variants.calculated_price,+variants.inventory_quantity`.
</Note>
In this example, you retrieve the product variants' inventory quantity by passing `+variants.inventory_quantity` in the `fields` query parameter. This will add a new `inventory_quantity` field to each variant object.
### When is a Variant in Stock?
@@ -65,11 +64,20 @@ A variant is in stock if:
## Full React Example
For example, to show on a product's page whether a variant is in stock in a React-based storefront:
<Note title="Tip">
Learn how to install and configure the JS SDK in the [JS SDK documentation](../../../js-sdk/page.mdx).
</Note>
export const reactHighlights = [
["23", "fields", "Pass `+variants.inventory_quantity` in the fields to retrieve."],
["29", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to retrieve the inventory quantity based on the associated sales channels' stock locations."],
["53", "isInStock", "Consider the selected variant in stock either if its `manage_inventory` property is disabled, or the `inventory_quantity` is greater than `0`."],
["94", "isInStock", "Show whether the selected variant is in stock."]
["23", "retrieve", "Retrieve the product with the inventory quantity."],
["24", "fields", "Pass `+variants.inventory_quantity` in the fields to retrieve."],
["32", "selectedVariant", "Find the selected variant."],
["46", "isInStock", "Consider the selected variant in stock either if its `manage_inventory` property is disabled, or the `inventory_quantity` is greater than `0`."],
["88", "isInStock", "Show whether the selected variant is in stock."]
]
```tsx title="React Storefront" highlights={reactHighlights}
@@ -77,6 +85,7 @@ export const reactHighlights = [
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"
type Props = {
id: string
@@ -94,17 +103,9 @@ export default function Product({ id }: Props) {
return
}
const queryParams = new URLSearchParams({
fields: `+variants.inventory_quantity`,
sdk.store.product.retrieve(id, {
fields: `*variants.calculated_price,+variants.inventory_quantity`,
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "temp",
},
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
@@ -130,7 +131,8 @@ export default function Product({ id }: Props) {
return undefined
}
return selectedVariant.manage_inventory === false || selectedVariant.inventory_quantity > 0
return selectedVariant.manage_inventory === false ||
(selectedVariant.inventory_quantity || 0) > 0
}, [selectedVariant])
return (
@@ -179,4 +181,12 @@ export default function Product({ id }: Props) {
}
```
In this example, you show whether the selected variant is in or out of stock.
In this example, you retrieve the product variants' inventory quantity by passing `+variants.inventory_quantity` in the `fields` query parameter. This will add a new `inventory_quantity` field to each variant object.
Then, you find the selected variant and show whether it's in stock. The variant is in stock if its `manage_inventory` property is disabled, or the `inventory_quantity` is greater than `0`.
<Note title="Tip">
Refer to the [Select Product Variants](../variants/page.mdx) guide to learn more about selecting a product variant.
</Note>

View File

@@ -101,11 +101,10 @@ The response object returns a `count` field, which is the total count of product
For example:
export const paginateHighlights = [
["20", "offset", "Calculate the number of products to skip based on the current page and limit."],
["27", "searchParams.toString()", "Pass the pagination parameters in the query."],
["34", "count", "The total number of products in the Medusa application."],
["45", "setHasMorePages", "Set whether there are more pages based on the total count."],
["62", "button", "Show a button to load more products if there are more pages."]
["21", "offset", "Calculate the number of products to skip based on the current page and limit."],
["27", "count", "The total number of products in the Medusa application."],
["38", "setHasMorePages", "Set whether there are more pages based on the total count."],
["55", "button", "Show a button to load more products if there are more pages."]
]
```tsx highlights={paginateHighlights}
@@ -113,7 +112,7 @@ export const paginateHighlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "../../lib/sdk"
import { sdk } from "@/lib/sdk"
export default function Products() {
const [loading, setLoading] = useState(true)

View File

@@ -100,8 +100,8 @@ export const saleHighlights = [
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
import { sdk } from "../../lib/sdk"
import { useRegion } from "@/providers/region"
import { sdk } from "@/lib/sdk"
type Props = {
id: string

View File

@@ -98,8 +98,8 @@ export const priceHighlights = [
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
import { sdk } from "../../lib/sdk"
import { useRegion } from "@/providers/region"
import { sdk } from "@/lib/sdk"
type Props = {
id: string

View File

@@ -124,8 +124,8 @@ export const taxHighlight = [
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
import { sdk } from "../../lib/sdk"
import { useRegion } from "@/providers/region"
import { sdk } from "@/lib/sdk"
type Props = {
id: string

View File

@@ -14,10 +14,14 @@ export const metadata = {
In this guide, you'll learn how to retrieve a product and its details in the storefront.
## How to Retrieve a Product in Storefront?
There are two ways to retrieve a product:
- Retrieve a product by its ID. This method is straightforward and useful when you only have access to the product's ID.
- Retrieve a product by its `handle` field. This is useful if you're creating human-readable URLs in your storefront.
- [Retrieve a product by its ID](#retrieve-a-product-by-id). This method is straightforward and useful when you only have access to the product's ID.
- [Retrieve a product by its `handle` field](#retrieve-a-product-by-handle). This is useful if you're creating human-readable URLs in your storefront.
---
## Retrieve a Product by ID
@@ -33,9 +37,8 @@ Learn how to install and configure the JS SDK in the [JS SDK documentation](../.
<CodeTab label="React" value="react">
export const highlights = [
["21"], ["22"], ["23"],
["24", "process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "Pass the Publishable API key to retrieve products of associated sales channel(s)."],
["25"], ["26"], ["27"], ["28"], ["29"], ["30"], ["31"]
["22"], ["23"],
["24"], ["25"], ["26"], ["27"],
]
```tsx highlights={highlights}
@@ -43,7 +46,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "../../lib/sdk"
import { sdk } from "@/lib/sdk"
type Props = {
id: string
@@ -141,7 +144,7 @@ export const handleHighlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "../../../lib/sdk"
import { sdk } from "@/lib/sdk"
type Params = {
params: {
@@ -206,7 +209,7 @@ export const handleHighlights = [
<CodeTab label="JS SDK" value="js-sdk">
export const handleFetchHighlights = [
["1", "handle", "The product's handle."],
["2", "handle", "The product's handle."],
]
```ts highlights={handleFetchHighlights}

View File

@@ -54,7 +54,7 @@ export const highlights = [
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "../../../lib/sdk"
import { sdk } from "@/lib/sdk"
type Props = {
id: string

View File

@@ -14,6 +14,8 @@ export const metadata = {
In this guide, you'll learn how to create a region context in your React storefront.
## Why Create a Region Context?
Throughout your storefront, you'll need to access the customer's selected region to perform different actions, such as retrieve product's prices in the selected region.
<Note title="Tip">
@@ -24,6 +26,8 @@ To learn how to allow customers to select their region, refer to the [Store Sele
So, if your storefront is React-based, create a region context and add it at the top of your components tree. Then, you can access the selected region anywhere in your storefront.
---
## Create Region Context Provider
For example, create the following file that exports a `RegionProvider` component and a `useRegion` hook:
@@ -55,7 +59,7 @@ import {
useState,
} from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "../lib/sdk"
import { sdk } from "@/lib/sdk"
type RegionContextType = {
region?: HttpTypes.StoreRegion
@@ -136,8 +140,8 @@ For example, if you're using Next.js, add it to the `app/layout.tsx` or `src/app
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { CartProvider } from "../providers/cart"
import { RegionProvider } from "../providers/region"
import { CartProvider } from "@/providers/cart"
import { RegionProvider } from "@/providers/region"
const inter = Inter({ subsets: ["latin"] })
@@ -173,7 +177,7 @@ For example:
```tsx
"use client" // include with Next.js 13+
// ...
import { useRegion } from "../providers/region"
import { useRegion } from "@/providers/region"
export default function Products() {
const { region } = useRegion()

View File

@@ -40,7 +40,7 @@ export const highlights = [
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "../../lib/sdk"
import { sdk } from "@/lib/sdk"
export default function Regions() {
const [loading, setLoading] = useState(true)

View File

@@ -12,9 +12,11 @@ export const metadata = {
In this guide, you'll learn how to store a customer's selected region in your storefront, then retrieve it for later use.
## Why Store and Retrieve Region?
## Why Select and Store Region?
In your store, prices, taxes, and available payment methods can vary between regions. So, you should allow customers to select their region to see the correct prices, taxes, and payment methods.
In your store, prices, taxes, and available payment methods can vary between regions.
So, you should allow customers to select their region to see the correct prices, taxes, and payment methods.
---
@@ -61,4 +63,8 @@ sdk.store.region.retrieve(regionId)
The response has a `regions` field, which is an array of [regions](!api!/store#regions_region_schema).
---
## Store Region Details in React Context
If you're using React, it's then recommended to create a context that stores the region details and make it available to all components in your application, as explained in the [Region React Context in Storefront](../context/page.mdx) guide.

View File

@@ -134,50 +134,50 @@ export const generatedEditDates = {
"app/service-factory-reference/methods/update/page.mdx": "2024-07-31T17:01:33+03:00",
"app/service-factory-reference/tips/filtering/page.mdx": "2025-03-21T08:32:39.125Z",
"app/service-factory-reference/page.mdx": "2024-07-26T14:40:56+00:00",
"app/storefront-development/cart/context/page.mdx": "2025-01-06T16:00:34.296Z",
"app/storefront-development/cart/create/page.mdx": "2025-02-26T11:44:58.922Z",
"app/storefront-development/cart/manage-items/page.mdx": "2024-12-26T15:59:48.445Z",
"app/storefront-development/cart/retrieve/page.mdx": "2025-02-26T11:45:30.920Z",
"app/storefront-development/cart/update/page.mdx": "2025-01-06T16:01:33.752Z",
"app/storefront-development/cart/context/page.mdx": "2025-03-27T14:47:14.258Z",
"app/storefront-development/cart/create/page.mdx": "2025-03-27T14:46:51.473Z",
"app/storefront-development/cart/manage-items/page.mdx": "2025-03-26T15:54:31.446Z",
"app/storefront-development/cart/retrieve/page.mdx": "2025-03-27T14:46:51.473Z",
"app/storefront-development/cart/update/page.mdx": "2025-03-27T07:11:57.749Z",
"app/storefront-development/cart/page.mdx": "2024-06-11T11:56:37+03:00",
"app/storefront-development/checkout/address/page.mdx": "2025-02-03T16:32:02.682Z",
"app/storefront-development/checkout/complete-cart/page.mdx": "2024-12-19T16:30:41.019Z",
"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": "2025-01-02T08:47:20.531Z",
"app/storefront-development/checkout/shipping/page.mdx": "2025-01-13T11:31:35.361Z",
"app/storefront-development/checkout/address/page.mdx": "2025-03-27T14:47:14.265Z",
"app/storefront-development/checkout/complete-cart/page.mdx": "2025-03-27T14:47:14.277Z",
"app/storefront-development/checkout/email/page.mdx": "2025-03-27T14:47:14.283Z",
"app/storefront-development/checkout/payment/stripe/page.mdx": "2025-03-27T14:47:14.276Z",
"app/storefront-development/checkout/payment/page.mdx": "2025-03-27T14:47:14.274Z",
"app/storefront-development/checkout/shipping/page.mdx": "2025-03-27T14:47:14.270Z",
"app/storefront-development/checkout/page.mdx": "2024-06-12T19:46:06+02:00",
"app/storefront-development/customers/addresses/page.mdx": "2025-02-26T11:46:18.629Z",
"app/storefront-development/customers/context/page.mdx": "2024-12-19T16:38:43.703Z",
"app/storefront-development/customers/log-out/page.mdx": "2024-12-19T16:31:28.347Z",
"app/storefront-development/customers/login/page.mdx": "2024-12-19T16:31:34.194Z",
"app/storefront-development/customers/profile/page.mdx": "2024-12-19T16:31:43.978Z",
"app/storefront-development/customers/register/page.mdx": "2025-01-13T11:31:35.362Z",
"app/storefront-development/customers/retrieve/page.mdx": "2025-01-06T16:07:12.542Z",
"app/storefront-development/customers/addresses/page.mdx": "2025-03-27T14:47:14.252Z",
"app/storefront-development/customers/context/page.mdx": "2025-03-27T14:47:14.248Z",
"app/storefront-development/customers/log-out/page.mdx": "2025-03-27T14:45:23.360Z",
"app/storefront-development/customers/login/page.mdx": "2025-03-27T14:46:51.419Z",
"app/storefront-development/customers/profile/page.mdx": "2025-03-27T14:47:14.251Z",
"app/storefront-development/customers/register/page.mdx": "2025-03-27T14:46:51.415Z",
"app/storefront-development/customers/retrieve/page.mdx": "2025-03-27T14:41:39.996Z",
"app/storefront-development/customers/page.mdx": "2024-06-13T12:21:54+03:00",
"app/storefront-development/products/categories/list/page.mdx": "2024-12-19T16:33:06.547Z",
"app/storefront-development/products/categories/nested-categories/page.mdx": "2025-01-06T15:50:56.324Z",
"app/storefront-development/products/categories/products/page.mdx": "2025-02-26T11:46:56.674Z",
"app/storefront-development/products/categories/retrieve/page.mdx": "2025-01-06T15:44:45.608Z",
"app/storefront-development/products/categories/list/page.mdx": "2025-03-27T14:46:51.437Z",
"app/storefront-development/products/categories/nested-categories/page.mdx": "2025-03-27T14:46:51.437Z",
"app/storefront-development/products/categories/products/page.mdx": "2025-03-27T14:46:51.450Z",
"app/storefront-development/products/categories/retrieve/page.mdx": "2025-03-27T14:46:51.450Z",
"app/storefront-development/products/categories/page.mdx": "2024-06-11T19:55:56+02:00",
"app/storefront-development/products/collections/list/page.mdx": "2025-02-26T11:47:22.010Z",
"app/storefront-development/products/collections/products/page.mdx": "2025-02-26T11:47:46.882Z",
"app/storefront-development/products/collections/retrieve/page.mdx": "2025-02-26T11:48:08.506Z",
"app/storefront-development/products/collections/list/page.mdx": "2025-03-27T14:46:51.455Z",
"app/storefront-development/products/collections/products/page.mdx": "2025-03-27T14:46:51.466Z",
"app/storefront-development/products/collections/retrieve/page.mdx": "2025-03-27T14:46:51.458Z",
"app/storefront-development/products/collections/page.mdx": "2024-06-11T19:55:56+02:00",
"app/storefront-development/products/list/page.mdx": "2025-03-26T11:27:42.966Z",
"app/storefront-development/products/price/examples/sale-price/page.mdx": "2025-03-26T12:06:29.002Z",
"app/storefront-development/products/price/examples/show-price/page.mdx": "2025-03-26T12:04:39.304Z",
"app/storefront-development/products/price/examples/tax-price/page.mdx": "2025-03-26T12:16:45.294Z",
"app/storefront-development/products/list/page.mdx": "2025-03-27T14:46:51.431Z",
"app/storefront-development/products/price/examples/sale-price/page.mdx": "2025-03-27T14:47:14.308Z",
"app/storefront-development/products/price/examples/show-price/page.mdx": "2025-03-27T14:47:14.292Z",
"app/storefront-development/products/price/examples/tax-price/page.mdx": "2025-03-27T14:47:14.292Z",
"app/storefront-development/products/price/page.mdx": "2025-03-26T12:08:16.029Z",
"app/storefront-development/products/retrieve/page.mdx": "2025-03-26T11:27:19.695Z",
"app/storefront-development/products/variants/page.mdx": "2025-03-26T11:27:49.917Z",
"app/storefront-development/products/retrieve/page.mdx": "2025-03-27T14:46:51.433Z",
"app/storefront-development/products/variants/page.mdx": "2025-03-27T14:46:51.576Z",
"app/storefront-development/products/page.mdx": "2024-06-11T19:55:56+02:00",
"app/storefront-development/regions/context/page.mdx": "2025-03-26T10:53:16.917Z",
"app/storefront-development/regions/list/page.mdx": "2025-03-26T10:51:33.951Z",
"app/storefront-development/regions/store-retrieve-region/page.mdx": "2025-03-26T10:51:27.384Z",
"app/storefront-development/regions/context/page.mdx": "2025-03-27T14:47:14.286Z",
"app/storefront-development/regions/list/page.mdx": "2025-03-27T14:46:52.029Z",
"app/storefront-development/regions/store-retrieve-region/page.mdx": "2025-03-27T13:29:54.041Z",
"app/storefront-development/regions/page.mdx": "2024-06-09T15:19:09+02:00",
"app/storefront-development/tips/page.mdx": "2025-03-26T10:31:43.816Z",
"app/storefront-development/page.mdx": "2025-03-26T10:31:43.815Z",
"app/storefront-development/page.mdx": "2025-03-27T13:28:22.077Z",
"app/troubleshooting/cors-errors/page.mdx": "2024-05-03T17:36:38+03:00",
"app/troubleshooting/create-medusa-app-errors/page.mdx": "2024-07-11T10:29:13+03:00",
"app/troubleshooting/database-errors/page.mdx": "2024-05-03T17:36:38+03:00",
@@ -838,7 +838,7 @@ export const generatedEditDates = {
"references/types/interfaces/types.BaseClaim/page.mdx": "2025-02-24T10:48:36.876Z",
"app/commerce-modules/auth/auth-providers/github/page.mdx": "2025-01-13T11:31:35.361Z",
"app/commerce-modules/auth/auth-providers/google/page.mdx": "2025-01-13T11:31:35.361Z",
"app/storefront-development/customers/third-party-login/page.mdx": "2025-01-13T11:31:35.362Z",
"app/storefront-development/customers/third-party-login/page.mdx": "2025-03-27T14:46:51.417Z",
"references/types/HttpTypes/types/types.HttpTypes.AdminWorkflowRunResponse/page.mdx": "2024-12-09T13:21:34.761Z",
"references/types/HttpTypes/types/types.HttpTypes.BatchResponse/page.mdx": "2024-12-09T13:21:33.549Z",
"references/types/WorkflowsSdkTypes/types/types.WorkflowsSdkTypes.Acknowledgement/page.mdx": "2024-12-09T13:21:35.873Z",
@@ -884,7 +884,7 @@ export const generatedEditDates = {
"references/promotion/interfaces/promotion.IPromotionModuleService/page.mdx": "2024-11-25T17:49:58.612Z",
"references/types/EventBusTypes/interfaces/types.EventBusTypes.IEventBusService/page.mdx": "2024-12-09T13:21:33.073Z",
"references/types/TransactionBaseTypes/interfaces/types.TransactionBaseTypes.ITransactionBaseService/page.mdx": "2024-09-06T00:11:08.494Z",
"app/storefront-development/products/inventory/page.mdx": "2025-01-06T15:42:09.252Z",
"app/storefront-development/products/inventory/page.mdx": "2025-03-27T14:46:51.435Z",
"references/auth/IAuthModuleService/methods/auth.IAuthModuleService.updateAuthIdentities/page.mdx": "2024-12-09T13:21:36.269Z",
"references/auth/IAuthModuleService/methods/auth.IAuthModuleService.updateProvider/page.mdx": "2024-12-09T13:21:36.245Z",
"references/auth/IAuthModuleService/methods/auth.IAuthModuleService.updateProviderIdentities/page.mdx": "2024-12-09T13:21:36.289Z",
@@ -2106,7 +2106,7 @@ export const generatedEditDates = {
"app/admin-components/layouts/two-column/page.mdx": "2024-10-07T11:16:10.092Z",
"app/admin-components/components/forms/page.mdx": "2024-10-09T12:48:04.229Z",
"app/commerce-modules/auth/reset-password/page.mdx": "2025-02-26T11:18:00.391Z",
"app/storefront-development/customers/reset-password/page.mdx": "2025-03-04T09:15:25.662Z",
"app/storefront-development/customers/reset-password/page.mdx": "2025-03-27T14:46:51.424Z",
"app/commerce-modules/api-key/links-to-other-modules/page.mdx": "2025-03-14T14:33:38.886Z",
"app/commerce-modules/cart/extend/page.mdx": "2024-12-25T12:48:59.149Z",
"app/commerce-modules/cart/links-to-other-modules/page.mdx": "2025-03-14T14:33:26.754Z",
@@ -5749,7 +5749,7 @@ export const generatedEditDates = {
"references/types/StockLocationTypes/interfaces/types.StockLocationTypes.FilterableStockLocationAddressProps/page.mdx": "2025-01-13T18:05:55.410Z",
"references/types/StockLocationTypes/types/types.StockLocationTypes.UpdateStockLocationAddressInput/page.mdx": "2025-01-07T12:54:23.057Z",
"references/types/StockLocationTypes/types/types.StockLocationTypes.UpsertStockLocationAddressInput/page.mdx": "2025-01-07T12:54:23.058Z",
"app/storefront-development/guides/express-checkout/page.mdx": "2025-03-11T08:56:39.078Z",
"app/storefront-development/guides/express-checkout/page.mdx": "2025-03-27T14:47:14.309Z",
"app/commerce-modules/inventory/inventory-kit/page.mdx": "2025-02-26T11:18:00.353Z",
"app/commerce-modules/api-key/workflows/page.mdx": "2025-01-09T13:41:46.573Z",
"app/commerce-modules/api-key/js-sdk/page.mdx": "2025-01-09T12:04:39.787Z",
@@ -6058,8 +6058,8 @@ export const generatedEditDates = {
"references/modules/notification_service/page.mdx": "2025-03-17T15:24:05.164Z",
"references/notification_service/interfaces/notification_service.INotificationModuleService/page.mdx": "2025-03-17T15:24:05.173Z",
"app/nextjs-starter/guides/revalidate-cache/page.mdx": "2025-03-18T08:47:59.628Z",
"app/storefront-development/cart/totals/page.mdx": "2025-03-18T09:20:59.533Z",
"app/storefront-development/checkout/order-confirmation/page.mdx": "2025-03-18T09:44:14.561Z",
"app/storefront-development/cart/totals/page.mdx": "2025-03-27T14:47:14.252Z",
"app/storefront-development/checkout/order-confirmation/page.mdx": "2025-03-27T14:29:45.669Z",
"app/how-to-tutorials/tutorials/product-reviews/page.mdx": "2025-03-19T13:00:56.901Z",
"app/troubleshooting/api-routes/page.mdx": "2025-03-21T07:17:56.248Z",
"app/troubleshooting/data-models/default-fields/page.mdx": "2025-03-21T06:59:06.775Z",