easymerchantsdk 0.0.4 copy "easymerchantsdk: ^0.0.4" to clipboard
easymerchantsdk: ^0.0.4 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: ^0.0.4

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

  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,
        };

        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'],
        );

        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,
        }
            : {
          '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'],
        };

        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: isLoading || androidConfig['paymentMethod'].isEmpty ? null : _startBilling,
                      child: const Text('Pay'),
                    ),
                  ),
                  if (Platform.isIOS) ...[
                    const SizedBox(width: 10),
                    Expanded(
                      child: ElevatedButton(
                        onPressed: isLoading ? null : _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'],
    );
  }
}