docs: cache revalidation in next.js storefront + storefront totals (#11887)
* initial changes * generated sidebar
This commit is contained in:
@@ -92,10 +92,10 @@ module.exports = defineConfig({
|
||||
options: {
|
||||
providers: [
|
||||
// add providers here...
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ module.exports = defineConfig({
|
||||
// and you have other Locking Module Providers registered.
|
||||
is_default: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -75,10 +75,10 @@ module.exports = defineConfig({
|
||||
id: "locking-postgres",
|
||||
is_default: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ module.exports = defineConfig({
|
||||
is_default: true,
|
||||
options: {
|
||||
redisUrl: process.env.LOCKING_REDIS_URL,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -257,12 +257,12 @@ module.exports = defineConfig({
|
||||
is_default: true,
|
||||
options: {
|
||||
// ...
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
export const metadata = {
|
||||
title: `Revalidate Cache in Next.js Starter Storefront`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this guide, you'll learn about the general approach to revalidating cache in the Next.js Starter Storefront when data is updated in the Medusa application.
|
||||
|
||||
## Approach Overview
|
||||
|
||||
By default, the data that the Next.js Starter Storefront retrieves from the Medusa application is cached in the browser. This cache is used to improve the performance and speed of the storefront.
|
||||
|
||||
In some cases, you may need to revalidate the cache in the storefront when data is updated in the Medusa application. For example, when a product variant's price is updated in the Medusa application, you may want to revalidate the cache in the storefront to reflect the updated price.
|
||||
|
||||
You're free to choose the approach that works for your use case, custom requirements, and tech stack. The approach that Medusa recommends is:
|
||||
|
||||
1. Create a [subscriber](!docs!/learn/fundamentals/events-and-subscribers) in the Medusa application that listens for the event that triggers the data update. For example, you can listen to the `product.updated` event.
|
||||
2. In the subscriber, send a request to a custom endpoint in the Next.js Starter Storefront to trigger the cache revalidation.
|
||||
3. Create the custom endpoint in the Next.js Starter Storefront that listens for the request from the subscriber and revalidates the cache.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the [Events Reference](../../../events-reference/page.mdx) for a full list of events that the Medusa application emits.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Example: Revalidating Cache for Product Update
|
||||
|
||||
Consider you want to revalidate the cache in the Next.js Starter Storefront whenever a product is updated.
|
||||
|
||||
Start by creating the following subscriber in the Medusa application:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SubscriberArgs,
|
||||
SubscriberConfig,
|
||||
} from "@medusajs/framework"
|
||||
|
||||
export default async function productUpdatedHandler({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
// send request to next.js storefront to revalidate cache
|
||||
await fetch(`${process.env.STOREFRONT_URL}/api/revalidate?tags=products`)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "product.updated",
|
||||
}
|
||||
```
|
||||
|
||||
In the subscriber, you send a request to the custom endpoint `/api/revalidate` in the Next.js Starter Storefront. The request includes the query parameter `tags=product-${data.id}` to specify the cache that needs to be revalidated.
|
||||
|
||||
<Note>
|
||||
|
||||
Make sure to set the `STOREFRONT_URL` environment variable in the Medusa application to the URL of the Next.js Starter Storefront.
|
||||
|
||||
</Note>
|
||||
|
||||
Then, create in the Next.js Starter Storefront the custom endpoint that listens for the request and revalidates the cache:
|
||||
|
||||
```ts title="src/app/api/revalidate/route.ts"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { revalidateTag } from "next/cache"
|
||||
import { getCacheTag } from "../../../lib/data/cookies"
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const searchParams = req.nextUrl.searchParams
|
||||
const tags = searchParams.get("tags") as string
|
||||
|
||||
if (!tags) {
|
||||
return NextResponse.json({ error: "No tags provided" }, { status: 400 })
|
||||
}
|
||||
|
||||
const tagsArray = tags.split(",")
|
||||
await Promise.all(
|
||||
tagsArray.map(async (tag) => {
|
||||
const cacheTag = await getCacheTag(tag)
|
||||
// revalidate cache for the tag
|
||||
revalidateTag(cacheTag)
|
||||
})
|
||||
)
|
||||
|
||||
return NextResponse.json({ message: "Revalidated" }, { status: 200 })
|
||||
}
|
||||
```
|
||||
@@ -585,7 +585,7 @@ export const vendorWorkflowHighlights = [
|
||||
```ts title="src/workflows/marketplace/create-vendor/index.ts" highlights={vendorWorkflowHighlights}
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
setAuthAppMetadataStep,
|
||||
@@ -617,7 +617,7 @@ const createVendorWorkflow = createWorkflow(
|
||||
|
||||
const vendorAdminData = transform({
|
||||
input,
|
||||
vendor
|
||||
vendor,
|
||||
}, (data) => {
|
||||
return {
|
||||
...data.input.admin,
|
||||
@@ -701,13 +701,13 @@ export const vendorRouteSchemaHighlights = [
|
||||
```ts title="src/api/vendors/route.ts" highlights={vendorRouteSchemaHighlights}
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { MedusaError } from "@medusajs/framework/utils"
|
||||
import { z } from "zod"
|
||||
import createVendorWorkflow, {
|
||||
CreateVendorWorkflowInput
|
||||
} from "../../workflows/marketplace/create-vendor";
|
||||
CreateVendorWorkflowInput,
|
||||
} from "../../workflows/marketplace/create-vendor"
|
||||
|
||||
export const PostVendorCreateSchema = z.object({
|
||||
name: z.string(),
|
||||
@@ -716,8 +716,8 @@ export const PostVendorCreateSchema = z.object({
|
||||
admin: z.object({
|
||||
email: z.string(),
|
||||
first_name: z.string().optional(),
|
||||
last_name: z.string().optional()
|
||||
}).strict()
|
||||
last_name: z.string().optional(),
|
||||
}).strict(),
|
||||
}).strict()
|
||||
|
||||
type RequestBody = z.infer<typeof PostVendorCreateSchema>
|
||||
@@ -750,7 +750,7 @@ export const POST = async (
|
||||
input: {
|
||||
...vendorData,
|
||||
authIdentityId: req.auth_context.auth_identity_id,
|
||||
} as CreateVendorWorkflowInput
|
||||
} as CreateVendorWorkflowInput,
|
||||
})
|
||||
|
||||
res.json({
|
||||
@@ -788,7 +788,7 @@ You define middlewares in Medusa in the `src/api/middlewares.ts` special file. S
|
||||
import {
|
||||
defineMiddlewares,
|
||||
authenticate,
|
||||
validateAndTransformBody
|
||||
validateAndTransformBody,
|
||||
} from "@medusajs/framework/http"
|
||||
import { PostVendorCreateSchema } from "./vendors/route"
|
||||
|
||||
@@ -960,12 +960,12 @@ import { CreateProductWorkflowInputDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
WorkflowResponse
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductsWorkflow,
|
||||
createRemoteLinkStep,
|
||||
useQueryGraphStep
|
||||
useQueryGraphStep,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
import { MARKETPLACE_MODULE } from "../../../modules/marketplace"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
@@ -988,22 +988,22 @@ const createVendorProductWorkflow = createWorkflow(
|
||||
|
||||
const productData = transform({
|
||||
input,
|
||||
stores
|
||||
stores,
|
||||
}, (data) => {
|
||||
return {
|
||||
products: [{
|
||||
...data.input.product,
|
||||
sales_channels: [
|
||||
{
|
||||
id: data.stores[0].default_sales_channel_id
|
||||
}
|
||||
]
|
||||
}]
|
||||
id: data.stores[0].default_sales_channel_id,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}
|
||||
})
|
||||
|
||||
const createdProducts = createProductsWorkflow.runAsStep({
|
||||
input: productData
|
||||
input: productData,
|
||||
})
|
||||
|
||||
// TODO link vendor and products
|
||||
@@ -1029,23 +1029,23 @@ const { data: vendorAdmins } = useQueryGraphStep({
|
||||
entity: "vendor_admin",
|
||||
fields: ["vendor.id"],
|
||||
filters: {
|
||||
id: input.vendor_admin_id
|
||||
}
|
||||
id: input.vendor_admin_id,
|
||||
},
|
||||
}).config({ name: "retrieve-vendor-admins" })
|
||||
|
||||
const linksToCreate = transform({
|
||||
input,
|
||||
createdProducts,
|
||||
vendorAdmins
|
||||
vendorAdmins,
|
||||
}, (data) => {
|
||||
return data.createdProducts.map((product) => {
|
||||
return {
|
||||
[MARKETPLACE_MODULE]: {
|
||||
vendor_id: data.vendorAdmins[0].vendor.id
|
||||
vendor_id: data.vendorAdmins[0].vendor.id,
|
||||
},
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: product.id
|
||||
}
|
||||
product_id: product.id,
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1056,12 +1056,12 @@ const { data: products } = useQueryGraphStep({
|
||||
entity: "product",
|
||||
fields: ["*", "variants.*"],
|
||||
filters: {
|
||||
id: createdProducts[0].id
|
||||
}
|
||||
id: createdProducts[0].id,
|
||||
},
|
||||
}).config({ name: "retrieve-products" })
|
||||
|
||||
return new WorkflowResponse({
|
||||
product: products[0]
|
||||
product: products[0],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1086,8 +1086,8 @@ Create the file `src/api/vendors/products/route.ts` with the following content:
|
||||
```ts title="src/api/vendors/products/route.ts"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse
|
||||
} from "@medusajs/framework/http";
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
HttpTypes,
|
||||
} from "@medusajs/framework/types"
|
||||
@@ -1101,12 +1101,12 @@ export const POST = async (
|
||||
.run({
|
||||
input: {
|
||||
vendor_admin_id: req.auth_context.actor_id,
|
||||
product: req.validatedBody
|
||||
}
|
||||
product: req.validatedBody,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({
|
||||
product: result.product
|
||||
product: result.product,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -1133,9 +1133,9 @@ export default defineMiddlewares({
|
||||
method: ["POST"],
|
||||
middlewares: [
|
||||
validateAndTransformBody(AdminCreateProduct),
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1202,7 +1202,7 @@ To create the API route that retrieves the vendor’s products, add the followin
|
||||
```ts title="src/api/vendors/products/route.ts"
|
||||
// other imports...
|
||||
import {
|
||||
ContainerRegistrationKeys
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
@@ -1217,13 +1217,13 @@ export const GET = async (
|
||||
filters: {
|
||||
id: [
|
||||
// ID of the authenticated vendor admin
|
||||
req.auth_context.actor_id
|
||||
req.auth_context.actor_id,
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
res.json({
|
||||
products: vendorAdmin.vendor.products
|
||||
products: vendorAdmin.vendor.products,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -1339,8 +1339,8 @@ const groupVendorItemsStep = createStep(
|
||||
entity: "product",
|
||||
fields: ["vendor.*"],
|
||||
filters: {
|
||||
id: [item.product_id]
|
||||
}
|
||||
id: [item.product_id],
|
||||
},
|
||||
})
|
||||
|
||||
const vendorId = product.vendor?.id
|
||||
@@ -1350,12 +1350,12 @@ const groupVendorItemsStep = createStep(
|
||||
}
|
||||
vendorsItems[vendorId] = [
|
||||
...(vendorsItems[vendorId] || []),
|
||||
item
|
||||
item,
|
||||
]
|
||||
}))
|
||||
|
||||
return new StepResponse({
|
||||
vendorsItems
|
||||
vendorsItems,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -1815,7 +1815,7 @@ export const getOrderHighlights = [
|
||||
]
|
||||
|
||||
```ts title="src/api/vendors/orders/route.ts" highlights={getOrderHighlights}
|
||||
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http";
|
||||
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import { getOrdersListWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
@@ -1829,8 +1829,8 @@ export const GET = async (
|
||||
entity: "vendor_admin",
|
||||
fields: ["vendor.orders.*"],
|
||||
filters: {
|
||||
id: [req.auth_context.actor_id]
|
||||
}
|
||||
id: [req.auth_context.actor_id],
|
||||
},
|
||||
})
|
||||
|
||||
const { result: orders } = await getOrdersListWorkflow(req.scope)
|
||||
@@ -1854,14 +1854,14 @@ export const GET = async (
|
||||
],
|
||||
variables: {
|
||||
filters: {
|
||||
id: vendorAdmin.vendor.orders.map((order) => order.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
id: vendorAdmin.vendor.orders.map((order) => order.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
res.json({
|
||||
orders
|
||||
orders,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
tags:
|
||||
- cart
|
||||
- storefront
|
||||
---
|
||||
|
||||
import { CodeTabs, CodeTab, Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Show Cart Totals`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this guide, you'll learn how to show the cart totals in the checkout flow. This is usually shown as part of the checkout and cart pages.
|
||||
|
||||
## Cart Total Fields
|
||||
|
||||
The `Cart` object has various fields related to its totals, which you can check out in the [Store API reference](!api!/store#carts_cart_schema).
|
||||
|
||||
The fields that are most commonly used are:
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Field</Table.HeaderCell>
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`subtotal`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The cart's subtotal excluding taxes and shipping, and including discounts.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`discount_total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total discounts or promotions applied to the cart.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`shipping_total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total shipping cost.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`tax_total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total tax amount.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total amount of the cart including all taxes, shipping, and discounts.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
---
|
||||
|
||||
## Example: React Storefront
|
||||
|
||||
Here's an example of how you can show the cart totals in a React component:
|
||||
|
||||
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."],
|
||||
["23", "formatPrice", "Show the cart's subtotal"],
|
||||
["27", "formatPrice", "Show the total discounts"],
|
||||
["31", "formatPrice", "Show the shipping total"],
|
||||
["35", "formatPrice", "Show the tax total"],
|
||||
["39", "formatPrice", "Show the total amount"],
|
||||
]
|
||||
|
||||
```tsx highlights={highlights}
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { useCart } from "../../../providers/cart"
|
||||
|
||||
export default function CartTotals() {
|
||||
const { cart } = useCart()
|
||||
|
||||
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 && (
|
||||
<ul>
|
||||
<li>
|
||||
<span>Subtotal (excl. shipping & taxes)</span>
|
||||
<span>{formatPrice(cart.subtotal ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Discounts</span>
|
||||
<span>{formatPrice(cart.discount_total ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Shipping</span>
|
||||
<span>{formatPrice(cart.shipping_total ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Taxes</span>
|
||||
<span>{formatPrice(cart.tax_total ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Total</span>
|
||||
<span>{formatPrice(cart.total ?? 0)}</span>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
In the example, you first retrieve the cart using the [Cart Context](../context/page.mdx). Then, you define the [formatPrice](../retrieve/page.mdx#format-prices) function to format the total amounts.
|
||||
|
||||
Finally, you render the cart totals in a list, showing the subtotal, discounts, shipping, taxes, and the total amount.
|
||||
@@ -0,0 +1,248 @@
|
||||
---
|
||||
tags:
|
||||
- order
|
||||
- storefront
|
||||
---
|
||||
|
||||
import { CodeTabs, CodeTab, Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Order Confirmation in Storefront`,
|
||||
}
|
||||
|
||||
# {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.
|
||||
|
||||
## 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).
|
||||
|
||||
You need the order's ID to retrieve the order. You can pass it from the [complete cart step](../complete-cart/page.mdx) or store it in the `localStorage`.
|
||||
|
||||
The following example assumes you already have the order ID:
|
||||
|
||||
<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
|
||||
"use client" // include with Next.js 13+
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useEffect } from "react"
|
||||
import { useState } from "react"
|
||||
|
||||
export function OrderConfirmation({ id }: { id: string }) {
|
||||
const [order, setOrder] = useState<HttpTypes.StoreOrder | undefined>()
|
||||
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())
|
||||
.then(({ order: dataOrder }) => {
|
||||
setOrder(dataOrder)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [id])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && <span>Loading...</span>}
|
||||
{!loading && order && (
|
||||
<div>
|
||||
<h1>Order Confirmation</h1>
|
||||
<p>Order ID: {order.id}</p>
|
||||
<p>Order Date: {order.created_at.toLocaleString()}</p>
|
||||
<p>Order Customer: {order.email}</p>
|
||||
{/* TODO show more info */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
In the above example, you retrieve the order's details from the [Get an Order API route](!api!store#orders_getordersid). Then, in the React example, you show the order details like the order ID, order date, and customer email.
|
||||
|
||||
The rest of this guide will expand on the React example to show more order details.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the [Order schema in the API reference](!api!/store#orders_order_schema) for all the available order fields.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Show Order Items
|
||||
|
||||
An order has an `items` field that contains the order items. You can show the order items on the order confirmation page.
|
||||
|
||||
For example, add to the React component a `formatPrice` function to format prices with the order's currency:
|
||||
|
||||
```tsx
|
||||
const formatPrice = (amount: number): string => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: order?.currency_code,
|
||||
})
|
||||
.format(amount)
|
||||
}
|
||||
```
|
||||
|
||||
Since this is the same function used to format the prices of products and cart totals, 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.
|
||||
|
||||
Then, you can show the order items in a list:
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<div>
|
||||
{loading && <span>Loading...</span>}
|
||||
{!loading && order && (
|
||||
<div>
|
||||
{/* ... */}
|
||||
<p>
|
||||
<span>Order Items</span>
|
||||
<ul>
|
||||
{order.items?.map((item) => (
|
||||
<li key={item.id}>
|
||||
{item.title} - {item.quantity} x {formatPrice(item.unit_price)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</p>
|
||||
{/* TODO show more details */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
In the above example, you show the order items in a list, displaying the item's title, quantity, and unit price formatted with the `formatPrice` function.
|
||||
|
||||
---
|
||||
|
||||
## Show Order Totals
|
||||
|
||||
An order has various fields for the order totals, which you can check out in the [Order schema in the Store API reference](https://docs.medusajs.com/api/store#orders_order_schema). The most commonly used fields are:
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Field</Table.HeaderCell>
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`subtotal`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The order's subtotal excluding taxes and shipping, and including discounts.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`discount_total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total discounts or promotions applied to the order.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`shipping_total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total shipping cost.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`tax_total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total tax amount.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
`total`
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
The total amount of the order including all taxes, shipping, and discounts.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
You can show these totals on the order confirmation page. For example:
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<div>
|
||||
{loading && <span>Loading...</span>}
|
||||
{!loading && order && (
|
||||
<div>
|
||||
{/* ... */}
|
||||
<div>
|
||||
<span>Order Totals</span>
|
||||
<ul>
|
||||
<li>
|
||||
<span>Subtotal (excl. shipping & taxes)</span>
|
||||
<span>{formatPrice(order.subtotal ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Discounts</span>
|
||||
<span>{formatPrice(order.discount_total ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Shipping</span>
|
||||
<span>{formatPrice(order.shipping_total ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Taxes</span>
|
||||
<span>{formatPrice(order.tax_total ?? 0)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Total</span>
|
||||
<span>{formatPrice(order.total ?? 0)}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
In the above example, you show the order totals in a list, displaying the subtotal, discounts, shipping, taxes, and total amount formatted with the [formatPrice function](#show-order-items).
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ChildDocs } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `Checkout in Storefront`,
|
||||
}
|
||||
@@ -10,12 +8,16 @@ Once a customer finishes adding products to cart, they go through the checkout f
|
||||
|
||||
The checkout flow is composed of five steps:
|
||||
|
||||
1. **Email:** Enter customer email. For logged-in customer, you can pre-fill it.
|
||||
2. **Address:** Enter shipping/billing address details.
|
||||
3. **Shipping**: Choose a shipping method.
|
||||
4. **Payment:** Choose a payment provider.
|
||||
5. **Complete Cart:** Perform any payment action necessary (for example, enter card details), complete the cart, and place the order.
|
||||
1. [Email](./email/page.mdx): Enter customer email. For logged-in customer, you can pre-fill it.
|
||||
2. [Address](./address/page.mdx): Enter shipping/billing address details.
|
||||
3. [Shipping](./shipping/page.mdx): Choose a shipping method.
|
||||
4. [Payment](./payment/page.mdx): Choose a payment provider.
|
||||
5. [Complete Cart](./complete-cart/page.mdx): Perform any payment action necessary (for example, enter card details), complete the cart, and place the order.
|
||||
|
||||
You can combine steps based on your desired checkout flow.
|
||||
You can combine steps or change their order based on your desired checkout flow. Once the customer places the order, you can show them an [order confirmation page](./order-confirmation/page.mdx).
|
||||
|
||||
<ChildDocs type="item" onlyTopLevel={true} />
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the [Express Checkout Tutorial](../guides/express-checkout/page.mdx) for a complete example of a different checkout flow.
|
||||
|
||||
</Note>
|
||||
|
||||
@@ -6057,5 +6057,8 @@ export const generatedEditDates = {
|
||||
"references/modules/event/page.mdx": "2025-03-17T15:24:03.021Z",
|
||||
"references/modules/file_service/page.mdx": "2025-03-17T15:24:03.025Z",
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
@@ -891,6 +891,10 @@ export const filesMap = [
|
||||
"filePath": "/www/apps/resources/app/nextjs-starter/guides/customize-stripe/page.mdx",
|
||||
"pathname": "/nextjs-starter/guides/customize-stripe"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/nextjs-starter/guides/revalidate-cache/page.mdx",
|
||||
"pathname": "/nextjs-starter/guides/revalidate-cache"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/nextjs-starter/page.mdx",
|
||||
"pathname": "/nextjs-starter"
|
||||
@@ -1039,6 +1043,10 @@ export const filesMap = [
|
||||
"filePath": "/www/apps/resources/app/storefront-development/cart/retrieve/page.mdx",
|
||||
"pathname": "/storefront-development/cart/retrieve"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/cart/totals/page.mdx",
|
||||
"pathname": "/storefront-development/cart/totals"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/cart/update/page.mdx",
|
||||
"pathname": "/storefront-development/cart/update"
|
||||
@@ -1055,6 +1063,10 @@ export const filesMap = [
|
||||
"filePath": "/www/apps/resources/app/storefront-development/checkout/email/page.mdx",
|
||||
"pathname": "/storefront-development/checkout/email"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/checkout/order-confirmation/page.mdx",
|
||||
"pathname": "/storefront-development/checkout/order-confirmation"
|
||||
},
|
||||
{
|
||||
"filePath": "/www/apps/resources/app/storefront-development/checkout/page.mdx",
|
||||
"pathname": "/storefront-development/checkout"
|
||||
|
||||
@@ -1195,6 +1195,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = {
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/retrieve",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "ref",
|
||||
"title": "Show Cart Totals",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/totals",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
@@ -5957,6 +5965,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = {
|
||||
"title": "Implement Express Checkout with Medusa",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/guides/express-checkout",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "ref",
|
||||
"title": "Order Confirmation in Storefront",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/checkout/order-confirmation",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -293,6 +293,14 @@ const generatedgeneratedStorefrontDevelopmentSidebarSidebar = {
|
||||
"path": "/storefront-development/cart/manage-items",
|
||||
"title": "Manage Line Items",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/cart/totals",
|
||||
"title": "Show Totals",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -359,6 +367,14 @@ const generatedgeneratedStorefrontDevelopmentSidebarSidebar = {
|
||||
"path": "/storefront-development/checkout/complete-cart",
|
||||
"title": "5. Complete Cart",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/storefront-development/checkout/order-confirmation",
|
||||
"title": "Show Order Confirmation",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -756,7 +756,7 @@ const generatedgeneratedToolsSidebarSidebar = {
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "category",
|
||||
"title": "Payment",
|
||||
"title": "How-to Guides",
|
||||
"initialOpen": true,
|
||||
"children": [
|
||||
{
|
||||
@@ -766,6 +766,14 @@ const generatedgeneratedToolsSidebarSidebar = {
|
||||
"path": "/nextjs-starter/guides/customize-stripe",
|
||||
"title": "Customize Stripe Integration",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"loaded": true,
|
||||
"isPathHref": true,
|
||||
"type": "link",
|
||||
"path": "/nextjs-starter/guides/revalidate-cache",
|
||||
"title": "Revalidate Cache",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -179,6 +179,11 @@ export const storefrontDevelopmentSidebar = [
|
||||
path: "/storefront-development/cart/manage-items",
|
||||
title: "Manage Line Items",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/cart/totals",
|
||||
title: "Show Totals",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -224,6 +229,11 @@ export const storefrontDevelopmentSidebar = [
|
||||
path: "/storefront-development/checkout/complete-cart",
|
||||
title: "5. Complete Cart",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/storefront-development/checkout/order-confirmation",
|
||||
title: "Show Order Confirmation",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -100,7 +100,7 @@ export const toolsSidebar = [
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
title: "Payment",
|
||||
title: "How-to Guides",
|
||||
initialOpen: true,
|
||||
children: [
|
||||
{
|
||||
@@ -108,6 +108,11 @@ export const toolsSidebar = [
|
||||
path: "/nextjs-starter/guides/customize-stripe",
|
||||
title: "Customize Stripe Integration",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/nextjs-starter/guides/revalidate-cache",
|
||||
title: "Revalidate Cache",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -27,6 +27,10 @@ export const cart = [
|
||||
"title": "Retrieve Cart in Storefront",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/retrieve"
|
||||
},
|
||||
{
|
||||
"title": "Show Cart Totals",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/totals"
|
||||
},
|
||||
{
|
||||
"title": "Update Cart in Storefront",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/update"
|
||||
|
||||
@@ -43,6 +43,10 @@ export const order = [
|
||||
"title": "Checkout Step 5: Complete Cart",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/checkout/complete-cart"
|
||||
},
|
||||
{
|
||||
"title": "Order Confirmation in Storefront",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/checkout/order-confirmation"
|
||||
},
|
||||
{
|
||||
"title": "Implement Express Checkout with Medusa",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/guides/express-checkout"
|
||||
|
||||
@@ -19,6 +19,10 @@ export const storefront = [
|
||||
"title": "Retrieve Cart in Storefront",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/retrieve"
|
||||
},
|
||||
{
|
||||
"title": "Show Cart Totals",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/totals"
|
||||
},
|
||||
{
|
||||
"title": "Update Cart in Storefront",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/cart/update"
|
||||
@@ -35,6 +39,10 @@ export const storefront = [
|
||||
"title": "Checkout Step 1: Enter Email",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/checkout/email"
|
||||
},
|
||||
{
|
||||
"title": "Order Confirmation in Storefront",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/checkout/order-confirmation"
|
||||
},
|
||||
{
|
||||
"title": "Checkout Step 4: Choose Payment Provider",
|
||||
"path": "https://docs.medusajs.com/resources/storefront-development/checkout/payment"
|
||||
|
||||
Reference in New Issue
Block a user