🧩 @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
💳 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
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.
MpAccountHolder
MpPaymentBody
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
🧩 Returned Object
"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
export interface MpCardProps {
selectedPaymentMethod: string
amount: number
cart: B2BCart
locale: MpLocale
}
💳 MpCardRef
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
🌎 MpLocale
🔄 Returned Object — MpCreatePayment
🧠 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