PayPal Pub Hell in Flutter

Video thumbnail

A while ago, I had uploaded a video here on YouTube where I talked about the webview hell:

The WebView Hell in Flutter

I'll quickly summarize the problem so you have context for what I'm about to talk about.

The nightmare consisted of the following: for each operating system (Android, iOS, macOS, Windows) and for each type of operation (whether rendering HTML content via a URL, like a browser, or directly representing an HTML code string), the implementation was chaos.

  • For Android and iOS, we had one plugin.
  • For Mac, we had others that, in fact, none worked correctly for me.
  • For Windows, we had another, and so on.

Therefore, simply to render a page or HTML content, it became a nightmare for all platforms, leading to having four different plugins, and I still have several installed.

However, it seems that in trying to solve those problems, we ran into another one.

Introduction: why integrating PayPal in Flutter is still so complicated

If you're going to integrate PayPal in Flutter, be prepared: it's not a beautiful path. I'm saying this, having already eaten the WebViews hell in Flutter, and just when I thought the worst was over, I discovered that PayPal brings the same damn story... but with an extra twist.

When I dealt with WebViews a while ago, I had one plugin for Android, another for iOS, another for macOS (which didn't even work), another distinct one for Windows... it felt like collecting WebViews like trading cards. Rendering simple HTML turned into a nightmare because each platform did whatever it wanted.

And I mention this because most PayPal plugins in Flutter are exactly that: disguised WebViews. And you know what happens when a WebView breaks: the entire app explodes with you inside.

The Problem with PayPal Plugins

We've gone through the nightmare or hell with payment plugins or extensions (like PayPal) in Flutter, where we find a similar problem. I mentioned the Webviews issue for the simple reason that, basically, these plugins work analogously: they simply internally install a "blessed WebView" and the payment is processed from there.

The Real Problem: PayPal doesn't have an official SDK for Flutter

Stripe *did* bet on Flutter. It has its official, solid, maintained, and cross-platform SDK.
PayPal does not. PayPal in Flutter is a jungle of:

  • Unofficial plugins
  • WebViews wrapped in sugar
  • Broken dependencies
  • Errors that appear on some platforms and not others
  • Fragmented documentation

And since there is no native SDK, all attempts are based on:

  1. WebView
  2. REST APIs (create order with backend)
  3. Launching PayPal in an external browser (the most stable, but less elegant)

I've tried all these paths. Some made me doubt my life choices.

flutter_paypal

This plugin, as I speak these words, prevents the application from starting because its dependency is very old:

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.

When a PayPal plugin works, it's almost always because it internally installs a WebView that displays the checkout. And when it fails, it usually has nothing to do with PayPal:

  • The problem is the WebView or its dependency.

In my case, for example, the `flutter_paypal` plugin wouldn't even let my app start because of `webview_flutter_x5` and a broken namespace. As soon as I removed it, the app came back to life as if nothing had happened. That's how fragile the ecosystem is.

webview_flutter_x5

By the way, a point that often bothers me is that none of these plugins are PayPal-specific.

This highlights another of the structural problems we have in Flutter: because Flutter is at the top of the pyramid (where the base or native mobile development is Swift, Kotlin, Android Studio, etc.), many companies don't bother to create a dedicated native plugin for their platforms.

It would be greatly appreciated if more companies adopted this approach, as Stripe did, for example (even though the implementation in Stripe is also a small nightmare):

Flutter Stripe

To use the 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');
      });
}

Configuring the plugin because we have to configure at least five things but that's another story but at least it's here

This is a plugin that ChatGPT recommended to me (since, by the way, it was hard for me to find). I'll show you a bit of the code and the syntax, taking the opportunity to include it.

paypal_sdk

Meanwhile, I'm going to run this other plugin that I've also been using: the PayPal SDK (which I've already removed from view).

This is a small nightmare because, if you've taken my Laravel course, you know the joke is that you create the entire order manually (you define products, quantities, taxes, etc.). At the end of the process, the plugin returns a blessed link for you to launch it again.

This is where we return, again, to the web part. You have to take that link and launch it in the browser (or "wherever God knows you want") so that the user processes the order there.

// 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;
}

So, the process becomes a nightmare because when you leave the mobile application (that is, you go to a web application or an external browser), you lose the direct way of knowing when the payment operation will end.

When the user returns to your Flutter app, you need a way to verify the status. The only solution I could think of was to place a synchronization button or a similar mechanism to force the update of operations once the user has returned to the application.

I simply didn't like this implementation because:

It Breaks the User Experience (UX): The flow is neither fluid nor automatic, requiring a manual action by the user.

Loss of State: Immediate control and the transaction callback that you would have in a native or internal environment are lost.

This need to manually synchronize upon return is what unnecessarily complicates the integration.

flutter_paypal_payment

With this other plugin, I previously had problems on MacOS where upon making the payment, I got the error:

Your PayPal credentials seems incorrect flutter_paypal_payment

But only on MacOS. After some time and updating dependencies and Flutter, that error no longer appears, but in the emulator the order remains processing:

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:');
                    },
                  ),
                ));

Real Options for Integrating PayPal in Flutter (and what to expect)

1) WebView-based Plugins (flutter_paypal, flutter_paypal_payment, others)

They are the "fastest," but also the most problematic.
Pros:

  • Simple to use
  • You don't need a backend

Cons:

  • They break easily
  • Outdated dependencies
  • Unstable behavior depending on the platform
  • Don't work the same on mobile, web, Windows, macOS
  • I tested almost all of them. None survived on all 4–5 platforms.

2) Using PayPal SDK via REST API (creating orders manually)

Here the idea is to build the order from code:

  • Create an order
  • Get the approveUrl
  • Launch it in a browser
  • Wait for the user to approve
  • Capture the order from the backend

That's what I did using `paypal_sdk`. It works, yes, but it's a manual nightmare: assembling the `purchaseUnits`, catching errors, validating tokens... all very "Laravel" if you come from backend.

3) Opening PayPal in an external browser and returning to the app

Curiously, this was the most "stable" part.

But it brings a huge problem:

When the user exits to a browser, there is NO automatic way to know when they finished.

In my case, I ended up placing a button that synchronized the operation when the user returned to the app. Not elegant, but effective.

4) Integration for Flutter Web

Here the story is even more limited:

  • You cannot use native WebViews
  • Most plugins don't support web
  • The most stable approach is to use REST API + redirect + backend verification

Frequently Asked Questions (FAQ)

  • Is there an official PayPal SDK for Flutter?
    • No. That's why all the plugins are workarounds.
  • What is the most stable plugin?
    • None is 100%.
      The stable path is backend + redirect.
  • Why is PayPal on Flutter Web limited?
    • Because there is no native WebView and no official SDK exists.
  • How do I prevent PayPal from opening an external browser?
    • You need a WebView, but that comes with risks.
      External browser is more stable.
  • How to correctly validate the payment?
    • Always from your backend with the official PayPal API.

Remember that this is the second payment gateway we looked at, along with Stripe and PayPal.

I agree to receive announcements of interest about this Blog.

I'll talk about the problems I've had trying to use a PayPal plugin for Flutter, some possible uses, general testing, and recommendations.

| 👤 Andrés Cruz

🇪🇸 En español