Auth: - profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password Multi-org (Phase 3): - OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry - org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role Navigation: - MorePage: navigation conditionnelle par typeOrganisation - Suppression adaptive_navigation (remplacé par main_navigation_layout) Auth AppAuth: - keycloak_webview_auth_service: fixes AppAuth Android - AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet Onboarding: - Nouveaux états: payment_method_page, onboarding_shared_widgets - SouscriptionStatusModel mis à jour StatutValidationSouscription Android: - build.gradle: ProGuard/R8, network_security_config - Gradle wrapper mis à jour
332 lines
12 KiB
Dart
332 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 {}
|
|
|
|
/// É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());
|
|
try {
|
|
final souscId = _souscription?.souscriptionId;
|
|
if (souscId != null) {
|
|
await _datasource.confirmerPaiement(souscId);
|
|
}
|
|
// Émettre OnboardingPaiementConfirme pour déclencher re-check du compte
|
|
// Si le backend auto-active le compte, AuthStatusChecked redirigera vers dashboard
|
|
emit(OnboardingPaiementConfirme());
|
|
} catch (e) {
|
|
emit(OnboardingPaiementConfirme());
|
|
}
|
|
}
|
|
}
|