Refactoring
This commit is contained in:
@@ -0,0 +1,668 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/services/wave_integration_service.dart';
|
||||
import '../../../../core/services/wave_payment_service.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/buttons/primary_button.dart';
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
|
||||
/// Page de démonstration de l'intégration Wave Money
|
||||
/// Permet de tester toutes les fonctionnalités Wave
|
||||
class WaveDemoPage extends StatefulWidget {
|
||||
const WaveDemoPage({super.key});
|
||||
|
||||
@override
|
||||
State<WaveDemoPage> createState() => _WaveDemoPageState();
|
||||
}
|
||||
|
||||
class _WaveDemoPageState extends State<WaveDemoPage>
|
||||
with TickerProviderStateMixin {
|
||||
late WaveIntegrationService _waveIntegrationService;
|
||||
late WavePaymentService _wavePaymentService;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
final _amountController = TextEditingController(text: '5000');
|
||||
final _phoneController = TextEditingController(text: '77123456');
|
||||
final _nameController = TextEditingController(text: 'Test User');
|
||||
|
||||
bool _isLoading = false;
|
||||
String _lastResult = '';
|
||||
WavePaymentStats? _stats;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_waveIntegrationService = getIt<WaveIntegrationService>();
|
||||
_wavePaymentService = getIt<WavePaymentService>();
|
||||
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
_loadStats();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
_phoneController.dispose();
|
||||
_nameController.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Wave Money Demo',
|
||||
subtitle: 'Test d\'intégration Wave Money',
|
||||
showBackButton: true,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWaveHeader(),
|
||||
const SizedBox(height: 24),
|
||||
_buildTestForm(),
|
||||
const SizedBox(height: 24),
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatsSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildResultSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.waves,
|
||||
size: 32,
|
||||
color: Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Wave Money Integration',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Test et démonstration',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.white, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Environnement de test - Aucun paiement réel ne sera effectué',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTestForm() {
|
||||
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(
|
||||
'Paramètres de test',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Montant
|
||||
TextFormField(
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Montant (XOF)',
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Numéro de téléphone
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Numéro Wave Money',
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
prefixText: '+225 ',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Nom
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du payeur',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Bouton de test
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: PrimaryButton(
|
||||
text: _isLoading ? 'Test en cours...' : 'Tester le paiement Wave',
|
||||
icon: _isLoading ? null : Icons.play_arrow,
|
||||
onPressed: _isLoading ? null : _testWavePayment,
|
||||
isLoading: _isLoading,
|
||||
backgroundColor: const Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions() {
|
||||
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(
|
||||
'Actions rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildActionChip(
|
||||
'Calculer frais',
|
||||
Icons.calculate,
|
||||
_calculateFees,
|
||||
),
|
||||
_buildActionChip(
|
||||
'Historique',
|
||||
Icons.history,
|
||||
_showHistory,
|
||||
),
|
||||
_buildActionChip(
|
||||
'Statistiques',
|
||||
Icons.analytics,
|
||||
_loadStats,
|
||||
),
|
||||
_buildActionChip(
|
||||
'Vider cache',
|
||||
Icons.clear_all,
|
||||
_clearCache,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionChip(String label, IconData icon, VoidCallback onPressed) {
|
||||
return ActionChip(
|
||||
avatar: Icon(icon, size: 16),
|
||||
label: Text(label),
|
||||
onPressed: onPressed,
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
side: const BorderSide(color: AppTheme.borderLight),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsSection() {
|
||||
if (_stats == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
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(
|
||||
'Statistiques Wave Money',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 2.5,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
children: [
|
||||
_buildStatCard(
|
||||
'Total paiements',
|
||||
_stats!.totalPayments.toString(),
|
||||
Icons.payment,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
_buildStatCard(
|
||||
'Réussis',
|
||||
_stats!.completedPayments.toString(),
|
||||
Icons.check_circle,
|
||||
AppTheme.successColor,
|
||||
),
|
||||
_buildStatCard(
|
||||
'Montant total',
|
||||
'${_stats!.totalAmount.toStringAsFixed(0)} XOF',
|
||||
Icons.attach_money,
|
||||
AppTheme.warningColor,
|
||||
),
|
||||
_buildStatCard(
|
||||
'Taux de réussite',
|
||||
'${_stats!.successRate.toStringAsFixed(1)}%',
|
||||
Icons.trending_up,
|
||||
AppTheme.infoColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultSection() {
|
||||
if (_lastResult.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
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: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Dernier résultat',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy, size: 16),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: _lastResult));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Résultat copié')),
|
||||
);
|
||||
},
|
||||
tooltip: 'Copier',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.backgroundLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
),
|
||||
child: Text(
|
||||
_lastResult,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Actions
|
||||
Future<void> _testWavePayment() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_lastResult = '';
|
||||
});
|
||||
|
||||
try {
|
||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||
if (amount <= 0) {
|
||||
throw Exception('Montant invalide');
|
||||
}
|
||||
|
||||
// Créer une cotisation de test
|
||||
final testCotisation = CotisationModel(
|
||||
id: 'test_${DateTime.now().millisecondsSinceEpoch}',
|
||||
numeroReference: 'TEST-${DateTime.now().millisecondsSinceEpoch}',
|
||||
membreId: 'test_member',
|
||||
nomMembre: _nameController.text,
|
||||
typeCotisation: 'MENSUELLE',
|
||||
montantDu: amount,
|
||||
montantPaye: 0,
|
||||
codeDevise: 'XOF',
|
||||
dateEcheance: DateTime.now().add(const Duration(days: 30)),
|
||||
statut: 'EN_ATTENTE',
|
||||
recurrente: false,
|
||||
nombreRappels: 0,
|
||||
annee: DateTime.now().year,
|
||||
dateCreation: DateTime.now(),
|
||||
);
|
||||
|
||||
// Initier le paiement Wave
|
||||
final result = await _waveIntegrationService.initiateWavePayment(
|
||||
cotisationId: testCotisation.id,
|
||||
montant: amount,
|
||||
numeroTelephone: _phoneController.text,
|
||||
nomPayeur: _nameController.text,
|
||||
metadata: {
|
||||
'test_mode': true,
|
||||
'demo_page': true,
|
||||
},
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_lastResult = '''
|
||||
Test de paiement Wave Money
|
||||
|
||||
Résultat: ${result.success ? 'SUCCÈS' : 'ÉCHEC'}
|
||||
${result.success ? '''
|
||||
ID Paiement: ${result.payment?.id}
|
||||
Session Wave: ${result.session?.waveSessionId}
|
||||
URL Checkout: ${result.checkoutUrl}
|
||||
Montant: ${amount.toStringAsFixed(0)} XOF
|
||||
Frais: ${_wavePaymentService.calculateWaveFees(amount).toStringAsFixed(0)} XOF
|
||||
''' : '''
|
||||
Erreur: ${result.errorMessage}
|
||||
'''}
|
||||
Timestamp: ${DateTime.now().toIso8601String()}
|
||||
'''.trim();
|
||||
});
|
||||
|
||||
// Feedback haptique
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
// Recharger les statistiques
|
||||
await _loadStats();
|
||||
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_lastResult = 'Erreur lors du test: $e';
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _calculateFees() {
|
||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||
if (amount <= 0) {
|
||||
setState(() {
|
||||
_lastResult = 'Montant invalide pour le calcul des frais';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final fees = _wavePaymentService.calculateWaveFees(amount);
|
||||
final total = amount + fees;
|
||||
|
||||
setState(() {
|
||||
_lastResult = '''
|
||||
Calcul des frais Wave Money
|
||||
|
||||
Montant: ${amount.toStringAsFixed(0)} XOF
|
||||
Frais Wave: ${fees.toStringAsFixed(0)} XOF
|
||||
Total: ${total.toStringAsFixed(0)} XOF
|
||||
|
||||
Barème Wave CI 2024:
|
||||
• 0-2000 XOF: Gratuit
|
||||
• 2001-10000 XOF: 25 XOF
|
||||
• 10001-50000 XOF: 100 XOF
|
||||
• 50001-100000 XOF: 200 XOF
|
||||
• 100001-500000 XOF: 500 XOF
|
||||
• >500000 XOF: 0.1% du montant
|
||||
'''.trim();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showHistory() async {
|
||||
try {
|
||||
final history = await _waveIntegrationService.getWavePaymentHistory(limit: 10);
|
||||
|
||||
setState(() {
|
||||
_lastResult = '''
|
||||
Historique des paiements Wave (10 derniers)
|
||||
|
||||
${history.isEmpty ? 'Aucun paiement trouvé' : history.map((payment) => '''
|
||||
• ${payment.numeroReference} - ${payment.montant.toStringAsFixed(0)} XOF
|
||||
Statut: ${payment.statut}
|
||||
Date: ${payment.dateTransaction.toString().substring(0, 16)}
|
||||
''').join('\n')}
|
||||
|
||||
Total: ${history.length} paiement(s)
|
||||
'''.trim();
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_lastResult = 'Erreur lors de la récupération de l\'historique: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadStats() async {
|
||||
try {
|
||||
final stats = await _waveIntegrationService.getWavePaymentStats();
|
||||
setState(() {
|
||||
_stats = stats;
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur lors du chargement des statistiques: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearCache() async {
|
||||
try {
|
||||
// TODO: Implémenter le nettoyage du cache
|
||||
setState(() {
|
||||
_lastResult = 'Cache Wave Money vidé avec succès';
|
||||
_stats = null;
|
||||
});
|
||||
await _loadStats();
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_lastResult = 'Erreur lors du nettoyage du cache: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user