npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@qbk/mercadopago-medusa-plugin

v0.0.3

Published

A starter for Medusa plugins.

Readme

🧩 @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

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

Configure the plugin in medusa.config.ts:

    [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

Contributing

Maintained by: Javier Sosa & MedusaJS LATAM Community

👨‍💻 Autors

🚀 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 | 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 | 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 | 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 | 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 | 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 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