title: "Flutter" description: "Embed secure payment checkout experiences directly into your Flutter mobile application"
Overview
The Moment Flutter SDK enables seamless payment integration into any Flutter application. The SDK provides a simple, unified API to handle the entire payment flow through a native bottom sheet experience.
Built with pure Dart (minimal dependencies), the SDK works with both iOS and Android platforms.
Key Features
- Minimal Dependencies – Only requires
webview_flutter - Native Experience – Bottom sheet presentation feels native on both platforms
- Flexible Callbacks – Handle success, failure, and cancellation events
- Secure – Sensitive payment data handled entirely in Moment's secure WebView
- Easy Extraction – Single file can be published as a standalone Flutter package
- PCI Compliant – No card data touches your application
How It Works
sequenceDiagram
participant Customer
participant Flutter App
participant Your Backend
participant Moment API
participant Moment Checkout
Customer->>Flutter App: Taps "Pay Now"
Flutter App->>Your Backend: Request payment session
Your Backend->>Moment API: POST /collect/payment_sessions
Moment API-->>Your Backend: Returns client_token (JWT)
Your Backend-->>Flutter App: Returns client_token
Flutter App->>Moment Checkout: MomentSDK.open(clientToken)
Moment Checkout-->>Customer: Displays checkout UI (Bottom Sheet)
Customer->>Moment Checkout: Completes payment
Moment Checkout-->>Flutter App: Returns MomentResult
Moment Checkout-->>Your Backend: Sends webhooks
Flutter App->>Customer: Shows confirmation
Flow Summary
- Customer initiates payment in your app
- Backend creates a payment session and receives a
client_token - App opens checkout using
MomentSDK.open(clientToken) - Customer completes payment in Moment's secure checkout
- SDK returns a
MomentResultwith status and data - Webhooks are delivered to your backend
- Your app shows confirmation or handles errors
Installation
1. Add Dependencies
Add the following to your pubspec.yaml:
dependencies:
webview_flutter: ^4.4.2
webview_flutter_wkwebview: ^3.9.2 # iOS
webview_flutter_android: ^3.12.0 # Android
2. Add the SDK File
Copy moment_sdk.dart into your project:
lib/
├── moment_sdk/
│ └── moment_sdk.dart
└── main.dart
3. Import the SDK
import 'package:your_app/moment_sdk/moment_sdk.dart';
Platform Configuration
iOS
Add to ios/Runner/Info.plist:
<key>io.flutter.embedded_views_preview</key>
<true/>
Android
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
Ensure minimum SDK version in android/app/build.gradle:
android {
defaultConfig {
minSdkVersion 20
}
}
Quick Start
1. Create a Session (Backend)
Your backend should create a payment session and return the client_token:
// Node.js example
const response = await fetch(
'https://api.momentpay.net/collect/payment_sessions',
{
method: 'POST',
headers: {
Authorization: 'Bearer sk_test_your_secret_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 10000, // Amount in cents (e.g., R100.00)
currency: 'ZAR',
type: 'one_time',
options: {
checkout_options: {
presentation_mode: {
mode: 'embedded',
},
},
},
}),
}
);
const { client_token } = await response.json();
// Return client_token to your Flutter app
2. Fetch Token from Backend (Flutter)
class PaymentService {
static const String _baseUrl = 'https://your-backend.com';
static Future<String> createPaymentSession({
required int amount,
required String currency,
}) async {
final response = await http.post(
Uri.parse('$_baseUrl/create-session'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'amount': amount,
'currency': currency,
}),
);
if (response.statusCode != 200) {
throw Exception('Failed to create session');
}
final data = jsonDecode(response.body);
return data['client_token'];
}
}
3. Open Checkout
Future<void> handlePayment() async {
try {
// Get client_token from your backend
final clientToken = await PaymentService.createPaymentSession(
amount: 10000, // R100.00 in cents
currency: 'ZAR',
);
// Open checkout
final result = await MomentSDK.open(
context: context,
clientToken: clientToken,
onComplete: (data) {
print('Payment successful: $data');
},
onError: (error) {
print('Payment error: $error');
},
onCancel: () {
print('Payment cancelled');
},
);
// Handle result
if (result.isSuccess) {
// Navigate to success screen
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => SuccessScreen()),
);
} else if (result.isError) {
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Payment failed: ${result.errorMessage}')),
);
}
} catch (e) {
print('Error: $e');
}
}
API Reference
MomentSDK.open()
Opens the Moment payment checkout.
static Future<MomentResult> open({
required BuildContext context,
required String clientToken,
Function(Map<String, dynamic>)? onComplete,
Function(String)? onError,
Function()? onCancel,
})
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
context |
BuildContext |
Yes | Flutter build context for showing bottom sheet |
clientToken |
String |
Yes | JWT token from your backend's create-session endpoint |
onComplete |
Function(Map<String, dynamic>)? |
No | Callback when payment is successful |
onError |
Function(String)? |
No | Callback when an error occurs |
onCancel |
Function()? |
No | Callback when user cancels the payment |
Returns
Returns a Future<MomentResult> containing the payment outcome.
MomentResult
Result object returned from MomentSDK.open().
class MomentResult {
final MomentStatus status;
final Map<String, dynamic>? data;
final String? errorMessage;
bool get isSuccess;
bool get isCancelled;
bool get isError;
}
Properties
| Property | Type | Description |
|---|---|---|
status |
MomentStatus |
The payment status (success, cancelled, error) |
data |
Map<String, dynamic>? |
Payment data returned on success |
errorMessage |
String? |
Error message if payment failed |
isSuccess |
bool |
Convenience getter for success status |
isCancelled |
bool |
Convenience getter for cancelled status |
isError |
bool |
Convenience getter for error status |
MomentStatus
Enum representing the payment outcome.
enum MomentStatus {
success, // Payment completed successfully
cancelled, // User closed or cancelled the checkout
error // An error occurred during payment
}
MomentException
Custom exception thrown for SDK errors.
class MomentException implements Exception {
final String message;
}
Complete Example
import 'package:flutter/material.dart';
import 'moment_sdk/moment_sdk.dart';
class CheckoutScreen extends StatefulWidget {
final double amount;
const CheckoutScreen({required this.amount});
@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
bool _isLoading = false;
Future<void> _handlePayment() async {
setState(() => _isLoading = true);
try {
final clientToken = await PaymentService.createPaymentSession(
amount: (widget.amount * 100).round(),
currency: 'ZAR',
);
if (!mounted) return;
final result = await MomentSDK.open(
context: context,
clientToken: clientToken,
onComplete: (data) => debugPrint('Complete: $data'),
onError: (error) => debugPrint('Error: $error'),
onCancel: () => debugPrint('Cancelled'),
);
if (!mounted) return;
switch (result.status) {
case MomentStatus.success:
_showSuccess();
break;
case MomentStatus.cancelled:
_showMessage('Payment cancelled');
break;
case MomentStatus.error:
_showError(result.errorMessage ?? 'Unknown error');
break;
}
} catch (e) {
_showError(e.toString());
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
void _showSuccess() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Payment successful!'),
backgroundColor: Colors.green,
),
);
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $message'),
backgroundColor: Colors.red,
),
);
}
void _showMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Checkout')),
body: Center(
child: ElevatedButton(
onPressed: _isLoading ? null : _handlePayment,
child: _isLoading
? const CircularProgressIndicator()
: Text('Pay R${widget.amount.toStringAsFixed(2)}'),
),
),
);
}
}
Handling Edge Cases
Network Errors
try {
final result = await MomentSDK.open(
context: context,
clientToken: clientToken,
);
} on MomentException catch (e) {
// Handle SDK-specific errors
print('Moment error: ${e.message}');
} catch (e) {
// Handle other errors (network, etc.)
print('Error: $e');
}
Token Expiry
Client tokens have a limited lifespan. Always request a fresh token before opening checkout:
// Good - fresh token each time
Future<void> pay() async {
final clientToken = await PaymentService.createPaymentSession(...);
await MomentSDK.open(context: context, clientToken: clientToken);
}
// Bad - stale token may be expired
String? _cachedToken;
Future<void> pay() async {
_cachedToken ??= await PaymentService.createPaymentSession(...);
await MomentSDK.open(context: context, clientToken: _cachedToken!);
}
Preventing Double Submissions
bool _isProcessing = false;
Future<void> _handlePayment() async {
if (_isProcessing) return; // Prevent double tap
setState(() => _isProcessing = true);
try {
// ... payment logic
} finally {
if (mounted) {
setState(() => _isProcessing = false);
}
}
}
Security Best Practices
- Never expose your secret key (
sk_...) in your Flutter app - Always create sessions server-side via your backend
- Validate webhooks to confirm payment state before fulfilling orders
- Use HTTPS for all backend communication
- Handle token expiry by requesting a new session when needed
- Check
mountedbefore updating state after async operations
Troubleshooting
WebView Not Loading
Ensure you've added the required platform configurations (see Platform Configuration).
"Invalid client token format" Error
The client token must be a valid JWT with 3 parts separated by dots. Verify your backend is returning the correct token.
Bottom Sheet Closes Immediately
Check that isDismissible: false and enableDrag: false are set in the bottom sheet configuration.
Payment Events Not Firing
Ensure the JavaScript channel name matches what the checkout page expects (MomentSDK).
Next Steps
- View Sample Applications for end-to-end examples
- Create your first Payment Session
- Configure Webhooks
- Test your integration in Sandbox mode
Libraries
- moment_sdk
- Moment payment checkout SDK for Flutter.