Back to Medusa

{metadata.title}

www/apps/resources/app/storefront-development/checkout/payment/page.mdx

2.14.211.2 KB
Original Source

import { CodeTabs, CodeTab } from "docs-ui"

export const metadata = { title: Checkout Step 4: Choose Payment Provider, }

{metadata.title}

In this guide, you'll learn how to implement the last step of the checkout flow, where the customer chooses the payment provider and performs any necessary actions. This is typically the fourth step of the checkout flow, but you can change the steps of the checkout flow as you see fit.

Payment Step Flow in Storefront Checkout

The payment step requires implementing the following flow:

  1. Retrieve the payment providers using the List Payment Providers API route.
  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 using the Create Payment Collection API route.
  4. Initialize the payment sessions of the cart's payment collection using the Initialize Payment Sessions API route.
    • If you're using the JS SDK, it combines the third and fourth steps in a single initiatePaymentSession function.
  5. Optionally perform additional actions for payment based on the chosen payment provider. For example, if the customer chooses Stripe, you show them the UI to enter their card details.
    • You can refer to the Stripe guide for an example of how to implement this.

How to Implement the Payment Step Flow

For example, to implement the payment step flow:

<Note title="Tip"> </Note> <CodeTabs group="store-request"> <CodeTab label="React" value="react">

export const highlights = [ ["4", "useCart", "The useCart hook was defined in the Cart React Context documentation."], ["24", "listPaymentProviders", "Retrieve available payment providers."], ["29", "setSelectedPaymentProvider", "If a payment provider was selected before, pre-fill it."], ["35", "handleSelectProvider", "This function is executed when the customer submits their chosen payment provider."], ["45", "initiatePaymentSession", "Create a payment collection and initialize the payment session for the chosen provider."], ["50", "retrieve", "Retrieve the cart again to update its data."], ["56", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."], ["57", "activePaymentSession", "The active session is the first in the payment collection's sessions."], ["62", "", "Test which payment provider is chosen based on the prefix of the provider ID."], ["63", "pp_stripe_", "Check if the chosen provider is Stripe."], ["71", "pp_system_default", "Check if the chosen provider is the default system provider."], ["77", "default", "Handle unrecognized providers."], ["112", "getPaymentUi", "If a provider is chosen, render its UI."] ]

tsx
"use client" // include with Next.js 13+

import { useCallback, useEffect, useState } from "react"
import { useCart } from "@/providers/cart"
import { HttpTypes } from "@medusajs/types"
import { sdk } from "@/lib/sdk"

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
    }

    sdk.store.payment.listPaymentProviders({
      region_id: cart.region_id || "",
    })
    .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(true)

    await sdk.store.payment.initiatePaymentSession(cart, {
      provider_id: selectedPaymentProvider,
    })

    // re-fetch cart
    const { cart: updatedCart } = await sdk.store.cart.retrieve(cart.id)

    setCart(updatedCart)
    setLoading(false)
  }

  const getPaymentUi = useCallback(() => {
    const activePaymentSession = cart?.payment_collection?.payment_sessions?.[0]
    if (!activePaymentSession) {
      return
    }

    switch(true) {
      case activePaymentSession.provider_id.startsWith("pp_stripe_"):
        return (
          <span>
            You chose stripe!
          </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> <CodeTab label="JS SDK" value="js-sdk">

export const fetchHighlights = [ ["8", "retrievePaymentProviders", "This function retrieves the payment provider that the customer can choose from."], ["9", "listPaymentProviders", "Retrieve available payment providers."], ["16", "selectPaymentProvider", "This function is executed when the customer submits their chosen payment provider."], ["19", "initiatePaymentSession", "Create a payment collection and initialize the payment session for the chosen provider."], ["26", "retrieve", "Retrieve the cart again to update its data."], ["31", "getPaymentUi", "This function shows the necessary UI based on the selected payment provider."], ["32", "activePaymentSession", "The active session is the first in the payment collection's sessions."], ["38", "", "Test which payment provider is chosen based on the prefix of the provider ID."], ["39", "pp_stripe_", "Check if the chosen provider is Stripe."], ["43", "pp_system_default", "Check if the chosen provider is the default system provider."], ["45", "default", "Handle unrecognized providers."], ["52", "handlePayment", "The function that handles the payment process using the above functions."] ]

ts
// assuming the cart is previously fetched
const cart = {
  id: "cart_123",
  region_id: "reg_123",
  // cart object...
}

const retrievePaymentProviders = async () => {
  const { payment_providers } = await sdk.store.payment.listPaymentProviders({
    region_id: cart.region_id || "",
  })

  return payment_providers
}

const selectPaymentProvider = async (
  selectedPaymentProviderId: string
) => {
  await sdk.store.payment.initiatePaymentSession(cart, {
    provider_id: selectedPaymentProviderId,
  })

  // re-fetch cart
  const { 
    cart: updatedCart,
  } = await sdk.store.cart.retrieve(cart.id)

  return updatedCart
}

const getPaymentUi = () => {
  const activePaymentSession = cart?.payment_collection?.
    payment_sessions?.[0]
  if (!activePaymentSession) {
    return
  }

  switch(true) {
    case activePaymentSession.provider_id.startsWith("pp_stripe_"):
      // TODO handle Stripe UI
      return "You chose stripe!"
    case activePaymentSession.provider_id
      .startsWith("pp_system_default"):
      return "You chose manual payment! No additional actions required."
    default:
      return `You chose ${
        activePaymentSession.provider_id
      } which is in development.`
  }
}

const handlePayment = () => {
  retrievePaymentProviders()

  // ... customer chooses payment provider
  // const providerId = ...

  selectPaymentProvider(providerId)

  getPaymentUi()
}
</CodeTab> </CodeTabs>

In the example above, you:

  • Retrieve the payment providers from the Medusa application using the List Payment Providers API route. You use those to show the customer the available options.
  • When the customer chooses a payment provider, you use the initiatePaymentSession function to create a payment collection and initialize the payment session for the chosen provider.
  • Once the cart has a payment session, you optionally render the UI to perform additional actions. For example, if the customer chose Stripe, you can show them the card form to enter their credit card.

In the Fetch API example, the handlePayment function implements this flow by calling the different functions in the correct order.


Troubleshooting

Unknown Error for Zero Cart Total

If your cart has a total of 0, you might encounter an unknown error when trying to create a payment session.

Some payment providers, such as Stripe, require a non-zero amount to create a payment session. So, if your cart has a total of 0, the error will be thrown on the payment provider's side.

In those cases, you can either:

  • Make sure the payment session is only initialized when the cart has a total greater than 0.
  • Use payment providers like the Manual System Payment Provider, which doesn't create a payment session with a third-party provider.
    • The Manual System Payment Provider is available by default in Medusa and can be used to handle payments without a third-party provider. It allows you to mark the order as paid without requiring any additional actions from the customer.
    • Make sure to configure the Manual System Payment Provider in your store's region. Learn more in the Manage Region user guide.

Stripe Example

If you're integrating Stripe in your Medusa application and storefront, refer to the Stripe guide for an example of how to handle the payment process using Stripe.