npm package: @qbk/mercadopago-medusa-plugin
Description: A starter for Medusa plugins.
Author: Medusa
Latest version: 0.0.3

<p align="center">
  <a href="https://www.medusajs.com">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/59018053/229103275-b5e482bb-4601-46e6-8142-244f531cebdb.svg">
    <source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
    <img alt="Medusa logo" src="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
    </picture>
  </a>
</p>
<h1 align="center">
  Medusa Plugin Starter
</h1>

<h4 align="center">
  <a href="https://docs.medusajs.com">Documentation</a> |
  <a href="https://www.medusajs.com">Website</a>
</h4>

<p align="center">
  Building blocks for digital commerce
</p>
<p align="center">
  <a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md">
    <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
  </a>
    <a href="https://www.producthunt.com/posts/medusa"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a>
  <a href="https://discord.gg/xpCwq3Kfn8">
    <img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
  </a>
  <a href="https://twitter.com/intent/follow?screen_name=medusajs">
    <img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
  </a>
</p>

## 🧩 @qbk/mercadopago-medusa-plugin

A MedusaJS plugin to integrate Mercado Pago as a payment provider.

It supports credit/debit card payments, ticket payments, and allows you to save and list customer cards for future purchases.



## 📘 Useful Resources

MedusaJS Official Documentation

Mercado Pago API

Mercado Pago Node.js SDK

## Compatibility


| Requisito            | Versión mínima |
| -------------------- | -------------- |
| **MedusaJS**         | `>=2.9.0`       |
| **Node.js**          | `>=20`         |
| **Mercado Pago SDK** | `2.9.0`        |


## 💳 Features

✅ Credit and debit card payments

✅ Ticket payments (e.g., Pago Fácil, Rapipago)

✅ Payment authorization and capture (manual or automatic)

✅ Save customer cards for future purchases

✅ List saved cards

✅ Automatic payment status updates via webhooks

✅ Full integration with MedusaJS payment flow

## 🧠 Usage

Once configured, Medusa will recognize Mercado Pago as an available payment provider. You can create payments from your Storefront or Admin using the corresponding provider_id:

pp_credit_mercadopago

pp_debit_mercadopago

pp_ticket_mercadopago

## 🔔 Webhooks

The plugin automatically handles notifications (notification_url) sent by Mercado Pago.
Ensure your MEDUSA_BACKEND_URL points to /hooks/payment/provider_id.

## 🚀 Instalación

```bash
npm install @qbk/mercadopago-medusa-plugin
# o
yarn add @qbk/mercadopago-medusa-plugin

```

Configure the plugin in medusa.config.ts:

```bash
    [Modules.PAYMENT]: {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@qbk/mercadopago-medusa-plugin/providers/mercadopago",
            id: "mercadopago",
            options: {
              accessToken: process.env.MP_ACCESS_TOKEN,
              capturePayment: process.env.MP_CAPTURE_PAYMENT,
             backendUrl: process.env.MEDUSA_BACKEND_URL
            },
          },
        ],
      },
    },

```
## Environment Variables

To run this project, you will need to add the following environment variables to your .env file

| Parameter |  Description                |
| :-------- | :------------------------- |
| `MP_ACCESS_TOKEN`  | accessToken mercadopago|
| `MP_CAPTURE_PAYMENT`  | true o false |
| `MEDUSA_BACKEND_URL`  | backendUrl |




## Tech Stack

**Client:** React, TailwindCSS

**Server:** Node, Express, Next js 

**Framework** Medusa

## License

[MIT](https://choosealicense.com/licenses/mit/)

## Contributing

Maintained by: Javier Sosa & MedusaJS LATAM Community

## 👨‍💻 Autors

- [@qbk](https://www.github.com/qbk)
- [@javi](https://www.github.com/javi)


## 🚀 About Me
I'm a full stack developer...

## Store

Retrieve customer saved payment methods:
```
src/api/store/payment-methods/[account_holder_id]/route.ts
```
```
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
import { MedusaError } from "@medusajs/framework/utils";

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { account_holder_id } = req.params;
  const query = req.scope.resolve("query");
  const paymentModuleService = req.scope.resolve("payment");

  const {
    data: [accountHolder],
  } = await query.graph({
    entity: "account_holder",
    fields: ["data", "provider_id", "*"],
    filters: { id: account_holder_id },
  });

  if (!accountHolder) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      "Account holder not found"
    );
  }

  const paymentMethods = await paymentModuleService.listPaymentMethods({
    provider_id: accountHolder.provider_id,
    context: {
      account_holder: {
        data: { id: accountHolder.data.id },
      },
    },
  });

  res.json({ payment_methods: paymentMethods });
}
```

## Storefront

Add the following attributes when querying cards: 

```
+customer.account_holders.id, +customer.account_holders.provider_id
```

Helper function to fetch saved payment methods:

```
export const getSavedPaymentMethods = async (accountHolderId: string) => {
  const headers = { ...(await getAuthHeaders()) }

  return sdk.client
    .fetch<{
      payment_methods: SavedPaymentMethod[]
    }>(`/store/payment-methods/${accountHolderId}`, {
      method: "GET",
      headers,
    })
    .catch(() => {
      return { payment_methods: [] }
    })
}

```
## Steps in the storefront to make a payment with Mercado Pago

Once you have selected the payment provider in the checkout and completed all the card details — or chosen a ticket option — you need to start the payment session by calling the initiatePaymentSession method

```
export async function initiatePaymentSession(
  cart: B2BCart,
  data: {
    provider_id: string
    context?: Record<string, unknown>
    data?: Record<string, any>
  }
) {
  const headers = { ...(await getAuthHeaders()) }

  return sdk.store.payment
    .initiatePaymentSession(cart, data, {}, headers)
    .then(async (resp) => {
      const cartCacheTag = await getCacheTag("carts")
      revalidateTag(cartCacheTag)
      return resp
    })
    .catch(medusaError)
}
```

In the data parameter, you must include all the information required by Mercado Pago to create a payment.

 | Property              | Type                                  | Description                                                      |
| --------------------- | ------------------------------------- | ---------------------------------------------------------------- |
| `mpPaymentBody`       | [`MpPaymentBody`](#mppaymentbody)     | Main payment body containing Mercado Pago form data.             |
| `mpPaymentId`         | `number \| null`                      | Optional Mercado Pago payment ID if it already exists.           |
| `session_id`          | `string \| null`                      | Session identifier from the payment flow.                        |
| `saveCard`            | `boolean`                             | Indicates if the card should be saved for future use.            |
| `externalResourceUrl` | `string`                              | URL provided by Mercado Pago for redirection or resource access. |
| `accountHolder`       | [`MpAccountHolder`](#mpaccountholder) | Information about the cardholder or account holder.              |
| `metadata`            | `object`                              | Extra metadata for card saving and holder reference.             |
| ├─ `saveCard`         | `boolean`                             | Indicates if the card should be saved.                           |
| ├─ `isSavedCard`      | `boolean`                             | Indicates if the payment uses a saved card.                      |
| └─ `accountHolderId`  | `string`                              | Optional ID of the associated account holder.                    |

## MpAccountHolder

| Property         | Type     | Description                                          |
| ---------------- | -------- | ---------------------------------------------------- |
| `cardholderName` | `string` | Name of the cardholder.                              |
| `lastFourDigits` | `string` | Last four digits of the card.                        |
| `entity`         | `string` | Payment entity or processor.                         |
| `displayDigints` | `string` | Masked digits to display (e.g., `**** 1234`).        |
| `paymentType`    | `string` | Type of payment (e.g., `credit_card`, `debit_card`). |
| `installments`   | `number` | Number of installments.                              |
| `collectCenter`  | `string` | Collection center or origin of payment.              |

## MpPaymentBody

| Property             | Type                                           | Description                                                        |
| -------------------- | ---------------------------------------------- | ------------------------------------------------------------------ |
| `external_reference` | `string`                                       | Optional external reference (usually the order ID).                |
| `payment_method`     | `{ id: string; type: string }`                 | Mercado Pago payment method details.                               |
| `issuer_id`          | `number`                                       | Card issuer ID.                                                    |
| *(+)*                | `ICardPaymentFormData<ICardPaymentBrickPayer>` | Includes standard Mercado Pago card form data.                     |
| *(+)*                | `ICardPaymentFormData<ISavedCardPayer>`        | Includes saved card payer data.                                    |
| *(+)*                | `TicketFormData`                               | Includes ticket-based payment data (for cash or voucher payments). |
| *(+)*                | `IFormDataAdditionalInfo`                      | Additional Mercado Pago form information.                          |


```
import {
  ICardPaymentBrickPayer,
  ICardPaymentFormData,
} from "@mercadopago/sdk-react/esm/bricks/cardPayment/type"
import {
  IFormDataAdditionalInfo,
  ISavedCardPayer,
  TicketFormData,
} from "@mercadopago/sdk-react/esm/bricks/payment/type"
import { TOptions } from "@mercadopago/sdk-react/esm/mercadoPago/initMercadoPago/type"

export type MpLocale = TOptions["locale"]

export interface PayerCost {
  installments: number
  recommended_message: string
  total_amount: number
}

export type SavedPaymentMethod = {
  id: string
  provider_id: string
  data: Card
}

export type Card = {
  additional_info: {
    request_public: string
    api_client_application: string
    api_client_scope: string
  }
  cardholder: {
    name: string
    identification: {
      number: string | null
      type: string
    }
  }
  customer_id: string
  date_created: string
  date_last_updated: string
  expiration_month: number
  expiration_year: number
  first_six_digits: string
  id: string
  issuer: {
    id: number
    name: string
  }
  last_four_digits: string
  live_mode: boolean
  payment_method: {
    id: string
    name: string
    payment_type_id: "credit_card" | "debit_card" | "prepaid_card" | string
    thumbnail: string
    secure_thumbnail: string
  }
  security_code: {
    length: number
    card_location: "front" | "back" | string
  }
  user_id: string
}

export type MpCreatePayment = {
  mpPaymentBody: MpPaymentBody
  mpPaymentId?: number | null
  session_id?: string | null
  saveCard?: boolean
  externalResourceUrl?: string
  accountHolder?: MpAccountHolder
  metadata: {
    saveCard: boolean
    isSavedCard: boolean
    accountHolderId?: string
  }
}

export type MpAccountHolder = {
  cardholderName?: string
  lastFourDigits?: string
  entity?: string
  displayDigints?: string
  paymentType?: string
  installments?: number
  collectCenter?: string
}

export type MpPaymentBody = ICardPaymentFormData<ICardPaymentBrickPayer> &
  ICardPaymentFormData<ISavedCardPayer> &
  TicketFormData &
  IFormDataAdditionalInfo & { external_reference?: string } & {
    payment_method?: {
      id: string
      type: string
    }
    issuer_id: number
  }

```
## Components
🧩 CardSelector

React component to list and select saved cards, generate secure tokens, and fetch available installments (for credit cards).


This component exposes a method that can be invoked through its reference, which returns…

```
export interface CardSelectorRef {
  cardListhandleSubmit: (e?: Event) => Promise<{
    token: string
    card: SavedPaymentMethod | undefined
    installments: PayerCost
  } | null>
}

const cardSelectorRef = useRef<CardSelectorRef | null>(null)
   if (cardSelectorRef.current) {
        const selectedCard =
          await cardSelectorRef.current.cardListhandleSubmit()
    }

           <CardSelector
              className="mb-4"
              publicKey={publicKey}
              ref={cardSelectorRef}
              cards={paymentMethods}
              locale={locale}
              amount={amount}
              isDebit={selectedPaymentMethod === "pp_debit_mercadopago"}
            />
```



## 💳 CardSelectorRef

| Property               | Type                                                                                                                | Description                                                                                                                                                                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cardListhandleSubmit` | `(e?: Event) => Promise<{ token: string; card: SavedPaymentMethod \| undefined; installments: PayerCost } \| null>` | Asynchronous function that handles the card selection form submission. Returns a promise that resolves with the generated card token, the selected saved card (if any), and the selected installment option. Returns `null` if the submission fails or is cancelled. |


🧩 Returned Object

| Property       | Type                              | Description                                                                       |
| -------------- | --------------------------------- | --------------------------------------------------------------------------------- |
| `token`        | `string`                          | Token generated by Mercado Pago for the selected card.                            |
| `card`         | `SavedPaymentMethod \| undefined` | The saved card used for the payment, if applicable.                               |
| `installments` | `PayerCost`                       | The selected installment plan, including number of payments and interest details. |


```
"use client"

import {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from "react"
import { MpLocale, SavedPaymentMethod } from "./types/mp-types"
import { PayerCost } from "@mercadopago/sdk-react/esm/coreMethods/util/types"

declare global {
  interface Window {
    MercadoPago: any
  }
}



export interface CardSelectorRef {
  cardListhandleSubmit: (e?: Event) => Promise<{
    token: string
    card: SavedPaymentMethod | undefined
    installments: PayerCost
  } | null>
}

interface CardSelectorProps extends React.HTMLAttributes<HTMLDivElement> {
  cards: SavedPaymentMethod[]
  amount: number
  locale: MpLocale
  isDebit: boolean
  publicKey: string
}

const CardSelector = forwardRef<CardSelectorRef, CardSelectorProps>(
  ({ cards, amount, locale, isDebit, publicKey }, ref) => {
    const formRef = useRef<HTMLFormElement | null>(null)
    const tokenRef = useRef<HTMLInputElement | null>(null)
    const installmentsRef = useRef<HTMLSelectElement | null>(null)
    const [selectedCard, setSelectedCard] = useState<SavedPaymentMethod>()
    const [selectedInstallment, setSelectedInstallment] = useState<PayerCost>()
    const [installments, setInstallments] = useState<PayerCost[]>([])
    const [mpInstance, setMpInstance] = useState<any>(null)

    useEffect(() => {
      const script = document.createElement("script")
      script.src = "https://sdk.mercadopago.com/js/v2"
      script.async = true
      script.onload = () => {
        const mp = new window.MercadoPago(publicKey, { locale })
        setMpInstance(mp)
      }

      document.body.appendChild(script)
    }, [])

    useEffect(() => {
      if (!mpInstance) return
      const securityField = mpInstance.fields.create("securityCode", {
        placeholder: "CVV",
      })
      securityField.mount("form-checkout__securityCode-container")
      if (cards?.length === 1) {
        setSelectedCard(cards[0])
      }
    }, [mpInstance])

    const cardListhandleSubmit = async (e?: Event) => {
      const tokenInput = tokenRef.current!
      try {
        if (e) e.preventDefault()

        const cardId = selectedCard?.id
        const token = await mpInstance.fields.createCardToken({ cardId })
        tokenInput.value = token.id

        return {
          token: token.id as string,
          card: selectedCard,
          installments: isDebit
            ? ({ installments: 1 } as PayerCost)
            : selectedInstallment ?? installments?.[0],
        }
      } catch (err) {
        return null
      }
    }

    useImperativeHandle(ref, () => ({ cardListhandleSubmit }))

    useEffect(() => {
      if (!mpInstance || !selectedCard) return

      setInstallments([])
      if (installmentsRef.current) installmentsRef.current.value = ""

      const fetchInstallments = async () => {
        try {
          if (
            selectedCard.data.payment_method?.payment_type_id === "credit_card"
          ) {
            const response = await mpInstance.getInstallments({
              amount: amount.toString(),
              bin: selectedCard.data.first_six_digits,
            })

            const options = response[0]?.payer_costs || []
            setInstallments(options)
          }
        } catch (err) {}
      }

      fetchInstallments()
    }, [selectedCard, mpInstance])

    return (
      <div>
        <form
          ref={formRef}
          id="form-checkout"
          method="POST"
          className="grid gap-4 mt-2"
        >
          <div className="grid grid-cols-2 md:grid-cols-3 gap-3 mt-2 px-2">
            {cards?.map((card) => {
              const isSelected = selectedCard?.id === card.id
              const brand = card.data.payment_method.name.toUpperCase()
              const last4 = card.data.last_four_digits
              const expMonth = String(card.data.expiration_month).padStart(
                2,
                "0"
              )
              const expYear = String(card.data.expiration_year).slice(-2)

              return (
                <div
                  key={card.id}
                  onClick={() => setSelectedCard(card)}
                  className={`cursor-pointer border rounded-xl p-3 flex flex-col justify-between shadow-sm transition-all duration-150
          ${
            isSelected
              ? "border-blue-500 ring-2 ring-blue-300 shadow-md"
              : "border-gray-300 hover:border-blue-400"
          }`}
                >
                  <div className="flex justify-between items-center mb-2">
                    <span className="text-sm font-semibold text-gray-800">
                      {brand}
                    </span>
                    <img
                      src={card.data.payment_method.secure_thumbnail}
                      alt={brand}
                      className="w-10 h-6 object-contain"
                    />
                  </div>

                  <div className="text-gray-700 tracking-widest font-mono text-sm">
                    •••• •••• •••• {last4}
                  </div>

                  <div className="flex justify-between items-center text-xs text-gray-500 mt-2">
                    <span className="truncate">
                      {card.data.cardholder.name}
                    </span>
                    <span>
                      {expMonth}/{expYear}
                    </span>
                  </div>

                  <div className="text-[11px] text-gray-400 mt-1">
                    {card.data.payment_method.payment_type_id === "credit_card"
                      ? "Crédito"
                      : card.data.payment_method.payment_type_id ===
                        "debit_card"
                      ? "Débito"
                      : "Prepagada"}
                  </div>
                </div>
              )
            })}
          </div>

          <div
            id="form-checkout__securityCode-container"
            className="inline-block border border-gray-400 rounded-md p-[2px] h-[40px] px-4"
          ></div>

          {installments.length > 0 && (
            <select
              ref={installmentsRef}
              name="installments"
              className="border border-gray-400 rounded-md p-2 text-gray-700 h-[40px] px-4"
              onChange={(e) => {
                const installment = installments.find(
                  (c) => c.installments === Number(e.target.value)
                )
                setSelectedInstallment(installment)
              }}
            >
              {installments.map((i) => (
                <option key={i.installments} value={i.installments}>
                  {i.recommended_message}
                </option>
              ))}
            </select>
          )}

          <input type="hidden" name="token" id="token" ref={tokenRef} />
        </form>
      </div>
    )
  }
)

export default CardSelector

```

## MpCardForm Component

💳 MpCardProps

| Property                | Type                    | Description                                                                      |
| ----------------------- | ----------------------- | -------------------------------------------------------------------------------- |
| `selectedPaymentMethod` | `string`                | The payment method selected by the user (e.g., `"credit_card"`, `"debit_card"`). |
| `amount`                | `number`                | The total amount to be charged in the transaction.                               |
| `cart`                  | `B2BCart`               | The current shopping cart object containing products, quantities, and totals.    |
| `locale`                | [`MpLocale`](#mplocale) | Locale configuration for Mercado Pago, used to adapt language and formatting.    |


```
export interface MpCardProps {
  selectedPaymentMethod: string
  amount: number
  cart: B2BCart
  locale: MpLocale
}
```

💳 MpCardRef
| Property             | Type                                                                                          | Description                                                                                                                                                                                                                                                    |
| -------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mpCardHandleSubmit` | `(event?: React.FormEvent<HTMLFormElement>) => Promise<MpCreatePayment \| null \| undefined>` | Asynchronous method exposed by the component reference. It can be invoked to submit the card payment form and returns a promise that resolves with the created payment data (`MpCreatePayment`), or `null`/`undefined` if the submission fails or is canceled. |

Implement

```
const cardRef = useRef<MpCardRef>(null)

 if (cardRef.current) {
          const data: MpCreatePayment =
            await cardRef.current.mpCardHandleSubmit(e)
}

       <MpCardForm
                    ref={cardRef}
                    selectedPaymentMethod={selectedPaymentMethod}
                    amount={cart.total}
                    cart={cart}
                    locale={"es-AR"}
                  />
```

```
"use client"

import { getSavedPaymentMethods } from "@/lib/data/payment"
import { CardPayment, initMercadoPago } from "@mercadopago/sdk-react"
import {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from "react"
import { TCardPayment } from "@mercadopago/sdk-react/esm/bricks/cardPayment/type"
import { Checkbox, Label, ProgressAccordion } from "@medusajs/ui"
import type {
  MpCreatePayment,
  MpLocale,
  MpPaymentBody,
  SavedPaymentMethod,
} from "./types/mp-types"
import { B2BAccountholders, B2BCart } from "@/types"
import CardSelector, { CardSelectorRef } from "./mp-card-select"

export interface MpCardProps {
  selectedPaymentMethod: string
  amount: number
  cart: B2BCart
  locale: MpLocale
}

const _paymnetType = {
  pp_debit_mercadopago: "debit_card",
  pp_credit_mercadopago: "credit_card",
}

export interface MpCardRef {
  mpCardHandleSubmit: (
    event?: React.FormEvent<HTMLFormElement>
  ) => Promise<MpCreatePayment | null | undefined>
}

export const MpCardForm = forwardRef<MpCardRef, MpCardProps>(
  ({ selectedPaymentMethod, amount, cart, locale }, ref) => {
    const provideId = ["pp_debit_mercadopago", "pp_credit_mercadopago"]
    if (!provideId.includes(selectedPaymentMethod)) return null
    const publicKey = process.env.NEXT_PUBLIC_MP_PUBLIC_KEY || ""
    if (!publicKey) throw new Error("mp public key do not provided")

    const cardSelectorRef = useRef<CardSelectorRef | null>(null)
    const paymentType =
      _paymnetType[selectedPaymentMethod as keyof typeof _paymnetType]

    const [paymentMethods, setPaymentMethods] = useState<SavedPaymentMethod[]>(
      []
    )
    const [accountHolder, setAccountHolder] = useState<B2BAccountholders>()
    const [ready, setReady] = useState(false)
    const [saveCard, setSaveCard] = useState(false)
    const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false)

    useEffect(() => {
      initMercadoPago(publicKey, { locale })
      loadPaymentMethods()
    }, [selectedPaymentMethod])

    useImperativeHandle(ref, () => ({ mpCardHandleSubmit }))

    const loadPaymentMethods = async () => {
      setPaymentMethods([])
      const accountHolder = cart.customer?.account_holders?.find(
        (accountHolder) => accountHolder?.provider_id === selectedPaymentMethod
      )

      if (accountHolder?.id) {
        setAccountHolder(accountHolder)
        setLoadingPaymentMethods(true)
        const { payment_methods } = await getSavedPaymentMethods(
          accountHolder.id
        )

        const filterPaymentMethods = payment_methods?.filter(
          (p) => p.data.payment_method.payment_type_id === paymentType
        )

        setPaymentMethods(filterPaymentMethods || [])
        setLoadingPaymentMethods(false)
      }
    }

    async function mpCardHandleSubmit(
      event?: React.FormEvent<HTMLFormElement>
    ) {
      event?.preventDefault()

      if (cardSelectorRef.current) {
        const selectedCard =
          await cardSelectorRef.current.cardListhandleSubmit()

        if (
          !selectedCard?.card ||
          !selectedCard?.installments ||
          !selectedCard?.token
        )
          throw new Error("Datos incompletos")

        return {
          mpPaymentId: null,
          mpPaymentBody: {
            installments: selectedCard.installments.installments,
            payment_method_id: selectedCard.card.data.payment_method.name,
            issuer_id: Number(selectedCard.card.data.issuer.id),
            transaction_amount: cart.total,
            token: selectedCard.token,
            payer: {
              type: "customer",
              id: selectedCard.card.data.customer_id,
            },
          } as MpPaymentBody,
          accountHolder: {
            cardholderName: selectedCard.card.data.cardholder.name,
            lastFourDigits: selectedCard.card.data.last_four_digits,
            entity: selectedCard.card.data.payment_method.id,
            displayDigints: `•••• •••• •••• ${selectedCard.card.data.last_four_digits}`,
            paymentType: selectedCard.card.data.payment_method.payment_type_id,
            installments: selectedCard.installments.installments,
          },
          metadata: {
            saveCard: false,
            isSavedCard: true,
          },
        } as MpCreatePayment
      } else {
        try {
          const controller = (window as any).cardPaymentBrickController as any
          const mpPaymentBody = await controller.getFormData()
          if (!mpPaymentBody) return undefined
          const { paymentTypeId, cardholderName, lastFourDigits } =
            await controller.getAdditionalData()

          return {
            mpPaymentId: null,
            accountHolder: {
              cardholderName,
              lastFourDigits,
              entity: mpPaymentBody.payment_method_id,
              displayDigints: `•••• •••• •••• ${lastFourDigits}`,
              paymentType: paymentTypeId,
              installments: mpPaymentBody?.installments,
            },
            mpPaymentBody: {
              ...mpPaymentBody,
              issuer_id: Number(mpPaymentBody.issuer_id),
            },
            metadata: {
              saveCard: saveCard,
              isSavedCard: false,
              accountHolderId: accountHolder?.id,
            },
          } as MpCreatePayment
        } catch (error) {
          return null
        }
      }
    }

    const cardPaymentProps: TCardPayment = {
      initialization: {
        payer: { email: cart.email },
        amount,
      },
      customization: {
        visual: {
          hidePaymentButton: true,
          hideRedirectionPanel: true,
          hideFormTitle: true,
        },
        paymentMethods: {
          types: { included: [paymentType as any] },
        },
      },
      onReady: () => setReady(true),
      onSubmit: async () => Promise.resolve(),
    }

    const cardForm = useMemo(() => {
      setReady(false)
      return <CardPayment {...cardPaymentProps} />
    }, [paymentMethods.length, selectedPaymentMethod, cart])

    if (loadingPaymentMethods) return <div>Loading...</div>

    return paymentMethods.length === 0 ? (
      <div>
        {cardForm}

        {ready && (
          <div className="flex items-center space-x-2 ml-3">
            <Checkbox
              checked={saveCard}
              onCheckedChange={(checked: boolean) => setSaveCard(checked)}
              id="save_card"
            />

            <Label htmlFor="save_card">Guardar Tarjeta</Label>
          </div>
        )}
      </div>
    ) : (
      <ProgressAccordion
        type="single"
        defaultValue="savedCards"
        className="w-full"
      >
        <ProgressAccordion.Item value="savedCards">
          <ProgressAccordion.Header className="p-0" status="completed">
            Tarjetas guardadas
          </ProgressAccordion.Header>

          <ProgressAccordion.Content className="p-0">
            <CardSelector
              className="mb-4"
              publicKey={publicKey}
              ref={cardSelectorRef}
              cards={paymentMethods}
              locale={locale}
              amount={amount}
              isDebit={selectedPaymentMethod === "pp_debit_mercadopago"}
            />
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>

        <ProgressAccordion.Item value="cardForm">
          <ProgressAccordion.Header className="p-0" status="completed">
            Nueva tarjeta
          </ProgressAccordion.Header>

          <ProgressAccordion.Content className="p-0">
            <div>
              {cardForm}

              {ready && (
                <div className="flex items-center space-x-2 ml-3">
                  <Checkbox
                    id="save_card"
                    checked={saveCard}
                    onCheckedChange={(checked: boolean) => setSaveCard(checked)}
                  />

                  <Label htmlFor="save_card">Guardar Tarjeta</Label>
                </div>
              )}
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    )
  }
)

```

## 🎟️ MpTicketForm Component

A React component that renders a Mercado Pago Ticket payment form (cash or voucher-based payments).
It uses the @mercadopago/sdk-react
 library and exposes a method via ref to programmatically submit the payment form

🚀 Features

Integrates Mercado Pago's Ticket Payment Brick.

Automatically initializes Mercado Pago with your public key.

Supports localized configuration via MpLocale.

Exposes a ref method (mpTicketHandleSubmit) to handle form submission and obtain payment data.

Can be easily used inside a checkout flow (e.g. in Medusa.js storefronts).

⚙️ Props

| Property                | Type                    | Description                                                                                              |
| ----------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------- |
| `selectedPaymentMethod` | `string`                | The selected payment provider. The component renders only if the value equals `"pp_ticket_mercadopago"`. |
| `amount`                | `number`                | The total transaction amount.                                                                            |
| `cart`                  | `B2BCart`               | The current shopping cart containing user and order data (used for the payer’s email).                   |
| `locale`                | [`MpLocale`](#mplocale) | Mercado Pago locale (e.g. `'es-AR'`, `'pt-BR'`, `'en-US'`).                                              |

🌎 MpLocale

| Locale    | Description             |
| --------- | ----------------------- |
| `'es-AR'` | Spanish (Argentina)     |
| `'es-CL'` | Spanish (Chile)         |
| `'es-CO'` | Spanish (Colombia)      |
| `'es-MX'` | Spanish (Mexico)        |
| `'es-VE'` | Spanish (Venezuela)     |
| `'es-UY'` | Spanish (Uruguay)       |
| `'es-PE'` | Spanish (Peru)          |
| `'pt-BR'` | Portuguese (Brazil)     |
| `'en-US'` | English (United States) |

🔄 Returned Object — MpCreatePayment

| Property           | Type                              | Description                                         |
| ------------------ | --------------------------------- | --------------------------------------------------- |
| `mpPaymentBody`    | [`MpPaymentBody`](#mppaymentbody) | Payment form data returned by the Mercado Pago SDK. |
| `mpPaymentId`      | `number \| null`                  | Mercado Pago payment ID (null for new payments).    |
| `accountHolder`    | `object`                          | Contains details about the payment method and type. |
| ├─ `collectCenter` | `string`                          | The payment collection method (e.g., `ticket`).     |
| └─ `paymentType`   | `string`                          | The payment type returned by Mercado Pago.          |


## 🧠 Internal Logic

Initializes Mercado Pago using initMercadoPago(publicKey, { locale }).

Renders a <Payment /> component configured for ticket payments.

Hides default UI elements (button, title, redirection panel).

Exposes mpTicketHandleSubmit, which:

Calls paymentBrickController.getFormData() to get the form data.

Builds and returns an MpCreatePayment object.

```
"use client"

import { useRef } from "react"
import { MpTicketForm } from "@/components/mp-ticket-form"
import { MpCardRef } from "@/components/types/mp-types"

export default function CheckoutTicket({ cart }) {
  const ticketRef = useRef<MpCardRef>(null)

  const handlePayment = async () => {
    const result = await ticketRef.current?.mpTicketHandleSubmit()
    console.log("Payment data:", result)
  }

  return (
    <>
      <MpTicketForm
        ref={ticketRef}
        amount={cart.total}
        cart={cart}
        selectedPaymentMethod="pp_ticket_mercadopago"
        locale="es-AR"
      />
      <button onClick={handlePayment}>Confirm Payment</button>
    </>
  )
}

```

## ⚠️ Environment Variables

You must define your Mercado Pago public key in your environment:
```
NEXT_PUBLIC_MP_PUBLIC_KEY=YOUR_MERCADOPAGO_PUBLIC_KEY

```

```
"use client"

import {
  isCreditMercadopago,
  isDebitMercadopago,
  isStripe as isStripeFunc,
  isTicketMercadopago,
  paymentInfoMap,
} from "@/lib/constants"
import { initiatePaymentSession } from "@/lib/data/cart"
import ErrorMessage from "@/modules/checkout/components/error-message"
import PaymentContainer from "@/modules/checkout/components/payment-container"
import { StripeContext } from "@/modules/checkout/components/payment-wrapper"
import Button from "@/modules/common/components/button"
import Divider from "@/modules/common/components/divider"
import { ApprovalStatusType } from "@/types"
import { RadioGroup } from "@headlessui/react"
import { CheckCircleSolid, CreditCard } from "@medusajs/icons"
import { Container, Heading, Text, clx } from "@medusajs/ui"
import { CardElement } from "@stripe/react-stripe-js"
import { StripeCardElementOptions } from "@stripe/stripe-js"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useContext, useEffect, useMemo, useState } from "react"

import { MpAccountHolder } from "../mercadopago/mp-account-holder"
import { useRef } from "react"
import { MpCardForm } from "../mercadopago/mp-card-form"
import { MpCreatePayment } from "../mercadopago/types/mp-types"
import { MpTicketForm } from "../mercadopago/mp-ticket-form"

const Payment = ({
  cart,
  availablePaymentMethods,
}: {
  cart: any
  availablePaymentMethods: any[]
}) => {
  const cardRef = useRef<any>(null)
  const ticketRef = useRef<any>(null)
  const activeSession = cart.payment_collection?.payment_sessions?.find(
    (paymentSession: any) => paymentSession.status === "pending"
  )
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [cardBrand, setCardBrand] = useState<string | null>(null)
  const [cardComplete, setCardComplete] = useState(false)
  const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
    activeSession?.provider_id ?? ""
  )

  const searchParams = useSearchParams()
  const router = useRouter()
  const pathname = usePathname()

  const isOpen = searchParams.get("step") === "payment"

  const cartApprovalStatus = cart.approval_status?.status

  const stripeReady = useContext(StripeContext)

  const paidByGiftcard =
    cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0

  const paymentReady =
    (activeSession && cart?.shipping_methods.length !== 0) || paidByGiftcard

  const useOptions: StripeCardElementOptions = useMemo(() => {
    return {
      style: {
        base: {
          fontFamily: "Inter, sans-serif",
          color: "#424270",
          "::placeholder": {
            color: "rgb(107 114 128)",
          },
        },
      },
      classes: {
        base: "pt-3 pb-1 block w-full h-11 px-4 mt-0 bg-ui-bg-field border rounded-md appearance-none focus:outline-none focus:ring-0 focus:shadow-borders-interactive-with-active border-ui-border-base hover:bg-ui-bg-field-hover transition-all duration-300 ease-in-out",
      },
    }
  }, [])

  const createQueryString = useCallback(
    (name: string, value: string) => {
      const params = new URLSearchParams(searchParams)
      params.set(name, value)

      return params.toString()
    },
    [searchParams]
  )

  const handleEdit = () => {
    router.push(pathname + "?" + createQueryString("step", "payment"), {
      scroll: false,
    })
  }

  const handleSubmit = async (e: any) => {
    console.log("payment_collection: ", cart.payment_collection)

    e.preventDefault()
    setIsLoading(true)

    try {
      if (
        isCreditMercadopago(selectedPaymentMethod) ||
        isDebitMercadopago(selectedPaymentMethod)
      ) {
        if (cardRef.current) {
          const data: MpCreatePayment =
            await cardRef.current.mpCardHandleSubmit(e)

          if (!data) return

          await initPaymentSession(data)
          return goToReviewPage()
        }
      } else if (isTicketMercadopago(selectedPaymentMethod)) {
        if (ticketRef.current) {
          const data: MpCreatePayment =
            await ticketRef.current.mpTicketHandleSubmit(e)

          if (!data) return

          await initPaymentSession(data)

          return goToReviewPage()
        }
      } else {
        const shouldInputCard =
          isStripeFunc(selectedPaymentMethod) && !activeSession
        if (
          !activeSession ||
          activeSession.provider_id !== selectedPaymentMethod
        ) {
          await initPaymentSession()
        }
        if (!shouldInputCard) return goToReviewPage()
      }
    } catch (err: any) {
      setError(err.message)
    } finally {
      setIsLoading(false)
    }
  }

  const initPaymentSession = async (data?: any) => {
    return await initiatePaymentSession(cart, {
      provider_id: selectedPaymentMethod,
      data,
    })
  }

  const goToReviewPage = () => {
    return router.push(pathname + "?" + createQueryString("step", "review"), {
      scroll: false,
    })
  }

  useEffect(() => {
    setError(null)
  }, [isOpen])

  return (
    <Container>
      <div className="flex flex-col gap-y-2">
        <div className="flex flex-row items-center justify-between w-full">
          <Heading
            level="h2"
            className={clx(
              "flex flex-row jmp-font-headlines-desktop-h5 gap-x-2 items-center",
              {
                "opacity-50 pointer-events-none select-none":
                  !isOpen && !paymentReady,
              }
            )}
          >
            Método de pago
            {!isOpen && paymentReady && <CheckCircleSolid />}
          </Heading>
          {!isOpen &&
            paymentReady &&
            cartApprovalStatus !== ApprovalStatusType.PENDING && (
              <Text>
                <button
                  onClick={handleEdit}
                  className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
                  data-testid="edit-payment-button"
                >
                  Editar
                </button>
              </Text>
            )}
        </div>
        {(isOpen || (cart && paymentReady && activeSession)) && <Divider />}
      </div>
      <div>
        <div className={isOpen ? "block" : "hidden"}>
          {!paidByGiftcard && availablePaymentMethods?.length && (
            <>
              <RadioGroup
                value={selectedPaymentMethod}
                onChange={(value: string) => setSelectedPaymentMethod(value)}
              >
                {availablePaymentMethods
                  .sort((a, b) => {
                    return a.provider_id > b.provider_id ? 1 : -1
                  })
                  .map((paymentMethod) => {
                    return (
                      <PaymentContainer
                        paymentInfoMap={paymentInfoMap}
                        paymentProviderId={paymentMethod.id}
                        key={paymentMethod.id}
                        selectedPaymentOptionId={selectedPaymentMethod}
                      />
                    )
                  })}
              </RadioGroup>

              {isOpen && (
                <>
                  <MpCardForm
                    ref={cardRef}
                    selectedPaymentMethod={selectedPaymentMethod}
                    amount={cart.total}
                    cart={cart}
                    locale={"es-AR"}
                  />

                  <MpTicketForm
                    ref={ticketRef}
                    selectedPaymentMethod={selectedPaymentMethod}
                    amount={cart.total}
                    cart={cart}
                    locale={"es-AR"}
                  />
                </>
              )}

              {stripeReady && selectedPaymentMethod === "pp_stripe_stripe" && (
                <div className="mt-5 transition-all duration-150 ease-in-out">
                  <Text className="txt-medium-plus text-ui-fg-base mb-1">
                    Ingresa los detalles de tu tarjeta:
                  </Text>

                  <CardElement
                    options={useOptions as StripeCardElementOptions}
                    onChange={(e) => {
                      setCardBrand(
                        e.brand &&
                          e.brand.charAt(0).toUpperCase() + e.brand.slice(1)
                      )
                      setError(e.error?.message || null)
                      setCardComplete(e.complete)
                    }}
                  />
                </div>
              )}
            </>
          )}

          {paidByGiftcard && (
            <div className="flex flex-col w-1/3">
              <Text
                className="txt-medium text-ui-fg-subtle"
                data-testid="payment-method-summary"
              >
                Tarjeta de regalo
              </Text>
            </div>
          )}

          <div className="flex flex-col gap-y-2 items-end">
            <ErrorMessage
              error={error}
              data-testid="payment-method-error-message"
            />

            {
              <Button
                size="large"
                className="jmp-btn-sm-primary mt-2"
                onClick={handleSubmit}
                isLoading={isLoading}
                data-testid="submit-payment-button"
              >
                {!activeSession && isStripeFunc(selectedPaymentMethod)
                  ? "Ingrese los detalles de tarjeta"
                  : "Siguiente paso"}
              </Button>
            }
          </div>
        </div>

        <div className={isOpen ? "hidden" : "block"}>
          {cart && paymentReady && activeSession ? (
            <div className="flex items-center gap-x-1 w-full pt-2">
              <div className="flex flex-col w-1/3">
                <Text
                  className="jmp-font-texts-subtitle-2 text-ui-fg-subtle"
                  data-testid="payment-method-summary"
                >
                  {paymentInfoMap[selectedPaymentMethod]?.title ||
                    selectedPaymentMethod}
                </Text>
              </div>
              <div className="flex flex-col w-1/3">
                <div
                  className="flex gap-2 txt-medium text-ui-fg-subtle items-center"
                  data-testid="payment-details-summary"
                >
                  <Container className="flex items-center h-7 w-fit p-2 bg-ui-button-neutral-hover">
                    {paymentInfoMap[selectedPaymentMethod]?.icon || (
                      <CreditCard />
                    )}
                  </Container>
                  <Text>
                    {isStripeFunc(selectedPaymentMethod) && cardBrand ? (
                      cardBrand
                    ) : isCreditMercadopago(selectedPaymentMethod) ||
                      isDebitMercadopago(selectedPaymentMethod) ? (
                      <MpAccountHolder data={activeSession?.data} />
                    ) : (
                      paymentInfoMap[selectedPaymentMethod]?.title
                    )}
                  </Text>
                </div>
              </div>
            </div>
          ) : paidByGiftcard ? (
            <div className="flex flex-col w-1/3">
              <Text className="txt-medium-plus text-ui-fg-base mb-1">
                Método de pago
              </Text>
              <Text
                className="txt-medium text-ui-fg-subtle"
                data-testid="payment-method-summary"
              >
                Tarjeta de regalo
              </Text>
            </div>
          ) : null}
        </div>
      </div>
    </Container>
  )
}

export default Payment

```