esaalat 0.4.0
esaalat: ^0.4.0 copied to clipboard
Hosted checkout for Flutter — a ready-made pay button, a full-screen WebView, and verified payment results.
example/lib/main.dart
import 'package:esaalat/esaalat.dart';
import 'package:flutter/material.dart';
/// Pass your key at build time:
///
/// ```bash
/// flutter run --dart-define=ESAALAT_PUBLISHABLE_KEY=pk_...
/// ```
const String _publishableKey = String.fromEnvironment(
'ESAALAT_PUBLISHABLE_KEY',
defaultValue: 'pk_test_replace_me',
);
const String _apiBaseUrl = String.fromEnvironment(
'ESAALAT_API_BASE_URL',
defaultValue: esaalatDefaultApiBaseUrl,
);
const String _portalBaseUrl = String.fromEnvironment(
'ESAALAT_PORTAL_BASE_URL',
defaultValue: esaalatDefaultPortalBaseUrl,
);
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Esaalat checkout',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF0B0B0B),
useMaterial3: true,
),
home: const CheckoutDemoPage(),
);
}
}
class CheckoutDemoPage extends StatefulWidget {
const CheckoutDemoPage({super.key});
@override
State<CheckoutDemoPage> createState() => _CheckoutDemoPageState();
}
class _CheckoutDemoPageState extends State<CheckoutDemoPage> {
final TextEditingController _amount = TextEditingController(text: '1000');
late final String _orderId =
'flutter-${DateTime.now().millisecondsSinceEpoch}';
CheckoutResult? _result;
String? _error;
@override
void dispose() {
_amount.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Esaalat checkout')),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
Text(
'One button: session, checkout route and result routing.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 20),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Order amount', style: theme.textTheme.labelLarge),
const SizedBox(height: 8),
TextField(
controller: _amount,
// Rebuild so the button reads the amount the buyer typed.
onChanged: (_) => setState(() {}),
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(),
suffixText: 'YER',
),
),
const SizedBox(height: 16),
Align(
alignment: AlignmentDirectional.centerStart,
child: EsaalatCheckoutButton(
publishableKey: _publishableKey,
amount: _amount.text.trim(),
currency: 'YER',
externalOrderId: _orderId,
apiBaseUrl: _apiBaseUrl,
portalBaseUrl: _portalBaseUrl,
onVerified: (result) => setState(() {
_result = result;
_error = null;
}),
onCancelled: (result) => setState(() => _result = result),
onWrongAmount: (result) =>
setState(() => _result = result),
onExpired: (result) => setState(() => _result = result),
onFailed: (result) => setState(() => _result = result),
onError: (error) => setState(
() => _error = '${error.message} (${error.code.name})',
),
),
),
],
),
),
),
if (_error != null) ...[
const SizedBox(height: 20),
Card(
color: theme.colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
_error!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onErrorContainer,
),
),
),
),
],
if (_result != null) ...[
const SizedBox(height: 20),
_ResultCard(result: _result!),
],
],
),
);
}
}
class _ResultCard extends StatelessWidget {
const _ResultCard({required this.result});
final CheckoutResult result;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final verified = result.isVerified;
return Card(
color: verified
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerHighest,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
result.status.wire,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
if (result.reason != null)
Text(
'reason: ${result.reason!.wire}',
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 12),
_Row(label: 'Amount', value: '${result.amount} ${result.currency}'),
if (result.paidAmount != null)
_Row(
label: 'Paid',
value: '${result.paidAmount} ${result.currency}'),
if (result.intentCode != null)
_Row(label: 'Intent', value: result.intentCode!),
if (result.externalOrderId.isNotEmpty)
_Row(label: 'Order', value: result.externalOrderId),
_Row(label: 'Session', value: result.sessionToken),
const SizedBox(height: 12),
Text(
'Confirm the order server-side from the signed webhook before '
'fulfilling it.',
style: theme.textTheme.bodySmall,
),
],
),
),
);
}
}
class _Row extends StatelessWidget {
const _Row({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 72,
child: Text(label, style: theme.textTheme.bodySmall),
),
Expanded(
child: SelectableText(
value,
style: theme.textTheme.bodyMedium,
),
),
],
),
);
}
}