Use Saved Payment Methods During Checkout

In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around Commerce Modules which are available out-of-the-box.

Medusa's architecture facilitates integrating third-party services, such as payment providers. These payment providers can process payments and securely store customers' payment methods for future use.

In this tutorial, you'll expand on Medusa's Stripe Module Provider to allow customers to re-use their saved payment methods during checkout.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

Note: While this tutorial uses Stripe as an example, you can follow the same steps to implement saved payment methods with other payment providers.

Summary#

By following this tutorial, you'll learn how to:

  • Install and set up Medusa and the Next.js Starter Storefront.
  • Set up the Stripe Module Provider in Medusa.
  • Customize the checkout flow to save customers' payment methods.
  • Allow customers to select saved payment methods during checkout.

Diagram illustrating the features of this guide

Saved Payment Methods Repository
Find the full code for this guide in this repository.

Step 1: Install a Medusa Application#

Start by installing the Medusa application on your machine with the following command:

Terminal
npx create-medusa-app@latest

You'll first be asked for the project's name. Then, when asked whether you want to install the Next.js starter storefront, choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the {project-name}-storefront name.

Why is the storefront installed separately: The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called API routes. Learn more in Medusa's Architecture documentation.

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Ran into Errors: Check out the troubleshooting guides for help.

Step 2: Set Up the Stripe Module Provider#

Medusa's Payment Module provides payment-related models and the interface to manage and process payments. However, it delegates the actual payment processing to module providers that integrate third-party payment services.

The Stripe Module Provider is a Payment Module Provider that integrates Stripe into your Medusa application to process payments. It can also save payment methods securely.

In this section, you'll set up the Stripe Module Provider in your Medusa application.

Register the Stripe Module Provider#

To register the Stripe Module Provider in your Medusa application, add it to the array of providers passed to the Payment Module in medusa-config.ts:

medusa-config.ts
1module.exports = defineConfig({2  // ...3  modules: [4    {5      resolve: "@medusajs/medusa/payment",6      options: {7        providers: [8          {9            resolve: "@medusajs/medusa/payment-stripe",10            id: "stripe",11            options: {12              apiKey: process.env.STRIPE_API_KEY,13            },14          },15        ],16      },17    },18  ],19})

The Medusa configuration accepts a modules array, which contains the modules to be loaded. While the Payment Module is loaded by default, you need to add it again when registering a new provider.

You register provides in the providers option of the Payment Module. Each provider is an object with the following properties:

  • resolve: The package name of the provider.
  • id: The ID of the provider. This is used to identify the provider in the Medusa application.
  • options: The options to be passed to the provider. In this case, the apiKey option is required for the Stripe Module Provider.
Note: Learn about other options in the Stripe Module Provider documentation.

Add Environment Variables#

Next, add the following environment variables to your .env file:

Code
STRIPE_API_KEY=sk_...

Where STRIPE_API_KEY is your Stripe Secret API Key. You can find it in the Stripe dashboard under Developers > API keys.

Secret API Key in the Stripe dashboard

Enable Stripe in a Region#

In Medusa, each region (which is a geographical area where your store operates) can have different payment methods enabled. So, after registering the Stripe Module Provider, you need to enable it in a region.

To enable it in a region, start the Medusa application with the following command:

Then, go to localhost:9000/app and log in with the user you created earlier.

Once you're logged in:

  1. Go to Settings -> Regions.
  2. Click on the region where you want to enable the payment provider.
  3. Click the icon at the top right of the first section
  4. Choose "Edit" from the dropdown menu
  5. In the side window that opens, find the "Payment Providers" field and select Stripe from the dropdown.
  6. Once you're done, click the "Save" button.

Stripe will now be available as a payment option during checkout.

Note: The Stripe Module Provider supports different payment methods in Stripe, such as Bancontact or iDEAL. This guide focuses only on the card payment method, but you can enable other payment methods as well.

Stripe in the dropdown

Add Evnironement Variable to Storefront#

The Next.js Starter Storefront supports payment with Stripe during checkout if it's enabled in the region.

Reminder: 

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is {your-project}-storefront.

So, if your Medusa application's directory is medusa-payment, you can find the storefront by going back to the parent directory and changing to the medusa-payment-storefront directory:

Terminal
cd ../medusa-payment-storefront # change based on your project name

In the Next.js Starter Storefront project, add the Stripe public API key as an environment variable in .env.local:

Storefront
Code
NEXT_PUBLIC_STRIPE_KEY=pk_123...

Where NEXT_PUBLIC_STRIPE_KEY is your Stripe public API key. You can find it in the Stripe dashboard under Developers > API keys.


Step 3: List Payment Methods API Route#

The Payment Module uses account holders to represent a customer's details that are stored in a third-party payment provider. Medusa creates an account holder for each customer, allowing you later to retrieve the customer's saved payment methods in the third-party provider.

Diagram illustrating the relation between customers and account holders in Medusa, and customers in Stripe

While this feature is available out-of-the-box, you need to expose it to clients, like storefronts, by creating an API route. An API Route is an endpoint that exposes commerce features to external applications and clients.

In this step, you'll create an API route that lists the saved payment methods for an authenticated customer.

Note: Refer to the API routes documentation to learn more.

Create API Route#

An API route is created in a route.ts file under a sub-directory of the src/api directory. The path of the API route is the file's path relative to src/api, and it can include path parameters using square brackets.

So, to create an API route at the path /store/payment-methods/:account-holder-id, create the file src/api/store/payment-methods/[account_holder_id]/route.ts with the following content:

src/api/store/payment-methods/[account_holder_id]/route.ts
1import { MedusaError } from "@medusajs/framework/utils"2import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"3
4export async function GET(5  req: MedusaRequest,6  res: MedusaResponse7) {8  const { account_holder_id } = req.params9  const query = req.scope.resolve("query")10  const paymentModuleService = req.scope.resolve("payment")11
12  const { data: [accountHolder] } = await query.graph({13    entity: "account_holder",14    fields: [15      "data",16      "provider_id",17    ],18    filters: {19      id: account_holder_id,20    },21  })22
23  if (!accountHolder) {24    throw new MedusaError(25      MedusaError.Types.NOT_FOUND, 26      "Account holder not found"27    )28  }29
30  const paymentMethods = await paymentModuleService.listPaymentMethods(31    {32      provider_id: accountHolder.provider_id,33      context: {34        account_holder: {35          data: {36            id: accountHolder.data.id,37          },38        },39      },40    }41  )42
43  res.json({44    payment_methods: paymentMethods,45  })46}

Since you export a route handler function named GET, you expose a GET API route at /store/payment-methods/:account-holder-id. The route handler function accepts two parameters:

  1. A request object with details and context on the request, such as body parameters or authenticated customer details.
  2. A response object to manipulate and send the response.

The request object has a scope property, which is an instance of the Medusa container. The Medusa container is a registry of Framework and commerce tools that you can access in the API route.

You use the Medusa container to resolve:

  • Query, which is a tool that retrieves data across modules in the Medusa application.
  • The Payment Module's service, which provides an interface to manage and process payments with third-party providers.

You use Query to retrieve the account holder with the ID passed as a path parameter. If the account holder is not found, you throw an error.

Then, you use the listPaymentMethods method of the Payment Module's service to retrieve the payment providers saved in the third-party provider. The method accepts an object with the following properties:

  • provider_id: The ID of the provider, such as Stripe's ID. The account holder stores the ID its associated provider.
  • context: The context of the request. In this case, you pass the account holder's ID to retrieve the payment methods associated with it in the third-party provider.

Finally, you return the payment methods in the response.

Protect API Route#

Only authenticated customers can access and use saved payment methods. So, you need to protect the API route to ensure that only authenticated customers can access it.

To protect an API route, you can add a middleware. A middleware is a function executed when a request is sent to an API Route. You can add an authentication middleware that ensures that the request is authenticated before executing the route handler function.

Note: Refer to the Middlewares documentation to learn more.

Middlewares are added in the src/api/middlewares.ts file. So, create the file with the following content:

src/api/middlewares.ts
1import { authenticate, defineMiddlewares } from "@medusajs/framework/http"2
3export default defineMiddlewares({4  routes: [5    {6      matcher: "/store/payment-methods/:provider_id/:account_holder_id",7      method: "GET",8      middlewares: [9        authenticate("customer", ["bearer", "session"]),10      ],11    },12  ],13})

The src/api/middlewares.ts file must use the defineMiddlewares function and export its result. The defineMiddlewares function accepts a routes array that accepts objects with the following properties:

  • matcher: The path of the API route to apply the middleware to.
  • method: The HTTP method of the API route to apply the middleware to.
  • middlewares: An array of middlewares to apply to the API route.

You apply the authenticate middleware to the API route you created earlier. The authenticate middleware ensures that only authenticated customers can access the API route.

Note: Refer to the Protected Routes documentation to learn more about the authenticate middleware.

Your API route can now only be accessed by authenticated customers. You'll test it out as you customize the Next.js Starter Storefront in the next steps.


Step 4: Save Payment Methods During Checkout#

In this step, you'll customize the checkout flow in the Next.js Starter Storefront to save payment methods during checkout.

Reminder: 

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is {your-project}-storefront.

So, if your Medusa application's directory is medusa-payment, you can find the storefront by going back to the parent directory and changing to the medusa-payment-storefront directory:

Terminal
cd ../medusa-payment-storefront # change based on your project name

During checkout, when the customer chooses a payment method, such as Stripe, the Next.js Starter Storefront creates a payment session in Medusa using the Initialize Payment Session API route.

Under the hood, Medusa uses the associated payment provider (Stripe) to initiate the payment process with the associated third-party payment provider. The Initialize Payment Session API route accepts a data object parameter in the request body that allows you to pass data relevant to the third-party payment provider.

So, to save the payment method that the customer uses during checkout with Stripe, you must pass the setup_future_usage property in the data object. The setup_future_usage property is a Stripe-specific property that allows you to save the payment method for future use.

In src/modules/checkout/components/payment/index.tsx of the Next.js Starter Storefront, there are two uses of the initiatePaymentSession function. Update each of them to pass the data property:

Storefront
src/modules/checkout/components/payment/index.tsx
1// update in two places2await initiatePaymentSession(cart, {3  // ...4  data: {5    setup_future_usage: "off_session",6  },7})

You customize the initiatePaymentSession function to pass the data object with the setup_future_usage property. You set the value to off_session to allow using the payment method outside of the checkout flow, such as for follow up payments. You can use on_session instead if you only want the payment method to be used by the customer during checkout.

Note: By making this change, you always save the payment method that the customer uses during checkout. You can alternatively show a checkbox to confirm saving the payment method, and only pass the data object if the customer checks it.

Test it Out#

To test it out, start the Medusa application by running the following command in the Medusa application's directory:

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

You can open the storefront in your browser at localhost:8000. Then, create a new customer account by clicking on the "Account" link at the top right.

After creating an account and logging in, add a product to the cart and go to the checkout page. Once you get to the payment step, choose Stripe and enter a test card number, such as 4242 4242 4242 4242.

Then, place the order. Once the order is placed, you can check on the Stripe dashboard that the payment method was saved by:

  1. Going to the "Customers" section in the Stripe dashboard.
  2. Clicking on the customer you just placed the order with.
  3. Scrolling down to the "Payment methods" section. You'll find the payment method you just used to place the order.

Saved payment method on the Stripe dashboard

In the next step, you'll show the saved payment methods to the customer during checkout and allow them to select one of them.


Step 5: Use Saved Payment Methods During Checkout#

In this step, you'll customize the checkout flow in the Next.js Starter Storefront to show the saved payment methods to the customer and allow them to select one of them to place the order.

Retrieve Saved Payment Methods#

To retrieve the saved payment methods, you'll add a server function that retrieves the customer's saved payment methods from the API route you created earlier.

Add the following in src/lib/data/payment.ts:

Storefront
src/lib/data/payment.ts
1export type SavedPaymentMethod = {2  id: string3  provider_id: string4  data: {5    card: {6      brand: string7      last4: string8      exp_month: number9      exp_year: number10    }11  }12}13
14export const getSavedPaymentMethods = async (accountHolderId: string) => {15  const headers = {16    ...(await getAuthHeaders()),17  }18
19  return sdk.client.fetch<{20    payment_methods: SavedPaymentMethod[]21  }>(22    `/store/payment-methods/${accountHolderId}`,23    {24      method: "GET",25      headers,26    }27  ).catch(() => {28    return {29      payment_methods: [],30    }31  })32}

You define a type for the retrieved payment methods. It contains the following properties:

  • id: The ID of the payment method in the third-party provider.
  • provider_id: The ID of the provider in the Medusa application, such as Stripe's ID.
  • data: Additional data retrieved from the third-party provider related to the saved payment method. The type is modeled after the data returned by Stripe, but you can change it to match other payment providers.

You also create a getSavedPaymentMethods function that retrieves the saved payment methods from the API route you created earlier. The function accepts the account holder ID as a parameter and returns the saved payment methods.

Add Saved Payment Methods Component#

Next, you'll add the component that shows the saved payment methods and allows the customer to select one of them.

The component that shows the Stripe card element is defined in src/modules/checkout/components/payment-container/index.tsx. So, you'll define the component for the saved payment methods in the same file.

Start by adding the following import statements at the top of the file:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1import { Button } from "@medusajs/ui"2import { useEffect, useState } from "react"3import { HttpTypes } from "@medusajs/types"4import { SavedPaymentMethod, getSavedPaymentMethods } from "@lib/data/payment"5import { initiatePaymentSession } from "../../../../lib/data/cart"6import { capitalize } from "lodash"

Then, update the PaymentContainerProps type to include the payment session and cart details:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1type PaymentContainerProps = {2  // ...3  paymentSession?: HttpTypes.StorePaymentSession4  cart: HttpTypes.StoreCart5}

You'll need these details to find which saved payment method the customer selected, and to initiate a new payment session for the cart when the customer chooses a saved payment method.

Next, add the following component at the end of the file:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1const StripeSavedPaymentMethodsContainer = ({2  paymentSession,3  setCardComplete,4  setCardBrand,5  setError,6  cart,7}: {8  paymentSession?: HttpTypes.StorePaymentSession9  setCardComplete: (complete: boolean) => void10  setCardBrand: (brand: string) => void11  setError: (error: string | null) => void12  cart: HttpTypes.StoreCart13}) => {14  const [savedPaymentMethods, setSavedPaymentMethods] = useState<15    SavedPaymentMethod[]16  >([])17  const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<18    string | undefined19  >(20    paymentSession?.data?.payment_method_id as string | undefined21  )22
23  useEffect(() => {24    const accountHolderId = (25      paymentSession?.context?.account_holder as Record<string, string>26    )27      ?.id28
29    if (!accountHolderId) {30      return31    }32
33    getSavedPaymentMethods(accountHolderId)34      .then(({ payment_methods }) => {35        setSavedPaymentMethods(payment_methods)36      })37  }, [paymentSession])38
39  useEffect(() => {40    if (!selectedPaymentMethod || !savedPaymentMethods.length) {41      setCardComplete(false)42      setCardBrand("")43      setError(null)44      return45    }46    const selectedMethod = savedPaymentMethods.find(47      (method) => method.id === selectedPaymentMethod48    )49
50    if (!selectedMethod) {51      return52    }53
54    setCardBrand(capitalize(selectedMethod.data.card.brand))55    setCardComplete(true)56    setError(null)57  }, [selectedPaymentMethod, savedPaymentMethods])58
59  const handleSelect = async (method: SavedPaymentMethod) => {60    // initiate a new payment session with the selected payment method61    await initiatePaymentSession(cart, {62      provider_id: method.provider_id,63      data: {64        payment_method_id: method.id,65      },66    }).catch((error) => {67      setError(error.message)68    })69
70    setSelectedPaymentMethod(method.id)71  }72
73  if (!savedPaymentMethods.length) {74    return <></>75  }76
77  // TODO add return statement78}

You define a StripeSavedPaymentMethodsContainer component that accepts the following props:

  • paymentSession: The cart's current payment session.
  • setCardComplete: A function to tell parent components whether the cart or payment method selection is complete. This allows the customer to proceed to the next step in the checkout flow.
  • setCardBrand: A function to set the brand of the selected payment method. This is useful to show the brand of the selected payment method in review sections of the checkout flow.
  • setError: A function to set the error message in case of an error.
  • cart: The cart's details.

In the component, you define a state variable to store the saved payment methods and another one to store the selected payment method.

Then, you use the useEffect hook to retrieve the saved payment methods for the account holder set in the cart's payment session. You use the getSavedPaymentMethods function you created earlier to retrieve the saved payment methods.

You also use another useEffect hook that is executed when the selected payment method changes. In this hook, you check if the selected payment method is valid and set the card brand and completion status accordingly.

Finally, you define a handleSelect function that you'll execute when the customer selects a saved payment method. It creates a new payment session with the selected payment method.

To show the saved payment methods, replace the TODO with the following return statement:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1return (2  <div className="flex flex-col gap-y-2">3    <Text className="txt-medium-plus text-ui-fg-base">4      Choose a saved payment method:5    </Text>6    {savedPaymentMethods.map((method) => (7      <div 8        key={method.id}9        className={`flex items-center justify-between p-4 border rounded-lg cursor-pointer hover:border-ui-border-interactive ${10          selectedPaymentMethod === method.id ? "border-ui-border-interactive" : ""11        }`}12        role="button"13        onClick={() => handleSelect(method)}14      >15        <div className="flex items-center gap-x-4">16          <input17            type="radio"18            name="saved-payment-method" 19            value={method.id}20            checked={selectedPaymentMethod === method.id}21            className="h-4 w-4 text-ui-fg-interactive"22            onChange={(e) => {23              if (e.target.checked) {24                handleSelect(method)25              }26            }}27          />28          <div className="flex flex-col">29            <span className="text-sm font-medium text-ui-fg-base">30              {capitalize(method.data.card.brand)} •••• {method.data.card.last4}31            </span>32            <span className="text-xs text-ui-fg-subtle">33              Expires {method.data.card.exp_month}/{method.data.card.exp_year}34            </span>35          </div>36        </div>37      </div>38    ))}39  </div>40)

You display the saved payment methods as radio buttons. When the customer selects one of them, you execute the handleSelect function to initiate a new payment session with the selected payment method.

Modify Existing Stripe Element#

Now that you have the component to show the saved payment methods, you need to modify the existing Stripe element to allow customers to select an existing payment method or enter a new one.

In the same src/modules/checkout/components/payment-container/index.tsx file, expand the new paymentSession and cart props of the StripeCardContainer component:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1export const StripeCardContainer = ({2  // ...3  paymentSession,4  cart,5}: Omit<PaymentContainerProps, "children"> & {6  // ...7}) => {8  // ...9}

Then, add a new state variable that keeps track of whether the customer is using a saved payment method or entering a new one:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1const [isUsingSavedPaymentMethod, setIsUsingSavedPaymentMethod] = useState(2  paymentSession?.data?.payment_method_id !== undefined3)

Next, add a function that resets the payment session when the customer switches between saved and new payment methods:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1const handleRefreshSession = async () => {2  await initiatePaymentSession(cart, {3    provider_id: paymentProviderId,4  })5  setIsUsingSavedPaymentMethod(false)6}

This function initiates a new payment session for the cart and disables the isUsingSavedPaymentMethod state variable.

Finally, replace the return statement of the StripeCardContainer component with the following:

Storefront
src/modules/checkout/components/payment-container/index.tsx
1return (2  <PaymentContainer3    paymentProviderId={paymentProviderId}4    selectedPaymentOptionId={selectedPaymentOptionId}5    paymentInfoMap={paymentInfoMap}6    disabled={disabled}7    paymentSession={paymentSession}8    cart={cart}9  >10    {selectedPaymentOptionId === paymentProviderId &&11      (stripeReady ? (12        <div className="my-4 transition-all duration-150 ease-in-out">13          <StripeSavedPaymentMethodsContainer14            setCardComplete={setCardComplete}15            setCardBrand={setCardBrand}16            setError={setError}17            paymentSession={paymentSession}18            cart={cart}19          />20          {isUsingSavedPaymentMethod && (21            <Button22              variant="secondary" 23              size="small" 24              className="mt-2" 25              onClick={handleRefreshSession}26            >27              Use a new payment method28            </Button>29          )}30          {!isUsingSavedPaymentMethod && (31            <>32              <Text className="txt-medium-plus text-ui-fg-base my-1">33                Enter your card details:34              </Text>35              <CardElement36                options={useOptions as StripeCardElementOptions}37                onChange={(e) => {38                  setCardBrand(39                    e.brand && e.brand.charAt(0).toUpperCase() + e.brand.slice(1)40                  )41                  setError(e.error?.message || null)42                  setCardComplete(e.complete)43                  }}44                />              45            </>46          )}47        </div>48      ) : (49        <SkeletonCardDetails />50      ))}51  </PaymentContainer>52)

You update the return statement to:

  • Pass the new paymentSession and cart props to the PaymentContainer component.
  • Show the StripeSavedPaymentMethodsContainer component before Stripe's card element.
  • Add a button that's shown when the customer selects a saved payment method. The button allows the customer to switch back to entering a new payment method.

The existing Stripe element in checkout will now show the saved payment methods to the customer along with the component to enter a card's details.

Since you added new props to the StripeCardContainer and PaymentContainer components, you need to update other components that use them to pass the props.

In src/modules/checkout/components/payment/index.tsx, find usages of StripeCardContainer and PaymentContainer in the return statement and add the paymentSession and cart props:

Storefront
src/modules/checkout/components/payment/index.tsx
1<div key={paymentMethod.id}>2  {isStripeFunc(paymentMethod.id) ? (3    <StripeCardContainer4      // ...5      paymentSession={activeSession}6      cart={cart}7    />8  ) : (9    <PaymentContainer10      // ...11      paymentSession={activeSession}12      cart={cart}13    />14  )}15</div>

Support Updating Stripe's Client Secret#

The Next.js Starter Storefront uses Stripe's Elements component to wrap the payment elements. The Elements component requires a clientSecret prop, which is available in the cart's payment session.

With the recent changes, the client secret will be updated whenever a payment session is initiated, such as when the customer selects a saved payment method. However, the options.clientSecret prop of the Elements component is immutable, meaning that it cannot be changed after the component is mounted.

To force the component to re-mount and update the clientSecret prop, you can add a key prop to the Elements component. The key prop ensures that the Elements component re-mounts whenever the client secret changes, allowing Stripe to process the updated payment session.

In src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx, find the Elements component in the return statement and add the key prop:

Storefront
src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx
1<Elements 2  options={options} 3  stripe={stripePromise} 4  key={options.clientSecret}5>6  {children}7</Elements>

You set the key prop to the client secret, which forces the Elements component to re-mount whenever the client secret changes.

Support Payment with Saved Payment Method#

The last change you need to make ensures that the customer can place an order with a saved payment method.

When the customer places the order, and they've chosen Stripe as a payment method, the Next.js Starter Storefront uses Stripe's confirmCardPayment method to confirm the payment. This method accepts either the ID of a saved payment method, or the details of a new card.

So, you need to update the confirmCardPayment usage to support passing the ID of the selected payment method if the customer has selected one.

In src/modules/checkout/components/payment-button/index.tsx, find the handlePayment method and update its first if condition:

Storefront
src/modules/checkout/components/payment-button/index.tsx
1if (!stripe || !elements || (!card && !session?.data.payment_method_id) || !cart) {2  setSubmitting(false)3  return4}

This allows the customer to place their order if they have selected a saved payment method but have not entered a new card.

Then, find the usage of confirmCardPayment in the handlePayment function and change it to the following:

Storefront
src/modules/checkout/components/payment-button/index.tsx
1await stripe2.confirmCardPayment(session?.data.client_secret as string, {3  payment_method: session?.data.payment_method_id as string || {4    card: card!,5    billing_details: {6      name:7        cart.billing_address?.first_name +8        " " +9        cart.billing_address?.last_name,10      address: {11        city: cart.billing_address?.city ?? undefined,12        country: cart.billing_address?.country_code ?? undefined,13        line1: cart.billing_address?.address_1 ?? undefined,14        line2: cart.billing_address?.address_2 ?? undefined,15        postal_code: cart.billing_address?.postal_code ?? undefined,16        state: cart.billing_address?.province ?? undefined,17      },18      email: cart.email,19      phone: cart.billing_address?.phone ?? undefined,20    },21  },22})23.then(({ error, paymentIntent }) => {24  if (error) {25    const pi = error.payment_intent26
27    if (28      (pi && pi.status === "requires_capture") ||29      (pi && pi.status === "succeeded")30    ) {31      onPaymentCompleted()32    }33
34    setErrorMessage(error.message || null)35    return36  }37
38  if (39    (paymentIntent && paymentIntent.status === "requires_capture") ||40    paymentIntent.status === "succeeded"41  ) {42    return onPaymentCompleted()43  }44
45  return46})

In particular, you're changing the payment_method property to either be the ID of the selected payment method, or the details of a new card. This allows the customer to place an order with either a saved payment method or a new one.

Test it Out#

You can now test out placing orders with a saved payment method.

To do that, start the Medusa application by running the following command in the Medusa application's directory:

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

In the Next.js Starter Storefront, login with the customer account you created earlier and add a product to the cart.

Then, proceed to the checkout flow. In the payment step, you should see the saved payment method you used earlier. You can select it and place the order.

Saved payment method in checkout

Once the order is placed successfully, you can check it in the Medusa Admin dashboard. You can view the order and capture the payment.

Order in the Medusa Admin dashboard


Next Steps#

You've added support for saved payment methods in your Medusa application and Next.js Starter Storefront, allowing customers to save their payment methods during checkout and use them in future orders.

You can add more features to the saved payment methods, such as allowing customers to delete saved payment methods. You can use Stripe's APIs in the storefront or add an API route in Medusa to delete the saved payment method.

If you're new to Medusa, check out the main documentation, where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's Commerce Modules.

Was this page helpful?
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break