docs: added cart storefront guides (#7662)

* docs: added cart storefront guides

* add context guides

* small fixes to the context
This commit is contained in:
Shahed Nasser
2024-06-11 11:56:37 +03:00
committed by GitHub
parent 37426939da
commit f3bf8c73a3
11 changed files with 969 additions and 1 deletions
@@ -0,0 +1,180 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Create Cart Context in Storefront`,
}
# {metadata.title}
Throughout your storefront, you'll need to access the customer's cart to perform different actions.
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:
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."],
["25", "CartProvider", "The provider component to use in your component tree."],
["31", "useRegion", "Use the `useRegion` hook defined in the Region Context guide."],
["36", "setItem", "Set the cart's ID in `localStorage` in case it changed."],
["44", "fetch", "If the customer doesn't have a cart, create a new one."],
["48", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to associate the correct sales channel(s)."],
["62", "fetch", "Retrieve the customer's cart."],
["82", "useCart", "The hook that child components of the provider use to access the cart."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import {
createContext,
useContext,
useEffect,
useState
} from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "./region"
type CartContextType = {
cart?: HttpTypes.StoreCart
setCart: React.Dispatch<
React.SetStateAction<HttpTypes.StoreCart | undefined>
>
}
const CartContext = createContext<CartContextType | null>(null)
type CartProviderProps = {
children: React.ReactNode
}
export const CartProvider = ({ children }: CartProviderProps) => {
const [cart, setCart] = useState<
HttpTypes.StoreCart
>()
const { region } = useRegion()
useEffect(() => {
if (cart || !region) {
return
}
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_PAK || "temp",
"Content-Type": "application/json"
},
body: JSON.stringify({
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"
})
.then((res) => res.json())
.then(({ cart: dataCart }) => {
setCart(dataCart)
})
}
}, [cart, region])
return (
<CartContext.Provider value={{
cart,
setCart
}}>
{children}
</CartContext.Provider>
)
}
export const useCart = () => {
const context = useContext(CartContext)
if (!context) {
throw new Error("useCart must be used within a CartProvider")
}
return context
}
```
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 `useCart` hook returns the value of the `CartContext`. Child components of `CartProvider` use this hook to access `cart` or `setCart`.
---
## Use CartProvider in Component Tree
To use the cart context's value, add the `CartProvider` high in your component tree.
For example, if you're using Next.js, add it to the `app/layout.tsx` or `src/app/layout.tsx` file:
```tsx title="app/layout.tsx" collapsibleLines="1-14" highlights={[["23"]]}
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { CartProvider } from "../providers/cart"
import { RegionProvider } from "../providers/region"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<RegionProvider>
<CartProvider>
{/* Other providers... */}
{children}
</CartProvider>
</RegionProvider>
</body>
</html>
)
}
```
---
## Use useCart Hook
Now, you can use the `useCart` hook in child components of `CartProvider`.
For example:
```tsx
"use client" // include with Next.js 13+
// ...
import { useCart } from "../providers/cart"
export default function Products() {
const { cart } = useCart()
// ...
}
```
@@ -0,0 +1,140 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Create Cart in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to create and store a cart.
## 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`.
To create a cart, send a request to the [Create Cart API route](!api!/store#carts_postcarts).
For example:
<CodeTabs group="store-request">
<CodeTab label="Fetch API" value="fetch">
export const fetchHighlights = [
["5", "process.env.NEXT_PUBLIC_PAK", "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_PAK || "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."],
["21", "", "Send a request to create the cart."],
["26", "process.env.NEXT_PUBLIC_PAK", "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`."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react";
// other imports...
export default function Home() {
// TODO assuming you have the region retrieved
const region = {
// ...
}
// ...
useEffect(() => {
const cartId = localStorage.getItem("cart_id")
if (cartId) {
// customer already has a cart created
return
}
// 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_PAK || "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>
</CodeTabs>
{/* TODO add a link to cart object in API reference (once available). */}
The response of the Create Cart API route has a `cart` field, which is a cart object.
Refer to the [Create Cart API reference](!api!/store#carts_postcarts) for details on other available request parameters.
### Publishable API Key
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.
This is necessary, as only products matching the cart's sales channel(s) can be added to the cart.
---
## Associate Customer with Cart
When creating the cart, you can associate the logged-in customer's ID with the cart by passing a `customer_id` request body parameter.
For example:
export const customerHighlights = [
["5", "customer.id", "Assuming you have the customer object."]
]
```ts highlights={customerHighlights}
fetch(`http://localhost:9000/store/carts`, {
// ...
body: JSON.stringify({
// ...
customer_id: customer.id
})
})
.then((res) => res.json())
.then(({ cart }) => {
localStorage.setItem("cart_id", cart.id)
})
```
@@ -0,0 +1,150 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Manage Cart's Items in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to manage a cart's line items, including adding, updating, and removing them.
## Add Product Variant to Cart
{/* TODO add section on checking variant quantity once it's fixed in v2. */}
To add a product variant to a cart, use the [Add Line Item API route](!api!/store#carts_postcartsidlineitems).
For example:
export const addHighlights = [
["1", "variant_id", "The ID of the selected variant."],
["2", "cartId", "Retrieve the cart ID from the `localStorage`."],
["16", "quantity", "You can also allow customers to specify the quantity."]
]
```ts highlights={addHighlights}
const addToCart = (variant_id: string) => {
const cartId = localStorage.getItem("cart_id")
if (!cartId) {
return
}
fetch(`http://localhost:9000/store/carts/${cartId}/line-items`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
variant_id,
quantity: 1,
})
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart
console.log(cart)
alert("Product added to cart")
})
}
```
The Add Line Item API route 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.
---
## Update Line Item in Cart
You can update the quantity of a line item in the cart using the [Update Line Item API route](!api!/store#carts_postcartsidlineitemsline_id).
For example:
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."],
]
```ts highlights={updateHighlights}
const updateQuantity = (
itemId: string,
quantity: number
) => {
const cartId = localStorage.getItem("cart_id")
if (!cartId) {
return
}
fetch(`http://localhost:9000/store/carts/${cartId}/line-items/${
itemId
}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
quantity
})
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart
console.log(cart)
})
}
```
The Update Line Item API route 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.
---
## Remove Line Item from Cart
To remove a line item from the cart, send a request to the [Remove Line Item API route](!api!/store#carts_deletecartsidlineitemsline_id).
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."],
["15", "parent", "The updated cart is returned as the `parent` field."]
]
```ts highlights={deleteHighlights}
const removeItem = (itemId: string) => {
const cartId = localStorage.getItem("cart_id")
if (!cartId) {
return
}
fetch(`http://localhost:9000/store/carts/${cartId}/line-items/${
itemId
}`, {
credentials: "include",
method: "DELETE",
})
.then((res) => res.json())
.then(({ parent: cart }) => {
// use cart
console.log(cart)
})
}
```
The Delete Line Item API route returns the updated cart object as the `parent` field.
@@ -0,0 +1,13 @@
import { ChildDocs } from "docs-ui"
export const metadata = {
title: `Carts in Storefront`,
}
# {metadata.title}
A cart holds the items that the customer wants to purchase.
Using Medusa's Store APIs, you can create a cart for the customer and allow them to add, update, and remove items from the cart.
<ChildDocs type="item" onlyTopLevel={true} />
@@ -0,0 +1,129 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Retrieve Cart in Storefront`,
}
# {metadata.title}
You can retrieve a cart by sending a request to the [Get a Cart API route](!api!/store#carts_getcartsid).
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.
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"
})
.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"],
["31", "formatPrice", "This function was previously created to format product prices. You can re-use the same function."],
["34", "currency_code", "If you reuse the `formatPrice` function, pass the currency code as a parameter."],
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
export default function Cart () {
const [cart, setCart] = useState<
HttpTypes.StoreCart
>()
useEffect(() => {
if (cart) {
return
}
const cartId = localStorage.getItem("cart_id")
if (!cartId) {
// TODO create cart
return
}
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include"
})
.then((res) => res.json())
.then(({ cart: dataCart }) => {
setCart(dataCart)
})
}, [cart])
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: cart?.currency_code,
})
.format(amount)
}
return (
<div>
{!cart && <span>Loading...</span>}
{cart && (
<>
<span>Cart ID: {cart.id}</span>
<ul>
{cart.items?.map((item) => (
<li key={item.id}>
{item.title} -
Quantity: {item.quantity} -
Price: {formatPrice(item.unit_price)}
</li>
))}
</ul>
<span>Cart Total: {formatPrice(cart.total)}</span>
</>
)}
</div>
)
}
```
</CodeTab>
</CodeTabs>
{/* TODO add a link to cart object in API reference (once available). */}
The response of the Retrieve Cart API route has a `cart` field, which is a cart object.
---
## 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:
```ts
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: cart?.currency_code,
})
.format(amount)
}
```
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.
@@ -0,0 +1,77 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Update Cart in Storefront`,
}
# {metadata.title}
In this document, you'll learn how to update different details of a cart.
<Note>
All cart updates are performed using the [Update Cart API route](!api!/store#carts_postcartsid).
</Note>
## Update Cart's Region
If a customer changes their region, you must update their cart to be associated with that region.
For example:
export const updateRegionHighlights = [
["8", `"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"
},
body: JSON.stringify({
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.
---
## Update Cart's Customer
If a guest customer logs in, you must update the cart to be associated with the logged-in customer.
For example:
export const updateCustomerHighlights = [
["8", `"logged_in_id"`, "Pass the logged-in customer's ID."]
]
```ts highlights={updateCustomerHighlights}
fetch(`http://localhost:9000/store/carts/${cartId}`, {
credentials: "include",
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
customer_id: "logged_in_id"
})
})
.then((res) => res.json())
.then(({ cart }) => {
// use cart...
console.log(cart)
})
```
The Update Cart API route accepts a `customer_id` request body parameter, whose value is the customer to associate with the cart.
@@ -0,0 +1,171 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Region Context in Storefront`,
}
# {metadata.title}
Throughout your storefront, you'll need to access the selected region to perform different actions, such as retrieve product's prices in the selected region.
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:
export const highlights = [
["12", "region", "Expose region to children of the context provider."],
["13", "setRegion", "Allow the context provider's children to change the selected region."],
["24", "RegionProvider", "The provider component to use in your component tree."],
["32", "", "If a region is set, set its ID in the local storage again in case it changed."],
["39", "regionId", "Retrieve the selected region from the `localStorage`."],
["42", "fetch", "If no region is selected, retrieve the list of regions from the Medusa application and select the first one."],
["51", "fetch", "If a region is selected, retrieve it from the Medusa application."],
["71", "useRegion", "The hook that child components of the provider use to access the region."]
]
```tsx highlights={highlights}
"use client" // include with Next.js 13+
import {
createContext,
useContext,
useEffect,
useState
} from "react"
import { HttpTypes } from "@medusajs/types"
type RegionContextType = {
region?: HttpTypes.StoreRegion
setRegion: React.Dispatch<
React.SetStateAction<HttpTypes.StoreRegion | undefined>
>
}
const RegionContext = createContext<RegionContextType | null>(null)
type RegionProviderProps = {
children: React.ReactNode
}
export const RegionProvider = (
{ children }: RegionProviderProps
) => {
const [region, setRegion] = useState<
HttpTypes.StoreRegion
>()
useEffect(() => {
if (region) {
// set its ID in the local storage in
// case it changed
localStorage.setItem("region_id", region.id)
return
}
const regionId = localStorage.getItem("region_id")
if (!regionId) {
// retrieve regions and select the first one
fetch(`http://localhost:9000/store/regions`, {
credentials: "include"
})
.then((res) => res.json())
.then(({ regions }) => {
setRegion(regions[0])
})
} else {
// retrieve selected region
fetch(`http://localhost:9000/store/regions/${regionId}`, {
credentials: "include"
})
.then((res) => res.json())
.then(({ region: dataRegion }) => {
setRegion(dataRegion)
})
}
}, [region])
return (
<RegionContext.Provider value={{
region,
setRegion
}}>
{children}
</RegionContext.Provider>
)
}
export const useRegion = () => {
const context = useContext(RegionContext)
if (!context) {
throw new Error("useRegion must be used within a RegionProvider")
}
return context
}
```
The `RegionProvider` handles retrieving the selected region from the Medusa application, and updating its ID in the `localStorage`.
The `useRegion` hook returns the value of the `RegionContext`. Child components of `RegionProvider` use this hook to access `region` or `setRegion`.
---
## Use RegionProvider in Component Tree
To use the region context's value, add the `RegionProvider` high in your component tree.
For example, if you're using Next.js, add it to the `app/layout.tsx` or `src/app/layout.tsx` file:
```tsx title="app/layout.tsx" collapsibleLines="1-14" highlights={[["22"]]}
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { CartProvider } from "../providers/cart"
import { RegionProvider } from "../providers/region"
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<RegionProvider>
{/* Other providers... */}
{children}
</RegionProvider>
</body>
</html>
);
}
```
---
## Use useRegion Hook
Now, you can use the `useRegion` hook in child components of `RegionProvider`.
For example:
```tsx
"use client" // include with Next.js 13+
// ...
import { useRegion } from "../providers/region"
export default function Products() {
const { region } = useRegion()
// ...
}
```
@@ -8,7 +8,7 @@ In this document, you'll learn how to store a customer's region's ID and retriev
## Store Selected Region ID
When the customer selects their region, for example, from a dropdown, store that region's ID in the `localstorage`.
When the customer selects their region, for example, from a dropdown, store that region's ID in the `localStorage`.
For example:
@@ -871,10 +871,38 @@ export const filesMap = [
"filePath": "/www/apps/resources/app/references/[...slug]/page.tsx",
"pathname": "/references/[...slug]"
},
{
"filePath": "/www/apps/resources/app/storefront-development/cart/context/page.mdx",
"pathname": "/storefront-development/cart/context"
},
{
"filePath": "/www/apps/resources/app/storefront-development/cart/create/page.mdx",
"pathname": "/storefront-development/cart/create"
},
{
"filePath": "/www/apps/resources/app/storefront-development/cart/manage-items/page.mdx",
"pathname": "/storefront-development/cart/manage-items"
},
{
"filePath": "/www/apps/resources/app/storefront-development/cart/page.mdx",
"pathname": "/storefront-development/cart"
},
{
"filePath": "/www/apps/resources/app/storefront-development/cart/retrieve/page.mdx",
"pathname": "/storefront-development/cart/retrieve"
},
{
"filePath": "/www/apps/resources/app/storefront-development/cart/update/page.mdx",
"pathname": "/storefront-development/cart/update"
},
{
"filePath": "/www/apps/resources/app/storefront-development/page.mdx",
"pathname": "/storefront-development"
},
{
"filePath": "/www/apps/resources/app/storefront-development/regions/context/page.mdx",
"pathname": "/storefront-development/regions/context"
},
{
"filePath": "/www/apps/resources/app/storefront-development/regions/list/page.mdx",
"pathname": "/storefront-development/regions/list"
+50
View File
@@ -7130,6 +7130,56 @@ export const generatedSidebar = [
"path": "/storefront-development/regions/store-retrieve-region",
"title": "Store and Retrieve Regions",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/regions/context",
"title": "Region React Context",
"children": []
}
]
},
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/cart",
"title": "Carts",
"children": [
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/cart/create",
"title": "Create Cart",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/cart/retrieve",
"title": "Retrieve Cart",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/cart/context",
"title": "Cart React Context",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/cart/update",
"title": "Update Cart",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/cart/manage-items",
"title": "Manage Line Items",
"children": []
}
]
}
+30
View File
@@ -1803,6 +1803,36 @@ export const sidebar = sidebarAttachHrefCommonOptions([
path: "/storefront-development/regions/store-retrieve-region",
title: "Store and Retrieve Regions",
},
{
path: "/storefront-development/regions/context",
title: "Region React Context",
},
],
},
{
path: "/storefront-development/cart",
title: "Carts",
children: [
{
path: "/storefront-development/cart/create",
title: "Create Cart",
},
{
path: "/storefront-development/cart/retrieve",
title: "Retrieve Cart",
},
{
path: "/storefront-development/cart/context",
title: "Cart React Context",
},
{
path: "/storefront-development/cart/update",
title: "Update Cart",
},
{
path: "/storefront-development/cart/manage-items",
title: "Manage Line Items",
},
],
},
],