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
362 lines
12 KiB
Dart
362 lines
12 KiB
Dart
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import 'package:injectable/injectable.dart';
|
|
import '../data/datasources/souscription_datasource.dart';
|
|
import '../data/models/formule_model.dart';
|
|
import '../data/models/souscription_status_model.dart';
|
|
import '../../../../core/utils/logger.dart';
|
|
|
|
// ─────────────────────────────────────────────────────────────── Events ──────
|
|
|
|
abstract class OnboardingEvent extends Equatable {
|
|
const OnboardingEvent();
|
|
@override
|
|
List<Object?> get props => [];
|
|
}
|
|
|
|
/// Démarre le workflow (charge les formules + état courant si souscription existante)
|
|
class OnboardingStarted extends OnboardingEvent {
|
|
final String? existingSouscriptionId;
|
|
final String initialState; // NO_SUBSCRIPTION | AWAITING_PAYMENT | PAYMENT_INITIATED | AWAITING_VALIDATION
|
|
final String? typeOrganisation;
|
|
final String? organisationId;
|
|
const OnboardingStarted({
|
|
required this.initialState,
|
|
this.existingSouscriptionId,
|
|
this.typeOrganisation,
|
|
this.organisationId,
|
|
});
|
|
@override
|
|
List<Object?> get props => [initialState, existingSouscriptionId, typeOrganisation, organisationId];
|
|
}
|
|
|
|
/// L'utilisateur a sélectionné une formule et une plage
|
|
class OnboardingFormuleSelected extends OnboardingEvent {
|
|
final String codeFormule;
|
|
final String plage;
|
|
const OnboardingFormuleSelected({required this.codeFormule, required this.plage});
|
|
@override
|
|
List<Object?> get props => [codeFormule, plage];
|
|
}
|
|
|
|
/// L'utilisateur a sélectionné la période et le type d'organisation
|
|
class OnboardingPeriodeSelected extends OnboardingEvent {
|
|
final String typePeriode;
|
|
final String typeOrganisation;
|
|
final String organisationId;
|
|
const OnboardingPeriodeSelected({
|
|
required this.typePeriode,
|
|
required this.typeOrganisation,
|
|
required this.organisationId,
|
|
});
|
|
@override
|
|
List<Object?> get props => [typePeriode, typeOrganisation, organisationId];
|
|
}
|
|
|
|
/// Confirme la demande et crée la souscription en base
|
|
class OnboardingDemandeConfirmee extends OnboardingEvent {
|
|
const OnboardingDemandeConfirmee();
|
|
}
|
|
|
|
/// Ouvre l'écran de choix du moyen de paiement (depuis le récapitulatif)
|
|
class OnboardingChoixPaiementOuvert extends OnboardingEvent {
|
|
const OnboardingChoixPaiementOuvert();
|
|
}
|
|
|
|
/// Initie le paiement Wave
|
|
class OnboardingPaiementInitie extends OnboardingEvent {
|
|
const OnboardingPaiementInitie();
|
|
}
|
|
|
|
/// L'utilisateur est revenu dans l'app après le paiement Wave
|
|
class OnboardingRetourDepuisWave extends OnboardingEvent {
|
|
const OnboardingRetourDepuisWave();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────── States ──────
|
|
|
|
abstract class OnboardingState extends Equatable {
|
|
const OnboardingState();
|
|
@override
|
|
List<Object?> get props => [];
|
|
}
|
|
|
|
class OnboardingInitial extends OnboardingState {}
|
|
class OnboardingLoading extends OnboardingState {}
|
|
|
|
class OnboardingError extends OnboardingState {
|
|
final String message;
|
|
const OnboardingError(this.message);
|
|
@override
|
|
List<Object?> get props => [message];
|
|
}
|
|
|
|
/// Étape 1 : choix formule + plage (affiche la grille des formules)
|
|
class OnboardingStepFormule extends OnboardingState {
|
|
final List<FormuleModel> formules;
|
|
const OnboardingStepFormule(this.formules);
|
|
@override
|
|
List<Object?> get props => [formules];
|
|
}
|
|
|
|
/// Étape 2 : choix période + type organisation (formule/plage déjà sélectionnés)
|
|
class OnboardingStepPeriode extends OnboardingState {
|
|
final String codeFormule;
|
|
final String plage;
|
|
final List<FormuleModel> formules;
|
|
const OnboardingStepPeriode({
|
|
required this.codeFormule,
|
|
required this.plage,
|
|
required this.formules,
|
|
});
|
|
@override
|
|
List<Object?> get props => [codeFormule, plage, formules];
|
|
}
|
|
|
|
/// Étape 3 : récapitulatif avec montant calculé (avant paiement)
|
|
class OnboardingStepSummary extends OnboardingState {
|
|
final SouscriptionStatusModel souscription;
|
|
const OnboardingStepSummary(this.souscription);
|
|
@override
|
|
List<Object?> get props => [souscription];
|
|
}
|
|
|
|
/// Étape 3b : choix du moyen de paiement (Wave, Orange Money, etc.)
|
|
class OnboardingStepChoixPaiement extends OnboardingState {
|
|
final SouscriptionStatusModel souscription;
|
|
const OnboardingStepChoixPaiement(this.souscription);
|
|
@override
|
|
List<Object?> get props => [souscription];
|
|
}
|
|
|
|
/// Étape 4 : paiement Wave — URL à ouvrir dans le navigateur
|
|
class OnboardingStepPaiement extends OnboardingState {
|
|
final SouscriptionStatusModel souscription;
|
|
final String waveLaunchUrl;
|
|
const OnboardingStepPaiement({required this.souscription, required this.waveLaunchUrl});
|
|
@override
|
|
List<Object?> get props => [souscription, waveLaunchUrl];
|
|
}
|
|
|
|
/// Paiement confirmé — déclenche un re-check du statut du compte
|
|
class OnboardingPaiementConfirme extends OnboardingState {}
|
|
|
|
/// Erreur de confirmation paiement — l'utilisateur peut réessayer
|
|
class OnboardingPaiementEchoue extends OnboardingState {
|
|
final String message;
|
|
final SouscriptionStatusModel souscription;
|
|
final String waveLaunchUrl;
|
|
const OnboardingPaiementEchoue({
|
|
required this.message,
|
|
required this.souscription,
|
|
required this.waveLaunchUrl,
|
|
});
|
|
@override
|
|
List<Object?> get props => [message, souscription, waveLaunchUrl];
|
|
}
|
|
|
|
/// Étape 5 : en attente de validation SuperAdmin
|
|
class OnboardingStepAttente extends OnboardingState {
|
|
final SouscriptionStatusModel? souscription;
|
|
const OnboardingStepAttente({this.souscription});
|
|
@override
|
|
List<Object?> get props => [souscription];
|
|
}
|
|
|
|
/// Souscription rejetée par le SuperAdmin
|
|
class OnboardingRejected extends OnboardingState {
|
|
final String? commentaire;
|
|
const OnboardingRejected({this.commentaire});
|
|
@override
|
|
List<Object?> get props => [commentaire];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────── BLoC ────────
|
|
|
|
@injectable
|
|
class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
|
|
final SouscriptionDatasource _datasource;
|
|
|
|
// Données accumulées au fil du wizard
|
|
List<FormuleModel> _formules = [];
|
|
String? _codeFormule;
|
|
String? _plage;
|
|
String? _typePeriode;
|
|
String _typeOrganisation = '';
|
|
String? _organisationId;
|
|
SouscriptionStatusModel? _souscription;
|
|
|
|
OnboardingBloc(this._datasource) : super(OnboardingInitial()) {
|
|
on<OnboardingStarted>(_onStarted);
|
|
on<OnboardingFormuleSelected>(_onFormuleSelected);
|
|
on<OnboardingPeriodeSelected>(_onPeriodeSelected);
|
|
on<OnboardingDemandeConfirmee>(_onDemandeConfirmee);
|
|
on<OnboardingChoixPaiementOuvert>(_onChoixPaiementOuvert);
|
|
on<OnboardingPaiementInitie>(_onPaiementInitie);
|
|
on<OnboardingRetourDepuisWave>(_onRetourDepuisWave);
|
|
}
|
|
|
|
Future<void> _onStarted(OnboardingStarted event, Emitter<OnboardingState> emit) async {
|
|
emit(OnboardingLoading());
|
|
if (event.typeOrganisation != null && event.typeOrganisation!.isNotEmpty) {
|
|
_typeOrganisation = event.typeOrganisation!;
|
|
}
|
|
if (event.organisationId != null && event.organisationId!.isNotEmpty) {
|
|
_organisationId = event.organisationId;
|
|
}
|
|
try {
|
|
_formules = await _datasource.getFormules();
|
|
|
|
switch (event.initialState) {
|
|
case 'NO_SUBSCRIPTION':
|
|
emit(OnboardingStepFormule(_formules));
|
|
|
|
case 'AWAITING_PAYMENT':
|
|
final sosc = await _datasource.getMaSouscription();
|
|
if (sosc != null) {
|
|
_souscription = sosc;
|
|
emit(OnboardingStepSummary(sosc));
|
|
} else {
|
|
emit(OnboardingStepFormule(_formules));
|
|
}
|
|
|
|
case 'PAYMENT_INITIATED':
|
|
final sosc = await _datasource.getMaSouscription();
|
|
if (sosc != null && sosc.waveLaunchUrl != null) {
|
|
_souscription = sosc;
|
|
emit(OnboardingStepPaiement(
|
|
souscription: sosc,
|
|
waveLaunchUrl: sosc.waveLaunchUrl!,
|
|
));
|
|
} else {
|
|
emit(OnboardingStepAttente(souscription: sosc));
|
|
}
|
|
|
|
case 'AWAITING_VALIDATION':
|
|
case 'VALIDATED': // Paiement confirmé mais activation compte non encore effective
|
|
final sosc = await _datasource.getMaSouscription();
|
|
_souscription = sosc;
|
|
emit(OnboardingStepAttente(souscription: sosc));
|
|
|
|
case 'REJECTED':
|
|
final sosc = await _datasource.getMaSouscription();
|
|
emit(OnboardingRejected(commentaire: sosc?.statutValidation));
|
|
|
|
default:
|
|
emit(OnboardingStepFormule(_formules));
|
|
}
|
|
} catch (e) {
|
|
AppLogger.error('OnboardingBloc._onStarted: $e');
|
|
emit(const OnboardingError('Impossible de charger les formules. Vérifiez votre connexion.'));
|
|
}
|
|
}
|
|
|
|
void _onFormuleSelected(OnboardingFormuleSelected event, Emitter<OnboardingState> emit) {
|
|
_codeFormule = event.codeFormule;
|
|
_plage = event.plage;
|
|
emit(OnboardingStepPeriode(
|
|
codeFormule: event.codeFormule,
|
|
plage: event.plage,
|
|
formules: _formules,
|
|
));
|
|
}
|
|
|
|
void _onPeriodeSelected(OnboardingPeriodeSelected event, Emitter<OnboardingState> emit) {
|
|
_typePeriode = event.typePeriode;
|
|
// typeOrganisation already set from OnboardingStarted; override only if event provides one
|
|
if (event.typeOrganisation.isNotEmpty) {
|
|
_typeOrganisation = event.typeOrganisation;
|
|
}
|
|
_organisationId = event.organisationId;
|
|
}
|
|
|
|
void _onChoixPaiementOuvert(OnboardingChoixPaiementOuvert event, Emitter<OnboardingState> emit) {
|
|
if (_souscription != null) {
|
|
emit(OnboardingStepChoixPaiement(_souscription!));
|
|
}
|
|
}
|
|
|
|
Future<void> _onDemandeConfirmee(
|
|
OnboardingDemandeConfirmee event, Emitter<OnboardingState> emit) async {
|
|
if (_codeFormule == null || _plage == null || _typePeriode == null ||
|
|
_organisationId == null) {
|
|
emit(const OnboardingError('Données manquantes. Recommencez depuis le début.'));
|
|
return;
|
|
}
|
|
emit(OnboardingLoading());
|
|
try {
|
|
final sosc = await _datasource.creerDemande(
|
|
typeFormule: _codeFormule!,
|
|
plageMembres: _plage!,
|
|
typePeriode: _typePeriode!,
|
|
typeOrganisation: _typeOrganisation.isNotEmpty ? _typeOrganisation : null,
|
|
organisationId: _organisationId!,
|
|
);
|
|
if (sosc != null) {
|
|
_souscription = sosc;
|
|
emit(OnboardingStepSummary(sosc));
|
|
} else {
|
|
emit(const OnboardingError('Erreur lors de la création de la demande.'));
|
|
}
|
|
} catch (e) {
|
|
emit(OnboardingError('Erreur: $e'));
|
|
}
|
|
}
|
|
|
|
Future<void> _onPaiementInitie(
|
|
OnboardingPaiementInitie event, Emitter<OnboardingState> emit) async {
|
|
final souscId = _souscription?.souscriptionId;
|
|
if (souscId == null) {
|
|
emit(const OnboardingError('Souscription introuvable.'));
|
|
return;
|
|
}
|
|
emit(OnboardingLoading());
|
|
try {
|
|
final updated = await _datasource.initierPaiement(souscId);
|
|
if (updated?.waveLaunchUrl != null) {
|
|
_souscription = updated;
|
|
emit(OnboardingStepPaiement(
|
|
souscription: updated!,
|
|
waveLaunchUrl: updated.waveLaunchUrl!,
|
|
));
|
|
} else {
|
|
emit(const OnboardingError('Impossible d\'initier le paiement Wave.'));
|
|
}
|
|
} catch (e) {
|
|
emit(OnboardingError('Erreur paiement: $e'));
|
|
}
|
|
}
|
|
|
|
Future<void> _onRetourDepuisWave(
|
|
OnboardingRetourDepuisWave event, Emitter<OnboardingState> emit) async {
|
|
emit(OnboardingLoading());
|
|
final souscId = _souscription?.souscriptionId;
|
|
final waveUrl = _souscription?.waveLaunchUrl;
|
|
|
|
if (souscId == null) {
|
|
emit(const OnboardingError('Souscription introuvable.'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final success = await _datasource.confirmerPaiement(souscId);
|
|
if (success) {
|
|
emit(OnboardingPaiementConfirme());
|
|
} else {
|
|
// La confirmation a échoué côté backend — l'utilisateur doit réessayer
|
|
emit(OnboardingPaiementEchoue(
|
|
message: 'Le paiement n\'a pas pu être confirmé. Vérifiez que le paiement Wave a bien été effectué et réessayez.',
|
|
souscription: _souscription!,
|
|
waveLaunchUrl: waveUrl ?? '',
|
|
));
|
|
}
|
|
} catch (e) {
|
|
emit(OnboardingPaiementEchoue(
|
|
message: 'Erreur lors de la confirmation: ${e.toString().replaceFirst("Exception: ", "")}',
|
|
souscription: _souscription!,
|
|
waveLaunchUrl: waveUrl ?? '',
|
|
));
|
|
}
|
|
}
|
|
}
|