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:
@@ -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());
|
||||
final souscId = _souscription?.souscriptionId;
|
||||
final waveUrl = _souscription?.waveLaunchUrl;
|
||||
|
||||
if (souscId == null) {
|
||||
emit(const OnboardingError('Souscription introuvable.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final souscId = _souscription?.souscriptionId;
|
||||
if (souscId != null) {
|
||||
await _datasource.confirmerPaiement(souscId);
|
||||
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 ?? '',
|
||||
));
|
||||
}
|
||||
// Émettre OnboardingPaiementConfirme pour déclencher re-check du compte
|
||||
// Si le backend auto-active le compte, AuthStatusChecked redirigera vers dashboard
|
||||
emit(OnboardingPaiementConfirme());
|
||||
} catch (e) {
|
||||
emit(OnboardingPaiementConfirme());
|
||||
emit(OnboardingPaiementEchoue(
|
||||
message: 'Erreur lors de la confirmation: ${e.toString().replaceFirst("Exception: ", "")}',
|
||||
souscription: _souscription!,
|
||||
waveLaunchUrl: waveUrl ?? '',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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,88 +79,129 @@ 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(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: UnionFlowColors.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
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(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: enabled ? onPressed : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
disabledBackgroundColor: UnionFlowColors.border,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (hint != null) ...[
|
||||
Text(
|
||||
hint!,
|
||||
style: TextStyle(fontSize: 11, color: hintColor),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
elevation: enabled ? 2 : 0,
|
||||
shadowColor: UnionFlowColors.unionGreen.withOpacity(0.4),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.3,
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: enabled ? onPressed : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
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)),
|
||||
elevation: enabled ? 2 : 0,
|
||||
shadowColor: UnionFlowColors.unionGreen.withOpacity(0.4),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,30 +18,31 @@ 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 = {
|
||||
'BASIC': UnionFlowColors.unionGreen,
|
||||
'BASIC': UnionFlowColors.unionGreen,
|
||||
'STANDARD': UnionFlowColors.gold,
|
||||
'PREMIUM': UnionFlowColors.indigo,
|
||||
'PREMIUM': UnionFlowColors.indigo,
|
||||
};
|
||||
|
||||
static const _formuleIcons = {
|
||||
'BASIC': Icons.star_border_rounded,
|
||||
'BASIC': Icons.star_border_rounded,
|
||||
'STANDARD': Icons.star_half_rounded,
|
||||
'PREMIUM': Icons.star_rounded,
|
||||
'PREMIUM': Icons.star_rounded,
|
||||
};
|
||||
|
||||
static const _formuleFeatures = {
|
||||
'BASIC': ['Gestion des membres', 'Cotisations de base', 'Rapports mensuels', 'Support email'],
|
||||
'BASIC': ['Gestion des membres', 'Cotisations de base', 'Rapports mensuels', 'Support email'],
|
||||
'STANDARD': ['Tout Basic +', 'Événements & solidarité', 'Communication interne', 'Tableaux de bord avancés', 'Support prioritaire'],
|
||||
'PREMIUM': ['Tout Standard +', 'Multi-organisations', 'Analytics temps réel', 'API ouverte', 'Support dédié 24/7'],
|
||||
'PREMIUM': ['Tout Standard +', 'Multi-organisations', 'Analytics temps réel', 'API ouverte', 'Support dédié 24/7'],
|
||||
};
|
||||
|
||||
List<FormuleModel> get _filteredFormules => widget.formules
|
||||
@@ -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(
|
||||
plage: p,
|
||||
selected: _selectedPlage == p.code,
|
||||
onTap: () => setState(() {
|
||||
_selectedPlage = p.code;
|
||||
_selectedFormule = null;
|
||||
}),
|
||||
))),
|
||||
|
||||
if (_selectedPlage != null) ...[
|
||||
const SizedBox(height: 28),
|
||||
OnboardingSectionTitle(
|
||||
icon: Icons.workspace_premium_outlined,
|
||||
title: 'Niveau d\'abonnement',
|
||||
// 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,
|
||||
isDark: isDark,
|
||||
onTap: () => _onPlageSelected(p.code),
|
||||
)).toList(),
|
||||
),
|
||||
|
||||
// ── 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,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
..._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,
|
||||
onTap: () => setState(() => _selectedFormule = f.code),
|
||||
)),
|
||||
],
|
||||
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: 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,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? UnionFlowColors.unionGreen
|
||||
: UnionFlowColors.unionGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Icon(
|
||||
plage.icon,
|
||||
color: selected ? Colors.white : UnionFlowColors.unionGreen,
|
||||
size: 22),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
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),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
plage.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary),
|
||||
),
|
||||
],
|
||||
size: 19,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
selected
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.radio_button_unchecked,
|
||||
color: selected
|
||||
? UnionFlowColors.unionGreen
|
||||
: UnionFlowColors.border,
|
||||
size: 22,
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
selected
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.radio_button_unchecked,
|
||||
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,72 +324,102 @@ 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: [
|
||||
Text(
|
||||
formule.libelle,
|
||||
style: TextStyle(
|
||||
color: selected ? Colors.white : color,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16,
|
||||
),
|
||||
// Wrap permet au badge de passer à la ligne sur écrans étroits
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
formule.libelle,
|
||||
style: TextStyle(
|
||||
color: selected ? Colors.white : color,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (isPopular)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? 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
|
||||
: UnionFlowColors.textSecondary,
|
||||
color: selected ? Colors.white70 : textSecondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Prix
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
@@ -325,41 +432,86 @@ class _FormuleCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'FCFA / mois',
|
||||
'FCFA/mois',
|
||||
style: TextStyle(
|
||||
color: selected
|
||||
? Colors.white70
|
||||
: UnionFlowColors.textSecondary,
|
||||
fontSize: 11,
|
||||
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.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),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
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),
|
||||
Expanded(
|
||||
child: Text(
|
||||
f,
|
||||
style: TextStyle(fontSize: 13, color: textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)).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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -11,54 +13,46 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
||||
const SubscriptionSummaryPage({super.key, required this.souscription});
|
||||
|
||||
static const _periodeLabels = {
|
||||
'MENSUEL': 'Mensuel',
|
||||
'TRIMESTRIEL': 'Trimestriel',
|
||||
'SEMESTRIEL': 'Semestriel',
|
||||
'ANNUEL': 'Annuel',
|
||||
'MENSUEL': 'Mensuel',
|
||||
'TRIMESTRIEL': 'Trimestriel',
|
||||
'SEMESTRIEL': 'Semestriel',
|
||||
'ANNUEL': 'Annuel',
|
||||
};
|
||||
|
||||
static const _periodeRemises = {
|
||||
'MENSUEL': null,
|
||||
'TRIMESTRIEL': '–5% de remise',
|
||||
'SEMESTRIEL': '–10% de remise',
|
||||
'ANNUEL': '–20% de remise',
|
||||
};
|
||||
|
||||
static const _orgLabels = {
|
||||
'ASSOCIATION': 'Association / ONG locale',
|
||||
'MUTUELLE': 'Mutuelle (santé, fonctionnaires…)',
|
||||
'COOPERATIVE': 'Coopérative / Microfinance',
|
||||
'FEDERATION': 'Fédération / Grande ONG',
|
||||
'MENSUEL': null,
|
||||
'TRIMESTRIEL': '–5 % de remise',
|
||||
'SEMESTRIEL': '–10 % de remise',
|
||||
'ANNUEL': '–20 % de remise',
|
||||
};
|
||||
|
||||
static const _plageLabels = {
|
||||
'PETITE': '1–100 membres',
|
||||
'MOYENNE': '101–500 membres',
|
||||
'GRANDE': '501–2 000 membres',
|
||||
'PETITE': '1 – 100 membres',
|
||||
'MOYENNE': '101 – 500 membres',
|
||||
'GRANDE': '501 – 2 000 membres',
|
||||
'TRES_GRANDE': '2 000+ membres',
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final montant = souscription.montantTotal ?? 0;
|
||||
final remise = _periodeRemises[souscription.typePeriode];
|
||||
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'),
|
||||
label: 'Prix de base',
|
||||
value: '${_formatPrix(souscription.montantMensuelBase!)} FCFA/mois',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
@@ -196,71 +184,86 @@ 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),
|
||||
label: 'Période',
|
||||
value: _periodeLabels[souscription.typePeriode] ?? souscription.typePeriode,
|
||||
),
|
||||
if (souscription.coefficientApplique != null)
|
||||
_DetailItem(
|
||||
label: 'Coefficient',
|
||||
value:
|
||||
'×${souscription.coefficientApplique!.toStringAsFixed(4)}'),
|
||||
if (souscription.dateDebut != null &&
|
||||
souscription.dateFin != null) ...[
|
||||
_DetailItem(
|
||||
label: 'Début',
|
||||
value: _formatDate(souscription.dateDebut!)),
|
||||
_DetailItem(
|
||||
label: 'Fin',
|
||||
value: _formatDate(souscription.dateFin!)),
|
||||
],
|
||||
label: 'Coefficient',
|
||||
value: '×${souscription.coefficientApplique!.toStringAsFixed(4)}',
|
||||
),
|
||||
if (souscription.dateDebut != null)
|
||||
_DetailItem(label: 'Début', value: _formatDate(souscription.dateDebut!)),
|
||||
if (souscription.dateFin != null)
|
||||
_DetailItem(label: 'Fin', value: _formatDate(souscription.dateFin!)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,336 +89,354 @@ 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,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (!_paymentLaunched && !_simulating)
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
color: UnionFlowColors.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)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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 (_simulating) ...[
|
||||
// Animation de simulation
|
||||
Container(
|
||||
width: 110,
|
||||
height: 110,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF00B9F1), Color(0xFF0096C7)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: waveBlue.withOpacity(0.35),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_paymentLaunched && !_simulating)
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
color: textSecondary,
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
const Text(
|
||||
'Simulation du paiement…',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Confirmation en cours',
|
||||
style: TextStyle(
|
||||
color: UnionFlowColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
] else ...[
|
||||
// Logo Wave
|
||||
Container(
|
||||
width: 110,
|
||||
height: 110,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: UnionFlowColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: waveBlue.withOpacity(0.2),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Image.asset(
|
||||
'assets/images/payment_methods/wave/logo.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) => const Icon(
|
||||
Icons.waves_rounded,
|
||||
color: waveBlue,
|
||||
size: 52,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
Text(
|
||||
_paymentLaunched
|
||||
? 'Paiement en cours…'
|
||||
: _isMock
|
||||
? 'Simuler le paiement'
|
||||
: 'Prêt à payer',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Montant
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${_formatPrix(montant)} ',
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: waveBlue,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: 'FCFA',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.souscription.organisationNom != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.souscription.organisationNom!,
|
||||
style: const TextStyle(
|
||||
color: UnionFlowColors.textSecondary,
|
||||
fontSize: 13),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
|
||||
if (!_paymentLaunched) ...[
|
||||
if (_isMock)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.warningPale,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color:
|
||||
UnionFlowColors.warning.withOpacity(0.3)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.science_outlined,
|
||||
color: UnionFlowColors.warning, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Environnement de développement — le paiement sera simulé automatiquement.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.warning,
|
||||
height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _lancerOuSimuler,
|
||||
icon: Icon(_isMock
|
||||
? 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),
|
||||
),
|
||||
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),
|
||||
elevation: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Paiement lancé en prod
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: waveBlue,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Paiement en cours dans Wave',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'Revenez dans l\'app une fois\nvotre paiement confirmé.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: UnionFlowColors.textSecondary,
|
||||
fontSize: 13,
|
||||
height: 1.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => context
|
||||
.read<OnboardingBloc>()
|
||||
.add(const OnboardingRetourDepuisWave()),
|
||||
icon: const Icon(
|
||||
Icons.check_circle_outline_rounded),
|
||||
label: const Text(
|
||||
'J\'ai effectué le paiement',
|
||||
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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton.icon(
|
||||
onPressed: _lancerOuSimuler,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 18),
|
||||
label: const Text('Rouvrir Wave'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: waveBlue),
|
||||
),
|
||||
],
|
||||
],
|
||||
const Spacer(),
|
||||
if (_isMock)
|
||||
_buildDevBadge()
|
||||
else
|
||||
_buildWaveBadge(),
|
||||
],
|
||||
),
|
||||
|
||||
// ── Contenu principal ─────────────────────────
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_simulating)
|
||||
_buildSimulatingView(textPrimary, textSecondary)
|
||||
else
|
||||
_buildPaymentView(
|
||||
montant: montant,
|
||||
bgSurface: bgSurface,
|
||||
borderColor: borderColor,
|
||||
textPrimary: textPrimary,
|
||||
textSecondary: textSecondary,
|
||||
isDark: isDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Badges ────────────────────────────────────────────────
|
||||
|
||||
Widget _buildDevBadge() => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
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),
|
||||
),
|
||||
);
|
||||
|
||||
// ─── Vue simulation ───────────────────────────────────────
|
||||
|
||||
Widget _buildSimulatingView(Color textPrimary, Color textSecondary) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 110,
|
||||
height: 110,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF00B9F1), Color(0xFF0096C7)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _waveBlue.withOpacity(0.35),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Text(
|
||||
'Simulation du paiement…',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: textPrimary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Confirmation en cours auprès du serveur',
|
||||
style: TextStyle(color: textSecondary, fontSize: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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: borderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _waveBlue.withOpacity(0.2),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Image.asset(
|
||||
'assets/images/payment_methods/wave/logo.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.waves_rounded, color: _waveBlue, size: 52),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
Text(
|
||||
_paymentLaunched
|
||||
? 'Paiement en cours…'
|
||||
: _isMock ? 'Simuler le paiement' : 'Prêt à payer',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w800, color: textPrimary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Montant
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${_formatPrix(montant)} ',
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: _waveBlue,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'FCFA',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w700, color: textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.souscription.organisationNom != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.souscription.organisationNom!,
|
||||
style: TextStyle(color: textSecondary, fontSize: 13),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
|
||||
if (!_paymentLaunched) ...[
|
||||
if (_isMock)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withOpacity(isDark ? 0.15 : 0.08),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.warning.withOpacity(0.3)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.science_outlined, color: AppColors.warning, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Environnement de développement — le paiement sera simulé automatiquement. En production, vous serez redirigé vers l\'application Wave.',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppColors.warning, height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _lancerOuSimuler,
|
||||
icon: Icon(_isMock
|
||||
? 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),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _waveBlue,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
shadowColor: _waveBlue.withOpacity(0.4),
|
||||
elevation: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Paiement lancé en prod
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: bgSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Paiement en cours dans Wave',
|
||||
style: TextStyle(fontWeight: FontWeight.w700, color: textPrimary),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Revenez dans l\'app une fois\nvotre paiement confirmé.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: textSecondary, fontSize: 13, height: 1.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => context
|
||||
.read<OnboardingBloc>()
|
||||
.add(const OnboardingRetourDepuisWave()),
|
||||
icon: const Icon(Icons.check_circle_outline_rounded),
|
||||
label: const Text(
|
||||
'J\'ai effectué le paiement',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
foregroundColor: AppColors.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton.icon(
|
||||
onPressed: _lancerOuSimuler,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 18),
|
||||
label: const Text('Rouvrir Wave'),
|
||||
style: TextButton.styleFrom(foregroundColor: _waveBlue),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user