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.
Types are imported from the generated OpenAPI types where possible, with SDK-specific types defined separately.
types.dart
// Import generated types from OpenAPI specimport '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 convenienceexport '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 conveniencetypedef Quote = gen.GetQuoteResponse;// Alias payment method for conveniencetypedef 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, });}
api.dart - API client functions
Uses the generated PaymentMethod and Quote types from the OpenAPI spec.
api.dart
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(), )); }}
client.dart - Main client
Manages credentials internally. The connect event handler captures credentials automatically.
client.dart
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 parametersclass ApplePayParams { final String clientToken; final String quoteSignature; ApplePayParams({required this.clientToken, required this.quoteSignature});}/// Create a new MoonPay clientResult<MoonPayClient, CreateClientError> createClient({ required String sessionToken,}) { if (sessionToken.isEmpty) { return Err(SdkError( kind: 'validationError', message: 'Invalid sessionToken', )); } return Ok(MoonPayClient._(sessionToken: sessionToken));}
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.
crypto.dart
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';}
frames.dart - Frame handling
Matches the Web SDK’s frame handling pattern. Uses MoonPayBridge JavaScript channel with message bridge injection.
frames.dart
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 Widgetclass 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 Widgetclass 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/apple-pay', { '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); }}