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:
Shahed Nasser
2024-06-12 19:46:06 +02:00
committed by GitHub
parent 7e7e6e3311
commit 85d487d90b
13 changed files with 1375 additions and 11 deletions
@@ -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:
![A diagram illustrating the flow of the payment step](https://res.cloudinary.com/dza7lstvk/image/upload/v1718029777/Medusa%20Resources/storefront-payment_dxry7l.jpg)
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.