Enhance authentication flow and user role management

- Updated AuthViewModel to hydrate user data from storage on initialization.
- Added method to AuthRepository for retrieving stored customer data.
- Enhanced AuthService to save and retrieve customer data from SharedPreferences.
- Refactored UI components to use context.watch for user role updates in MeetingTimeDialog, TenderDetailActions, and TendersPage.
- Improved navigation logic in final completion pages to handle user interactions more effectively.
This commit is contained in:
amirrezaghabeli
2025-10-14 12:24:04 +03:30
parent a343cf2469
commit 38222afcfa
9 changed files with 80 additions and 8 deletions
+7 -2
View File
@@ -157,11 +157,16 @@ List<SingleChildWidget> get services {
List<SingleChildWidget> get authProvider { List<SingleChildWidget> get authProvider {
return [ return [
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => AuthViewModel(authRepository: context.read()), create: (context) {
final vm = AuthViewModel(authRepository: context.read());
// Hydrate stored user/customer so userRole is available on hot reloads
vm.hydrateFromStorage();
return vm;
},
lazy: false, // Load immediately for auth state checks lazy: false, // Load immediately for auth state checks
), ),
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => FinalCompletionOfDocumentsViewModel(), create: (context) => FinalCompletionOfDocumentsViewModel(),
lazy: false, // Load immediately for auth state checks lazy: false, // Load immediately for auth state checks
), ),
@@ -1,3 +1,4 @@
import 'package:tm_app/data/services/model/customer/customer.dart';
import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart'; import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart';
import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; import 'package:tm_app/data/services/model/login_response/login_response_model.dart';
import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart'; import 'package:tm_app/data/services/model/verify_otp_response/verify_otp_response_model.dart';
@@ -51,4 +52,8 @@ class AuthRepository {
Future<Result<bool>> checkIsLoggedIn() async { Future<Result<bool>> checkIsLoggedIn() async {
return _authService.checkIsLoggedIn(); return _authService.checkIsLoggedIn();
} }
Future<Customer?> getStoredCustomer() async {
return _authService.getStoredCustomer();
}
} }
+32 -1
View File
@@ -6,6 +6,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/utils/logger.dart'; import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/data/services/api/auth_api.dart'; import 'package:tm_app/data/services/api/auth_api.dart';
import 'package:tm_app/data/services/model/customer/customer.dart';
import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart'; import 'package:tm_app/data/services/model/forgot_password_response/forgot_password_response_model.dart';
import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; import 'package:tm_app/data/services/model/login_response/login_response_model.dart';
import 'package:tm_app/data/services/model/logout_response/logout_response.dart'; import 'package:tm_app/data/services/model/logout_response/logout_response.dart';
@@ -55,9 +56,17 @@ class AuthService {
final token = result.value.data.accessToken; final token = result.value.data.accessToken;
AppLogger().info('🔑 Bearer token: $token'); AppLogger().info('🔑 Bearer token: $token');
final refreshToken = result.value.data.refreshToken; final refreshToken = result.value.data.refreshToken;
final customer = result.value.data.customer;
await prefs.setString('bearer', token!); await prefs.setString('bearer', token!);
await prefs.setString('refresh_token', refreshToken!); await prefs.setString('refresh_token', refreshToken!);
// Save customer data as JSON string
if (customer != null) {
final customerJson = jsonEncode(customer.toJson());
await prefs.setString('customer_data', customerJson);
AppLogger().info('👤 Customer data saved to preferences');
}
} }
return result; return result;
} }
@@ -68,6 +77,12 @@ class AuthService {
method: 'DELETE', method: 'DELETE',
(json) => LogoutResponse.fromJson(json), (json) => LogoutResponse.fromJson(json),
); );
// Clear local data after successful logout
if (result is Ok<LogoutResponse>) {
await localLogout();
}
return result; return result;
} }
@@ -89,7 +104,8 @@ class AuthService {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove('bearer'); await prefs.remove('bearer');
await prefs.remove('refresh_token'); await prefs.remove('refresh_token');
appLogger.info('tokens cleared!'); await prefs.remove('customer_data');
appLogger.info('tokens and customer data cleared!');
} }
Future<Result<ForgotPasswordResponseModel>> forgotPassword({ Future<Result<ForgotPasswordResponseModel>> forgotPassword({
@@ -131,4 +147,19 @@ class AuthService {
} }
return const Result.ok(true); return const Result.ok(true);
} }
Future<Customer?> getStoredCustomer() async {
final prefs = await SharedPreferences.getInstance();
final customerJson = prefs.getString('customer_data');
if (customerJson == null) {
return null;
}
try {
final customerMap = jsonDecode(customerJson) as Map<String, dynamic>;
return Customer.fromJson(customerMap);
} catch (e) {
AppLogger().error('❌ Failed to parse stored customer data: $e');
return null;
}
}
} }
+25
View File
@@ -46,6 +46,19 @@ class AuthViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> hydrateFromStorage() async {
try {
final customer = await _authRepository.getStoredCustomer();
if (customer != null) {
_loggedInUser = customer;
_userRole = customer.role == 'admin' ? Role.admin : Role.analyst;
notifyListeners();
}
} catch (e) {
AppLogger().error('❌ Failed to hydrate auth state: $e');
}
}
void _onTextChanged() { void _onTextChanged() {
notifyListeners(); notifyListeners();
} }
@@ -97,11 +110,22 @@ class AuthViewModel with ChangeNotifier {
if (result is Ok<bool>) { if (result is Ok<bool>) {
if (result.value) { if (result.value) {
_isLoggedIn = result.value; _isLoggedIn = result.value;
// Restore customer data from SharedPreferences
final customer = await _authRepository.getStoredCustomer();
if (customer != null) {
_loggedInUser = customer;
_userRole = customer.role == 'admin' ? Role.admin : Role.analyst;
}
} else { } else {
_isLoggedIn = false; _isLoggedIn = false;
_loggedInUser = null;
_userRole = null;
} }
} else { } else {
_isLoggedIn = false; _isLoggedIn = false;
_loggedInUser = null;
_userRole = null;
} }
notifyListeners(); notifyListeners();
} }
@@ -113,6 +137,7 @@ class AuthViewModel with ChangeNotifier {
passwordController.clear(); passwordController.clear();
_isLoggedIn = false; _isLoggedIn = false;
_loggedInUser = null; _loggedInUser = null;
_userRole = null;
notifyListeners(); notifyListeners();
} }
@@ -29,7 +29,7 @@ class _MeetingTimeDialogState extends State<MeetingTimeDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userRole = context.read<AuthViewModel>().userRole; final userRole = context.watch<AuthViewModel>().userRole;
return Dialog( return Dialog(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@@ -35,7 +35,7 @@ class TenderDetailActions extends StatefulWidget {
class _TenderDetailActionsState extends State<TenderDetailActions> { class _TenderDetailActionsState extends State<TenderDetailActions> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userRole = context.read<AuthViewModel>().userRole; final userRole = context.watch<AuthViewModel>().userRole;
return widget.isScreenBig return widget.isScreenBig
? _webActions(userRole!) ? _webActions(userRole!)
: Column( : Column(
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
@@ -50,7 +51,9 @@ class DesktopFinalCompletionOfDocumentsPage extends StatelessWidget {
children: [ children: [
BaseButton( BaseButton(
isEnabled: true, isEnabled: true,
onPressed: () {}, onPressed: () {
context.pop();
},
width: 176, width: 176,
backgroundColor: AppColors.primary10, backgroundColor: AppColors.primary10,
textColor: AppColors.mainBlue, textColor: AppColors.mainBlue,
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
@@ -61,7 +62,9 @@ class TabletFinalCompletionOfDocumentsPage extends StatelessWidget {
children: [ children: [
BaseButton( BaseButton(
isEnabled: true, isEnabled: true,
onPressed: () {}, onPressed: () {
context.pop();
},
width: 176.0.w(), width: 176.0.w(),
backgroundColor: AppColors.primary10, backgroundColor: AppColors.primary10,
textColor: AppColors.mainBlue, textColor: AppColors.mainBlue,
+1 -1
View File
@@ -50,7 +50,7 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userRole = context.read<AuthViewModel>().userRole; final userRole = context.watch<AuthViewModel>().userRole;
return Scaffold( return Scaffold(
backgroundColor: AppColors.backgroundColor, backgroundColor: AppColors.backgroundColor,
body: SafeArea( body: SafeArea(