mtn_momo_sdk 0.0.1 copy "mtn_momo_sdk: ^0.0.1" to clipboard
mtn_momo_sdk: ^0.0.1 copied to clipboard

An unofficial package for integrating with MTN Mobile Money Collections & Disbursements & Remittances API, enabling the payment of goods and services using MTN Mobile Money.

example/lib/main.dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mtn_momo_sdk/mtn_momo_sdk.dart';
import 'package:uuid/uuid.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]);
  runApp(const MomoSdkPlaygroundApp());
}

/// The main entry point of the interactive SDK Playground.
class MomoSdkPlaygroundApp extends StatelessWidget {
  const MomoSdkPlaygroundApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MTN MoMo SDK Playground',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        fontFamily: 'Inter',
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFFFCB05), // MTN Sunshine Yellow (#FFCB05)
          primary: const Color(0xFFFFCB05),
          onPrimary: Colors.black,
          secondary: const Color(0xFF004F71), // MoMo Blue (#004F71)
          onSecondary: Colors.white,
          surface: const Color(0xFFFFFFFF), // Pure White (#FFFFFF)
        ),
        cardTheme: const CardThemeData(
          color: Colors.white,
          elevation: 0,
          margin: EdgeInsets.zero,
          shape: RoundedRectangleBorder(
            side: BorderSide(color: Color(0xFFE5E5E5), width: 1),
            borderRadius: BorderRadius.all(Radius.circular(12)),
          ),
        ),
        inputDecorationTheme: InputDecorationTheme(
          filled: true,
          fillColor: const Color(0xFFF5F5F5),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: BorderSide.none,
          ),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: const BorderSide(color: Color(0xFFE5E5E5), width: 1),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: const BorderSide(color: Color(0xFF004F71), width: 1.5),
          ),
          labelStyle: const TextStyle(color: Colors.grey, fontSize: 14),
        ),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        fontFamily: 'Inter',
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFFFFCB05), // MTN Sunshine Yellow (#FFCB05)
          onPrimary: Colors.black,
          secondary: Color(0xFF004F71), // MoMo Blue (#004F71)
          onSecondary: Colors.white,
          surface: Color(
              0xFF0C2B3E), // Lighter deep navy blue for dark card surfaces
        ),
        cardTheme: const CardThemeData(
          color: Color(0xFF0C2B3E),
          elevation: 0,
          margin: EdgeInsets.zero,
          shape: RoundedRectangleBorder(
            side: BorderSide(color: Color(0xFF143D56), width: 1),
            borderRadius: BorderRadius.all(Radius.circular(12)),
          ),
        ),
        inputDecorationTheme: InputDecorationTheme(
          filled: true,
          fillColor: const Color(0xFF072030),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: BorderSide.none,
          ),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: const BorderSide(color: Color(0xFF143D56), width: 1),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: const BorderSide(color: Color(0xFFFFCB05), width: 1.5),
          ),
          labelStyle: const TextStyle(color: Colors.grey, fontSize: 14),
        ),
      ),
      themeMode: ThemeMode.system,
      home: const PlaygroundHomeScreen(),
    );
  }
}

class PlaygroundHomeScreen extends StatefulWidget {
  const PlaygroundHomeScreen({super.key});

  @override
  State<PlaygroundHomeScreen> createState() => _PlaygroundHomeScreenState();
}

class _PlaygroundHomeScreenState extends State<PlaygroundHomeScreen> {
  // Controller inputs
  final _subKeyController =
      TextEditingController(text: 'a9acc520ea7d487baa58af01167d5659'); // This is an example key, replace it with your own!
  final _userIdController = TextEditingController();
  final _apiKeyController = TextEditingController();
  final _collectMsisdnController = TextEditingController(text: '256772123456');
  final _collectAmountController = TextEditingController(text: '5000');
  final _disbMsisdnController = TextEditingController(text: '256772987654');
  final _disbAmountController = TextEditingController(text: '12000');

  // New Controllers for Advanced examples
  final _preAppMsisdnController = TextEditingController(text: '256772123456');
  final _preAppValidityController = TextEditingController(text: '3600');
  final _refundTransferIdController = TextEditingController();
  final _refundAmountController = TextEditingController(text: '5000');

  // Interactive UI States
  bool _isProvisioning = false;
  bool _isCollectionsRunning = false;
  bool _isDisbursementsRunning = false;
  bool _isPreAppRunning = false;
  bool _isRefundRunning = false;
  bool _isSimulatorRunning = false;

  // Active status values
  String _colBalance = '—';
  String _disbBalance = '—';
  String _colTxnState = 'No active transaction';
  String _disbTxnState = 'No active transaction';
  String _preAppTxnState = 'No active request';
  String _refundTxnState = 'No active request';
  String _simulatorState = 'Idle';

  // Live Terminal Logs
  final List<String> _terminalLogs = [];
  final ScrollController _terminalScrollController = ScrollController();

  // Unified Momo SDK wrappers for Product Token Isolation
  MtnMomo? _collectionsMomo;
  MtnMomo? _disbursementsMomo;

  @override
  void initState() {
    super.initState();
    _logTerminal('System: MTN Mobile Money SDK Playground Initialized.');
    _logTerminal(
        'System: Aligned with official MoMo Blue (#004F71) & MTN Sunshine (#FFCB05) guidelines.');
  }

  @override
  void dispose() {
    _subKeyController.dispose();
    _userIdController.dispose();
    _apiKeyController.dispose();
    _collectMsisdnController.dispose();
    _collectAmountController.dispose();
    _disbMsisdnController.dispose();
    _disbAmountController.dispose();
    _preAppMsisdnController.dispose();
    _preAppValidityController.dispose();
    _refundTransferIdController.dispose();
    _refundAmountController.dispose();
    _terminalScrollController.dispose();
    super.dispose();
  }

  void _logTerminal(String message) {
    final timestamp = DateTime.now().toLocal().toString().substring(11, 19);
    setState(() {
      _terminalLogs.add('[$timestamp] $message');
    });
    // Autoscroll terminal
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_terminalScrollController.hasClients) {
        _terminalScrollController.animateTo(
          _terminalScrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 200),
          curve: Curves.easeOut,
        );
      }
    });
  }

  /// Instantiates the client wrappers using current fields.
  bool _initializeClient() {
    if (_collectionsMomo != null && _disbursementsMomo != null) return true;

    final subKey = _subKeyController.text.trim();
    final uid = _userIdController.text.trim();
    final key = _apiKeyController.text.trim();

    if (subKey.isEmpty || uid.isEmpty || key.isEmpty) {
      _logTerminal(
          'Client: Error - Subscription Key, API User, or API Key cannot be empty.');
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
            content: Text('Please complete Sandbox Credentials first!')),
      );
      return false;
    }

    _collectionsMomo = MtnMomo(
      baseUrl: 'https://sandbox.momodeveloper.mtn.com',
      subscriptionKey: subKey,
      userId: uid,
      apiKey: key,
      targetEnvironment: 'sandbox',
    );

    _disbursementsMomo = MtnMomo(
      baseUrl: 'https://sandbox.momodeveloper.mtn.com',
      subscriptionKey: subKey,
      userId: uid,
      apiKey: key,
      targetEnvironment: 'sandbox',
    );

    _logTerminal(
        'Client: Separate dedicated Collections and Disbursements clients initialized.');
    return true;
  }

  /// Flow 1: Provision Sandbox User
  Future<void> _provisionSandbox() async {
    final subKey = _subKeyController.text.trim();
    if (subKey.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please enter a Subscription Key first.')),
      );
      return;
    }

    setState(() {
      _isProvisioning = true;
      _collectionsMomo = null;
      _disbursementsMomo = null; // Reset existing wrappers
    });

    _logTerminal('Sandbox: Initializing user provisioning process...');
    final dio = Dio(
      BaseOptions(
        baseUrl: 'https://sandbox.momodeveloper.mtn.com',
        headers: {
          'Ocp-Apim-Subscription-Key': subKey,
          'Content-Type': 'application/json',
        },
      ),
    );

    final provisioner = SandboxProvisioningClient(dio);
    final generatedUserId = const Uuid().v4();

    try {
      _logTerminal('Sandbox: Posting V1.0 API User registration request...');
      _logTerminal('Sandbox: X-Reference-Id generated: $generatedUserId');

      await provisioner.postV10Apiuser(
        xReferenceId: generatedUserId,
        body: const ApiUser(
          providerCallbackHost: 'callbacks.playground-app.com',
        ),
      );

      _logTerminal(
          'Sandbox: Registration completed. Propagating database records...');
      await Future.delayed(const Duration(seconds: 2));

      _logTerminal(
          'Sandbox: Requesting API Key for User ID: $generatedUserId...');
      final response = await provisioner.postV10ApiuserApikey(
        xReferenceId: generatedUserId,
      );

      final key = response.apiKey;
      if (key == null || key.isEmpty) {
        throw Exception('Gateway returned empty or null API Key.');
      }

      setState(() {
        _userIdController.text = generatedUserId;
        _apiKeyController.text = key;
      });

      _logTerminal('Sandbox: ✓ API Key successfully generated: $key');
      _logTerminal('Sandbox: Provisioning fully completed!');
    } on MtnMomoException catch (e) {
      _logTerminal('Sandbox: ✗ Exception: ${e.message}');
    } catch (e) {
      _logTerminal('Sandbox: ✗ Error: $e');
    } finally {
      setState(() {
        _isProvisioning = false;
      });
    }
  }

  /// Flow 2: Collections - Check Balance
  Future<void> _fetchCollectionsBalance() async {
    if (!_initializeClient()) return;

    _logTerminal('Collections: Fetching merchant account balance...');
    try {
      final balance = await _collectionsMomo!.collection.getAccountBalance();
      setState(() {
        _colBalance = '${balance.availableBalance} ${balance.currency}';
      });
      _logTerminal('Collections: ✓ Balance fetched: $_colBalance');
    } on MtnMomoException catch (e) {
      _logTerminal('Collections: ✗ Exception: ${e.message}');
    } catch (e) {
      _logTerminal('Collections: ✗ Error: $e');
    }
  }

  /// Flow 3: Collections - Push USSD
  Future<void> _runCollectionsFlow() async {
    if (!_initializeClient()) return;

    final customer = _collectMsisdnController.text.trim();
    final amount = _collectAmountController.text.trim();

    if (customer.isEmpty || amount.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
            content: Text('Please specify customer phone number and amount.')),
      );
      return;
    }

    setState(() {
      _isCollectionsRunning = true;
      _colTxnState = 'Initializing request...';
    });

    try {
      // 1. Validate status
      _logTerminal(
          'Collections: Validating customer wallet registration ($customer)...');
      await _collectionsMomo!.collection.validateAccountHolderStatus(
        accountHolderId: customer,
        accountHolderIdType: 'msisdn',
      );
      _logTerminal('Collections: ✓ Wallet verified.');

      // 2. Trigger push
      final referenceId = const Uuid().v4();
      _logTerminal(
          'Collections: Dispatching Request to Pay ($referenceId) for $amount EUR...');

      final requestBody = RequestToPay(
        amount: amount,
        currency: 'EUR',
        externalId: 'TXN_PLAYGROUND_${const Uuid().v4().substring(0, 8)}',
        payer: Party(
          partyIdType: PartyPartyIdType.msisdn,
          partyId: customer,
        ),
        payerMessage: 'Playground transaction',
        payeeNote: 'Antigravity systems integration',
      );

      await _collectionsMomo!.collection.requesttoPay(
        xReferenceId: referenceId,
        body: requestBody,
      );
      _logTerminal(
          'Collections: ✓ Request to pay sent to handset. Polling active status...');

      // 3. Polling
      RequestToPayResultStatus? finalState;
      var attempts = 0;

      while (attempts < 10) {
        attempts++;
        setState(() {
          _colTxnState = 'Polling status (Attempt $attempts/10)';
        });

        final status =
            await _collectionsMomo!.collection.requesttoPayTransactionStatus(
          referenceId: referenceId,
        );

        finalState = status.status;
        _logTerminal('Collections: Current poll status: $finalState');

        if (finalState == RequestToPayResultStatus.successful) {
          setState(() {
            _colTxnState = '✓ Transaction Successful';
          });
          _logTerminal('Collections: ✓ Payment completed SUCCESSFUL!');
          break;
        } else if (finalState == RequestToPayResultStatus.failed) {
          setState(() {
            _colTxnState = '✗ Transaction Failed: ${status.reason?.code}';
          });
          _logTerminal(
              'Collections: ✗ Payment FAILED. Reason: ${status.reason?.code}');
          break;
        }

        await Future.delayed(const Duration(seconds: 3));
      }

      if (finalState == RequestToPayResultStatus.pending) {
        setState(() {
          _colTxnState = '✗ Polling Timeout';
        });
        _logTerminal(
            'Collections: ✗ Polling timeout. Handset response took too long.');
      }
    } on MtnMomoException catch (e) {
      setState(() {
        _colTxnState = 'Exception: ${e.message}';
      });
      _logTerminal('Collections: ✗ Exception: ${e.message}');
    } catch (e) {
      setState(() {
        _colTxnState = 'Error: $e';
      });
      _logTerminal('Collections: ✗ Error: $e');
    } finally {
      setState(() {
        _isCollectionsRunning = false;
      });
      _fetchCollectionsBalance();
    }
  }

  /// Flow 4: Disbursements - Check Balance
  Future<void> _fetchDisbursementsBalance() async {
    if (!_initializeClient()) return;

    _logTerminal('Disbursements: Fetching merchant payout balance...');
    try {
      final balance =
          await _disbursementsMomo!.disbursements.getAccountBalance();
      setState(() {
        _disbBalance = '${balance.availableBalance} ${balance.currency}';
      });
      _logTerminal('Disbursements: ✓ Balance fetched: $_disbBalance');
    } on MtnMomoException catch (e) {
      _logTerminal('Disbursements: ✗ Exception: ${e.message}');
    } catch (e) {
      _logTerminal('Disbursements: ✗ Error: $e');
    }
  }

  /// Flow 5: Disbursements - Transfer
  Future<void> _runDisbursementsFlow() async {
    if (!_initializeClient()) return;

    final payee = _disbMsisdnController.text.trim();
    final amount = _disbAmountController.text.trim();

    if (payee.isEmpty || amount.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
            content: Text('Please specify payee phone number and amount.')),
      );
      return;
    }

    setState(() {
      _isDisbursementsRunning = true;
      _disbTxnState = 'Initializing transfer...';
    });

    try {
      // 1. Validate status
      _logTerminal('Disbursements: Checking payee wallet status ($payee)...');
      await _disbursementsMomo!.disbursements.validateAccountHolderStatus(
        accountHolderId: payee,
        accountHolderIdType: 'msisdn',
      );
      _logTerminal('Disbursements: ✓ Payee wallet is active.');

      // 2. Trigger transfer
      final referenceId = const Uuid().v4();
      _logTerminal(
          'Disbursements: Initiating transfer ($referenceId) for $amount EUR...');

      final transferBody = Transfer(
        amount: amount,
        currency: 'EUR',
        externalId: 'TXN_DISB_${const Uuid().v4().substring(0, 8)}',
        payee: Party(
          partyIdType: PartyPartyIdType.msisdn,
          partyId: payee,
        ),
        payerMessage: 'Salary disbursement',
        payeeNote: 'Playground transfer',
      );

      await _disbursementsMomo!.disbursements.transfer(
        xReferenceId: referenceId,
        body: transferBody,
      );
      _logTerminal(
          'Disbursements: ✓ Transfer posted on gateway. Polling status...');

      // 3. Polling
      TransferResultStatus? finalState;
      var attempts = 0;

      while (attempts < 10) {
        attempts++;
        setState(() {
          _disbTxnState = 'Polling status (Attempt $attempts/10)';
        });

        final status =
            await _disbursementsMomo!.disbursements.getTransferStatus(
          referenceId: referenceId,
        );

        finalState = status.status;
        _logTerminal('Disbursements: Current poll status: $finalState');

        if (finalState == TransferResultStatus.successful) {
          setState(() {
            _disbTxnState = '✓ Transfer Successful';
          });
          _logTerminal('Disbursements: ✓ Transfer completed SUCCESSFUL!');
          break;
        } else if (finalState == TransferResultStatus.failed) {
          setState(() {
            _disbTxnState = '✗ Payout Failed: ${status.reason?.code}';
          });
          _logTerminal(
              'Disbursements: ✗ Payout FAILED. Reason: ${status.reason?.code}');
          break;
        }

        await Future.delayed(const Duration(seconds: 3));
      }

      if (finalState == TransferResultStatus.pending) {
        setState(() {
          _disbTxnState = '✗ Polling Timeout';
        });
        _logTerminal(
            'Disbursements: ✗ Polling timeout. Gateway took too long to complete.');
      }
    } on MtnMomoException catch (e) {
      setState(() {
        _disbTxnState = 'Exception: ${e.message}';
      });
      _logTerminal('Disbursements: ✗ Exception: ${e.message}');
    } catch (e) {
      setState(() {
        _disbTxnState = 'Error: $e';
      });
      _logTerminal('Disbursements: ✗ Error: $e');
    } finally {
      setState(() {
        _isDisbursementsRunning = false;
      });
      _fetchDisbursementsBalance();
    }
  }

  /// Advanced Flow 1: Collections Pre-Approval
  Future<void> _runPreApprovalFlow() async {
    if (!_initializeClient()) return;

    final customer = _preAppMsisdnController.text.trim();
    final validityStr = _preAppValidityController.text.trim();

    if (customer.isEmpty || validityStr.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
            content:
                Text('Please specify customer phone and validity duration.')),
      );
      return;
    }

    final validity = int.tryParse(validityStr) ?? 3600;

    setState(() {
      _isPreAppRunning = true;
      _preAppTxnState = 'Initializing request...';
    });

    try {
      final referenceId = const Uuid().v4();
      _logTerminal(
          'Pre-Approval: Initiating consent request ($referenceId) for $customer...');

      final preApproval = PreApproval(
        payer: Party(
          partyIdType: PartyPartyIdType.msisdn,
          partyId: customer,
        ),
        payerCurrency: 'EUR',
        payerMessage: 'Consent to charge wallet for subscription.',
        validityTime: validity,
      );

      await _collectionsMomo!.collection.preApproval(
        xReferenceId: referenceId,
        body: preApproval,
      );
      _logTerminal(
          'Pre-Approval: ✓ Consent request submitted. Polling status...');

      PreApprovalResultStatus? finalState;
      var attempts = 0;

      while (attempts < 10) {
        attempts++;
        setState(() {
          _preAppTxnState = 'Polling status (Attempt $attempts/10)';
        });
        await Future.delayed(const Duration(seconds: 2));

        try {
          final status = await _collectionsMomo!.collection
              .getPreApprovalStatus(referenceId: referenceId);
          finalState = status.status;
          _logTerminal('Pre-Approval: Current status: $finalState');

          if (finalState == PreApprovalResultStatus.successful) {
            setState(() {
              _preAppTxnState = '✓ Consent Granted';
            });
            _logTerminal('Pre-Approval: ✓ Mandate GRANTED successfully!');
            break;
          } else if (finalState == PreApprovalResultStatus.failed) {
            setState(() {
              _preAppTxnState = '✗ Consent Rejected: ${status.reason?.code}';
            });
            _logTerminal(
                'Pre-Approval: ✗ Consent REJECTED. Reason: ${status.reason?.code}');
            break;
          }
        } on MtnMomoNotFoundException catch (_) {
          _logTerminal(
              'Pre-Approval: [Attempt $attempts/10] Not propagated on gateway yet (404)...');
        }
      }

      if (finalState == PreApprovalResultStatus.pending) {
        setState(() {
          _preAppTxnState = '✗ Polling Timeout';
        });
        _logTerminal('Pre-Approval: ✗ Polling timeout. No response received.');
      }
    } on MtnMomoException catch (e) {
      setState(() {
        _preAppTxnState = 'Exception: ${e.message}';
      });
      _logTerminal('Pre-Approval: ✗ Exception: ${e.message}');
    } catch (e) {
      setState(() {
        _preAppTxnState = 'Error: $e';
      });
      _logTerminal('Pre-Approval: ✗ Error: $e');
    } finally {
      setState(() {
        _isPreAppRunning = false;
      });
    }
  }

  /// Advanced Flow 2: Disbursements Refund
  Future<void> _runRefundFlow() async {
    if (!_initializeClient()) return;

    final transferId = _refundTransferIdController.text.trim();
    final amount = _refundAmountController.text.trim();

    if (transferId.isEmpty || amount.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
            content: Text(
                'Please specify the original Transfer Reference ID and refund amount.')),
      );
      return;
    }

    setState(() {
      _isRefundRunning = true;
      _refundTxnState = 'Initializing refund...';
    });

    try {
      final refundRefId = const Uuid().v4();
      _logTerminal(
          'Refund: Initiating refund request ($refundRefId) for Transfer ID: $transferId...');

      final refund = Refund(
        amount: amount,
        currency: 'EUR',
        externalId: 'TXN_REFUND_${refundRefId.substring(0, 8)}',
        referenceIdToRefund: transferId,
        payerMessage: 'Refunding salary payment error',
        payeeNote: 'Refunding payout via Playground',
      );

      await _disbursementsMomo!.disbursements.refundV1(
        xReferenceId: refundRefId,
        body: refund,
      );
      _logTerminal('Refund: ✓ Refund request accepted. Polling status...');

      RefundResultStatus? finalState;
      var attempts = 0;

      while (attempts < 10) {
        attempts++;
        setState(() {
          _refundTxnState = 'Polling status (Attempt $attempts/10)';
        });
        await Future.delayed(const Duration(seconds: 2));

        try {
          final status = await _disbursementsMomo!.disbursements
              .getRefundStatus(referenceId: refundRefId);
          finalState = status.status;
          _logTerminal('Refund: Current status: $finalState');

          if (finalState == RefundResultStatus.successful) {
            setState(() {
              _refundTxnState = '✓ Refund Successful';
            });
            _logTerminal('Refund: ✓ Refund completed SUCCESSFUL!');
            break;
          } else if (finalState == RefundResultStatus.failed) {
            setState(() {
              _refundTxnState = '✗ Refund Failed: ${status.reason?.code}';
            });
            _logTerminal(
                'Refund: ✗ Refund FAILED. Reason: ${status.reason?.code}');
            break;
          }
        } on MtnMomoNotFoundException catch (_) {
          _logTerminal(
              'Refund: [Attempt $attempts/10] Not propagated on gateway yet (404)...');
        }
      }

      if (finalState == RefundResultStatus.pending) {
        setState(() {
          _refundTxnState = '✗ Polling Timeout';
        });
        _logTerminal('Refund: ✗ Polling timeout. Gateway took too long.');
      }
    } on MtnMomoException catch (e) {
      setState(() {
        _refundTxnState = 'Exception: ${e.message}';
      });
      _logTerminal('Refund: ✗ Exception: ${e.message}');
    } catch (e) {
      setState(() {
        _refundTxnState = 'Error: $e';
      });
      _logTerminal('Refund: ✗ Error: $e');
    } finally {
      setState(() {
        _isRefundRunning = false;
      });
      _fetchDisbursementsBalance();
    }
  }

  /// Advanced Flow 3: Sandbox Use Cases Simulator
  Future<void> _runSandboxSimulatorFlow() async {
    if (!_initializeClient()) return;

    setState(() {
      _isSimulatorRunning = true;
      _simulatorState = 'Running Simulation...';
    });

    _logTerminal('Simulator: starting full sandbox test cases execution...');

    try {
      // 1. Success Case
      await _simulateCollect('256772123456', 'Success Case (Standard MSISDN)');

      // 2. Account Not Found
      await _simulateValidation('46733123450', 'Account Holder Not Found');

      // 3. Account Holder Inactive (Expired)
      await _simulateCollect(
          '46733123451', 'Account Holder Not Active (Expired Status)');

      // 4. Operation Not Allowed
      await _simulateValidation('46733123452', 'Operation Not Allowed');

      // 5. Target Environment Forbidden
      await _simulateCollect('46733123453', 'Target Environment Forbidden');

      // 6. Internal Processing Error
      await _simulateCollect('46733123454', 'Internal Processing Error Status');

      setState(() {
        _simulatorState = '✓ Completed';
      });
      _logTerminal(
          'Simulator: All simulation test cases completed successfully.');
    } catch (e) {
      setState(() {
        _simulatorState = '✗ Simulation Error';
      });
      _logTerminal('Simulator: ✗ Unexpected Simulator Error: $e');
    } finally {
      setState(() {
        _isSimulatorRunning = false;
      });
    }
  }

  Future<void> _simulateValidation(String msisdn, String label) async {
    _logTerminal('Simulator: Running validation: $label (MSISDN: $msisdn)...');
    try {
      await _collectionsMomo!.collection.validateAccountHolderStatus(
        accountHolderId: msisdn,
        accountHolderIdType: 'msisdn',
      );
      _logTerminal('Simulator: ✓ Active account status verified.');
    } on MtnMomoException catch (e) {
      _logTerminal(
          'Simulator: Caught expected exception: ${e.runtimeType} -> ${e.message}');
    }
  }

  Future<void> _simulateCollect(String msisdn, String label) async {
    _logTerminal('Simulator: Running collection: $label (MSISDN: $msisdn)...');
    final referenceId = const Uuid().v4();

    try {
      await _collectionsMomo!.collection.requesttoPay(
        xReferenceId: referenceId,
        body: RequestToPay(
          amount: '1000',
          currency: 'EUR',
          externalId: 'TXN_SIM_${referenceId.substring(0, 8)}',
          payer: Party(
            partyIdType: PartyPartyIdType.msisdn,
            partyId: msisdn,
          ),
          payerMessage: 'Simulation of $label',
          payeeNote: 'Sandbox simulation',
        ),
      );
      _logTerminal('Simulator: Request submitted. Polling...');

      RequestToPayResultStatus? state;
      var attempts = 0;

      while (attempts < 10) {
        attempts++;
        await Future.delayed(const Duration(milliseconds: 1500));
        try {
          final status = await _collectionsMomo!.collection
              .requesttoPayTransactionStatus(referenceId: referenceId);
          state = status.status;
          _logTerminal(
              'Simulator:   [Poll $attempts/10] Current status: $state');

          if (state == RequestToPayResultStatus.successful) {
            _logTerminal('Simulator:   ✓ Success!');
            break;
          } else if (state == RequestToPayResultStatus.failed) {
            _logTerminal(
                'Simulator:   ✗ Failed. Reason: ${status.reason?.code}');
            break;
          }
        } on MtnMomoNotFoundException catch (_) {
          _logTerminal(
              'Simulator:   [Poll $attempts/10] Not propagated yet (404)...');
        }
      }
    } on MtnMomoException catch (e) {
      _logTerminal(
          'Simulator: Caught expected initialization exception: ${e.runtimeType} -> ${e.message}');
    } catch (e) {
      _logTerminal('Simulator: Unexpected error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;

    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: const Color(0xFF004F71), // Solid MoMo Blue
          foregroundColor: Colors.white,
          elevation: 0,
          title: Row(
            children: [
              Container(
                padding: const EdgeInsets.all(6),
                decoration: const BoxDecoration(
                  color: Color(0xFFFFCB05), // MTN Sunshine Yellow
                  shape: BoxShape.circle,
                ),
                child: const Icon(Icons.wallet,
                    color: Color(0xFF004F71), size: 20),
              ),
              const SizedBox(width: 10),
              const Text(
                'MTN MoMo SDK Playground',
                style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 18,
                    color: Colors.white),
              ),
            ],
          ),
          actions: [
            IconButton(
              icon: const Icon(Icons.delete_outline, color: Colors.white),
              tooltip: 'Clear Logs',
              onPressed: () {
                setState(() {
                  _terminalLogs.clear();
                });
              },
            ),
          ],
          bottom: const TabBar(
            indicatorColor: Color(0xFFFFCB05),
            labelColor: Color(0xFFFFCB05),
            unselectedLabelColor: Colors.white70,
            tabs: [
              Tab(icon: Icon(Icons.star), text: 'Core APIs'),
              Tab(icon: Icon(Icons.extension), text: 'Advanced'),
              Tab(icon: Icon(Icons.bug_report), text: 'Simulator'),
            ],
          ),
        ),
        body: Column(
          children: [
            Expanded(
              child: TabBarView(
                children: [
                  // Tab 1: Core APIs
                  SingleChildScrollView(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        _buildSandboxCredentialsCard(isDark),
                        const SizedBox(height: 16),
                        _buildCollectionsCard(isDark),
                        const SizedBox(height: 16),
                        _buildDisbursementsCard(isDark),
                      ],
                    ),
                  ),
                  // Tab 2: Advanced
                  SingleChildScrollView(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        _buildPreApprovalCard(isDark),
                        const SizedBox(height: 16),
                        _buildRefundCard(isDark),
                      ],
                    ),
                  ),
                  // Tab 3: Simulator
                  SingleChildScrollView(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        _buildSimulatorCard(isDark),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            _buildTerminalConsole(isDark),
          ],
        ),
      ),
    );
  }

  Widget _buildPreApprovalCard(bool isDark) {
    final credentialsValid =
        _userIdController.text.isNotEmpty && _apiKeyController.text.isNotEmpty;

    return Opacity(
      opacity: credentialsValid ? 1.0 : 0.5,
      child: AbsorbPointer(
        absorbing: !credentialsValid,
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Row(
                  children: [
                    const Icon(Icons.lock_person, color: Color(0xFF004F71)),
                    const SizedBox(width: 8),
                    Text(
                      'Collections Pre-Approval',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: isDark ? Colors.white : Colors.black,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(
                      flex: 3,
                      child: TextField(
                        controller: _preAppMsisdnController,
                        decoration: const InputDecoration(
                            labelText: 'Payer Phone (MSISDN)'),
                        keyboardType: TextInputType.phone,
                      ),
                    ),
                    const SizedBox(width: 10),
                    Expanded(
                      flex: 2,
                      child: TextField(
                        controller: _preAppValidityController,
                        decoration:
                            const InputDecoration(labelText: 'Validity (sec)'),
                        keyboardType: TextInputType.number,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                ElevatedButton(
                  onPressed: _isPreAppRunning ? null : _runPreApprovalFlow,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF004F71),
                    foregroundColor: Colors.white,
                    disabledBackgroundColor: isDark
                        ? const Color(0xFF143D56)
                        : const Color(0xFFE5E5E5),
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                    elevation: 0,
                  ),
                  child: _isPreAppRunning
                      ? const SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Colors.white,
                          ),
                        )
                      : const Text(
                          'Request Payment Consent',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                ),
                const SizedBox(height: 8),
                Center(
                  child: Text(
                    _preAppTxnState,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 12,
                      color: isDark ? Colors.grey[400] : Colors.grey[700],
                      fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildRefundCard(bool isDark) {
    final credentialsValid =
        _userIdController.text.isNotEmpty && _apiKeyController.text.isNotEmpty;

    return Opacity(
      opacity: credentialsValid ? 1.0 : 0.5,
      child: AbsorbPointer(
        absorbing: !credentialsValid,
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Row(
                  children: [
                    const Icon(Icons.settings_backup_restore,
                        color: Color(0xFF004F71)),
                    const SizedBox(width: 8),
                    Text(
                      'Disbursements Refund',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: isDark ? Colors.white : Colors.black,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                TextField(
                  controller: _refundTransferIdController,
                  decoration: const InputDecoration(
                    labelText: 'Original Transfer Reference ID (UUID)',
                    hintText: 'Enter transfer UUID to refund...',
                  ),
                ),
                const SizedBox(height: 12),
                TextField(
                  controller: _refundAmountController,
                  decoration: const InputDecoration(
                    labelText: 'Refund Amount (EUR)',
                  ),
                  keyboardType: TextInputType.number,
                ),
                const SizedBox(height: 12),
                ElevatedButton(
                  onPressed: _isRefundRunning ? null : _runRefundFlow,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF004F71),
                    foregroundColor: Colors.white,
                    disabledBackgroundColor: isDark
                        ? const Color(0xFF143D56)
                        : const Color(0xFFE5E5E5),
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                    elevation: 0,
                  ),
                  child: _isRefundRunning
                      ? const SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Colors.white,
                          ),
                        )
                      : const Text(
                          'Initiate Payout Refund',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                ),
                const SizedBox(height: 8),
                Center(
                  child: Text(
                    _refundTxnState,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 12,
                      color: isDark ? Colors.grey[400] : Colors.grey[700],
                      fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildSimulatorCard(bool isDark) {
    final credentialsValid =
        _userIdController.text.isNotEmpty && _apiKeyController.text.isNotEmpty;

    return Opacity(
      opacity: credentialsValid ? 1.0 : 0.5,
      child: AbsorbPointer(
        absorbing: !credentialsValid,
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Row(
                  children: [
                    const Icon(Icons.play_circle_outline,
                        color: Color(0xFF004F71)),
                    const SizedBox(width: 8),
                    Text(
                      'Predefined Sandbox Use Cases Simulator',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: isDark ? Colors.white : Colors.black,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Text(
                  'Runs a sequential suite of 6 standard sandbox test scenarios using official MSISDN configurations (Success, Bad Wallet, Inactive User, Permissions Error, Forbidden Env, Server Fault). Results stream live into the scrolling Console below.',
                  style: TextStyle(
                    fontSize: 13,
                    color: isDark ? Colors.grey[300] : Colors.grey[800],
                  ),
                ),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed:
                      _isSimulatorRunning ? null : _runSandboxSimulatorFlow,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFFFFCB05),
                    foregroundColor: Colors.black,
                    disabledBackgroundColor: isDark
                        ? const Color(0xFF143D56)
                        : const Color(0xFFE5E5E5),
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                    elevation: 0,
                  ),
                  child: _isSimulatorRunning
                      ? const SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Colors.black,
                          ),
                        )
                      : const Text(
                          'Run Use Cases Simulator',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                ),
                const SizedBox(height: 8),
                Center(
                  child: Text(
                    _simulatorState,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 12,
                      color: isDark ? Colors.grey[400] : Colors.grey[700],
                      fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildSandboxCredentialsCard(bool isDark) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                const Icon(Icons.settings,
                    color: Color(0xFF004F71)), // MoMo Blue
                const SizedBox(width: 8),
                Text(
                  '1. Sandbox Credentials',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: isDark ? Colors.white : Colors.black,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _subKeyController,
              decoration: const InputDecoration(
                labelText: 'Subscription Key',
                hintText: 'Enter subscription key...',
                helperText:
                    'Get this Primary/Secondary Subscription Key from your profile at https://momodeveloper.mtn.com/',
                helperMaxLines: 2,
              ),
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: _isProvisioning ? null : _provisionSandbox,
                    style: ElevatedButton.styleFrom(
                      backgroundColor:
                          const Color(0xFFFFCB05), // MTN Sunshine Yellow
                      foregroundColor: Colors.black,
                      disabledBackgroundColor: isDark
                          ? const Color(0xFF143D56)
                          : const Color(0xFFE5E5E5),
                      padding: const EdgeInsets.symmetric(vertical: 14),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8),
                      ),
                      elevation: 0,
                    ),
                    child: _isProvisioning
                        ? const SizedBox(
                            width: 20,
                            height: 20,
                            child: CircularProgressIndicator(
                              strokeWidth: 2,
                              color: Colors.black,
                            ),
                          )
                        : const Text(
                            'Provision Sandbox User',
                            style: TextStyle(fontWeight: FontWeight.bold),
                          ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _userIdController,
              readOnly: true,
              decoration: InputDecoration(
                labelText: 'Generated User ID (API User)',
                suffixIcon: IconButton(
                  icon: const Icon(Icons.copy, size: 18),
                  onPressed: () {
                    Clipboard.setData(
                        ClipboardData(text: _userIdController.text));
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('User ID copied!')),
                    );
                  },
                ),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _apiKeyController,
              readOnly: true,
              decoration: InputDecoration(
                labelText: 'Generated API Key',
                suffixIcon: IconButton(
                  icon: const Icon(Icons.copy, size: 18),
                  onPressed: () {
                    Clipboard.setData(
                        ClipboardData(text: _apiKeyController.text));
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('API Key copied!')),
                    );
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildCollectionsCard(bool isDark) {
    final credentialsValid =
        _userIdController.text.isNotEmpty && _apiKeyController.text.isNotEmpty;

    return Opacity(
      opacity: credentialsValid ? 1.0 : 0.5,
      child: AbsorbPointer(
        absorbing: !credentialsValid,
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Row(
                      children: [
                        const Icon(Icons.arrow_downward,
                            color: Color(0xFF004F71)), // MoMo Blue
                        const SizedBox(width: 8),
                        Text(
                          '2. Collections API',
                          style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold,
                            color: isDark ? Colors.white : Colors.black,
                          ),
                        ),
                      ],
                    ),
                    TextButton.icon(
                      onPressed: _fetchCollectionsBalance,
                      icon: const Icon(Icons.refresh,
                          size: 16, color: Color(0xFF004F71)),
                      label: Text(
                        'Bal: $_colBalance',
                        style: const TextStyle(
                          color: Color(0xFF004F71), // MoMo Blue
                          fontWeight: FontWeight.bold,
                          fontSize: 13,
                        ),
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(
                      flex: 3,
                      child: TextField(
                        controller: _collectMsisdnController,
                        decoration: const InputDecoration(
                            labelText: 'Customer Phone (MSISDN)'),
                        keyboardType: TextInputType.phone,
                      ),
                    ),
                    const SizedBox(width: 10),
                    Expanded(
                      flex: 2,
                      child: TextField(
                        controller: _collectAmountController,
                        decoration:
                            const InputDecoration(labelText: 'Amount (EUR)'),
                        keyboardType: TextInputType.number,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                ElevatedButton(
                  onPressed: _isCollectionsRunning ? null : _runCollectionsFlow,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF004F71), // Solid MoMo Blue
                    foregroundColor: Colors.white,
                    disabledBackgroundColor: isDark
                        ? const Color(0xFF143D56)
                        : const Color(0xFFE5E5E5),
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                    elevation: 0,
                  ),
                  child: _isCollectionsRunning
                      ? const SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Colors.white,
                          ),
                        )
                      : const Text(
                          'Request Payment (USSD Push)',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                ),
                const SizedBox(height: 8),
                Center(
                  child: Text(
                    _colTxnState,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 12,
                      color: isDark ? Colors.grey[400] : Colors.grey[700],
                      fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildDisbursementsCard(bool isDark) {
    final credentialsValid =
        _userIdController.text.isNotEmpty && _apiKeyController.text.isNotEmpty;

    return Opacity(
      opacity: credentialsValid ? 1.0 : 0.5,
      child: AbsorbPointer(
        absorbing: !credentialsValid,
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Row(
                      children: [
                        const Icon(Icons.arrow_upward,
                            color: Color(0xFF004F71)), // MoMo Blue
                        const SizedBox(width: 8),
                        Text(
                          '3. Disbursements API',
                          style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold,
                            color: isDark ? Colors.white : Colors.black,
                          ),
                        ),
                      ],
                    ),
                    TextButton.icon(
                      onPressed: _fetchDisbursementsBalance,
                      icon: const Icon(Icons.refresh,
                          size: 16, color: Color(0xFF004F71)),
                      label: Text(
                        'Bal: $_disbBalance',
                        style: const TextStyle(
                          color: Color(0xFF004F71), // MoMo Blue
                          fontWeight: FontWeight.bold,
                          fontSize: 13,
                        ),
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Row(
                  children: [
                    Expanded(
                      flex: 3,
                      child: TextField(
                        controller: _disbMsisdnController,
                        decoration: const InputDecoration(
                            labelText: 'Payee Phone (MSISDN)'),
                        keyboardType: TextInputType.phone,
                      ),
                    ),
                    const SizedBox(width: 10),
                    Expanded(
                      flex: 2,
                      child: TextField(
                        controller: _disbAmountController,
                        decoration:
                            const InputDecoration(labelText: 'Amount (EUR)'),
                        keyboardType: TextInputType.number,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                ElevatedButton(
                  onPressed:
                      _isDisbursementsRunning ? null : _runDisbursementsFlow,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF004F71), // Solid MoMo Blue
                    foregroundColor: Colors.white,
                    disabledBackgroundColor: isDark
                        ? const Color(0xFF143D56)
                        : const Color(0xFFE5E5E5),
                    padding: const EdgeInsets.symmetric(vertical: 14),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                    elevation: 0,
                  ),
                  child: _isDisbursementsRunning
                      ? const SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(
                            strokeWidth: 2,
                            color: Colors.white,
                          ),
                        )
                      : const Text(
                          'Disburse Funds (Transfer)',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                ),
                const SizedBox(height: 8),
                Center(
                  child: Text(
                    _disbTxnState,
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 12,
                      color: isDark ? Colors.grey[400] : Colors.grey[700],
                      fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildTerminalConsole(bool isDark) {
    return Container(
      height: 200,
      width: double.infinity,
      color: Colors.black,
      padding: const EdgeInsets.all(8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Row(
                children: [
                  Icon(Icons.terminal,
                      color: Color(0xFFFFCB05),
                      size: 14), // MTN Sunshine Yellow
                  SizedBox(width: 6),
                  Text(
                    'LIVE SDK TERMINAL LOGS',
                    style: TextStyle(
                      color: Color(0xFFFFCB05),
                      fontSize: 10,
                      fontWeight: FontWeight.bold,
                      letterSpacing: 1.2,
                    ),
                  ),
                ],
              ),
              GestureDetector(
                onTap: () {
                  setState(() {
                    _terminalLogs.clear();
                  });
                },
                child: const Text(
                  'CLEAR',
                  style: TextStyle(
                    color: Colors.grey,
                    fontSize: 10,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ],
          ),
          const Divider(color: Color(0xFF2C2C2C), height: 10, thickness: 1),
          Expanded(
            child: ListView.builder(
              controller: _terminalScrollController,
              itemCount: _terminalLogs.length,
              physics: const ClampingScrollPhysics(),
              itemBuilder: (context, index) {
                final logText = _terminalLogs[index];
                Color textColor = Colors.greenAccent;
                if (logText.contains('✗') ||
                    logText.contains('Error') ||
                    logText.contains('Exception')) {
                  textColor = Colors.redAccent;
                } else if (logText.contains('✓')) {
                  textColor = const Color(0xFFFFCB05); // MTN Sunshine Yellow
                } else if (logText.contains('System:')) {
                  textColor = Colors.white70;
                }
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 2.0),
                  child: Text(
                    logText,
                    style: TextStyle(
                      color: textColor,
                      fontFamily: 'Courier',
                      fontSize: 11,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
30
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

An unofficial package for integrating with MTN Mobile Money Collections & Disbursements & Remittances API, enabling the payment of goods and services using MTN Mobile Money.

Repository (GitHub)
View/report issues

Topics

#mtn-momo #mobile-money #payments

License

MIT (license)

Dependencies

dart_mappable, dio, retrofit, uuid

More

Packages that depend on mtn_momo_sdk