finmont_checkout 0.1.0
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),
),
);
},
),
],
),
],
],
),
);
}
}