**Last updated**: 17 March 2025

# Flutter integration with Web SDK

## Overview

The integration consists of using [web_flutter](https://pub.dev/packages/webview_flutter) (the Flutter equivalent of an iframe) to display a HTML form, which uses the Access Checkout Web SDK to interface with the Flutter layer.

## Integration

1. Follow one of our guides (e.g. [create a session to pay with a card](/access/products/checkout/web/card-only)) to integrate the Access Checkout Web SDK into a HTML page.
2. Add the [webview_flutter](https://pub.dev/packages/webview_flutter) package to your application by following the webview_flutter [installation steps](https://pub.dev/packages/webview_flutter/install).
3. Your iOS folder `Podfile` must be set up to automatically source an additional dependency called `webview_flutter_wkwebview`.
  - if you have a `Podfile`, make sure that it contains the same code as in the [webview_flutter example application Podfile](https://github.com/flutter/packages/blob/main/packages/webview_flutter/webview_flutter/example/ios/Podfile)
  - if you don't have a `Podfile` under the `ios` folder, you must generate one and it should be correctly set up by default. Run `flutter run` and select an iOS device as the destination or, run `flutter build ios` and follow the instructions
4. Import the `webview_flutter` package in your stateful widget using `import 'package:webview_flutter/webview_flutter.dart';`.
5. Create an instance of `WebViewController` in your widget's state and set the `JavaScript` mode as `unrestricted`:



```dart
class _WebViewState extends State<AccessCheckoutWebWidget> {
   late WebViewController controller;
   // ...

   @override
   void initState() {
      super.initState();

      // ...

      controller = WebViewController()
         ..setJavaScriptMode(JavaScriptMode.unrestricted)
         ..setNavigationDelegate(
            NavigationDelegate(
               onProgress: (int progress) {},
               onPageStarted: (String url) {},
               onPageFinished: (String url) {},
               onHttpError: (HttpResponseError error) {
                  print(error.response);
               },
               onWebResourceError: (WebResourceError error) {
                  print(error.description);
               },
            ),
         );
   }
}
```

1. Add a JavaScript channel with a message listener to the `WebViewController`. The JavaScript channel intercepts the messages that is sent to Flutter from the JavaScript layer:



```dart
controller = WebViewController()
   ..addJavaScriptChannel(
      'flutterWebView',
      onMessageReceived: (dynamic message) {
         setState(() {
            sessionToken = message.message;
         });
      },
   )
   // ...
```

1. In `WebViewController`, load the URL of the HTML page that contains your card form. At this stage, you should be able to see your card form displayed in your Flutter application:



```dart
controller = WebViewController()
   // ...
   ..loadRequest(
      Uri.parse("..."),
   );
```

1. Use the Flutter 'postMessage' function from the JavaScript channel in your JavaScript code to communicate from the JavaScript layer to the Flutter layer. It is available in the JavaScript context as a variable that has the same name as the name used when defining the JavaScript channel in the Flutter code:



```javascript
Worldpay.checkout.init(
 {
   ...
 },
 function (error, checkout) {
   // ...

   form.addEventListener("submit", function (event) {
     event.preventDefault();

     checkout.generateSessionState(function (error, sessionState) {
       // the session is sent to the Flutter layer using the Flutter postMessage mechanism 
       flutterJSChannel.postMessage(sessionState)
     });
   });
   
   // ...
 }
);
```

### Full integration example

Flutter

```dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class AccessCheckoutWebWidget extends StatefulWidget {
  const AccessCheckoutWebWidget({super.key});

  @override
  State<AccessCheckoutWebWidget> createState() => _WebViewState();
}

class _WebViewState extends State<AccessCheckoutWebWidget> {
  late WebViewController controller;
  String sessionToken = "";

  @override
  void initState() {
    super.initState();
    checkoutId = widget.checkoutId;
    iframeBaseUrl = widget.iframeBaseUrl;

    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'flutterJSChannel',
        onMessageReceived: (dynamic message) {
          setState(() {
            sessionToken = message.message;
          });
        },
      )
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {},
          onPageStarted: (String url) {},
          onPageFinished: (String url) {},
          onHttpError: (HttpResponseError error) {
            print(error.response);
          },
          onWebResourceError: (WebResourceError error) {
            print(error.description);
          },
        ),
      )
      ..loadRequest(
        Uri.parse("<replace-with-your-url-to-form.html>"),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            SizedBox(
                width: 400,
                height: 450,
                child: WebViewWidget(controller: controller)
            ),
            if (sessionToken != "") Text(sessionToken),
          ],
        )));
  }
}
```

HTML (form.html)

```html
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="form.css" />
</head>
<body>
<section class="container">
    <section class="card">
        <form class="checkout" id="card-form">
            <div class="label">Card number <span class="type"></span></div>
            <section id="card-pan" class="field"></section>
            <section class="col-2">
                <section class="col">
                    <div class="label">Expiry date</div>
                    <section id="card-expiry" class="field"></section>
                </section>
                <section class="col">
                    <div class="label">CVV</div>
                    <section id="card-cvv" class="field"></section>
                </section>
            </section>
            <button class="submit" type="submit">Pay Now</button>
        </form>
        <button class="clear" id="clear">Clear</button>
    </section>
</section>

<script src="https://try.access.worldpay-bsh.securedataplatform.com/access-checkout/v2/checkout.js"></script>
<script src="form.js"></script>
</body>
</html>
```

CSS (form.css)

```css
body {
    font: 11px/22px sans-serif;
    text-transform: uppercase;
    background-color: #f7f7f7;
    color: black;
}

.container {
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: flex-start;
}

.card {
    position: relative;
    background: white;
    padding: 40px 30px;
    top: 5px;
    width: 100%;
    max-width: 300px;
    border-radius: 12px;
    box-shadow: 3px 3px 60px 0px rgba(0, 0, 0, 0.1);
}
.card .checkout .col-2 {
    display: flex;
}
.card .checkout .col-2 .col:first-child {
    margin-right: 15px;
}
.card .checkout .col-2 .col:last-child {
    margin-left: 15px;
}
.card .checkout .label .type {
    color: green;
}
.card .checkout.visa .label .type:before {
    content: "(visa)";
}
.card .checkout.mastercard .label .type:before {
    content: "(master card)";
}
.card .checkout.amex .label .type:before {
    content: "(american express)";
}
.card .checkout .field {
    height: 40px;
    border-bottom: 1px solid lightgray;
}
.card .checkout .field#card-pan {
    margin-bottom: 30px;
}
.card .checkout .field.is-onfocus {
    border-color: black;
}
.card .checkout .field.is-empty {
    border-color: orange;
}
.card .checkout .field.is-invalid {
    border-color: red;
}
.card .checkout .field.is-valid {
    border-color: green;
}
.card .checkout .submit {
    background: red;
    position: absolute;
    cursor: pointer;
    left: 50%;
    bottom: -60px;
    width: 200px;
    margin-left: -100px;
    color: white;
    outline: 0;
    font-size: 14px;
    border: 0;
    border-radius: 30px;
    text-transform: uppercase;
    font-weight: bold;
    padding: 15px 0;
    transition: background 0.3s ease;
}
.card .checkout.is-valid .submit {
    background: green;
}

.clear {
    background: grey;
    position: absolute;
    cursor: pointer;
    left: 50%;
    bottom: -120px;
    width: 200px;
    margin-left: -100px;
    color: white;
    outline: 0;
    font-size: 14px;
    border: 0;
    border-radius: 30px;
    text-transform: uppercase;
    font-weight: bold;
    padding: 15px 0;
    transition: background 0.3s ease;
}
```

JavaScript (form.js)

```javascript
(function () {
  var form = document.getElementById("card-form");
  var clear = document.getElementById("clear");

  Worldpay.checkout.init(
    {
      id: "<replace-with-your-checkout-id>",
      form: "#card-form",
      fields: {
        pan: {
          selector: "#card-pan",
          placeholder: "4444 3333 2222 1111"
        },
        expiry: {
          selector: "#card-expiry",
          placeholder: "MM/YY"
        },
        cvv: {
          selector: "#card-cvv",
          placeholder: "123"
        }
      },
      styles: {
        "input": {
          "color": "black",
          "font-weight": "bold",
          "font-size": "20px",
          "letter-spacing": "3px"
        },
        "input#pan": {
          "font-size": "24px"
        },
        "input.is-valid": {
          "color": "green"
        },
        "input.is-invalid": {
          "color": "red"
        },
        "input.is-onfocus": {
          "color": "black"
        }
      },
      accessibility: {
        ariaLabel: {
          pan: "my custom aria label for pan input",
          expiry: "my custom aria label for expiry input",
          cvv: "my custom aria label for cvv input"
        },
        lang: {
          locale: "en-GB"
        },
        title: {
          enabled: true,
          pan: "my custom title for pan",
          expiry: "my custom title for expiry date",
          cvv: "my custom title for security code"
        }
      },
      enablePanFormatting: true
    },
    function (error, checkout) {
      if (error) {
        // the error is sent to the Flutter layer using the Flutter postMessage mechanism
        flutterJSChannel.postMessage(error);
        return;
      }

      form.addEventListener("submit", function (event) {
        event.preventDefault();

        checkout.generateSessionState(function (error, sessionState) {
          if (error) {
            // the error is sent to the Flutter layer using the Flutter postMessage mechanism
            flutterJSChannel.postMessage(error);
            return;
          }

          // the session is sent to the Flutter layer using the Flutter postMessage mechanism
          flutterJSChannel.postMessage(sessionState)
        });
      });

      clear.addEventListener("click", function(event) {
        event.preventDefault();
        checkout.clearForm(function() {});
      });
    }
  );
})();
```

### Tips for Android emulators

Tips for Android emulators
On Android emulator devices, `localhost` points to the emulated device. If you want the iframe to load a page hosted in your local environment you must use the `10.0.2.2` IP address. More information is available on the official [Android Emulator networking](https://developer.android.com/studio/run/emulator-networking) page.

The use of the `https` protocol has been enforced since Android 6.0. As this can be impractical for local development, you can configure an exception to allow plain text http on `10.0.2.2`. Note that this is for development only, and that you **must** always use https in Production.

1. Create a `network_config.xml` in the `res/xml` folder with the following content:



```xml
 <?xml version="1.0" encoding="utf-8"?>
 <network-security-config>
   <domain-config cleartextTrafficPermitted="true">
     <domain includeSubdomains="true">10.0.2.2</domain>
   </domain-config>
 </network-security-config>
```

1. Configure your Android app to use this file as network security configuration by adding the `android:networkSecurityConfig="@xml/network_config"` attribute to the `<application>` node of your `AndroidManifest.xml` file.