From ac8a769ff086dffb5a7be22edd2e4355bdffefeb Mon Sep 17 00:00:00 2001 From: Mohammed Al-Samarraie Date: Tue, 13 Jan 2026 12:43:43 +0300 Subject: [PATCH 1/2] 11111 --- lib/ARCHITECTURE.md | 173 ++++++++++++++++++ lib/core/constants/dimensions.dart | 146 +++++++++++++++ lib/core/di/injection_container.dart | 41 +++++ lib/core/enums/app_enums.dart | 7 + lib/core/error/exceptions.dart | 18 ++ lib/core/error/failures.dart | 22 +++ lib/core/network/api_client.dart | 103 +++++++++++ lib/core/utils/validators.dart | 37 ++++ lib/data/datasources/.gitkeep | 25 +++ .../datasources/user_local_data_source.dart | 29 +++ lib/data/dto/.gitkeep | 24 +++ lib/data/repositories/.gitkeep | 27 +++ lib/domain/models/.gitkeep | 3 + lib/domain/repositories/.gitkeep | 8 + lib/domain/usecases/.gitkeep | 13 ++ lib/main.dart | 7 +- lib/presentation/blocs/.gitkeep | 6 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 16 ++ macos/Runner.xcodeproj/project.pbxproj | 80 +++++++- .../contents.xcworkspacedata | 3 + pubspec.lock | 152 +++++++++++++++ pubspec.yaml | 5 + 23 files changed, 945 insertions(+), 2 deletions(-) create mode 100644 lib/ARCHITECTURE.md create mode 100644 lib/core/constants/dimensions.dart create mode 100644 lib/core/di/injection_container.dart create mode 100644 lib/core/enums/app_enums.dart create mode 100644 lib/core/error/exceptions.dart create mode 100644 lib/core/error/failures.dart create mode 100644 lib/core/network/api_client.dart create mode 100644 lib/core/utils/validators.dart create mode 100644 lib/data/datasources/.gitkeep create mode 100644 lib/data/datasources/user_local_data_source.dart create mode 100644 lib/data/dto/.gitkeep create mode 100644 lib/data/repositories/.gitkeep create mode 100644 lib/domain/models/.gitkeep create mode 100644 lib/domain/repositories/.gitkeep create mode 100644 lib/domain/usecases/.gitkeep create mode 100644 lib/presentation/blocs/.gitkeep create mode 100644 macos/Podfile.lock diff --git a/lib/ARCHITECTURE.md b/lib/ARCHITECTURE.md new file mode 100644 index 0000000..4b9601e --- /dev/null +++ b/lib/ARCHITECTURE.md @@ -0,0 +1,173 @@ +# هيكلية المشروع (Project Architecture) + +هذا المشروع يتبع نمط Clean Architecture مع فصل واضح بين الطبقات. + +## الهيكلية العامة + +``` +lib/ +├── core/ # المكونات الأساسية المشتركة +│ ├── constants/ # الثوابت (مثل الأبعاد) +│ ├── di/ # Dependency Injection (GetIt) +│ ├── enums/ # التعدادات +│ ├── error/ # معالجة الأخطاء (Exceptions & Failures) +│ ├── network/ # عميل API (ApiClient) +│ └── utils/ # الأدوات المساعدة +│ +├── data/ # طبقة البيانات +│ ├── datasources/ # مصادر البيانات (Remote & Local) +│ ├── dto/ # Data Transfer Objects (للاتصال مع API) +│ └── repositories/ # تطبيقات الـ Repositories +│ +├── domain/ # طبقة الأعمال (Business Logic) +│ ├── models/ # نماذج الأعمال +│ ├── repositories/ # واجهات الـ Repositories +│ └── usecases/ # حالات الاستخدام (Use Cases) +│ +├── presentation/ # طبقة العرض +│ ├── blocs/ # State Management (BLoC) +│ ├── screens/ # الشاشات +│ └── widgets/ # الويدجتات القابلة لإعادة الاستخدام +│ +├── models/ # النماذج القديمة (يمكن نقلها لـ domain/models) +├── screens/ # الشاشات القديمة (يمكن نقلها لـ presentation/screens) +├── services/ # الخدمات القديمة +└── widgets/ # الويدجتات القديمة (يمكن نقلها لـ presentation/widgets) +``` + +## كيفية الاستخدام + +### 1. إضافة API جديد + +#### أ) إنشاء DTO في `data/dto/` +```dart +class LoginDto { + final String phoneNumber; + final String password; + + LoginDto({required this.phoneNumber, required this.password}); + + Map toJson() => { + 'phoneNumber': phoneNumber, + 'password': password, + }; +} +``` + +#### ب) إنشاء Remote Data Source في `data/datasources/` +```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()); + // Handle response + } on DioException catch (e) { + // Handle errors + } + } +} +``` + +#### ج) إنشاء Repository Interface في `domain/repositories/` +```dart +abstract class AuthRepository { + Future> login(LoginRequest request); +} +``` + +#### د) إنشاء Repository Implementation في `data/repositories/` +```dart +class AuthRepositoryImpl implements AuthRepository { + final AuthRemoteDataSource remoteDataSource; + + AuthRepositoryImpl({required this.remoteDataSource}); + + @override + Future> login(LoginRequest request) async { + try { + final dto = LoginDto(...); + final responseDto = await remoteDataSource.login(dto); + final model = _convertDtoToModel(responseDto); + return Right(model); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } + } +} +``` + +#### هـ) إنشاء Use Case في `domain/usecases/` +```dart +class LoginUseCase { + final AuthRepository repository; + + LoginUseCase({required this.repository}); + + Future> call(LoginRequest request) { + return repository.login(request); + } +} +``` + +#### و) تسجيل في `core/di/injection_container.dart` +```dart +// Data source +sl.registerLazySingleton( + () => AuthRemoteDataSourceImpl(apiClient: sl()), +); + +// Repository +sl.registerLazySingleton( + () => AuthRepositoryImpl(remoteDataSource: sl()), +); + +// Use case +sl.registerLazySingleton(() => LoginUseCase(repository: sl())); +``` + +### 2. استخدام في BLoC +```dart +class LoginBloc extends Bloc { + final LoginUseCase loginUseCase; + + LoginBloc({required this.loginUseCase}) : super(LoginInitial()) { + on(_onLoginSubmitted); + } + + Future _onLoginSubmitted( + LoginSubmitted event, + Emitter emit, + ) async { + emit(LoginLoading()); + final result = await loginUseCase(event.request); + result.fold( + (failure) => emit(LoginError(failure.message)), + (response) => emit(LoginSuccess(response)), + ); + } +} +``` + +## الحزم المستخدمة + +- `dio`: للاتصال بالـ API +- `get_it`: لإدارة Dependency Injection +- `dartz`: لاستخدام `Either` للتعامل مع الأخطاء +- `equatable`: للمساواة بين الكائنات +- `shared_preferences`: للتخزين المحلي + +## ملاحظات مهمة + +1. **تحديث baseUrl**: قم بتحديث `baseUrl` في `core/network/api_client.dart` +2. **إضافة Token**: يمكن إضافة interceptor في `ApiClient` لإضافة token تلقائياً +3. **معالجة الأخطاء**: جميع الأخطاء تمر عبر `Exceptions` ثم `Failures` +4. **التحويل**: DTOs للـ API، Models للـ Domain diff --git a/lib/core/constants/dimensions.dart b/lib/core/constants/dimensions.dart new file mode 100644 index 0000000..b5d9612 --- /dev/null +++ b/lib/core/constants/dimensions.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; + +class AppDimensions { + // Private constructor to prevent instantiation + AppDimensions._(); + + // Screen breakpoints + static const double smallScreenWidth = 460; + static const double tabletWidth = 600; + static const double largeScreenWidth = 1200; + + // Padding values + static const double paddingXS = 4.0; + static const double paddingS = 8.0; + static const double paddingM = 12.0; + static const double paddingL = 16.0; + static const double paddingXL = 25.0; + static const double paddingXXL = 30.0; + static const double paddingXXXL = 30.0; + static const double paddingHuge = 40.0; + + // Spacing values + static const double spacingXS = 4.0; + static const double spacingS = 8.0; + static const double spacingM = 12.0; + static const double spacingL = 16.0; + static const double spacingXL = 20.0; + static const double spacingXXL = 23.0; + static const double spacingXXXL = 30.0; + static const double spacingHuge = 40.0; + + // Font sizes + static const double fontSizeXS = 10.0; + static const double fontSizeS = 12.0; + static const double fontSizeM = 14.0; + static const double fontSizeL = 16.0; + static const double fontSizeXL = 18.0; + static const double fontSizeXXL = 20.0; + static const double fontSizeXXXL = 22.0; + static const double fontSizeHuge = 24.0; + static const double fontSizeTitle = 36.0; + static const double fontSizeTitleLarge = 42.0; + + // Icon sizes + static const double iconSizeXS = 16.0; + static const double iconSizeS = 20.0; + static const double iconSizeM = 24.0; + static const double iconSizeL = 30.0; + static const double iconSizeXL = 33.0; + static const double iconSizeXXL = 40.0; + static const double iconSizeHuge = 60.0; + + // Border radius + static const double radiusXS = 3.0; + static const double radiusS = 5.0; + static const double radiusM = 8.0; + static const double radiusL = 10.0; + static const double radiusXL = 15.0; + static const double radiusXXL = 20.0; + static const double radiusCircle = 50.0; + + // Button heights + static const double buttonHeightS = 40.0; + static const double buttonHeightM = 48.0; + static const double buttonHeightL = 56.0; + + // Image heights + static const double imageHeightS = 150.0; + static const double imageHeightM = 200.0; + static const double imageHeightL = 250.0; + + // Page indicator sizes + static const double pageIndicatorSize = 10.0; + static const double pageIndicatorSizeActive = 16.0; + + // Responsive methods + static bool isSmallScreen(BuildContext context) { + return MediaQuery.of(context).size.width < smallScreenWidth; + } + + static bool isTablet(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return width >= tabletWidth && width < largeScreenWidth; + } + + static bool isLargeScreen(BuildContext context) { + return MediaQuery.of(context).size.width >= largeScreenWidth; + } + + // Responsive padding + static double getHorizontalPadding(BuildContext context) { + return isSmallScreen(context) ? paddingXL : paddingXXL; + } + + static double getVerticalPadding(BuildContext context) { + return isSmallScreen(context) ? paddingL : paddingXXL; + } + + // Responsive font size + static double getTitleFontSize(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + if (isSmallScreen(context)) { + return screenWidth * 0.07; + } else if (isTablet(context)) { + return fontSizeTitleLarge; + } else { + return screenWidth * 0.08; + } + } + + static double getSubtitleFontSize(BuildContext context) { + return isSmallScreen(context) ? fontSizeM : fontSizeL; + } + + static double getBodyFontSize(BuildContext context) { + return isSmallScreen(context) ? fontSizeS : fontSizeM; + } + + // Responsive spacing + static double getVerticalSpacing(BuildContext context) { + return isSmallScreen(context) ? spacingL : spacingXXL; + } + + static double getSmallVerticalSpacing(BuildContext context) { + return isSmallScreen(context) ? spacingM : spacingL; + } + + // Responsive icon size + static double getIconSize(BuildContext context) { + return isSmallScreen(context) ? iconSizeM : iconSizeXL; + } + + // Screen dimensions + static double screenWidth(BuildContext context) { + return MediaQuery.of(context).size.width; + } + + static double screenHeight(BuildContext context) { + return MediaQuery.of(context).size.height; + } + + // Safe area padding + static EdgeInsets getSafeAreaPadding(BuildContext context) { + return MediaQuery.of(context).padding; + } +} diff --git a/lib/core/di/injection_container.dart b/lib/core/di/injection_container.dart new file mode 100644 index 0000000..7055227 --- /dev/null +++ b/lib/core/di/injection_container.dart @@ -0,0 +1,41 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../network/api_client.dart'; + +final sl = GetIt.instance; + +Future initializeDependencies() async { + // External + sl.registerLazySingleton(() => Dio()); + + // SharedPreferences + final sharedPreferences = await SharedPreferences.getInstance(); + sl.registerLazySingleton(() => sharedPreferences); + + // Core + sl.registerLazySingleton(() => ApiClient(dio: sl())); + + // Data sources will be registered here + // Example: + // sl.registerLazySingleton( + // () => AuthRemoteDataSourceImpl(apiClient: sl()), + // ); + + // Repositories will be registered here + // Example: + // sl.registerLazySingleton( + // () => AuthRepositoryImpl( + // remoteDataSource: sl(), + // localDataSource: sl(), + // ), + // ); + + // Use cases will be registered here + // Example: + // sl.registerLazySingleton(() => LoginUseCase(repository: sl())); + + // Blocs will be registered here + // Example: + // sl.registerFactory(() => LoginBloc(loginUseCase: sl())); +} diff --git a/lib/core/enums/app_enums.dart b/lib/core/enums/app_enums.dart new file mode 100644 index 0000000..32d9dec --- /dev/null +++ b/lib/core/enums/app_enums.dart @@ -0,0 +1,7 @@ +// Add your app-specific enums here +// Example: +// enum UserRole { +// admin, +// user, +// guest, +// } diff --git a/lib/core/error/exceptions.dart b/lib/core/error/exceptions.dart new file mode 100644 index 0000000..2ced2ac --- /dev/null +++ b/lib/core/error/exceptions.dart @@ -0,0 +1,18 @@ +class ServerException implements Exception { + final String message; + final int? statusCode; + + ServerException({required this.message, this.statusCode}); +} + +class NetworkException implements Exception { + final String message; + + NetworkException({required this.message}); +} + +class ValidationException implements Exception { + final String message; + + ValidationException({required this.message}); +} diff --git a/lib/core/error/failures.dart b/lib/core/error/failures.dart new file mode 100644 index 0000000..2af3aa1 --- /dev/null +++ b/lib/core/error/failures.dart @@ -0,0 +1,22 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + final String message; + + const Failure(this.message); + + @override + List get props => [message]; +} + +class ServerFailure extends Failure { + const ServerFailure(super.message); +} + +class NetworkFailure extends Failure { + const NetworkFailure(super.message); +} + +class ValidationFailure extends Failure { + const ValidationFailure(super.message); +} diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart new file mode 100644 index 0000000..3372c87 --- /dev/null +++ b/lib/core/network/api_client.dart @@ -0,0 +1,103 @@ +import 'package:dio/dio.dart'; + +class ApiClient { + final Dio dio; + static const String baseUrl = 'YOUR_API_BASE_URL_HERE'; + + ApiClient({required this.dio}) { + dio.options = BaseOptions( + baseUrl: baseUrl, + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + headers: { + 'Content-Type': 'application/json', + 'accept': 'text/plain', + }, + ); + + // Add interceptors for logging and error handling + dio.interceptors.add( + LogInterceptor( + requestBody: true, + responseBody: true, + error: true, + requestHeader: true, + responseHeader: false, + ), + ); + } + + Future post( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + try { + final response = await dio.post( + path, + data: data, + queryParameters: queryParameters, + options: options, + ); + return response; + } catch (e) { + rethrow; + } + } + + Future get( + String path, { + Map? queryParameters, + Options? options, + }) async { + try { + final response = await dio.get( + path, + queryParameters: queryParameters, + options: options, + ); + return response; + } catch (e) { + rethrow; + } + } + + Future put( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + try { + final response = await dio.put( + path, + data: data, + queryParameters: queryParameters, + options: options, + ); + return response; + } catch (e) { + rethrow; + } + } + + Future delete( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + }) async { + try { + final response = await dio.delete( + path, + data: data, + queryParameters: queryParameters, + options: options, + ); + return response; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart new file mode 100644 index 0000000..f818fa4 --- /dev/null +++ b/lib/core/utils/validators.dart @@ -0,0 +1,37 @@ +class Validators { + static String? validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'البريد الإلكتروني مطلوب'; + } + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value)) { + return 'البريد الإلكتروني غير صحيح'; + } + return null; + } + + static String? validatePhone(String? value) { + if (value == null || value.isEmpty) { + return 'رقم الهاتف مطلوب'; + } + // Add your phone validation logic here + return null; + } + + static String? validateRequired(String? value, String fieldName) { + if (value == null || value.isEmpty) { + return '$fieldName مطلوب'; + } + return null; + } + + static String? validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'كلمة المرور مطلوبة'; + } + if (value.length < 6) { + return 'كلمة المرور يجب أن تكون 6 أحرف على الأقل'; + } + return null; + } +} diff --git a/lib/data/datasources/.gitkeep b/lib/data/datasources/.gitkeep new file mode 100644 index 0000000..e709089 --- /dev/null +++ b/lib/data/datasources/.gitkeep @@ -0,0 +1,25 @@ +# Data sources directory +# Create your remote data sources here following this pattern: +# +# abstract class YourRemoteDataSource { +# Future yourMethod(YourRequest request); +# } +# +# class YourRemoteDataSourceImpl implements YourRemoteDataSource { +# final ApiClient apiClient; +# +# YourRemoteDataSourceImpl({required this.apiClient}); +# +# @override +# Future yourMethod(YourRequest request) async { +# try { +# final response = await apiClient.post( +# '/your-endpoint', +# data: request.toJson(), +# ); +# // Handle response and return DTO +# } on DioException catch (e) { +# // Handle errors +# } +# } +# } diff --git a/lib/data/datasources/user_local_data_source.dart b/lib/data/datasources/user_local_data_source.dart new file mode 100644 index 0000000..dacf105 --- /dev/null +++ b/lib/data/datasources/user_local_data_source.dart @@ -0,0 +1,29 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class UserLocalDataSource { + Future cacheUserToken(String token); + Future getCachedUserToken(); + Future clearCache(); +} + +class UserLocalDataSourceImpl implements UserLocalDataSource { + final SharedPreferences sharedPreferences; + static const String _tokenKey = 'user_token'; + + UserLocalDataSourceImpl({required this.sharedPreferences}); + + @override + Future cacheUserToken(String token) async { + await sharedPreferences.setString(_tokenKey, token); + } + + @override + Future getCachedUserToken() async { + return sharedPreferences.getString(_tokenKey); + } + + @override + Future clearCache() async { + await sharedPreferences.remove(_tokenKey); + } +} diff --git a/lib/data/dto/.gitkeep b/lib/data/dto/.gitkeep new file mode 100644 index 0000000..e38ca9a --- /dev/null +++ b/lib/data/dto/.gitkeep @@ -0,0 +1,24 @@ +# DTO (Data Transfer Objects) directory +# Create your DTOs here for API request/response mapping +# Example: +# +# class LoginDto { +# final String phoneNumber; +# final String password; +# +# LoginDto({required this.phoneNumber, required this.password}); +# +# Map toJson() { +# return { +# 'phoneNumber': phoneNumber, +# 'password': password, +# }; +# } +# +# factory LoginDto.fromJson(Map json) { +# return LoginDto( +# phoneNumber: json['phoneNumber'], +# password: json['password'], +# ); +# } +# } diff --git a/lib/data/repositories/.gitkeep b/lib/data/repositories/.gitkeep new file mode 100644 index 0000000..5fa8e5f --- /dev/null +++ b/lib/data/repositories/.gitkeep @@ -0,0 +1,27 @@ +# Repository implementations directory +# Create your repository implementations here +# Example: +# +# 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(...); +# final responseDto = await remoteDataSource.login(dto); +# final responseModel = _convertDtoToModel(responseDto); +# return Right(responseModel); +# } on ServerException catch (e) { +# return Left(ServerFailure(e.message)); +# } on NetworkException catch (e) { +# return Left(NetworkFailure(e.message)); +# } +# } +# } diff --git a/lib/domain/models/.gitkeep b/lib/domain/models/.gitkeep new file mode 100644 index 0000000..afab31e --- /dev/null +++ b/lib/domain/models/.gitkeep @@ -0,0 +1,3 @@ +# Domain models directory +# Create your domain models here (business logic models) +# These are different from DTOs - they represent your app's domain entities diff --git a/lib/domain/repositories/.gitkeep b/lib/domain/repositories/.gitkeep new file mode 100644 index 0000000..d44dfb0 --- /dev/null +++ b/lib/domain/repositories/.gitkeep @@ -0,0 +1,8 @@ +# Repository interfaces directory +# Create your repository interfaces here +# Example: +# +# abstract class AuthRepository { +# Future> login(LoginRequest request); +# Future> register(RegisterRequest request); +# } diff --git a/lib/domain/usecases/.gitkeep b/lib/domain/usecases/.gitkeep new file mode 100644 index 0000000..3b49da7 --- /dev/null +++ b/lib/domain/usecases/.gitkeep @@ -0,0 +1,13 @@ +# Use cases directory +# Create your use cases here (business logic) +# Example: +# +# class LoginUseCase { +# final AuthRepository repository; +# +# LoginUseCase({required this.repository}); +# +# Future> call(LoginRequest request) { +# return repository.login(request); +# } +# } diff --git a/lib/main.dart b/lib/main.dart index 753db99..80a4edc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'core/di/injection_container.dart'; import 'screens/splash_screen.dart'; -void main() { +void main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + + // Initialize dependency injection + await initializeDependencies(); + runApp(const CodaApp()); } diff --git a/lib/presentation/blocs/.gitkeep b/lib/presentation/blocs/.gitkeep new file mode 100644 index 0000000..d45bece --- /dev/null +++ b/lib/presentation/blocs/.gitkeep @@ -0,0 +1,6 @@ +# BLoC directory +# Create your BLoCs here for state management +# Each BLoC should have its own folder with: +# - bloc_name_bloc.dart +# - bloc_name_event.dart +# - bloc_name_state.dart diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..724bb2a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..d6b83f3 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - FlutterMacOS (1.0.0) + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + +PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index d4841f4..b3e2fe0 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 426126D67DBD3142EF2DC173 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59E8D21861D6167BC86AD219 /* Pods_RunnerTests.framework */; }; + A34F225E7247409903A905E4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAE82037DE8C61D063D492A5 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0CB18BBF478F344C686F435E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1947B83DB3093FD23FE0D59E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* coda_project.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "coda_project.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* coda_project.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = coda_project.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +80,14 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 43EA9FAFB40865A263C1DE07 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 59E8D21861D6167BC86AD219 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + DAADAB5CEB9A1FD331BC72CA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F95332891AC6CB1046F7CE26 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + FAE82037DE8C61D063D492A5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FDD61E81F4A4D672FA555791 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 426126D67DBD3142EF2DC173 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A34F225E7247409903A905E4 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 90884EA0D598E32A2E874D7C /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 90884EA0D598E32A2E874D7C /* Pods */ = { + isa = PBXGroup; + children = ( + DAADAB5CEB9A1FD331BC72CA /* Pods-Runner.debug.xcconfig */, + 0CB18BBF478F344C686F435E /* Pods-Runner.release.xcconfig */, + F95332891AC6CB1046F7CE26 /* Pods-Runner.profile.xcconfig */, + 43EA9FAFB40865A263C1DE07 /* Pods-RunnerTests.debug.xcconfig */, + FDD61E81F4A4D672FA555791 /* Pods-RunnerTests.release.xcconfig */, + 1947B83DB3093FD23FE0D59E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + FAE82037DE8C61D063D492A5 /* Pods_Runner.framework */, + 59E8D21861D6167BC86AD219 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + B5815815AFBCD5EB4EA925DE /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,6 +234,7 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 18A4EC124D3C458317CBA9CD /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, @@ -291,6 +322,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 18A4EC124D3C458317CBA9CD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -329,6 +382,28 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + B5815815AFBCD5EB4EA925DE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +455,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 43EA9FAFB40865A263C1DE07 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +470,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = FDD61E81F4A4D672FA555791 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +485,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1947B83DB3093FD23FE0D59E /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/pubspec.lock b/pubspec.lock index 4d34b0b..e309ec4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -145,6 +145,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -161,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -216,6 +256,14 @@ packages: description: flutter source: sdk version: "0.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" html: dependency: transitive description: @@ -312,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -328,6 +384,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -336,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -352,6 +440,62 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e + url: "https://pub.dev" + source: hosted + version: "2.4.13" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -477,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 54c33ad..547495d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,11 @@ dependencies: flutter: sdk: flutter flutter_svg: ^2.0.9 + dio: ^5.4.0 + get_it: ^7.6.4 + dartz: ^0.10.1 + equatable: ^2.0.5 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: From fa4bee477122c2294deaacffe69f559be6390c91 Mon Sep 17 00:00:00 2001 From: Mohammed Al-Samarraie Date: Tue, 13 Jan 2026 13:07:31 +0300 Subject: [PATCH 2/2] 1111 --- devtools_options.yaml | 3 + ios/Podfile.lock | 7 + ios/Runner.xcodeproj/project.pbxproj | 4 +- lib/LOGIN_SETUP.md | 113 ++++++++++++++++ lib/core/di/injection_container.dart | 42 +++--- lib/core/network/api_client.dart | 21 ++- .../datasources/auth_remote_data_source.dart | 77 +++++++++++ lib/data/dto/login_dto.dart | 16 +++ lib/data/dto/login_response_dto.dart | 85 ++++++++++++ .../repositories/auth_repository_impl.dart | 65 ++++++++++ lib/domain/models/login_request.dart | 9 ++ lib/domain/models/login_response_model.dart | 37 ++++++ lib/domain/repositories/auth_repository.dart | 8 ++ lib/domain/usecases/login_usecase.dart | 15 +++ lib/widgets/auth_form.dart | 121 ++++++++++++++---- 15 files changed, 578 insertions(+), 45 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/LOGIN_SETUP.md create mode 100644 lib/data/datasources/auth_remote_data_source.dart create mode 100644 lib/data/dto/login_dto.dart create mode 100644 lib/data/dto/login_response_dto.dart create mode 100644 lib/data/repositories/auth_repository_impl.dart create mode 100644 lib/domain/models/login_request.dart create mode 100644 lib/domain/models/login_response_model.dart create mode 100644 lib/domain/repositories/auth_repository.dart create mode 100644 lib/domain/usecases/login_usecase.dart 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,