docs: added checkout storefront guides (#7678)
* add first guides * added more guides * finished payment + added stripe * finished adding guides * updated generated sidebar
This commit is contained in:
1 parent
7e7e6e3311
commit
85d487d90b
13 files changed
+1375
-11
No files matched your search
@@ -17,13 +17,15 @@ For example, create the following file that exports a `CartProvider` component a
|
|||||||
export const highlights = [
|
export const highlights = [
|
||||||
["13", "cart", "Expose cart to children of the context provider."],
|
["13", "cart", "Expose cart to children of the context provider."],
|
||||||
["14", "setCart", "Allow the context provider's children to update the cart."],
|
["14", "setCart", "Allow the context provider's children to update the cart."],
|
||||||
["25", "CartProvider", "The provider component to use in your component tree."],
|
["17", "refreshCart", "Allow the context provider's children to unset and reset the cart."],
|
||||||
["31", "useRegion", "Use the `useRegion` hook defined in the Region Context guide."],
|
["26", "CartProvider", "The provider component to use in your component tree."],
|
||||||
["36", "setItem", "Set the cart's ID in `localStorage` in case it changed."],
|
["32", "useRegion", "Use the `useRegion` hook defined in the Region Context guide."],
|
||||||
["44", "fetch", "If the customer doesn't have a cart, create a new one."],
|
["37", "setItem", "Set the cart's ID in `localStorage` in case it changed."],
|
||||||
["48", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to associate the correct sales channel(s)."],
|
["45", "fetch", "If the customer doesn't have a cart, create a new one."],
|
||||||
["62", "fetch", "Retrieve the customer's cart."],
|
["49", "process.env.NEXT_PUBLIC_PAK", "Pass the Publishable API key to associate the correct sales channel(s)."],
|
||||||
["82", "useCart", "The hook that child components of the provider use to access the cart."]
|
["63", "fetch", "Retrieve the customer's cart."],
|
||||||
|
["73", "refreshCart", "This function unsets the cart, which triggers the `useEffect` callback to create a cart."],
|
||||||
|
["89", "useCart", "The hook that child components of the provider use to access the cart."]
|
||||||
]
|
]
|
||||||
|
|
||||||
```tsx highlights={highlights}
|
```tsx highlights={highlights}
|
||||||
@@ -43,6 +45,7 @@ type CartContextType = {
|
|||||||
setCart: React.Dispatch<
|
setCart: React.Dispatch<
|
||||||
React.SetStateAction<HttpTypes.StoreCart | undefined>
|
React.SetStateAction<HttpTypes.StoreCart | undefined>
|
||||||
>
|
>
|
||||||
|
refreshCart: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const CartContext = createContext<CartContextType | null>(null)
|
const CartContext = createContext<CartContextType | null>(null)
|
||||||
@@ -93,10 +96,16 @@ export const CartProvider = ({ children }: CartProviderProps) => {
|
|||||||
}
|
}
|
||||||
}, [cart, region])
|
}, [cart, region])
|
||||||
|
|
||||||
|
const refreshCart = () => {
|
||||||
|
localStorage.removeItem("cart_id")
|
||||||
|
setCart(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CartContext.Provider value={{
|
<CartContext.Provider value={{
|
||||||
cart,
|
cart,
|
||||||
setCart
|
setCart,
|
||||||
|
refreshCart
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</CartContext.Provider>
|
</CartContext.Provider>
|
||||||
@@ -116,7 +125,7 @@ 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).
|
||||||
|
|
||||||
The `useCart` hook returns the value of the `CartContext`. Child components of `CartProvider` use this hook to access `cart` or `setCart`.
|
The `useCart` hook returns the value of the `CartContext`. Child components of `CartProvider` use this hook to access `cart`, `setCart`, or `refreshCart`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { CodeTabs, CodeTab } from "docs-ui"
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Checkout Step 2: Enter Address`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {metadata.title}
|
||||||
|
|
||||||
|
The second step of the checkout flow is to ask the customer for their address.
|
||||||
|
|
||||||
|
{/* TODO add how to list addresses of logged in customer. */}
|
||||||
|
|
||||||
|
A cart has shipping and billing addesses. Use the [Update Cart API route]() to update the cart's addresses.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
<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"
|
||||||
|
},
|
||||||
|
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"],
|
||||||
|
["102", "", "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";
|
||||||
|
|
||||||
|
export default function CheckoutAddressStep () {
|
||||||
|
const { cart, setCart } = useCart()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [firstName, setFirstName] = useState("")
|
||||||
|
const [lastName, setLastName] = useState("")
|
||||||
|
const [address1, setAddress1] = useState("")
|
||||||
|
const [company, setCompany] = useState("")
|
||||||
|
const [postalCode, setPostalCode] = useState("")
|
||||||
|
const [city, setCity] = useState("")
|
||||||
|
const [countryCode, setCountryCode] = useState("")
|
||||||
|
const [province, setProvince] = useState("")
|
||||||
|
const [phoneNumber, setPhoneNumber] = useState("")
|
||||||
|
|
||||||
|
const updateAddress = (
|
||||||
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
if (!cart) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const address = {
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
address_1: address1,
|
||||||
|
company,
|
||||||
|
postal_code: postalCode,
|
||||||
|
city,
|
||||||
|
country_code: countryCode,
|
||||||
|
province,
|
||||||
|
phone: phoneNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`http://localhost:9000/store/carts/${cart.id}`, {
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
shipping_address: address,
|
||||||
|
billing_address: address
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ cart: updatedCart }) => {
|
||||||
|
setCart(updatedCart)
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
{!cart && <span>Loading...</span>}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="First Name"
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Address Line"
|
||||||
|
value={address1}
|
||||||
|
onChange={(e) => setAddress1(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Company"
|
||||||
|
value={company}
|
||||||
|
onChange={(e) => setCompany(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Postal Code"
|
||||||
|
value={postalCode}
|
||||||
|
onChange={(e) => setPostalCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="City"
|
||||||
|
value={city}
|
||||||
|
onChange={(e) => setCity(e.target.value)}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={countryCode}
|
||||||
|
onChange={(e) => setCountryCode(e.target.value)}
|
||||||
|
>
|
||||||
|
{cart?.region?.countries?.map((country) => (
|
||||||
|
<option
|
||||||
|
key={country.iso_2}
|
||||||
|
value={country.iso_2}
|
||||||
|
>
|
||||||
|
{country.display_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Province"
|
||||||
|
value={province}
|
||||||
|
onChange={(e) => setProvince(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
placeholder="Phone Number"
|
||||||
|
value={phoneNumber}
|
||||||
|
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={!cart || loading}
|
||||||
|
onClick={updateAddress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeTab>
|
||||||
|
</CodeTabs>
|
||||||
|
|
||||||
|
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 retuned 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.
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { CodeTabs, CodeTab } from "docs-ui"
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Checkout Step 5: Complete Cart`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {metadata.title}
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
fetch(
|
||||||
|
`http://localhost:9000/store/carts/${cartId}/complete`,
|
||||||
|
{
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ type, cart, order, error }) => {
|
||||||
|
if (type === "cart" && cart) {
|
||||||
|
// an error occured
|
||||||
|
console.error(error)
|
||||||
|
} else if (type === "order" && order) {
|
||||||
|
// TODO redirect to order success page
|
||||||
|
alert("Order placed.")
|
||||||
|
console.log(order)
|
||||||
|
// unset cart ID from local storage
|
||||||
|
localStorage.removeItem("cart_id")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In the response of the request, the `type` field determines whether the cart completion was successful:
|
||||||
|
|
||||||
|
- If the `type` is `cart`, it means the cart completion failed. The `error` response field holds the error details.
|
||||||
|
- If the `type` is `order`, it means the cart was completed and the order was placed successfully.
|
||||||
|
|
||||||
|
When the cart completion is successful, it's important to unset the cart ID from the `localStorage`, as the cart is no longer usable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## React Example with Default System Payment Provider
|
||||||
|
|
||||||
|
For example, to complete the cart when the default system payment provider is used:
|
||||||
|
|
||||||
|
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."],
|
||||||
|
["33", `type === "cart"`, "If the `type` returned is `cart`,\nit means an error occurred and the cart wasn't completed."],
|
||||||
|
["36", `type === "order"`, "If the `type` returned is `order`,\nit means the cart was completed and the order was placed successfully."],
|
||||||
|
["40", "refreshCart", "Unset and reset the cart."],
|
||||||
|
["47", "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"
|
||||||
|
|
||||||
|
export default function SystemDefaultPayment () {
|
||||||
|
const { cart, refreshCart } = useCart()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const handlePayment = (
|
||||||
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// TODO perform any custom payment handling logic
|
||||||
|
|
||||||
|
// complete the cart
|
||||||
|
fetch(
|
||||||
|
`http://localhost:9000/store/carts/${cart.id}/complete`,
|
||||||
|
{
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ type, cart, order, error }) => {
|
||||||
|
if (type === "cart" && cart) {
|
||||||
|
// an error occured
|
||||||
|
console.error(error)
|
||||||
|
} else if (type === "order" && order) {
|
||||||
|
// TODO redirect to order success page
|
||||||
|
alert("Order placed.")
|
||||||
|
console.log(order)
|
||||||
|
refreshCart()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handlePayment}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Place Order
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## React Example with Third-Party Provider
|
||||||
|
|
||||||
|
Refer to the [Stripe guide](../payment/stripe/page.mdx) for an example on integrating a third-party provider and implementing card completion.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { CodeTabs, CodeTab } from "docs-ui"
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Checkout Step 1: Enter Email`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {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.
|
||||||
|
|
||||||
|
<Note title="Tip">
|
||||||
|
|
||||||
|
If the customer is logged-in, you can pre-fill the email with the customer's email.
|
||||||
|
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
<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"
|
||||||
|
},
|
||||||
|
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"]
|
||||||
|
]
|
||||||
|
|
||||||
|
```tsx highlights={highlights}
|
||||||
|
"use client" // include with Next.js 13+
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useCart } from "../../../providers/cart"
|
||||||
|
|
||||||
|
export default function CheckoutEmailStep () {
|
||||||
|
const { cart, setCart } = useCart()
|
||||||
|
const [email, setEmail] = useState("")
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cart && !cart.items?.length) {
|
||||||
|
// TODO redirect to another path
|
||||||
|
}
|
||||||
|
}, [cart])
|
||||||
|
|
||||||
|
const updateCartEmail = (
|
||||||
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
if (!cart || !email.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
fetch(`http://localhost:9000/store/carts/${cart.id}`, {
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ cart: updatedCart }) => {
|
||||||
|
setCart(updatedCart)
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!cart && <span>Loading...</span>}
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
disabled={!cart}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={!cart || !email || loading}
|
||||||
|
onClick={updateCartEmail}
|
||||||
|
>
|
||||||
|
Set Email
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</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.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ChildDocs } from "docs-ui"
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Checkout in Storefront`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {metadata.title}
|
||||||
|
|
||||||
|
Once a customer finishes adding products to cart, they go through the checkout flow to place their order.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
You can combine steps based on your desired checkout flow.
|
||||||
|
|
||||||
|
<ChildDocs type="item" onlyTopLevel={true} />
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
import { CodeTabs, CodeTab } from "docs-ui"
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Checkout Step 4: Choose Payment Provider`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {metadata.title}
|
||||||
|
|
||||||
|
The last step before completing the order is choosing the payment provider and performing any necessary actions.
|
||||||
|
|
||||||
|
The actions required after choosing the payment provider are different for each provider. So, this guide doesn't cover that.
|
||||||
|
|
||||||
|
## Payment Step Flow
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Example
|
||||||
|
|
||||||
|
For example, to implement the payment step flow:
|
||||||
|
|
||||||
|
<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."],
|
||||||
|
["18", "selectPaymentProvider", "This function is executed when the customer submits their chosen payment provider."],
|
||||||
|
["25", "fetch", "Create a payment collection for the cart when it doesn't have one."],
|
||||||
|
["47", "fetch", "Initialize the payment session in the payment collection for the chosen provider."],
|
||||||
|
["65", "fetch", "Retrieve the cart again to update its data."],
|
||||||
|
["76", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."],
|
||||||
|
["77", "activePaymentSession", "The active session is the first in the payment collection's sessions."],
|
||||||
|
["83", "", "Test which payment provider is chosen based on the prefix of the provider ID."],
|
||||||
|
["84", `"pp_stripe_"`, "Check if the chosen provider is Stripe."],
|
||||||
|
["88", `"pp_system_default"`, "Check if the chosen provider is the default systen provider."],
|
||||||
|
["90", "default", "Handle unrecognized providers."],
|
||||||
|
]
|
||||||
|
|
||||||
|
```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"
|
||||||
|
})
|
||||||
|
.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"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cart_id: cart.id,
|
||||||
|
region_id: cart.region_id,
|
||||||
|
currency_code: cart.currency_code,
|
||||||
|
amount: cart.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.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"
|
||||||
|
},
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.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.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</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."],
|
||||||
|
["31", "setSelectedPaymentProvider", "If a payment provider was selected before, pre-fill it."],
|
||||||
|
["37", "handleSelectProvider", "This function is executed when the customer submits their chosen payment provider."],
|
||||||
|
["50", "fetch", "Create a payment collection for the cart when it doesn't have one."],
|
||||||
|
["72", "fetch", "Initialize the payment session in the payment collection for the chosen provider."],
|
||||||
|
["90", "fetch", "Retrieve the cart again to update its data."],
|
||||||
|
["103", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."],
|
||||||
|
["104", "activePaymentSession", "The active session is the first in the payment collection's sessions."],
|
||||||
|
["110", "", "Test which payment provider is chosen based on the prefix of the provider ID."],
|
||||||
|
["111", `"pp_stripe_"`, "Check if the chosen provider is Stripe."],
|
||||||
|
["119", `"pp_system_default"`, "Check if the chosen provider is the default systen provider."],
|
||||||
|
["125", "default", "Handle unrecognized providers."],
|
||||||
|
["160", "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 { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
export default function CheckoutPaymentStep () {
|
||||||
|
const { cart, setCart } = useCart()
|
||||||
|
const [paymentProviders, setPaymentProviders] = useState<
|
||||||
|
HttpTypes.StorePaymentProvider[]
|
||||||
|
>([])
|
||||||
|
const [
|
||||||
|
selectedPaymentProvider,
|
||||||
|
setSelectedPaymentProvider
|
||||||
|
] = useState<string | undefined>()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cart) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`http://localhost:9000/store/payment-providers?region_id=${
|
||||||
|
cart.region_id
|
||||||
|
}`, {
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ payment_providers }) => {
|
||||||
|
setPaymentProviders(payment_providers)
|
||||||
|
setSelectedPaymentProvider(
|
||||||
|
cart.payment_collection?.payment_sessions?.[0]?.id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [cart])
|
||||||
|
|
||||||
|
const handleSelectProvider = async (
|
||||||
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!cart || !selectedPaymentProvider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cart_id: cart.id,
|
||||||
|
region_id: cart.region_id,
|
||||||
|
currency_code: cart.currency_code,
|
||||||
|
amount: cart.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.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"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider_id: selectedPaymentProvider
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
// re-fetch cart
|
||||||
|
const {
|
||||||
|
cart: updatedCart
|
||||||
|
} = await fetch(
|
||||||
|
`http://localhost:9000/store/carts/${cart.id}`,
|
||||||
|
{
|
||||||
|
credentials: "include"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
setCart(updatedCart)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentUi = useCallback(() => {
|
||||||
|
const activePaymentSession = cart?.payment_collection?.
|
||||||
|
payment_sessions?.[0]
|
||||||
|
if (!activePaymentSession) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(true) {
|
||||||
|
case activePaymentSession.provider_id.startsWith("pp_stripe_"):
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
You chose stripe!
|
||||||
|
{/* TODO add stripe UI */}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
case activePaymentSession.provider_id
|
||||||
|
.startsWith("pp_system_default"):
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
You chose manual payment! No additional actions required.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
You chose {activePaymentSession.provider_id} which is
|
||||||
|
in development.
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} , [cart])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<select
|
||||||
|
value={selectedPaymentProvider}
|
||||||
|
onChange={(e) => setSelectedPaymentProvider(e.target.value)}
|
||||||
|
>
|
||||||
|
{paymentProviders.map((provider) => (
|
||||||
|
<option
|
||||||
|
key={provider.id}
|
||||||
|
value={provider.id}
|
||||||
|
>
|
||||||
|
{provider.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={async (e) => {
|
||||||
|
await handleSelectProvider(e)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{getPaymentUi()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeTab>
|
||||||
|
</CodeTabs>
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import { CodeTabs, CodeTab } from "docs-ui"
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Payment with Stripe in React Storefront`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {metadata.title}
|
||||||
|
|
||||||
|
In this document, 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.
|
||||||
|
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<Note type="check">
|
||||||
|
|
||||||
|
- [Stripe Provider Module](../../../../commerce-modules/payment/payment-provider/stripe/page.mdx) installed and configured in your Medusa application.
|
||||||
|
- [Stripe publishable API key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard).
|
||||||
|
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## 1. Install Stripe SDK
|
||||||
|
|
||||||
|
In your storefront, use the following command to install Stripe's JS and React SDKs:
|
||||||
|
|
||||||
|
```bash npm2yarn
|
||||||
|
npm install @stripe/react-stripe-js @stripe/stripe-js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Add Stripe Environment Variables
|
||||||
|
|
||||||
|
Next, add an environment variable holding your Stripe publishable API key.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_STRIPE_PK=pk_test_51Kj...
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note title="Tip">
|
||||||
|
|
||||||
|
For Next.js storefronts, the environment variable's name must be prefixed with `NEXT_PUBLIC`. If your storefront's framework requires a different prefix, make sure to change it.
|
||||||
|
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Create Stripe Component
|
||||||
|
|
||||||
|
Then, create a file holding the following Stripe component:
|
||||||
|
|
||||||
|
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."],
|
||||||
|
["94", `type === "cart"`, "If the `type` returned is `cart`,\nit means an error occurred and the cart wasn't completed."],
|
||||||
|
["97", `type === "order"`, "If the `type` returned is `order`,\nit means the cart was completed and the order was placed successfully."],
|
||||||
|
["101", "refreshCart", "Unset and reset the cart."],
|
||||||
|
["111", "button", "This button triggers the `handlePayment` function when clicked."]
|
||||||
|
]
|
||||||
|
|
||||||
|
```tsx highlights={highlights}
|
||||||
|
"use client" // include with Next.js 13+
|
||||||
|
|
||||||
|
import {
|
||||||
|
CardElement,
|
||||||
|
Elements,
|
||||||
|
useElements,
|
||||||
|
useStripe
|
||||||
|
} from "@stripe/react-stripe-js"
|
||||||
|
import { loadStripe } from "@stripe/stripe-js"
|
||||||
|
import { useCart } from "../../providers/cart"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
const stripePromise = loadStripe(
|
||||||
|
process.env.NEXT_PUBLIC_STRIPE_PK || "temp"
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function StripePayment() {
|
||||||
|
const { cart } = useCart()
|
||||||
|
const clientSecret = cart?.payment_collection?.
|
||||||
|
payment_sessions?.[0].data.client_secret as string
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Elements stripe={stripePromise} options={{
|
||||||
|
clientSecret,
|
||||||
|
}}>
|
||||||
|
<StripeForm clientSecret={clientSecret} />
|
||||||
|
</Elements>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StripeForm = ({
|
||||||
|
clientSecret
|
||||||
|
}: {
|
||||||
|
clientSecret: string | undefined
|
||||||
|
}) => {
|
||||||
|
const { cart, refreshCart } = useCart()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const stripe = useStripe()
|
||||||
|
const elements = useElements()
|
||||||
|
|
||||||
|
async function handlePayment(
|
||||||
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
const card = elements?.getElement(CardElement)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!stripe ||
|
||||||
|
!elements ||
|
||||||
|
!card ||
|
||||||
|
!cart ||
|
||||||
|
!clientSecret
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
stripe?.confirmCardPayment(clientSecret, {
|
||||||
|
payment_method: {
|
||||||
|
card,
|
||||||
|
billing_details: {
|
||||||
|
name: cart.billing_address?.first_name,
|
||||||
|
email: cart.email,
|
||||||
|
phone: cart.billing_address?.phone,
|
||||||
|
address: {
|
||||||
|
city: cart.billing_address?.city,
|
||||||
|
country: cart.billing_address?.country_code,
|
||||||
|
line1: cart.billing_address?.address_1,
|
||||||
|
line2: cart.billing_address?.address_2,
|
||||||
|
postal_code: cart.billing_address?.postal_code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ error }) => {
|
||||||
|
if (error) {
|
||||||
|
// TODO handle errors
|
||||||
|
console.error(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
`http://localhost:9000/store/carts/${cart.id}/complete`,
|
||||||
|
{
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ type, cart, order, error }) => {
|
||||||
|
if (type === "cart" && cart) {
|
||||||
|
// an error occured
|
||||||
|
console.error(error)
|
||||||
|
} else if (type === "order" && order) {
|
||||||
|
// TODO redirect to order success page
|
||||||
|
alert("Order placed.")
|
||||||
|
console.log(order)
|
||||||
|
refreshCart()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<CardElement />
|
||||||
|
<button
|
||||||
|
onClick={handlePayment}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Place Order
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
For example, you can use it in the `getPaymentUi` function defined in the [Payment Checkout Step guide](../page.mdx):
|
||||||
|
|
||||||
|
```tsx highlights={[["10"]]}
|
||||||
|
const getPaymentUi = useCallback(() => {
|
||||||
|
const activePaymentSession = cart?.payment_collection?.
|
||||||
|
payment_sessions?.[0]
|
||||||
|
if (!activePaymentSession) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(true) {
|
||||||
|
case activePaymentSession.provider_id.startsWith("pp_stripe_"):
|
||||||
|
return <StripePayment />
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
} , [cart])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## More Resources
|
||||||
|
|
||||||
|
Refer to [Stripe's documentation](https://docs.stripe.com/) for more details on integrating it in your storefront.
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { CodeTabs, CodeTab } from "docs-ui"
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: `Checkout Step 3: Choose Shipping Method`,
|
||||||
|
}
|
||||||
|
|
||||||
|
# {metadata.title}
|
||||||
|
|
||||||
|
In the third step of the checkout flow, the customer chooses the shipping method to receive their order's items.
|
||||||
|
|
||||||
|
To do that, you:
|
||||||
|
|
||||||
|
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. 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.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
<CodeTabs group="store-request">
|
||||||
|
<CodeTab label="Fetch API" value="fetch">
|
||||||
|
|
||||||
|
export const fetchHighlights = [
|
||||||
|
["3", "retrieveShippingOptions", "This function retrieves the shipping options of the customer's cart."],
|
||||||
|
["16", "setShippingMethod", "This function sets the shipping method of the cart using the selected shipping option."],
|
||||||
|
["29", "data", "Pass in this property any data relevant to the fulfillment provider."],
|
||||||
|
]
|
||||||
|
|
||||||
|
```ts highlights={fetchHighlights}
|
||||||
|
const cartId = localStorage.getItem("cart_id")
|
||||||
|
|
||||||
|
const retrieveShippingOptions = () => {
|
||||||
|
const { shipping_options } = await fetch(
|
||||||
|
`http://localhost:9000/store/shipping-options?cart_id=${
|
||||||
|
cart.id
|
||||||
|
}`, {
|
||||||
|
credentials: "include"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
return shipping_options
|
||||||
|
}
|
||||||
|
|
||||||
|
const setShippingMethod = (
|
||||||
|
selectedShippingOptionId: string
|
||||||
|
) => {
|
||||||
|
fetch(`http://localhost:9000/store/carts/${
|
||||||
|
cart.id
|
||||||
|
}/shipping-methods`, {
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
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."],
|
||||||
|
["22", "fetch", "Retrieve available shipping method of the customer's cart."],
|
||||||
|
["43", "fetch", "Set the cart's shipping method using the selected shipping option."],
|
||||||
|
["53", "data", "Pass in this property any data relevant to the fulfillment provider."]
|
||||||
|
]
|
||||||
|
|
||||||
|
```tsx highlights={highlights}
|
||||||
|
"use client" // include with Next.js 13+
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useCart } from "../../../providers/cart"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
export default function CheckoutShippingStep () {
|
||||||
|
const { cart, setCart } = useCart()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [shippingOptions, setShippingOptions] = useState<
|
||||||
|
HttpTypes.StoreCartShippingOption[]
|
||||||
|
>([])
|
||||||
|
const [
|
||||||
|
selectedShippingOption,
|
||||||
|
setSelectedShippingOption
|
||||||
|
] = useState<string | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cart) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetch(`http://localhost:9000/store/shipping-options?cart_id=${
|
||||||
|
cart.id
|
||||||
|
}`, {
|
||||||
|
credentials: "include"
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ shipping_options }) => {
|
||||||
|
setShippingOptions(shipping_options)
|
||||||
|
})
|
||||||
|
}, [cart])
|
||||||
|
|
||||||
|
const setShipping = (
|
||||||
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
if (!cart || !selectedShippingOption) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
fetch(`http://localhost:9000/store/carts/${
|
||||||
|
cart.id
|
||||||
|
}/shipping-methods`, {
|
||||||
|
credentials: "include",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
option_id: selectedShippingOption,
|
||||||
|
data: {
|
||||||
|
// TODO add any data necessary for
|
||||||
|
// fulfillment provider
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ cart: updatedCart }) => {
|
||||||
|
setCart(updatedCart)
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{loading || !cart && <span>Loading...</span>}
|
||||||
|
<form>
|
||||||
|
<select
|
||||||
|
value={selectedShippingOption}
|
||||||
|
onChange={(e) => setSelectedShippingOption(
|
||||||
|
e.target.value
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{shippingOptions.map((shippingOption) => (
|
||||||
|
<option
|
||||||
|
key={shippingOption.id}
|
||||||
|
value={shippingOption.id}
|
||||||
|
>
|
||||||
|
{shippingOption.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
disabled={loading || !cart}
|
||||||
|
onClick={setShipping}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeTab>
|
||||||
|
</CodeTabs>
|
||||||
|
|
||||||
|
In the example above, you:
|
||||||
|
|
||||||
|
- Retrieve the available shipping options of the cart to allow the customer to select from them.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## data Request Body Parameter
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -895,6 +895,34 @@ export const filesMap = [
|
|||||||
"filePath": "/www/apps/resources/app/storefront-development/cart/update/page.mdx",
|
"filePath": "/www/apps/resources/app/storefront-development/cart/update/page.mdx",
|
||||||
"pathname": "/storefront-development/cart/update"
|
"pathname": "/storefront-development/cart/update"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"filePath": "/www/apps/resources/app/storefront-development/checkout/address/page.mdx",
|
||||||
|
"pathname": "/storefront-development/checkout/address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filePath": "/www/apps/resources/app/storefront-development/checkout/complete-cart/page.mdx",
|
||||||
|
"pathname": "/storefront-development/checkout/complete-cart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filePath": "/www/apps/resources/app/storefront-development/checkout/email/page.mdx",
|
||||||
|
"pathname": "/storefront-development/checkout/email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filePath": "/www/apps/resources/app/storefront-development/checkout/page.mdx",
|
||||||
|
"pathname": "/storefront-development/checkout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filePath": "/www/apps/resources/app/storefront-development/checkout/payment/page.mdx",
|
||||||
|
"pathname": "/storefront-development/checkout/payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filePath": "/www/apps/resources/app/storefront-development/checkout/payment/stripe/page.mdx",
|
||||||
|
"pathname": "/storefront-development/checkout/payment/stripe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filePath": "/www/apps/resources/app/storefront-development/checkout/shipping/page.mdx",
|
||||||
|
"pathname": "/storefront-development/checkout/shipping"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"filePath": "/www/apps/resources/app/storefront-development/page.mdx",
|
"filePath": "/www/apps/resources/app/storefront-development/page.mdx",
|
||||||
"pathname": "/storefront-development"
|
"pathname": "/storefront-development"
|
||||||
|
|||||||
@@ -7283,6 +7283,57 @@ export const generatedSidebar = [
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"path": "/storefront-development/checkout",
|
||||||
|
"title": "Checkout",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"path": "/storefront-development/checkout/email",
|
||||||
|
"title": "1. Enter Email",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"path": "/storefront-development/checkout/address",
|
||||||
|
"title": "2. Enter Address",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"path": "/storefront-development/checkout/shipping",
|
||||||
|
"title": "3. Choose Shipping Method",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"path": "/storefront-development/checkout/payment",
|
||||||
|
"title": "4. Choose Payment Provider",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"path": "/storefront-development/checkout/payment/stripe",
|
||||||
|
"title": "Example: Stripe",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"loaded": true,
|
||||||
|
"isPathHref": true,
|
||||||
|
"path": "/storefront-development/checkout/complete-cart",
|
||||||
|
"title": "5. Complete Cart",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1897,6 +1897,38 @@ export const sidebar = sidebarAttachHrefCommonOptions([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/storefront-development/checkout",
|
||||||
|
title: "Checkout",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/storefront-development/checkout/email",
|
||||||
|
title: "1. Enter Email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/storefront-development/checkout/address",
|
||||||
|
title: "2. Enter Address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/storefront-development/checkout/shipping",
|
||||||
|
title: "3. Choose Shipping Method",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/storefront-development/checkout/payment",
|
||||||
|
title: "4. Choose Payment Provider",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/storefront-development/checkout/payment/stripe",
|
||||||
|
title: "Example: Stripe",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/storefront-development/checkout/complete-cart",
|
||||||
|
title: "5. Complete Cart",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export const CodeBlockLine = ({
|
|||||||
tooltipClassName="font-base"
|
tooltipClassName="font-base"
|
||||||
render={({ content }) => (
|
render={({ content }) => (
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
allowedElements={["a", "strong", "code"]}
|
allowedElements={["a", "strong", "code", "br"]}
|
||||||
unwrapDisallowed={true}
|
unwrapDisallowed={true}
|
||||||
>
|
>
|
||||||
{content || ""}
|
{content || ""}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const Tooltip = ({
|
|||||||
"!text-compact-x-small !shadow-elevation-tooltip dark:!shadow-elevation-tooltip-dark !rounded-docs_DEFAULT",
|
"!text-compact-x-small !shadow-elevation-tooltip dark:!shadow-elevation-tooltip-dark !rounded-docs_DEFAULT",
|
||||||
"!py-docs_0.25 !z-[399] hidden !px-docs_0.5 lg:block",
|
"!py-docs_0.25 !z-[399] hidden !px-docs_0.5 lg:block",
|
||||||
"!bg-medusa-bg-component",
|
"!bg-medusa-bg-component",
|
||||||
"!text-medusa-fg-base",
|
"!text-medusa-fg-base text-center",
|
||||||
tooltipClassName
|
tooltipClassName
|
||||||
)}
|
)}
|
||||||
wrapper="span"
|
wrapper="span"
|
||||||
|
|||||||
Reference in new issue
Block a user