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 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 | VALIDATED final String? typeOrganisation; final String? organisationId; const OnboardingStarted({ required this.initialState, this.existingSouscriptionId, this.typeOrganisation, this.organisationId, }); @override List 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 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 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 get props => []; } class OnboardingInitial extends OnboardingState {} class OnboardingLoading extends OnboardingState {} class OnboardingError extends OnboardingState { final String message; const OnboardingError(this.message); @override List get props => [message]; } /// Étape 1 : choix formule + plage (affiche la grille des formules) class OnboardingStepFormule extends OnboardingState { final List formules; const OnboardingStepFormule(this.formules); @override List 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 formules; const OnboardingStepPeriode({ required this.codeFormule, required this.plage, required this.formules, }); @override List 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 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 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 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 get props => [message, souscription, waveLaunchUrl]; } /// Étape 5 : en attente d'activation (paiement reçu, activation en cours) class OnboardingStepAttente extends OnboardingState { final SouscriptionStatusModel? souscription; const OnboardingStepAttente({this.souscription}); @override List get props => [souscription]; } /// Souscription rejetée par le SuperAdmin class OnboardingRejected extends OnboardingState { final String? commentaire; const OnboardingRejected({this.commentaire}); @override List get props => [commentaire]; } // ─────────────────────────────────────────────────────────────── BLoC ──────── @injectable class OnboardingBloc extends Bloc { final SouscriptionDatasource _datasource; // Données accumulées au fil du wizard List _formules = []; String? _codeFormule; String? _plage; String? _typePeriode; String _typeOrganisation = ''; String? _organisationId; SouscriptionStatusModel? _souscription; OnboardingBloc(this._datasource) : super(OnboardingInitial()) { on(_onStarted); on(_onFormuleSelected); on(_onPeriodeSelected); on(_onDemandeConfirmee); on(_onChoixPaiementOuvert); on(_onPaiementInitie); on(_onRetourDepuisWave); } Future _onStarted(OnboardingStarted event, Emitter 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': final soscAwait = await _datasource.getMaSouscription(); _souscription = soscAwait; emit(OnboardingStepAttente(souscription: soscAwait)); case 'VALIDATED': // Paiement confirmé, souscription validée, mais activation pas encore effective // (edge case : auth_bloc a déjà tenté un refresh sans succès). // Afficher la page d'attente avec polling 15s (AwaitingValidationPage). final soscValidated = await _datasource.getMaSouscription(); _souscription = soscValidated; emit(OnboardingStepAttente(souscription: soscValidated)); 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 emit) { _codeFormule = event.codeFormule; _plage = event.plage; emit(OnboardingStepPeriode( codeFormule: event.codeFormule, plage: event.plage, formules: _formules, )); } void _onPeriodeSelected(OnboardingPeriodeSelected event, Emitter 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 emit) { if (_souscription != null) { emit(OnboardingStepChoixPaiement(_souscription!)); } } Future _onDemandeConfirmee( OnboardingDemandeConfirmee event, Emitter 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 _onPaiementInitie( OnboardingPaiementInitie event, Emitter 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 _onRetourDepuisWave( OnboardingRetourDepuisWave event, Emitter 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 ?? '', )); } } }