diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 16503a2..3936013 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,11 +4,15 @@ PODS: - Flutter (1.0.0) - flutter_native_splash (2.4.3): - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS DEPENDENCIES: - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) EXTERNAL SOURCES: camera_avfoundation: @@ -17,11 +21,14 @@ EXTERNAL SOURCES: :path: Flutter flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9bcf7d3..77e8aff 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -161,7 +161,6 @@ C9C24CB30CC2EF64A5028A78 /* Pods-RunnerTests.release.xcconfig */, C8EAEACEAC43BB801D9EFF4B /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -471,6 +470,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 6YV3J6426X; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -653,6 +653,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 6YV3J6426X; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -675,6 +676,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 6YV3J6426X; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/lib/LOGIN_SETUP.md b/lib/LOGIN_SETUP.md new file mode 100644 index 0000000..7963905 --- /dev/null +++ b/lib/LOGIN_SETUP.md @@ -0,0 +1,113 @@ +# إعداد تسجيل الدخول (Login Setup) + +تم ربط عملية تسجيل الدخول مع السيرفر بنجاح. + +## الملفات المُنشأة + +### 1. Data Layer +- `data/dto/login_dto.dart` - DTO لإرسال بيانات تسجيل الدخول +- `data/dto/login_response_dto.dart` - DTO لاستقبال استجابة تسجيل الدخول +- `data/datasources/auth_remote_data_source.dart` - مصدر البيانات البعيدة لتسجيل الدخول + +### 2. Domain Layer +- `domain/models/login_request.dart` - نموذج طلب تسجيل الدخول +- `domain/models/login_response_model.dart` - نموذج استجابة تسجيل الدخول +- `domain/repositories/auth_repository.dart` - واجهة Repository +- `domain/usecases/login_usecase.dart` - Use Case لتسجيل الدخول + +### 3. Data Implementation +- `data/repositories/auth_repository_impl.dart` - تطبيق Repository + +### 4. Core Updates +- `core/network/api_client.dart` - تم تحديثه لإضافة token تلقائياً في الطلبات +- `core/di/injection_container.dart` - تم تسجيل جميع التبعيات + +### 5. UI Updates +- `widgets/auth_form.dart` - تم تحديثه لاستخدام LoginUseCase + +## كيفية الاستخدام + +### في الكود: +```dart +// الحصول على LoginUseCase من dependency injection +final loginUseCase = sl(); + +// إنشاء طلب تسجيل الدخول +final request = LoginRequest( + phoneNumber: '7856121557', + password: 'qaqaqa', +); + +// استدعاء UseCase +final result = await loginUseCase(request); + +// التعامل مع النتيجة +result.fold( + (failure) { + // معالجة الخطأ + print('خطأ: ${failure.message}'); + }, + (response) { + // معالجة النجاح + if (response.isSuccess) { + print('تم تسجيل الدخول بنجاح'); + print('Token: ${response.data?.token}'); + print('اسم المستخدم: ${response.data?.fullName}'); + } + }, +); +``` + +## API Endpoint + +- **URL**: `https://hrm.go.iq/api/Auth/login` +- **Method**: POST +- **Headers**: + - `Content-Type: application/json` + - `accept: text/plain` + +### Request Body: +```json +{ + "phoneNumber": "7856121557", + "password": "qaqaqa" +} +``` + +### Response: +```json +{ + "statusCode": 200, + "isSuccess": true, + "message": "Login Successful", + "data": { + "token": "...", + "id": "...", + "username": "...", + "fullName": "...", + "role": "...", + "email": "...", + "phoneNumber": "...", + "permissions": [...] + } +} +``` + +## المميزات + +1. ✅ حفظ Token تلقائياً في SharedPreferences +2. ✅ إضافة Token تلقائياً في جميع الطلبات عبر ApiClient interceptor +3. ✅ معالجة الأخطاء بشكل شامل (Network, Server, Validation) +4. ✅ رسائل خطأ بالعربية +5. ✅ Loading state في واجهة المستخدم +6. ✅ التحقق من صحة المدخلات + +## الخطوات التالية + +1. قم بتشغيل `flutter pub get` لتثبيت الحزم +2. اختبر تسجيل الدخول باستخدام البيانات الصحيحة +3. يمكنك إضافة المزيد من الميزات مثل: + - حفظ بيانات المستخدم الكاملة + - تذكر المستخدم (Remember Me) + - تسجيل الخروج (Logout) + - تحديث Token تلقائياً عند انتهاء الصلاحية diff --git a/lib/core/di/injection_container.dart b/lib/core/di/injection_container.dart index 7055227..3bb5b7e 100644 --- a/lib/core/di/injection_container.dart +++ b/lib/core/di/injection_container.dart @@ -2,6 +2,11 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../network/api_client.dart'; +import '../../data/datasources/auth_remote_data_source.dart'; +import '../../data/datasources/user_local_data_source.dart'; +import '../../data/repositories/auth_repository_impl.dart'; +import '../../domain/repositories/auth_repository.dart'; +import '../../domain/usecases/login_usecase.dart'; final sl = GetIt.instance; @@ -14,26 +19,29 @@ Future initializeDependencies() async { sl.registerLazySingleton(() => sharedPreferences); // Core - sl.registerLazySingleton(() => ApiClient(dio: sl())); + sl.registerLazySingleton( + () => ApiClient(dio: sl(), sharedPreferences: sl()), + ); - // Data sources will be registered here - // Example: - // sl.registerLazySingleton( - // () => AuthRemoteDataSourceImpl(apiClient: sl()), - // ); + // Data sources + sl.registerLazySingleton( + () => AuthRemoteDataSourceImpl(apiClient: sl()), + ); + + sl.registerLazySingleton( + () => UserLocalDataSourceImpl(sharedPreferences: sl()), + ); - // Repositories will be registered here - // Example: - // sl.registerLazySingleton( - // () => AuthRepositoryImpl( - // remoteDataSource: sl(), - // localDataSource: sl(), - // ), - // ); + // Repositories + sl.registerLazySingleton( + () => AuthRepositoryImpl( + remoteDataSource: sl(), + localDataSource: sl(), + ), + ); - // Use cases will be registered here - // Example: - // sl.registerLazySingleton(() => LoginUseCase(repository: sl())); + // Use cases + sl.registerLazySingleton(() => LoginUseCase(repository: sl())); // Blocs will be registered here // Example: diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 3372c87..d936000 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -1,10 +1,13 @@ import 'package:dio/dio.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class ApiClient { final Dio dio; - static const String baseUrl = 'YOUR_API_BASE_URL_HERE'; + final SharedPreferences? sharedPreferences; + static const String baseUrl = 'https://hrm.go.iq/api'; + static const String _tokenKey = 'user_token'; - ApiClient({required this.dio}) { + ApiClient({required this.dio, this.sharedPreferences}) { dio.options = BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 30), @@ -15,6 +18,20 @@ class ApiClient { }, ); + // Add interceptor to add token to requests + dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) async { + // Get token from SharedPreferences + final token = sharedPreferences?.getString(_tokenKey); + if (token != null && token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + return handler.next(options); + }, + ), + ); + // Add interceptors for logging and error handling dio.interceptors.add( LogInterceptor( diff --git a/lib/data/datasources/auth_remote_data_source.dart b/lib/data/datasources/auth_remote_data_source.dart new file mode 100644 index 0000000..408c7a8 --- /dev/null +++ b/lib/data/datasources/auth_remote_data_source.dart @@ -0,0 +1,77 @@ +import 'package:dio/dio.dart'; +import '../../core/error/exceptions.dart'; +import '../../core/network/api_client.dart'; +import '../dto/login_dto.dart'; +import '../dto/login_response_dto.dart'; + +abstract class AuthRemoteDataSource { + Future login(LoginDto dto); +} + +class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { + final ApiClient apiClient; + + AuthRemoteDataSourceImpl({required this.apiClient}); + + @override + Future login(LoginDto dto) async { + try { + final response = await apiClient.post( + '/Auth/login', + data: dto.toJson(), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = response.data; + + if (responseData is Map) { + return LoginResponseDto.fromJson(responseData); + } else { + throw ServerException( + message: 'استجابة غير صحيحة من الخادم', + statusCode: response.statusCode, + ); + } + } else { + throw ServerException( + message: 'فشل تسجيل الدخول', + statusCode: response.statusCode, + ); + } + } on DioException catch (e) { + if (e.type == DioExceptionType.connectionTimeout || + e.type == DioExceptionType.receiveTimeout) { + throw NetworkException(message: 'انتهت مهلة الاتصال'); + } else if (e.type == DioExceptionType.connectionError) { + throw NetworkException(message: 'لا يوجد اتصال بالانترنيت'); + } else if (e.response?.statusCode == 500) { + throw ServerException(message: 'خطأ في الخادم يرجى المحاولة لاحقا'); + } else if (e.response != null) { + final message = e.response?.data?['message'] ?? + e.response?.data?['error'] ?? + 'فشل تسجيل الدخول'; + + // Check for invalid credentials + final customMessage = + message.toString().toLowerCase().contains('invalid') || + message.toString().toLowerCase().contains('incorrect') + ? 'رقم الهاتف أو كلمة المرور غير صحيحة' + : message.toString().toLowerCase().contains('not found') + ? 'المستخدم غير موجود' + : message; + + throw ServerException( + message: customMessage, + statusCode: e.response?.statusCode, + ); + } else { + throw NetworkException(message: 'خطأ في الانترنيت يرجى المحاولة لاحقا'); + } + } catch (e) { + if (e is ServerException || e is NetworkException) { + rethrow; + } + throw ServerException(message: 'خطأ غير متوقع'); + } + } +} diff --git a/lib/data/dto/login_dto.dart b/lib/data/dto/login_dto.dart new file mode 100644 index 0000000..2d1a861 --- /dev/null +++ b/lib/data/dto/login_dto.dart @@ -0,0 +1,16 @@ +class LoginDto { + final String phoneNumber; + final String password; + + LoginDto({ + required this.phoneNumber, + required this.password, + }); + + Map toJson() { + return { + 'phoneNumber': phoneNumber, + 'password': password, + }; + } +} diff --git a/lib/data/dto/login_response_dto.dart b/lib/data/dto/login_response_dto.dart new file mode 100644 index 0000000..635e354 --- /dev/null +++ b/lib/data/dto/login_response_dto.dart @@ -0,0 +1,85 @@ +class LoginResponseDto { + final int statusCode; + final bool isSuccess; + final String message; + final LoginDataDto? data; + + LoginResponseDto({ + required this.statusCode, + required this.isSuccess, + required this.message, + this.data, + }); + + factory LoginResponseDto.fromJson(Map json) { + return LoginResponseDto( + statusCode: json['statusCode'] ?? 0, + isSuccess: json['isSuccess'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null ? LoginDataDto.fromJson(json['data']) : null, + ); + } + + Map toJson() { + return { + 'statusCode': statusCode, + 'isSuccess': isSuccess, + 'message': message, + 'data': data?.toJson(), + }; + } +} + +class LoginDataDto { + final String? token; + final String? id; + final String? employeeId; + final String? username; + final String? fullName; + final String? role; + final String? email; + final String? phoneNumber; + final List? permissions; + + LoginDataDto({ + this.token, + this.id, + this.employeeId, + this.username, + this.fullName, + this.role, + this.email, + this.phoneNumber, + this.permissions, + }); + + factory LoginDataDto.fromJson(Map json) { + return LoginDataDto( + token: json['token'], + id: json['id'], + employeeId: json['employeeId'], + username: json['username'], + fullName: json['fullName'], + role: json['role'], + email: json['email'], + phoneNumber: json['phoneNumber'], + permissions: json['permissions'] != null + ? List.from(json['permissions']) + : null, + ); + } + + Map toJson() { + return { + 'token': token, + 'id': id, + 'employeeId': employeeId, + 'username': username, + 'fullName': fullName, + 'role': role, + 'email': email, + 'phoneNumber': phoneNumber, + 'permissions': permissions, + }; + } +} diff --git a/lib/data/repositories/auth_repository_impl.dart b/lib/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..20dd1eb --- /dev/null +++ b/lib/data/repositories/auth_repository_impl.dart @@ -0,0 +1,65 @@ +import 'package:dartz/dartz.dart'; +import '../../core/error/exceptions.dart'; +import '../../core/error/failures.dart'; +import '../datasources/auth_remote_data_source.dart'; +import '../datasources/user_local_data_source.dart'; +import '../dto/login_dto.dart'; +import '../dto/login_response_dto.dart'; +import '../../domain/models/login_request.dart'; +import '../../domain/models/login_response_model.dart'; +import '../../domain/repositories/auth_repository.dart'; + +class AuthRepositoryImpl implements AuthRepository { + final AuthRemoteDataSource remoteDataSource; + final UserLocalDataSource localDataSource; + + AuthRepositoryImpl({ + required this.remoteDataSource, + required this.localDataSource, + }); + + @override + Future> login(LoginRequest request) async { + try { + final dto = LoginDto( + phoneNumber: request.phoneNumber, + password: request.password, + ); + + final responseDto = await remoteDataSource.login(dto); + + // Cache the token locally + if (responseDto.data?.token != null) { + await localDataSource.cacheUserToken(responseDto.data!.token!); + } + + // Convert DTO to Model + final responseModel = LoginResponseModel( + statusCode: responseDto.statusCode, + isSuccess: responseDto.isSuccess, + message: responseDto.message, + data: responseDto.data != null + ? LoginDataModel( + token: responseDto.data!.token, + id: responseDto.data!.id, + employeeId: responseDto.data!.employeeId, + username: responseDto.data!.username, + fullName: responseDto.data!.fullName, + role: responseDto.data!.role, + email: responseDto.data!.email, + phoneNumber: responseDto.data!.phoneNumber, + permissions: responseDto.data!.permissions, + ) + : null, + ); + + return Right(responseModel); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return Left(NetworkFailure(e.message)); + } catch (e) { + return Left(ServerFailure('خطأ غير متوقع: $e')); + } + } +} diff --git a/lib/domain/models/login_request.dart b/lib/domain/models/login_request.dart new file mode 100644 index 0000000..5877c04 --- /dev/null +++ b/lib/domain/models/login_request.dart @@ -0,0 +1,9 @@ +class LoginRequest { + final String phoneNumber; + final String password; + + LoginRequest({ + required this.phoneNumber, + required this.password, + }); +} diff --git a/lib/domain/models/login_response_model.dart b/lib/domain/models/login_response_model.dart new file mode 100644 index 0000000..f4ef7e5 --- /dev/null +++ b/lib/domain/models/login_response_model.dart @@ -0,0 +1,37 @@ +class LoginResponseModel { + final int statusCode; + final bool isSuccess; + final String message; + final LoginDataModel? data; + + LoginResponseModel({ + required this.statusCode, + required this.isSuccess, + required this.message, + this.data, + }); +} + +class LoginDataModel { + final String? token; + final String? id; + final String? employeeId; + final String? username; + final String? fullName; + final String? role; + final String? email; + final String? phoneNumber; + final List? permissions; + + LoginDataModel({ + this.token, + this.id, + this.employeeId, + this.username, + this.fullName, + this.role, + this.email, + this.phoneNumber, + this.permissions, + }); +} diff --git a/lib/domain/repositories/auth_repository.dart b/lib/domain/repositories/auth_repository.dart new file mode 100644 index 0000000..e5a101e --- /dev/null +++ b/lib/domain/repositories/auth_repository.dart @@ -0,0 +1,8 @@ +import 'package:dartz/dartz.dart'; +import '../../core/error/failures.dart'; +import '../models/login_request.dart'; +import '../models/login_response_model.dart'; + +abstract class AuthRepository { + Future> login(LoginRequest request); +} diff --git a/lib/domain/usecases/login_usecase.dart b/lib/domain/usecases/login_usecase.dart new file mode 100644 index 0000000..de54c57 --- /dev/null +++ b/lib/domain/usecases/login_usecase.dart @@ -0,0 +1,15 @@ +import 'package:dartz/dartz.dart'; +import '../../core/error/failures.dart'; +import '../models/login_request.dart'; +import '../models/login_response_model.dart'; +import '../repositories/auth_repository.dart'; + +class LoginUseCase { + final AuthRepository repository; + + LoginUseCase({required this.repository}); + + Future> call(LoginRequest request) { + return repository.login(request); + } +} diff --git a/lib/widgets/auth_form.dart b/lib/widgets/auth_form.dart index d97a03a..99d049e 100644 --- a/lib/widgets/auth_form.dart +++ b/lib/widgets/auth_form.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; -import '../screens/main_screen.dart'; +import '../screens/main_screen.dart'; +import '../core/di/injection_container.dart'; +import '../domain/usecases/login_usecase.dart'; +import '../domain/models/login_request.dart'; import 'onboarding_button.dart'; class AuthForm extends StatefulWidget { @@ -13,25 +16,86 @@ class AuthForm extends StatefulWidget { class _AuthFormState extends State { bool _obscure = true; + bool _isLoading = false; + + // Text controllers + final TextEditingController _phoneNumberController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); // Focus nodes for text fields - late FocusNode _usernameFocusNode; + late FocusNode _phoneNumberFocusNode; late FocusNode _passwordFocusNode; - void _handleLogin() { - // Unfocus any focused text field - _usernameFocusNode.unfocus(); - _passwordFocusNode.unfocus(); + // Get LoginUseCase from dependency injection + final LoginUseCase _loginUseCase = sl(); - // Call the onSubmit callback if provided (for any other logic you might have) - if (widget.onSubmit != null) { - widget.onSubmit!(); + Future _handleLogin() async { + // Validate inputs + if (_phoneNumberController.text.trim().isEmpty) { + _showError('الرجاء إدخال رقم الهاتف'); + return; } - // Navigate to the AttendancePage - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const MainPage()), + if (_passwordController.text.trim().isEmpty) { + _showError('الرجاء إدخال كلمة المرور'); + return; + } + + // Unfocus any focused text field + _phoneNumberFocusNode.unfocus(); + _passwordFocusNode.unfocus(); + + setState(() { + _isLoading = true; + }); + + try { + final request = LoginRequest( + phoneNumber: _phoneNumberController.text.trim(), + password: _passwordController.text.trim(), + ); + + final result = await _loginUseCase(request); + + result.fold( + (failure) { + _showError(failure.message); + }, + (response) { + if (response.isSuccess) { + // Call the onSubmit callback if provided + if (widget.onSubmit != null) { + widget.onSubmit!(); + } + + // Navigate to the MainPage + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const MainPage()), + ); + } else { + _showError(response.message); + } + }, + ); + } catch (e) { + _showError('حدث خطأ غير متوقع: $e'); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), ); } @@ -39,14 +103,16 @@ class _AuthFormState extends State { void initState() { super.initState(); // Initialize focus nodes - _usernameFocusNode = FocusNode(); + _phoneNumberFocusNode = FocusNode(); _passwordFocusNode = FocusNode(); } @override void dispose() { - // Clean up focus nodes when widget is disposed - _usernameFocusNode.dispose(); + // Clean up controllers and focus nodes + _phoneNumberController.dispose(); + _passwordController.dispose(); + _phoneNumberFocusNode.dispose(); _passwordFocusNode.dispose(); super.dispose(); } @@ -130,11 +196,11 @@ class _AuthFormState extends State { SizedBox(height: verticalSpacing), - /// Username Label + /// Phone Number Label Align( alignment: Alignment.centerRight, child: Text( - "اسم المستخدم", + "رقم الهاتف", style: TextStyle( fontSize: labelFontSize, color: Colors.black87, @@ -144,9 +210,11 @@ class _AuthFormState extends State { const SizedBox(height: 8), _buildField( - hint: "اسم المستخدم", + controller: _phoneNumberController, + hint: "رقم الهاتف", obscure: false, - focusNode: _usernameFocusNode, + keyboardType: TextInputType.phone, + focusNode: _phoneNumberFocusNode, textInputAction: TextInputAction.next, onSubmitted: (_) { // Move focus to password field when next is pressed @@ -171,14 +239,13 @@ class _AuthFormState extends State { const SizedBox(height: 8), _buildField( + controller: _passwordController, hint: "كلمة المرور", obscure: _obscure, hasEye: true, focusNode: _passwordFocusNode, textInputAction: TextInputAction.done, - onSubmitted: - (_) => - _handleLogin(), // Added parentheses to call the method + onSubmitted: (_) => _handleLogin(), fontSize: fieldFontSize, ), @@ -186,9 +253,9 @@ class _AuthFormState extends State { Center( child: OnboardingButton( - text: "تسجيل دخول", + text: _isLoading ? "جاري تسجيل الدخول..." : "تسجيل دخول", backgroundColor: const Color.fromARGB(239, 35, 87, 74), - onPressed: _handleLogin, + onPressed: _isLoading ? null : _handleLogin, ), ), @@ -203,9 +270,11 @@ class _AuthFormState extends State { } Widget _buildField({ + TextEditingController? controller, required String hint, required bool obscure, bool hasEye = false, + TextInputType? keyboardType, FocusNode? focusNode, TextInputAction? textInputAction, Function(String)? onSubmitted, @@ -220,8 +289,10 @@ class _AuthFormState extends State { ], ), child: TextField( + controller: controller, focusNode: focusNode, obscureText: obscure, + keyboardType: keyboardType, textAlign: TextAlign.right, textInputAction: textInputAction, onSubmitted: onSubmitted,