finmont_checkout 0.1.0 copy "finmont_checkout: ^0.1.0" to clipboard
finmont_checkout: ^0.1.0 copied to clipboard

Flutter SDK for FinMont payment orchestration — headless core and prebuilt UI supporting cards, wallets, bank redirects, APMs, 3DS, and split payments.

example/lib/main.dart

import 'package:finmont_checkout/finmont_checkout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'
    show rootBundle, Clipboard, ClipboardData;
import 'flight_booking_page.dart';
import 'flight_confirmation_page.dart';

const String testApiKey = String.fromEnvironment(
  'FINMONT_API_KEY',
  defaultValue: 'YOUR_FINMONT_API_KEY_HERE',
);

void main() {
  runApp(const FinmontCheckoutMockApp());
}

/// Standalone interactive mock application with dummy flight booking & FinMont checkout.
class FinmontCheckoutMockApp extends StatefulWidget {
  const FinmontCheckoutMockApp({super.key});

  @override
  State<FinmontCheckoutMockApp> createState() => _FinmontCheckoutMockAppState();
}

class _FinmontCheckoutMockAppState extends State<FinmontCheckoutMockApp> {
  FinmontCheckout? _checkout;
  bool _isLoading = true;

  // Active flight booking session
  PaymentIntent? _activeIntent;
  String? _activeCacheReference;
  FlightBookingDetails? _activeFlight;

  @override
  void initState() {
    super.initState();
    _initCheckout();
  }

  Future<void> _initCheckout() async {
    FinmontConfig config;
    try {
      final envString = await rootBundle.loadString('.env');
      config = FinmontConfig.fromMap(FinmontConfig.parseDotEnv(envString));
      if (config.apiKey.isEmpty ||
          config.apiKey == 'YOUR_FINMONT_API_KEY_HERE' ||
          config.apiKey == 'your_api_key_here') {
        const envKey = String.fromEnvironment('FINMONT_API_KEY');
        if (envKey.isNotEmpty) {
          config = FinmontConfig(
            apiKey: envKey,
            environment: config.environment,
            paymentBaseUrl: config.paymentBaseUrl,
            tokenizationBaseUrl: config.tokenizationBaseUrl,
            checkoutBaseUrl: config.checkoutBaseUrl,
          );
        }
      }
    } catch (_) {
      config = const FinmontConfig(
        apiKey: testApiKey,
        environment: FinmontEnvironment.dev,
        sendIdempotencyHeader: false,
      );
    }
    if (mounted) {
      setState(() {
        _checkout = FinmontCheckout(
          apiKey: config.apiKey,
          environment: config.environment,
          config: config,
        );
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FinMont Flight Booking & Checkout',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        fontFamily: 'sans-serif',
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF003366),
          primary: const Color(0xFF003366),
          surface: Colors.white,
        ),
        scaffoldBackgroundColor: const Color(0xFFD7DFDD),
      ),
      home: _isLoading
          ? const Scaffold(
              body: Center(child: CircularProgressIndicator()),
            )
          : (_activeIntent == null
              ? FlightBookingPage(
                  checkout: _checkout!,
                  onProceedToCheckout: (intent, cacheRef, flight) {
                    setState(() {
                      _activeIntent = intent;
                      _activeCacheReference = cacheRef;
                      _activeFlight = flight;
                    });
                  },
                )
              : WebCheckoutScreen(
                  checkout: _checkout!,
                  initialIntent: _activeIntent!,
                  cacheReference: _activeCacheReference!,
                  flightDetails: _activeFlight,
                  onBackToBooking: () {
                    setState(() {
                      _activeIntent = null;
                      _activeCacheReference = null;
                      _activeFlight = null;
                    });
                  },
                )),
    );
  }
}

/// The main checkout screen — now powered by [FinmontPaymentSheet] with
/// init-driven APM loading via [FinmontCheckoutConfig].
///
/// Mirrors the pattern of [PaymentSheetContentView] from the Swift SDK reference:
/// - APMs shown are driven entirely by [FinmontCheckoutConfig.enabledMethods]
/// - No dependency on `getCheckoutSettings()` to determine which methods render
/// - Card payment + 3DS + DDC flow is preserved exactly as before
class WebCheckoutScreen extends StatefulWidget {
  final FinmontCheckout? checkout;
  final PaymentIntent? initialIntent;
  final String? cacheReference;
  final FlightBookingDetails? flightDetails;
  final VoidCallback? onBackToBooking;

  const WebCheckoutScreen({
    super.key,
    this.checkout,
    this.initialIntent,
    this.cacheReference,
    this.flightDetails,
    this.onBackToBooking,
  });

  @override
  State<WebCheckoutScreen> createState() => _WebCheckoutScreenState();
}

class _WebCheckoutScreenState extends State<WebCheckoutScreen> {
  late FinmontCheckout _checkout;
  FinmontConfig? _config;

  // Session state
  String? _cacheReference;
  String? _actionToken;
  String? _checkoutUrl;
  bool _isLoadingSession = false;

  // Result state
  CheckoutResult? _checkoutResult;

  // Order details (initialized from flight booking or passed intent)
  String _bookingRef = '004369_70706_3102554';
  String _orderRef = '266430c6-a028-434b-9d9b-bf70342caa84';
  Amount _fareAmount = const Amount(currency: 'USD', value: 10000);

  // The active payment intent (set after session creation)
  PaymentIntent? _activeIntent;

  @override
  void initState() {
    super.initState();
    if (widget.initialIntent != null) {
      _bookingRef =
          widget.initialIntent!.merchantClientReference ?? _bookingRef;
      _orderRef = widget.initialIntent!.order.reference;
      _fareAmount = widget.initialIntent!.amount;
      _activeIntent = widget.initialIntent;
    }
    if (widget.cacheReference != null) {
      _cacheReference = widget.cacheReference;
    }
    _initSdk();
  }

  Future<void> _initSdk() async {
    if (widget.checkout != null) {
      _checkout = widget.checkout!;
      _config = _checkout.config;
    } else {
      FinmontConfig config;
      try {
        final envString = await rootBundle.loadString('.env');
        config = FinmontConfig.fromMap(FinmontConfig.parseDotEnv(envString));
      } catch (_) {
        config = const FinmontConfig(
          apiKey: testApiKey,
          environment: FinmontEnvironment.dev,
        );
      }
      _config = config;
      _checkout = FinmontCheckout(apiKey: config.apiKey, config: config);
    }

    if (_cacheReference != null && _cacheReference!.isNotEmpty) {
      await _elevateExistingSession(_cacheReference!);
    } else if (_activeIntent == null) {
      await _initPaymentSession();
    }

    if (mounted) setState(() {});
  }

  Future<void> _elevateExistingSession(String cacheRef) async {
    setState(() => _isLoadingSession = true);
    try {
      String? actionToken;
      try {
        actionToken = await _checkout.getActionToken(cacheRef);
      } catch (_) {}

      PaymentIntent? cachedIntent;
      try {
        cachedIntent = await _checkout.getCachedSession(cacheRef);
      } catch (_) {}

      if (mounted) {
        setState(() {
          _cacheReference = cacheRef;
          _actionToken = actionToken;
          _checkoutUrl =
              '${_config?.effectiveCheckoutUrl ?? "https://pop-checkout-dev.finmont.com"}/pay?reference=$cacheRef';
          if (cachedIntent != null) {
            if (cachedIntent.order.reference.isNotEmpty) {
              _orderRef = cachedIntent.order.reference;
            }
            if (cachedIntent.merchantClientReference != null) {
              _bookingRef = cachedIntent.merchantClientReference!;
            }
            _fareAmount = cachedIntent.amount;
            _activeIntent ??= cachedIntent;
          }
        });
      }
    } catch (e) {
      debugPrint('Failed to elevate existing session: $e');
    } finally {
      if (mounted) setState(() => _isLoadingSession = false);
    }
  }

  Future<void> _initPaymentSession() async {
    setState(() => _isLoadingSession = true);
    try {
      final initialIntent = PaymentIntent(
        merchantClientReference: _bookingRef,
        amount: _fareAmount,
        threeDsIndicator: 'THREE_DS',
        integrationType: 'sdk',
        captureStrategy: CaptureStrategy.delay,
        sessionExpiry: 10,
        groupSessionExpiry: 30,
        returnUrl: 'https://example.com',
        order: Order.create(orderDescription: '9735935985988388043042'),
        browserInfo: const BrowserInfo(
          jsEnabled: true,
          ip: '192.168.12.1',
          javaEnabled: true,
          language: 'EN',
          userAgent:
              'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
          acceptHeader: 'text/html',
          timezoneOffset: -330,
          name: 'Firefox',
          version: '15',
          colorDepth: 32,
          screenHeight: 1080,
          screenWidth: 1920,
          latitude: '12.2344',
          longitude: '12.2344',
        ),
        billingAddress: const Address(
          name: 'Jane Smith',
          country: 'US',
          stateProvince: 'NY',
          city: 'New York',
          postalCode: '10001',
          street: '123 Test Street',
          houseNumberName: 'Suite 100',
        ),
      );

      final sessionResp = await _checkout.createPaymentSession(
        initialIntent,
        redirect: false,
      );

      final cacheRef = sessionResp.cacheReference;
      if (cacheRef != null && cacheRef.isNotEmpty) {
        String? actionToken;
        try {
          actionToken = await _checkout.getActionToken(cacheRef);
        } catch (_) {}

        PaymentIntent? cachedIntent;
        try {
          cachedIntent = await _checkout.getCachedSession(cacheRef);
        } catch (_) {}

        if (mounted) {
          setState(() {
            _cacheReference = cacheRef;
            _actionToken = actionToken;
            _checkoutUrl =
                '${_config?.effectiveCheckoutUrl ?? "https://pop-checkout-dev.finmont.com"}/pay?reference=$cacheRef';
            if (cachedIntent != null) {
              if (cachedIntent.order.reference.isNotEmpty) {
                _orderRef = cachedIntent.order.reference;
              }
              if (cachedIntent.merchantClientReference != null) {
                _bookingRef = cachedIntent.merchantClientReference!;
              }
              _fareAmount = cachedIntent.amount;
              _activeIntent = cachedIntent;
            }
          });
        }
      }
    } catch (e) {
      debugPrint('Failed to create payment session: $e');
    } finally {
      if (mounted) setState(() => _isLoadingSession = false);
    }
  }

  /// Builds the [FinmontCheckoutConfig] from initialization parameters.
  /// APMs and widgets are loaded strictly from this config — NOT from getCheckoutSettings().
  FinmontCheckoutConfig _buildCheckoutConfig() {
    return FinmontCheckoutConfig(
      merchantDisplayName: 'FinMont Airways',
      enabledMethods: const [
        PaymentMethodType.card,
        PaymentMethodType.bank,
        PaymentMethodType.wallet,
        PaymentMethodType.voucher,
        PaymentMethodType.binance,
        PaymentMethodType.razorpay,
        PaymentMethodType.iatapay,
        PaymentMethodType.rma,
        PaymentMethodType.crypto,
      ],
      quickPayMethods: const [],
      theme: const CheckoutTheme(
        primaryColor: Color(0xFF1E5288),
        accentColor: Color(0xFF2E6AAB),
        backgroundColor: Color(0xFFF8F9FA),
        surfaceColor: Colors.white,
      ),
      widgetStyle: const FinmontWidgetStyle(
        padding: 16.0,
        cornerRadius: 8.0,
        fontSize: 14.0,
        headerFontSize: 16.0,
        fieldHeight: 46.0,
        accentColor: Color(0xFF1E5288),
        borderColor: Color(0xFFD1D5DB),
        textColor: Color(0xFF1F2937),
        secondaryTextColor: Color(0xFF6B7280),
      ),
      buttonStyle: const FinmontButtonStyle(
        backgroundColor: Color(0xFF1E5288),
        textColor: Colors.white,
        cornerRadius: 8.0,
        height: 48.0,
        fontSize: 15.0,
        fontWeight: FontWeight.w700,
      ),
      returnUrl: 'http://localhost:3005/sdkresult',
    );
  }

  PaymentIntent _buildPaymentIntent() {
    return PaymentIntent(
      merchantClientReference: _bookingRef,
      orderPayloadReference: _cacheReference,
      amount: _fareAmount,
      threeDsIndicator: 'THREE_DS',
      integrationType: 'sdk',
      captureStrategy: CaptureStrategy.delay,
      sessionExpiry: 10,
      groupSessionExpiry: 30,
      returnUrl: 'https://example.com',
      order: Order(
        reference: _orderRef,
        orderDescription: '9735935985988388043042',
      ),
      browserInfo: const BrowserInfo(
        jsEnabled: true,
        ip: '192.168.12.1',
        javaEnabled: true,
        language: 'EN',
        userAgent:
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
        acceptHeader: 'text/html',
        timezoneOffset: -330,
        name: 'Firefox',
        version: '15',
        colorDepth: 32,
        screenHeight: 1080,
        screenWidth: 1920,
        latitude: '12.2344',
        longitude: '12.2344',
      ),
      billingAddress: const Address(
        name: 'Jane Smith',
        country: 'US',
        stateProvince: 'NY',
        city: 'New York',
        postalCode: '10001',
        street: '123 Test Street',
        houseNumberName: 'Suite 100',
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    if (_checkoutResult != null) {
      final flight = widget.flightDetails;
      return Scaffold(
        body: FlightConfirmationPage(
          result: _checkoutResult!,
          bookingReference: _bookingRef,
          flightDetails: flight,
          onBackToBooking: () {
            widget.onBackToBooking?.call();
          },
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFF1E5288),
        foregroundColor: Colors.white,
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: widget.onBackToBooking,
        ),
        title: const Text(
          'Checkout',
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 18,
            color: Colors.white,
          ),
        ),
        actions: [
          Container(
            margin: const EdgeInsets.only(right: 12),
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            decoration: BoxDecoration(
              color: Colors.white.withValues(alpha: 0.15),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Text(
              _config?.environment.name.toUpperCase() ?? 'DEV',
              style: const TextStyle(
                fontSize: 11,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
          ),
        ],
      ),
      body: _isLoadingSession
          ? const Center(child: CircularProgressIndicator())
          : SingleChildScrollView(
              child: Column(
                children: [
                  // Session info card
                  _buildSessionInfoCard(),

                  // Order summary card
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    child: _buildOrderSummarySection(),
                  ),
                  const SizedBox(height: 16),

                  // FinmontPaymentSheet — init-driven, no settings API dependency
                  FinmontPaymentSheet(
                    checkout: _checkout,
                    intent: _buildPaymentIntent(),
                    config: _buildCheckoutConfig(),
                    bannerTitle: 'FinMont Airways',
                    bannerSubtitle: 'Secure Payment',
                    onSuccess: (result) {
                      setState(() => _checkoutResult = result);
                    },
                    onError: (err) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text('Payment Error: $err'),
                          backgroundColor: const Color(0xFFDC3545),
                          duration: const Duration(seconds: 5),
                        ),
                      );
                    },
                  ),
                ],
              ),
            ),
    );
  }

  Widget _buildOrderSummarySection() {
    final flight = widget.flightDetails;
    return OrderSummaryCard(
      bookingReference: _bookingRef,
      orderReference: _orderRef,
      ticketDetails: flight != null
          ? '${flight.airline} · ${flight.flightNumber} (${flight.cabinClass})'
          : 'Ticket issued 0',
      bookingFare: flight != null
          ? Amount(
              currency: _fareAmount.currency,
              value: (flight.baseFare * 100).round(),
            )
          : _fareAmount,
      taxes: flight != null
          ? Amount(
              currency: _fareAmount.currency,
              value: (flight.taxes * 100).round(),
            )
          : Amount(currency: _fareAmount.currency, value: 0),
      totalFare: _fareAmount,
      statusLabel: 'Awaiting payment',
    );
  }

  Widget _buildSessionInfoCard() {
    final env = _config?.environment.name.toUpperCase() ?? 'DEV';
    final isDev = env == 'DEV';

    return Container(
      margin: const EdgeInsets.all(16),
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: Colors.blueGrey.shade100),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.03),
            blurRadius: 6,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
                decoration: BoxDecoration(
                  color:
                      isDev ? const Color(0xFFE0F2FE) : const Color(0xFFDCFCE7),
                  borderRadius: BorderRadius.circular(6),
                  border: Border.all(
                    color: isDev
                        ? const Color(0xFF0284C7)
                        : const Color(0xFF16A34A),
                  ),
                ),
                child: Text(
                  'ENV: $env',
                  style: TextStyle(
                    fontSize: 11,
                    fontWeight: FontWeight.bold,
                    color: isDev
                        ? const Color(0xFF0369A1)
                        : const Color(0xFF15803D),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              const Text(
                'Payment Session Active',
                style: TextStyle(
                  fontSize: 13,
                  fontWeight: FontWeight.w600,
                  color: Color(0xFF0F172A),
                ),
              ),
              const Spacer(),
              if (_isLoadingSession)
                const SizedBox(
                  width: 14,
                  height: 14,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
              else
                IconButton(
                  icon: const Icon(Icons.refresh, size: 18),
                  tooltip: 'Create new payment session',
                  padding: EdgeInsets.zero,
                  constraints: const BoxConstraints(),
                  onPressed: _initPaymentSession,
                ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                'cacheReference: ',
                style: TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.bold,
                  color: Colors.black87,
                ),
              ),
              Expanded(
                child: SelectableText(
                  _cacheReference ?? 'None (Creating...)',
                  style: const TextStyle(
                    fontSize: 12,
                    fontFamily: 'monospace',
                    color: Color(0xFF1E5288),
                    fontWeight: FontWeight.w600,
                  ),
                ),
              ),
            ],
          ),
          if (_actionToken != null) ...[
            const SizedBox(height: 4),
            Row(
              children: [
                const Text(
                  'actionToken: ',
                  style: TextStyle(
                    fontSize: 11,
                    fontWeight: FontWeight.bold,
                    color: Colors.black54,
                  ),
                ),
                Expanded(
                  child: Text(
                    '${_actionToken!.substring(0, _actionToken!.length > 30 ? 30 : _actionToken!.length)}...',
                    style: const TextStyle(
                      fontSize: 11,
                      fontFamily: 'monospace',
                      color: Colors.black54,
                    ),
                  ),
                ),
              ],
            ),
          ],
          if (_checkoutUrl != null) ...[
            const SizedBox(height: 4),
            Row(
              children: [
                const Text(
                  'Web URL: ',
                  style: TextStyle(
                    fontSize: 11,
                    fontWeight: FontWeight.bold,
                    color: Colors.black54,
                  ),
                ),
                Expanded(
                  child: SelectableText(
                    _checkoutUrl!,
                    style: const TextStyle(
                      fontSize: 11,
                      fontFamily: 'monospace',
                      color: Color(0xFF0284C7),
                      decoration: TextDecoration.underline,
                    ),
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.copy, size: 14),
                  tooltip: 'Copy Checkout URL',
                  padding: EdgeInsets.zero,
                  constraints: const BoxConstraints(),
                  onPressed: () {
                    Clipboard.setData(ClipboardData(text: _checkoutUrl!));
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('Checkout URL copied to clipboard'),
                        duration: Duration(seconds: 2),
                      ),
                    );
                  },
                ),
              ],
            ),
          ],
        ],
      ),
    );
  }
}
0
likes
160
points
334
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for FinMont payment orchestration — headless core and prebuilt UI supporting cards, wallets, bank redirects, APMs, 3DS, and split payments.

Homepage
Repository (GitHub)
View/report issues

Topics

#payments #checkout #payment-gateway #three-ds #fintech

License

MIT (license)

Dependencies

dio, equatable, flutter, intl, pay, uuid, web, webview_flutter, webview_flutter_web

More

Packages that depend on finmont_checkout