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

@@ -141,6 +141,20 @@ class OnboardingStepPaiement extends OnboardingState {
/// Paiement confirmé — déclenche un re-check du statut du compte
class OnboardingPaiementConfirme extends OnboardingState {}
/// Erreur de confirmation paiement — l'utilisateur peut réessayer
class OnboardingPaiementEchoue extends OnboardingState {
final String message;
final SouscriptionStatusModel souscription;
final String waveLaunchUrl;
const OnboardingPaiementEchoue({
required this.message,
required this.souscription,
required this.waveLaunchUrl,
});
@override
List<Object?> get props => [message, souscription, waveLaunchUrl];
}
/// Étape 5 : en attente de validation SuperAdmin
class OnboardingStepAttente extends OnboardingState {
final SouscriptionStatusModel? souscription;
@@ -316,16 +330,32 @@ class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
Future<void> _onRetourDepuisWave(
OnboardingRetourDepuisWave event, Emitter<OnboardingState> emit) async {
emit(OnboardingLoading());
try {
final souscId = _souscription?.souscriptionId;
if (souscId != null) {
await _datasource.confirmerPaiement(souscId);
final waveUrl = _souscription?.waveLaunchUrl;
if (souscId == null) {
emit(const OnboardingError('Souscription introuvable.'));
return;
}
// Émettre OnboardingPaiementConfirme pour déclencher re-check du compte
// Si le backend auto-active le compte, AuthStatusChecked redirigera vers dashboard
try {
final success = await _datasource.confirmerPaiement(souscId);
if (success) {
emit(OnboardingPaiementConfirme());
} else {
// La confirmation a échoué côté backend — l'utilisateur doit réessayer
emit(OnboardingPaiementEchoue(
message: 'Le paiement n\'a pas pu être confirmé. Vérifiez que le paiement Wave a bien été effectué et réessayez.',
souscription: _souscription!,
waveLaunchUrl: waveUrl ?? '',
));
}
} catch (e) {
emit(OnboardingPaiementConfirme());
emit(OnboardingPaiementEchoue(
message: 'Erreur lors de la confirmation: ${e.toString().replaceFirst("Exception: ", "")}',
souscription: _souscription!,
waveLaunchUrl: waveUrl ?? '',
));
}
}
}

View File

@@ -53,6 +53,43 @@ class SouscriptionDatasource {
return null;
}
/// Mappe le type d'organisation détaillé (ex: MUTUELLE_EPARGNE, CLUB_SPORTIF)
/// vers l'une des 4 catégories acceptées par le backend souscription :
/// ASSOCIATION, MUTUELLE, COOPERATIVE, FEDERATION.
static String? _mapTypeOrganisationBilling(String? detailedType) {
if (detailedType == null || detailedType.isEmpty) return null;
switch (detailedType.toUpperCase()) {
// Catégorie ASSOCIATIF + RELIGIEUX
case 'ASSOCIATION':
case 'CLUB_SERVICE':
case 'CLUB_SPORTIF':
case 'CLUB_CULTUREL':
case 'EGLISE':
case 'GROUPE_PRIERE':
return 'ASSOCIATION';
// Catégorie FINANCIER_SOLIDAIRE (épargne/crédit)
case 'TONTINE':
case 'MUTUELLE_EPARGNE':
case 'MUTUELLE_CREDIT':
case 'MUTUELLE':
return 'MUTUELLE';
// Catégorie COOPERATIVE
case 'COOPERATIVE':
case 'GIE':
return 'COOPERATIVE';
// Catégorie PROFESSIONNEL + RESEAU_FEDERATION
case 'ONG':
case 'FONDATION':
case 'SYNDICAT':
case 'ORDRE_PROFESSIONNEL':
case 'FEDERATION':
case 'RESEAU':
return 'FEDERATION';
default:
return 'ASSOCIATION'; // fallback sûr
}
}
/// Crée une demande de souscription
Future<SouscriptionStatusModel?> creerDemande({
required String typeFormule,
@@ -63,14 +100,15 @@ class SouscriptionDatasource {
}) async {
try {
final opts = await _authOptions();
final mappedType = _mapTypeOrganisationBilling(typeOrganisation);
final body = <String, dynamic>{
'typeFormule': typeFormule,
'plageMembres': plageMembres,
'typePeriode': typePeriode,
'organisationId': organisationId,
};
if (typeOrganisation != null && typeOrganisation.isNotEmpty) {
body['typeOrganisation'] = typeOrganisation;
if (mappedType != null && mappedType.isNotEmpty) {
body['typeOrganisation'] = mappedType;
}
final response = await _dio.post(
'$_base/api/souscriptions/demande',

View File

@@ -4,6 +4,7 @@ import '../../bloc/onboarding_bloc.dart';
import '../../../../core/di/injection.dart';
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import 'plan_selection_page.dart';
import 'period_selection_page.dart';
import 'subscription_summary_page.dart';
@@ -57,7 +58,7 @@ class _OnboardingFlowView extends StatelessWidget {
builder: (context, state) {
if (state is OnboardingLoading || state is OnboardingInitial || state is OnboardingPaiementConfirme) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -83,7 +84,7 @@ class _OnboardingFlowView extends StatelessWidget {
if (state is OnboardingError) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Center(
child: Padding(
padding: const EdgeInsets.all(32),
@@ -115,7 +116,7 @@ class _OnboardingFlowView extends StatelessWidget {
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
foregroundColor: AppColors.onPrimary,
),
child: const Text('Réessayer'),
),
@@ -153,6 +154,14 @@ class _OnboardingFlowView extends StatelessWidget {
);
}
// Échec de confirmation — retourne à la page Wave avec le SnackBar d'erreur
if (state is OnboardingPaiementEchoue) {
return WavePaymentPage(
souscription: state.souscription,
waveLaunchUrl: state.waveLaunchUrl,
);
}
if (state is OnboardingStepAttente) {
return AwaitingValidationPage(souscription: state.souscription);
}
@@ -176,7 +185,7 @@ class _RejectedPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(32),
@@ -247,7 +256,7 @@ class _RejectedPage extends StatelessWidget {
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
/// Header commun à toutes les étapes d'onboarding
@@ -19,9 +20,7 @@ class OnboardingStepHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
),
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient),
child: SafeArea(
bottom: false,
child: Padding(
@@ -29,6 +28,7 @@ class OnboardingStepHeader extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Barre de progression segmentée
Row(
children: List.generate(total, (i) {
final done = i < step;
@@ -37,9 +37,7 @@ class OnboardingStepHeader extends StatelessWidget {
height: 4,
margin: EdgeInsets.only(right: i < total - 1 ? 6 : 0),
decoration: BoxDecoration(
color: done
? Colors.white
: Colors.white.withOpacity(0.3),
color: done ? Colors.white : Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
@@ -81,76 +79,115 @@ class OnboardingStepHeader extends StatelessWidget {
}
}
/// Titre de section avec icône
/// Titre de section avec icône — dark/light adaptatif
class OnboardingSectionTitle extends StatelessWidget {
final IconData icon;
final String title;
final String? badge; // Ex: "Étape 1 complète ✓"
const OnboardingSectionTitle({
super.key,
required this.icon,
required this.title,
this.badge,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textColor = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
return Row(
children: [
Icon(icon, color: UnionFlowColors.unionGreen, size: 20),
const SizedBox(width: 8),
Text(
Expanded(
child: Text(
title,
style: const TextStyle(
color: UnionFlowColors.textPrimary,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
),
if (badge != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreen.withOpacity(0.12),
borderRadius: BorderRadius.circular(12),
),
child: Text(
badge!,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: UnionFlowColors.unionGreen,
),
),
),
],
);
}
}
/// Barre de bouton principale en bas de page
/// Barre de bouton principale en bas de page — dark/light adaptatif
class OnboardingBottomBar extends StatelessWidget {
final bool enabled;
final String label;
final VoidCallback onPressed;
final String? hint; // Texte optionnel au-dessus du bouton
const OnboardingBottomBar({
super.key,
required this.enabled,
required this.label,
required this.onPressed,
this.hint,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final bgColor = isDark ? AppColors.surfaceDark : AppColors.surface;
final borderColor = isDark ? AppColors.borderDark : AppColors.border;
final hintColor = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary;
return Container(
padding: EdgeInsets.fromLTRB(
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: bgColor,
border: Border(top: BorderSide(color: borderColor, width: 0.5)),
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(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (hint != null) ...[
Text(
hint!,
style: TextStyle(fontSize: 11, color: hintColor),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
],
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: enabled ? onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
disabledBackgroundColor: UnionFlowColors.border,
foregroundColor: Colors.white,
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: enabled ? 2 : 0,
shadowColor: UnionFlowColors.unionGreen.withOpacity(0.4),
),
@@ -164,6 +201,8 @@ class OnboardingBottomBar extends StatelessWidget {
),
),
),
],
),
);
}
}

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',
),
@@ -41,16 +46,19 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
@override
Widget build(BuildContext context) {
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,
),
@@ -171,34 +163,31 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
..._methods.map((m) => _MethodCard(
method: m,
selected: _selected == m.id,
onTap: m.available
? () => setState(() => _selected = m.id)
: null,
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 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,
),
],

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/formule_model.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
import 'onboarding_shared_widgets.dart';
@@ -17,12 +18,13 @@ class PlanSelectionPage extends StatefulWidget {
class _PlanSelectionPageState extends State<PlanSelectionPage> {
String? _selectedPlage;
String? _selectedFormule;
final _scrollController = ScrollController();
static const _plages = [
_Plage('PETITE', 'Petite', '1 100 membres', Icons.group_outlined, 'Associations naissantes et petites structures'),
_Plage('MOYENNE', 'Moyenne', '101 500 membres', Icons.groups_outlined, 'Associations établies en croissance'),
_Plage('GRANDE', 'Grande', '501 2 000 membres', Icons.corporate_fare_outlined, 'Grandes organisations régionales'),
_Plage('TRES_GRANDE', 'Très grande', '2 000+ membres', Icons.account_balance_outlined, 'Fédérations et réseaux nationaux'),
_Plage('PETITE', 'Petite', '1 100', Icons.group_outlined, 'Associations naissantes'),
_Plage('MOYENNE', 'Moyenne', '101 500', Icons.groups_outlined, 'Organisations établies'),
_Plage('GRANDE', 'Grande', '501 2 000', Icons.corporate_fare_outlined, 'Grandes structures'),
_Plage('TRES_GRANDE', 'Très grande', '2 000+', Icons.account_balance_outlined, 'Fédérations & réseaux'),
];
static const _formuleColors = {
@@ -50,10 +52,35 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
bool get _canProceed => _selectedPlage != null && _selectedFormule != null;
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onPlageSelected(String code) {
setState(() {
_selectedPlage = code;
_selectedFormule = null;
});
// Scroll vers la section formules après la frame courante
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
);
}
});
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column(
children: [
OnboardingStepHeader(
@@ -64,41 +91,85 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 100),
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Step 1a: Taille de l'organisation
// ── Section 1 : Taille ────────────────────────────────
OnboardingSectionTitle(
icon: Icons.people_alt_outlined,
title: 'Taille de votre organisation',
badge: _selectedPlage != null ? 'Sélectionné ✓' : null,
),
const SizedBox(height: 12),
...(_plages.map((p) => _PlageCard(
// Grille 2×2 pour scanner rapidement
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.45,
children: _plages.map((p) => _PlageCard(
plage: p,
selected: _selectedPlage == p.code,
onTap: () => setState(() {
_selectedPlage = p.code;
_selectedFormule = null;
}),
))),
isDark: isDark,
onTap: () => _onPlageSelected(p.code),
)).toList(),
),
if (_selectedPlage != null) ...[
// ── Section 2 : Formule (apparaît en fondu) ──────────
AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
transitionBuilder: (child, anim) => FadeTransition(
opacity: anim,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.08),
end: Offset.zero,
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut)),
child: child,
),
),
child: _selectedPlage == null
? const SizedBox.shrink()
: Column(
key: ValueKey(_selectedPlage),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 28),
OnboardingSectionTitle(
icon: Icons.workspace_premium_outlined,
title: 'Niveau d\'abonnement',
badge: _selectedFormule != null ? 'Sélectionné ✓' : null,
),
const SizedBox(height: 12),
const SizedBox(height: 4),
Text(
'Modifiable à tout moment depuis vos paramètres.',
style: TextStyle(
fontSize: 11,
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
),
),
const SizedBox(height: 14),
..._filteredFormules.map((f) => _FormuleCard(
formule: f,
color: _formuleColors[f.code] ?? UnionFlowColors.unionGreen,
icon: _formuleIcons[f.code] ?? Icons.star_border_rounded,
features: _formuleFeatures[f.code] ?? [],
selected: _selectedFormule == f.code,
isPopular: f.code == 'STANDARD',
isDark: isDark,
onTap: () => setState(() => _selectedFormule = f.code),
)),
],
),
),
],
),
),
@@ -107,7 +178,8 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
),
bottomNavigationBar: OnboardingBottomBar(
enabled: _canProceed,
label: 'Choisir la période',
label: 'Choisir la période',
hint: _canProceed ? null : 'Sélectionnez une taille et une formule pour continuer',
onPressed: () => context.read<OnboardingBloc>().add(
OnboardingFormuleSelected(
codeFormule: _selectedFormule!,
@@ -119,7 +191,7 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
}
}
// ─── Widgets locaux ──────────────────────────────────────────────────────────
// ─── Carte de taille (grille 2×2) ────────────────────────────────────────────
class _Plage {
final String code, label, sublabel, description;
@@ -130,115 +202,120 @@ class _Plage {
class _PlageCard extends StatelessWidget {
final _Plage plage;
final bool selected;
final bool isDark;
final VoidCallback onTap;
const _PlageCard({required this.plage, required this.selected, required this.onTap});
const _PlageCard({
required this.plage,
required this.selected,
required this.isDark,
required this.onTap,
});
@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 GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 10),
duration: const Duration(milliseconds: 180),
decoration: BoxDecoration(
color: selected ? UnionFlowColors.unionGreenPale : UnionFlowColors.surface,
color: selected
? UnionFlowColors.unionGreen.withOpacity(isDark ? 0.15 : 0.06)
: bgColor,
border: Border.all(
color: selected ? UnionFlowColors.unionGreen : UnionFlowColors.border,
color: selected ? UnionFlowColors.unionGreen : borderColor,
width: selected ? 2 : 1,
),
borderRadius: BorderRadius.circular(14),
boxShadow: selected ? UnionFlowColors.greenGlowShadow : UnionFlowColors.softShadow,
boxShadow: selected ? UnionFlowColors.greenGlowShadow : null,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: selected
? UnionFlowColors.unionGreen
: UnionFlowColors.unionGreenPale,
borderRadius: BorderRadius.circular(10),
),
child: Icon(plage.icon,
color: selected ? Colors.white : UnionFlowColors.unionGreen,
size: 22),
),
const SizedBox(width: 14),
Expanded(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
plage.label,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15,
color: selected
? UnionFlowColors.unionGreen
: UnionFlowColors.textPrimary,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
width: 36,
height: 36,
decoration: BoxDecoration(
color: selected
? UnionFlowColors.unionGreen.withOpacity(0.15)
: UnionFlowColors.surfaceVariant,
borderRadius: BorderRadius.circular(20),
),
child: Text(
plage.sublabel,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: selected
? UnionFlowColors.unionGreen
: UnionFlowColors.textSecondary,
: UnionFlowColors.unionGreen.withOpacity(0.1),
borderRadius: BorderRadius.circular(9),
),
),
),
],
),
const SizedBox(height: 2),
Text(
plage.description,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary),
),
],
child: Icon(
plage.icon,
color: selected ? Colors.white : UnionFlowColors.unionGreen,
size: 19,
),
),
Icon(
selected
? Icons.check_circle_rounded
: Icons.radio_button_unchecked,
color: selected
? UnionFlowColors.unionGreen
: UnionFlowColors.border,
size: 22,
color: selected ? UnionFlowColors.unionGreen : borderColor,
size: 18,
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
plage.label,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
color: selected ? UnionFlowColors.unionGreen : textPrimary,
),
),
const SizedBox(height: 1),
Text(
'${plage.sublabel} membres',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: selected
? UnionFlowColors.unionGreen.withOpacity(0.8)
: textSecondary,
),
),
Text(
plage.description,
style: TextStyle(
fontSize: 9,
color: textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
],
),
),
);
}
}
// ─── Carte de formule ─────────────────────────────────────────────────────────
class _FormuleCard extends StatelessWidget {
final FormuleModel formule;
final Color color;
final IconData icon;
final List<String> features;
final bool selected;
final bool isPopular;
final bool isDark;
final VoidCallback onTap;
const _FormuleCard({
@@ -247,50 +324,58 @@ class _FormuleCard extends StatelessWidget {
required this.icon,
required this.features,
required this.selected,
required this.isPopular,
required this.isDark,
required this.onTap,
});
@override
Widget build(BuildContext context) {
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 GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: bgCard,
border: Border.all(
color: selected ? color : UnionFlowColors.border,
color: selected ? color : borderColor,
width: selected ? 2.5 : 1,
),
borderRadius: BorderRadius.circular(16),
boxShadow: selected
? [
BoxShadow(
color: color.withOpacity(0.2),
blurRadius: 20,
offset: const Offset(0, 8),
)
]
: UnionFlowColors.softShadow,
? [BoxShadow(color: color.withOpacity(isDark ? 0.25 : 0.18), blurRadius: 20, offset: const Offset(0, 8))]
: isDark ? null : UnionFlowColors.softShadow,
),
child: Column(
children: [
// Header
// ── Header coloré ─────────────────────────────────
Container(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration(
color: selected ? color : color.withOpacity(0.06),
color: selected ? color : color.withOpacity(isDark ? 0.15 : 0.06),
borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
),
child: Row(
children: [
Icon(icon,
color: selected ? Colors.white : color, size: 24),
color: selected ? Colors.white : color,
size: 24),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Wrap permet au badge de passer à la ligne sur écrans étroits
Wrap(
spacing: 8,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
formule.libelle,
@@ -300,19 +385,41 @@ class _FormuleCard extends StatelessWidget {
fontSize: 16,
),
),
if (formule.description != null)
Text(
formule.description!,
style: TextStyle(
if (isPopular)
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: selected
? Colors.white70
: UnionFlowColors.textSecondary,
fontSize: 12,
? Colors.white.withOpacity(0.25)
: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'⭐ POPULAIRE',
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w800,
color: selected ? Colors.white : color,
letterSpacing: 0.3,
),
),
),
],
),
if (formule.description != null)
Text(
formule.description!,
style: TextStyle(
color: selected ? Colors.white70 : textSecondary,
fontSize: 12,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
// Prix
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
@@ -327,37 +434,82 @@ class _FormuleCard extends StatelessWidget {
Text(
'FCFA/mois',
style: TextStyle(
color: selected ? Colors.white70 : textSecondary,
fontSize: 10,
),
),
if (formule.prixAnnuel != null && formule.prixAnnuel! > 0) ...[
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: selected
? Colors.white70
: UnionFlowColors.textSecondary,
fontSize: 11,
? Colors.white.withOpacity(0.2)
: AppColors.success.withOpacity(0.12),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${_annualSavingPct(formule)}% /an',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w700,
color: selected ? Colors.white : AppColors.success,
),
),
),
],
],
),
],
),
),
// Features
// ── Features ──────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
child: Column(
children: features
.map((f) => Padding(
padding: const EdgeInsets.only(bottom: 6),
children: features.map((f) => Padding(
padding: const EdgeInsets.only(bottom: 7),
child: Row(
children: [
Icon(Icons.check_circle_outline_rounded,
size: 16, color: color),
const SizedBox(width: 8),
Text(f,
style: const TextStyle(
fontSize: 13,
color: UnionFlowColors.textPrimary)),
Expanded(
child: Text(
f,
style: TextStyle(fontSize: 13, color: textPrimary),
),
),
],
),
))
.toList(),
)).toList(),
),
),
// ── Sélection indicator ────────────────────────────
if (selected)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: color.withOpacity(isDark ? 0.2 : 0.08),
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle_rounded, size: 14, color: color),
const SizedBox(width: 6),
Text(
'Formule sélectionnée',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: color,
),
),
],
),
),
],
@@ -367,14 +519,15 @@ class _FormuleCard extends StatelessWidget {
}
String _formatPrix(double prix) {
if (prix >= 1000000) {
return '${(prix / 1000000).toStringAsFixed(1)} M';
}
if (prix >= 1000) {
final k = (prix / 1000).toStringAsFixed(0);
return '$k 000';
}
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
if (prix >= 1000) return '${(prix / 1000).toStringAsFixed(0)} k';
return prix.toStringAsFixed(0);
}
}
String _annualSavingPct(FormuleModel f) {
if (f.prixAnnuel == null || f.prixAnnuel! <= 0 || f.prixMensuel <= 0) return '0';
final monthly12 = f.prixMensuel * 12;
final saving = ((monthly12 - f.prixAnnuel!) / monthly12 * 100).round();
return saving.toString();
}
}

View File

@@ -2,7 +2,9 @@ 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 {
@@ -24,13 +26,6 @@ class SubscriptionSummaryPage extends StatelessWidget {
'ANNUEL': '20 % de remise',
};
static const _orgLabels = {
'ASSOCIATION': 'Association / ONG locale',
'MUTUELLE': 'Mutuelle (santé, fonctionnaires…)',
'COOPERATIVE': 'Coopérative / Microfinance',
'FEDERATION': 'Fédération / Grande ONG',
};
static const _plageLabels = {
'PETITE': '1 100 membres',
'MOYENNE': '101 500 membres',
@@ -40,25 +35,24 @@ class SubscriptionSummaryPage extends StatelessWidget {
@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: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Column(
children: [
// Header hero
// ── Header hero gradient ───────────────────────────
Container(
decoration: const BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
),
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient),
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
child: Column(
children: [
// Step bar
// Barre de progression — toutes complètes à l'étape 3
Row(
children: List.generate(3, (i) => Expanded(
child: Container(
@@ -84,52 +78,50 @@ class SubscriptionSummaryPage extends StatelessWidget {
),
),
const SizedBox(height: 20),
// Montant principal
// Icône principale
Container(
width: 90,
height: 90,
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),
border: Border.all(color: Colors.white.withOpacity(0.4), width: 2),
),
child: const Icon(Icons.receipt_long_rounded,
color: Colors.white, size: 44),
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: 40,
fontSize: 42,
fontWeight: FontWeight.w900,
letterSpacing: -1,
),
),
const Text(
'FCFA à régler',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w500),
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500),
),
// Badge remise
if (remise != null) ...[
const SizedBox(height: 8),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
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)),
border: Border.all(color: UnionFlowColors.goldLight.withOpacity(0.5)),
),
child: Text(
remise,
'🎉 $remise appliquée',
style: const TextStyle(
color: UnionFlowColors.goldLight,
fontSize: 12,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
@@ -141,28 +133,26 @@ class SubscriptionSummaryPage extends StatelessWidget {
),
),
// Content
// ── 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),
_DetailItem(
label: 'Type',
value: _orgLabels[souscription.typeOrganisation] ??
souscription.typeOrganisation),
_DetailItem(label: 'Nom', value: souscription.organisationNom!, bold: true),
if (souscription.typeOrganisation != null)
_DetailItem(label: 'Type', value: souscription.typeOrganisation!),
],
),
const SizedBox(height: 14),
@@ -173,20 +163,18 @@ class SubscriptionSummaryPage extends StatelessWidget {
title: 'Formule souscrite',
icon: Icons.workspace_premium_rounded,
iconColor: UnionFlowColors.gold,
isDark: isDark,
items: [
_DetailItem(label: 'Niveau', value: souscription.typeFormule, bold: true),
_DetailItem(
label: 'Niveau',
value: souscription.typeFormule,
bold: true),
_DetailItem(
label: 'Taille',
value: _plageLabels[souscription.plageMembres] ??
souscription.plageLibelle),
label: 'Capacité',
value: _plageLabels[souscription.plageMembres] ?? souscription.plageLibelle,
),
if (souscription.montantMensuelBase != null)
_DetailItem(
label: 'Prix de base',
value:
'${_formatPrix(souscription.montantMensuelBase!)} FCFA/mois'),
value: '${_formatPrix(souscription.montantMensuelBase!)} FCFA/mois',
),
],
),
const SizedBox(height: 14),
@@ -196,69 +184,84 @@ class SubscriptionSummaryPage extends StatelessWidget {
title: 'Facturation',
icon: Icons.calendar_today_rounded,
iconColor: UnionFlowColors.unionGreen,
isDark: isDark,
items: [
_DetailItem(
label: 'Période',
value:
_periodeLabels[souscription.typePeriode] ??
souscription.typePeriode),
value: _periodeLabels[souscription.typePeriode] ?? souscription.typePeriode,
),
if (souscription.coefficientApplique != null)
_DetailItem(
label: 'Coefficient',
value:
'×${souscription.coefficientApplique!.toStringAsFixed(4)}'),
if (souscription.dateDebut != null &&
souscription.dateFin != null) ...[
_DetailItem(
label: 'Début',
value: _formatDate(souscription.dateDebut!)),
_DetailItem(
label: 'Fin',
value: _formatDate(souscription.dateFin!)),
],
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: 14),
const SizedBox(height: 20),
// Montant total
// Bloc montant total — proéminent
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: UnionFlowColors.goldPale,
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: UnionFlowColors.goldGlowShadow,
border: Border.all(color: UnionFlowColors.gold.withOpacity(0.4)),
boxShadow: isDark ? null : UnionFlowColors.goldGlowShadow,
),
child: Row(
children: [
Container(
width: 48,
height: 48,
width: 52,
height: 52,
decoration: BoxDecoration(
gradient: UnionFlowColors.goldGradient,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(Icons.monetization_on_rounded,
color: Colors.white, size: 26),
color: Colors.white, size: 28),
),
const SizedBox(width: 14),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Total à payer',
Text(
'TOTAL À PAYER',
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 13),
color: isDark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
),
Text(
'${_formatPrix(montant)} FCFA',
style: const TextStyle(
color: UnionFlowColors.textPrimary,
fontSize: 22,
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,
),
),
],
@@ -269,35 +272,32 @@ class SubscriptionSummaryPage extends StatelessWidget {
),
const SizedBox(height: 20),
// Notes importantes
// Notes
_NoteBox(
icon: Icons.security_rounded,
iconColor: UnionFlowColors.unionGreen,
backgroundColor: UnionFlowColors.unionGreenPale,
borderColor: UnionFlowColors.unionGreen.withOpacity(0.25),
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 le paiement effectué, votre compte sera activé automatiquement.',
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,
backgroundColor: const Color(0xFFFFFBF0),
borderColor: UnionFlowColors.amber.withOpacity(0.3),
accentColor: UnionFlowColors.amber,
isDark: isDark,
title: 'Activation immédiate',
message:
'Dès que le paiement est confirmé par Wave, votre compte d\'administrateur est activé et vous pouvez accéder à toutes les fonctionnalités de votre formule.',
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,
backgroundColor: UnionFlowColors.infoPale,
borderColor: UnionFlowColors.info.withOpacity(0.2),
accentColor: UnionFlowColors.info,
isDark: isDark,
title: 'Besoin d\'aide ?',
message:
'En cas de problème lors du paiement, contactez notre support à support@unionflow.app — nous vous répondrons sous 24h.',
message: 'En cas de problème, contactez support@unionflow.app — réponse sous 24h.',
),
],
),
@@ -305,41 +305,10 @@ class SubscriptionSummaryPage extends StatelessWidget {
),
],
),
bottomNavigationBar: Container(
padding: EdgeInsets.fromLTRB(
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, -4),
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => context
.read<OnboardingBloc>()
.add(const OnboardingChoixPaiementOuvert()),
icon: const Icon(Icons.payment_rounded),
label: const Text(
'Choisir le moyen de paiement',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
shadowColor: UnionFlowColors.unionGreen.withOpacity(0.4),
elevation: 3,
),
),
),
bottomNavigationBar: OnboardingBottomBar(
enabled: true,
label: 'Choisir le moyen de paiement',
onPressed: () => context.read<OnboardingBloc>().add(const OnboardingChoixPaiementOuvert()),
),
);
}
@@ -347,18 +316,13 @@ class SubscriptionSummaryPage extends StatelessWidget {
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)}';
}
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) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
String _formatDate(DateTime date) =>
'${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
// ─── Widgets locaux ──────────────────────────────────────────────────────────
@@ -367,8 +331,7 @@ class _DetailItem {
final String label;
final String value;
final bool bold;
const _DetailItem(
{required this.label, required this.value, this.bold = false});
const _DetailItem({required this.label, required this.value, this.bold = false});
}
class _DetailCard extends StatelessWidget {
@@ -376,25 +339,34 @@ class _DetailCard extends StatelessWidget {
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: UnionFlowColors.surface,
color: bgColor,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
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(
@@ -403,7 +375,7 @@ class _DetailCard extends StatelessWidget {
width: 34,
height: 34,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
color: iconColor.withOpacity(isDark ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: iconColor, size: 18),
@@ -411,16 +383,16 @@ class _DetailCard extends StatelessWidget {
const SizedBox(width: 10),
Text(
title,
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
color: UnionFlowColors.textPrimary,
color: textPrimary,
),
),
],
),
),
const Divider(height: 1, color: UnionFlowColors.border),
Divider(height: 1, color: borderColor),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
child: Column(
@@ -430,23 +402,17 @@ class _DetailCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
item.label,
style: const TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 13),
),
width: 110,
child: Text(item.label,
style: TextStyle(color: textSecondary, fontSize: 13)),
),
Expanded(
child: Text(
item.value,
style: TextStyle(
color: UnionFlowColors.textPrimary,
color: textPrimary,
fontSize: 13,
fontWeight: item.bold
? FontWeight.w700
: FontWeight.w500,
fontWeight: item.bold ? FontWeight.w700 : FontWeight.w500,
),
),
),
@@ -464,26 +430,30 @@ class _DetailCard extends StatelessWidget {
class _NoteBox extends StatelessWidget {
final IconData icon;
final Color iconColor;
final Color backgroundColor;
final Color borderColor;
final Color accentColor;
final bool isDark;
final String title;
final String message;
const _NoteBox({
required this.icon,
required this.iconColor,
required this.backgroundColor,
required this.borderColor,
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: backgroundColor,
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor),
),
@@ -498,19 +468,12 @@ class _NoteBox extends StatelessWidget {
children: [
Text(
title,
style: TextStyle(
color: iconColor,
fontWeight: FontWeight.w700,
fontSize: 13,
),
style: TextStyle(color: iconColor, fontWeight: FontWeight.w700, fontSize: 13),
),
const SizedBox(height: 3),
Text(
message,
style: const TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 12,
height: 1.5),
style: TextStyle(color: textSecondary, fontSize: 12, height: 1.5),
),
],
),

View File

@@ -4,6 +4,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../bloc/onboarding_bloc.dart';
import '../../data/models/souscription_status_model.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
import '../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../core/config/environment.dart';
/// Étape 4 — Lancement du paiement Wave + attente du retour
@@ -33,6 +34,9 @@ class _WavePaymentPageState extends State<WavePaymentPage>
widget.waveLaunchUrl.contains('localhost') ||
!AppConfig.isProd;
// Couleur de marque Wave (volontairement hardcodée)
static const _waveBlue = Color(0xFF00B9F1);
@override
void initState() {
super.initState();
@@ -72,10 +76,10 @@ class _WavePaymentPageState extends State<WavePaymentPage>
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
SnackBar(
content: const Text(
'Impossible d\'ouvrir Wave. Vérifiez que l\'application est installée.'),
backgroundColor: UnionFlowColors.error,
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
),
);
@@ -85,77 +89,131 @@ class _WavePaymentPageState extends State<WavePaymentPage>
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final bgSurface = 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;
final montant = widget.souscription.montantTotal ?? 0;
const waveBlue = Color(0xFF00B9F1);
return Scaffold(
backgroundColor: UnionFlowColors.background,
return BlocListener<OnboardingBloc, OnboardingState>(
listener: (context, state) {
// Afficher snackbar si la confirmation a échoué
if (state is OnboardingPaiementEchoue) {
setState(() {
_paymentLaunched = false;
_appResumed = false;
_simulating = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(children: [
const Icon(Icons.error_outline, color: Colors.white, size: 18),
const SizedBox(width: 8),
Expanded(child: Text(state.message)),
]),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 5),
),
);
}
},
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// ── Top bar ───────────────────────────────────
Row(
children: [
if (!_paymentLaunched && !_simulating)
IconButton(
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(Icons.arrow_back_rounded),
color: UnionFlowColors.textSecondary,
color: textSecondary,
),
const Spacer(),
if (_isMock)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: UnionFlowColors.warningPale,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: UnionFlowColors.warning.withOpacity(0.4)),
_buildDevBadge()
else
_buildWaveBadge(),
],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
// ── Contenu principal ─────────────────────────
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.science_rounded,
size: 13, color: UnionFlowColors.warning),
SizedBox(width: 4),
Text(
'Mode dev',
style: TextStyle(
color: UnionFlowColors.warning,
fontSize: 11,
fontWeight: FontWeight.w700,
if (_simulating)
_buildSimulatingView(textPrimary, textSecondary)
else
_buildPaymentView(
montant: montant,
bgSurface: bgSurface,
borderColor: borderColor,
textPrimary: textPrimary,
textSecondary: textSecondary,
isDark: isDark,
),
],
),
),
],
),
)
else
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
),
),
),
);
}
// ─── Badges ────────────────────────────────────────────────
Widget _buildDevBadge() => Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: waveBlue.withOpacity(0.1),
color: AppColors.warning.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.warning.withOpacity(0.4)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.science_rounded, size: 13, color: AppColors.warning),
SizedBox(width: 4),
Text(
'Mode dev',
style: TextStyle(
color: AppColors.warning,
fontSize: 11,
fontWeight: FontWeight.w700),
),
],
),
);
Widget _buildWaveBadge() => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: _waveBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Wave Mobile Money',
style: TextStyle(
color: waveBlue,
fontSize: 12,
fontWeight: FontWeight.w700,
color: _waveBlue, fontSize: 12, fontWeight: FontWeight.w700),
),
),
),
],
),
Expanded(
child: Column(
);
// ─── Vue simulation ───────────────────────────────────────
Widget _buildSimulatingView(Color textPrimary, Color textSecondary) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_simulating) ...[
// Animation de simulation
Container(
width: 110,
height: 110,
@@ -168,7 +226,7 @@ class _WavePaymentPageState extends State<WavePaymentPage>
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: waveBlue.withOpacity(0.35),
color: _waveBlue.withOpacity(0.35),
blurRadius: 24,
offset: const Offset(0, 10),
),
@@ -178,40 +236,48 @@ class _WavePaymentPageState extends State<WavePaymentPage>
child: SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3),
),
),
),
const SizedBox(height: 28),
const Text(
Text(
'Simulation du paiement…',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: textPrimary),
),
const SizedBox(height: 8),
const Text(
'Confirmation en cours',
style: TextStyle(
color: UnionFlowColors.textSecondary, fontSize: 14),
Text(
'Confirmation en cours auprès du serveur',
style: TextStyle(color: textSecondary, fontSize: 14),
),
] else ...[
// Logo Wave
],
);
}
// ─── Vue paiement ─────────────────────────────────────────
Widget _buildPaymentView({
required double montant,
required Color bgSurface,
required Color borderColor,
required Color textPrimary,
required Color textSecondary,
required bool isDark,
}) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo Wave — fond blanc intentionnel (brand)
Container(
width: 110,
height: 110,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
border: Border.all(color: UnionFlowColors.border),
border: Border.all(color: borderColor),
boxShadow: [
BoxShadow(
color: waveBlue.withOpacity(0.2),
color: _waveBlue.withOpacity(0.2),
blurRadius: 24,
offset: const Offset(0, 10),
),
@@ -221,11 +287,8 @@ class _WavePaymentPageState extends State<WavePaymentPage>
child: Image.asset(
'assets/images/payment_methods/wave/logo.png',
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Icon(
Icons.waves_rounded,
color: waveBlue,
size: 52,
),
errorBuilder: (_, __, ___) =>
const Icon(Icons.waves_rounded, color: _waveBlue, size: 52),
),
),
const SizedBox(height: 28),
@@ -233,14 +296,8 @@ class _WavePaymentPageState extends State<WavePaymentPage>
Text(
_paymentLaunched
? 'Paiement en cours…'
: _isMock
? 'Simuler le paiement'
: 'Prêt à payer',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: UnionFlowColors.textPrimary,
),
: _isMock ? 'Simuler le paiement' : 'Prêt à payer',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w800, color: textPrimary),
),
const SizedBox(height: 8),
@@ -253,17 +310,14 @@ class _WavePaymentPageState extends State<WavePaymentPage>
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w900,
color: waveBlue,
color: _waveBlue,
letterSpacing: -0.5,
),
),
const TextSpan(
TextSpan(
text: 'FCFA',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textSecondary,
),
fontSize: 18, fontWeight: FontWeight.w700, color: textSecondary),
),
],
),
@@ -272,9 +326,7 @@ class _WavePaymentPageState extends State<WavePaymentPage>
const SizedBox(height: 4),
Text(
widget.souscription.organisationNom!,
style: const TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 13),
style: TextStyle(color: textSecondary, fontSize: 13),
),
],
const SizedBox(height: 32),
@@ -285,24 +337,19 @@ class _WavePaymentPageState extends State<WavePaymentPage>
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: UnionFlowColors.warningPale,
color: AppColors.warning.withOpacity(isDark ? 0.15 : 0.08),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color:
UnionFlowColors.warning.withOpacity(0.3)),
border: Border.all(color: AppColors.warning.withOpacity(0.3)),
),
child: const Row(
children: [
Icon(Icons.science_outlined,
color: UnionFlowColors.warning, size: 16),
Icon(Icons.science_outlined, color: AppColors.warning, size: 16),
SizedBox(width: 8),
Expanded(
child: Text(
'Environnement de développement — le paiement sera simulé automatiquement.',
'Environnement de développement — le paiement sera simulé automatiquement. En production, vous serez redirigé vers l\'application Wave.',
style: TextStyle(
fontSize: 12,
color: UnionFlowColors.warning,
height: 1.4),
fontSize: 12, color: AppColors.warning, height: 1.4),
),
),
],
@@ -316,20 +363,15 @@ class _WavePaymentPageState extends State<WavePaymentPage>
? Icons.play_circle_rounded
: Icons.open_in_new_rounded),
label: Text(
_isMock
? 'Simuler le paiement Wave'
: 'Ouvrir Wave',
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w700),
_isMock ? 'Simuler le paiement Wave' : 'Ouvrir Wave',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
style: ElevatedButton.styleFrom(
backgroundColor: waveBlue,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
shadowColor: waveBlue.withOpacity(0.4),
backgroundColor: _waveBlue,
foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
shadowColor: _waveBlue.withOpacity(0.4),
elevation: 3,
),
),
@@ -339,36 +381,29 @@ class _WavePaymentPageState extends State<WavePaymentPage>
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
color: bgSurface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
border: Border.all(color: borderColor),
boxShadow: isDark ? null : UnionFlowColors.softShadow,
),
child: Column(
children: [
const SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
color: waveBlue,
strokeWidth: 3,
),
child: CircularProgressIndicator(color: _waveBlue, strokeWidth: 3),
),
const SizedBox(height: 16),
const Text(
Text(
'Paiement en cours dans Wave',
style: TextStyle(
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
style: TextStyle(fontWeight: FontWeight.w700, color: textPrimary),
),
const SizedBox(height: 6),
const Text(
Text(
'Revenez dans l\'app une fois\nvotre paiement confirmé.',
textAlign: TextAlign.center,
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 13,
height: 1.4),
color: textSecondary, fontSize: 13, height: 1.4),
),
],
),
@@ -380,21 +415,16 @@ class _WavePaymentPageState extends State<WavePaymentPage>
onPressed: () => context
.read<OnboardingBloc>()
.add(const OnboardingRetourDepuisWave()),
icon: const Icon(
Icons.check_circle_outline_rounded),
icon: const Icon(Icons.check_circle_outline_rounded),
label: const Text(
'J\'ai effectué le paiement',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
foregroundColor: AppColors.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
),
@@ -403,18 +433,10 @@ class _WavePaymentPageState extends State<WavePaymentPage>
onPressed: _lancerOuSimuler,
icon: const Icon(Icons.refresh_rounded, size: 18),
label: const Text('Rouvrir Wave'),
style: TextButton.styleFrom(
foregroundColor: waveBlue),
style: TextButton.styleFrom(foregroundColor: _waveBlue),
),
],
],
],
),
),
],
),
),
),
);
}