Add session persistence with shared_preferences - maintain login across page refreshes
This commit is contained in:
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon-sharp.png
Normal file
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon-sharp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 574 KiB |
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon.png
Normal file
BIN
b0esche_cloud/assets/icons/b0esche-cloud-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 731 KiB |
@@ -252,7 +252,21 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
CheckAuthRequested event,
|
||||
Emitter<AuthState> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SessionEvent, SessionState> {
|
||||
Timer? _expiryTimer;
|
||||
static const String _tokenKey = 'auth_token';
|
||||
static const String _expiryKey = 'auth_expiry';
|
||||
|
||||
SessionBloc() : super(SessionInitial()) {
|
||||
on<SessionStarted>(_onSessionStarted);
|
||||
on<SessionExpired>(_onSessionExpired);
|
||||
on<SessionRefreshed>(_onSessionRefreshed);
|
||||
on<SessionEnded>(_onSessionEnded);
|
||||
on<SessionRestored>(_onSessionRestored);
|
||||
}
|
||||
|
||||
void _onSessionStarted(SessionStarted event, Emitter<SessionState> emit) {
|
||||
void _onSessionStarted(SessionStarted event, Emitter<SessionState> 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<SessionState> emit) {
|
||||
_expiryTimer?.cancel();
|
||||
_clearStoredSession();
|
||||
emit(SessionExpiredState());
|
||||
}
|
||||
|
||||
void _onSessionRefreshed(SessionRefreshed event, Emitter<SessionState> emit) {
|
||||
void _onSessionRefreshed(SessionRefreshed event, Emitter<SessionState> 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<SessionState> emit) {
|
||||
_expiryTimer?.cancel();
|
||||
_clearStoredSession();
|
||||
emit(SessionInitial());
|
||||
}
|
||||
|
||||
void _onSessionRestored(SessionRestored event, Emitter<SessionState> 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<void> _clearStoredSession() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_tokenKey);
|
||||
await prefs.remove(_expiryKey);
|
||||
}
|
||||
|
||||
static Future<void> 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());
|
||||
|
||||
@@ -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<Object> get props => [token, expiresAt];
|
||||
}
|
||||
|
||||
@@ -41,23 +41,37 @@ void main() {
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
class MainApp extends StatefulWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
State<MainApp> createState() => _MainAppState();
|
||||
}
|
||||
|
||||
class _MainAppState extends State<MainApp> {
|
||||
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<SessionBloc>(create: (_) => SessionBloc()),
|
||||
BlocProvider<AuthBloc>(
|
||||
create: (context) => AuthBloc(
|
||||
apiClient: ApiClient(context.read<SessionBloc>()),
|
||||
sessionBloc: context.read<SessionBloc>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<SessionBloc>.value(value: _sessionBloc),
|
||||
BlocProvider<AuthBloc>.value(value: _authBloc),
|
||||
BlocProvider<ActivityBloc>(
|
||||
create: (context) =>
|
||||
ActivityBloc(ActivityApi(ApiClient(context.read<SessionBloc>()))),
|
||||
ActivityBloc(ActivityApi(ApiClient(_sessionBloc))),
|
||||
),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
@@ -66,4 +80,11 @@ class MainApp extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_authBloc.close();
|
||||
_sessionBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user