easymerchantsdk 1.0.2
easymerchantsdk: ^1.0.2 copied to clipboard
Flutter SDK for EasyMerchant's native mobile checkout integration.
Easy Merchant Sdk Implementation. #
To implement the sdk in your flutter project, you have to add the below path in your pubspec.yaml file inside dependencies section:
dependencies: easymerchantsdk: ^1.0.2
Android Side #
Changes in android side. #
Now open your android folder and there is a build.gradle file. Open it and add the below code in it.
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven {
url = uri(properties.getProperty('GITHUB_URL'))
credentials {
username = properties.getProperty('GITHUB_USERNAME')
password = properties.getProperty('GITHUB_PASSWORD')
}
}
}
}
Changes in IOS side. #
Requirements #
- Ruby 3.2.8
How to call the sdk. #
Now, to call the sdk, please check the below exmaple code of dart file.
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:easymerchantsdk/easymerchantsdk.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Easymerchantsdk easymerchant = Easymerchantsdk();
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
// Controllers for basic info
final TextEditingController amountController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController sandboxApiKeyController = TextEditingController();
final TextEditingController sandboxSecretKeyController = TextEditingController();
final TextEditingController stagingApiKeyController = TextEditingController();
final TextEditingController stagingSecretKeyController = TextEditingController();
// Environment and toggles
String environment = 'sandbox';
bool isRecurring = false;
bool isAuthenticatedACH = false;
bool isSecureAuthentication = false;
bool isBillingVisible = false;
bool isAdditionalVisible = false;
bool isEmail = true; // iOS
bool emailEditable = true; // Android
bool isLoading = false;
bool showConfig = true;
bool showSecretKey = false;
bool useMinimalConfig = false; // Toggle for minimal config testing
final Map<String, dynamic> metadata = {
"metaKey": "metaValue",
"metaKey1": "metaValue1",
"metaKey2": "metaValue2",
"metaKey3": "metaValue3",
"metaKey4": "metaValue4",
};
String result = 'No response yet';
String referenceToken = '';
String _platformVersion = 'Unknown';
final Map<String, Map<String, String>> apiKeys = {
'sandbox': {
'apiKey': 'sandBoxApiKey',
'secretKey': 'sandBoxSecretKey',
},
'staging': {
'apiKey': 'stagingApiKey',
'secretKey': 'stagingSecretKey',
},
};
final Map<String, dynamic> billingInfo = {
'visibility': {'billing': false, 'additional': false},
'billing': {
'address': '',
'country': '',
'state': '',
'city': '',
'postal_code': '',
},
'billingRequired': {
'address': true,
'country': true,
'state': true,
'city': true,
'postal_code': true,
},
'additional': {
'name': 'Test User',
'email_address': 'test@gmail.com',
'phone_number': '',
'description': 'Test Payment',
},
'additionalRequired': {
'name': true,
'email_address': true,
'phone_number': true,
'description': false,
},
};
final Map<String, dynamic> themeConfiguration = {
'bodyBackgroundColor': '#0f1715',
'containerBackgroundColor': '#152321',
'primaryFontColor': '#FFFFFF',
'secondaryFontColor': '#A0B5A4',
'primaryButtonBackgroundColor': '#10B981',
'primaryButtonHoverColor': '#059669',
'primaryButtonFontColor': '#FFFFFF',
'secondaryButtonBackgroundColor': '#374151',
'secondaryButtonHoverColor': '#4B5563',
'secondaryButtonFontColor': '#E5E7EB',
'borderRadius': '8',
'fontSize': '14',
'fontWeight': 500,
'fontFamily': '"Inter", sans-serif',
};
final Map<String, dynamic> grailPayParams = {
'role': 'business',
'timeout': 10,
'isSandbox': true,
'brandingName': 'Lyfecycle Payments',
'finderSubtitle': 'Search for your bank',
'searchPlaceholder': 'Enter bank name',
};
final Map<String, dynamic> recurringData = {
'allowCycles': 2,
'intervals': ['weekly', 'monthly'],
'recurringStartType': Platform.isAndroid ? 'Custom' : 'custom',
'recurringStartDate': '07/08/2030',
};
final Map<String, dynamic> androidConfig = {
'currency': 'usd',
'saveCard': true,
'saveAccount': true,
'showReceipt': true,
'showDonate': false,
'showTotal': true,
'showSubmitButton': true,
'paymentMethod': ['card', 'ach'],
'name': 'Pavan',
'fields': {
'visibility': {'billing': false, 'additional': false},
'billing': [
{'name': 'address', 'required': true, 'value': ''},
{'name': 'country', 'required': true, 'value': ''},
{'name': 'state', 'required': true, 'value': ''},
{'name': 'city', 'required': true, 'value': 'Goa'},
{'name': 'postal_code', 'required': true, 'value': ''},
],
'additional': [
{'name': 'name', 'required': true, 'value': 'Test User 7'},
{'name': 'email_address', 'required': true, 'value': 'test@gmail.com'},
{'name': 'phone_number', 'required': true, 'value': ''},
{'name': 'description', 'required': true, 'value': 'Hi This is description'},
],
},
'appearanceSettings': {
'theme': 'dark',
'bodyBackgroundColor': '#121212',
'containerBackgroundColor': '#1E1E1E',
'primaryFontColor': '#FFFFFF',
'secondaryFontColor': '#B0B0B0',
'primaryButtonBackgroundColor': '#2563EB',
'primaryButtonHoverColor': '#1D4ED8',
'primaryButtonFontColor': '#FFFFFF',
'secondaryButtonBackgroundColor': '#374151',
'secondaryButtonHoverColor': '#4B5563',
'secondaryButtonFontColor': '#E5E7EB',
'borderRadius': '8',
'fontSize': '14',
'fontWeight': '500',
'fontFamily': 'Inter, sans-serif',
},
};
// Controllers for dynamic fields
final Map<String, TextEditingController> billingControllers = {};
final Map<String, TextEditingController> additionalControllers = {};
final Map<String, TextEditingController> themeControllers = {};
final Map<String, TextEditingController> grailPayControllers = {};
final Map<String, TextEditingController> recurringControllers = {};
final Map<String, TextEditingController> androidControllers = {};
@override
void initState() {
super.initState();
// Initialize controllers
sandboxApiKeyController.text = apiKeys['sandbox']!['apiKey']!;
sandboxSecretKeyController.text = apiKeys['sandbox']!['secretKey']!;
stagingApiKeyController.text = apiKeys['staging']!['apiKey']!;
stagingSecretKeyController.text = apiKeys['staging']!['secretKey']!;
billingInfo['billing'].forEach((key, value) {
billingControllers[key] = TextEditingController(text: value);
});
billingInfo['additional'].forEach((key, value) {
additionalControllers[key] = TextEditingController(text: value);
});
themeConfiguration.forEach((key, value) {
themeControllers[key] = TextEditingController(text: value.toString());
});
grailPayParams.forEach((key, value) {
grailPayControllers[key] = TextEditingController(text: value.toString());
});
recurringData.forEach((key, value) {
recurringControllers[key] = TextEditingController(text: value.toString());
});
androidConfig['fields']['billing'].asMap().forEach((index, field) {
androidControllers['billing_${field['name']}'] =
TextEditingController(text: field['value']);
});
androidConfig['fields']['additional'].asMap().forEach((index, field) {
androidControllers['additional_${field['name']}'] =
TextEditingController(text: field['value']);
});
androidConfig['appearanceSettings'].forEach((key, value) {
androidControllers['appearance_$key'] = TextEditingController(text: value.toString());
});
androidControllers['currency'] = TextEditingController(text: androidConfig['currency']);
androidControllers['name'] = TextEditingController(text: androidConfig['name']);
WidgetsBinding.instance.addPostFrameCallback((_) {
initPlatformState();
if (Platform.isIOS) {
initializeViewController();
_configureEnvironment();
}
});
}
Future<void> initializeViewController() async {
try {
await easymerchant.setViewController();
debugPrint('ViewController initialized successfully');
} catch (e) {
debugPrint('Failed to initialize ViewController: $e');
_showSnackBar('Failed to initialize ViewController: $e');
}
}
Future<void> initPlatformState() async {
try {
final platformVersion = await easymerchant.getPlatformVersion() ?? 'Unknown';
setState(() {
_platformVersion = platformVersion;
});
} catch (e) {
setState(() {
_platformVersion = 'Failed to get platform version: $e';
});
}
}
Future<void> _configureEnvironment() async {
try {
final keys = _getActiveKeys();
debugPrint('Configuring environment: $environment with keys: $keys');
await easymerchant.configureEnvironment(
environment,
keys['apiKey']!,
keys['secretKey']!,
);
debugPrint('Environment configured: $environment');
_showSnackBar('Environment configured: $environment');
} catch (e) {
setState(() {
result = 'Configuration failed: $e';
if (e.toString().contains('MissingPluginException')) {
result += '\nPlease ensure easymerchantsdk is properly integrated for Android. Check pubspec.yaml and Android native setup.';
}
});
debugPrint('Configuration error: $e');
_showSnackBar(result);
}
}
Map<String, String> _getActiveKeys() {
return {
'apiKey': environment == 'sandbox'
? sandboxApiKeyController.text.trim().isEmpty
? apiKeys['sandbox']!['apiKey']!
: sandboxApiKeyController.text.trim()
: stagingApiKeyController.text.trim().isEmpty
? apiKeys['staging']!['apiKey']!
: stagingApiKeyController.text.trim(),
'secretKey': environment == 'sandbox'
? sandboxSecretKeyController.text.trim().isEmpty
? apiKeys['sandbox']!['secretKey']!
: sandboxSecretKeyController.text.trim()
: stagingSecretKeyController.text.trim().isEmpty
? apiKeys['staging']!['secretKey']!
: stagingSecretKeyController.text.trim(),
};
}
Future<void> _startBilling() async {
if (amountController.text.isEmpty ||
double.tryParse(amountController.text) == null ||
double.parse(amountController.text) <= 0) {
_showSnackBar('Please enter a valid amount');
return;
}
if (emailController.text.isEmpty) {
_showSnackBar('Please enter an email address');
return;
}
if (androidConfig['paymentMethod'].isEmpty) {
_showSnackBar('Please select at least one payment method');
return;
}
setState(() {
// isLoading = true;
result = 'Processing payment...';
});
try {
if (Platform.isIOS) {
await easymerchant.setViewController();
final billingParams = {
'amount': amountController.text.trim(),
'currency': androidConfig['currency'],
'billingInfo': jsonEncode(billingInfo),
'paymentMethods': androidConfig['paymentMethod'],
'themeConfiguration': themeConfiguration,
'tokenOnly': false,
'saveCard': androidConfig['saveCard'],
'saveAccount': androidConfig['saveAccount'],
'authenticatedACH': isAuthenticatedACH,
'grailPayParams': grailPayParams,
'submitButtonText': 'Submit',
'isRecurring': isRecurring,
'numOfCycle': recurringData['allowCycles'],
'recurringIntervals': recurringData['intervals'],
'recurringStartDateType': recurringData['recurringStartType'],
'recurringStartDate': recurringData['recurringStartDate'],
'secureAuthentication': isSecureAuthentication,
'showReceipt': androidConfig['showReceipt'],
'showTotal': androidConfig['showTotal'],
'showSubmitButton': androidConfig['showSubmitButton'],
'isEmail': isEmail,
'email': emailController.text.trim(),
'name': androidConfig['name'],
'enable3DS': isSecureAuthentication,
'metadata':metadata,
};
debugPrint('📦 Billing Params (iOS):');
debugPrint(const JsonEncoder.withIndent(' ').convert(billingParams));
final response = await easymerchant.billing(
amount: billingParams['amount'],
currency: billingParams['currency'],
billingInfo: billingParams['billingInfo'],
paymentMethods: List<String>.from(billingParams['paymentMethods']),
themeConfiguration: themeConfiguration,
tokenOnly: billingParams['tokenOnly'],
saveCard: billingParams['saveCard'],
saveAccount: billingParams['saveAccount'],
authenticatedACH: billingParams['authenticatedACH'],
grailPayParams: grailPayParams,
submitButtonText: billingParams['submitButtonText'],
isRecurring: billingParams['isRecurring'],
numOfCycle: billingParams['numOfCycle'],
recurringIntervals: List<String>.from(billingParams['recurringIntervals']),
recurringStartDateType: billingParams['recurringStartDateType'],
recurringStartDate: billingParams['recurringStartDate'],
secureAuthentication: billingParams['secureAuthentication'],
showReceipt: billingParams['showReceipt'],
showTotal: billingParams['showTotal'],
showSubmitButton: billingParams['showSubmitButton'],
isEmail: billingParams['isEmail'],
email: billingParams['email'],
name: billingParams['name'],
enable3DS: billingParams['enable3DS'],
metadata: billingParams['metadata']
);
debugPrint('📩 Billing Response (iOS): $response');
final responseJson = jsonDecode(response!);
final sdkResult = SDKResult.fromJson(responseJson);
debugPrint('📋 SDKResult: type=${sdkResult.type}, billingInfo=${sdkResult.billingInfo}, additionalInfo=${sdkResult.additionalInfo}');
setState(() {
result = jsonEncode(responseJson, toEncodable: (obj) => obj.toString());
referenceToken = sdkResult.additionalInfo?['threeDSecureStatus']?['data']?['ref_token'] ?? '';
isLoading = false;
});
if (sdkResult.billingInfo == null || sdkResult.additionalInfo == null) {
debugPrint('⚠️ Warning: billingInfo or additionalInfo is null in response');
_showSnackBar('Billing or additional info missing in response');
}
} else {
final keys = _getActiveKeys();
debugPrint('API Key: ${keys['apiKey']}, Secret Key: ${keys['secretKey']}');
debugPrint('Selected Payment Methods: ${androidConfig['paymentMethod']}');
androidConfig['fields']['visibility']['billing'] = isBillingVisible;
androidConfig['fields']['visibility']['additional'] = isAdditionalVisible;
// Try both paymentMethod and paymentMethods keys to ensure compatibility
final config = useMinimalConfig
? {
'environment': environment,
'apiKey': keys['apiKey'],
'secretKey': keys['secretKey'],
'amount': amountController.text.trim(),
'currency': 'usd',
'email': emailController.text.trim(),
'paymentMethod': androidConfig['paymentMethod'], // Singular key
'paymentMethods': androidConfig['paymentMethod'], // Plural key
'ShowFields': false,
'showFields': false,
'show_fields': false,
'displayFields': false,
"metadata": metadata,
}
: {
'environment': environment,
'apiKey': keys['apiKey'],
'secretKey': keys['secretKey'],
'amount': amountController.text.trim(),
'tokenOnly': false,
'currency': androidConfig['currency'],
'saveCard': androidConfig['saveCard'],
'saveAccount': androidConfig['saveAccount'],
'authenticatedACH': isAuthenticatedACH,
'secureAuthentication': isSecureAuthentication,
'showReceipt': androidConfig['showReceipt'],
'showDonate': androidConfig['showDonate'],
'showTotal': androidConfig['showTotal'],
'showSubmitButton': androidConfig['showSubmitButton'],
'paymentMethod': androidConfig['paymentMethod'], // Singular key
'paymentMethods': androidConfig['paymentMethod'], // Plural key
'emailEditable': emailEditable,
'email': emailController.text.trim(),
'name': androidConfig['name'],
'ShowFields': isBillingVisible || isAdditionalVisible,
'showFields': isBillingVisible || isAdditionalVisible,
'show_fields': isBillingVisible || isAdditionalVisible,
'displayFields': isBillingVisible || isAdditionalVisible,
'fields': {
'billing': androidConfig['fields']['billing'],
'additional': androidConfig['fields']['additional'],
'visibility': {
'billing': isBillingVisible,
'additional': isAdditionalVisible,
},
},
if (isRecurring)
'recurring': {
'enableRecurring': isRecurring,
'recurringData': {
'allowCycles': recurringData['allowCycles'].toString(),
'intervals': recurringData['intervals'],
'recurringStartType': recurringData['recurringStartType'],
'recurringStartDate': recurringData['recurringStartDate'],
},
},
'grailPayParams': grailPayParams,
'appearanceSettings': androidConfig['appearanceSettings'],
"metadata": metadata,
};
final configJson = jsonEncode(config);
debugPrint('Android Config:');
debugPrint(const JsonEncoder.withIndent(' ').convert(config));
// Validate JSON string
if (configJson.isEmpty) {
throw Exception('Failed to serialize config to JSON');
}
final response = await easymerchant.makePayment(configJson);
debugPrint('Raw Android Response: $response');
if (response == null) {
setState(() {
result = 'Billing failed: No response from SDK';
isLoading = false;
});
_showSnackBar('Billing failed: No response from SDK');
return;
}
try {
final responseJson = jsonDecode(response);
final sdkResult = SDKResult.fromJson(responseJson);
setState(() {
result = jsonEncode(responseJson, toEncodable: (obj) => obj.toString());
referenceToken = sdkResult.additionalInfo?['threeDSecureStatus']?['data']?['ref_token'] ?? '';
isLoading = false;
});
if (sdkResult.billingInfo == null || sdkResult.additionalInfo == null) {
debugPrint('Warning: billingInfo or additionalInfo is null in response');
_showSnackBar('Billing or additional info missing in response');
}
} catch (e) {
setState(() {
result = 'Billing failed: $e';
isLoading = false;
});
debugPrint('Billing error: $e');
_showSnackBar('Billing failed: $e');
}
}
} catch (e) {
setState(() {
result = 'Billing failed: $e';
isLoading = false;
});
debugPrint('Billing error: $e');
_showSnackBar('Billing failed: $e');
}
}
Future<void> _checkPaymentStatus() async {
setState(() {
isLoading = true;
result = 'Checking payment status...';
});
try {
// final response = await easymerchant.checkPaymentStatus();
// final responseJson = jsonDecode(response);
// setState(() {
// result = jsonEncode(responseJson, toEncodable: (obj) => obj.toString());
// isLoading = false;
// });
} catch (e) {
setState(() {
result = 'Status check failed: $e';
isLoading = false;
});
_showSnackBar('Status check failed: $e');
}
}
Future<void> _paymentReference() async {
if (Platform.isAndroid) {
setState(() {
result = 'Payment Reference not supported on Android';
});
return;
}
if (referenceToken.isEmpty) {
setState(() {
result = 'No reference token available';
});
_showSnackBar('No reference token available');
return;
}
setState(() {
isLoading = true;
result = 'Checking payment reference...';
});
try {
final response = await easymerchant.paymentReference(referenceToken);
setState(() {
result = jsonEncode(jsonDecode(response!), toEncodable: (obj) => obj.toString());
isLoading = false;
});
} catch (e) {
setState(() {
result = 'Payment reference failed: $e';
isLoading = false;
});
_showSnackBar('Payment reference failed: $e');
}
}
void _showSnackBar(String message) {
_scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(content: Text(message)),
);
}
void _togglePaymentMethod(String method) {
setState(() {
final normalizedMethod = Platform.isAndroid ? method.toLowerCase() : method.toLowerCase(); // Use lowercase for Android
if (androidConfig['paymentMethod'].contains(normalizedMethod)) {
androidConfig['paymentMethod'].remove(normalizedMethod);
} else {
androidConfig['paymentMethod'].add(normalizedMethod);
}
debugPrint('Toggled Payment Method: $normalizedMethod, Current Methods: ${androidConfig['paymentMethod']}');
});
}
void _toggleInterval(String interval) {
setState(() {
if (recurringData['intervals'].contains(interval)) {
recurringData['intervals'].remove(interval);
} else {
recurringData['intervals'].add(interval);
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: _scaffoldMessengerKey,
theme: ThemeData(
primaryColor: const Color(0xFF2563EB),
scaffoldBackgroundColor: const Color(0xFFF9FAFB),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2563EB),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
),
),
home: Scaffold(
appBar: AppBar(
title: const Text(
'EasyMerchant Flutter App',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
const Text('Basic Info', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: amountController,
decoration: const InputDecoration(
labelText: 'Amount (e.g., 10.00)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 10),
TextField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 10),
CheckboxListTile(
value: useMinimalConfig,
title: const Text('Use Minimal Config (Android)'),
onChanged: (v) => setState(() => useMinimalConfig = v!),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ElevatedButton(
onPressed: androidConfig['paymentMethod'].isEmpty ? null : _startBilling,
child: const Text('Pay'),
),
),
if (Platform.isIOS) ...[
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: _paymentReference,
child: const Text('Payment Ref'),
),
),
],
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Show Configurations', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
Switch(
value: showConfig,
onChanged: (value) => setState(() => showConfig = value),
activeColor: const Color(0xFF2563EB),
),
],
),
if (showConfig) ...[
const SizedBox(height: 20),
const Text('Environment', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: environment == 'sandbox' || isLoading
? null
: () {
setState(() => environment = 'sandbox');
if (Platform.isIOS) _configureEnvironment();
},
style: ElevatedButton.styleFrom(
backgroundColor: environment == 'sandbox' ? const Color(0xFF2563EB) : Colors.grey,
),
child: const Text('Sandbox'),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: environment == 'staging' || isLoading
? null
: () {
setState(() => environment = 'staging');
if (Platform.isIOS) _configureEnvironment();
},
style: ElevatedButton.styleFrom(
backgroundColor: environment == 'staging' ? const Color(0xFF2563EB) : Colors.grey,
),
child: const Text('Staging'),
),
),
],
),
const SizedBox(height: 10),
TextField(
controller: environment == 'sandbox' ? sandboxApiKeyController : stagingApiKeyController,
decoration: InputDecoration(
labelText: '${environment == 'sandbox' ? 'Sandbox' : 'Staging'} API Key',
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
controller: environment == 'sandbox' ? sandboxSecretKeyController : stagingSecretKeyController,
decoration: InputDecoration(
labelText: '${environment == 'sandbox' ? 'Sandbox' : 'Staging'} Secret Key',
border: const OutlineInputBorder(),
),
obscureText: !showSecretKey,
),
),
IconButton(
icon: Icon(showSecretKey ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => showSecretKey = !showSecretKey),
),
],
),
const SizedBox(height: 20),
const Text('Payment Options', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
CheckboxListTile(
value: isRecurring,
title: const Text('Recurring Payment'),
onChanged: (v) => setState(() => isRecurring = v!),
),
CheckboxListTile(
value: isAuthenticatedACH,
title: const Text('Authenticated ACH'),
onChanged: (v) => setState(() => isAuthenticatedACH = v!),
),
CheckboxListTile(
value: isSecureAuthentication,
title: const Text('3DS'),
onChanged: (v) => setState(() => isSecureAuthentication = v!),
),
CheckboxListTile(
value: isBillingVisible,
title: const Text('Billing Visible'),
onChanged: (v) => setState(() {
isBillingVisible = v!;
billingInfo['visibility']['billing'] = v;
}),
),
CheckboxListTile(
value: isAdditionalVisible,
title: const Text('Additional Info Visible'),
onChanged: (v) => setState(() {
isAdditionalVisible = v!;
billingInfo['visibility']['additional'] = v;
}),
),
if (Platform.isAndroid)
CheckboxListTile(
value: emailEditable,
title: const Text('Email Editable (Android)'),
onChanged: (v) => setState(() => emailEditable = v!),
),
if (Platform.isIOS)
CheckboxListTile(
value: isEmail,
title: const Text('Allow Email (iOS)'),
onChanged: (v) => setState(() => isEmail = v!),
),
const SizedBox(height: 10),
const Text('Payment Methods', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => _togglePaymentMethod('card'),
style: ElevatedButton.styleFrom(
backgroundColor: androidConfig['paymentMethod'].contains(Platform.isAndroid ? 'card' : 'card')
? const Color(0xFF2563EB)
: Colors.grey,
),
child: const Text('Card'),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: () => _togglePaymentMethod('ach'),
style: ElevatedButton.styleFrom(
backgroundColor: androidConfig['paymentMethod'].contains(Platform.isAndroid ? 'ach' : 'ach')
? const Color(0xFF2563EB)
: Colors.grey,
),
child: const Text('ACH'),
),
),
],
),
const SizedBox(height: 20),
if (Platform.isAndroid) ...[
const SizedBox(height: 20),
const Text('Android Configuration', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
TextField(
controller: androidControllers['currency'],
decoration: const InputDecoration(labelText: 'Currency', border: OutlineInputBorder()),
onChanged: (value) => androidConfig['currency'] = value,
),
CheckboxListTile(
value: androidConfig['saveCard'],
title: const Text('Save Card'),
onChanged: (v) => setState(() => androidConfig['saveCard'] = v!),
),
CheckboxListTile(
value: androidConfig['saveAccount'],
title: const Text('Save Account'),
onChanged: (v) => setState(() => androidConfig['saveAccount'] = v!),
),
CheckboxListTile(
value: androidConfig['showReceipt'],
title: const Text('Show Receipt'),
onChanged: (v) => setState(() => androidConfig['showReceipt'] = v!),
),
CheckboxListTile(
value: androidConfig['showDonate'],
title: const Text('Show Donate'),
onChanged: (v) => setState(() => androidConfig['showDonate'] = v!),
),
CheckboxListTile(
value: androidConfig['showTotal'],
title: const Text('Show Total'),
onChanged: (v) => setState(() => androidConfig['showTotal'] = v!),
),
CheckboxListTile(
value: androidConfig['showSubmitButton'],
title: const Text('Show Submit Button'),
onChanged: (v) => setState(() => androidConfig['showSubmitButton'] = v!),
),
TextField(
controller: androidControllers['name'],
decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
onChanged: (value) => androidConfig['name'] = value,
),
const SizedBox(height: 10),
const Text('Android Fields', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
...androidConfig['fields']['billing'].asMap().entries.map((entry) {
final index = entry.key;
final field = entry.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [],
);
}),
...androidConfig['fields']['additional'].asMap().entries.map((entry) {
final index = entry.key;
final field = entry.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [],
);
}),
const SizedBox(height: 10),
const Text('Android Appearance Settings',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
...androidConfig['appearanceSettings'].keys.map((key) => TextField(
controller: androidControllers['appearance_$key'],
onChanged: (value) => androidConfig['appearanceSettings'][key] = value,
)),
],
if (Platform.isIOS) ...[
const SizedBox(height: 20),
const Text('Theme Configuration (iOS)',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
...themeConfiguration.keys.map((key) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: themeControllers[key],
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) => themeConfiguration[key] = value,
),
)),
],
const SizedBox(height: 20),
const Text('GrailPay Parameters', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
...grailPayParams.keys.map((key) => key == 'isSandbox'
? CheckboxListTile(
value: grailPayParams[key],
title: const Text('Is Sandbox'),
onChanged: (v) => setState(() => grailPayParams[key] = v!),
)
: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: grailPayControllers[key],
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
keyboardType: key == 'timeout' ? TextInputType.number : TextInputType.text,
onChanged: (value) => grailPayParams[key] = key == 'timeout' ? int.parse(value) : value,
),
)),
const SizedBox(height: 20),
const Text('Recurring Data', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: recurringControllers['allowCycles'],
decoration: const InputDecoration(
labelText: 'Allow Cycles',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => recurringData['allowCycles'] = int.parse(value),
),
const SizedBox(height: 10),
const Text('Intervals', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => _toggleInterval('weekly'),
style: ElevatedButton.styleFrom(
backgroundColor: recurringData['intervals'].contains('weekly')
? const Color(0xFF2563EB)
: Colors.grey,
),
child: const Text('Weekly'),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton(
onPressed: () => _toggleInterval('monthly'),
style: ElevatedButton.styleFrom(
backgroundColor: recurringData['intervals'].contains('monthly')
? const Color(0xFF2563EB)
: Colors.grey,
),
child: const Text('Monthly'),
),
),
],
),
const SizedBox(height: 10),
TextField(
controller: recurringControllers['recurringStartType'],
decoration: const InputDecoration(
labelText: 'Recurring Start Type',
border: OutlineInputBorder(),
),
onChanged: (value) => recurringData['recurringStartType'] = Platform.isAndroid ? value : value.toLowerCase(),
),
const SizedBox(height: 10),
TextField(
controller: recurringControllers['recurringStartDate'],
decoration: const InputDecoration(
labelText: 'Recurring Start Date (MM/DD/YYYY)',
border: OutlineInputBorder(),
),
onChanged: (value) => recurringData['recurringStartDate'] = value,
),
],
const SizedBox(height: 20),
const Text('SDK Response', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SelectableText(
result,
style: const TextStyle(fontSize: 14, fontFamily: 'monospace', color: Colors.black87),
),
if (referenceToken.isNotEmpty)
SelectableText(
'Reference Token: $referenceToken',
style: const TextStyle(fontSize: 14, fontFamily: 'monospace', color: Colors.black87),
),
const SizedBox(height: 20),
const Text('Billing Info', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Builder(
builder: (context) {
try {
final sdkResult = SDKResult.fromJson(jsonDecode(result));
final billing = sdkResult.billingInfo;
if (billing != null) {
final pretty = const JsonEncoder.withIndent(' ').convert(billing);
return SelectableText(pretty, style: const TextStyle(fontFamily: 'monospace'));
} else {
return const Text('Not available');
}
} catch (_) {
return const Text('Not available');
}
},
),
const SizedBox(height: 10),
const Text('Additional Info', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Builder(
builder: (context) {
try {
final sdkResult = SDKResult.fromJson(jsonDecode(result));
final additional = sdkResult.additionalInfo;
if (additional != null) {
final pretty = const JsonEncoder.withIndent(' ').convert(additional);
return SelectableText(pretty, style: const TextStyle(fontFamily: 'monospace'));
} else {
return const Text('Not available');
}
} catch (_) {
return const Text('Not available');
}
},
),
],
),
),
),
);
}
@override
void dispose() {
amountController.dispose();
emailController.dispose();
sandboxApiKeyController.dispose();
sandboxSecretKeyController.dispose();
stagingApiKeyController.dispose();
stagingSecretKeyController.dispose();
billingControllers.values.forEach((controller) => controller.dispose());
additionalControllers.values.forEach((controller) => controller.dispose());
themeControllers.values.forEach((controller) => controller.dispose());
grailPayControllers.values.forEach((controller) => controller.dispose());
recurringControllers.values.forEach((controller) => controller.dispose());
androidControllers.values.forEach((controller) => controller.dispose());
super.dispose();
}
}
extension StringExtension on String {
String titleCase() {
return split(' ').map((word) => word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}'
: word).join(' ');
}
}
class SDKResult {
final String type;
final Map<String, dynamic>? chargeData;
final Map<String, dynamic>? billingInfo;
final Map<String, dynamic>? additionalInfo;
final String? error;
SDKResult({
required this.type,
this.chargeData,
this.billingInfo,
this.additionalInfo,
this.error,
});
factory SDKResult.fromJson(Map<String, dynamic> json) {
return SDKResult(
type: json['type'] ?? 'success',
chargeData: json['chargeData'],
billingInfo: json['billingInfo'],
additionalInfo: json['additionalInfo'] ?? json['additional_info'],
error: json['error'],
);
}
}