This commit is contained in:
Mohammed Al-Samarraie
2026-01-13 13:07:31 +03:00
parent ac8a769ff0
commit fa4bee4771
15 changed files with 578 additions and 45 deletions

3
devtools_options.yaml Normal file
View File

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

View File

@@ -4,11 +4,15 @@ PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_native_splash (2.4.3): - flutter_native_splash (2.4.3):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
camera_avfoundation: camera_avfoundation:
@@ -17,11 +21,14 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5

View File

@@ -161,7 +161,6 @@
C9C24CB30CC2EF64A5028A78 /* Pods-RunnerTests.release.xcconfig */, C9C24CB30CC2EF64A5028A78 /* Pods-RunnerTests.release.xcconfig */,
C8EAEACEAC43BB801D9EFF4B /* Pods-RunnerTests.profile.xcconfig */, C8EAEACEAC43BB801D9EFF4B /* Pods-RunnerTests.profile.xcconfig */,
); );
name = Pods;
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -471,6 +470,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 6YV3J6426X;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -653,6 +653,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 6YV3J6426X;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -675,6 +676,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 6YV3J6426X;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (

113
lib/LOGIN_SETUP.md Normal file
View File

@@ -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<LoginUseCase>();
// إنشاء طلب تسجيل الدخول
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 تلقائياً عند انتهاء الصلاحية

View File

@@ -2,6 +2,11 @@ import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../network/api_client.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; final sl = GetIt.instance;
@@ -14,26 +19,29 @@ Future<void> initializeDependencies() async {
sl.registerLazySingleton<SharedPreferences>(() => sharedPreferences); sl.registerLazySingleton<SharedPreferences>(() => sharedPreferences);
// Core // Core
sl.registerLazySingleton<ApiClient>(() => ApiClient(dio: sl())); sl.registerLazySingleton<ApiClient>(
() => ApiClient(dio: sl(), sharedPreferences: sl()),
);
// Data sources will be registered here // Data sources
// Example: sl.registerLazySingleton<AuthRemoteDataSource>(
// sl.registerLazySingleton<AuthRemoteDataSource>( () => AuthRemoteDataSourceImpl(apiClient: sl()),
// () => AuthRemoteDataSourceImpl(apiClient: sl()), );
// );
sl.registerLazySingleton<UserLocalDataSource>(
() => UserLocalDataSourceImpl(sharedPreferences: sl()),
);
// Repositories will be registered here // Repositories
// Example: sl.registerLazySingleton<AuthRepository>(
// sl.registerLazySingleton<AuthRepository>( () => AuthRepositoryImpl(
// () => AuthRepositoryImpl( remoteDataSource: sl(),
// remoteDataSource: sl(), localDataSource: sl(),
// localDataSource: sl(), ),
// ), );
// );
// Use cases will be registered here // Use cases
// Example: sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
// sl.registerLazySingleton(() => LoginUseCase(repository: sl()));
// Blocs will be registered here // Blocs will be registered here
// Example: // Example:

View File

@@ -1,10 +1,13 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ApiClient { class ApiClient {
final Dio dio; 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( dio.options = BaseOptions(
baseUrl: baseUrl, baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 30), 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 // Add interceptors for logging and error handling
dio.interceptors.add( dio.interceptors.add(
LogInterceptor( LogInterceptor(

View File

@@ -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<LoginResponseDto> login(LoginDto dto);
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final ApiClient apiClient;
AuthRemoteDataSourceImpl({required this.apiClient});
@override
Future<LoginResponseDto> 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<String, dynamic>) {
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: 'خطأ غير متوقع');
}
}
}

View File

@@ -0,0 +1,16 @@
class LoginDto {
final String phoneNumber;
final String password;
LoginDto({
required this.phoneNumber,
required this.password,
});
Map<String, dynamic> toJson() {
return {
'phoneNumber': phoneNumber,
'password': password,
};
}
}

View File

@@ -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<String, dynamic> json) {
return LoginResponseDto(
statusCode: json['statusCode'] ?? 0,
isSuccess: json['isSuccess'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null ? LoginDataDto.fromJson(json['data']) : null,
);
}
Map<String, dynamic> 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<String>? permissions;
LoginDataDto({
this.token,
this.id,
this.employeeId,
this.username,
this.fullName,
this.role,
this.email,
this.phoneNumber,
this.permissions,
});
factory LoginDataDto.fromJson(Map<String, dynamic> 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<String>.from(json['permissions'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'token': token,
'id': id,
'employeeId': employeeId,
'username': username,
'fullName': fullName,
'role': role,
'email': email,
'phoneNumber': phoneNumber,
'permissions': permissions,
};
}
}

View File

@@ -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<Either<Failure, LoginResponseModel>> 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'));
}
}
}

View File

@@ -0,0 +1,9 @@
class LoginRequest {
final String phoneNumber;
final String password;
LoginRequest({
required this.phoneNumber,
required this.password,
});
}

View File

@@ -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<String>? permissions;
LoginDataModel({
this.token,
this.id,
this.employeeId,
this.username,
this.fullName,
this.role,
this.email,
this.phoneNumber,
this.permissions,
});
}

View File

@@ -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<Either<Failure, LoginResponseModel>> login(LoginRequest request);
}

View File

@@ -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<Either<Failure, LoginResponseModel>> call(LoginRequest request) {
return repository.login(request);
}
}

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; 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'; import 'onboarding_button.dart';
class AuthForm extends StatefulWidget { class AuthForm extends StatefulWidget {
@@ -13,25 +16,86 @@ class AuthForm extends StatefulWidget {
class _AuthFormState extends State<AuthForm> { class _AuthFormState extends State<AuthForm> {
bool _obscure = true; bool _obscure = true;
bool _isLoading = false;
// Text controllers
final TextEditingController _phoneNumberController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
// Focus nodes for text fields // Focus nodes for text fields
late FocusNode _usernameFocusNode; late FocusNode _phoneNumberFocusNode;
late FocusNode _passwordFocusNode; late FocusNode _passwordFocusNode;
void _handleLogin() { // Get LoginUseCase from dependency injection
// Unfocus any focused text field final LoginUseCase _loginUseCase = sl<LoginUseCase>();
_usernameFocusNode.unfocus();
_passwordFocusNode.unfocus();
// Call the onSubmit callback if provided (for any other logic you might have) Future<void> _handleLogin() async {
if (widget.onSubmit != null) { // Validate inputs
widget.onSubmit!(); if (_phoneNumberController.text.trim().isEmpty) {
_showError('الرجاء إدخال رقم الهاتف');
return;
} }
// Navigate to the AttendancePage if (_passwordController.text.trim().isEmpty) {
Navigator.pushReplacement( _showError('الرجاء إدخال كلمة المرور');
context, return;
MaterialPageRoute(builder: (context) => const MainPage()), }
// 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<AuthForm> {
void initState() { void initState() {
super.initState(); super.initState();
// Initialize focus nodes // Initialize focus nodes
_usernameFocusNode = FocusNode(); _phoneNumberFocusNode = FocusNode();
_passwordFocusNode = FocusNode(); _passwordFocusNode = FocusNode();
} }
@override @override
void dispose() { void dispose() {
// Clean up focus nodes when widget is disposed // Clean up controllers and focus nodes
_usernameFocusNode.dispose(); _phoneNumberController.dispose();
_passwordController.dispose();
_phoneNumberFocusNode.dispose();
_passwordFocusNode.dispose(); _passwordFocusNode.dispose();
super.dispose(); super.dispose();
} }
@@ -130,11 +196,11 @@ class _AuthFormState extends State<AuthForm> {
SizedBox(height: verticalSpacing), SizedBox(height: verticalSpacing),
/// Username Label /// Phone Number Label
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Text( child: Text(
"اسم المستخدم", "رقم الهاتف",
style: TextStyle( style: TextStyle(
fontSize: labelFontSize, fontSize: labelFontSize,
color: Colors.black87, color: Colors.black87,
@@ -144,9 +210,11 @@ class _AuthFormState extends State<AuthForm> {
const SizedBox(height: 8), const SizedBox(height: 8),
_buildField( _buildField(
hint: "اسم المستخدم", controller: _phoneNumberController,
hint: "رقم الهاتف",
obscure: false, obscure: false,
focusNode: _usernameFocusNode, keyboardType: TextInputType.phone,
focusNode: _phoneNumberFocusNode,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onSubmitted: (_) { onSubmitted: (_) {
// Move focus to password field when next is pressed // Move focus to password field when next is pressed
@@ -171,14 +239,13 @@ class _AuthFormState extends State<AuthForm> {
const SizedBox(height: 8), const SizedBox(height: 8),
_buildField( _buildField(
controller: _passwordController,
hint: "كلمة المرور", hint: "كلمة المرور",
obscure: _obscure, obscure: _obscure,
hasEye: true, hasEye: true,
focusNode: _passwordFocusNode, focusNode: _passwordFocusNode,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
onSubmitted: onSubmitted: (_) => _handleLogin(),
(_) =>
_handleLogin(), // Added parentheses to call the method
fontSize: fieldFontSize, fontSize: fieldFontSize,
), ),
@@ -186,9 +253,9 @@ class _AuthFormState extends State<AuthForm> {
Center( Center(
child: OnboardingButton( child: OnboardingButton(
text: "تسجيل دخول", text: _isLoading ? "جاري تسجيل الدخول..." : "تسجيل دخول",
backgroundColor: const Color.fromARGB(239, 35, 87, 74), backgroundColor: const Color.fromARGB(239, 35, 87, 74),
onPressed: _handleLogin, onPressed: _isLoading ? null : _handleLogin,
), ),
), ),
@@ -203,9 +270,11 @@ class _AuthFormState extends State<AuthForm> {
} }
Widget _buildField({ Widget _buildField({
TextEditingController? controller,
required String hint, required String hint,
required bool obscure, required bool obscure,
bool hasEye = false, bool hasEye = false,
TextInputType? keyboardType,
FocusNode? focusNode, FocusNode? focusNode,
TextInputAction? textInputAction, TextInputAction? textInputAction,
Function(String)? onSubmitted, Function(String)? onSubmitted,
@@ -220,8 +289,10 @@ class _AuthFormState extends State<AuthForm> {
], ],
), ),
child: TextField( child: TextField(
controller: controller,
focusNode: focusNode, focusNode: focusNode,
obscureText: obscure, obscureText: obscure,
keyboardType: keyboardType,
textAlign: TextAlign.right, textAlign: TextAlign.right,
textInputAction: textInputAction, textInputAction: textInputAction,
onSubmitted: onSubmitted, onSubmitted: onSubmitted,