feat(onboarding): UI/UX polish + mapping typeOrg + gestion erreur paiement Wave

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
This commit is contained in:
dahoud
2026-04-15 20:14:27 +00:00
parent 36a903c80e
commit 21b519de53
8 changed files with 1081 additions and 859 deletions

View File

@@ -2,6 +2,7 @@ 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';
/// Écran de sélection du moyen de paiement
@@ -17,13 +18,17 @@ class PaymentMethodPage extends StatefulWidget {
class _PaymentMethodPageState extends State<PaymentMethodPage> {
String? _selected;
// Couleurs de marque Wave et Orange Money — volontairement hardcodées
static const _waveBlue = Color(0xFF00B9F1);
static const _orangeOrange = Color(0xFFFF6600);
static const _methods = [
_PayMethod(
id: 'WAVE',
name: 'Wave Mobile Money',
description: 'Paiement rapide via votre compte Wave',
logoAsset: 'assets/images/payment_methods/wave/logo.png',
color: Color(0xFF00B9F1),
color: _waveBlue,
available: true,
badge: 'Recommandé',
),
@@ -32,7 +37,7 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
name: 'Orange Money',
description: 'Paiement via Orange Money',
logoAsset: 'assets/images/payment_methods/orange_money/logo-black.png',
color: Color(0xFFFF6600),
color: _orangeOrange,
available: false,
badge: 'Prochainement',
),
@@ -40,17 +45,20 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
@override
Widget build(BuildContext context) {
final montant = widget.souscription.montantTotal ?? 0;
final montant = widget.souscription.montantTotal ?? 0;
final isDark = Theme.of(context).brightness == Brightness.dark;
final bgCard = 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 Scaffold(
backgroundColor: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column(
children: [
// Header
// Header gradient — toujours sombre, texte blanc intentionnel
Container(
decoration: const BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
),
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient),
child: SafeArea(
bottom: false,
child: Padding(
@@ -60,25 +68,19 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
children: [
IconButton(
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(Icons.arrow_back_rounded,
color: Colors.white),
icon: const Icon(Icons.arrow_back_rounded, color: Colors.white),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(height: 12),
const Text(
'Moyen de paiement',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w800,
),
style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800),
),
const SizedBox(height: 4),
Text(
'Choisissez comment régler votre souscription',
style: TextStyle(
color: Colors.white.withOpacity(0.8), fontSize: 13),
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 13),
),
],
),
@@ -92,13 +94,14 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Montant rappel
// Rappel montant
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: bgCard,
borderRadius: BorderRadius.circular(14),
boxShadow: UnionFlowColors.softShadow,
border: Border.all(color: borderColor),
boxShadow: isDark ? null : UnionFlowColors.softShadow,
),
child: Row(
children: [
@@ -109,24 +112,19 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
gradient: UnionFlowColors.goldGradient,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.receipt_rounded,
color: Colors.white, size: 22),
child: const Icon(Icons.receipt_rounded, color: Colors.white, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Montant total',
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 12),
),
Text('Montant total',
style: TextStyle(color: textSecondary, fontSize: 12)),
Text(
'${_formatPrix(montant)} FCFA',
style: const TextStyle(
color: UnionFlowColors.textPrimary,
style: TextStyle(
color: textPrimary,
fontSize: 20,
fontWeight: FontWeight.w900,
),
@@ -138,18 +136,12 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text(
'Organisation',
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 11),
),
Text('Organisation',
style: TextStyle(color: textSecondary, fontSize: 11)),
Text(
widget.souscription.organisationNom!,
style: const TextStyle(
color: UnionFlowColors.textPrimary,
fontSize: 12,
fontWeight: FontWeight.w600),
style: TextStyle(
color: textPrimary, fontSize: 12, fontWeight: FontWeight.w600),
),
],
),
@@ -158,10 +150,10 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
),
const SizedBox(height: 24),
const Text(
Text(
'Sélectionnez un moyen de paiement',
style: TextStyle(
color: UnionFlowColors.textPrimary,
color: textPrimary,
fontWeight: FontWeight.w700,
fontSize: 15,
),
@@ -169,36 +161,33 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
const SizedBox(height: 12),
..._methods.map((m) => _MethodCard(
method: m,
selected: _selected == m.id,
onTap: m.available
? () => setState(() => _selected = m.id)
: null,
)),
method: m,
selected: _selected == m.id,
onTap: m.available ? () => setState(() => _selected = m.id) : null,
)),
const SizedBox(height: 20),
// Bandeau sécurité — fond vert pâle adaptatif
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreenPale,
color: isDark
? UnionFlowColors.unionGreen.withOpacity(0.1)
: UnionFlowColors.unionGreenPale,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
UnionFlowColors.unionGreen.withOpacity(0.25)),
border: Border.all(color: UnionFlowColors.unionGreen.withOpacity(0.25)),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.lock_rounded,
color: UnionFlowColors.unionGreen, size: 18),
Icon(Icons.lock_rounded, color: UnionFlowColors.unionGreen, size: 18),
SizedBox(width: 10),
Expanded(
child: Text(
'Vos informations de paiement sont sécurisées et ne sont jamais stockées sur nos serveurs. La transaction est traitée directement par Wave.',
style: TextStyle(
fontSize: 12,
color: UnionFlowColors.unionGreen,
height: 1.4),
fontSize: 12, color: UnionFlowColors.unionGreen, height: 1.4),
),
),
],
@@ -214,41 +203,34 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
padding: EdgeInsets.fromLTRB(
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: bgCard,
border: Border(top: BorderSide(color: borderColor)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, -4),
),
BoxShadow(color: AppColors.shadow, blurRadius: 12, offset: const Offset(0, -4)),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _selected == 'WAVE'
? () => context
.read<OnboardingBloc>()
.add(const OnboardingPaiementInitie())
? () => context.read<OnboardingBloc>().add(const OnboardingPaiementInitie())
: null,
icon: const Icon(Icons.open_in_new_rounded),
label: Text(
_selected == 'WAVE'
? 'Payer avec Wave'
: 'Sélectionnez un moyen de paiement',
style:
const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00B9F1),
disabledBackgroundColor: UnionFlowColors.border,
foregroundColor: Colors.white,
disabledForegroundColor: UnionFlowColors.textSecondary,
backgroundColor: _waveBlue,
disabledBackgroundColor: isDark ? AppColors.borderDark : AppColors.border,
foregroundColor: AppColors.onPrimary,
disabledForegroundColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: _selected == 'WAVE' ? 3 : 0,
shadowColor: const Color(0xFF00B9F1).withOpacity(0.4),
shadowColor: _waveBlue.withOpacity(0.4),
),
),
),
@@ -259,9 +241,7 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
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)}';
}
if (s.length > 3) return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
return s;
}
}
@@ -287,15 +267,18 @@ class _MethodCard extends StatelessWidget {
final bool selected;
final VoidCallback? onTap;
const _MethodCard({
required this.method,
required this.selected,
this.onTap,
});
const _MethodCard({required this.method, required this.selected, this.onTap});
@override
Widget build(BuildContext context) {
final disabled = onTap == null;
final disabled = onTap == null;
final isDark = Theme.of(context).brightness == Brightness.dark;
final bgSurface = isDark ? AppColors.surfaceDark : AppColors.surface;
final bgDisabled = isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariant;
final borderDefault = isDark ? AppColors.borderDark : AppColors.border;
final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
final textTertiary = isDark ? AppColors.textSecondaryDark : AppColors.textTertiary;
final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return GestureDetector(
onTap: onTap,
@@ -304,51 +287,43 @@ class _MethodCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: disabled
? UnionFlowColors.surfaceVariant
? bgDisabled
: selected
? method.color.withOpacity(0.06)
: UnionFlowColors.surface,
? method.color.withOpacity(isDark ? 0.12 : 0.06)
: bgSurface,
border: Border.all(
color: selected ? method.color : UnionFlowColors.border,
color: selected ? method.color : borderDefault,
width: selected ? 2 : 1,
),
borderRadius: BorderRadius.circular(14),
boxShadow: disabled
? []
: selected
? [
BoxShadow(
color: method.color.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 6),
)
]
: UnionFlowColors.softShadow,
? [BoxShadow(color: method.color.withOpacity(0.15), blurRadius: 16, offset: const Offset(0, 6))]
: isDark ? null : UnionFlowColors.softShadow,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Logo image
// Logo — fond blanc intentionnel (logos de marque)
Container(
width: 56,
height: 48,
decoration: BoxDecoration(
color: disabled
? UnionFlowColors.border
: Colors.white,
color: disabled ? bgDisabled : Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: UnionFlowColors.border),
border: Border.all(color: borderDefault),
),
padding: const EdgeInsets.all(6),
child: Image.asset(
method.logoAsset,
fit: BoxFit.contain,
color: disabled ? UnionFlowColors.textTertiary : null,
color: disabled ? textTertiary : null,
colorBlendMode: disabled ? BlendMode.srcIn : null,
errorBuilder: (_, __, ___) => Icon(
Icons.account_balance_wallet_rounded,
color: disabled ? UnionFlowColors.textTertiary : method.color,
color: disabled ? textTertiary : method.color,
size: 24,
),
),
@@ -363,18 +338,14 @@ class _MethodCard extends StatelessWidget {
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
color: disabled
? UnionFlowColors.textTertiary
: UnionFlowColors.textPrimary,
color: disabled ? textTertiary : textPrimary,
),
),
Text(
method.description,
style: TextStyle(
fontSize: 12,
color: disabled
? UnionFlowColors.textTertiary
: UnionFlowColors.textSecondary,
color: disabled ? textTertiary : textSecondary,
),
),
],
@@ -384,14 +355,13 @@ class _MethodCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: disabled
? UnionFlowColors.border
? borderDefault
: method.available
? method.color.withOpacity(0.1)
: UnionFlowColors.surfaceVariant,
? method.color.withOpacity(isDark ? 0.2 : 0.1)
: isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(20),
),
child: Text(
@@ -400,20 +370,18 @@ class _MethodCard extends StatelessWidget {
fontSize: 10,
fontWeight: FontWeight.w700,
color: disabled
? UnionFlowColors.textTertiary
? textTertiary
: method.available
? method.color
: UnionFlowColors.textSecondary,
: textSecondary,
),
),
),
if (!disabled) ...[
const SizedBox(height: 6),
Icon(
selected
? Icons.check_circle_rounded
: Icons.radio_button_unchecked,
color: selected ? method.color : UnionFlowColors.border,
selected ? Icons.check_circle_rounded : Icons.radio_button_unchecked,
color: selected ? method.color : borderDefault,
size: 20,
),
],