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:
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user