Files
unionflow-mobile-apps/lib/features/onboarding/bloc/onboarding_bloc.dart
dahoud 70cbd1c873 fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod
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
2026-04-07 20:56:03 +00:00

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());
}
}
}