import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../bloc/onboarding_bloc.dart'; import '../../data/models/souscription_status_model.dart'; import '../../../../shared/design_system/tokens/unionflow_colors.dart'; import '../../../../shared/design_system/tokens/app_colors.dart'; import '../../../../core/config/environment.dart'; /// Étape 4 — Lancement du paiement Wave + attente du retour class WavePaymentPage extends StatefulWidget { final SouscriptionStatusModel souscription; final String waveLaunchUrl; const WavePaymentPage({ super.key, required this.souscription, required this.waveLaunchUrl, }); @override State createState() => _WavePaymentPageState(); } class _WavePaymentPageState extends State with WidgetsBindingObserver { bool _paymentLaunched = false; bool _appResumed = false; bool _simulating = false; /// En dev/mock, la session Wave ne peut pas s'ouvrir — on simule directement. bool get _isMock => widget.waveLaunchUrl.contains('mock') || widget.waveLaunchUrl.contains('localhost') || !AppConfig.isProd; // Couleur de marque Wave (volontairement hardcodée) static const _waveBlue = Color(0xFF00B9F1); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed && _paymentLaunched && !_appResumed) { _appResumed = true; context.read().add(const OnboardingRetourDepuisWave()); } } Future _lancerOuSimuler() async { if (_isMock) { // Mode dev/mock : simuler le paiement directement setState(() => _simulating = true); await Future.delayed(const Duration(milliseconds: 800)); if (mounted) { context.read().add(const OnboardingRetourDepuisWave()); } return; } // Mode prod : ouvrir Wave final uri = Uri.parse(widget.waveLaunchUrl); if (await canLaunchUrl(uri)) { setState(() => _paymentLaunched = true); await launchUrl(uri, mode: LaunchMode.externalApplication); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Impossible d\'ouvrir Wave. Vérifiez que l\'application est installée.'), backgroundColor: AppColors.error, behavior: SnackBarBehavior.floating, ), ); } } } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final bgSurface = isDark ? AppColors.surfaceDark : AppColors.surface; final borderColor = isDark ? AppColors.borderDark : AppColors.border; final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; final montant = widget.souscription.montantTotal ?? 0; return BlocListener( listener: (context, state) { // Afficher snackbar si la confirmation a échoué if (state is OnboardingPaiementEchoue) { setState(() { _paymentLaunched = false; _appResumed = false; _simulating = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row(children: [ const Icon(Icons.error_outline, color: Colors.white, size: 18), const SizedBox(width: 8), Expanded(child: Text(state.message)), ]), backgroundColor: AppColors.error, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 5), ), ); } }, child: Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( child: Padding( padding: const EdgeInsets.all(24), child: Column( children: [ // ── Top bar ─────────────────────────────────── Row( children: [ if (!_paymentLaunched && !_simulating) IconButton( onPressed: () => Navigator.of(context).maybePop(), icon: const Icon(Icons.arrow_back_rounded), color: textSecondary, ), const Spacer(), if (_isMock) _buildDevBadge() else _buildWaveBadge(), ], ), // ── Contenu principal ───────────────────────── Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_simulating) _buildSimulatingView(textPrimary, textSecondary) else _buildPaymentView( montant: montant, bgSurface: bgSurface, borderColor: borderColor, textPrimary: textPrimary, textSecondary: textSecondary, isDark: isDark, ), ], ), ), ], ), ), ), ), ); } // ─── Badges ──────────────────────────────────────────────── Widget _buildDevBadge() => Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppColors.warning.withOpacity(0.15), borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.warning.withOpacity(0.4)), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.science_rounded, size: 13, color: AppColors.warning), SizedBox(width: 4), Text( 'Mode dev', style: TextStyle( color: AppColors.warning, fontSize: 11, fontWeight: FontWeight.w700), ), ], ), ); Widget _buildWaveBadge() => Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: _waveBlue.withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: const Text( 'Wave Mobile Money', style: TextStyle( color: _waveBlue, fontSize: 12, fontWeight: FontWeight.w700), ), ); // ─── Vue simulation ─────────────────────────────────────── Widget _buildSimulatingView(Color textPrimary, Color textSecondary) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 110, height: 110, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF00B9F1), Color(0xFF0096C7)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(28), boxShadow: [ BoxShadow( color: _waveBlue.withOpacity(0.35), blurRadius: 24, offset: const Offset(0, 10), ), ], ), child: const Center( child: SizedBox( width: 48, height: 48, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3), ), ), ), const SizedBox(height: 28), Text( 'Simulation du paiement…', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: textPrimary), ), const SizedBox(height: 8), Text( 'Confirmation en cours auprès du serveur', style: TextStyle(color: textSecondary, fontSize: 14), ), ], ); } // ─── Vue paiement ───────────────────────────────────────── Widget _buildPaymentView({ required double montant, required Color bgSurface, required Color borderColor, required Color textPrimary, required Color textSecondary, required bool isDark, }) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Logo Wave — fond blanc intentionnel (brand) Container( width: 110, height: 110, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), border: Border.all(color: borderColor), boxShadow: [ BoxShadow( color: _waveBlue.withOpacity(0.2), blurRadius: 24, offset: const Offset(0, 10), ), ], ), padding: const EdgeInsets.all(16), child: Image.asset( 'assets/images/payment_methods/wave/logo.png', fit: BoxFit.contain, errorBuilder: (_, __, ___) => const Icon(Icons.waves_rounded, color: _waveBlue, size: 52), ), ), const SizedBox(height: 28), Text( _paymentLaunched ? 'Paiement en cours…' : _isMock ? 'Simuler le paiement' : 'Prêt à payer', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w800, color: textPrimary), ), const SizedBox(height: 8), // Montant RichText( text: TextSpan( children: [ TextSpan( text: '${_formatPrix(montant)} ', style: const TextStyle( fontSize: 32, fontWeight: FontWeight.w900, color: _waveBlue, letterSpacing: -0.5, ), ), TextSpan( text: 'FCFA', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: textSecondary), ), ], ), ), if (widget.souscription.organisationNom != null) ...[ const SizedBox(height: 4), Text( widget.souscription.organisationNom!, style: TextStyle(color: textSecondary, fontSize: 13), ), ], const SizedBox(height: 32), if (!_paymentLaunched) ...[ if (_isMock) Container( margin: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.warning.withOpacity(isDark ? 0.15 : 0.08), borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.warning.withOpacity(0.3)), ), child: const Row( children: [ Icon(Icons.science_outlined, color: AppColors.warning, size: 16), SizedBox(width: 8), Expanded( child: Text( 'Environnement de développement — le paiement sera simulé automatiquement. En production, vous serez redirigé vers l\'application Wave.', style: TextStyle( fontSize: 12, color: AppColors.warning, height: 1.4), ), ), ], ), ), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _lancerOuSimuler, icon: Icon(_isMock ? Icons.play_circle_rounded : Icons.open_in_new_rounded), label: Text( _isMock ? 'Simuler le paiement Wave' : 'Ouvrir Wave', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), ), style: ElevatedButton.styleFrom( backgroundColor: _waveBlue, foregroundColor: AppColors.onPrimary, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), shadowColor: _waveBlue.withOpacity(0.4), elevation: 3, ), ), ), ] else ...[ // Paiement lancé en prod Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: bgSurface, borderRadius: BorderRadius.circular(16), border: Border.all(color: borderColor), boxShadow: isDark ? null : UnionFlowColors.softShadow, ), child: Column( children: [ const SizedBox( width: 40, height: 40, child: CircularProgressIndicator(color: _waveBlue, strokeWidth: 3), ), const SizedBox(height: 16), Text( 'Paiement en cours dans Wave', style: TextStyle(fontWeight: FontWeight.w700, color: textPrimary), ), const SizedBox(height: 6), Text( 'Revenez dans l\'app une fois\nvotre paiement confirmé.', textAlign: TextAlign.center, style: TextStyle( color: textSecondary, fontSize: 13, height: 1.4), ), ], ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () => context .read() .add(const OnboardingRetourDepuisWave()), icon: const Icon(Icons.check_circle_outline_rounded), label: const Text( 'J\'ai effectué le paiement', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700), ), style: ElevatedButton.styleFrom( backgroundColor: UnionFlowColors.unionGreen, foregroundColor: AppColors.onPrimary, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), ), ), const SizedBox(height: 10), TextButton.icon( onPressed: _lancerOuSimuler, icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('Rouvrir Wave'), style: TextButton.styleFrom(foregroundColor: _waveBlue), ), ], ], ); } String _formatPrix(double prix) { if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M'; final s = prix.toStringAsFixed(0); if (s.length > 3) { return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}'; } return s; } }