> ## Documentation Index
> Fetch the complete documentation index at: https://dev.moonpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter

> Reference implementation for Flutter (Dart)

This page contains a complete reference implementation for integrating the MoonPay Developer Platform in Flutter. The implementation is entirely in Dart and uses `webview_flutter` to render MoonPay frames.

## Overview

The Flutter SDK consists of the following files:

| File | Description |
| - | - |
| `types.dart` | Type definitions imported from generated OpenAPI types, plus SDK-specific types like `Result`, `Connection`, and error types |
| `api.dart` | API client functions for fetching payment methods and quotes |
| `client.dart` | Main `MoonPayClient` class that manages credentials and provides a unified interface |
| `crypto.dart` | X25519 key generation and AES-GCM decryption for secure credential handling |
| `frames.dart` | Flutter widgets (`ConnectFrame`, `ApplePayFrame`) that wrap WebViews to render MoonPay frames |

## Project Setup

> [accordion] pubspec.yaml - Dependencies
>
> ```yaml pubspec.yaml theme={null}
> dependencies:
>   flutter:
>     sdk: flutter
>   webview_flutter: ^4.4.2
>   webview_flutter_android: ^4.3.0 # Android platform
>   webview_flutter_wkwebview: ^3.18.0 # iOS/macOS platform
>   http: ^1.2.0 # For API requests
>   cryptography: ^2.7.0 # For X25519 and AES-GCM
>   convert: ^3.1.1 # For hex encoding
>
> dev_dependencies:
>   build_runner: ^2.4.0
>   openapi_generator: ^5.0.0
>
> # OpenAPI type generation config
> openapi_generator:
>   input_spec:
>     path: openapi.json
>   generator_name: dart
>   output_directory: lib/gen
> ```

> [accordion] Platform Configuration
>
> > [tab] iOS
> >
> > Add the following to your `ios/Runner/Info.plist`:
> >
> > ```xml theme={null}
> > <key>io.flutter.embedded_views_preview</key>
> > <true/>
> > ```
>
> > [tab] Android
> >
> > Set the minimum SDK version in `android/app/build.gradle`:
> >
> > ```groovy theme={null}
> > android {
> >     defaultConfig {
> >         minSdkVersion 21
> >     }
> > }
> > ```

> [accordion] Type Generation
>
> To generate Dart types from the [OpenAPI spec](https://dev.moonpay.com/platform/guides/openapi-codegen.md):
>
> 1. Download the `openapi.json` file to your project root
> 2. Run the generator:
>
> ```bash theme={null}
> flutter pub run build_runner build
> ```
>
> This generates typed models in `lib/gen/` that you can import instead of manually defining types.

## Source Files

> [accordion] types.dart - Type definitions
>
> Types are imported from the generated OpenAPI types where possible, with SDK-specific types defined separately.
>
> ```dart types.dart theme={null}
> // Import generated types from OpenAPI spec
> import 'package:moonpay_platform/gen/model/authorize_return_type.dart'
>     as gen show AuthorizeReturnType;
> import 'package:moonpay_platform/gen/model/get_quote_response.dart'
>     as gen show GetQuoteResponse;
> import 'package:moonpay_platform/gen/model/moon_pay_payments_payment_method.dart'
>     as gen show MoonPayPaymentsPaymentMethod;
> import 'package:moonpay_platform/gen/model/transaction.dart'
>     as gen show Transaction, TransactionStatus;
> import 'package:moonpay_platform/gen/model/fees.dart' as gen show Fees;
> import 'package:moonpay_platform/gen/model/asset.dart' as gen show Asset;
>
> // Re-export generated types for convenience
> export 'package:moonpay_platform/gen/model/get_quote_response.dart'
>     show GetQuoteResponse;
> export 'package:moonpay_platform/gen/model/moon_pay_payments_payment_method.dart'
>     show MoonPayPaymentsPaymentMethod;
> export 'package:moonpay_platform/gen/model/transaction.dart'
>     show Transaction, TransactionStatus;
> export 'package:moonpay_platform/gen/model/fees.dart' show Fees;
> export 'package:moonpay_platform/gen/model/asset.dart' show Asset;
>
> // Alias GetQuoteResponse as Quote for convenience
> typedef Quote = gen.GetQuoteResponse;
>
> // Alias payment method for convenience
> typedef PaymentMethod = gen.MoonPayPaymentsPaymentMethod;
>
> // Result type for SDK operations (SDK-specific)
> sealed class Result<T, E> {}
>
> class Ok<T, E> extends Result<T, E> {
>   final T value;
>   Ok(this.value);
> }
>
> class Err<T, E> extends Result<T, E> {
>   final E error;
>   Err(this.error);
> }
>
> // Credentials derived from AuthorizeReturnType (SDK-specific)
> class Credentials {
>   final String accessToken;
>   final String clientToken;
>
>   Credentials({required this.accessToken, required this.clientToken});
>
>   factory Credentials.fromJson(Map<String, dynamic> json) {
>     return Credentials(
>       accessToken: json['accessToken'] as String,
>       clientToken: json['clientToken'] as String,
>     );
>   }
>
>   /// Create from generated AuthorizeReturnType
>   factory Credentials.fromAuthorizeReturnType(gen.AuthorizeReturnType auth) {
>     return Credentials(
>       accessToken: auth.accessToken,
>       clientToken: auth.clientToken,
>     );
>   }
> }
>
> // Connection status (SDK-specific)
> enum ConnectionStatus {
>   connectionRequired,
>   active,
>   unavailable,
>   pending,
>   failed,
> }
>
> class Connection {
>   final ConnectionStatus status;
>   final Credentials? credentials;
>   final String? reason;
>
>   Connection({required this.status, this.credentials, this.reason});
> }
>
> // Error types (SDK-specific)
> class SdkError {
>   final String kind;
>   final String message;
>
>   SdkError({required this.kind, required this.message});
> }
>
> typedef CreateClientError = SdkError;
> typedef GetConnectionError = SdkError;
> typedef ConnectError = SdkError;
> typedef GetPaymentMethodsError = SdkError;
> typedef GetQuoteError = SdkError;
> typedef SetupApplePayError = SdkError;
>
> // Quote params (SDK-specific for convenience)
> class GetQuoteParams {
>   final String source;
>   final String destination;
>   final String sourceAmount;
>   final String walletAddress;
>   final String paymentMethod;
>
>   GetQuoteParams({
>     required this.source,
>     required this.destination,
>     required this.sourceAmount,
>     required this.walletAddress,
>     required this.paymentMethod,
>   });
> }
> ```

> [accordion] api.dart - API client functions
>
> Uses the generated `PaymentMethod` and `Quote` types from the OpenAPI spec.
>
> ```dart api.dart theme={null}
> import 'dart:convert';
> import 'package:http/http.dart' as http;
> import 'types.dart';
>
> const String _moonpayApiUrl = 'https://api.moonpay.com';
>
> Future<Result<List<PaymentMethod>, GetPaymentMethodsError>> getPaymentMethods({
>   required String accessToken,
> }) async {
>   try {
>     final response = await http.get(
>       Uri.parse('$_moonpayApiUrl/platform/v1/payment-methods'),
>       headers: {
>         'Authorization': 'Bearer $accessToken',
>         'Content-Type': 'application/json',
>       },
>     );
>
>     if (response.statusCode == 200) {
>       final List<dynamic> data = jsonDecode(response.body);
>       // PaymentMethod.fromJson is generated from OpenAPI spec
>       final methods = data.map((item) => PaymentMethod.fromJson(item)).toList();
>       return Ok(methods);
>     }
>
>     final errorData = jsonDecode(response.body);
>     return Err(SdkError(
>       kind: 'genericError',
>       message: errorData['message'] ?? 'Failed to fetch payment methods',
>     ));
>   } catch (e) {
>     return Err(SdkError(
>       kind: 'genericError',
>       message: e.toString(),
>     ));
>   }
> }
>
> Future<Result<Quote, GetQuoteError>> getQuote({
>   required String accessToken,
>   required GetQuoteParams params,
> }) async {
>   try {
>     final response = await http.post(
>       Uri.parse('$_moonpayApiUrl/platform/v1/quotes/buy'),
>       headers: {
>         'Authorization': 'Bearer $accessToken',
>         'Content-Type': 'application/json',
>       },
>       body: jsonEncode({
>         'source': {
>           'asset': {'code': params.source},
>           'amount': params.sourceAmount,
>         },
>         'destination': {
>           'asset': {'code': params.destination},
>         },
>         'wallet': {'address': params.walletAddress},
>         'paymentMethod': {'type': params.paymentMethod},
>       }),
>     );
>
>     if (response.statusCode == 200) {
>       final data = jsonDecode(response.body);
>       if (data['data'] != null) {
>         // Quote.fromJson is generated from OpenAPI spec (aliased from GetQuoteResponse)
>         final quote = Quote.fromJson(data['data']);
>         if (quote.signature == null || quote.signature!.isEmpty) {
>           return Err(SdkError(
>             kind: 'validationError',
>             message: 'Quote is not executable - signature missing',
>           ));
>         }
>         return Ok(quote);
>       }
>     }
>
>     final errorData = jsonDecode(response.body);
>     return Err(SdkError(
>       kind: 'genericError',
>       message: errorData['message'] ?? 'Failed to get quote',
>     ));
>   } catch (e) {
>     return Err(SdkError(
>       kind: 'genericError',
>       message: e.toString(),
>     ));
>   }
> }
> ```

> [accordion] client.dart - Main client
>
> Manages credentials internally. The `connect` event handler captures credentials automatically.
>
> ```dart client.dart theme={null}
> import 'types.dart';
> import 'api.dart' as api;
>
> class MoonPayClient {
>   final String sessionToken;
>   Credentials? _credentials;
>
>   MoonPayClient._({required this.sessionToken});
>
>   /// Handle ConnectFrame events - captures credentials on successful connection
>   void Function(String kind, dynamic payload) createConnectEventHandler({
>     void Function(String kind, dynamic payload)? onEvent,
>   }) {
>     return (kind, payload) {
>       if (kind == 'complete' && payload is Map && payload['status'] == 'active') {
>         final creds = payload['credentials'];
>         if (creds is Credentials) {
>           _credentials = creds;
>         }
>       }
>       onEvent?.call(kind, payload);
>     };
>   }
>
>   /// Get payment methods for the connected user
>   Future<Result<List<PaymentMethod>, GetPaymentMethodsError>> getPaymentMethods() async {
>     if (_credentials == null) {
>       return Err(SdkError(
>         kind: 'validationError',
>         message: 'No active connection. Call getConnection() or connect() first.',
>       ));
>     }
>     return api.getPaymentMethods(accessToken: _credentials!.accessToken);
>   }
>
>   /// Get a quote for a buy transaction
>   Future<Result<Quote, GetQuoteError>> getQuote(GetQuoteParams params) async {
>     if (_credentials == null) {
>       return Err(SdkError(
>         kind: 'validationError',
>         message: 'No active connection. Call getConnection() or connect() first.',
>       ));
>     }
>     return api.getQuote(accessToken: _credentials!.accessToken, params: params);
>   }
>
>   /// Get Apple Pay parameters (uses internally stored clientToken)
>   Result<ApplePayParams, SetupApplePayError> getApplePayParams(String quoteSignature) {
>     if (_credentials == null) {
>       return Err(SdkError(
>         kind: 'genericError',
>         message: 'No active connection. Call getConnection() or connect() first.',
>       ));
>     }
>     return Ok(ApplePayParams(
>       clientToken: _credentials!.clientToken,
>       quoteSignature: quoteSignature,
>     ));
>   }
> }
>
> /// Apple Pay frame parameters
> class ApplePayParams {
>   final String clientToken;
>   final String quoteSignature;
>   ApplePayParams({required this.clientToken, required this.quoteSignature});
> }
>
> /// Create a new MoonPay client
> Result<MoonPayClient, CreateClientError> createClient({
>   required String sessionToken,
> }) {
>   if (sessionToken.isEmpty) {
>     return Err(SdkError(
>       kind: 'validationError',
>       message: 'Invalid sessionToken',
>     ));
>   }
>
>   return Ok(MoonPayClient._(sessionToken: sessionToken));
> }
> ```

> [accordion] crypto.dart - Encryption utilities
>
> Matches the Web implementation's cryptographic approach: X25519 key exchange, HKDF key derivation (no salt, no info), and AES-GCM decryption.
>
> ```dart crypto.dart theme={null}
> import 'dart:convert';
> import 'dart:typed_data';
> import 'package:cryptography/cryptography.dart';
> import 'package:convert/convert.dart';
> import 'types.dart';
>
> /// X25519 keypair with hex-encoded keys (matches Web KeyPair type)
> class KeyPair {
>   final SimpleKeyPair _keyPair;
>   final String privateKey; // hex-encoded (not used directly, kept for API parity)
>   final String publicKey;  // hex-encoded
>
>   KeyPair._(this._keyPair, this.privateKey, this.publicKey);
>
>   /// Generate a new X25519 keypair
>   static Future<KeyPair> generate() async {
>     final algorithm = X25519();
>     final keyPair = await algorithm.newKeyPair();
>     final publicKey = await keyPair.extractPublicKey();
>     final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
>     return KeyPair._(
>       keyPair,
>       hex.encode(privateKeyBytes),
>       hex.encode(publicKey.bytes),
>     );
>   }
>
>   /// Get the underlying SimpleKeyPair for cryptographic operations
>   SimpleKeyPair get simpleKeyPair => _keyPair;
> }
>
> /// Decrypt credentials from the connect frame
> /// Matches Web: JSON.parse(atob(encryptedData))
> Future<Credentials> decrypt(String encryptedData, KeyPair keyPair) async {
>   // Base64 decode first, then parse JSON (matches Web: atob then JSON.parse)
>   final decodedPayload = utf8.decode(base64Decode(encryptedData));
>   final parsed = jsonDecode(decodedPayload) as Map<String, dynamic>;
>
>   final ephemeralPublicKeyBytes = hex.decode(parsed['ephemeralPublicKey'] as String);
>   final ivBytes = hex.decode(parsed['iv'] as String);
>   final ciphertextBytes = hex.decode(parsed['ciphertext'] as String);
>
>   // X25519 key agreement
>   final x25519 = X25519();
>   final ephemeralPublicKey = SimplePublicKey(
>     Uint8List.fromList(ephemeralPublicKeyBytes),
>     type: KeyPairType.x25519,
>   );
>
>   final sharedSecret = await x25519.sharedSecretKey(
>     keyPair: keyPair.simpleKeyPair,
>     remotePublicKey: ephemeralPublicKey,
>   );
>
>   // HKDF key derivation - matches Web: hkdf(sha256, sharedSecret, undefined, undefined, 32)
>   // No salt (undefined), no info (undefined), 32 bytes output
>   final hkdfAlgorithm = Hkdf(hmac: Hmac.sha256(), outputLength: 32);
>   final encryptionKey = await hkdfAlgorithm.deriveKey(
>     secretKey: sharedSecret,
>     info: <int>[],  // undefined in Web
>     nonce: <int>[], // undefined in Web (salt)
>   );
>
>   // AES-GCM decryption
>   final aesGcm = AesGcm.with256bits();
>   final secretBox = SecretBox(
>     ciphertextBytes,
>     nonce: ivBytes,
>     mac: Mac.empty, // MAC is appended to ciphertext
>   );
>
>   final plainTextBytes = await aesGcm.decrypt(secretBox, secretKey: encryptionKey);
>   final credentialsJson = jsonDecode(utf8.decode(plainTextBytes)) as Map<String, dynamic>;
>   return Credentials.fromJson(credentialsJson);
> }
>
> /// Generate a unique channel ID (matches Web: `ch_${crypto.randomUUID()}`)
> String generateChannelId() {
>   // Dart doesn't have crypto.randomUUID(), use timestamp + random
>   final timestamp = DateTime.now().millisecondsSinceEpoch;
>   final random = DateTime.now().microsecondsSinceEpoch.toRadixString(36);
>   return 'ch_${timestamp}_$random';
> }
> ```

> [accordion] frames.dart - Frame handling
>
> Matches the Web SDK's frame handling pattern. Uses `MoonPayBridge` JavaScript channel with message bridge injection.
>
> ```dart frames.dart theme={null}
> import 'dart:convert';
> import 'package:flutter/material.dart';
> import 'package:webview_flutter/webview_flutter.dart';
> import 'types.dart';
> import 'crypto.dart';
>
> const String frameOrigin = 'https://platform.moonpay.com';
>
> typedef MessageDirection = String; // 'inbound' | 'outbound'
>
> class FrameMessage {
>   final int version;
>   final String channelId;
>   final String kind;
>   final Map<String, dynamic>? payload;
>
>   FrameMessage({required this.version, required this.channelId, required this.kind, this.payload});
>
>   factory FrameMessage.fromJson(Map<String, dynamic> json) {
>     return FrameMessage(
>       version: json['version'] as int,
>       channelId: (json['meta'] as Map<String, dynamic>)['channelId'] as String,
>       kind: json['kind'] as String,
>       payload: json['payload'] as Map<String, dynamic>?,
>     );
>   }
> }
>
> FrameMessage? parseMessage(dynamic data, String channelId) {
>   try {
>     final Map<String, dynamic> json = data is String ? jsonDecode(data) : data;
>     if (json['version'] != 2 || (json['meta'] as Map?)?['channelId'] != channelId) return null;
>     return FrameMessage.fromJson(json);
>   } catch (_) {
>     return null;
>   }
> }
>
> Credentials decryptCredentials(String encrypted, KeyPair keyPair) {
>   // Note: In Flutter this is async, but we wrap it for API parity
>   throw UnimplementedError('Use decryptCredentialsAsync instead');
> }
>
> Future<Credentials> decryptCredentialsAsync(String encrypted, KeyPair keyPair) async {
>   return decrypt(encrypted, keyPair);
> }
>
> String buildFrameUrl(String path, Map<String, String> params) {
>   final url = Uri.parse('$frameOrigin$path');
>   return url.replace(queryParameters: params).toString();
> }
>
> // Connect Frame Widget
> class ConnectFrame extends StatefulWidget {
>   final String sessionToken;
>   final String? theme;
>   final Function(String kind, dynamic payload)? onEvent;
>
>   const ConnectFrame({
>     super.key,
>     required this.sessionToken,
>     this.theme,
>     this.onEvent,
>   });
>
>   @override
>   State<ConnectFrame> createState() => _ConnectFrameState();
> }
>
> class _ConnectFrameState extends State<ConnectFrame> {
>   late final WebViewController _controller;
>   late final Future<KeyPair> _keyPairFuture;
>   late final String _channelId;
>
>   @override
>   void initState() {
>     super.initState();
>     _channelId = generateChannelId();
>     _keyPairFuture = KeyPair.generate();
>   }
>
>   void _initController(KeyPair keyPair) {
>     _controller = WebViewController()
>       ..setJavaScriptMode(JavaScriptMode.unrestricted)
>       ..addJavaScriptChannel(
>         'MoonPayBridge',
>         onMessageReceived: (msg) => _handleMessage(msg, keyPair),
>       )
>       ..setNavigationDelegate(
>         NavigationDelegate(onPageFinished: (_) => _injectMessageBridge()),
>       )
>       ..loadRequest(Uri.parse(buildFrameUrl('/platform/v1/connect', {
>         'sessionToken': widget.sessionToken,
>         'publicKey': keyPair.publicKey,
>         'channelId': _channelId,
>         if (widget.theme != null) 'theme': widget.theme!,
>       })));
>   }
>
>   void _injectMessageBridge() {
>     _controller.runJavaScript('''
>       (function() {
>         window.addEventListener('message', function(e) {
>           if (MoonPayBridge && MoonPayBridge.postMessage) {
>             MoonPayBridge.postMessage(typeof e.data === 'string' ? e.data : JSON.stringify(e.data));
>           }
>         });
>       })();
>     ''');
>   }
>
>   void _sendMessage(String kind, [Map<String, dynamic>? payload]) {
>     final data = {
>       'version': 2,
>       'meta': {'channelId': _channelId},
>       'kind': kind,
>       if (payload != null) 'payload': payload,
>     };
>     _controller.runJavaScript('window.postMessage(${jsonEncode(data)}, "*");');
>   }
>
>   Future<void> _handleMessage(JavaScriptMessage jsMessage, KeyPair keyPair) async {
>     final message = parseMessage(jsMessage.message, _channelId);
>     if (message == null) return;
>
>     switch (message.kind) {
>       case 'handshake':
>         _sendMessage('ack');
>         break;
>
>       case 'ready':
>         widget.onEvent?.call('ready', null);
>         break;
>
>       case 'complete':
>         final payload = message.payload!;
>         final status = payload['status'] as String;
>         if (status == 'connectionRequired') return;
>
>         if (status == 'active' && payload['credentials'] != null) {
>           try {
>             final credentials = await decrypt(payload['credentials'] as String, keyPair);
>             widget.onEvent?.call('complete', {'status': 'active', 'credentials': credentials});
>           } catch (e) {
>             widget.onEvent?.call('error', {'kind': 'genericError', 'message': 'Failed to decrypt credentials'});
>           }
>         } else if (status == 'failed') {
>           widget.onEvent?.call('complete', {'status': 'failed', 'reason': payload['reason'] ?? 'Connection failed'});
>         } else {
>           widget.onEvent?.call('complete', {'status': status});
>         }
>         break;
>
>       case 'error':
>         final payload = message.payload as Map<String, dynamic>;
>         widget.onEvent?.call('error', {'kind': payload['code'], 'message': payload['message']});
>         break;
>     }
>   }
>
>   @override
>   Widget build(BuildContext context) {
>     return FutureBuilder<KeyPair>(
>       future: _keyPairFuture,
>       builder: (context, snapshot) {
>         if (!snapshot.hasData) {
>           return const Center(child: CircularProgressIndicator());
>         }
>         _initController(snapshot.data!);
>         return WebViewWidget(controller: _controller);
>       },
>     );
>   }
> }
>
> // Apple Pay Frame Widget
> class ApplePayFrame extends StatefulWidget {
>   final String clientToken;
>   final String quoteSignature;
>   final Function(String kind, dynamic payload)? onEvent;
>
>   const ApplePayFrame({
>     super.key,
>     required this.clientToken,
>     required this.quoteSignature,
>     this.onEvent,
>   });
>
>   @override
>   State<ApplePayFrame> createState() => _ApplePayFrameState();
> }
>
> class _ApplePayFrameState extends State<ApplePayFrame> {
>   late final WebViewController _controller;
>   late final String _channelId;
>
>   @override
>   void initState() {
>     super.initState();
>     _channelId = generateChannelId();
>
>     _controller = WebViewController()
>       ..setJavaScriptMode(JavaScriptMode.unrestricted)
>       ..addJavaScriptChannel('MoonPayBridge', onMessageReceived: _handleMessage)
>       ..setNavigationDelegate(
>         NavigationDelegate(onPageFinished: (_) => _injectMessageBridge()),
>       )
>       ..loadRequest(Uri.parse(buildFrameUrl('/platform/v1/buy-button', {
>         'clientToken': widget.clientToken,
>         'signature': widget.quoteSignature,
>         'channelId': _channelId,
>       })));
>   }
>
>   void _injectMessageBridge() {
>     _controller.runJavaScript('''
>       (function() {
>         window.addEventListener('message', function(e) {
>           if (MoonPayBridge && MoonPayBridge.postMessage) {
>             MoonPayBridge.postMessage(typeof e.data === 'string' ? e.data : JSON.stringify(e.data));
>           }
>         });
>       })();
>     ''');
>   }
>
>   void _sendMessage(String kind, [Map<String, dynamic>? payload]) {
>     final data = {
>       'version': 2,
>       'meta': {'channelId': _channelId},
>       'kind': kind,
>       if (payload != null) 'payload': payload,
>     };
>     _controller.runJavaScript('window.postMessage(${jsonEncode(data)}, "*");');
>   }
>
>   void setQuote(String signature) {
>     _sendMessage('setQuote', {'quote': {'signature': signature}});
>   }
>
>   void _handleMessage(JavaScriptMessage jsMessage) {
>     final message = parseMessage(jsMessage.message, _channelId);
>     if (message == null) return;
>
>     switch (message.kind) {
>       case 'handshake':
>         _sendMessage('ack');
>         break;
>
>       case 'ready':
>         widget.onEvent?.call('ready', null);
>         break;
>
>       case 'complete':
>         final payload = message.payload as Map<String, dynamic>;
>         widget.onEvent?.call('complete', {'transaction': payload['transaction']});
>         break;
>
>       case 'error':
>         final payload = message.payload as Map<String, dynamic>;
>         final errorKind = payload['kind'] ?? payload['code'];
>         if (errorKind == 'quoteExpired') {
>           widget.onEvent?.call('quoteExpired', {'setQuote': setQuote});
>         } else if (errorKind == 'applePayUnavailable') {
>           widget.onEvent?.call('unsupported', null);
>         } else {
>           widget.onEvent?.call('error', payload);
>         }
>         break;
>     }
>   }
>
>   @override
>   Widget build(BuildContext context) {
>     return WebViewWidget(controller: _controller);
>   }
> }
> ```
