import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/models/cotisation_model.dart'; import '../../../../core/models/payment_model.dart'; import '../../../../core/models/wave_checkout_session_model.dart'; import '../../../../core/services/wave_payment_service.dart'; import '../../../../shared/theme/app_theme.dart'; import '../../../../shared/widgets/buttons/primary_button.dart'; import '../../../../shared/widgets/common/unified_page_layout.dart'; import '../bloc/cotisations_bloc.dart'; import '../bloc/cotisations_event.dart'; import '../bloc/cotisations_state.dart'; /// Page dédiée aux paiements Wave Money /// Interface moderne et sécurisée pour les paiements mobiles class WavePaymentPage extends StatefulWidget { final CotisationModel cotisation; const WavePaymentPage({ super.key, required this.cotisation, }); @override State createState() => _WavePaymentPageState(); } class _WavePaymentPageState extends State with TickerProviderStateMixin { late CotisationsBloc _cotisationsBloc; late WavePaymentService _wavePaymentService; late AnimationController _animationController; late AnimationController _pulseController; late Animation _fadeAnimation; late Animation _slideAnimation; late Animation _pulseAnimation; final _formKey = GlobalKey(); final _phoneController = TextEditingController(); final _nameController = TextEditingController(); final _emailController = TextEditingController(); bool _isProcessing = false; bool _termsAccepted = false; WaveCheckoutSessionModel? _currentSession; String? _paymentUrl; @override void initState() { super.initState(); _cotisationsBloc = getIt(); _wavePaymentService = getIt(); // Animations _animationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _pulseController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); _slideAnimation = Tween(begin: 50.0, end: 0.0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), ); _pulseAnimation = Tween(begin: 1.0, end: 1.1).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); _animationController.forward(); _pulseController.repeat(reverse: true); // Pré-remplir les champs si disponible _nameController.text = widget.cotisation.nomMembre; } @override void dispose() { _phoneController.dispose(); _nameController.dispose(); _emailController.dispose(); _animationController.dispose(); _pulseController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocProvider.value( value: _cotisationsBloc, child: UnifiedPageLayout( title: 'Paiement Wave Money', subtitle: 'Paiement sécurisé et instantané', showBackButton: true, backgroundColor: AppTheme.backgroundLight, child: BlocConsumer( listener: _handleBlocState, builder: (context, state) { return FadeTransition( opacity: _fadeAnimation, child: Transform.translate( offset: Offset(0, _slideAnimation.value), child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildWaveHeader(), const SizedBox(height: 24), _buildCotisationSummary(), const SizedBox(height: 24), _buildPaymentForm(), const SizedBox(height: 24), _buildSecurityInfo(), const SizedBox(height: 24), _buildPaymentButton(state), ], ), ), ), ), ); }, ), ), ); } Widget _buildWaveHeader() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF00D4FF), Color(0xFF0099CC)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: const Color(0xFF00D4FF).withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Row( children: [ ScaleTransition( scale: _pulseAnimation, child: Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: const Icon( Icons.waves, size: 32, color: Color(0xFF00D4FF), ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Wave Money', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 4), const Text( 'Paiement mobile sécurisé', style: TextStyle( fontSize: 14, color: Colors.white70, ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: const Text( '🇨🇮 Côte d\'Ivoire', style: TextStyle( fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ), ); } Widget _buildCotisationSummary() { final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye; final fees = _wavePaymentService.calculateWaveFees(remainingAmount); final total = remainingAmount + fees; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.borderLight), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Résumé de la cotisation', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 16), _buildSummaryRow('Type', widget.cotisation.typeCotisation), _buildSummaryRow('Membre', widget.cotisation.nomMembre), _buildSummaryRow('Référence', widget.cotisation.numeroReference), const Divider(height: 24), _buildSummaryRow('Montant', '${remainingAmount.toStringAsFixed(0)} XOF'), _buildSummaryRow('Frais Wave', '${fees.toStringAsFixed(0)} XOF'), const Divider(height: 24), _buildSummaryRow( 'Total à payer', '${total.toStringAsFixed(0)} XOF', isTotal: true, ), ], ), ); } Widget _buildSummaryRow(String label, String value, {bool isTotal = false}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: isTotal ? 16 : 14, fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, color: AppTheme.textSecondary, ), ), Text( value, style: TextStyle( fontSize: isTotal ? 16 : 14, fontWeight: FontWeight.bold, color: isTotal ? AppTheme.primaryColor : AppTheme.textPrimary, ), ), ], ), ); } Widget _buildPaymentForm() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.borderLight), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Informations de paiement', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 16), _buildPhoneField(), const SizedBox(height: 16), _buildNameField(), const SizedBox(height: 16), _buildEmailField(), const SizedBox(height: 16), _buildTermsCheckbox(), ], ), ); } Widget _buildPhoneField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Numéro Wave Money *', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), TextFormField( controller: _phoneController, keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], decoration: InputDecoration( hintText: '77 123 45 67', prefixIcon: const Icon(Icons.phone_android, color: Color(0xFF00D4FF)), prefixText: '+225 ', prefixStyle: const TextStyle( color: AppTheme.textSecondary, fontWeight: FontWeight.w500, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppTheme.borderLight), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00D4FF), width: 2), ), filled: true, fillColor: AppTheme.backgroundLight, ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez saisir votre numéro Wave Money'; } if (value.length < 8) { return 'Numéro invalide (minimum 8 chiffres)'; } return null; }, ), ], ); } Widget _buildNameField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Nom complet *', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), TextFormField( controller: _nameController, textCapitalization: TextCapitalization.words, decoration: InputDecoration( hintText: 'Votre nom complet', prefixIcon: const Icon(Icons.person, color: Color(0xFF00D4FF)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppTheme.borderLight), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00D4FF), width: 2), ), filled: true, fillColor: AppTheme.backgroundLight, ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Veuillez saisir votre nom complet'; } if (value.trim().length < 2) { return 'Le nom doit contenir au moins 2 caractères'; } return null; }, ), ], ); } Widget _buildEmailField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Email (optionnel)', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( hintText: 'votre.email@exemple.com', prefixIcon: const Icon(Icons.email, color: Color(0xFF00D4FF)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppTheme.borderLight), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: Color(0xFF00D4FF), width: 2), ), filled: true, fillColor: AppTheme.backgroundLight, ), validator: (value) { if (value != null && value.isNotEmpty) { if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Format d\'email invalide'; } } return null; }, ), ], ); } Widget _buildTermsCheckbox() { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Checkbox( value: _termsAccepted, onChanged: (value) { setState(() { _termsAccepted = value ?? false; }); }, activeColor: const Color(0xFF00D4FF), ), Expanded( child: GestureDetector( onTap: () { setState(() { _termsAccepted = !_termsAccepted; }); }, child: const Text( 'J\'accepte les conditions d\'utilisation de Wave Money et autorise le prélèvement du montant indiqué.', style: TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ), ), ], ); } Widget _buildSecurityInfo() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF0F9FF), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF00D4FF).withOpacity(0.2)), ), child: Column( children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFF00D4FF).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.security, color: Color(0xFF00D4FF), size: 20, ), ), const SizedBox(width: 12), const Expanded( child: Text( 'Paiement 100% sécurisé', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ), ], ), const SizedBox(height: 12), const Text( '• Chiffrement SSL/TLS de bout en bout\n' '• Conformité aux standards PCI DSS\n' '• Aucune donnée bancaire stockée\n' '• Transaction instantanée et traçable', style: TextStyle( fontSize: 12, color: AppTheme.textSecondary, height: 1.4, ), ), ], ), ); } Widget _buildPaymentButton(CotisationsState state) { final isLoading = state is PaymentInProgress || _isProcessing; final canPay = _formKey.currentState?.validate() == true && _termsAccepted && _phoneController.text.isNotEmpty && !isLoading; return SizedBox( width: double.infinity, child: PrimaryButton( text: isLoading ? 'Traitement en cours...' : 'Payer avec Wave Money', icon: isLoading ? null : Icons.waves, onPressed: canPay ? _processWavePayment : null, isLoading: isLoading, backgroundColor: const Color(0xFF00D4FF), ), ); } void _handleBlocState(BuildContext context, CotisationsState state) { if (state is PaymentSuccess) { _showPaymentSuccessDialog(state.payment); } else if (state is PaymentFailure) { _showPaymentErrorDialog(state.errorMessage); } } void _processWavePayment() async { if (!_formKey.currentState!.validate() || !_termsAccepted) { return; } setState(() { _isProcessing = true; }); try { final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye; // Initier le paiement Wave via le BLoC _cotisationsBloc.add(InitiatePayment( cotisationId: widget.cotisation.id, montant: remainingAmount, methodePaiement: 'WAVE', numeroTelephone: _phoneController.text.trim(), nomPayeur: _nameController.text.trim(), emailPayeur: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(), )); } catch (e) { setState(() { _isProcessing = false; }); _showPaymentErrorDialog('Erreur lors de l\'initiation du paiement: $e'); } } void _showPaymentSuccessDialog(PaymentModel payment) { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.check_circle, color: AppTheme.successColor, size: 28), SizedBox(width: 8), Text('Paiement réussi !'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Votre paiement de ${payment.montant.toStringAsFixed(0)} XOF a été confirmé.'), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.backgroundLight, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Référence: ${payment.numeroReference}'), Text('Transaction: ${payment.numeroTransaction ?? 'N/A'}'), Text('Date: ${DateTime.now().toString().substring(0, 16)}'), ], ), ), ], ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); // Retour à la liste }, child: const Text('Fermer'), ), ], ), ); } void _showPaymentErrorDialog(String errorMessage) { showDialog( context: context, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.error, color: AppTheme.errorColor, size: 28), SizedBox(width: 8), Text('Erreur de paiement'), ], ), content: Text(errorMessage), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ), ); } }