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
|
/// Paiement confirmé — déclenche un re-check du statut du compte
|
||||||
class OnboardingPaiementConfirme extends OnboardingState {}
|
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
|
/// Étape 5 : en attente de validation SuperAdmin
|
||||||
class OnboardingStepAttente extends OnboardingState {
|
class OnboardingStepAttente extends OnboardingState {
|
||||||
final SouscriptionStatusModel? souscription;
|
final SouscriptionStatusModel? souscription;
|
||||||
@@ -316,16 +330,32 @@ class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
|
|||||||
Future<void> _onRetourDepuisWave(
|
Future<void> _onRetourDepuisWave(
|
||||||
OnboardingRetourDepuisWave event, Emitter<OnboardingState> emit) async {
|
OnboardingRetourDepuisWave event, Emitter<OnboardingState> emit) async {
|
||||||
emit(OnboardingLoading());
|
emit(OnboardingLoading());
|
||||||
try {
|
|
||||||
final souscId = _souscription?.souscriptionId;
|
final souscId = _souscription?.souscriptionId;
|
||||||
if (souscId != null) {
|
final waveUrl = _souscription?.waveLaunchUrl;
|
||||||
await _datasource.confirmerPaiement(souscId);
|
|
||||||
|
if (souscId == null) {
|
||||||
|
emit(const OnboardingError('Souscription introuvable.'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Émettre OnboardingPaiementConfirme pour déclencher re-check du compte
|
|
||||||
// Si le backend auto-active le compte, AuthStatusChecked redirigera vers dashboard
|
try {
|
||||||
|
final success = await _datasource.confirmerPaiement(souscId);
|
||||||
|
if (success) {
|
||||||
emit(OnboardingPaiementConfirme());
|
emit(OnboardingPaiementConfirme());
|
||||||
|
} else {
|
||||||
|
// La confirmation a échoué côté backend — l'utilisateur doit réessayer
|
||||||
|
emit(OnboardingPaiementEchoue(
|
||||||
|
message: 'Le paiement n\'a pas pu être confirmé. Vérifiez que le paiement Wave a bien été effectué et réessayez.',
|
||||||
|
souscription: _souscription!,
|
||||||
|
waveLaunchUrl: waveUrl ?? '',
|
||||||
|
));
|
||||||
|
}
|
||||||
} catch (e) {
|
} 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;
|
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
|
/// Crée une demande de souscription
|
||||||
Future<SouscriptionStatusModel?> creerDemande({
|
Future<SouscriptionStatusModel?> creerDemande({
|
||||||
required String typeFormule,
|
required String typeFormule,
|
||||||
@@ -63,14 +100,15 @@ class SouscriptionDatasource {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final opts = await _authOptions();
|
final opts = await _authOptions();
|
||||||
|
final mappedType = _mapTypeOrganisationBilling(typeOrganisation);
|
||||||
final body = <String, dynamic>{
|
final body = <String, dynamic>{
|
||||||
'typeFormule': typeFormule,
|
'typeFormule': typeFormule,
|
||||||
'plageMembres': plageMembres,
|
'plageMembres': plageMembres,
|
||||||
'typePeriode': typePeriode,
|
'typePeriode': typePeriode,
|
||||||
'organisationId': organisationId,
|
'organisationId': organisationId,
|
||||||
};
|
};
|
||||||
if (typeOrganisation != null && typeOrganisation.isNotEmpty) {
|
if (mappedType != null && mappedType.isNotEmpty) {
|
||||||
body['typeOrganisation'] = typeOrganisation;
|
body['typeOrganisation'] = mappedType;
|
||||||
}
|
}
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
'$_base/api/souscriptions/demande',
|
'$_base/api/souscriptions/demande',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import '../../bloc/onboarding_bloc.dart';
|
|||||||
import '../../../../core/di/injection.dart';
|
import '../../../../core/di/injection.dart';
|
||||||
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
|
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
|
||||||
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||||||
|
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||||
import 'plan_selection_page.dart';
|
import 'plan_selection_page.dart';
|
||||||
import 'period_selection_page.dart';
|
import 'period_selection_page.dart';
|
||||||
import 'subscription_summary_page.dart';
|
import 'subscription_summary_page.dart';
|
||||||
@@ -57,7 +58,7 @@ class _OnboardingFlowView extends StatelessWidget {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is OnboardingLoading || state is OnboardingInitial || state is OnboardingPaiementConfirme) {
|
if (state is OnboardingLoading || state is OnboardingInitial || state is OnboardingPaiementConfirme) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UnionFlowColors.background,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -83,7 +84,7 @@ class _OnboardingFlowView extends StatelessWidget {
|
|||||||
|
|
||||||
if (state is OnboardingError) {
|
if (state is OnboardingError) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UnionFlowColors.background,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
@@ -115,7 +116,7 @@ class _OnboardingFlowView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: UnionFlowColors.unionGreen,
|
backgroundColor: UnionFlowColors.unionGreen,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: AppColors.onPrimary,
|
||||||
),
|
),
|
||||||
child: const Text('Réessayer'),
|
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) {
|
if (state is OnboardingStepAttente) {
|
||||||
return AwaitingValidationPage(souscription: state.souscription);
|
return AwaitingValidationPage(souscription: state.souscription);
|
||||||
}
|
}
|
||||||
@@ -176,7 +185,7 @@ class _RejectedPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UnionFlowColors.background,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
@@ -247,7 +256,7 @@ class _RejectedPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: UnionFlowColors.unionGreen,
|
backgroundColor: UnionFlowColors.unionGreen,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: AppColors.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||||
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||||||
|
|
||||||
/// Header commun à toutes les étapes d'onboarding
|
/// Header commun à toutes les étapes d'onboarding
|
||||||
@@ -19,9 +20,7 @@ class OnboardingStepHeader extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient),
|
||||||
gradient: UnionFlowColors.primaryGradient,
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -29,6 +28,7 @@ class OnboardingStepHeader extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Barre de progression segmentée
|
||||||
Row(
|
Row(
|
||||||
children: List.generate(total, (i) {
|
children: List.generate(total, (i) {
|
||||||
final done = i < step;
|
final done = i < step;
|
||||||
@@ -37,9 +37,7 @@ class OnboardingStepHeader extends StatelessWidget {
|
|||||||
height: 4,
|
height: 4,
|
||||||
margin: EdgeInsets.only(right: i < total - 1 ? 6 : 0),
|
margin: EdgeInsets.only(right: i < total - 1 ? 6 : 0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: done
|
color: done ? Colors.white : Colors.white.withOpacity(0.3),
|
||||||
? Colors.white
|
|
||||||
: Colors.white.withOpacity(0.3),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -81,76 +79,115 @@ class OnboardingStepHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Titre de section avec icône
|
/// Titre de section avec icône — dark/light adaptatif
|
||||||
class OnboardingSectionTitle extends StatelessWidget {
|
class OnboardingSectionTitle extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
|
final String? badge; // Ex: "Étape 1 complète ✓"
|
||||||
|
|
||||||
const OnboardingSectionTitle({
|
const OnboardingSectionTitle({
|
||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.title,
|
required this.title,
|
||||||
|
this.badge,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final textColor = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: UnionFlowColors.unionGreen, size: 20),
|
Icon(icon, color: UnionFlowColors.unionGreen, size: 20),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textPrimary,
|
color: textColor,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 16,
|
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 {
|
class OnboardingBottomBar extends StatelessWidget {
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
final String? hint; // Texte optionnel au-dessus du bouton
|
||||||
|
|
||||||
const OnboardingBottomBar({
|
const OnboardingBottomBar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
|
this.hint,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
|
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.surface,
|
color: bgColor,
|
||||||
|
border: Border(top: BorderSide(color: borderColor, width: 0.5)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(color: AppColors.shadow, blurRadius: 12, offset: const Offset(0, -4)),
|
||||||
color: Colors.black.withOpacity(0.08),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, -4),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (hint != null) ...[
|
||||||
|
Text(
|
||||||
|
hint!,
|
||||||
|
style: TextStyle(fontSize: 11, color: hintColor),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: enabled ? onPressed : null,
|
onPressed: enabled ? onPressed : null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: UnionFlowColors.unionGreen,
|
backgroundColor: UnionFlowColors.unionGreen,
|
||||||
disabledBackgroundColor: UnionFlowColors.border,
|
disabledBackgroundColor: isDark ? AppColors.borderDark : AppColors.border,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: AppColors.onPrimary,
|
||||||
|
disabledForegroundColor: isDark
|
||||||
|
? AppColors.textSecondaryDark
|
||||||
|
: AppColors.textSecondary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
elevation: enabled ? 2 : 0,
|
elevation: enabled ? 2 : 0,
|
||||||
shadowColor: UnionFlowColors.unionGreen.withOpacity(0.4),
|
shadowColor: UnionFlowColors.unionGreen.withOpacity(0.4),
|
||||||
),
|
),
|
||||||
@@ -164,6 +201,8 @@ class OnboardingBottomBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../bloc/onboarding_bloc.dart';
|
import '../../bloc/onboarding_bloc.dart';
|
||||||
import '../../data/models/souscription_status_model.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 '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||||||
|
|
||||||
/// Écran de sélection du moyen de paiement
|
/// Écran de sélection du moyen de paiement
|
||||||
@@ -17,13 +18,17 @@ class PaymentMethodPage extends StatefulWidget {
|
|||||||
class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
||||||
String? _selected;
|
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 = [
|
static const _methods = [
|
||||||
_PayMethod(
|
_PayMethod(
|
||||||
id: 'WAVE',
|
id: 'WAVE',
|
||||||
name: 'Wave Mobile Money',
|
name: 'Wave Mobile Money',
|
||||||
description: 'Paiement rapide via votre compte Wave',
|
description: 'Paiement rapide via votre compte Wave',
|
||||||
logoAsset: 'assets/images/payment_methods/wave/logo.png',
|
logoAsset: 'assets/images/payment_methods/wave/logo.png',
|
||||||
color: Color(0xFF00B9F1),
|
color: _waveBlue,
|
||||||
available: true,
|
available: true,
|
||||||
badge: 'Recommandé',
|
badge: 'Recommandé',
|
||||||
),
|
),
|
||||||
@@ -32,7 +37,7 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
name: 'Orange Money',
|
name: 'Orange Money',
|
||||||
description: 'Paiement via Orange Money',
|
description: 'Paiement via Orange Money',
|
||||||
logoAsset: 'assets/images/payment_methods/orange_money/logo-black.png',
|
logoAsset: 'assets/images/payment_methods/orange_money/logo-black.png',
|
||||||
color: Color(0xFFFF6600),
|
color: _orangeOrange,
|
||||||
available: false,
|
available: false,
|
||||||
badge: 'Prochainement',
|
badge: 'Prochainement',
|
||||||
),
|
),
|
||||||
@@ -41,16 +46,19 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Scaffold(
|
||||||
backgroundColor: UnionFlowColors.background,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header gradient — toujours sombre, texte blanc intentionnel
|
||||||
Container(
|
Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient),
|
||||||
gradient: UnionFlowColors.primaryGradient,
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -60,25 +68,19 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).maybePop(),
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
icon: const Icon(Icons.arrow_back_rounded,
|
icon: const Icon(Icons.arrow_back_rounded, color: Colors.white),
|
||||||
color: Colors.white),
|
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text(
|
||||||
'Moyen de paiement',
|
'Moyen de paiement',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800),
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Choisissez comment régler votre souscription',
|
'Choisissez comment régler votre souscription',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 13),
|
||||||
color: Colors.white.withOpacity(0.8), fontSize: 13),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -92,13 +94,14 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Montant rappel
|
// Rappel montant
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.surface,
|
color: bgCard,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
boxShadow: UnionFlowColors.softShadow,
|
border: Border.all(color: borderColor),
|
||||||
|
boxShadow: isDark ? null : UnionFlowColors.softShadow,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -109,24 +112,19 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
gradient: UnionFlowColors.goldGradient,
|
gradient: UnionFlowColors.goldGradient,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.receipt_rounded,
|
child: const Icon(Icons.receipt_rounded, color: Colors.white, size: 22),
|
||||||
color: Colors.white, size: 22),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text('Montant total',
|
||||||
'Montant total',
|
style: TextStyle(color: textSecondary, fontSize: 12)),
|
||||||
style: TextStyle(
|
|
||||||
color: UnionFlowColors.textSecondary,
|
|
||||||
fontSize: 12),
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
'${_formatPrix(montant)} FCFA',
|
'${_formatPrix(montant)} FCFA',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textPrimary,
|
color: textPrimary,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
@@ -138,18 +136,12 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text('Organisation',
|
||||||
'Organisation',
|
style: TextStyle(color: textSecondary, fontSize: 11)),
|
||||||
style: TextStyle(
|
|
||||||
color: UnionFlowColors.textSecondary,
|
|
||||||
fontSize: 11),
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
widget.souscription.organisationNom!,
|
widget.souscription.organisationNom!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textPrimary,
|
color: textPrimary, fontSize: 12, fontWeight: FontWeight.w600),
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -158,10 +150,10 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
const Text(
|
Text(
|
||||||
'Sélectionnez un moyen de paiement',
|
'Sélectionnez un moyen de paiement',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textPrimary,
|
color: textPrimary,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
),
|
),
|
||||||
@@ -171,34 +163,31 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
..._methods.map((m) => _MethodCard(
|
..._methods.map((m) => _MethodCard(
|
||||||
method: m,
|
method: m,
|
||||||
selected: _selected == m.id,
|
selected: _selected == m.id,
|
||||||
onTap: m.available
|
onTap: m.available ? () => setState(() => _selected = m.id) : null,
|
||||||
? () => setState(() => _selected = m.id)
|
|
||||||
: null,
|
|
||||||
)),
|
)),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Bandeau sécurité — fond vert pâle adaptatif
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.unionGreenPale,
|
color: isDark
|
||||||
|
? UnionFlowColors.unionGreen.withOpacity(0.1)
|
||||||
|
: UnionFlowColors.unionGreenPale,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(color: UnionFlowColors.unionGreen.withOpacity(0.25)),
|
||||||
color:
|
|
||||||
UnionFlowColors.unionGreen.withOpacity(0.25)),
|
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: const Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.lock_rounded,
|
Icon(Icons.lock_rounded, color: UnionFlowColors.unionGreen, size: 18),
|
||||||
color: UnionFlowColors.unionGreen, size: 18),
|
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
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.',
|
'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(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12, color: UnionFlowColors.unionGreen, height: 1.4),
|
||||||
color: UnionFlowColors.unionGreen,
|
|
||||||
height: 1.4),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -214,41 +203,34 @@ class _PaymentMethodPageState extends State<PaymentMethodPage> {
|
|||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
|
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.surface,
|
color: bgCard,
|
||||||
|
border: Border(top: BorderSide(color: borderColor)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(color: AppColors.shadow, blurRadius: 12, offset: const Offset(0, -4)),
|
||||||
color: Colors.black.withOpacity(0.08),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, -4),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: _selected == 'WAVE'
|
onPressed: _selected == 'WAVE'
|
||||||
? () => context
|
? () => context.read<OnboardingBloc>().add(const OnboardingPaiementInitie())
|
||||||
.read<OnboardingBloc>()
|
|
||||||
.add(const OnboardingPaiementInitie())
|
|
||||||
: null,
|
: null,
|
||||||
icon: const Icon(Icons.open_in_new_rounded),
|
icon: const Icon(Icons.open_in_new_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
_selected == 'WAVE'
|
_selected == 'WAVE'
|
||||||
? 'Payer avec Wave'
|
? 'Payer avec Wave'
|
||||||
: 'Sélectionnez un moyen de paiement',
|
: 'Sélectionnez un moyen de paiement',
|
||||||
style:
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||||||
const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00B9F1),
|
backgroundColor: _waveBlue,
|
||||||
disabledBackgroundColor: UnionFlowColors.border,
|
disabledBackgroundColor: isDark ? AppColors.borderDark : AppColors.border,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: AppColors.onPrimary,
|
||||||
disabledForegroundColor: UnionFlowColors.textSecondary,
|
disabledForegroundColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
borderRadius: BorderRadius.circular(14)),
|
|
||||||
elevation: _selected == 'WAVE' ? 3 : 0,
|
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) {
|
String _formatPrix(double prix) {
|
||||||
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
|
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
|
||||||
final s = prix.toStringAsFixed(0);
|
final s = prix.toStringAsFixed(0);
|
||||||
if (s.length > 3) {
|
if (s.length > 3) return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
|
||||||
return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
|
|
||||||
}
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,15 +267,18 @@ class _MethodCard extends StatelessWidget {
|
|||||||
final bool selected;
|
final bool selected;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const _MethodCard({
|
const _MethodCard({required this.method, required this.selected, this.onTap});
|
||||||
required this.method,
|
|
||||||
required this.selected,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -304,51 +287,43 @@ class _MethodCard extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 10),
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: disabled
|
color: disabled
|
||||||
? UnionFlowColors.surfaceVariant
|
? bgDisabled
|
||||||
: selected
|
: selected
|
||||||
? method.color.withOpacity(0.06)
|
? method.color.withOpacity(isDark ? 0.12 : 0.06)
|
||||||
: UnionFlowColors.surface,
|
: bgSurface,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: selected ? method.color : UnionFlowColors.border,
|
color: selected ? method.color : borderDefault,
|
||||||
width: selected ? 2 : 1,
|
width: selected ? 2 : 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
boxShadow: disabled
|
boxShadow: disabled
|
||||||
? []
|
? []
|
||||||
: selected
|
: selected
|
||||||
? [
|
? [BoxShadow(color: method.color.withOpacity(0.15), blurRadius: 16, offset: const Offset(0, 6))]
|
||||||
BoxShadow(
|
: isDark ? null : UnionFlowColors.softShadow,
|
||||||
color: method.color.withOpacity(0.15),
|
|
||||||
blurRadius: 16,
|
|
||||||
offset: const Offset(0, 6),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: UnionFlowColors.softShadow,
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Logo image
|
// Logo — fond blanc intentionnel (logos de marque)
|
||||||
Container(
|
Container(
|
||||||
width: 56,
|
width: 56,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: disabled
|
color: disabled ? bgDisabled : Colors.white,
|
||||||
? UnionFlowColors.border
|
|
||||||
: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: UnionFlowColors.border),
|
border: Border.all(color: borderDefault),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
method.logoAsset,
|
method.logoAsset,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
color: disabled ? UnionFlowColors.textTertiary : null,
|
color: disabled ? textTertiary : null,
|
||||||
colorBlendMode: disabled ? BlendMode.srcIn : null,
|
colorBlendMode: disabled ? BlendMode.srcIn : null,
|
||||||
errorBuilder: (_, __, ___) => Icon(
|
errorBuilder: (_, __, ___) => Icon(
|
||||||
Icons.account_balance_wallet_rounded,
|
Icons.account_balance_wallet_rounded,
|
||||||
color: disabled ? UnionFlowColors.textTertiary : method.color,
|
color: disabled ? textTertiary : method.color,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -363,18 +338,14 @@ class _MethodCard extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: disabled
|
color: disabled ? textTertiary : textPrimary,
|
||||||
? UnionFlowColors.textTertiary
|
|
||||||
: UnionFlowColors.textPrimary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
method.description,
|
method.description,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: disabled
|
color: disabled ? textTertiary : textSecondary,
|
||||||
? UnionFlowColors.textTertiary
|
|
||||||
: UnionFlowColors.textSecondary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -384,14 +355,13 @@ class _MethodCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
horizontal: 8, vertical: 3),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: disabled
|
color: disabled
|
||||||
? UnionFlowColors.border
|
? borderDefault
|
||||||
: method.available
|
: method.available
|
||||||
? method.color.withOpacity(0.1)
|
? method.color.withOpacity(isDark ? 0.2 : 0.1)
|
||||||
: UnionFlowColors.surfaceVariant,
|
: isDark ? AppColors.surfaceVariantDark : AppColors.surfaceVariant,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -400,20 +370,18 @@ class _MethodCard extends StatelessWidget {
|
|||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: disabled
|
color: disabled
|
||||||
? UnionFlowColors.textTertiary
|
? textTertiary
|
||||||
: method.available
|
: method.available
|
||||||
? method.color
|
? method.color
|
||||||
: UnionFlowColors.textSecondary,
|
: textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!disabled) ...[
|
if (!disabled) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Icon(
|
Icon(
|
||||||
selected
|
selected ? Icons.check_circle_rounded : Icons.radio_button_unchecked,
|
||||||
? Icons.check_circle_rounded
|
color: selected ? method.color : borderDefault,
|
||||||
: Icons.radio_button_unchecked,
|
|
||||||
color: selected ? method.color : UnionFlowColors.border,
|
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../bloc/onboarding_bloc.dart';
|
import '../../bloc/onboarding_bloc.dart';
|
||||||
import '../../data/models/formule_model.dart';
|
import '../../data/models/formule_model.dart';
|
||||||
|
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||||
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||||||
import 'onboarding_shared_widgets.dart';
|
import 'onboarding_shared_widgets.dart';
|
||||||
|
|
||||||
@@ -17,12 +18,13 @@ class PlanSelectionPage extends StatefulWidget {
|
|||||||
class _PlanSelectionPageState extends State<PlanSelectionPage> {
|
class _PlanSelectionPageState extends State<PlanSelectionPage> {
|
||||||
String? _selectedPlage;
|
String? _selectedPlage;
|
||||||
String? _selectedFormule;
|
String? _selectedFormule;
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
static const _plages = [
|
static const _plages = [
|
||||||
_Plage('PETITE', 'Petite', '1 – 100 membres', Icons.group_outlined, 'Associations naissantes et petites structures'),
|
_Plage('PETITE', 'Petite', '1 – 100', Icons.group_outlined, 'Associations naissantes'),
|
||||||
_Plage('MOYENNE', 'Moyenne', '101 – 500 membres', Icons.groups_outlined, 'Associations établies en croissance'),
|
_Plage('MOYENNE', 'Moyenne', '101 – 500', Icons.groups_outlined, 'Organisations établies'),
|
||||||
_Plage('GRANDE', 'Grande', '501 – 2 000 membres', Icons.corporate_fare_outlined, 'Grandes organisations régionales'),
|
_Plage('GRANDE', 'Grande', '501 – 2 000', Icons.corporate_fare_outlined, 'Grandes structures'),
|
||||||
_Plage('TRES_GRANDE', 'Très grande', '2 000+ membres', Icons.account_balance_outlined, 'Fédérations et réseaux nationaux'),
|
_Plage('TRES_GRANDE', 'Très grande', '2 000+', Icons.account_balance_outlined, 'Fédérations & réseaux'),
|
||||||
];
|
];
|
||||||
|
|
||||||
static const _formuleColors = {
|
static const _formuleColors = {
|
||||||
@@ -50,10 +52,35 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
|
|||||||
|
|
||||||
bool get _canProceed => _selectedPlage != null && _selectedFormule != null;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UnionFlowColors.background,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
OnboardingStepHeader(
|
OnboardingStepHeader(
|
||||||
@@ -64,41 +91,85 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Step 1a: Taille de l'organisation
|
// ── Section 1 : Taille ────────────────────────────────
|
||||||
OnboardingSectionTitle(
|
OnboardingSectionTitle(
|
||||||
icon: Icons.people_alt_outlined,
|
icon: Icons.people_alt_outlined,
|
||||||
title: 'Taille de votre organisation',
|
title: 'Taille de votre organisation',
|
||||||
|
badge: _selectedPlage != null ? 'Sélectionné ✓' : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
...(_plages.map((p) => _PlageCard(
|
|
||||||
|
// Grille 2×2 pour scanner rapidement
|
||||||
|
GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
childAspectRatio: 1.45,
|
||||||
|
children: _plages.map((p) => _PlageCard(
|
||||||
plage: p,
|
plage: p,
|
||||||
selected: _selectedPlage == p.code,
|
selected: _selectedPlage == p.code,
|
||||||
onTap: () => setState(() {
|
isDark: isDark,
|
||||||
_selectedPlage = p.code;
|
onTap: () => _onPlageSelected(p.code),
|
||||||
_selectedFormule = null;
|
)).toList(),
|
||||||
}),
|
),
|
||||||
))),
|
|
||||||
|
|
||||||
if (_selectedPlage != null) ...[
|
// ── Section 2 : Formule (apparaît en fondu) ──────────
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
transitionBuilder: (child, anim) => FadeTransition(
|
||||||
|
opacity: anim,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.08),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut)),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _selectedPlage == null
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Column(
|
||||||
|
key: ValueKey(_selectedPlage),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
OnboardingSectionTitle(
|
OnboardingSectionTitle(
|
||||||
icon: Icons.workspace_premium_outlined,
|
icon: Icons.workspace_premium_outlined,
|
||||||
title: 'Niveau d\'abonnement',
|
title: 'Niveau d\'abonnement',
|
||||||
|
badge: _selectedFormule != null ? 'Sélectionné ✓' : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Modifiable à tout moment depuis vos paramètres.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: isDark
|
||||||
|
? AppColors.textSecondaryDark
|
||||||
|
: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
..._filteredFormules.map((f) => _FormuleCard(
|
..._filteredFormules.map((f) => _FormuleCard(
|
||||||
formule: f,
|
formule: f,
|
||||||
color: _formuleColors[f.code] ?? UnionFlowColors.unionGreen,
|
color: _formuleColors[f.code] ?? UnionFlowColors.unionGreen,
|
||||||
icon: _formuleIcons[f.code] ?? Icons.star_border_rounded,
|
icon: _formuleIcons[f.code] ?? Icons.star_border_rounded,
|
||||||
features: _formuleFeatures[f.code] ?? [],
|
features: _formuleFeatures[f.code] ?? [],
|
||||||
selected: _selectedFormule == f.code,
|
selected: _selectedFormule == f.code,
|
||||||
|
isPopular: f.code == 'STANDARD',
|
||||||
|
isDark: isDark,
|
||||||
onTap: () => setState(() => _selectedFormule = f.code),
|
onTap: () => setState(() => _selectedFormule = f.code),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -107,7 +178,8 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
|
|||||||
),
|
),
|
||||||
bottomNavigationBar: OnboardingBottomBar(
|
bottomNavigationBar: OnboardingBottomBar(
|
||||||
enabled: _canProceed,
|
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(
|
onPressed: () => context.read<OnboardingBloc>().add(
|
||||||
OnboardingFormuleSelected(
|
OnboardingFormuleSelected(
|
||||||
codeFormule: _selectedFormule!,
|
codeFormule: _selectedFormule!,
|
||||||
@@ -119,7 +191,7 @@ class _PlanSelectionPageState extends State<PlanSelectionPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Widgets locaux ──────────────────────────────────────────────────────────
|
// ─── Carte de taille (grille 2×2) ────────────────────────────────────────────
|
||||||
|
|
||||||
class _Plage {
|
class _Plage {
|
||||||
final String code, label, sublabel, description;
|
final String code, label, sublabel, description;
|
||||||
@@ -130,115 +202,120 @@ class _Plage {
|
|||||||
class _PlageCard extends StatelessWidget {
|
class _PlageCard extends StatelessWidget {
|
||||||
final _Plage plage;
|
final _Plage plage;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
final bool isDark;
|
||||||
final VoidCallback onTap;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 180),
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: selected ? UnionFlowColors.unionGreenPale : UnionFlowColors.surface,
|
color: selected
|
||||||
|
? UnionFlowColors.unionGreen.withOpacity(isDark ? 0.15 : 0.06)
|
||||||
|
: bgColor,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: selected ? UnionFlowColors.unionGreen : UnionFlowColors.border,
|
color: selected ? UnionFlowColors.unionGreen : borderColor,
|
||||||
width: selected ? 2 : 1,
|
width: selected ? 2 : 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
boxShadow: selected ? UnionFlowColors.greenGlowShadow : UnionFlowColors.softShadow,
|
boxShadow: selected ? UnionFlowColors.greenGlowShadow : null,
|
||||||
),
|
),
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: selected
|
|
||||||
? UnionFlowColors.unionGreen
|
|
||||||
: UnionFlowColors.unionGreenPale,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: Icon(plage.icon,
|
|
||||||
color: selected ? Colors.white : UnionFlowColors.unionGreen,
|
|
||||||
size: 22),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 14),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
plage.label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
fontSize: 15,
|
|
||||||
color: selected
|
|
||||||
? UnionFlowColors.unionGreen
|
|
||||||
: UnionFlowColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
width: 36,
|
||||||
horizontal: 8, vertical: 2),
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
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
|
color: selected
|
||||||
? UnionFlowColors.unionGreen
|
? UnionFlowColors.unionGreen
|
||||||
: UnionFlowColors.textSecondary,
|
: UnionFlowColors.unionGreen.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(9),
|
||||||
),
|
),
|
||||||
),
|
child: Icon(
|
||||||
),
|
plage.icon,
|
||||||
],
|
color: selected ? Colors.white : UnionFlowColors.unionGreen,
|
||||||
),
|
size: 19,
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
plage.description,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: UnionFlowColors.textSecondary),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
selected
|
selected
|
||||||
? Icons.check_circle_rounded
|
? Icons.check_circle_rounded
|
||||||
: Icons.radio_button_unchecked,
|
: Icons.radio_button_unchecked,
|
||||||
color: selected
|
color: selected ? UnionFlowColors.unionGreen : borderColor,
|
||||||
? UnionFlowColors.unionGreen
|
size: 18,
|
||||||
: UnionFlowColors.border,
|
|
||||||
size: 22,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
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 {
|
class _FormuleCard extends StatelessWidget {
|
||||||
final FormuleModel formule;
|
final FormuleModel formule;
|
||||||
final Color color;
|
final Color color;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final List<String> features;
|
final List<String> features;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
final bool isPopular;
|
||||||
|
final bool isDark;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const _FormuleCard({
|
const _FormuleCard({
|
||||||
@@ -247,50 +324,58 @@ class _FormuleCard extends StatelessWidget {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
required this.features,
|
required this.features,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
|
required this.isPopular,
|
||||||
|
required this.isDark,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.surface,
|
color: bgCard,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: selected ? color : UnionFlowColors.border,
|
color: selected ? color : borderColor,
|
||||||
width: selected ? 2.5 : 1,
|
width: selected ? 2.5 : 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: selected
|
boxShadow: selected
|
||||||
? [
|
? [BoxShadow(color: color.withOpacity(isDark ? 0.25 : 0.18), blurRadius: 20, offset: const Offset(0, 8))]
|
||||||
BoxShadow(
|
: isDark ? null : UnionFlowColors.softShadow,
|
||||||
color: color.withOpacity(0.2),
|
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 8),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: UnionFlowColors.softShadow,
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// ── Header coloré ─────────────────────────────────
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||||
decoration: BoxDecoration(
|
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)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon,
|
Icon(icon,
|
||||||
color: selected ? Colors.white : color, size: 24),
|
color: selected ? Colors.white : color,
|
||||||
|
size: 24),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Wrap permet au badge de passer à la ligne sur écrans étroits
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
formule.libelle,
|
formule.libelle,
|
||||||
@@ -300,19 +385,41 @@ class _FormuleCard extends StatelessWidget {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (formule.description != null)
|
if (isPopular)
|
||||||
Text(
|
Container(
|
||||||
formule.description!,
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||||
style: TextStyle(
|
decoration: BoxDecoration(
|
||||||
color: selected
|
color: selected
|
||||||
? Colors.white70
|
? Colors.white.withOpacity(0.25)
|
||||||
: UnionFlowColors.textSecondary,
|
: color.withOpacity(0.15),
|
||||||
fontSize: 12,
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'⭐ POPULAIRE',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: selected ? Colors.white : color,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (formule.description != null)
|
||||||
|
Text(
|
||||||
|
formule.description!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: selected ? Colors.white70 : textSecondary,
|
||||||
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Prix
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
@@ -325,39 +432,84 @@ class _FormuleCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'FCFA / mois',
|
'FCFA/mois',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
color: selected ? Colors.white70 : textSecondary,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (formule.prixAnnuel != null && formule.prixAnnuel! > 0) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
color: selected
|
color: selected
|
||||||
? Colors.white70
|
? Colors.white.withOpacity(0.2)
|
||||||
: UnionFlowColors.textSecondary,
|
: AppColors.success.withOpacity(0.12),
|
||||||
fontSize: 11,
|
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(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: features
|
children: features.map((f) => Padding(
|
||||||
.map((f) => Padding(
|
padding: const EdgeInsets.only(bottom: 7),
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.check_circle_outline_rounded,
|
Icon(Icons.check_circle_outline_rounded,
|
||||||
size: 16, color: color),
|
size: 16, color: color),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(f,
|
Expanded(
|
||||||
style: const TextStyle(
|
child: Text(
|
||||||
fontSize: 13,
|
f,
|
||||||
color: UnionFlowColors.textPrimary)),
|
style: TextStyle(fontSize: 13, color: textPrimary),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
))
|
)).toList(),
|
||||||
.toList(),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Sélection indicator ────────────────────────────
|
||||||
|
if (selected)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(isDark ? 0.2 : 0.08),
|
||||||
|
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(14)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle_rounded, size: 14, color: color),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Formule sélectionnée',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -367,14 +519,15 @@ class _FormuleCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _formatPrix(double prix) {
|
String _formatPrix(double prix) {
|
||||||
if (prix >= 1000000) {
|
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
|
||||||
return '${(prix / 1000000).toStringAsFixed(1)} M';
|
if (prix >= 1000) return '${(prix / 1000).toStringAsFixed(0)} k';
|
||||||
}
|
|
||||||
if (prix >= 1000) {
|
|
||||||
final k = (prix / 1000).toStringAsFixed(0);
|
|
||||||
return '$k 000';
|
|
||||||
}
|
|
||||||
return prix.toStringAsFixed(0);
|
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 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../bloc/onboarding_bloc.dart';
|
import '../../bloc/onboarding_bloc.dart';
|
||||||
import '../../data/models/souscription_status_model.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 '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||||||
|
import 'onboarding_shared_widgets.dart';
|
||||||
|
|
||||||
/// Étape 3 — Récapitulatif détaillé avant paiement
|
/// Étape 3 — Récapitulatif détaillé avant paiement
|
||||||
class SubscriptionSummaryPage extends StatelessWidget {
|
class SubscriptionSummaryPage extends StatelessWidget {
|
||||||
@@ -19,46 +21,38 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
|||||||
|
|
||||||
static const _periodeRemises = {
|
static const _periodeRemises = {
|
||||||
'MENSUEL': null,
|
'MENSUEL': null,
|
||||||
'TRIMESTRIEL': '–5% de remise',
|
'TRIMESTRIEL': '–5 % de remise',
|
||||||
'SEMESTRIEL': '–10% de remise',
|
'SEMESTRIEL': '–10 % de remise',
|
||||||
'ANNUEL': '–20% 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',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const _plageLabels = {
|
static const _plageLabels = {
|
||||||
'PETITE': '1–100 membres',
|
'PETITE': '1 – 100 membres',
|
||||||
'MOYENNE': '101–500 membres',
|
'MOYENNE': '101 – 500 membres',
|
||||||
'GRANDE': '501–2 000 membres',
|
'GRANDE': '501 – 2 000 membres',
|
||||||
'TRES_GRANDE': '2 000+ membres',
|
'TRES_GRANDE': '2 000+ membres',
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final montant = souscription.montantTotal ?? 0;
|
final montant = souscription.montantTotal ?? 0;
|
||||||
final remise = _periodeRemises[souscription.typePeriode];
|
final remise = _periodeRemises[souscription.typePeriode];
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UnionFlowColors.background,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header hero
|
// ── Header hero gradient ───────────────────────────
|
||||||
Container(
|
Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient),
|
||||||
gradient: UnionFlowColors.primaryGradient,
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Step bar
|
// Barre de progression — toutes complètes à l'étape 3
|
||||||
Row(
|
Row(
|
||||||
children: List.generate(3, (i) => Expanded(
|
children: List.generate(3, (i) => Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -84,52 +78,50 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
// Montant principal
|
|
||||||
|
// Icône principale
|
||||||
Container(
|
Container(
|
||||||
width: 90,
|
width: 80,
|
||||||
height: 90,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.15),
|
color: Colors.white.withOpacity(0.15),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(color: Colors.white.withOpacity(0.4), width: 2),
|
||||||
color: Colors.white.withOpacity(0.4), width: 2),
|
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.receipt_long_rounded,
|
child: const Icon(Icons.receipt_long_rounded, color: Colors.white, size: 40),
|
||||||
color: Colors.white, size: 44),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
// Montant
|
||||||
Text(
|
Text(
|
||||||
_formatPrix(montant),
|
_formatPrix(montant),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 40,
|
fontSize: 42,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
letterSpacing: -1,
|
letterSpacing: -1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Text(
|
const Text(
|
||||||
'FCFA à régler',
|
'FCFA à régler',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
color: Colors.white70,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Badge remise
|
||||||
if (remise != null) ...[
|
if (remise != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
||||||
horizontal: 12, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.gold.withOpacity(0.3),
|
color: UnionFlowColors.gold.withOpacity(0.3),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(
|
border: Border.all(color: UnionFlowColors.goldLight.withOpacity(0.5)),
|
||||||
color: UnionFlowColors.goldLight.withOpacity(0.5)),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
remise,
|
'🎉 $remise appliquée',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: UnionFlowColors.goldLight,
|
color: UnionFlowColors.goldLight,
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -141,28 +133,26 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Content
|
// ── Contenu scrollable ──────────────────────────────
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
// Organisation
|
// Organisation
|
||||||
if (souscription.organisationNom != null) ...[
|
if (souscription.organisationNom != null) ...[
|
||||||
_DetailCard(
|
_DetailCard(
|
||||||
title: 'Organisation',
|
title: 'Organisation',
|
||||||
icon: Icons.business_rounded,
|
icon: Icons.business_rounded,
|
||||||
iconColor: UnionFlowColors.indigo,
|
iconColor: UnionFlowColors.indigo,
|
||||||
|
isDark: isDark,
|
||||||
items: [
|
items: [
|
||||||
_DetailItem(
|
_DetailItem(label: 'Nom', value: souscription.organisationNom!, bold: true),
|
||||||
label: 'Nom',
|
if (souscription.typeOrganisation != null)
|
||||||
value: souscription.organisationNom!,
|
_DetailItem(label: 'Type', value: souscription.typeOrganisation!),
|
||||||
bold: true),
|
|
||||||
_DetailItem(
|
|
||||||
label: 'Type',
|
|
||||||
value: _orgLabels[souscription.typeOrganisation] ??
|
|
||||||
souscription.typeOrganisation),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -173,20 +163,18 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
|||||||
title: 'Formule souscrite',
|
title: 'Formule souscrite',
|
||||||
icon: Icons.workspace_premium_rounded,
|
icon: Icons.workspace_premium_rounded,
|
||||||
iconColor: UnionFlowColors.gold,
|
iconColor: UnionFlowColors.gold,
|
||||||
|
isDark: isDark,
|
||||||
items: [
|
items: [
|
||||||
|
_DetailItem(label: 'Niveau', value: souscription.typeFormule, bold: true),
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
label: 'Niveau',
|
label: 'Capacité',
|
||||||
value: souscription.typeFormule,
|
value: _plageLabels[souscription.plageMembres] ?? souscription.plageLibelle,
|
||||||
bold: true),
|
),
|
||||||
_DetailItem(
|
|
||||||
label: 'Taille',
|
|
||||||
value: _plageLabels[souscription.plageMembres] ??
|
|
||||||
souscription.plageLibelle),
|
|
||||||
if (souscription.montantMensuelBase != null)
|
if (souscription.montantMensuelBase != null)
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
label: 'Prix de base',
|
label: 'Prix de base',
|
||||||
value:
|
value: '${_formatPrix(souscription.montantMensuelBase!)} FCFA/mois',
|
||||||
'${_formatPrix(souscription.montantMensuelBase!)} FCFA/mois'),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -196,69 +184,84 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
|||||||
title: 'Facturation',
|
title: 'Facturation',
|
||||||
icon: Icons.calendar_today_rounded,
|
icon: Icons.calendar_today_rounded,
|
||||||
iconColor: UnionFlowColors.unionGreen,
|
iconColor: UnionFlowColors.unionGreen,
|
||||||
|
isDark: isDark,
|
||||||
items: [
|
items: [
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
label: 'Période',
|
label: 'Période',
|
||||||
value:
|
value: _periodeLabels[souscription.typePeriode] ?? souscription.typePeriode,
|
||||||
_periodeLabels[souscription.typePeriode] ??
|
),
|
||||||
souscription.typePeriode),
|
|
||||||
if (souscription.coefficientApplique != null)
|
if (souscription.coefficientApplique != null)
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
label: 'Coefficient',
|
label: 'Coefficient',
|
||||||
value:
|
value: '×${souscription.coefficientApplique!.toStringAsFixed(4)}',
|
||||||
'×${souscription.coefficientApplique!.toStringAsFixed(4)}'),
|
),
|
||||||
if (souscription.dateDebut != null &&
|
if (souscription.dateDebut != null)
|
||||||
souscription.dateFin != null) ...[
|
_DetailItem(label: 'Début', value: _formatDate(souscription.dateDebut!)),
|
||||||
_DetailItem(
|
if (souscription.dateFin != null)
|
||||||
label: 'Début',
|
_DetailItem(label: 'Fin', value: _formatDate(souscription.dateFin!)),
|
||||||
value: _formatDate(souscription.dateDebut!)),
|
|
||||||
_DetailItem(
|
|
||||||
label: 'Fin',
|
|
||||||
value: _formatDate(souscription.dateFin!)),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Montant total
|
// Bloc montant total — proéminent
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(18),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(
|
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),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(color: UnionFlowColors.gold.withOpacity(0.4)),
|
||||||
color: UnionFlowColors.gold.withOpacity(0.4)),
|
boxShadow: isDark ? null : UnionFlowColors.goldGlowShadow,
|
||||||
boxShadow: UnionFlowColors.goldGlowShadow,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 48,
|
width: 52,
|
||||||
height: 48,
|
height: 52,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: UnionFlowColors.goldGradient,
|
gradient: UnionFlowColors.goldGradient,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.monetization_on_rounded,
|
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(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Total à payer',
|
'TOTAL À PAYER',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textSecondary,
|
color: isDark
|
||||||
fontSize: 13),
|
? AppColors.textSecondaryDark
|
||||||
|
: AppColors.textSecondary,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${_formatPrix(montant)} FCFA',
|
'${_formatPrix(montant)} FCFA',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textPrimary,
|
color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimary,
|
||||||
fontSize: 22,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.w900,
|
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),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Notes importantes
|
// Notes
|
||||||
_NoteBox(
|
_NoteBox(
|
||||||
icon: Icons.security_rounded,
|
icon: Icons.security_rounded,
|
||||||
iconColor: UnionFlowColors.unionGreen,
|
iconColor: UnionFlowColors.unionGreen,
|
||||||
backgroundColor: UnionFlowColors.unionGreenPale,
|
accentColor: UnionFlowColors.unionGreen,
|
||||||
borderColor: UnionFlowColors.unionGreen.withOpacity(0.25),
|
isDark: isDark,
|
||||||
title: 'Paiement sécurisé',
|
title: 'Paiement sécurisé',
|
||||||
message:
|
message: 'Votre paiement est traité de manière sécurisée via Wave Mobile Money. Une fois confirmé, votre compte sera activé automatiquement.',
|
||||||
'Votre paiement est traité de manière sécurisée via Wave Mobile Money. Une fois le paiement effectué, votre compte sera activé automatiquement.',
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_NoteBox(
|
_NoteBox(
|
||||||
icon: Icons.bolt_rounded,
|
icon: Icons.bolt_rounded,
|
||||||
iconColor: UnionFlowColors.amber,
|
iconColor: UnionFlowColors.amber,
|
||||||
backgroundColor: const Color(0xFFFFFBF0),
|
accentColor: UnionFlowColors.amber,
|
||||||
borderColor: UnionFlowColors.amber.withOpacity(0.3),
|
isDark: isDark,
|
||||||
title: 'Activation immédiate',
|
title: 'Activation immédiate',
|
||||||
message:
|
message: 'Dès que Wave confirme le paiement, votre espace administrateur est activé avec toutes les fonctionnalités de votre formule.',
|
||||||
'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.',
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_NoteBox(
|
_NoteBox(
|
||||||
icon: Icons.support_agent_rounded,
|
icon: Icons.support_agent_rounded,
|
||||||
iconColor: UnionFlowColors.info,
|
iconColor: UnionFlowColors.info,
|
||||||
backgroundColor: UnionFlowColors.infoPale,
|
accentColor: UnionFlowColors.info,
|
||||||
borderColor: UnionFlowColors.info.withOpacity(0.2),
|
isDark: isDark,
|
||||||
title: 'Besoin d\'aide ?',
|
title: 'Besoin d\'aide ?',
|
||||||
message:
|
message: 'En cas de problème, contactez support@unionflow.app — réponse sous 24h.',
|
||||||
'En cas de problème lors du paiement, contactez notre support à support@unionflow.app — nous vous répondrons sous 24h.',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -305,41 +305,10 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: Container(
|
bottomNavigationBar: OnboardingBottomBar(
|
||||||
padding: EdgeInsets.fromLTRB(
|
enabled: true,
|
||||||
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
|
label: 'Choisir le moyen de paiement',
|
||||||
decoration: BoxDecoration(
|
onPressed: () => context.read<OnboardingBloc>().add(const OnboardingChoixPaiementOuvert()),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -347,18 +316,13 @@ class SubscriptionSummaryPage extends StatelessWidget {
|
|||||||
String _formatPrix(double prix) {
|
String _formatPrix(double prix) {
|
||||||
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
|
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
|
||||||
final s = prix.toStringAsFixed(0);
|
final s = prix.toStringAsFixed(0);
|
||||||
if (s.length > 6) {
|
if (s.length > 6) return '${s.substring(0, s.length - 6)} ${s.substring(s.length - 6, s.length - 3)} ${s.substring(s.length - 3)}';
|
||||||
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 > 3) {
|
|
||||||
return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
|
|
||||||
}
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime date) =>
|
||||||
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
'${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Widgets locaux ──────────────────────────────────────────────────────────
|
// ─── Widgets locaux ──────────────────────────────────────────────────────────
|
||||||
@@ -367,8 +331,7 @@ class _DetailItem {
|
|||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
final bool bold;
|
final bool bold;
|
||||||
const _DetailItem(
|
const _DetailItem({required this.label, required this.value, this.bold = false});
|
||||||
{required this.label, required this.value, this.bold = false});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailCard extends StatelessWidget {
|
class _DetailCard extends StatelessWidget {
|
||||||
@@ -376,25 +339,34 @@ class _DetailCard extends StatelessWidget {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color iconColor;
|
final Color iconColor;
|
||||||
final List<_DetailItem> items;
|
final List<_DetailItem> items;
|
||||||
|
final bool isDark;
|
||||||
|
|
||||||
const _DetailCard({
|
const _DetailCard({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.iconColor,
|
required this.iconColor,
|
||||||
required this.items,
|
required this.items,
|
||||||
|
required this.isDark,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.surface,
|
color: bgColor,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: UnionFlowColors.softShadow,
|
border: Border.all(color: borderColor),
|
||||||
|
boxShadow: isDark ? null : UnionFlowColors.softShadow,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// En-tête section
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 10),
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -403,7 +375,7 @@ class _DetailCard extends StatelessWidget {
|
|||||||
width: 34,
|
width: 34,
|
||||||
height: 34,
|
height: 34,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: iconColor.withOpacity(0.1),
|
color: iconColor.withOpacity(isDark ? 0.2 : 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: iconColor, size: 18),
|
child: Icon(icon, color: iconColor, size: 18),
|
||||||
@@ -411,16 +383,16 @@ class _DetailCard extends StatelessWidget {
|
|||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: UnionFlowColors.textPrimary,
|
color: textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: UnionFlowColors.border),
|
Divider(height: 1, color: borderColor),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -430,23 +402,17 @@ class _DetailCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 110,
|
||||||
child: Text(
|
child: Text(item.label,
|
||||||
item.label,
|
style: TextStyle(color: textSecondary, fontSize: 13)),
|
||||||
style: const TextStyle(
|
|
||||||
color: UnionFlowColors.textSecondary,
|
|
||||||
fontSize: 13),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
item.value,
|
item.value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textPrimary,
|
color: textPrimary,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: item.bold
|
fontWeight: item.bold ? FontWeight.w700 : FontWeight.w500,
|
||||||
? FontWeight.w700
|
|
||||||
: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -464,26 +430,30 @@ class _DetailCard extends StatelessWidget {
|
|||||||
class _NoteBox extends StatelessWidget {
|
class _NoteBox extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color iconColor;
|
final Color iconColor;
|
||||||
final Color backgroundColor;
|
final Color accentColor;
|
||||||
final Color borderColor;
|
final bool isDark;
|
||||||
final String title;
|
final String title;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
const _NoteBox({
|
const _NoteBox({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.iconColor,
|
required this.iconColor,
|
||||||
required this.backgroundColor,
|
required this.accentColor,
|
||||||
required this.borderColor,
|
required this.isDark,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.message,
|
required this.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: bgColor,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: borderColor),
|
border: Border.all(color: borderColor),
|
||||||
),
|
),
|
||||||
@@ -498,19 +468,12 @@ class _NoteBox extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(color: iconColor, fontWeight: FontWeight.w700, fontSize: 13),
|
||||||
color: iconColor,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text(
|
||||||
message,
|
message,
|
||||||
style: const TextStyle(
|
style: TextStyle(color: textSecondary, fontSize: 12, height: 1.5),
|
||||||
color: UnionFlowColors.textSecondary,
|
|
||||||
fontSize: 12,
|
|
||||||
height: 1.5),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import '../../bloc/onboarding_bloc.dart';
|
import '../../bloc/onboarding_bloc.dart';
|
||||||
import '../../data/models/souscription_status_model.dart';
|
import '../../data/models/souscription_status_model.dart';
|
||||||
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||||||
|
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||||
import '../../../../core/config/environment.dart';
|
import '../../../../core/config/environment.dart';
|
||||||
|
|
||||||
/// Étape 4 — Lancement du paiement Wave + attente du retour
|
/// Étape 4 — Lancement du paiement Wave + attente du retour
|
||||||
@@ -33,6 +34,9 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
widget.waveLaunchUrl.contains('localhost') ||
|
widget.waveLaunchUrl.contains('localhost') ||
|
||||||
!AppConfig.isProd;
|
!AppConfig.isProd;
|
||||||
|
|
||||||
|
// Couleur de marque Wave (volontairement hardcodée)
|
||||||
|
static const _waveBlue = Color(0xFF00B9F1);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -72,10 +76,10 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
} else {
|
} else {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: const Text(
|
||||||
'Impossible d\'ouvrir Wave. Vérifiez que l\'application est installée.'),
|
'Impossible d\'ouvrir Wave. Vérifiez que l\'application est installée.'),
|
||||||
backgroundColor: UnionFlowColors.error,
|
backgroundColor: AppColors.error,
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -85,77 +89,131 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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;
|
final montant = widget.souscription.montantTotal ?? 0;
|
||||||
const waveBlue = Color(0xFF00B9F1);
|
|
||||||
|
|
||||||
return Scaffold(
|
return BlocListener<OnboardingBloc, OnboardingState>(
|
||||||
backgroundColor: UnionFlowColors.background,
|
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(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
// ── Top bar ───────────────────────────────────
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (!_paymentLaunched && !_simulating)
|
if (!_paymentLaunched && !_simulating)
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).maybePop(),
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
icon: const Icon(Icons.arrow_back_rounded),
|
icon: const Icon(Icons.arrow_back_rounded),
|
||||||
color: UnionFlowColors.textSecondary,
|
color: textSecondary,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (_isMock)
|
if (_isMock)
|
||||||
Container(
|
_buildDevBadge()
|
||||||
padding: const EdgeInsets.symmetric(
|
else
|
||||||
horizontal: 10, vertical: 4),
|
_buildWaveBadge(),
|
||||||
decoration: BoxDecoration(
|
],
|
||||||
color: UnionFlowColors.warningPale,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
border: Border.all(
|
|
||||||
color: UnionFlowColors.warning.withOpacity(0.4)),
|
|
||||||
),
|
),
|
||||||
child: const Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
// ── Contenu principal ─────────────────────────
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.science_rounded,
|
if (_simulating)
|
||||||
size: 13, color: UnionFlowColors.warning),
|
_buildSimulatingView(textPrimary, textSecondary)
|
||||||
SizedBox(width: 4),
|
else
|
||||||
Text(
|
_buildPaymentView(
|
||||||
'Mode dev',
|
montant: montant,
|
||||||
style: TextStyle(
|
bgSurface: bgSurface,
|
||||||
color: UnionFlowColors.warning,
|
borderColor: borderColor,
|
||||||
fontSize: 11,
|
textPrimary: textPrimary,
|
||||||
fontWeight: FontWeight.w700,
|
textSecondary: textSecondary,
|
||||||
|
isDark: isDark,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
else
|
),
|
||||||
Container(
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
);
|
||||||
horizontal: 12, vertical: 4),
|
}
|
||||||
|
|
||||||
|
// ─── Badges ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildDevBadge() => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: waveBlue.withOpacity(0.1),
|
color: AppColors.warning.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: AppColors.warning.withOpacity(0.4)),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.science_rounded, size: 13, color: AppColors.warning),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Mode dev',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.warning,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildWaveBadge() => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _waveBlue.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Wave Mobile Money',
|
'Wave Mobile Money',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: waveBlue,
|
color: _waveBlue, fontSize: 12, fontWeight: FontWeight.w700),
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
|
||||||
],
|
// ─── Vue simulation ───────────────────────────────────────
|
||||||
),
|
|
||||||
Expanded(
|
Widget _buildSimulatingView(Color textPrimary, Color textSecondary) {
|
||||||
child: Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (_simulating) ...[
|
|
||||||
// Animation de simulation
|
|
||||||
Container(
|
Container(
|
||||||
width: 110,
|
width: 110,
|
||||||
height: 110,
|
height: 110,
|
||||||
@@ -168,7 +226,7 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: waveBlue.withOpacity(0.35),
|
color: _waveBlue.withOpacity(0.35),
|
||||||
blurRadius: 24,
|
blurRadius: 24,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
@@ -178,40 +236,48 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 3),
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
const Text(
|
Text(
|
||||||
'Simulation du paiement…',
|
'Simulation du paiement…',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: textPrimary),
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: UnionFlowColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Text(
|
Text(
|
||||||
'Confirmation en cours',
|
'Confirmation en cours auprès du serveur',
|
||||||
style: TextStyle(
|
style: TextStyle(color: textSecondary, fontSize: 14),
|
||||||
color: UnionFlowColors.textSecondary, fontSize: 14),
|
|
||||||
),
|
),
|
||||||
] else ...[
|
],
|
||||||
// Logo Wave
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Vue paiement ─────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildPaymentView({
|
||||||
|
required double montant,
|
||||||
|
required Color bgSurface,
|
||||||
|
required Color borderColor,
|
||||||
|
required Color textPrimary,
|
||||||
|
required Color textSecondary,
|
||||||
|
required bool isDark,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Logo Wave — fond blanc intentionnel (brand)
|
||||||
Container(
|
Container(
|
||||||
width: 110,
|
width: 110,
|
||||||
height: 110,
|
height: 110,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
border: Border.all(color: UnionFlowColors.border),
|
border: Border.all(color: borderColor),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: waveBlue.withOpacity(0.2),
|
color: _waveBlue.withOpacity(0.2),
|
||||||
blurRadius: 24,
|
blurRadius: 24,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
@@ -221,11 +287,8 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/payment_methods/wave/logo.png',
|
'assets/images/payment_methods/wave/logo.png',
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (_, __, ___) => const Icon(
|
errorBuilder: (_, __, ___) =>
|
||||||
Icons.waves_rounded,
|
const Icon(Icons.waves_rounded, color: _waveBlue, size: 52),
|
||||||
color: waveBlue,
|
|
||||||
size: 52,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 28),
|
const SizedBox(height: 28),
|
||||||
@@ -233,14 +296,8 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
Text(
|
Text(
|
||||||
_paymentLaunched
|
_paymentLaunched
|
||||||
? 'Paiement en cours…'
|
? 'Paiement en cours…'
|
||||||
: _isMock
|
: _isMock ? 'Simuler le paiement' : 'Prêt à payer',
|
||||||
? 'Simuler le paiement'
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w800, color: textPrimary),
|
||||||
: 'Prêt à payer',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
color: UnionFlowColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
@@ -253,17 +310,14 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: waveBlue,
|
color: _waveBlue,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(
|
TextSpan(
|
||||||
text: 'FCFA',
|
text: 'FCFA',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18, fontWeight: FontWeight.w700, color: textSecondary),
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: UnionFlowColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -272,9 +326,7 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
widget.souscription.organisationNom!,
|
widget.souscription.organisationNom!,
|
||||||
style: const TextStyle(
|
style: TextStyle(color: textSecondary, fontSize: 13),
|
||||||
color: UnionFlowColors.textSecondary,
|
|
||||||
fontSize: 13),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
@@ -285,24 +337,19 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
margin: const EdgeInsets.only(bottom: 20),
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.warningPale,
|
color: AppColors.warning.withOpacity(isDark ? 0.15 : 0.08),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
border: Border.all(color: AppColors.warning.withOpacity(0.3)),
|
||||||
color:
|
|
||||||
UnionFlowColors.warning.withOpacity(0.3)),
|
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.science_outlined,
|
Icon(Icons.science_outlined, color: AppColors.warning, size: 16),
|
||||||
color: UnionFlowColors.warning, size: 16),
|
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Environnement de développement — le paiement sera simulé automatiquement.',
|
'Environnement de développement — le paiement sera simulé automatiquement. En production, vous serez redirigé vers l\'application Wave.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12, color: AppColors.warning, height: 1.4),
|
||||||
color: UnionFlowColors.warning,
|
|
||||||
height: 1.4),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -316,20 +363,15 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
? Icons.play_circle_rounded
|
? Icons.play_circle_rounded
|
||||||
: Icons.open_in_new_rounded),
|
: Icons.open_in_new_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
_isMock
|
_isMock ? 'Simuler le paiement Wave' : 'Ouvrir Wave',
|
||||||
? 'Simuler le paiement Wave'
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||||||
: 'Ouvrir Wave',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16, fontWeight: FontWeight.w700),
|
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: waveBlue,
|
backgroundColor: _waveBlue,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: AppColors.onPrimary,
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
const EdgeInsets.symmetric(vertical: 15),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
shape: RoundedRectangleBorder(
|
shadowColor: _waveBlue.withOpacity(0.4),
|
||||||
borderRadius: BorderRadius.circular(14)),
|
|
||||||
shadowColor: waveBlue.withOpacity(0.4),
|
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -339,36 +381,29 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UnionFlowColors.surface,
|
color: bgSurface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: UnionFlowColors.softShadow,
|
border: Border.all(color: borderColor),
|
||||||
|
boxShadow: isDark ? null : UnionFlowColors.softShadow,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(color: _waveBlue, strokeWidth: 3),
|
||||||
color: waveBlue,
|
|
||||||
strokeWidth: 3,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'Paiement en cours dans Wave',
|
'Paiement en cours dans Wave',
|
||||||
style: TextStyle(
|
style: TextStyle(fontWeight: FontWeight.w700, color: textPrimary),
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: UnionFlowColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
const Text(
|
Text(
|
||||||
'Revenez dans l\'app une fois\nvotre paiement confirmé.',
|
'Revenez dans l\'app une fois\nvotre paiement confirmé.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: UnionFlowColors.textSecondary,
|
color: textSecondary, fontSize: 13, height: 1.4),
|
||||||
fontSize: 13,
|
|
||||||
height: 1.4),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -380,21 +415,16 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
onPressed: () => context
|
onPressed: () => context
|
||||||
.read<OnboardingBloc>()
|
.read<OnboardingBloc>()
|
||||||
.add(const OnboardingRetourDepuisWave()),
|
.add(const OnboardingRetourDepuisWave()),
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.check_circle_outline_rounded),
|
||||||
Icons.check_circle_outline_rounded),
|
|
||||||
label: const Text(
|
label: const Text(
|
||||||
'J\'ai effectué le paiement',
|
'J\'ai effectué le paiement',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: FontWeight.w700),
|
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: UnionFlowColors.unionGreen,
|
backgroundColor: UnionFlowColors.unionGreen,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: AppColors.onPrimary,
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
const EdgeInsets.symmetric(vertical: 14),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(14)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -403,18 +433,10 @@ class _WavePaymentPageState extends State<WavePaymentPage>
|
|||||||
onPressed: _lancerOuSimuler,
|
onPressed: _lancerOuSimuler,
|
||||||
icon: const Icon(Icons.refresh_rounded, size: 18),
|
icon: const Icon(Icons.refresh_rounded, size: 18),
|
||||||
label: const Text('Rouvrir Wave'),
|
label: const Text('Rouvrir Wave'),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(foregroundColor: _waveBlue),
|
||||||
foregroundColor: waveBlue),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user