Plan selection :
- Grille 2×2 compacte pour les plages (au lieu de liste verticale)
- Badge ⭐ POPULAIRE sur STANDARD
- Remise annuelle affichée (−X%/an)
- AnimatedSwitcher + auto-scroll vers formules quand plage sélectionnée
- Dark mode adaptatif complet
Récapitulatif :
- Dark mode complet (AppColors pairs)
- Bloc Total gradient gold adaptatif
- NoteBox avec backgrounds accent.withOpacity()
- Utilise OnboardingBottomBar (consistence)
Payment method :
- Dark mode + couleurs de marque Wave/Orange hardcodées (intentionnel)
- Logo container reste blanc (brand)
- Mapping typeOrganisation détaillé → enum backend ASSOCIATION/MUTUELLE/
COOPERATIVE/FEDERATION (fix HTTP 400 'Valeur invalide pour typeOrganisation')
Wave payment :
- Dark mode adaptatif
- Message dev clair (simulation automatique)
- Gestion OnboardingPaiementEchoue : SnackBar rouge + reset flags + reste sur page
(plus de faux succès quand confirmerPaiement() return false ou lève exception)
Bloc : nouvel état OnboardingPaiementEchoue, _onRetourDepuisWave vérifie le return
de confirmerPaiement() (plus de catch silencieux qui émettait OnboardingPaiementConfirme)
Shared widgets : OnboardingSectionTitle + OnboardingBottomBar dark mode + hint optionnel
486 lines
19 KiB
Dart
486 lines
19 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import '../../bloc/onboarding_bloc.dart';
|
||
import '../../data/models/souscription_status_model.dart';
|
||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||
import 'onboarding_shared_widgets.dart';
|
||
|
||
/// Étape 3 — Récapitulatif détaillé avant paiement
|
||
class SubscriptionSummaryPage extends StatelessWidget {
|
||
final SouscriptionStatusModel souscription;
|
||
|
||
const SubscriptionSummaryPage({super.key, required this.souscription});
|
||
|
||
static const _periodeLabels = {
|
||
'MENSUEL': 'Mensuel',
|
||
'TRIMESTRIEL': 'Trimestriel',
|
||
'SEMESTRIEL': 'Semestriel',
|
||
'ANNUEL': 'Annuel',
|
||
};
|
||
|
||
static const _periodeRemises = {
|
||
'MENSUEL': null,
|
||
'TRIMESTRIEL': '–5 % de remise',
|
||
'SEMESTRIEL': '–10 % de remise',
|
||
'ANNUEL': '–20 % de remise',
|
||
};
|
||
|
||
static const _plageLabels = {
|
||
'PETITE': '1 – 100 membres',
|
||
'MOYENNE': '101 – 500 membres',
|
||
'GRANDE': '501 – 2 000 membres',
|
||
'TRES_GRANDE': '2 000+ membres',
|
||
};
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||
final montant = souscription.montantTotal ?? 0;
|
||
final remise = _periodeRemises[souscription.typePeriode];
|
||
|
||
return Scaffold(
|
||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||
body: Column(
|
||
children: [
|
||
// ── Header hero gradient ───────────────────────────
|
||
Container(
|
||
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient),
|
||
child: SafeArea(
|
||
bottom: false,
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
|
||
child: Column(
|
||
children: [
|
||
// Barre de progression — toutes complètes à l'étape 3
|
||
Row(
|
||
children: List.generate(3, (i) => Expanded(
|
||
child: Container(
|
||
height: 4,
|
||
margin: EdgeInsets.only(right: i < 2 ? 6 : 0),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
)),
|
||
),
|
||
const SizedBox(height: 6),
|
||
Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(
|
||
'Étape 3 sur 3',
|
||
style: TextStyle(
|
||
color: Colors.white.withOpacity(0.75),
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
// Icône principale
|
||
Container(
|
||
width: 80,
|
||
height: 80,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.15),
|
||
shape: BoxShape.circle,
|
||
border: Border.all(color: Colors.white.withOpacity(0.4), width: 2),
|
||
),
|
||
child: const Icon(Icons.receipt_long_rounded, color: Colors.white, size: 40),
|
||
),
|
||
const SizedBox(height: 14),
|
||
|
||
// Montant
|
||
Text(
|
||
_formatPrix(montant),
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 42,
|
||
fontWeight: FontWeight.w900,
|
||
letterSpacing: -1,
|
||
),
|
||
),
|
||
const Text(
|
||
'FCFA à régler',
|
||
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500),
|
||
),
|
||
|
||
// Badge remise
|
||
if (remise != null) ...[
|
||
const SizedBox(height: 10),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
||
decoration: BoxDecoration(
|
||
color: UnionFlowColors.gold.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(20),
|
||
border: Border.all(color: UnionFlowColors.goldLight.withOpacity(0.5)),
|
||
),
|
||
child: Text(
|
||
'🎉 $remise appliquée',
|
||
style: const TextStyle(
|
||
color: UnionFlowColors.goldLight,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
// ── Contenu scrollable ──────────────────────────────
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
physics: const AlwaysScrollableScrollPhysics(),
|
||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
|
||
// Organisation
|
||
if (souscription.organisationNom != null) ...[
|
||
_DetailCard(
|
||
title: 'Organisation',
|
||
icon: Icons.business_rounded,
|
||
iconColor: UnionFlowColors.indigo,
|
||
isDark: isDark,
|
||
items: [
|
||
_DetailItem(label: 'Nom', value: souscription.organisationNom!, bold: true),
|
||
if (souscription.typeOrganisation != null)
|
||
_DetailItem(label: 'Type', value: souscription.typeOrganisation!),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
],
|
||
|
||
// Formule
|
||
_DetailCard(
|
||
title: 'Formule souscrite',
|
||
icon: Icons.workspace_premium_rounded,
|
||
iconColor: UnionFlowColors.gold,
|
||
isDark: isDark,
|
||
items: [
|
||
_DetailItem(label: 'Niveau', value: souscription.typeFormule, bold: true),
|
||
_DetailItem(
|
||
label: 'Capacité',
|
||
value: _plageLabels[souscription.plageMembres] ?? souscription.plageLibelle,
|
||
),
|
||
if (souscription.montantMensuelBase != null)
|
||
_DetailItem(
|
||
label: 'Prix de base',
|
||
value: '${_formatPrix(souscription.montantMensuelBase!)} FCFA/mois',
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 14),
|
||
|
||
// Facturation
|
||
_DetailCard(
|
||
title: 'Facturation',
|
||
icon: Icons.calendar_today_rounded,
|
||
iconColor: UnionFlowColors.unionGreen,
|
||
isDark: isDark,
|
||
items: [
|
||
_DetailItem(
|
||
label: 'Période',
|
||
value: _periodeLabels[souscription.typePeriode] ?? souscription.typePeriode,
|
||
),
|
||
if (souscription.coefficientApplique != null)
|
||
_DetailItem(
|
||
label: 'Coefficient',
|
||
value: '×${souscription.coefficientApplique!.toStringAsFixed(4)}',
|
||
),
|
||
if (souscription.dateDebut != null)
|
||
_DetailItem(label: 'Début', value: _formatDate(souscription.dateDebut!)),
|
||
if (souscription.dateFin != null)
|
||
_DetailItem(label: 'Fin', value: _formatDate(souscription.dateFin!)),
|
||
],
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
// Bloc montant total — proéminent
|
||
Container(
|
||
padding: const EdgeInsets.all(18),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: isDark
|
||
? [UnionFlowColors.gold.withOpacity(0.18), UnionFlowColors.amber.withOpacity(0.12)]
|
||
: [UnionFlowColors.goldPale, const Color(0xFFFFF3C8)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: UnionFlowColors.gold.withOpacity(0.4)),
|
||
boxShadow: isDark ? null : UnionFlowColors.goldGlowShadow,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 52,
|
||
height: 52,
|
||
decoration: BoxDecoration(
|
||
gradient: UnionFlowColors.goldGradient,
|
||
borderRadius: BorderRadius.circular(14),
|
||
),
|
||
child: const Icon(Icons.monetization_on_rounded,
|
||
color: Colors.white, size: 28),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'TOTAL À PAYER',
|
||
style: TextStyle(
|
||
color: isDark
|
||
? AppColors.textSecondaryDark
|
||
: AppColors.textSecondary,
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
letterSpacing: 0.5,
|
||
),
|
||
),
|
||
Text(
|
||
'${_formatPrix(montant)} FCFA',
|
||
style: TextStyle(
|
||
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimary,
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.w900,
|
||
letterSpacing: -0.5,
|
||
),
|
||
),
|
||
if (remise != null)
|
||
Text(
|
||
remise,
|
||
style: const TextStyle(
|
||
color: UnionFlowColors.gold,
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
// Notes
|
||
_NoteBox(
|
||
icon: Icons.security_rounded,
|
||
iconColor: UnionFlowColors.unionGreen,
|
||
accentColor: UnionFlowColors.unionGreen,
|
||
isDark: isDark,
|
||
title: 'Paiement sécurisé',
|
||
message: 'Votre paiement est traité de manière sécurisée via Wave Mobile Money. Une fois confirmé, votre compte sera activé automatiquement.',
|
||
),
|
||
const SizedBox(height: 10),
|
||
_NoteBox(
|
||
icon: Icons.bolt_rounded,
|
||
iconColor: UnionFlowColors.amber,
|
||
accentColor: UnionFlowColors.amber,
|
||
isDark: isDark,
|
||
title: 'Activation immédiate',
|
||
message: 'Dès que Wave confirme le paiement, votre espace administrateur est activé avec toutes les fonctionnalités de votre formule.',
|
||
),
|
||
const SizedBox(height: 10),
|
||
_NoteBox(
|
||
icon: Icons.support_agent_rounded,
|
||
iconColor: UnionFlowColors.info,
|
||
accentColor: UnionFlowColors.info,
|
||
isDark: isDark,
|
||
title: 'Besoin d\'aide ?',
|
||
message: 'En cas de problème, contactez support@unionflow.app — réponse sous 24h.',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
bottomNavigationBar: OnboardingBottomBar(
|
||
enabled: true,
|
||
label: 'Choisir le moyen de paiement',
|
||
onPressed: () => context.read<OnboardingBloc>().add(const OnboardingChoixPaiementOuvert()),
|
||
),
|
||
);
|
||
}
|
||
|
||
String _formatPrix(double prix) {
|
||
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
|
||
final s = prix.toStringAsFixed(0);
|
||
if (s.length > 6) return '${s.substring(0, s.length - 6)} ${s.substring(s.length - 6, s.length - 3)} ${s.substring(s.length - 3)}';
|
||
if (s.length > 3) return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
|
||
return s;
|
||
}
|
||
|
||
String _formatDate(DateTime date) =>
|
||
'${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||
}
|
||
|
||
// ─── Widgets locaux ──────────────────────────────────────────────────────────
|
||
|
||
class _DetailItem {
|
||
final String label;
|
||
final String value;
|
||
final bool bold;
|
||
const _DetailItem({required this.label, required this.value, this.bold = false});
|
||
}
|
||
|
||
class _DetailCard extends StatelessWidget {
|
||
final String title;
|
||
final IconData icon;
|
||
final Color iconColor;
|
||
final List<_DetailItem> items;
|
||
final bool isDark;
|
||
|
||
const _DetailCard({
|
||
required this.title,
|
||
required this.icon,
|
||
required this.iconColor,
|
||
required this.items,
|
||
required this.isDark,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final bgColor = 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;
|
||
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: bgColor,
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: borderColor),
|
||
boxShadow: isDark ? null : UnionFlowColors.softShadow,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// En-tête section
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 10),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 34,
|
||
height: 34,
|
||
decoration: BoxDecoration(
|
||
color: iconColor.withOpacity(isDark ? 0.2 : 0.1),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Icon(icon, color: iconColor, size: 18),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.w700,
|
||
fontSize: 14,
|
||
color: textPrimary,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Divider(height: 1, color: borderColor),
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
|
||
child: Column(
|
||
children: items.map((item) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 8),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
width: 110,
|
||
child: Text(item.label,
|
||
style: TextStyle(color: textSecondary, fontSize: 13)),
|
||
),
|
||
Expanded(
|
||
child: Text(
|
||
item.value,
|
||
style: TextStyle(
|
||
color: textPrimary,
|
||
fontSize: 13,
|
||
fontWeight: item.bold ? FontWeight.w700 : FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)).toList(),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _NoteBox extends StatelessWidget {
|
||
final IconData icon;
|
||
final Color iconColor;
|
||
final Color accentColor;
|
||
final bool isDark;
|
||
final String title;
|
||
final String message;
|
||
|
||
const _NoteBox({
|
||
required this.icon,
|
||
required this.iconColor,
|
||
required this.accentColor,
|
||
required this.isDark,
|
||
required this.title,
|
||
required this.message,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final bgColor = accentColor.withOpacity(isDark ? 0.12 : 0.06);
|
||
final borderColor = accentColor.withOpacity(isDark ? 0.3 : 0.2);
|
||
final textSecondary= isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: bgColor,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: borderColor),
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Icon(icon, color: iconColor, size: 20),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: TextStyle(color: iconColor, fontWeight: FontWeight.w700, fontSize: 13),
|
||
),
|
||
const SizedBox(height: 3),
|
||
Text(
|
||
message,
|
||
style: TextStyle(color: textSecondary, fontSize: 12, height: 1.5),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|