Skip to main content
On this page


Prerequisites#

Before implementing Mercado Pago Wallet payments, ensure you have:

  1. Enabled Mercado Pago in the DEUNA Admin Panel.
  2. A generated order token.
  3. Integrated the DEUNA SDK in your project.
  4. Reviewed the Payment Widget documentation for your platform.

Payment Method configuration#

Display the Payment Widget by passing the Mercado Pago configuration in the paymentMethods parameter.

JSON
[
    {
        "paymentMethod": "wallet",
        "processors": [ "mercadopago_wallet" ]
    }
]

Payment Widget - Web#

JavaScript
DeunaSDK.initPaymentWidget({
  orderToken: 'YOUR_ORDER_TOKEN',
  paymentMethods: [
    {
      paymentMethod: 'wallet',
      processors: ['mercadopago_wallet'],
    },
  ],
  callbacks: { ... },
});


Payment Widget - iOS#

Modal (PageSheet)Swift
deunaSDK.initPaymentWidget(
    orderToken: "<DEUNA_ORDER_TOKEN>",
    callbacks: PaymentWidgetCallbacks(
        onSuccess: { order in
            // Close the DEUNA widget
            self.deunaSDK.close {
                // Handle successful payment:
                // - Navigate to confirmation screen
                // - Show success message
                // - Update order status
            }
        },
        onError: { error in                
            if error.type == .paymentError {
                // Handle payment errors:
                // - Show error message
                // - Allow retry
                // - Log analytics
            }
        }
    ),
    paymentMethods: [
        [
            "paymentMethod": "wallet",
            "processors": ["mercadopago_wallet"]
        ]
    ]
)
Embedded (Swift UI)Swift
import DeunaSDK
import SwiftUI

struct PaymentView: View {
    let deunaSDK: DeunaSDK
    
    var body: some View {
        VStack {
            DeunaWidget(
                deunaSDK: deunaSDK,
                configuration: PaymentWidgetConfiguration(
                    orderToken: "YOUR_ORDER_TOKEN",
                    callbacks: PaymentWidgetCallbacks(
                        onSuccess: { order in
                            deunaSDK.dispose {
                                // Handle post-payment flow
                            }
                        },
                        onError: { error in
                            if error.type == .paymentError {
                                // Implement error handling
                            }        
                        }
                    ),
                    paymentMethods: [
                        [
                            "paymentMethod": "wallet",
                            "processors": ["mercadopago_wallet"]
                        ]
                    ]
                )
            )
        }
    }
}

Overriding callback URLs per APM (orders tokenized without callback_urls)

Some merchants tokenize the order through a third party (e.g. Sabre) that doesn't pass callback_urls at order creation. Without them, Mercado Pago Wallet has no deep link to return the shopper to, and the default DEUNA redirect bridge is used instead — which won't resume your app via a custom scheme.

To fix this from the client, pass per-APM callback URLs in behavior.paymentMethods.apms, keyed by processor name (order.payment.data.processor):

Swift
deunaSDK.initPaymentWidget(
    orderToken: "<DEUNA_ORDER_TOKEN>", // tokenized without callback_urls
    callbacks: PaymentWidgetCallbacks(
        onSuccess: { order in
            self.deunaSDK.close { }
        },
        onError: { error in }
    ),
    paymentMethods: [
        [
            "paymentMethod": "wallet",
            "processors": ["mercadopago_wallet"]
        ]
    ],
    behavior: [
        "paymentMethods": [
            "apms": [
                "mercadopago_wallet": [
                    "callbackUrls": [
                        "onSuccess": "yourapp://payment/success",
                        "onCanceled": "yourapp://payment/canceled",
                        "onPending": "yourapp://payment/pending"
                    ]
                ]
            ]
        ]
    ]
)

When the Mercado Pago link opens inside a SFSafariViewController, the system will hand the flow off to the native Mercado Pago app if it’s installed. To return to your app afterwards, add the same deep link scheme you configured in callback_urls to your Info.plist (CFBundleURLSchemes).

XML
<dict>
     …

	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleTypeRole</key>
			<string>Editor</string>
			<key>CFBundleURLName</key>
			<string>your_identifier_here</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>yourapp</string>
			</array>
		</dict>
	</array>
    …
	
</dict>


Payment Widget - Android#

For security reasons, Mercado Pago requires that their payment URLs must be loaded in a Custom Tab instead of a WebView.

It's crucial to understand that while a Custom Tab is open, you should avoid updating your UI or performing actions like navigation or showing dialogs. These operations could cause unexpected behavior, including app crashes.

Therefore, you should only execute such actions after the user has closed the Custom Tab. To detect when the Custom Tab closes, implement the following code in your MainActivity:

Kotlin
class MainActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        /**
        * Register for activity result callbacks, 
				* needed to wait until the custom chrome tab is closed
        */
        ExternalUrlHelper.registerForActivityResult(this)
    }
}

Now show the Payment Widget

Modal (DialogFragment)Kotlin
deunaSDK.initPaymentWidget(
    orderToken = "<DEUNA_ORDER_TOKEN>",
    callbacks = PaymentWidgetCallbacks().apply {
        onSuccess = { order ->
            // Close the DEUNA widget
            this.deunaSDK.close {
                // Handle successful payment:
                // - Navigate to confirmation screen
                // - Show success message
                // - Update order status
            }
        }
        onError = { error ->
            if (error.type == PaymentErrorType.PAYMENT_ERROR) {
                // Handle payment errors:
                // - Show error message
                // - Allow retry
                // - Log analytics
            }
        }
    },
    paymentMethods = listOf(
        mapOf(
            "paymentMethod" to "wallet",
            "processors" to listOf("mercadopago_wallet")
        )
    )
)
Jetpack ComposeKotlin
@Composable
fun YourScreen(
    deunaSDK: DeunaSDK,
    orderToken: String,
    userToken: String,
) {
    // Maintains the Deuna WebView instance across recompositions
    val deunaWidget = remember { mutableStateOf<DeunaWidget?>(null) }

    Column(modifier = Modifier.padding(16.dp)) {
        // Container for the embedded payment widget
        // MUST specify dimensions (e.g., .height(400.dp) or .weight(1f) in parent Column)
        Box(
            modifier = yourModifier
        ) {
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { context ->
                    DeunaWidget(context).apply {
                        this.widgetConfiguration = PaymentWidgetConfiguration(
                            sdkInstance = deunaSDK,
                            orderToken = orderToken,
                            userToken = userToken,
                            paymentMethods = listOf(
                                mapOf(
                                    "paymentMethod" to "wallet",
                                    "processors" to listOf("mercadopago_wallet")
                                )
                            ),
                            callbacks = PaymentWidgetCallbacks().apply {
                                onSuccess = { data ->
                                    deunaWidget.value?.waitUntilExternalUrlIsClosed {
                                        // Handle post-payment flow
                                    }
                                }
                                onError = { error ->
                                    // Handle error
                                }
                            },
                        )

                        this.build() // Render the DEUNA widget
                        // Store reference for later submission
                        deunaWidget.value = this
                    }
                }
            )
        }

        Spacer(modifier = Modifier.height(16.dp))
    }

    // Critical: Clean up DeunaWidget resources when composable leaves composition
    DisposableEffect(Unit) {
        onDispose {
            deunaWidget.value?.destroy()
            Log.d("DeunaWidget", "WebView resources cleaned up")
        }
    }
}

Overriding callback URLs per APM (orders tokenized without callback_urls)

Some merchants tokenize the order through a third party (e.g. Sabre) that doesn't pass callback_urls at order creation. Without them, Mercado Pago Wallet has no deep link to return the shopper to, and the default DEUNA redirect bridge is used instead — which won't resume your app via a custom scheme.

To fix this from the client, pass per-APM callback URLs in behavior.paymentMethods.apms, keyed by processor name (order.payment.data.processor):

Kotlin
deunaSDK.initPaymentWidget(
    context = this,
    orderToken = "<DEUNA_ORDER_TOKEN>", // tokenized without callback_urls
    callbacks = PaymentWidgetCallbacks().apply {
        onSuccess = { order -> }
        onError = { error -> }
    },
    paymentMethods = listOf(
        mapOf(
            "paymentMethod" to "wallet",
            "processors" to listOf("mercadopago_wallet")
        )
    ),
    behavior = mapOf(
        "paymentMethods" to mapOf(
            "apms" to mapOf(
                "mercadopago_wallet" to mapOf(
                    "callbackUrls" to mapOf(
                        "onSuccess" to "yourapp://payment/success",
                        "onCanceled" to "yourapp://payment/canceled",
                        "onPending" to "yourapp://payment/pending"
                    )
                )
            )
        )
    )
)

When the Mercado Pago link launches in a Custom Tab, Android may open the native Mercado Pago app if it's installed. To return the shopper to your app, register an intent filter for your deep link scheme in AndroidManifest.xml on the activity that handles the callback. Keeping android:launchMode="singleTask" is important so the OS reuses your existing task instead of killing the process when Custom Tabs launches the Mercado Pago link.

AndroidManifest.xmlXML
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">
    <intent-filter>
        <category android:name="android.intent.category.LAUNCHER"/>
        <action android:name="android.intent.action.MAIN"/>
    </intent-filter>

    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="yourapp" />
    </intent-filter>
</activity>

Don't declare android:host here — a scheme-only filter matches any host and path under yourapp://, so one filter covers yourapp://payment/success, yourapp://payment/pending, yourapp://payment/canceled, or any other value you ever set in callback_urls, with no manifest change needed later.


Payment Widget - React Native#

JavaScript
import React from 'react';
import { View, Button } from 'react-native';
import { DeunaSDK, DeunaWidget } from '@deuna/react-native-sdk';

// Initialize SDK
const deunaSDK = DeunaSDK.initialize({
  publicApiKey: 'YOUR_PUBLIC_API_KEY',
  environment: 'sandbox', // or "production"
});

const PaymentScreen = () => {
  const launchPayment = () => {
    deunaSDK.initPaymentWidget({
      orderToken: 'YOUR_ORDER_TOKEN',
      paymentMethods: [
        {
          paymentMethod: 'wallet',
          processors: ['mercadopago_wallet'],
        },
      ],
      callbacks: {
        onSuccess: async (order) => {
          await deunaSDK.close();
          // Handle successful payment
          console.log('Payment successful:', order);
        },
        onError: async (error) => {
          // Handle payment errors
          console.error('Payment error:', error);

          if (...) {
            // Always await the close operation
            await deunaSDK.close();
            // Handle UI updates
            console.log('Payment widget closed due to error.');
          }
        },
      },
    });
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <DeunaWidget instance={deunaSDK} />
      <Button title="Pay with Mercado Pago" onPress={launchPayment} />
    </View>
  );
};

Overriding callback URLs per APM (orders tokenized without callback_urls)

Some merchants tokenize the order through a third party (e.g. Sabre) that doesn't pass callback_urls at order creation. Without them, Mercado Pago Wallet has no deep link to return the shopper to, and the default DEUNA redirect bridge is used instead — which won't resume your app via a custom scheme.

To fix this from the client, pass per-APM callback URLs in behavior.paymentMethods.apms, keyed by processor name (order.payment.data.processor):

JavaScript
await deunaSDK.initPaymentWidget({
  orderToken: 'YOUR_ORDER_TOKEN', // tokenized without callback_urls
  paymentMethods: [
    {
      paymentMethod: 'wallet',
      processors: ['mercadopago_wallet'],
    },
  ],
  behavior: {
    paymentMethods: {
      apms: {
        mercadopago_wallet: {
          callbackUrls: {
            onSuccess: 'yourapp://payment/success',
            onCanceled: 'yourapp://payment/canceled',
            onPending: 'yourapp://payment/pending',
          },
        },
      },
    },
  },
  callbacks: {
    onSuccess: async (order) => {
      await deunaSDK.close();
    },
    onError: async (error) => {
      await deunaSDK.close();
    },
  },
});

Overview

When Mercado Pago opens in a native browser surface (SafariViewController on iOS or Custom Tabs on Android), you must configure deep links so the user returns to your app after the payment outcome. Use the same deep link URLs you send in order.callback_urls.

  1. Configure Deep Links (iOS) Register the scheme in Info.plist so iOS can route the callback to your app.
Info.plistXML
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>com.yourcompany.yourapp</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>yourapp</string>
    </array>
  </dict>
</array>
  1. Configure Deep Links (Android) Add a single VIEW intent filter for your scheme. Keep android:launchMode="singleTask" so the OS reuses the existing task when the deep link arrives.

    AndroidManifest.xmlXML
    <activity
      android:name=".MainActivity"
      android:exported="true"
      android:launchMode="singleTask">
    
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="yourapp" />
      </intent-filter>
    </activity>

    Don't add android:host here — a scheme-only filter matches any host/path under yourapp://, so it covers every outcome in callback_urls without further manifest changes.

  2. Handle Deep Links in React Native Listen for deep-link events and close the in-app browser when the callback arrives.

    JavaScript
    import { Linking } from 'react-native';
    import InAppBrowser from 'react-native-inappbrowser-reborn';
    
    Linking.addEventListener('url', ({ url }) => {
      if (url?.startsWith('yourapp://')) {
        InAppBrowser.close(); // iOS: closes SFSafariViewController
        // Resume/finish the checkout flow here.
      }
    });

    Match callback_urls to Your Deep Links When creating the order, ensure callback_urls match the same scheme/host configured above.

    JSON
    "callback_urls": {
      "on_success": "yourapp://payment/success",
      "on_pending": "yourapp://payment/pending",
      "on_canceled": "yourapp://payment/canceled"
    }

    Notes

    • android:launchMode="singleTask" is important so the OS delivers the callback to the existing activity instead of launching a new instance or killing the previous task.

    • Your JS handler should map each URL (success, pending, canceled) to the correct checkout state.

Overview For Expo apps, use expo-web-browser to open Mercado Pago and configure deep links with the scheme in app.json. Then listen to Linking events to close the browser and resume the flow after the callback.

  1. Configure Deep Links in app.json Define a scheme and add a single Android intent filter for it.

    JSON
    {
      "expo": {
        "scheme": "yourapp",
        "android": {
          "intentFilters": [
            {
              "action": "VIEW",
              "data": [{ "scheme": "yourapp" }],
              "category": ["BROWSABLE", "DEFAULT"]
            }
          ]
        }
      }
    }

    Don't add host to the data entry — a scheme-only filter matches any host/path under yourapp://, so it covers every outcome in callback_urls without further changes.


  2. Open Mercado Pago with expo-web-browser Implement an InAppBrowserAdapter that uses openBrowserAsync.

    JavaScript
    import * as WebBrowser from 'expo-web-browser';
    import { InAppBrowserAdapter } from '@deuna/react-native-sdk';
    
    class ExpoWebBrowserAdapter implements InAppBrowserAdapter {
      async openUrl(url: string): Promise<void> {
        await WebBrowser.openBrowserAsync(url, {
          presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET,
          dismissButtonStyle: 'close',
        });
      }
    }
  3. Handle Deep Link Callbacks in JS Listen for Linking events and dismiss the browser on iOS when the callback arrives.

    JavaScript
    import { Linking, Platform } from 'react-native';
    import * as WebBrowser from 'expo-web-browser';
    
    WebBrowser.maybeCompleteAuthSession();
    
    Linking.addEventListener('url', ({ url }) => {
      if (url?.startsWith('yourapp://') && Platform.OS === 'ios') {
        WebBrowser.dismissBrowser();
      }
      // Route success/pending/canceled based on the URL.
    });
  4. Match callback_urls to Your Deep Links Ensure your order uses the same scheme/host values.

    JSON
    "callback_urls": {
      "on_success": "yourapp://payment/success",
      "on_pending": "yourapp://payment/pending",
      "on_canceled": "yourapp://payment/canceled"
    }

Notes

  • WebBrowser.maybeCompleteAuthSession() is required so iOS can complete and close the session after a deep-link callback.
  • Ensure your JS handler maps each callback URL to the correct checkout state.