Merge branch 'dev' into codex/local-dev-fixes
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import java.util.Base64
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
@@ -6,6 +8,18 @@ plugins {
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
val dartDefinesString = project.findProperty("dart-defines") as? String ?: ""
|
||||
val dartEnvironmentVariables = mutableMapOf<String, String>()
|
||||
dartDefinesString.split(",").forEach {
|
||||
if (it.isNotEmpty()) {
|
||||
val decoded = String(Base64.getDecoder().decode(it))
|
||||
val components = decoded.split("=")
|
||||
if (components.size == 2) {
|
||||
dartEnvironmentVariables[components[0]] = components[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.krowwithus.client"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -29,6 +43,8 @@ android {
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: ""
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${GOOGLE_MAPS_API_KEY}" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import GoogleMaps
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@@ -7,7 +8,31 @@ import UIKit
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
if let apiKey = getDartDefine(key: "GOOGLE_MAPS_API_KEY") {
|
||||
GMSServices.provideAPIKey(apiKey)
|
||||
}
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
private func getDartDefine(key: String) -> String? {
|
||||
guard let dartDefines = Bundle.main.infoDictionary?["DART_DEFINES"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let defines = dartDefines.components(separatedBy: ",")
|
||||
for define in defines {
|
||||
guard let decodedData = Data(base64Encoded: define),
|
||||
let decodedString = String(data: decodedData, encoding: .utf8) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let components = decodedString.components(separatedBy: "=")
|
||||
if components.count == 2 && components[0] == key {
|
||||
return components[1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Krow With Us Client</string>
|
||||
<string>Krow With Us Client</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Krow With Us Client</string>
|
||||
<string>Krow With Us Client</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@@ -45,5 +45,7 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>DART_DEFINES</key>
|
||||
<string>$(DART_DEFINES)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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.toClientGetStartedPage();
|
||||
} 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.toClientGetStartedPage();
|
||||
}
|
||||
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.toClientGetStartedPage();
|
||||
}
|
||||
|
||||
@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:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import java.util.Base64
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
@@ -6,6 +8,18 @@ plugins {
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
val dartDefinesString = project.findProperty("dart-defines") as? String ?: ""
|
||||
val dartEnvironmentVariables = mutableMapOf<String, String>()
|
||||
dartDefinesString.split(",").forEach {
|
||||
if (it.isNotEmpty()) {
|
||||
val decoded = String(Base64.getDecoder().decode(it))
|
||||
val components = decoded.split("=")
|
||||
if (components.size == 2) {
|
||||
dartEnvironmentVariables[components[0]] = components[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.krowwithus.staff"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -29,6 +43,8 @@ android {
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: ""
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${GOOGLE_MAPS_API_KEY}" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
||||
@@ -30,11 +30,21 @@ public final class GeneratedPluginRegistrant {
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.googlemaps.GoogleMapsPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin google_maps_flutter_android, io.flutter.plugins.googlemaps.GoogleMapsPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||
} catch (Exception e) {
|
||||
@@ -50,5 +60,10 @@ public final class GeneratedPluginRegistrant {
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import GoogleMaps
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@@ -7,7 +8,31 @@ import UIKit
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
if let apiKey = getDartDefine(key: "GOOGLE_MAPS_API_KEY") {
|
||||
GMSServices.provideAPIKey(apiKey)
|
||||
}
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
private func getDartDefine(key: String) -> String? {
|
||||
guard let dartDefines = Bundle.main.infoDictionary?["DART_DEFINES"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let defines = dartDefines.components(separatedBy: ",")
|
||||
for define in defines {
|
||||
guard let decodedData = Data(base64Encoded: define),
|
||||
let decodedString = String(data: decodedData, encoding: .utf8) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let components = decodedString.components(separatedBy: "=")
|
||||
if components.count == 2 && components[0] == key {
|
||||
return components[1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
@import geolocator_apple;
|
||||
#endif
|
||||
|
||||
#if __has_include(<google_maps_flutter_ios/FLTGoogleMapsPlugin.h>)
|
||||
#import <google_maps_flutter_ios/FLTGoogleMapsPlugin.h>
|
||||
#else
|
||||
@import google_maps_flutter_ios;
|
||||
#endif
|
||||
|
||||
#if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>)
|
||||
#import <permission_handler_apple/PermissionHandlerPlugin.h>
|
||||
#else
|
||||
@@ -42,6 +48,12 @@
|
||||
@import shared_preferences_foundation;
|
||||
#endif
|
||||
|
||||
#if __has_include(<url_launcher_ios/URLLauncherPlugin.h>)
|
||||
#import <url_launcher_ios/URLLauncherPlugin.h>
|
||||
#else
|
||||
@import url_launcher_ios;
|
||||
#endif
|
||||
|
||||
@implementation GeneratedPluginRegistrant
|
||||
|
||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||
@@ -49,8 +61,10 @@
|
||||
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
|
||||
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
||||
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
|
||||
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
|
||||
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
||||
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Krow With Us Staff</string>
|
||||
<string>Krow With Us Staff</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Krow With Us Staff</string>
|
||||
<string>Krow With Us Staff</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@@ -45,5 +45,7 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>DART_DEFINES</key>
|
||||
<string>$(DART_DEFINES)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -5,25 +5,36 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krowwithus_staff/firebase_options.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart'
|
||||
as staff_authentication;
|
||||
import 'package:staff_main/staff_main.dart' as staff_main;
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import 'src/widgets/session_listener.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
|
||||
// Register global BLoC observer for centralized error logging
|
||||
Bloc.observer = CoreBlocObserver(
|
||||
logEvents: true,
|
||||
logStateChanges: false, // Set to true for verbose debugging
|
||||
);
|
||||
|
||||
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
||||
|
||||
// Initialize session listener for Firebase Auth state changes
|
||||
DataConnectService.instance.initializeAuthListener(
|
||||
allowedRoles: <String>['STAFF', 'BOTH'], // Only allow users with STAFF or BOTH roles
|
||||
);
|
||||
|
||||
runApp(
|
||||
ModularApp(
|
||||
module: AppModule(),
|
||||
child: const SessionListener(child: AppWidget()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The main application module.
|
||||
@@ -34,7 +45,10 @@ class AppModule extends Module {
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
// Set the initial route to the authentication module
|
||||
r.module(StaffPaths.root, module: staff_authentication.StaffAuthenticationModule());
|
||||
r.module(
|
||||
StaffPaths.root,
|
||||
module: staff_authentication.StaffAuthenticationModule(),
|
||||
);
|
||||
|
||||
r.module(StaffPaths.main, module: staff_main.StaffMainModule());
|
||||
}
|
||||
|
||||
163
apps/mobile/apps/staff/lib/src/widgets/session_listener.dart
Normal file
163
apps/mobile/apps/staff/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.toStaffHome();
|
||||
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.toGetStartedPage();
|
||||
}
|
||||
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.toGetStartedPage();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sessionSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -10,6 +10,7 @@ import firebase_auth
|
||||
import firebase_core
|
||||
import geolocator_apple
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
|
||||
@@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ dependencies:
|
||||
path: ../../packages/features/staff/staff_main
|
||||
krow_core:
|
||||
path: ../../packages/core
|
||||
krow_data_connect:
|
||||
path: ../../packages/data_connect
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_modular: ^6.3.0
|
||||
firebase_core: ^4.4.0
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <geolocator_windows/geolocator_windows.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FirebaseAuthPluginCApiRegisterWithRegistrar(
|
||||
@@ -20,4 +21,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
firebase_core
|
||||
geolocator_windows
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user