diff --git a/b0esche_cloud/assets/icons/b0esche-cloud-icon-sharp.png b/b0esche_cloud/assets/icons/b0esche-cloud-icon-sharp.png new file mode 100644 index 0000000..7f6ef69 Binary files /dev/null and b/b0esche_cloud/assets/icons/b0esche-cloud-icon-sharp.png differ diff --git a/b0esche_cloud/assets/icons/b0esche-cloud-icon.png b/b0esche_cloud/assets/icons/b0esche-cloud-icon.png new file mode 100644 index 0000000..f940341 Binary files /dev/null and b/b0esche_cloud/assets/icons/b0esche-cloud-icon.png differ diff --git a/b0esche_cloud/lib/blocs/auth/auth_bloc.dart b/b0esche_cloud/lib/blocs/auth/auth_bloc.dart index 1cdf31d..d059bda 100644 --- a/b0esche_cloud/lib/blocs/auth/auth_bloc.dart +++ b/b0esche_cloud/lib/blocs/auth/auth_bloc.dart @@ -252,7 +252,21 @@ class AuthBloc extends Bloc { CheckAuthRequested event, Emitter emit, ) async { - // Check if token is valid in SessionBloc - emit(AuthUnauthenticated()); + // Try to restore session from persistent storage + final sessionState = sessionBloc.state; + + if (sessionState is SessionActive) { + // Session already active + emit(AuthAuthenticated( + token: sessionState.token, + userId: '', + username: '', + email: '', + )); + } else { + // Try to restore from SharedPreferences + await Future.delayed(const Duration(milliseconds: 100)); // Give time for restoration + emit(AuthUnauthenticated()); + } } } diff --git a/b0esche_cloud/lib/blocs/session/session_bloc.dart b/b0esche_cloud/lib/blocs/session/session_bloc.dart index c1a0d66..8c00674 100644 --- a/b0esche_cloud/lib/blocs/session/session_bloc.dart +++ b/b0esche_cloud/lib/blocs/session/session_bloc.dart @@ -1,42 +1,98 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'session_event.dart'; import 'session_state.dart'; class SessionBloc extends Bloc { Timer? _expiryTimer; + static const String _tokenKey = 'auth_token'; + static const String _expiryKey = 'auth_expiry'; SessionBloc() : super(SessionInitial()) { on(_onSessionStarted); on(_onSessionExpired); on(_onSessionRefreshed); on(_onSessionEnded); + on(_onSessionRestored); } - void _onSessionStarted(SessionStarted event, Emitter emit) { + void _onSessionStarted(SessionStarted event, Emitter emit) async { final expiresAt = DateTime.now().add( const Duration(minutes: 15), ); // Match Go + + // Save token to persistent storage + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_tokenKey, event.token); + await prefs.setString(_expiryKey, expiresAt.toIso8601String()); + emit(SessionActive(token: event.token, expiresAt: expiresAt)); _startExpiryTimer(expiresAt); } void _onSessionExpired(SessionExpired event, Emitter emit) { _expiryTimer?.cancel(); + _clearStoredSession(); emit(SessionExpiredState()); } - void _onSessionRefreshed(SessionRefreshed event, Emitter emit) { + void _onSessionRefreshed(SessionRefreshed event, Emitter emit) async { final expiresAt = DateTime.now().add(const Duration(minutes: 15)); + + // Update stored token + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_tokenKey, event.newToken); + await prefs.setString(_expiryKey, expiresAt.toIso8601String()); + emit(SessionActive(token: event.newToken, expiresAt: expiresAt)); _startExpiryTimer(expiresAt); } void _onSessionEnded(SessionEnded event, Emitter emit) { _expiryTimer?.cancel(); + _clearStoredSession(); emit(SessionInitial()); } + void _onSessionRestored(SessionRestored event, Emitter emit) { + final expiresAt = event.expiresAt; + final now = DateTime.now(); + + // Check if token is still valid + if (expiresAt.isAfter(now)) { + emit(SessionActive(token: event.token, expiresAt: expiresAt)); + _startExpiryTimer(expiresAt); + } else { + // Token expired, clear it + _clearStoredSession(); + emit(SessionInitial()); + } + } + + Future _clearStoredSession() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_tokenKey); + await prefs.remove(_expiryKey); + } + + static Future restoreSession(SessionBloc bloc) async { + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString(_tokenKey); + final expiryStr = prefs.getString(_expiryKey); + + if (token != null && expiryStr != null) { + try { + final expiresAt = DateTime.parse(expiryStr); + bloc.add(SessionRestored(token: token, expiresAt: expiresAt)); + } catch (e) { + // Invalid stored data, clear it + await prefs.remove(_tokenKey); + await prefs.remove(_expiryKey); + } + } + } + void _startExpiryTimer(DateTime expiresAt) { _expiryTimer?.cancel(); final duration = expiresAt.difference(DateTime.now()); diff --git a/b0esche_cloud/lib/blocs/session/session_event.dart b/b0esche_cloud/lib/blocs/session/session_event.dart index 15471a8..6358834 100644 --- a/b0esche_cloud/lib/blocs/session/session_event.dart +++ b/b0esche_cloud/lib/blocs/session/session_event.dart @@ -28,3 +28,13 @@ class SessionRefreshed extends SessionEvent { } class SessionEnded extends SessionEvent {} + +class SessionRestored extends SessionEvent { + final String token; + final DateTime expiresAt; + + const SessionRestored({required this.token, required this.expiresAt}); + + @override + List get props => [token, expiresAt]; +} diff --git a/b0esche_cloud/lib/main.dart b/b0esche_cloud/lib/main.dart index af75458..2596c70 100644 --- a/b0esche_cloud/lib/main.dart +++ b/b0esche_cloud/lib/main.dart @@ -41,23 +41,37 @@ void main() { runApp(const MainApp()); } -class MainApp extends StatelessWidget { +class MainApp extends StatefulWidget { const MainApp({super.key}); + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + final _sessionBloc = SessionBloc(); + late final AuthBloc _authBloc; + + @override + void initState() { + super.initState(); + _authBloc = AuthBloc( + apiClient: ApiClient(_sessionBloc), + sessionBloc: _sessionBloc, + ); + // Restore session from persistent storage + SessionBloc.restoreSession(_sessionBloc); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (_) => SessionBloc()), - BlocProvider( - create: (context) => AuthBloc( - apiClient: ApiClient(context.read()), - sessionBloc: context.read(), - ), - ), + BlocProvider.value(value: _sessionBloc), + BlocProvider.value(value: _authBloc), BlocProvider( create: (context) => - ActivityBloc(ActivityApi(ApiClient(context.read()))), + ActivityBloc(ActivityApi(ApiClient(_sessionBloc))), ), ], child: MaterialApp.router( @@ -66,4 +80,11 @@ class MainApp extends StatelessWidget { ), ); } + + @override + void dispose() { + _authBloc.close(); + _sessionBloc.close(); + super.dispose(); + } }