Mercado Pago via Widget



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.

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

Payment Widget - Web

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


Payment Widget - iOS

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"]
        ]
    ]
)
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"]
                        ]
                    ]
                )
            )
        }
    }
}
📘

iOS-Specific Considerations

⚠️ Mercado Pago Security Requirements

  1. SafariViewController Requirement:
    • Mercado Pago mandates the use of SafariViewController for payment processing.
    • This ensures secure handling of payment credentials.
  2. ViewController Dismissal:
    • The SafariViewController must be manually dismissed by the user.
    • The SDK's close() function won't automatically dismiss it.

✅ Best Practices

Always provide a completion handler to the close() function to ensure your code executes only after both:

  1. The user manually dismisses the SafariViewController (as required by Mercado Pago)
  2. The payment modal completes its dismissal animation
self.deunaSDK.close {
    // This executes sequentially after:
    // 1. User closes SafariViewController
    // 2. Payment modal fully disappears

    // Recommended actions:
    // 1. Navigate to order confirmation
    // 2. Display transaction success UI
    // 3. Update application state
}

✅ Memory Management

For embedded implementations, always call dispose() when the widget is no longer needed to prevent memory leaks:

deunaSDK.dispose {
    // Cleanup resources
}

iOS - Deep links

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).

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

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

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")
        )
    )
)
@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")
        }
    }
}
📘

Android-Specific Considerations

⚠️ Mercado Pago Security Requirements

  1. Custom Tabs Requirement:
    • Payment flows must launch in Chrome Custom Tabs.
    • This ensures secure handling of payment credentials.
  2. Close the Custom Tab:
    • The Custom Tab must be manually closed by the user.
    • Programmatic closing is restricted for security. The SDK's close() function won't automatically close it.

✅ Best Practices for Modal Implementation

Always provide a completion handler to the close()function to ensure your code executes only after both:

  1. The user manually closes the Custom Tab (as required by Mercado Pago)
  2. The payment dialog is closed
self.deunaSDK.close {
    // Execution sequence:
    // 1. User closes Custom Tab
    // 2. Payment dialog animates out
    // 3. This block executes

    // Recommended actions:
    // 1. Navigate to order confirmation
    // 2. Display transaction success UI
    // 3. Update application state
}

✅ Best Practices for Embedded Implementation

Use the waitUntilExternalUrlIsClosed to ensure your code executes only after the user manually closes the Custom Tab.

deunaWidget.value?.waitUntilExternalUrlIsClosed {
	// Handle post-payment flow
}

✅ Memory Management

For embedded implementations, always call destroy() when the widget is no longer needed to prevent memory leaks:

@Composable
fun PaymentScreen() {
    DisposableEffect(Unit) {
        onDispose {
            // Always clean up WebView resources
            deunaWidget.destroy()
        }
    }
}

Android - Deep Links

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, mirror each deep link scheme/host from the callback_urls inside your AndroidManifest.xml by adding intent filters 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 Mercado Pago link.

<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" android:host="congrats" />
    </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" android:host="pending" />
    </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" android:host="failed" />
    </intent-filter>
</activity>

Match the host values to the ones you defined in callback_urls (congrats, pending, failed) so Mercado Pago can deep-link back to your Android activity for every outcome.


Payment Widget - React Native

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>
  );
};
📘

Specific Considerations for Mercado Pago

For secure payment processing and adherence to Mercado Pago's security policies, the SDK employs:

  • iOS: SFSafariViewController.
  • Android: Chrome Custom Tabs.
📘

IMPORTANT: Starting from v2.0.0 of the DEUNA SDK, payment methods that need to open in Custom Tabs (Android) or Safari View Controller (iOS) require configuring a custom adapter using the InAppBrowserAdapter interface and passing it to the DeunaSDK.initialize method.

For more information, see Mercado Pago Wallet - support.


✅ These components are crucial for:

  • Providing secure payment processing.
  • Maintaining session continuity.
  • Complying with Mercado Pago's security policies.

✅ SafariViewController or Custom Tab Dismissal

It's important to note that the SafariViewController or Custom Tab must be manually closed by the user. The SDK's close() function will not automatically close them.


✅ Best Practices

The close() function returns a Promise. To ensure your code executes only after both the user manually dismisses the SafariViewController/Custom Tab and the payment modal completes its dismissal animation, make sure to await this Promise.

callbacks: {
  onSuccess: async (order) => {
      // Always await the close operation
      await deunaSDK.close();
      
      // Safe to execute after:
      // 1. User closes browser component
      // 2. Widget completes dismissal
  }
}

✅ Memory Management

Always call deunaSDK.close() when you no longer need the DEUNA widget to:

  • Free up allocated resources
  • Prevent memory leaks
  • Ensure proper cleanup of payment sessions
useEffect(() => {
  return () => {
    // Clean up when component unmounts
    deunaSDK.close();
  };
}, []);

React Native CLI – Deep Links and Redirect Handling (Generic)

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 <Anchor label="order.callback_urls" target="_blank" href="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).">order.callback_urls</Anchor>.

  1. Configure Deep Links (iOS) Register the scheme in Info.plist so iOS can route the callback to your app.
<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 VIEW intent filters for each outcome URL you defined in callback_urls. Keep android:launchMode="singleTask" so the OS reuses the existing task when the deep link arrives.

    <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" android:host="success" />
      </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" android:host="pending" />
      </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" android:host="failed" />
      </intent-filter>
    </activity>
    
  2. Handle Deep Links in React Native Listen for deep-link events and close the in-app browser when the callback arrives.

    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.

    "callback_urls": {
      "on_success": "yourapp://success",
      "on_pending": "yourapp://pending",
      "on_failed": "yourapp://failed"
    }

    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, failed) to the correct checkout state.

Expo – Deep Links and Redirect Handling (Generic)

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 Android intent filters for each callback outcome.

    {
      "expo": {
        "scheme": "yourapp",
        "android": {
          "intentFilters": [
            {
              "action": "VIEW",
              "data": [{ "scheme": "yourapp", "host": "success" }],
              "category": ["BROWSABLE", "DEFAULT"]
            },
            {
              "action": "VIEW",
              "data": [{ "scheme": "yourapp", "host": "pending" }],
              "category": ["BROWSABLE", "DEFAULT"]
            },
            {
              "action": "VIEW",
              "data": [{ "scheme": "yourapp", "host": "failed" }],
              "category": ["BROWSABLE", "DEFAULT"]
            }
          ]
        }
      }
    }

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

    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.

    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/failed based on the URL.
    });
  4. Match callback_urls to Your Deep Links Ensure your order uses the same scheme/host values.

    "callback_urls": {
      "on_success": "yourapp://success",
      "on_pending": "yourapp://pending",
      "on_failed": "yourapp://failed"
    }

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.