diff --git a/.changeset/some-berries-teach.md b/.changeset/some-berries-teach.md new file mode 100644 index 00000000..19425a12 --- /dev/null +++ b/.changeset/some-berries-teach.md @@ -0,0 +1,6 @@ +--- +"nextjs": minor +"@godaddy/react": minor +--- + +Implementation of mercadopago diff --git a/examples/nextjs/app/checkout.tsx b/examples/nextjs/app/checkout.tsx index 0fe2ff77..9a381042 100644 --- a/examples/nextjs/app/checkout.tsx +++ b/examples/nextjs/app/checkout.tsx @@ -14,20 +14,52 @@ export function CheckoutPage({ session }: { session: CheckoutSession }) { ); } diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index 9ae69eb8..7593cdbf 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -58,17 +58,13 @@ export default async function Home() { processor: 'godaddy', checkoutTypes: ['standard'], }, - express: { - processor: 'godaddy', - checkoutTypes: ['express'], + mercadopago: { + processor: 'mercadopago', + checkoutTypes: ['standard'], }, paypal: { processor: 'paypal', - checkoutTypes: ['standard'], - }, - offline: { - processor: 'offline', - checkoutTypes: ['standard'], + checkoutTypes: ['express', 'standard'], }, }, operatingHours: { diff --git a/examples/nextjs/app/store/actions.ts b/examples/nextjs/app/store/actions.ts index 1cc06f39..06dac3e7 100644 --- a/examples/nextjs/app/store/actions.ts +++ b/examples/nextjs/app/store/actions.ts @@ -77,8 +77,6 @@ export async function checkoutWithOrder(orderId: string) { throw new Error('Failed to create checkout session'); } - console.log({ session }); - if (!session.url) { throw new Error('No checkout URL returned'); } diff --git a/examples/nextjs/app/store/product/[productId]/product.tsx b/examples/nextjs/app/store/product/[productId]/product.tsx index 9aa51c71..9b2453e4 100644 --- a/examples/nextjs/app/store/product/[productId]/product.tsx +++ b/examples/nextjs/app/store/product/[productId]/product.tsx @@ -17,10 +17,7 @@ export default function Product({ productId }: { productId: string }) { Back to Store - + ); } diff --git a/examples/nextjs/biome.json b/examples/nextjs/biome.json index 808d736f..faa4f36f 100644 --- a/examples/nextjs/biome.json +++ b/examples/nextjs/biome.json @@ -1,20 +1,20 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json", - "extends": ["biome-config-godaddy/biome.json"], - "css": { - "parser": { - "cssModules": true, - "tailwindDirectives": true - } - }, - "files": { - "includes": ["**/*", "!!**/src/globals.css"] - }, - "linter": { - "rules": { - "correctness": { - "useUniqueElementIds": "off" - } - } - } + "$schema": "https://biomejs.dev/schemas/2.3.3/schema.json", + "extends": ["biome-config-godaddy/biome.json"], + "css": { + "parser": { + "cssModules": true, + "tailwindDirectives": true + } + }, + "files": { + "includes": ["**/*", "!!**/src/globals.css"] + }, + "linter": { + "rules": { + "correctness": { + "useUniqueElementIds": "off" + } + } + } } diff --git a/examples/nextjs/env.sample b/examples/nextjs/env.sample index 337ff345..abd13fa6 100644 --- a/examples/nextjs/env.sample +++ b/examples/nextjs/env.sample @@ -21,3 +21,7 @@ NEXT_PUBLIC_GODADDY_APP_ID= NEXT_PUBLIC_SQUARE_APP_ID= NEXT_PUBLIC_SQUARE_LOCATION_ID= NEXT_PUBLIC_PAYPAL_CLIENT_ID= + +# MercadoPago Credentials +NEXT_PUBLIC_MERCADOPAGO_PUBLIC_KEY= +NEXT_PUBLIC_MERCADOPAGO_COUNTRY=AR diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts index a58bf364..705ce2ea 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -114,6 +114,8 @@ export const deDe = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Verwende das MercadoPago-Formular unten, um deinen Kauf sicher abzuschließen.', }, noMethodsAvailable: 'Keine Zahlungsmethoden verfügbar', cardNumber: 'Kartennummer', diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index 5751044b..b1b7bbee 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -114,6 +114,8 @@ export const enIe = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Use the MercadoPago form below to complete your purchase securely.', }, noMethodsAvailable: 'No payment methods available', cardNumber: 'Card number', diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts index b9472309..0c43e883 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -114,6 +114,7 @@ export const enUs = { googlePay: '', paze: '', offline: '', + mercadopago: 'Use the MercadoPago form below to complete your purchase securely.', }, noMethodsAvailable: 'No payment methods available', cardNumber: 'Card number', diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts index 92f66711..b60b2005 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -115,6 +115,8 @@ export const esAr = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts index d1438c84..bc5ef18a 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -115,6 +115,8 @@ export const esCl = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts index e82930d5..b6771178 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -115,6 +115,8 @@ export const esCo = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts index 51a51eec..77cc04c3 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -115,6 +115,8 @@ export const esEs = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts index 2a34cb82..f8bda364 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -115,6 +115,8 @@ export const esMx = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts index 9dbeca8e..82637e7e 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -115,6 +115,8 @@ export const esPe = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts index ad231176..b8bc226e 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -115,6 +115,8 @@ export const esUs = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa el formulario de MercadoPago a continuación para completar tu compra de forma segura.', }, noMethodsAvailable: 'No hay métodos de pago disponibles', cardNumber: 'Número de tarjeta', diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts index 1e136e32..b8114e85 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -115,6 +115,8 @@ export const frCa = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.', }, noMethodsAvailable: 'Aucune méthode de paiement disponible', cardNumber: 'Numéro de carte', diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts index 04b2a67c..7fe289f2 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -115,6 +115,8 @@ export const frFr = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Utilisez le formulaire MercadoPago ci-dessous pour finaliser votre achat en toute sécurité.', }, noMethodsAvailable: 'Aucune méthode de paiement disponible', cardNumber: 'Numéro de carte', diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts index a0b2b8a6..cfacf9ce 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -114,6 +114,8 @@ export const idId = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Gunakan formulir MercadoPago di bawah untuk menyelesaikan pembelian Anda dengan aman.', }, noMethodsAvailable: 'Tidak ada metode pembayaran tersedia', cardNumber: 'Nomor kartu', diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts index e6d5a3d3..1a9f284a 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -115,6 +115,8 @@ export const itIt = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Usa il modulo MercadoPago qui sotto per completare l’acquisto in modo sicuro.', }, noMethodsAvailable: 'Nessun metodo di pagamento disponibile', cardNumber: 'Numero della carta', diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts index 40dac240..f8880766 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -114,6 +114,8 @@ export const ptBr = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Use o formulário do MercadoPago abaixo para concluir sua compra com segurança.', }, noMethodsAvailable: 'Nenhum método de pagamento disponível', cardNumber: 'Número do cartão', diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts index 2824a45a..c09a2f7e 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -115,6 +115,8 @@ export const qaPs = { googlePay: '', paze: '', offline: '', + mercadopago: + '[Üšë ţhë MërçâðöÞâgö förm këlöw ţö çömþlëţë ÿöür þürçhâšë šëçürëlÿ.]', }, noMethodsAvailable: '[Ñö þâÿmëñţ mëţhödš âvâîlâblë âţ ţhîš ţîmë]', cardNumber: '[Çârd ñümkër îñþüţ fîëld]', diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts index 72afea53..aecf661a 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -114,6 +114,8 @@ export const trTr = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Satın alımınızı güvenle tamamlamak için aşağıdaki MercadoPago formunu kullanın.', }, noMethodsAvailable: 'Kullanılabilir ödeme yöntemi yok', cardNumber: 'Kart numarası', diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts index c1460e6c..6b120014 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -114,6 +114,8 @@ export const viVn = { googlePay: '', paze: '', offline: '', + mercadopago: + 'Hãy sử dụng biểu mẫu MercadoPago bên dưới để hoàn tất mua hàng một cách an toàn.', }, noMethodsAvailable: 'Không có phương thức thanh toán nào', cardNumber: 'Số thẻ', diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts index e71929b9..6f11c065 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -110,6 +110,7 @@ export const zhCn = { googlePay: '', paze: '', offline: '', + mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。', }, noMethodsAvailable: '暂无可用的付款方式', cardNumber: '卡号', diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts index 286d58ff..74088efe 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -110,6 +110,7 @@ export const zhSg = { googlePay: '', paze: '', offline: '', + mercadopago: '请使用下方的 MercadoPago 表单安全完成购买。', }, noMethodsAvailable: '无可用付款方式', cardNumber: '卡号', diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index bc2a0b21..8ae642b4 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -80,6 +80,11 @@ export type PayPalConfig = { disableFunding?: Array<'credit' | 'card' | 'paylater' | 'venmo'>; }; +export type MercadoPagoConfig = { + publicKey: string; + country: 'AR' | 'BR' | 'CO' | 'CL' | 'PE' | 'MX'; +}; + interface CheckoutContextValue { elements?: CheckoutElements; targets?: Partial< @@ -92,6 +97,7 @@ interface CheckoutContextValue { godaddyPaymentsConfig?: GodaddyPaymentsConfig; squareConfig?: SquareConfig; paypalConfig?: PayPalConfig; + mercadoPagoConfig?: MercadoPagoConfig; isConfirmingCheckout: boolean; setIsConfirmingCheckout: (isConfirming: boolean) => void; checkoutErrors?: string[] | undefined; @@ -202,6 +208,7 @@ export interface CheckoutProps { godaddyPaymentsConfig?: GodaddyPaymentsConfig; squareConfig?: SquareConfig; paypalConfig?: PayPalConfig; + mercadoPagoConfig?: MercadoPagoConfig; layout?: LayoutSection[]; direction?: 'ltr' | 'rtl'; showStoreHours?: boolean; @@ -221,6 +228,7 @@ export function Checkout(props: CheckoutProps) { godaddyPaymentsConfig, squareConfig, paypalConfig, + mercadoPagoConfig, isCheckoutDisabled, } = props; @@ -385,6 +393,7 @@ export function Checkout(props: CheckoutProps) { stripeConfig, godaddyPaymentsConfig, squareConfig, + mercadoPagoConfig, paypalConfig, requiredFields, isConfirmingCheckout, diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/mercadopago.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/mercadopago.tsx new file mode 100644 index 00000000..befbfeef --- /dev/null +++ b/packages/react/src/components/checkout/payment/checkout-buttons/credit-card/mercadopago.tsx @@ -0,0 +1,43 @@ +import { useFormContext } from 'react-hook-form'; +import { useMercadoPago } from '@/components/checkout/payment/utils/mercadopago-provider'; +import { Button } from '@/components/ui/button'; +import { useGoDaddyContext } from '@/godaddy-provider'; + +export function MercadoPagoCreditCardCheckoutButton() { + const { t } = useGoDaddyContext(); + const { handleBrickSubmit, isLoading } = useMercadoPago(); + const form = useFormContext(); + + const handleSubmit = async () => { + // Validate form first + const valid = await form.trigger(); + if (!valid) { + const firstError = Object.keys(form.formState.errors)[0]; + if (firstError) { + form.setFocus(firstError); + } + return; + } + + // Trigger MercadoPago brick submission + if (handleBrickSubmit) { + try { + await handleBrickSubmit(); + } catch (error) { + console.error('MercadoPago submission error:', error); + } + } + }; + + return ( + + ); +} diff --git a/packages/react/src/components/checkout/payment/icons/MercadoPago.tsx b/packages/react/src/components/checkout/payment/icons/MercadoPago.tsx new file mode 100644 index 00000000..a33064b1 --- /dev/null +++ b/packages/react/src/components/checkout/payment/icons/MercadoPago.tsx @@ -0,0 +1,39 @@ +export const MercadoPagoIcon = props => { + return ( + + + + + + + + + ); +}; + +export default MercadoPagoIcon; diff --git a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx index 00bb6c53..33e6d542 100644 --- a/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx +++ b/packages/react/src/components/checkout/payment/lazy-payment-loader.tsx @@ -40,6 +40,13 @@ const LazyComponents = { default: module.PayPalCreditCardForm, })) ), + MercadoPagoCreditCardForm: lazy(() => + import( + '@/components/checkout/payment/payment-methods/credit-card/mercadopago' + ).then(module => ({ + default: module.MercadoPagoCreditCardForm, + })) + ), // Credit Card Buttons CreditCardCheckoutButton: lazy(() => @@ -70,7 +77,13 @@ const LazyComponents = { default: module.PayPalCreditCardCheckoutButton, })) ), - + MercadoPagoCreditCardCheckoutButton: lazy(() => + import( + '@/components/checkout/payment/checkout-buttons/credit-card/mercadopago' + ).then(module => ({ + default: module.MercadoPagoCreditCardCheckoutButton, + })) + ), // Express Buttons ExpressCheckoutButton: lazy(() => import( @@ -179,6 +192,12 @@ type PaymentComponentRegistry = { button: PaymentComponentKey; }; }; + [PaymentMethodType.MERCADOPAGO]?: { + [PaymentProvider.MERCADOPAGO]: { + form: PaymentComponentKey; + button: PaymentComponentKey; + }; + }; }; export const lazyPaymentComponentRegistry: PaymentComponentRegistry = { @@ -228,6 +247,12 @@ export const lazyPaymentComponentRegistry: PaymentComponentRegistry = { button: 'PazeCheckoutButton', }, }, + [PaymentMethodType.MERCADOPAGO]: { + [PaymentProvider.MERCADOPAGO]: { + form: 'MercadoPagoCreditCardForm', + button: 'MercadoPagoCreditCardCheckoutButton', + }, + }, }; // Payment loading skeleton component diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 3155d71e..41d15695 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -18,6 +18,7 @@ import { } from '@/components/checkout/line-items'; import ApplePayIcon from '@/components/checkout/payment/icons/ApplePay'; import GooglePayIcon from '@/components/checkout/payment/icons/GooglePay'; +import MercadoPagoIcon from '@/components/checkout/payment/icons/MercadoPago'; import PayPalIcon from '@/components/checkout/payment/icons/PayPal'; import PazeIcon from '@/components/checkout/payment/icons/Paze'; import { @@ -66,6 +67,7 @@ const PAYMENT_METHOD_ICONS: Record = { applePay: , googlePay: , paze: , + mercadopago: , offline: , }; @@ -114,6 +116,8 @@ export function PaymentForm( return t.payment.methods.paze; case PaymentMethodType.OFFLINE: return t.payment.methods.offline; + case PaymentMethodType.MERCADOPAGO: + return 'MercadoPago'; default: return key; } @@ -137,6 +141,8 @@ export function PaymentForm( return t.payment.descriptions?.paze; case PaymentMethodType.OFFLINE: return t.payment.descriptions?.offline; + case PaymentMethodType.MERCADOPAGO: + return t.payment.descriptions?.mercadopago; default: return undefined; } @@ -430,6 +436,15 @@ export function PaymentForm( /> ) : null} + {/* Render MercadoPago form outside accordion */} + {paymentMethod === PaymentMethodType.MERCADOPAGO && methodConfig ? ( + + ) : null} + {isShipping && session?.enableShipping && paymentMethod !== PaymentMethodType.CREDIT_CARD ? ( diff --git a/packages/react/src/components/checkout/payment/payment-methods/credit-card/mercadopago.tsx b/packages/react/src/components/checkout/payment/payment-methods/credit-card/mercadopago.tsx new file mode 100644 index 00000000..4815f5f2 --- /dev/null +++ b/packages/react/src/components/checkout/payment/payment-methods/credit-card/mercadopago.tsx @@ -0,0 +1,275 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useCheckoutContext } from '@/components/checkout/checkout'; +import { useDraftOrderTotals } from '@/components/checkout/order/use-draft-order'; +import { useMercadoPago } from '@/components/checkout/payment/utils/mercadopago-provider'; +import { + PaymentProvider, + useConfirmCheckout, +} from '@/components/checkout/payment/utils/use-confirm-checkout'; +import { useLoadMercadoPago } from '@/components/checkout/payment/utils/use-load-mercadopago'; +import { useGoDaddyContext } from '@/godaddy-provider'; +import { GraphQLErrorWithCodes } from '@/lib/graphql-with-errors'; +import { eventIds } from '@/tracking/events'; +import { TrackingEventType, track } from '@/tracking/track'; +import { PaymentMethodType } from '@/types'; + +export function MercadoPagoCreditCardForm() { + const { t } = useGoDaddyContext(); + const { mercadoPagoConfig, setCheckoutErrors } = useCheckoutContext(); + const { data: totals } = useDraftOrderTotals(); + const form = useFormContext(); + const { + setIsLoading: setMercadoPagoLoading, + setHandleBrickSubmit, + } = useMercadoPago(); + const { isMercadoPagoLoaded } = useLoadMercadoPago(); + const [error, setError] = useState(''); + const [isInitialized, setIsInitialized] = useState(false); + const [mpInstance, setMpInstance] = useState(null); + const [bricksBuilder, setBricksBuilder] = useState(null); + const brickControllerRef = useRef(null); + const isInitializingRef = useRef(false); + const hasRenderedRef = useRef(false); + const onReadyRef = useRef<() => void>(null); + const onSubmitRef = useRef<(args: any) => void>(null); + const onErrorRef = useRef<(err: any) => void>(null); + + const confirmCheckout = useConfirmCheckout(); + + // Memoize brick callbacks to prevent recreating the brick on every render + const handleReady = useCallback(() => { + setMercadoPagoLoading(false); + + // Remove padding from internal form after brick loads + const container = document.getElementById('mercadopago-brick-container'); + const form = container?.querySelector('form'); + if (form) { + form.style.padding = '0'; + } + }, [setMercadoPagoLoading]); + + const handleSubmit = useCallback( + async ({ formData }: any) => { + setMercadoPagoLoading(true); + + // Validate form before processing payment + const valid = await form.trigger(); + if (!valid) { + const firstError = Object.keys(form.formState.errors)[0]; + if (firstError) { + form.setFocus(firstError); + } + setMercadoPagoLoading(false); + return; + } + + // Track MercadoPago click + track({ + eventId: eventIds.mercadopagoClick, + type: TrackingEventType.CLICK, + properties: { + paymentType: PaymentMethodType.MERCADOPAGO, + }, + }); + + try { + // MercadoPago SDK provides the payment token in formData.token + const paymentToken = formData?.token; + + if (!paymentToken) { + throw new Error('No payment token received from MercadoPago'); + } + + await confirmCheckout.mutateAsync({ + paymentToken, + paymentType: PaymentMethodType.MERCADOPAGO, + paymentProvider: PaymentProvider.MERCADOPAGO, + }); + setError(''); + } catch (err: unknown) { + if (err instanceof GraphQLErrorWithCodes) { + setCheckoutErrors(err.codes); + } else { + setError(t.errors.errorProcessingPayment); + } + } finally { + setMercadoPagoLoading(false); + } + }, + [ + form, + confirmCheckout, + setCheckoutErrors, + setMercadoPagoLoading, + t.errors.errorProcessingPayment, + ] + ); + + const handleError = useCallback( + (err: any) => { + const _errorMessage = err?.message || err?.error || 'Unknown error'; + setError(t.errors.errorProcessingPayment); + setMercadoPagoLoading(false); + }, + [setMercadoPagoLoading, t.errors.errorProcessingPayment] + ); + + useEffect(() => { + onReadyRef.current = handleReady; + onSubmitRef.current = handleSubmit; + onErrorRef.current = handleError; + }, [handleReady, handleSubmit, handleError]); + + // Exposing a function to submit the brick from the external button + useEffect(() => { + if (brickControllerRef.current) { + const submitHandler = async () => { + try { + const { formData } = await brickControllerRef.current.getFormData(); + + // Manually trigger the onSubmit callback with the form data + if (onSubmitRef.current) { + await onSubmitRef.current({ formData }); + } + } catch (error) { + console.error('Error getting/submitting MercadoPago form data:', error); + if (onErrorRef.current) { + onErrorRef.current(error); + } + } + }; + setHandleBrickSubmit(submitHandler); + } else { + setHandleBrickSubmit(null); + } + }, [brickControllerRef.current, setHandleBrickSubmit]); + + // Initialize MercadoPago instance + useLayoutEffect(() => { + if ( + !isMercadoPagoLoaded || + !mercadoPagoConfig?.publicKey || + mpInstance || + isInitialized + ) + return; + + try { + const mp = new (window as any).MercadoPago(mercadoPagoConfig.publicKey); + const builder = mp.bricks(); + setMpInstance(mp); + setBricksBuilder(builder); + setIsInitialized(true); + } catch (_err) { + setError(t.errors.errorProcessingPayment); + } + }, [ + isMercadoPagoLoaded, + mercadoPagoConfig?.publicKey, + mpInstance, + isInitialized, + setMpInstance, + setBricksBuilder, + ]); + + // Render Payment Brick + useLayoutEffect(() => { + if ( + !bricksBuilder || + brickControllerRef.current || + isInitializingRef.current || + hasRenderedRef.current + ) + return; + + const renderBrick = async () => { + const total = totals?.total?.value || 0; + + try { + isInitializingRef.current = true; + hasRenderedRef.current = true; + const container = document.getElementById( + 'mercadopago-brick-container' + ); + if (container) { + container.innerHTML = ''; + } + const settings = { + initialization: { + amount: total, + payer: { email: 'dummy@testuser.com' }, + }, + customization: { + visual: { + hideFormTitle: true, + hidePaymentButton: true, + style: { + theme: 'default', + }, + }, + paymentMethods: { + creditCard: 'all', + debitCard: 'all', + maxInstallments: 1, + }, + }, + callbacks: { + onReady: () => onReadyRef.current?.(), + onSubmit: (args: any) => onSubmitRef.current?.(args), + onError: (err: any) => onErrorRef.current?.(err), + }, + }; + + const controller = await bricksBuilder.create( + 'payment', + 'mercadopago-brick-container', + settings + ); + + brickControllerRef.current = controller; + isInitializingRef.current = false; + } catch (_err) { + setError(t.errors.errorProcessingPayment); + isInitializingRef.current = false; + hasRenderedRef.current = false; + } + }; + + renderBrick(); + }, [bricksBuilder]); + + // Unmount MercadoPago brick on component unmount only + useEffect(() => { + return () => { + if (brickControllerRef.current) { + try { + brickControllerRef.current.unmount(); + } catch (_e) { + // Ignore unmount errors + } + brickControllerRef.current = null; + } + const container = document.getElementById('mercadopago-brick-container'); + if (container) { + container.innerHTML = ''; + } + isInitializingRef.current = false; + }; + }, []); + + return ( + <> +
+ {error ? ( +

{error}

+ ) : null} + + ); +} diff --git a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx index 95df6409..e00ec293 100644 --- a/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx +++ b/packages/react/src/components/checkout/payment/utils/conditional-providers.tsx @@ -1,5 +1,6 @@ import { PayPalScriptProvider } from '@paypal/react-paypal-js'; import { useCheckoutContext } from '@/components/checkout/checkout'; +import { MercadoPagoProvider } from './mercadopago-provider'; import { PayPalProvider } from './paypal-provider'; import { PoyntCollectProvider } from './poynt-provider'; import { SquareProvider } from './square-provider'; @@ -17,8 +18,13 @@ interface ConditionalPaymentProvidersProps { export function ConditionalPaymentProviders({ children, }: ConditionalPaymentProvidersProps) { - const { stripeConfig, godaddyPaymentsConfig, squareConfig, paypalConfig } = - useCheckoutContext(); + const { + stripeConfig, + godaddyPaymentsConfig, + squareConfig, + paypalConfig, + mercadoPagoConfig, + } = useCheckoutContext(); const { payPalRequest } = useBuildPaymentRequest(); // Start with the children and conditionally wrap with providers @@ -36,6 +42,13 @@ export function ConditionalPaymentProviders({ ); } + // Only wrap with MercadoPagoProvider if MercadoPago is configured + if (mercadoPagoConfig?.publicKey?.trim()) { + wrappedChildren = ( + {wrappedChildren} + ); + } + // Only wrap with StripeProvider if Stripe is configured if (stripeConfig?.publishableKey?.trim()) { wrappedChildren = {wrappedChildren}; diff --git a/packages/react/src/components/checkout/payment/utils/mercadopago-provider.tsx b/packages/react/src/components/checkout/payment/utils/mercadopago-provider.tsx new file mode 100644 index 00000000..f21ee8d8 --- /dev/null +++ b/packages/react/src/components/checkout/payment/utils/mercadopago-provider.tsx @@ -0,0 +1,56 @@ +import React, { + createContext, + type ReactNode, + useCallback, + useContext, + useState, +} from 'react'; + +type MercadoPagoInstance = { + bricks: () => any; +}; + +type MercadoPagoContextType = { + isLoading: boolean; + setIsLoading: (loading: boolean) => void; + handleBrickSubmit: (() => Promise) | null; + setHandleBrickSubmit: (handler: (() => Promise) | null) => void; +}; + +const MercadoPagoContext = createContext( + undefined +); + +export const MercadoPagoProvider = ({ children }: { children: ReactNode }) => { + const [isLoading, setIsLoading] = useState(false); + const [handleBrickSubmit, setHandleBrickSubmit] = useState<(() => Promise) | null>(null); + + const setIsLoadingCallback = useCallback((loading: boolean) => { + setIsLoading(loading); + }, []); + + const setHandleBrickSubmitCallback = useCallback((handler: (() => Promise) | null) => { + setHandleBrickSubmit(() => handler); + }, []); + + return ( + + {children} + + ); +}; + +export const useMercadoPago = () => { + const context = useContext(MercadoPagoContext); + if (!context) { + throw new Error('useMercadoPago must be used within a MercadoPagoProvider'); + } + return context; +}; diff --git a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts index 93af44ba..155a6372 100644 --- a/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts +++ b/packages/react/src/components/checkout/payment/utils/use-confirm-checkout.ts @@ -58,6 +58,7 @@ export enum PaymentProvider { VANTIV_EXPRESS = 'VANTIV_EXPRESS', EZETAP = 'EZETAP', ADYEN = 'ADYEN', + MERCADOPAGO = 'MERCADOPAGO', LETGO = 'LETGO', CHECK_COMMERCE = 'CHECK_COMMERCE', SQUARE = 'SQUARE', @@ -154,6 +155,9 @@ export function useConfirmCheckout() { case 'paze': completedEventId = eventIds.pazePayCompleted; break; + case 'mercadopago': + completedEventId = eventIds.mercadopagoCompleted; + break; default: completedEventId = null; } diff --git a/packages/react/src/components/checkout/payment/utils/use-load-mercadopago.ts b/packages/react/src/components/checkout/payment/utils/use-load-mercadopago.ts new file mode 100644 index 00000000..47d67954 --- /dev/null +++ b/packages/react/src/components/checkout/payment/utils/use-load-mercadopago.ts @@ -0,0 +1,61 @@ +import { useEffect, useState } from 'react'; +import { useCheckoutContext } from '@/components/checkout/checkout'; + +let isMercadoPagoLoaded = false; +let isMercadoPagoCDNLoaded = false; +const listeners = new Set<(loaded: boolean) => void>(); +const MERCADOPAGO_SDK_ID = 'mercadopago-sdk'; + +export function useLoadMercadoPago() { + const { mercadoPagoConfig } = useCheckoutContext(); + const [loaded, setLoaded] = useState(isMercadoPagoLoaded); + + const mercadoPagoCDN = 'https://sdk.mercadopago.com/js/v2'; + + useEffect(() => { + // Register this component to be notified when MercadoPago loads + const updateLoaded = (newLoaded: boolean) => setLoaded(newLoaded); + listeners.add(updateLoaded); + + // If already loaded, update immediately + if (isMercadoPagoLoaded) { + setLoaded(true); + } + + return () => { + listeners.delete(updateLoaded); + }; + }, []); + + useEffect(() => { + if ( + isMercadoPagoLoaded || + isMercadoPagoCDNLoaded || + !mercadoPagoConfig || + !mercadoPagoCDN + ) { + return; + } + + const existingScript = document.getElementById(MERCADOPAGO_SDK_ID); + if (existingScript) { + isMercadoPagoCDNLoaded = true; + return; + } + + isMercadoPagoCDNLoaded = true; + const script = document.createElement('script'); + script.id = MERCADOPAGO_SDK_ID; + script.src = mercadoPagoCDN; + script.async = true; + script.onload = () => { + isMercadoPagoLoaded = true; + // Notify all components that MercadoPago has loaded + listeners.forEach(listener => listener(true)); + }; + + document.body.appendChild(script); + }, [mercadoPagoConfig, mercadoPagoCDN]); + + return { isMercadoPagoLoaded: loaded }; +} diff --git a/packages/react/src/lib/godaddy/checkout-env.ts b/packages/react/src/lib/godaddy/checkout-env.ts index 946b67bc..b9a84f3a 100644 --- a/packages/react/src/lib/godaddy/checkout-env.ts +++ b/packages/react/src/lib/godaddy/checkout-env.ts @@ -3002,6 +3002,15 @@ const introspection = { args: [], isDeprecated: false, }, + { + name: 'ccavenue', + type: { + kind: 'OBJECT', + name: 'CheckoutSessionPaymentMethodConfig', + }, + args: [], + isDeprecated: false, + }, { name: 'express', type: { @@ -3020,6 +3029,15 @@ const introspection = { args: [], isDeprecated: false, }, + { + name: 'mercadopago', + type: { + kind: 'OBJECT', + name: 'CheckoutSessionPaymentMethodConfig', + }, + args: [], + isDeprecated: false, + }, { name: 'offline', type: { @@ -3068,6 +3086,13 @@ const introspection = { name: 'CheckoutSessionPaymentMethodConfigInput', }, }, + { + name: 'ccavenue', + type: { + kind: 'INPUT_OBJECT', + name: 'CheckoutSessionPaymentMethodConfigInput', + }, + }, { name: 'express', type: { @@ -3103,6 +3128,13 @@ const introspection = { name: 'CheckoutSessionPaymentMethodConfigInput', }, }, + { + name: 'mercadopago', + type: { + kind: 'INPUT_OBJECT', + name: 'CheckoutSessionPaymentMethodConfigInput', + }, + }, ], isOneOf: false, }, @@ -9930,6 +9962,13 @@ const introspection = { name: 'UpdateMoneyInput', }, }, + { + name: 'unitPrice', + type: { + kind: 'INPUT_OBJECT', + name: 'UpdateMoneyInput', + }, + }, ], isOneOf: false, }, diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index c4e3815e..71464742 100644 --- a/packages/react/src/lib/godaddy/checkout-mutations.ts +++ b/packages/react/src/lib/godaddy/checkout-mutations.ts @@ -105,6 +105,10 @@ export const CreateCheckoutSessionMutation = graphql(` processor checkoutTypes } + mercadopago { + processor + checkoutTypes + } } draftOrder { id diff --git a/packages/react/src/lib/godaddy/checkout-queries.ts b/packages/react/src/lib/godaddy/checkout-queries.ts index 38491fb7..25d4b2dc 100644 --- a/packages/react/src/lib/godaddy/checkout-queries.ts +++ b/packages/react/src/lib/godaddy/checkout-queries.ts @@ -105,6 +105,10 @@ export const GetCheckoutSessionQuery = graphql(` processor checkoutTypes } + mercadopago { + processor + checkoutTypes + } } locations { id diff --git a/packages/react/src/tracking/events.ts b/packages/react/src/tracking/events.ts index d453b005..c0621e65 100644 --- a/packages/react/src/tracking/events.ts +++ b/packages/react/src/tracking/events.ts @@ -27,6 +27,8 @@ export const eventIds = { pazePayImpression: 'paze_pay.impression', pazePayClick: 'paze_pay.click', pazePayCompleted: 'paze_pay_completed.event', + mercadopagoClick: 'mercadopago.click', + mercadopagoCompleted: 'mercadopago_completed.event', expressCheckoutError: 'express_checkout_error.event', // Express checkout coupon events expressApplyCouponEvent: 'express_checkout_apply_coupon.event', diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 920c95b8..b726ff1c 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -43,6 +43,7 @@ export const PaymentProvider = { PAYPAL: 'paypal', PAZE: 'paze', OFFLINE: 'offline', + MERCADOPAGO: 'mercadopago', } as const; export const CheckoutType = { @@ -62,6 +63,7 @@ export const PaymentMethodType = { GOOGLE_PAY: 'googlePay', OFFLINE: 'offline', PAZE: 'paze', + MERCADOPAGO: 'mercadopago', } as const; // Union of all payment method keys