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

  1. Customer initiates payment in your app
  2. Backend creates a payment session and receives a client_token
  3. App opens checkout using MomentSDK.open(clientToken)
  4. Customer completes payment in Moment's secure checkout
  5. SDK returns a MomentResult with status and data
  6. Webhooks are delivered to your backend
  7. 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

  1. Never expose your secret key (sk_...) in your Flutter app
  2. Always create sessions server-side via your backend
  3. Validate webhooks to confirm payment state before fulfilling orders
  4. Use HTTPS for all backend communication
  5. Handle token expiry by requesting a new session when needed
  6. Check mounted before 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

Libraries

moment_sdk
Moment payment checkout SDK for Flutter.