Merge branch 'dev' into codex/local-dev-fixes

This commit is contained in:
Achintha Isuru
2026-02-17 16:34:58 -05:00
committed by GitHub
167 changed files with 5918 additions and 4286 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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.

View 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;
}

View File

@@ -41,6 +41,7 @@ dependencies:
flutter_localizations:
sdk: flutter
firebase_core: ^4.4.0
krow_data_connect: ^0.0.1
dev_dependencies:
flutter_test: