import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:qpay/models/responsive_policy.dart'; import 'package:qpay/screens/pay/pay_controller.dart'; import 'package:qpay/screens/transaction_controller.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:skeletonizer/skeletonizer.dart'; class PayScreen extends StatefulWidget { const PayScreen({super.key}); @override State createState() => _PayScreenState(); } class _PayScreenState extends State with TickerProviderStateMixin { late AnimationController _controller; late Animation _fadeAnimation; late Animation _slideAnimation; late PayController controller; late TransactionController transactionController; late SharedPreferences prefs; final List> _animations = []; final _formKey = GlobalKey(); final _amountController = TextEditingController(); final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _phoneController = TextEditingController(); bool showAdditionalRecipientDetails = false; String selectedCountryCode = '+263'; // Default to ZW // Common country codes with flags and names final List> countryCodes = [ {'code': '+263', 'name': 'ZW', 'flag': '🇿🇼'}, {'code': '+27', 'name': 'ZA', 'flag': '🇿🇦'}, {'code': '+1', 'name': 'US', 'flag': '🇺🇸'}, {'code': '+44', 'name': 'UK', 'flag': '🇬🇧'}, {'code': '+61', 'name': 'AU', 'flag': '🇦🇺'}, {'code': '+91', 'name': 'IN', 'flag': '🇮🇳'}, {'code': '+86', 'name': 'CN', 'flag': '🇨🇳'}, {'code': '+81', 'name': 'JP', 'flag': '🇯🇵'}, {'code': '+49', 'name': 'DE', 'flag': '🇩🇪'}, {'code': '+33', 'name': 'FR', 'flag': '🇫🇷'}, {'code': '+39', 'name': 'IT', 'flag': '🇮🇹'}, {'code': '+34', 'name': 'ES', 'flag': '🇪🇸'}, {'code': '+7', 'name': 'RU', 'flag': '🇷🇺'}, {'code': '+55', 'name': 'BR', 'flag': '🇧🇷'}, {'code': '+52', 'name': 'MX', 'flag': '🇲🇽'}, {'code': '+971', 'name': 'AE', 'flag': '🇦🇪'}, {'code': '+966', 'name': 'SA', 'flag': '🇸🇦'}, {'code': '+234', 'name': 'NG', 'flag': '🇳🇬'}, {'code': '+254', 'name': 'KE', 'flag': '🇰🇪'}, {'code': '+233', 'name': 'GH', 'flag': '🇬🇭'}, {'code': '+256', 'name': 'UG', 'flag': '🇺🇬'}, ]; @override void initState() { super.initState(); controller = PayController(context); controller.getPaymentProcessors(); controller.getProducts(controller.model.selectedProvider?.uid ?? ""); transactionController = Provider.of( context, listen: false, ); _setupData(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.0, 0.5, curve: Curves.easeOut), ), ); _slideAnimation = Tween(begin: const Offset(0, 0.1), end: Offset.zero).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.0, 0.5, curve: Curves.easeOut), ), ); List buttonIndices = [0, 1]; for (int i = 0; i < buttonIndices.length; i++) { _animations.add( Tween(begin: Offset(-0.15, 0), end: Offset.zero).animate( CurvedAnimation( parent: _controller, curve: Interval(0.055 * i, 1.0, curve: Curves.easeOut), ), ), ); } _controller.forward(); } _setupData() async { prefs = await SharedPreferences.getInstance(); if (prefs.containsKey('phone')) { updatePhoneNumber(prefs.getString('phone')!); } _nameController.text = transactionController.model.formData.creditName ?? ''; _emailController.text = transactionController.model.formData.creditEmail ?? ''; _amountController.text = transactionController.model.formData.amount; _phoneController.text = transactionController.model.formData.debitPhone ?? ''; } Future updatePhoneNumber(String newNumber) async { for (var country in countryCodes) { if (newNumber.startsWith(country['code']!)) { setState(() { selectedCountryCode = country['code']!; }); _phoneController.text = newNumber.substring(country['code']!.length); break; } } } Future _submitPayment(PaymentProcessor e, BuildContext context) async { controller.updateAmount(_amountController.text); if (!controller.model.selectedProvider!.requiresAmount) { controller.updateAmount( controller.model.selectedProduct!.defaultAmount.toString(), ); } String fullPhoneNumber = _phoneController.text; controller.updatePhone(fullPhoneNumber); if (showAdditionalRecipientDetails) { controller.updateAdditionalRecipientDetails( _nameController.text, _emailController.text, ); } controller.updateSelectedPaymentProcessor(e); transactionController.updateSelectedPaymentProcessor(e); final apiResponse = await controller.doTransaction(); if (!apiResponse.isSuccess) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( controller.model.errorMessage ?? apiResponse.error ?? "Transaction failed", ), behavior: SnackBarBehavior.floating, width: 600, backgroundColor: Colors.deepOrange, ), ); } } else if (controller.model.status == "SUCCESS" && context.mounted) { context.push('/confirm'); } } @override void dispose() { _controller.dispose(); _amountController.dispose(); _nameController.dispose(); _emailController.dispose(); _phoneController.dispose(); controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: context.canPop() ? IconButton( onPressed: () { context.pop(); }, icon: const Icon(Icons.arrow_back), ) : SizedBox.shrink(), title: const Text( 'Make Payment', style: TextStyle(color: Colors.black87), ), ), body: ListenableBuilder( listenable: controller, builder: (context, child) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(20.0), child: FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: Form( key: _formKey, child: Center( child: LayoutBuilder( builder: (context, constraints) { return SizedBox( width: double.parse( constraints.maxWidth > ResponsivePolicy.md ? ResponsivePolicy.md.toString() : constraints.maxWidth.toString(), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTransactionDetailsCard( children: [ _buildDetailRow( icon: Icons.business_outlined, label: "Provider", value: controller .model .selectedProvider ?.name ?? "", ), _buildDetailRow( icon: Icons.account_balance_outlined, label: controller .model .selectedProvider ?.accountFieldName ?? "", value: transactionController .model .formData .creditAccount, ), ], ), SizedBox(height: 20), InkWell( customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), onTap: () { setState(() { showAdditionalRecipientDetails = !showAdditionalRecipientDetails; }); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: () {}, icon: Icon( showAdditionalRecipientDetails ? Icons.arrow_drop_up : Icons.arrow_drop_down, ), color: Theme.of( context, ).colorScheme.primary, ), Text( showAdditionalRecipientDetails ? "Hide additional recipient details" : "Show additional recipient details", style: TextStyle(fontSize: 16), ), ], ), ), if (showAdditionalRecipientDetails) _buildAdditionalRecipientDetails(), const SizedBox(height: 7), if (controller.model.products.isNotEmpty) _buildBillProductSelector(controller), const SizedBox(height: 7), _buildPhoneField(), const SizedBox(height: 7), _buildAmountField(), const SizedBox(height: 20), ...controller.model.paymentProcessors.map( (e) => _buildPaymentProcessorItem(e, context), ), ], ), ); }, ), ), ), ), ), ), ); }, ), ); } Widget _buildPhoneField() { if (controller.model.selectedProvider?.requiresPhone == false) { return SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Debit Phone Number', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), SizedBox(width: 5), const Text( '(to be charged or notified)', style: TextStyle(fontSize: 12, color: Colors.grey), ), ], ), if (!kIsWeb) TextButton( onPressed: () async { if (await FlutterContacts.requestPermission()) { final contact = await FlutterContacts.openExternalPick(); if (contact != null) { updatePhoneNumber(contact.phones.first.normalizedNumber); } } }, child: Text( "Fetch from contacts", style: TextStyle(fontSize: 12, color: Colors.red), ), ), ], ), const SizedBox(height: 5), Row( children: [ // Country code dropdown Container( width: 100, decoration: BoxDecoration( border: Border.all( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.2), ), borderRadius: BorderRadius.circular(12), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: selectedCountryCode, isExpanded: true, icon: const Icon(Icons.arrow_drop_down), padding: const EdgeInsets.symmetric(horizontal: 8), items: countryCodes.map((country) { return DropdownMenuItem( value: country['code'], child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(country['flag']!), const SizedBox(width: 4), Text( country['code']!, style: const TextStyle(fontSize: 14), ), ], ), ); }).toList(), onChanged: (String? newValue) { if (newValue != null) { setState(() { selectedCountryCode = newValue; }); } }, ), ), ), const SizedBox(width: 8), // Phone number field Expanded( child: TextFormField( controller: _phoneController, keyboardType: TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( hintText: 'Enter phone number', focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of(context).colorScheme.secondary, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.2), ), ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a phone number'; } String digitsOnly = value.replaceAll(RegExp(r'[^\d]'), ''); if (digitsOnly.length < 7) { return 'Phone number must be at least 7 digits'; } if (digitsOnly.length > 15) { return 'Phone number is too long'; } return null; }, ), ), ], ), ], ); } Widget _buildAmountField() { if (controller.model.selectedProvider?.requiresAmount == false) { return SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Amount', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 5), TextFormField( controller: _amountController, keyboardType: TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( hintText: 'Enter amount', focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of(context).colorScheme.secondary, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.2), ), ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter an amount'; } if (double.tryParse(value) == null) { return 'Please enter a valid amount'; } return null; }, ), ], ); } Widget _buildAdditionalRecipientDetails() { return SlideTransition( position: _slideAnimation, child: Column( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Name', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 5), TextFormField( controller: _nameController, keyboardType: TextInputType.text, decoration: InputDecoration( hintText: 'Enter recipient\'s name', focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of(context).colorScheme.secondary, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.2), ), ), ), ), ], ), const SizedBox(height: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Email', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 5), TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( hintText: 'Enter recipient\'s email', focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of(context).colorScheme.secondary, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.2), ), ), ), ), ], ), const SizedBox(height: 10), Divider(color: Theme.of(context).colorScheme.primary), ], ), ); } Widget _buildBillProductSelector(PayController controller) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 20), const Text( 'Provider Product', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), const SizedBox(height: 5), Skeletonizer( enabled: controller.model.isLoading, child: DropdownButtonHideUnderline( child: DropdownButtonFormField( initialValue: controller.model.selectedProduct?.uid, isExpanded: true, icon: const Icon(Icons.arrow_drop_down), decoration: InputDecoration( hintText: 'Select Product', enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.2), ), ), ), items: [ DropdownMenuItem(child: Text("Select Product")), ...controller.model.products.map( (e) => DropdownMenuItem( value: e.uid, child: Text( '${e.displayName} - ' '${transactionController.model.formData.debitCurrency}${e.defaultAmount}', ), ), ), ], onChanged: (String? newValue) { if (newValue != null) { BillProduct product = controller.model.products.firstWhere( (e) => e.uid == newValue, ); controller.updateSelectedProduct(product); } }, validator: (value) { if (value == null || value.isEmpty) { return 'Please select a product'; } return null; }, ), ), ), ], ); } Widget _buildTransactionDetailsCard({required List children}) { return Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade200, width: 1), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.receipt_long_outlined, size: 18, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Text( "TRANSACTION DETAILS", style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Theme.of(context).colorScheme.primary, letterSpacing: 1.0, ), ), ], ), const SizedBox(height: 16), Divider(height: 1, color: Colors.grey.shade200), const SizedBox(height: 16), ...children, ], ), ), ); } Widget _buildDetailRow({ required IconData icon, required String label, required String value, }) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Icon(icon, size: 16, color: Colors.grey.shade500), const SizedBox(width: 10), Text( label, style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), const Spacer(), Text( value, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ), ); } Widget _buildPaymentProcessorItem(PaymentProcessor e, BuildContext context) { return Column( children: [ Skeletonizer( enabled: controller.model.isLoading, child: SlideTransition( position: _animations[controller.model.paymentProcessors.indexOf(e)], child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.black87.withAlpha(20)), borderRadius: BorderRadius.circular(12), gradient: LinearGradient( colors: [ Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.1), Theme.of( context, ).colorScheme.tertiary.withValues(alpha: 0.05), ], stops: const [0.3, 0.7], begin: Alignment.topRight, end: Alignment.bottomLeft, ), ), child: InkWell( customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), onTap: () async { if (_formKey.currentState!.validate()) { if (controller.model.isLoading) return; await _submitPayment(e, context); } }, child: ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Image(image: AssetImage("assets/${e.image}")), ), title: Text( e.name, style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( e.description, style: TextStyle( fontWeight: FontWeight.normal, fontSize: 10, ), ), trailing: Icon( Icons.chevron_right, color: Theme.of( context, ).colorScheme.secondary.withValues(alpha: 0.7), ), ), ), ), ), ), SizedBox(height: 10), ], ); } }