By: [email protected]

Tutorial on how to integrate native Javascript code with Flutter/Dart

Published 1/17/2022, 6:51:23 PM


Flutter / JS Bridge

Introduction

4 years ago, I effectively stumbled into what would soon be one of my favorite front end frameworks - Flutter At the time, I was looking to get really serious about cross-platform mobile development, and honestly I just really did not like ReactNative.

I ended up discovering what was a literal baby at the time, Flutter. It was only v0.2.3, very much in beta, but I loved the speed and ease of use - fun fact, that very first project ended up being my first 'real' start up BitBite. To this day it is my goto front end framework of choice especially for MVP style builds or any kind of 'internal tooling'.

Flutter isn't all sunshine and rainbows though, while it's far grown from 2018 it's still very much an infant in the scheme of tech - especially when compared to its 'rival', ReactNative.

In this post, I'm going to illustrate a method I use to 'fill in the gaps' for Flutter to help make it cover not only any use case ReactNative can but virtually any I can think of.

Flutter / JavaScript

Flutter Logo

I won't spend too long on this, but first, let's talk a bit about Flutter and JavaScript and how this is even possible to get a better understanding of what's going on.

Flutter Introduction

Flutter is a cross-platform mobile framework developed by Google and released in 2017. It's heavily focussed on design, speed and accessibility. As of Flutter 2.0, Flutter also supports Web and Desktop (macOS, Windows, Linux) out of the box as well, meaning you can write programs for all these platforms from one code base! Flutter doesn't compile down to the native language (Swift for iOS and Kotlin for Android) but instead runs on a C++ engine that is pretty efficient. Flutter plugins however are written in the native language and used at runtime

  • i.e., a location plugin will use native code to access iOS or Android location sensors properly. Check out this tutorial for more information on actual specifics for how this works.

The Problem

Flutter mostly delivers on its promises of cross-platform and simplicity, but as I mentioned earlier, due to its age (especially web/desktop) it sometimes falls short in terms of support and plugins. I first ran into this issue when developing the mobile/web application for my start-up BitBite.We started with only iOS and Android however wanted to make our platform more accessible via a web app.

Upon the production release of Flutter Web, I found most of the plugins and features carried over perfectly except one: payment. We used a platform called Braintree to handle our payment processing ( not an ad but highly recommend) and Flutter has an existing plugin to support it - fantastic! The catch? It doesn't support web, and the primary maintainer had no intention of adding support for it : /.

Braintree Logo

There's no real reason it could not, Braintree has support for web apps and JavaScript which is how Flutter plugins are implemented. This lead me down the path of learning about Flutter plugins and how to integrate native JS into a Flutter app - that process will be the focus of this tutorial. While this post will be focussed around this specific Braintree plugin/issue, this method works for any arbitrary JavaScript code you may want to implement into your Flutter app.

Let's Get Started!

Create Flutter Project

If you don't have a flutter project already, let's make one. Navigate to your project's root directory (probably empty at this point) and use the following command:

flutter create .

Important: Only use '_' NOT '-' for Dart/Flutter project names. And that's it! This will create all the generated files and skeleton associated with a flutter ap. If we want to run it simply type:

flutter run -d chrome

And after a few seconds a Chrome window should pop up and start doing its thing.

Basic Flutter App

Packages and Dependencies

Before getting started, we'll need to install some extra dependencies. Add the following to your pubspec.yaml in the dependencies section:

dependencies:
  flutter:
    sdk: flutter
  js: 0.6.3
  flutter_braintree: 2.2.0

'JS' is the Flutter library for integrating with native JavaScript and flutter braintree is the existing package that has iOS and Android support, allowing us to focus on a web only integration.

After that, just run flutter pub get to actually download these dependencies.

Web Directory

JS Directory

This is where the magic happens! The part that really helps us out here is Braintree has a JavaScript Drop in API already. This means we don't have to focus too much on actually writing the native JS which is great because that's not the point of this tutorial! First, let's create a js folder in the web folder of your project. This will contain any JavaScript files we want to include in the project. Create a file main.js to act as an entrypoint:

require.config({paths: {braintree: "https://js.braintreegateway.com/web/dropin/1.32.1/js/dropin.min.js",}});

define(function (require) {
    window.v = require('./scripts/braintree_payment');
});

This is the primary JS entrypoint, it declares where to get our Braintree drop-in UI - check to make sure you're using the most updated version - and most importantly uses the Require Library to ensure it is loaded properly at runtime. Next we need to actually 'install' RequireJS. Since we don't have the luxury of using a package manager like NPM for this, we have to include the raw JS. Thankfully this is a fairly common use case with RequireJS so this is documented well. Check the tutorial repository to get the contents of the require.js file or just get the most recent minified version online. Last but not least, create a scripts directory to contain any actual scripts we'd like to include. In this case, the only one we need right now is for the drop-in UI. Create a file called braintree_payment.js and fill it in like so:

async function payment(auth) {
    return new Promise((resolve, reject) => {
        braintree.dropin.create({authorization: auth, selector: '#dropin-container'},
            (errCreate, instance) => {
                if (errCreate) {
                    console.error("Error", errCreate);
                    return reject(errCreate);
                }
                document.getElementById("submit-button").addEventListener("click",
                    () => {
                        instance.requestPaymentMethod((errRequest, payload) => {
                            if (errRequest) {
                                console.error("Error", errRequest);
                                return reject(errRequest);
                            }
                            return resolve(payload.nonce);
                        });
                    });
            }
        );
    });
}

This is effectively the most basic version of this script, feel free to add better error handling, creation, etc. Some things to note:

  • "payment(auth)": we will pass the required authorization/API tokens to the drop in container via this function parameter.
  • "selector: #dropin-container": this is the CSS ID of the container we want to put the drop-in UI in. This will come .
  • "submit-button": should match the CSS ID of the submit button we place in. Also, will come l

Index.html

Flutter Web runs as a Single Page Application (SPA) off of web/index.html. All the above was to effectively make these JavaScript methods accessible for Flutter. To ensure that actually has the resources and dependencies it needs we need to edit said index.html file. Add the following somewhere in the head tag:

<!-- Braintree -->
<script src="https://js.braintreegateway.com/web/dropin/1.32.1/js/dropin.min.js"></script>

This will make sure these Braintree scripts are accessible from the index file. NOTE: Make sure the version of the drop-in UI you use here matches the one you put in the main.js file. You'll have to add this somewhere towards the bottom of the body section:


<script src="js/require.js"></script>
<script>require(["js/main"])</script>

To actually require our JS files as well.

Web Directory Summary

When all is said and done, your directory structure should look like this:

...

- web
    - icons
    - js
        - scripts
            - braintree_payment.js
        - require.js
        - main.js
    - favicon.jpg
    - index.html
    - manifest.json ...

Flutter Integration

Now that the pieces are all lined up, how do we actually use it?

Widgets Widgets Widgets

Flutter handles all UI/components via what are called 'widgets' which is exactly what we'll be building. Typically, you can write a widget once and expect it to work on all platforms, however, since we have already a plugin for Braintree for Flutter mobile, we're only really concerned with rendering a widget for web. This also will give us a good example of how to distinguish Flutter code by platform. We'll be doing this almost the same way actual Flutter plugins are developed, however making it a proper plugin would take a bit more work/integration.

braintree.dart

First, create a braintree folder wherever you store your widgets to hold the files. The first file will be braintree.dart and it should look like this:

export 'braintree_main.dart' if (dart.library.js) 'braintree_web.dart';

All this does is export our 'plugin' and say that this 'package' should only be available on web. This helps with compilation and warnings.

braintree_main.dart

Next, we need to make a braintree_main.dart file. This file isn't entirely necessary but allows for what I think is a nicer usage pattern that we'll see in a second. This file should look like this:

import 'package:flutter/material.dart';
import 'package:flutter_braintree/flutter_braintree.dart';

class BraintreeWidget {
  Future< BraintreeDropInResult > start(BuildContext context, BraintreeDropInRequest request) => throw UnsupportedError("BraintreeWidget unsupported on this platform");
}

NOTE: BraintreeDropInResult is a class from the Flutter plugin to help standardize how we handle the response from this and the plugin result.

braintree_web.dart

Now finally for the widget itself! This is a fairly big and open-ended file, so we'll go piece by piece. First, at the very very top of the file, import the following

@JS()
library braintree_payment;

import 'dart:html' as html; import 'dart:ui' as ui;

This declares our JS library 'braintree_payment' - this is determined by the filename of the file we placed in the 'scripts' folder. It also imports two important dart libraries we'll be using later. This should all come before the rest of your imports. Next, add the following after your imports:

@JS()
external void initBraintree(auth);

@JS()
external payment(String auth);

These are function prototypes to help the Dart compiler and things of that nature so they should match what you defined earlier as well. Now for the fun part - the widget itself. In Flutter there are two types of widgets - Stateful and Stateless - for this we'll be using a stateful widget since it will be updating and changing state during its lifecycle. Add the boilerplate for a stateful widget (hint: if you're using android studio, just type 'stfl').

To mimic the usage pattern of the braintree package, I opted to define a 'start' method accessible via the widget directly rather than via the state as one would typically do. This is actually the ' start' method we defined in 'braintree_web.dart and where most of the logic will be completed. First, for the signature:

Future< BraintreeDropInResult? > start(BuildContext context, BraintreeDropInRequest request) async { ... }

Nothing too complicated or new here - BraintreeDropInRequest is a class from the 'actual' plugin so again just mimicking its usage and structure. Worth noting as well, this object is what contains our Braintree auth tokens/transaction info so it makes abstracting all that a bit easier for me since it's already been thought about and done! For the body of the function I did the following:

// create div with html embedded
String htmlL = """< div id="checkout-message">< /div>
<div id="dropin-container"></div>
<button id="submit-button">Submit payment</button>""";
var paymentDiv = html.DivElement()..appendHtml(htmlL); // attach to container

// attach to payment container
// this line may or may not register as an error - known bug
ui.platformViewRegistry.registerViewFactory('braintree-container', (int viewId) => paymentDiv);

// show dialog
// this MUST be awaited you MUST MUST MUST store dialogResponse for some reason
Future dialogResponse = await showDialog(context: context, builder: (BuildContext context) {
  return SimpleDialog(
    title: const Text('Select assignment'), children: < Widget >[
    Container(child: this), // 'this' refers to the stateful widget itself
  ]);
});

// call js function
var promise = payment(request.clientToken ?? ""); String? nonce = await promiseToFuture(promise); // 'magic'

// close dialog
Navigator.pop(context,);

// return nonce
if (nonce != null) { // more info https://github.com/pikaju/flutter-braintree/blob/main/lib/src/result.dart
  BraintreePaymentMethodNonce btNonce = BraintreePaymentMethodNonce(nonce: nonce, typeLabel: 'Visa', description: 'Visa ending in', isDefault: false);
  return BraintreeDropInResult(paymentMethodNonce: btNonce, deviceData: null);
} else { // DIALOG CLOSED OR NONCE INVALID
  return null;
}

Once again as well, you DO NOT have to have anything like this, feel free to manage your error handling, HTML, etc. however best works for your app/feature. Then lastly - the widget itself you can do mostly what you'd like with, I kept it simple with the following:

class _BraintreeWidgetState extends State< BraintreeWidget > {
  @override Widget build(BuildContext context) {
    return const SingleChildScrollView(
      child: SizedBox(
        width: 600.0, height: 300.0, child:
        HtmlElementView(
          viewType: 'braintree-container',
      ),
    ),);
  }
}

Use the Widget!

Now to integrate into your code. I kept it fairly simple with the following code:

BraintreeDropInRequest request = BraintreeDropInRequest(
    clientToken: clientToken,
    collectDeviceData: true,
    cardEnabled: true,
    amount: getTotal(),
    googlePaymentRequest: BraintreeGooglePaymentRequest(
        currencyCode: 'USD',
        billingAddressRequired: false,
        totalPrice: getTotal(),
),);

BraintreeDropInResult? braintreeResult;
if (isWeb()) { // WEB VERSION =================================/
    braintreeResult = await BraintreeWidget().start(context, request);
} else { // MOBILE VERSION ===================================/
    braintreeResult = await BraintreeDropIn.start(request);
}

Be sure you're importing the 'braintree.dart' file as your IDE/intellisense may not be sure which is the right one. At this point - you should be 100% ready to roll! Let's start the app and see how it looks:

Braintree Payment

You can even let users access their saved payment methods via Braintree's Vault:

Braintree Vault

chefs kiss

Conclusion

And that's all there is to it! Super straight forward and obvious right? Not sure how well my sarcasm there translates but despite my happiness with Flutter, this was an incredibly frustrating journey when I had to figure it out so felt like a perfect post. Jokes aside as well, while annoying to figure out all this, it's definitely much easier once you know how. Flutter is far from perfect but truly is getting better month by month, and although annoying, it is very useful to have methods like this to fall back on in case some functionality you need is absolutely not available. From here, you have all the tools to improve on this solution and/or build your own 'plugins' for Flutter! Ensuring you have access to any features or capabilities you could virtually want! Hope this helps!

Resources

The full code is available here if you'd like to try for yourself! NOTE: this code base will not actually run properly as I have not included live Braintree keys :/ - if provided valid keys though it will work properly.


Comments

None available :/

Must be logged in to comment


Tags

Flutter