refactor(logging): remove sensitive flag from logSafe calls across multiple controllers and services

This commit is contained in:
Vaibhav Surve 2025-07-09 11:35:35 +05:30
parent e059ee71f3
commit aac65104ab
17 changed files with 178 additions and 237 deletions

View File

@ -32,7 +32,7 @@ class ForgotPasswordController extends MyController {
final email = data['email']?.toString() ?? ''; final email = data['email']?.toString() ?? '';
try { try {
logSafe("Forgot password requested for: $email", sensitive: true); logSafe("Forgot password requested for: $email", );
final result = await AuthService.forgotPassword(email); final result = await AuthService.forgotPassword(email);
@ -50,7 +50,7 @@ class ForgotPasswordController extends MyController {
message: errorMessage, message: errorMessage,
type: SnackbarType.error, type: SnackbarType.error,
); );
logSafe("Failed to send reset password email for $email: $errorMessage", level: LogLevel.warning, sensitive: true); logSafe("Failed to send reset password email for $email: $errorMessage", level: LogLevel.warning, );
} }
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace); logSafe("Error during forgot password", level: LogLevel.error, error: e, stackTrace: stacktrace);

View File

@ -55,12 +55,12 @@ class LoginController extends MyController {
try { try {
final loginData = basicValidator.getData(); final loginData = basicValidator.getData();
logSafe("Attempting login for user: ${loginData['username']}", sensitive: true); logSafe("Attempting login for user: ${loginData['username']}", );
final errors = await AuthService.loginUser(loginData); final errors = await AuthService.loginUser(loginData);
if (errors != null) { if (errors != null) {
logSafe("Login failed for user: ${loginData['username']} with errors: $errors", level: LogLevel.warning, sensitive: true); logSafe("Login failed for user: ${loginData['username']} with errors: $errors", level: LogLevel.warning, );
showAppSnackbar( showAppSnackbar(
title: "Login Failed", title: "Login Failed",
@ -73,7 +73,7 @@ class LoginController extends MyController {
basicValidator.clearErrors(); basicValidator.clearErrors();
} else { } else {
await _handleRememberMe(); await _handleRememberMe();
logSafe("Login successful for user: ${loginData['username']}", sensitive: true); logSafe("Login successful for user: ${loginData['username']}", );
Get.toNamed('/home'); Get.toNamed('/home');
} }
} catch (e, stacktrace) { } catch (e, stacktrace) {

View File

@ -29,7 +29,7 @@ class MPINController extends GetxController {
} }
void onDigitChanged(String value, int index, {bool isRetype = false}) { void onDigitChanged(String value, int index, {bool isRetype = false}) {
logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype", sensitive: true); logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype", );
final nodes = isRetype ? retypeFocusNodes : focusNodes; final nodes = isRetype ? retypeFocusNodes : focusNodes;
if (value.isNotEmpty && index < 5) { if (value.isNotEmpty && index < 5) {
nodes[index + 1].requestFocus(); nodes[index + 1].requestFocus();
@ -47,7 +47,7 @@ class MPINController extends GetxController {
} }
final enteredMPIN = digitControllers.map((c) => c.text).join(); final enteredMPIN = digitControllers.map((c) => c.text).join();
logSafe("Entered MPIN: $enteredMPIN", sensitive: true); logSafe("Entered MPIN: $enteredMPIN", );
if (enteredMPIN.length < 6) { if (enteredMPIN.length < 6) {
_showError("Please enter all 6 digits."); _showError("Please enter all 6 digits.");
@ -56,7 +56,7 @@ class MPINController extends GetxController {
if (isNewUser.value) { if (isNewUser.value) {
final retypeMPIN = retypeControllers.map((c) => c.text).join(); final retypeMPIN = retypeControllers.map((c) => c.text).join();
logSafe("Retyped MPIN: $retypeMPIN", sensitive: true); logSafe("Retyped MPIN: $retypeMPIN", );
if (retypeMPIN.length < 6) { if (retypeMPIN.length < 6) {
_showError("Please enter all 6 digits in Retype MPIN."); _showError("Please enter all 6 digits in Retype MPIN.");
@ -177,7 +177,7 @@ class MPINController extends GetxController {
return false; return false;
} }
logSafe("Calling AuthService.generateMpin for employeeId: $employeeId", sensitive: true); logSafe("Calling AuthService.generateMpin for employeeId: $employeeId", );
final response = await AuthService.generateMpin( final response = await AuthService.generateMpin(
employeeId: employeeId, employeeId: employeeId,
@ -222,7 +222,7 @@ class MPINController extends GetxController {
logSafe("verifyMPIN triggered"); logSafe("verifyMPIN triggered");
final enteredMPIN = digitControllers.map((c) => c.text).join(); final enteredMPIN = digitControllers.map((c) => c.text).join();
logSafe("Entered MPIN: $enteredMPIN", sensitive: true); logSafe("Entered MPIN: $enteredMPIN", );
if (enteredMPIN.length < 6) { if (enteredMPIN.length < 6) {
_showError("Please enter all 6 digits."); _showError("Please enter all 6 digits.");

View File

@ -144,7 +144,7 @@ class OTPController extends GetxController {
Get.offAllNamed('/home'); Get.offAllNamed('/home');
} else { } else {
final error = result['error'] ?? "Failed to verify OTP"; final error = result['error'] ?? "Failed to verify OTP";
logSafe("[OTPController] OTP verification failed", level: LogLevel.warning, error: error, sensitive: true); logSafe("[OTPController] OTP verification failed", level: LogLevel.warning, error: error, );
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: error, message: error,

View File

@ -20,7 +20,7 @@ class DashboardController extends GetxController {
logSafe( logSafe(
'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}', 'DashboardController initialized with project ID: ${projectController.selectedProjectId.value}',
level: LogLevel.info, level: LogLevel.info,
sensitive: true,
); );
if (projectController.selectedProjectId.value.isNotEmpty) { if (projectController.selectedProjectId.value.isNotEmpty) {
@ -30,7 +30,7 @@ class DashboardController extends GetxController {
// React to project change // React to project change
ever<String>(projectController.selectedProjectId, (id) { ever<String>(projectController.selectedProjectId, (id) {
if (id.isNotEmpty) { if (id.isNotEmpty) {
logSafe('Project changed to $id, fetching attendance', level: LogLevel.info, sensitive: true); logSafe('Project changed to $id, fetching attendance', level: LogLevel.info, );
fetchRoleWiseAttendance(); fetchRoleWiseAttendance();
} }
}); });

View File

@ -106,15 +106,15 @@ class EmployeesScreenController extends GetxController {
logSafe( logSafe(
"Employees fetched: ${employees.length} for project $projectId", "Employees fetched: ${employees.length} for project $projectId",
level: LogLevel.info, level: LogLevel.info,
sensitive: true,
); );
}, },
onEmpty: () { onEmpty: () {
employees.clear(); employees.clear();
logSafe("No employees found for project $projectId.", level: LogLevel.warning, sensitive: true); logSafe("No employees found for project $projectId.", level: LogLevel.warning, );
}, },
onError: (e) { onError: (e) {
logSafe("Error fetching employees for project $projectId", level: LogLevel.error, error: e, sensitive: true); logSafe("Error fetching employees for project $projectId", level: LogLevel.error, error: e, );
}, },
); );
@ -131,15 +131,15 @@ class EmployeesScreenController extends GetxController {
() => ApiService.getEmployeeDetails(employeeId), () => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) { onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data); selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
logSafe("Employee details loaded for $employeeId", level: LogLevel.info, sensitive: true); logSafe("Employee details loaded for $employeeId", level: LogLevel.info, );
}, },
onEmpty: () { onEmpty: () {
selectedEmployeeDetails.value = null; selectedEmployeeDetails.value = null;
logSafe("No employee details found for $employeeId", level: LogLevel.warning, sensitive: true); logSafe("No employee details found for $employeeId", level: LogLevel.warning, );
}, },
onError: (e) { onError: (e) {
selectedEmployeeDetails.value = null; selectedEmployeeDetails.value = null;
logSafe("Error fetching employee details for $employeeId", level: LogLevel.error, error: e, sensitive: true); logSafe("Error fetching employee details for $employeeId", level: LogLevel.error, error: e, );
}, },
); );

View File

@ -76,7 +76,7 @@ class PermissionController extends GetxController {
employeeInfo.value = userData['employeeInfo']; employeeInfo.value = userData['employeeInfo'];
projectsInfo.assignAll(userData['projects']); projectsInfo.assignAll(userData['projects']);
logSafe("State updated with new user data.", sensitive: true); logSafe("State updated with new user data.", );
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error updating state", level: LogLevel.error, error: e, stackTrace: stacktrace); logSafe("Error updating state", level: LogLevel.error, error: e, stackTrace: stacktrace);
} }
@ -86,7 +86,7 @@ class PermissionController extends GetxController {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('jwt_token'); final token = prefs.getString('jwt_token');
logSafe("Auth token retrieved successfully.", sensitive: true); logSafe("Auth token retrieved successfully.", );
return token; return token;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error retrieving auth token", level: LogLevel.error, error: e, stackTrace: stacktrace); logSafe("Error retrieving auth token", level: LogLevel.error, error: e, stackTrace: stacktrace);

View File

@ -147,6 +147,6 @@ class AddTaskController extends GetxController {
void selectCategory(String id) { void selectCategory(String id) {
selectedCategoryId.value = id; selectedCategoryId.value = id;
selectedCategoryName.value = categoryIdNameMap[id]; selectedCategoryName.value = categoryIdNameMap[id];
logSafe("Category selected", level: LogLevel.debug, sensitive: true); logSafe("Category selected", level: LogLevel.debug, );
} }
} }

View File

@ -50,12 +50,12 @@ class DailyTaskPlaningController extends GetxController {
.where((e) => uploadingStates[e.id]?.value == true) .where((e) => uploadingStates[e.id]?.value == true)
.toList(); .toList();
selectedEmployees.value = selected; selectedEmployees.value = selected;
logSafe("Updated selected employees", level: LogLevel.debug, sensitive: true); logSafe("Updated selected employees", level: LogLevel.debug, );
} }
void onRoleSelected(String? roleId) { void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId; selectedRoleId.value = roleId;
logSafe("Role selected", level: LogLevel.info, sensitive: true); logSafe("Role selected", level: LogLevel.info, );
} }
Future<void> fetchRoles() async { Future<void> fetchRoles() async {
@ -137,7 +137,7 @@ class DailyTaskPlaningController extends GetxController {
final data = response?['data']; final data = response?['data'];
if (data != null) { if (data != null) {
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)]; dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
logSafe("Daily task Planning Details fetched", level: LogLevel.info, sensitive: true); logSafe("Daily task Planning Details fetched", level: LogLevel.info, );
} else { } else {
logSafe("Data field is null", level: LogLevel.warning); logSafe("Data field is null", level: LogLevel.warning);
} }
@ -164,14 +164,14 @@ class DailyTaskPlaningController extends GetxController {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
logSafe("Employees fetched: ${employees.length} for project $projectId", logSafe("Employees fetched: ${employees.length} for project $projectId",
level: LogLevel.info, sensitive: true); level: LogLevel.info, );
} else { } else {
employees = []; employees = [];
logSafe("No employees found for project $projectId", level: LogLevel.warning, sensitive: true); logSafe("No employees found for project $projectId", level: LogLevel.warning, );
} }
} catch (e, stack) { } catch (e, stack) {
logSafe("Error fetching employees for project $projectId", logSafe("Error fetching employees for project $projectId",
level: LogLevel.error, error: e, stackTrace: stack, sensitive: true); level: LogLevel.error, error: e, stackTrace: stack, );
} finally { } finally {
isLoading.value = false; isLoading.value = false;
update(); update();

View File

@ -272,18 +272,18 @@ class ReportTaskActionController extends MyController {
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75); final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
if (pickedFile != null) { if (pickedFile != null) {
selectedImages.add(File(pickedFile.path)); selectedImages.add(File(pickedFile.path));
logSafe("Image added from camera: ${pickedFile.path}", sensitive: true); logSafe("Image added from camera: ${pickedFile.path}", );
} }
} else { } else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75); final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path))); selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
logSafe("${pickedFiles.length} images added from gallery.", sensitive: true); logSafe("${pickedFiles.length} images added from gallery.", );
} }
} }
void removeImageAt(int index) { void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) { if (index >= 0 && index < selectedImages.length) {
logSafe("Removing image at index $index", sensitive: true); logSafe("Removing image at index $index", );
selectedImages.removeAt(index); selectedImages.removeAt(index);
} }
} }

View File

@ -83,7 +83,7 @@ class ReportTaskController extends MyController {
required DateTime reportedDate, required DateTime reportedDate,
List<File>? images, List<File>? images,
}) async { }) async {
logSafe("Reporting task for projectId", sensitive: true); logSafe("Reporting task for projectId", );
final completedWork = completedWorkController.text.trim(); final completedWork = completedWorkController.text.trim();
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) { if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
_showError("Completed work must be a positive number."); _showError("Completed work must be a positive number.");
@ -138,7 +138,7 @@ class ReportTaskController extends MyController {
required String comment, required String comment,
List<File>? images, List<File>? images,
}) async { }) async {
logSafe("Submitting comment for project", sensitive: true); logSafe("Submitting comment for project", );
final commentField = commentController.text.trim(); final commentField = commentController.text.trim();
if (commentField.isEmpty) { if (commentField.isEmpty) {
@ -221,7 +221,7 @@ class ReportTaskController extends MyController {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75); final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path))); selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
} }
logSafe("Images picked: ${selectedImages.length}", sensitive: true); logSafe("Images picked: ${selectedImages.length}", );
} catch (e) { } catch (e) {
logSafe("Error picking images", level: LogLevel.warning, error: e); logSafe("Error picking images", level: LogLevel.warning, error: e);
} }

View File

@ -163,7 +163,7 @@ class ApiService {
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body", logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
sensitive: true); );
try { try {
final response = await http final response = await http
@ -203,7 +203,7 @@ class ApiService {
if (additionalHeaders != null) ...additionalHeaders, if (additionalHeaders != null) ...additionalHeaders,
}; };
logSafe("PUT $uri\nHeaders: $headers\nBody: $body", sensitive: true); logSafe("PUT $uri\nHeaders: $headers\nBody: $body", );
try { try {
final response = await http final response = await http

View File

@ -19,10 +19,10 @@ class PermissionService {
String token, { String token, {
bool hasRetried = false, bool hasRetried = false,
}) async { }) async {
logSafe("Fetching user data...", sensitive: true); logSafe("Fetching user data...", );
if (_userDataCache.containsKey(token)) { if (_userDataCache.containsKey(token)) {
logSafe("User data cache hit.", sensitive: true); logSafe("User data cache hit.", );
return _userDataCache[token]!; return _userDataCache[token]!;
} }

View File

@ -6,7 +6,7 @@ import 'package:marco/helpers/services/app_logger.dart';
class LauncherUtils { class LauncherUtils {
/// Launches the phone dialer with the provided phone number /// Launches the phone dialer with the provided phone number
static Future<void> launchPhone(String phoneNumber) async { static Future<void> launchPhone(String phoneNumber) async {
logSafe('Attempting to launch phone: $phoneNumber', sensitive: true); logSafe('Attempting to launch phone: $phoneNumber', );
final Uri url = Uri(scheme: 'tel', path: phoneNumber); final Uri url = Uri(scheme: 'tel', path: phoneNumber);
await _tryLaunch(url, 'Could not launch phone'); await _tryLaunch(url, 'Could not launch phone');
@ -14,7 +14,7 @@ class LauncherUtils {
/// Launches the email app with the provided email address /// Launches the email app with the provided email address
static Future<void> launchEmail(String email) async { static Future<void> launchEmail(String email) async {
logSafe('Attempting to launch email: $email', sensitive: true); logSafe('Attempting to launch email: $email', );
final Uri url = Uri(scheme: 'mailto', path: email); final Uri url = Uri(scheme: 'mailto', path: email);
await _tryLaunch(url, 'Could not launch email'); await _tryLaunch(url, 'Could not launch email');
@ -22,17 +22,17 @@ class LauncherUtils {
/// Launches WhatsApp with the provided phone number /// Launches WhatsApp with the provided phone number
static Future<void> launchWhatsApp(String phoneNumber) async { static Future<void> launchWhatsApp(String phoneNumber) async {
logSafe('Attempting to launch WhatsApp with: $phoneNumber', sensitive: true); logSafe('Attempting to launch WhatsApp with: $phoneNumber', );
String normalized = phoneNumber.replaceAll(RegExp(r'\D'), ''); String normalized = phoneNumber.replaceAll(RegExp(r'\D'), '');
if (!normalized.startsWith('91')) { if (!normalized.startsWith('91')) {
normalized = '91$normalized'; normalized = '91$normalized';
} }
logSafe('Normalized WhatsApp number: $normalized', sensitive: true); logSafe('Normalized WhatsApp number: $normalized', );
if (normalized.length < 12) { if (normalized.length < 12) {
logSafe('Invalid WhatsApp number: $normalized', sensitive: true); logSafe('Invalid WhatsApp number: $normalized', );
showAppSnackbar( showAppSnackbar(
title: 'Error', title: 'Error',
message: 'Invalid phone number for WhatsApp', message: 'Invalid phone number for WhatsApp',
@ -62,7 +62,7 @@ class LauncherUtils {
'Failed to copy $typeLabel to clipboard: $e', 'Failed to copy $typeLabel to clipboard: $e',
stackTrace: st, stackTrace: st,
level: LogLevel.error, level: LogLevel.error,
sensitive: true,
); );
showAppSnackbar( showAppSnackbar(
title: 'Error', title: 'Error',

View File

@ -14,7 +14,7 @@ Future<Uint8List?> compressImageToUnder100KB(File file) async {
const int maxWidth = 800; const int maxWidth = 800;
const int maxHeight = 800; const int maxHeight = 800;
logSafe("Starting image compression...", sensitive: true); logSafe("Starting image compression...", );
while (quality >= 10) { while (quality >= 10) {
try { try {
@ -59,7 +59,7 @@ Future<File> saveCompressedImageToFile(Uint8List bytes) async {
final file = File(filePath); final file = File(filePath);
final savedFile = await file.writeAsBytes(bytes); final savedFile = await file.writeAsBytes(bytes);
logSafe("Compressed image saved to ${savedFile.path}", sensitive: true); logSafe("Compressed image saved to ${savedFile.path}", );
return savedFile; return savedFile;
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe("Error saving compressed image", level: LogLevel.error, error: e, stackTrace: stacktrace); logSafe("Error saving compressed image", level: LogLevel.error, error: e, stackTrace: stacktrace);

View File

@ -1,225 +1,166 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/images.dart'; import 'package:marco/images.dart';
import 'package:marco/view/auth/email_login_form.dart'; import 'package:marco/view/auth/email_login_form.dart';
import 'package:marco/view/auth/otp_login_form.dart'; import 'package:marco/view/auth/otp_login_form.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/widgets/my_text.dart'; // Make sure this import is added
enum LoginOption { email, otp } enum LoginOption { email, otp }
class LoginOptionScreen extends StatefulWidget { class LoginOptionScreen extends StatelessWidget {
const LoginOptionScreen({super.key}); const LoginOptionScreen({super.key});
@override @override
State<LoginOptionScreen> createState() => _LoginOptionScreenState(); Widget build(BuildContext context) => const WelcomeScreen();
} }
class _LoginOptionScreenState extends State<LoginOptionScreen> with UIMixin { class WelcomeScreen extends StatelessWidget {
LoginOption _selectedOption = LoginOption.email; const WelcomeScreen({super.key});
bool get _isBetaEnvironment => ApiEndpoints.baseUrl.contains("stage"); bool get _isBetaEnvironment => ApiEndpoints.baseUrl.contains("stage");
void _showLoginDialog(BuildContext context, LoginOption option) {
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
insetPadding: const EdgeInsets.all(24),
child: Padding(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
MyText(
option == LoginOption.email ? "Login with Email" : "Login with OTP",
fontSize: 20,
fontWeight: 700,
),
const SizedBox(height: 20),
option == LoginOption.email ? EmailLoginForm() : const OTPLoginScreen(),
],
),
),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Scaffold( return Scaffold(
backgroundColor: contentTheme.brandRed, backgroundColor: const Color(0xFFB71C1C),
body: SafeArea( body: SafeArea(
child: LayoutBuilder( child: Center(
builder: (context, constraints) { child: SingleChildScrollView(
return Column( padding: const EdgeInsets.all(24),
children: [ child: ConstrainedBox(
const SizedBox(height: 24), constraints: BoxConstraints(maxWidth: screenWidth < 500 ? double.infinity : 420),
_buildHeader(), child: Column(
const SizedBox(height: 16), crossAxisAlignment: CrossAxisAlignment.center,
_buildWelcomeTextsAndChips(), children: [
const SizedBox(height: 16), // App Logo
Expanded( Container(
child: Container( padding: const EdgeInsets.all(16),
width: double.infinity, decoration: BoxDecoration(
decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: borderRadius: BorderRadius.circular(20),
BorderRadius.vertical(top: Radius.circular(32)), boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 8)],
), ),
child: SingleChildScrollView( child: Image.asset(Images.logoDark, height: 60),
padding: const EdgeInsets.symmetric( ),
horizontal: 20, vertical: 24),
child: ConstrainedBox( const SizedBox(height: 24),
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 200, // Welcome Text
), MyText(
child: IntrinsicHeight( "Welcome to Marco",
child: Column( fontSize: 24,
crossAxisAlignment: CrossAxisAlignment.start, fontWeight: 800,
children: [ color: Colors.white,
_buildLoginForm(), textAlign: TextAlign.center,
const SizedBox(height: 24), ),
const SizedBox(height: 8), const SizedBox(height: 10),
Center(child: _buildVersionInfo()), MyText(
], "Streamline Project Management\nBoost Productivity with Automation.",
), fontSize: 14,
), color: Colors.white70,
textAlign: TextAlign.center,
),
if (_isBetaEnvironment) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(6),
),
child: MyText(
'BETA',
color: Colors.white,
fontWeight: 600,
fontSize: 12,
), ),
), ),
],
const SizedBox(height: 36),
// Login Buttons
_buildLoginButton(
context,
label: "Login with Username",
icon: LucideIcons.mail,
option: LoginOption.email,
), ),
), const SizedBox(height: 16),
], _buildLoginButton(
); context,
}, label: "Login with OTP",
icon: LucideIcons.message_square,
option: LoginOption.otp,
),
const SizedBox(height: 36),
// Version Info
MyText(
'App version 1.0.0',
color: Colors.white60,
fontSize: 12,
),
],
),
),
),
), ),
), ),
); );
} }
Widget _buildHeader() { Widget _buildLoginButton(BuildContext context,
return Container( {required String label, required IconData icon, required LoginOption option}) {
padding: const EdgeInsets.all(12), return SizedBox(
decoration: BoxDecoration( width: double.infinity,
color: Colors.white, child: ElevatedButton.icon(
borderRadius: BorderRadius.circular(16), icon: Icon(icon, size: 20, color: Colors.white),
boxShadow: [ label: Padding(
BoxShadow( padding: const EdgeInsets.symmetric(vertical: 12),
color: Colors.black12, child: MyText(label, fontSize: 16, fontWeight: 600, color: Colors.white),
blurRadius: 6,
offset: Offset(0, 3),
)
],
),
child: Image.asset(Images.logoDark, height: 70),
);
}
Widget _buildBetaLabel() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(4),
),
child: MyText(
'BETA',
color: Colors.white,
fontWeight: 600,
fontSize: 12,
),
);
}
Widget _buildLoginOptionChips() {
return Wrap(
spacing: 12,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
_buildOptionChip(
title: "User Name",
icon: LucideIcons.mail,
value: LoginOption.email,
), ),
_buildOptionChip( style: ElevatedButton.styleFrom(
title: "OTP", backgroundColor: const Color(0xFFB71C1C),
icon: LucideIcons.message_square, side: const BorderSide(color: Colors.white70),
value: LoginOption.otp, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 4,
), ),
], onPressed: () => _showLoginDialog(context, option),
);
}
Widget _buildWelcomeTextsAndChips() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
MyText(
"Welcome to Marco",
fontSize: 20,
fontWeight: 700,
color: Colors.white,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
MyText(
"Streamline Project Management and Boost Productivity with Automation.",
fontSize: 14,
color: Colors.white70,
textAlign: TextAlign.center,
),
if (_isBetaEnvironment) ...[
const SizedBox(height: 8),
_buildBetaLabel(),
],
const SizedBox(height: 20),
_buildLoginOptionChips(),
],
), ),
); );
} }
}
Widget _buildOptionChip({
required String title,
required IconData icon,
required LoginOption value,
}) {
final bool isSelected = _selectedOption == value;
final Color selectedTextColor = contentTheme.brandRed;
final Color unselectedTextColor = Colors.white;
final Color selectedBgColor = Colors.grey[100]!;
final Color unselectedBgColor = contentTheme.brandRed;
return ChoiceChip(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 18,
color: isSelected ? selectedTextColor : unselectedTextColor,
),
const SizedBox(width: 6),
MyText(
title,
fontSize: 14,
fontWeight: 600,
color: isSelected ? selectedTextColor : unselectedTextColor,
),
],
),
selected: isSelected,
onSelected: (_) => setState(() => _selectedOption = value),
selectedColor: selectedBgColor,
backgroundColor: unselectedBgColor,
side: BorderSide(
color: Colors.white.withOpacity(0.6),
width: 1.2,
),
elevation: 3,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
);
}
Widget _buildLoginForm() {
switch (_selectedOption) {
case LoginOption.email:
return EmailLoginForm();
case LoginOption.otp:
return const OTPLoginScreen();
}
}
Widget _buildVersionInfo() {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MyText(
'App version 1.0.0',
color: Colors.grey.shade500,
fontSize: 12,
),
);
}
}

View File

@ -25,7 +25,7 @@ class MyApp extends StatelessWidget {
} }
final bool hasMpin = LocalStorage.getIsMpin(); final bool hasMpin = LocalStorage.getIsMpin();
logSafe("MPIN enabled: $hasMpin", sensitive: true); logSafe("MPIN enabled: $hasMpin", );
if (hasMpin) { if (hasMpin) {
await LocalStorage.setBool("mpin_verified", false); await LocalStorage.setBool("mpin_verified", false);