669 lines
19 KiB
Dart
669 lines
19 KiB
Dart
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';
|
|
});
|
|
}
|
|
}
|
|
}
|