feat: Implement session management with SessionListener and integrate krow_data_connect
This commit is contained in:
@@ -14,8 +14,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
|
||||
import 'firebase_options.dart';
|
||||
import 'src/widgets/session_listener.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -28,8 +30,18 @@ void main() async {
|
||||
logEvents: true,
|
||||
logStateChanges: false, // Set to true for verbose debugging
|
||||
);
|
||||
|
||||
// Initialize session listener for Firebase Auth state changes
|
||||
DataConnectService.instance.initializeAuthListener(
|
||||
allowedRoles: <String>['CLIENT', 'BUSINESS', 'BOTH'], // Only allow users with CLIENT, BUSINESS, or BOTH roles
|
||||
);
|
||||
|
||||
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
||||
runApp(
|
||||
ModularApp(
|
||||
module: AppModule(),
|
||||
child: const SessionListener(child: AppWidget()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The main application module for the Client app.
|
||||
|
||||
163
apps/mobile/apps/client/lib/src/widgets/session_listener.dart
Normal file
163
apps/mobile/apps/client/lib/src/widgets/session_listener.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
|
||||
/// A widget that listens to session state changes and handles global reactions.
|
||||
///
|
||||
/// This widget wraps the entire app and provides centralized session management,
|
||||
/// such as logging out when the session expires or handling session errors.
|
||||
class SessionListener extends StatefulWidget {
|
||||
/// Creates a [SessionListener].
|
||||
const SessionListener({required this.child, super.key});
|
||||
|
||||
/// The child widget to wrap.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<SessionListener> createState() => _SessionListenerState();
|
||||
}
|
||||
|
||||
class _SessionListenerState extends State<SessionListener> {
|
||||
late StreamSubscription<SessionState> _sessionSubscription;
|
||||
bool _sessionExpiredDialogShown = false;
|
||||
bool _isInitialState = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupSessionListener();
|
||||
}
|
||||
|
||||
void _setupSessionListener() {
|
||||
_sessionSubscription = DataConnectService.instance.onSessionStateChanged
|
||||
.listen((SessionState state) {
|
||||
_handleSessionChange(state);
|
||||
});
|
||||
|
||||
debugPrint('[SessionListener] Initialized session listener');
|
||||
}
|
||||
|
||||
void _handleSessionChange(SessionState state) {
|
||||
if (!mounted) return;
|
||||
|
||||
switch (state.type) {
|
||||
case SessionStateType.unauthenticated:
|
||||
debugPrint(
|
||||
'[SessionListener] Unauthenticated: Session expired or user logged out',
|
||||
);
|
||||
// On initial state (cold start), just proceed to login without dialog
|
||||
// Only show dialog if user was previously authenticated (session expired)
|
||||
if (_isInitialState) {
|
||||
_isInitialState = false;
|
||||
Modular.to.toGetStartedPage();
|
||||
} else if (!_sessionExpiredDialogShown) {
|
||||
_sessionExpiredDialogShown = true;
|
||||
_showSessionExpiredDialog();
|
||||
}
|
||||
break;
|
||||
|
||||
case SessionStateType.authenticated:
|
||||
// Session restored or user authenticated
|
||||
_isInitialState = false;
|
||||
_sessionExpiredDialogShown = false;
|
||||
debugPrint('[SessionListener] Authenticated: ${state.userId}');
|
||||
|
||||
// Navigate to the main app
|
||||
Modular.to.toClientHome();
|
||||
break;
|
||||
|
||||
case SessionStateType.error:
|
||||
// Show error notification with option to retry or logout
|
||||
// Only show if not initial state (avoid showing on cold start)
|
||||
if (!_isInitialState) {
|
||||
debugPrint('[SessionListener] Session error: ${state.errorMessage}');
|
||||
_showSessionErrorDialog(state.errorMessage ?? 'Session error occurred');
|
||||
} else {
|
||||
_isInitialState = false;
|
||||
Modular.to.toInitialPage();
|
||||
}
|
||||
break;
|
||||
|
||||
case SessionStateType.loading:
|
||||
// Session is loading, optionally show a loading indicator
|
||||
debugPrint('[SessionListener] Session loading...');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a dialog when the session expires.
|
||||
void _showSessionExpiredDialog() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Session Expired'),
|
||||
content: const Text(
|
||||
'Your session has expired. Please log in again to continue.',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_proceedToLogin();
|
||||
},
|
||||
child: const Text('Log In'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a dialog when a session error occurs, with retry option.
|
||||
void _showSessionErrorDialog(String errorMessage) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Session Error'),
|
||||
content: Text(errorMessage),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// User can retry by dismissing and continuing
|
||||
Modular.to.pop();
|
||||
},
|
||||
child: const Text('Continue'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_proceedToLogin();
|
||||
},
|
||||
child: const Text('Log Out'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Navigate to login screen and clear navigation stack.
|
||||
void _proceedToLogin() {
|
||||
// Clear service caches on sign-out
|
||||
DataConnectService.instance.handleSignOut();
|
||||
|
||||
// Navigate to authentication
|
||||
Modular.to.toInitialPage();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sessionSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
@@ -41,6 +41,7 @@ dependencies:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
firebase_core: ^4.4.0
|
||||
krow_data_connect: ^0.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user