El Infierno de los Pub de PayPal en Flutter

Video thumbnail

Hace un tiempo había subido un video aquí en YouTube en la cual te hablaba sobre el infierno de los webview:

El Infierno de los WebView en Flutter

Te resumo rápidamente el problema para que entres en contexto con lo que voy a hablar.

La pesadilla consistía en lo siguiente: para cada sistema operativo (Android, iOS, macOS, Windows) y para cada tipo de operación (ya sea renderizar contenido HTML mediante una URL, como si fuera un navegador, o representar directamente un string de código HTML), la implementación era un caos.

  • Para Android e iOS, teníamos un plugin.
  • Para Mac, teníamos otros que, de hecho, no me funcionaba ninguno correctamente.
  • Para Windows, teníamos otro, y así sucesivamente.

Por lo tanto, simplemente para renderizar una página o un contenido HTML, se convertía en una pesadilla para todas las plataformas, llegando a tener cuatro plugins diferentes y aún tengo varios instalados.

Sin embargo, parece que, al intentar solucionar esos problemas, nos encontramos con otro.

Introducción: por qué integrar PayPal en Flutter sigue siendo tan complicado

Si vas a integrar PayPal en Flutter, prepárate: no es un camino bonito. Lo digo yo, que ya me comí el infierno de los WebViews en Flutter y, cuando pensé que lo peor había pasado, descubrí que PayPal trae la misma maldita historia… pero con giro extra.

Cuando lidié con WebViews hace un tiempo, tenía un plugin para Android, otro para iOS, otro para macOS (que encima no funcionaba), otro distinto para Windows… parecía coleccionar WebViews como si fueran cromos. Renderizar un HTML simple se convertía en una pesadilla porque cada plataforma hacía lo que le daba la gana.

Y lo menciono porque la mayoría de plugins de PayPal en Flutter son exactamente eso: WebViews disfrazadas. Y ya sabes lo que pasa cuando un WebView se rompe: la app completa explota contigo dentro.

El problema de los plugins de PayPal

Hemos pasado por la pesadilla o el infierno con los plugins o extensiones de pago (como PayPal) en Flutter, donde encontramos una problemática similar. Mencioné el tema de los Webviews por la simple razón de que, básicamente, estos plugins funcionan de manera análoga: simplemente, internamente instalan un "bendito Webview" y, a partir de ahí, se procesa el pago.

El verdadero problema: PayPal no tiene SDK oficial para Flutter

Stripe sí apostó por Flutter. Tiene su SDK oficial, sólido, mantenido y multiplataforma.
PayPal no. PayPal en Flutter es una selva de:

  • Plugins no oficiales
  • WebViews envueltos en azúcar
  • Dependencias rotas
  • Errores que aparecen en unas plataformas y en otras no
  • Documentación fragmentada

Y como no hay SDK nativo, todos los intentos se basan en:

  1. WebView
  2. APIs REST (crear orden con backend)
  3. Lanzar PayPal en navegador externo (lo más estable, pero menos bonito)

Yo he probado todas estas vías. Algunas me hicieron dudar de mis decisiones de vida.

flutter_paypal

Este plugin al momento que digo estas palabras, hace que no levante la aplicación, debido a que su dependencia es muy antigua:

A problem occurred configuring project ':webview_flutter_x5'.
> Could not create an instance of type com.android.build.api.variant.impl.LibraryVariantBuilderImpl.
   > Namespace not specified. Specify a namespace in the module's build file: /Users/andrescruz/.pub-cache/hosted/pub.dev/webview_flutter_x5-2.0.8+9/android/build.gradle. See https://d.android.com/r/tools/upgrade-assistant/set-namespace for information about setting the namespace.

Cuando un plugin PayPal funciona, casi siempre es porque instala internamente un WebView que muestra el checkout. Y cuando falla, normalmente no tiene nada que ver con PayPal:

  • El problema es el WebView o su dependencia.

En mi caso, por ejemplo, el plugin flutter_paypal no dejó ni arrancar mi app por culpa de webview_flutter_x5 y un namespace roto. Apenas lo quitaba, la app volvía a la vida como si nada hubiese pasado. Así de frágil es el ecosistema.

webview_flutter_x5

Por cierto, un punto que a menudo me fastidia es que ninguno de estos plugins es específico de PayPal.

Esto evidencia otro de los problemas estructurales que tenemos en Flutter: debido a que Flutter se ubica en la cima de la pirámide (donde la base o el desarrollo móvil nativo es Swift, Kotlin, Android Studio, etc.), muchas empresas no se preocupan por crear un plugin nativo dedicado para sus plataformas.

Se agradecería muchísimo que más compañías adoptaran este enfoque, como sí lo hicieron, por ejemplo, en Stripe (aunque la implementación en Stripe sea también una pequeña pesadilla):

Flutter Stripe

Para usar el plugin:

payPackWithPaypalPlugin(
    String price, String tagPack, String userToken, BuildContext context,
    [String coupon = '']) {
  return UsePaypal(
      sandboxMode: !appPayPalProduction,
      clientId: paypalClientId,
      secretKey: paypalSecret,
      returnURL: "https://example.com/success",
      cancelURL: "https://example.com/cancel",
      transactions: [
        {
          "amount": {
            "total": price,
            "currency": "USD",
          },
          "description":
              "${LocaleKeys.thankYouForPurchasingThisCourse.tr()}: $tagPack",
          "item_list": {
            "items": [
              {
                "name": tagPack,
                "quantity": 1,
                "price": price,
                "currency": "USD"
              }
            ],
          }
        }
      ],
      note: "Contact us for any questions on your order.",
      onSuccess: (Map params) {
        print("onSuccess: $params");
      },
      onError: (error) {
        print("onError: $error");
      },
      onCancel: (params) {
        print('cancelled: $params');
      });
}

Configurar el plugin porque tenemos que configurar al menos como cinco cosas pero esa es otra historia pero al menos lo hay aquí 

Este es un plugin que me recomendó ChatGPT (ya que, por cierto, me costó encontrarlo). Te mostraré un poco del código y la sintaxis, aprovechando para colocarlo.

paypal_sdk

Mientras tanto, voy a ejecutar este otro plugin que también he estado utilizando: el PayPal SDK (que ya quité de la vista).

Este es una pequeña pesadilla porque, si has tomado mi curso de Laravel, sabes que el chiste es que creas toda la orden de manera manual (defines productos, cantidades, impuestos, etc.). Al final del proceso, el plugin te devuelve un bendito enlace para que tú lo lances otra vez.

Aquí es donde volvemos, nuevamente, a la parte web. Tienes que tomar ese enlace y lanzarlo al navegador (o "hacia donde Dios sabe tú quieras") para que por ahí el usuario procese la orden.

// Crea la orden de PayPal y genera el enlace de pago
Future<Order?> createOrder(String amount) async {
  // claves
  var paypalEnvironment =
      appPayPalProduction
          ? PayPalEnvironment.live(
            clientId: paypalClientId,
            clientSecret: paypalSecret,
          )
          : PayPalEnvironment.sandbox(
            clientId: paypalClientId,
            clientSecret: paypalSecret,
          );


  // client
  var payPalHttpClient = PayPalHttpClient(
    paypalEnvironment /*, accessToken: accessToken*/,
    accessTokenUpdatedCallback: (accessToken) async {
      // Persist token for re-use
    },
  );


  // crea la ordenApi
  final ordersApi = OrdersApi(payPalHttpClient);


  try {
    // orden
    final orderRequest = OrderRequest(
      intent: OrderRequestIntent.authorize,


      purchaseUnits: [
        PurchaseUnitRequest(
          // amount: AmountWithBreakdown(currencyCode: "USD", value: "10.00"),
          amount: AmountWithBreakdown(currencyCode: "USD", value: amount),
        ),
      ],
    );


    // creamos la orden
    final order = await ordersApi.createOrder(orderRequest);


    // link para procesar la orden
    String? approveUrl =
        order.links?.firstWhere((link) => link.rel == "approve").href;


    // la orden fue creada exitosamente
    if (approveUrl != null && approveUrl.isNotEmpty) {
      await launchUrl(
        Uri.parse(approveUrl),
        mode: LaunchMode.externalApplication,
      );


      print("Respuesta de la orden: ${jsonEncode(order.toJson())}");


      return order;
    } else {
      print("⚠ No se pudo obtener la URL de aprobación.");
    }
  } catch (e) {
    print("Error al crear la orden: $e");
  }


  return null;
}

Entonces, el proceso se convierte en una pesadilla porque, cuando tú sales de la aplicación móvil (es decir, vas a una aplicación web o a un navegador externo), pierdes la forma directa de saber cuándo va a terminar la operación de pago.

Cuando el usuario regresa a tu app de Flutter, necesitas una manera de verificar el estado. La única solución que se me ocurrió fue colocar un botón de sincronización o un mecanismo similar para forzar la actualización de las operaciones una vez que el usuario ha vuelto a la aplicación.

Simplemente, no me gustó esta implementación porque:

Rompe la Experiencia del Usuario (UX): El flujo no es fluido ni automático, requiere una acción manual del usuario.

Pérdida de Estado: Se pierde el control inmediato y el callback de la transacción que tendrías en un entorno nativo o interno.

Esta necesidad de sincronizar manualmente al regreso es lo que complica innecesariamente la integración.

flutter_paypal_payment

Este otro plugin, tuve problemas antes en MacOS que al hacer el pago, me daba el error de:

Your PayPal credentials seems incorrect flutter_paypal_payment

Pero solamente en MacOS, al pasar el tiempo y al actualizar dependencias y Flutter, ya no aparece ese error, pero en el emulador la orden se queda procesando:

Navigator.of(context).push(MaterialPageRoute(
                  builder: (BuildContext context) => PaypalCheckoutView(
                    sandboxMode: true,
                    clientId: "",
                    secretKey: "",
                    transactions: const [
                      {
                        "amount": {
                          "total": '70',
                          "currency": "USD",
                          "details": {
                            "subtotal": '70',
                            "shipping": '0',
                            "shipping_discount": 0
                          }
                        },
                        "description": "The payment transaction description.",
                        // "payment_options": {
                        //   "allowed_payment_method":
                        //       "INSTANT_FUNDING_SOURCE"
                        // },
                        "item_list": {
                          "items": [
                            {
                              "name": "Apple",
                              "quantity": 4,
                              "price": '5',
                              "currency": "USD"
                            },
                            {
                              "name": "Pineapple",
                              "quantity": 5,
                              "price": '10',
                              "currency": "USD"
                            }
                          ],


                          // shipping address is not required though
                          //   "shipping_address": {
                          //     "recipient_name": "tharwat",
                          //     "line1": "Alexandria",
                          //     "line2": "",
                          //     "city": "Alexandria",
                          //     "country_code": "EG",
                          //     "postal_code": "21505",
                          //     "phone": "+00000000",
                          //     "state": "Alexandria"
                          //  },
                        }
                      }
                    ],
                    note: "Contact us for any questions on your order.",
                    onSuccess: (Map params) async {
                      print("onSuccess: $params");
                    },
                    onError: (error) {
                      print("onError: $error");
                      Navigator.pop(context);
                    },
                    onCancel: () {
                      print('cancelled:');
                    },
                  ),
                ));

Opciones reales para integrar PayPal en Flutter (y qué puedes esperar)

1) Plugins basados en WebView (flutter_paypal, flutter_paypal_payment, otros)

Son los más “rápidos”, pero también los más problemáticos.
Pros:

  • Sencillos de usar
  • No necesitas backend

Contras:

  • Se rompen con facilidad
  • Dependencias desactualizadas
  • Comportamiento inestable según plataforma
  • No funcionan igual en móvil, web, Windows, macOS
  • Yo los probé casi todos. Ninguno se salvó en las 4–5 plataformas.

2) Usar PayPal SDK vía REST API (crear órdenes manualmente)

Aquí la idea es armar la orden desde código:

  • Crear una orden
  • Obtener el approveUrl
  • Lanzarlo en navegador
  • Esperar a que el usuario apruebe
  • Capturar la orden desde backend

Eso hice yo usando paypal_sdk. Funciona, sí, pero es una pesadilla manual: armar las purchaseUnits, capturar errores, validar tokens… todo muy “Laravel” si vienes de backend.

3) Abrir PayPal en un navegador externo y volver a la app

Curiosamente, esta fue la parte más “estable”.

Pero trae un problema enorme:

Cuando el usuario sale a un navegador NO hay forma automática de saber cuándo terminó.

En mi caso, terminé colocando un botón que sincronizaba la operación cuando el usuario volvía a la app. Nada elegante, pero efectivo.

4) Integración para Flutter Web

Aquí la historia es aún más limitada:

  • No puedes usar WebViews nativos
  • La mayoría de plugins no soportan web
  • Lo más estable es usar REST API + redirect + verificación backend

Preguntas frecuentes (FAQ)

  • ¿Existe un SDK oficial de PayPal para Flutter?
    • No. Por eso todos los plugins son workarounds.
  • ¿Cuál es el plugin más estable?
    • Ninguno al 100%.
      La vía estable es backend + redirect.
  • ¿Por qué PayPal en Flutter Web es limitado?
    • Porque no hay WebView nativo y no existe SDK oficial.
  • ¿Cómo evito que PayPal abra navegador externo?
    • Necesitas WebView, pero eso trae riesgos.
      Navegador externo es más estable.
  • ¿Cómo validar el pago correctamente?
    • Siempre desde tu backend con la API oficial de PayPal.

Recuerda que esta es la segunda pasarela de pagos que vimos junto con la pasarela de pago de Stripe y PayPal.

Acepto recibir anuncios de interes sobre este Blog.

Hablaré de los problemas que he tenido al intentar usar un plugin de PayPal para Flutter, algunos posibles usos, pruebas en general y recomendaciones.

| 👤 Andrés Cruz

🇺🇸 In english