import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/data/network/auth_session_event.dart'; import 'package:hl_lieferservice/data/network/keycloak_oidc_token_provider.dart'; import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart'; import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart'; import 'package:hl_lieferservice/main.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; /// AuthBloc bridge between der UI und dem [KeycloakOidcTokenProvider]. /// /// Eingehende UI-Events triggern den Provider (Login/Logout/Restore). /// Provider-Ereignisse kommen über den `events`-Stream rein, werden zu /// `ProviderSessionChanged` übersetzt und vom Bloc in Zustände überführt. class AuthBloc extends Bloc { AuthBloc({required this.tokenProvider, required this.operationBloc}) : super(Unauthenticated()) { on(_handleLogin); on(_handleLogout); on(_handleRestore); on(_handleProviderEvent); // Legacy: ERPframe-Repos feuern bei 401. on((event, emit) async { await tokenProvider.logout(); if (locator.isRegistered()) { locator.unregister(); } emit(Unauthenticated(sessionExpired: true)); }); _subscription = tokenProvider.events.listen( (event) => add(_toBlocEvent(event)), ); } final KeycloakOidcTokenProvider tokenProvider; final OperationBloc operationBloc; late final StreamSubscription _subscription; Future _handleLogin( LoginRequested event, Emitter emit, ) async { try { emit(Authenticating()); await tokenProvider.login(); // Erfolg landet via Stream-Subscription als // ProviderSessionChanged.loggedIn → _handleProviderEvent. } catch (err, st) { debugPrint('Login fehlgeschlagen: $err\n$st'); emit(Unauthenticated()); operationBloc.add( FailOperation( message: 'Login war nicht erfolgreich. Probieren Sie es erneut.', ), ); } } Future _handleLogout( LogoutRequested event, Emitter emit, ) async { await tokenProvider.logout(); // Provider feuert AuthLoggedOut → _handleProviderEvent setzt den State. } Future _handleRestore( RestoreSessionRequested event, Emitter emit, ) async { try { await tokenProvider.restoreSession(); // Erfolg landet via Stream als ProviderSessionChanged.loggedIn. // Misserfolg: bleibt bei Unauthenticated — kein Snackbar, das // ist der normale Cold-Start-Pfad. } catch (err, st) { debugPrint('Restore-Session fehlgeschlagen: $err\n$st'); // State unverändert (Unauthenticated). Kein Snackbar — das // wäre für den Cold-Start zu viel Lärm. } } Future _handleProviderEvent( ProviderSessionChanged event, Emitter emit, ) async { switch (event.kind) { case ProviderEventKind.loggedIn: try { final state = Authenticated.fromClaims( claims: event.claims ?? const {}, accessToken: event.accessToken ?? '', ); if (locator.isRegistered()) { locator.unregister(); } locator.registerSingleton(state); emit(state); } catch (err, st) { debugPrint('Konnte Claims nicht in Authenticated-State mappen: $err\n$st'); emit(Unauthenticated()); operationBloc.add( FailOperation( message: 'Login-Antwort vom Server unvollständig. Bitte erneut versuchen.', ), ); } case ProviderEventKind.loggedOut: if (locator.isRegistered()) { locator.unregister(); } emit(Unauthenticated()); case ProviderEventKind.sessionExpired: if (locator.isRegistered()) { locator.unregister(); } emit(Unauthenticated(sessionExpired: true)); } } ProviderSessionChanged _toBlocEvent(AuthSessionEvent event) { return switch (event) { AuthLoggedIn(:final claims) => ProviderSessionChanged( ProviderEventKind.loggedIn, claims: claims, // Wir würden hier idealerweise den Access-Token mit übergeben. // Da der Provider den Token nicht im Stream-Event mitliefert, // bauen wir später eine Brücke. Für jetzt: leerer String — // Authenticated.sessionId wird mit Phase D ohnehin abgeschafft. accessToken: '', ), AuthLoggedOut() => const ProviderSessionChanged( ProviderEventKind.loggedOut, ), AuthSessionExpired() => const ProviderSessionChanged( ProviderEventKind.sessionExpired, ), }; } @override Future close() async { await _subscription.cancel(); return super.close(); } }