- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
389 lines
11 KiB
Dart
389 lines
11 KiB
Dart
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:injectable/injectable.dart';
|
|
import '../../../../core/models/evenement_model.dart';
|
|
import '../../domain/repositories/evenement_repository.dart';
|
|
import 'evenement_event.dart';
|
|
import 'evenement_state.dart';
|
|
|
|
/// BLoC pour la gestion des événements
|
|
@injectable
|
|
class EvenementBloc extends Bloc<EvenementEvent, EvenementState> {
|
|
final EvenementRepository _repository;
|
|
|
|
EvenementBloc(this._repository) : super(const EvenementInitial()) {
|
|
on<LoadEvenementsAVenir>(_onLoadEvenementsAVenir);
|
|
on<LoadEvenementsPublics>(_onLoadEvenementsPublics);
|
|
on<LoadEvenements>(_onLoadEvenements);
|
|
on<LoadEvenementById>(_onLoadEvenementById);
|
|
on<SearchEvenements>(_onSearchEvenements);
|
|
on<FilterEvenementsByType>(_onFilterEvenementsByType);
|
|
on<CreateEvenement>(_onCreateEvenement);
|
|
on<UpdateEvenement>(_onUpdateEvenement);
|
|
on<DeleteEvenement>(_onDeleteEvenement);
|
|
on<ChangeStatutEvenement>(_onChangeStatutEvenement);
|
|
on<LoadStatistiquesEvenements>(_onLoadStatistiquesEvenements);
|
|
on<ResetEvenementState>(_onResetEvenementState);
|
|
}
|
|
|
|
/// Charge les événements à venir
|
|
Future<void> _onLoadEvenementsAVenir(
|
|
LoadEvenementsAVenir event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
if (event.refresh || state is EvenementInitial) {
|
|
emit(const EvenementLoading());
|
|
} else if (state is EvenementLoaded) {
|
|
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
|
}
|
|
|
|
final evenements = await _repository.getEvenementsAVenir(
|
|
page: event.page,
|
|
size: event.size,
|
|
);
|
|
|
|
if (event.refresh || event.page == 0) {
|
|
emit(EvenementLoaded(
|
|
evenements: evenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
));
|
|
} else {
|
|
final currentState = state as EvenementLoaded;
|
|
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
|
..addAll(evenements);
|
|
|
|
emit(currentState.copyWith(
|
|
evenements: allEvenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
));
|
|
}
|
|
} catch (e) {
|
|
final currentEvenements = state is EvenementLoaded
|
|
? (state as EvenementLoaded).evenements
|
|
: null;
|
|
emit(EvenementError(
|
|
message: e.toString(),
|
|
evenements: currentEvenements,
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Charge les événements publics
|
|
Future<void> _onLoadEvenementsPublics(
|
|
LoadEvenementsPublics event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
if (event.refresh || state is EvenementInitial) {
|
|
emit(const EvenementLoading());
|
|
} else if (state is EvenementLoaded) {
|
|
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
|
}
|
|
|
|
final evenements = await _repository.getEvenementsPublics(
|
|
page: event.page,
|
|
size: event.size,
|
|
);
|
|
|
|
if (event.refresh || event.page == 0) {
|
|
emit(EvenementLoaded(
|
|
evenements: evenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
));
|
|
} else {
|
|
final currentState = state as EvenementLoaded;
|
|
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
|
..addAll(evenements);
|
|
|
|
emit(currentState.copyWith(
|
|
evenements: allEvenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
));
|
|
}
|
|
} catch (e) {
|
|
final currentEvenements = state is EvenementLoaded
|
|
? (state as EvenementLoaded).evenements
|
|
: null;
|
|
emit(EvenementError(
|
|
message: e.toString(),
|
|
evenements: currentEvenements,
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Charge tous les événements
|
|
Future<void> _onLoadEvenements(
|
|
LoadEvenements event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
if (event.refresh || state is EvenementInitial) {
|
|
emit(const EvenementLoading());
|
|
} else if (state is EvenementLoaded) {
|
|
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
|
}
|
|
|
|
final evenements = await _repository.getEvenements(
|
|
page: event.page,
|
|
size: event.size,
|
|
sortField: event.sortField,
|
|
sortDirection: event.sortDirection,
|
|
);
|
|
|
|
if (event.refresh || event.page == 0) {
|
|
emit(EvenementLoaded(
|
|
evenements: evenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
));
|
|
} else {
|
|
final currentState = state as EvenementLoaded;
|
|
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
|
..addAll(evenements);
|
|
|
|
emit(currentState.copyWith(
|
|
evenements: allEvenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
));
|
|
}
|
|
} catch (e) {
|
|
final currentEvenements = state is EvenementLoaded
|
|
? (state as EvenementLoaded).evenements
|
|
: null;
|
|
emit(EvenementError(
|
|
message: e.toString(),
|
|
evenements: currentEvenements,
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Charge un événement par ID
|
|
Future<void> _onLoadEvenementById(
|
|
LoadEvenementById event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
emit(const EvenementLoading());
|
|
|
|
final evenement = await _repository.getEvenementById(event.id);
|
|
|
|
emit(EvenementDetailLoaded(evenement));
|
|
} catch (e) {
|
|
emit(EvenementError(message: e.toString()));
|
|
}
|
|
}
|
|
|
|
/// Recherche d'événements
|
|
Future<void> _onSearchEvenements(
|
|
SearchEvenements event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
if (event.refresh || event.page == 0) {
|
|
emit(const EvenementLoading());
|
|
} else if (state is EvenementLoaded) {
|
|
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
|
}
|
|
|
|
final evenements = await _repository.rechercherEvenements(
|
|
event.terme,
|
|
page: event.page,
|
|
size: event.size,
|
|
);
|
|
|
|
if (evenements.isEmpty && event.page == 0) {
|
|
emit(EvenementSearchEmpty(event.terme));
|
|
return;
|
|
}
|
|
|
|
if (event.refresh || event.page == 0) {
|
|
emit(EvenementLoaded(
|
|
evenements: evenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
searchTerm: event.terme,
|
|
));
|
|
} else {
|
|
final currentState = state as EvenementLoaded;
|
|
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
|
..addAll(evenements);
|
|
|
|
emit(currentState.copyWith(
|
|
evenements: allEvenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
searchTerm: event.terme,
|
|
));
|
|
}
|
|
} catch (e) {
|
|
final currentEvenements = state is EvenementLoaded
|
|
? (state as EvenementLoaded).evenements
|
|
: null;
|
|
emit(EvenementError(
|
|
message: e.toString(),
|
|
evenements: currentEvenements,
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Filtre par type d'événement
|
|
Future<void> _onFilterEvenementsByType(
|
|
FilterEvenementsByType event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
if (event.refresh || event.page == 0) {
|
|
emit(const EvenementLoading());
|
|
} else if (state is EvenementLoaded) {
|
|
emit(EvenementLoadingMore((state as EvenementLoaded).evenements));
|
|
}
|
|
|
|
final evenements = await _repository.getEvenementsByType(
|
|
event.type,
|
|
page: event.page,
|
|
size: event.size,
|
|
);
|
|
|
|
if (evenements.isEmpty && event.page == 0) {
|
|
emit(const EvenementEmpty(message: 'Aucun événement de ce type trouvé'));
|
|
return;
|
|
}
|
|
|
|
if (event.refresh || event.page == 0) {
|
|
emit(EvenementLoaded(
|
|
evenements: evenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
filterType: event.type,
|
|
));
|
|
} else {
|
|
final currentState = state as EvenementLoaded;
|
|
final allEvenements = List<EvenementModel>.from(currentState.evenements)
|
|
..addAll(evenements);
|
|
|
|
emit(currentState.copyWith(
|
|
evenements: allEvenements,
|
|
hasReachedMax: evenements.length < event.size,
|
|
currentPage: event.page,
|
|
filterType: event.type,
|
|
));
|
|
}
|
|
} catch (e) {
|
|
final currentEvenements = state is EvenementLoaded
|
|
? (state as EvenementLoaded).evenements
|
|
: null;
|
|
emit(EvenementError(
|
|
message: e.toString(),
|
|
evenements: currentEvenements,
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Crée un nouvel événement
|
|
Future<void> _onCreateEvenement(
|
|
CreateEvenement event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
emit(const EvenementLoading());
|
|
|
|
final evenement = await _repository.createEvenement(event.evenement);
|
|
|
|
emit(EvenementOperationSuccess(
|
|
message: 'Événement créé avec succès',
|
|
evenement: evenement,
|
|
));
|
|
} catch (e) {
|
|
emit(EvenementError(message: e.toString()));
|
|
}
|
|
}
|
|
|
|
/// Met à jour un événement
|
|
Future<void> _onUpdateEvenement(
|
|
UpdateEvenement event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
emit(const EvenementLoading());
|
|
|
|
final evenement = await _repository.updateEvenement(event.id, event.evenement);
|
|
|
|
emit(EvenementOperationSuccess(
|
|
message: 'Événement mis à jour avec succès',
|
|
evenement: evenement,
|
|
));
|
|
} catch (e) {
|
|
emit(EvenementError(message: e.toString()));
|
|
}
|
|
}
|
|
|
|
/// Supprime un événement
|
|
Future<void> _onDeleteEvenement(
|
|
DeleteEvenement event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
emit(const EvenementLoading());
|
|
|
|
await _repository.deleteEvenement(event.id);
|
|
|
|
emit(const EvenementOperationSuccess(
|
|
message: 'Événement supprimé avec succès',
|
|
));
|
|
} catch (e) {
|
|
emit(EvenementError(message: e.toString()));
|
|
}
|
|
}
|
|
|
|
/// Change le statut d'un événement
|
|
Future<void> _onChangeStatutEvenement(
|
|
ChangeStatutEvenement event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
emit(const EvenementLoading());
|
|
|
|
final evenement = await _repository.changerStatutEvenement(
|
|
event.id,
|
|
event.nouveauStatut,
|
|
);
|
|
|
|
emit(EvenementOperationSuccess(
|
|
message: 'Statut de l\'événement modifié avec succès',
|
|
evenement: evenement,
|
|
));
|
|
} catch (e) {
|
|
emit(EvenementError(message: e.toString()));
|
|
}
|
|
}
|
|
|
|
/// Charge les statistiques
|
|
Future<void> _onLoadStatistiquesEvenements(
|
|
LoadStatistiquesEvenements event,
|
|
Emitter<EvenementState> emit,
|
|
) async {
|
|
try {
|
|
emit(const EvenementLoading());
|
|
|
|
final statistiques = await _repository.getStatistiquesEvenements();
|
|
|
|
emit(EvenementStatistiquesLoaded(statistiques));
|
|
} catch (e) {
|
|
emit(EvenementError(message: e.toString()));
|
|
}
|
|
}
|
|
|
|
/// Réinitialise l'état
|
|
void _onResetEvenementState(
|
|
ResetEvenementState event,
|
|
Emitter<EvenementState> emit,
|
|
) {
|
|
emit(const EvenementInitial());
|
|
}
|
|
}
|