**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](/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 { 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 import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class AccessCheckoutWebWidget extends StatefulWidget { const AccessCheckoutWebWidget({super.key}); @override State createState() => _WebViewState(); } class _WebViewState extends State { 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(""), ); } @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)
Card number
Expiry date
CVV
CSS (form.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) (function () { var form = document.getElementById("card-form"); var clear = document.getElementById("clear"); Worldpay.checkout.init( { 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 10.0.2.2 ``` 1. Configure your Android app to use this file as network security configuration by adding the `android:networkSecurityConfig="@xml/network_config"` attribute to the `` node of your `AndroidManifest.xml` file.