diff --git a/.metadata b/.metadata index 83b34eb..3bfa89d 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407" + revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - platform: android - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - platform: ios - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - platform: linux - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - platform: macos - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 - platform: web - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - - platform: windows - create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 - base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407 + create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 + base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46 # User provided section diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5c595fb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "Forgetpassword", + "Onboardingscreen", + "Resetpassword" + ] +} \ No newline at end of file diff --git a/json_output.txt b/json_output.txt new file mode 100644 index 0000000..e69de29 diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index c3c9029..518f5f8 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -25,17 +25,29 @@ import '../../../features/auth/domain/repos/auth_repo.dart' as _i712; import '../../../features/auth/domain/usecase/apply_usecase.dart' as _i412; import '../../../features/auth/domain/usecase/change_password_usecase.dart' as _i991; +import '../../../features/auth/domain/usecase/forgetpassword_usecase.dart' + as _i769; import '../../../features/auth/domain/usecase/get_all_vehicles_usecase.dart' as _i1015; import '../../../features/auth/domain/usecase/get_countries_usecase.dart' as _i940; import '../../../features/auth/domain/usecase/login_usecase.dart' as _i75; +import '../../../features/auth/domain/usecase/resertpassword_usecase.dart' + as _i294; +import '../../../features/auth/domain/usecase/verifyreaset_usecase.dart' + as _i112; import '../../../features/auth/presentation/apply/manager/apply_cubit.dart' as _i377; +import '../../../features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart' + as _i614; import '../../../features/auth/presentation/login/manager/login_cubit.dart' as _i810; import '../../../features/auth/presentation/reset_password/manager/change_password_cubit.dart' as _i14; +import '../../../features/auth/presentation/reset_password/manager/reset_password_cubit.dart' + as _i378; +import '../../../features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart' + as _i466; import '../../core/api_manger/api_client.dart' as _i890; import '../auth_storage/auth_storage.dart' as _i603; import '../network/network_module.dart' as _i200; @@ -63,19 +75,46 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i708.AuthRemoteDataSource>( () => _i777.AuthRemoteDataSourceImpl(gh<_i890.ApiClient>())); gh.factory<_i712.AuthRepo>( - () => _i566.AuthRepoImp(gh<_i708.AuthRemoteDataSource>())); + () => _i566.AuthRepoImpl(gh<_i708.AuthRemoteDataSource>())); + gh.factory<_i769.ForgetPasswordUsecase>( + () => _i769.ForgetPasswordUsecase(gh<_i712.AuthRepo>())); + gh.factory<_i294.ResetPasswordUsecase>( + () => _i294.ResetPasswordUsecase(gh<_i712.AuthRepo>())); + gh.factory<_i112.VerifyResetCodeUsecase>( + () => _i112.VerifyResetCodeUsecase(gh<_i712.AuthRepo>())); gh.factory<_i991.ChangePasswordUsecase>( () => _i991.ChangePasswordUsecase(gh<_i712.AuthRepo>())); - gh.lazySingleton<_i1015.GetAllVehiclesUseCase>( - () => _i1015.GetAllVehiclesUseCase(gh<_i712.AuthRepo>())); + gh.factoryParam<_i466.VerifyResetCodeCubit, String, dynamic>(( + email, + _, + ) => + _i466.VerifyResetCodeCubit( + gh<_i112.VerifyResetCodeUsecase>(), + gh<_i769.ForgetPasswordUsecase>(), + email, + )); + gh.factoryParam<_i378.ResetPasswordCubit, String, dynamic>(( + email, + _, + ) => + _i378.ResetPasswordCubit( + email, + gh<_i294.ResetPasswordUsecase>(), + )); gh.lazySingleton<_i412.ApplyUseCase>( () => _i412.ApplyUseCase(gh<_i712.AuthRepo>())); + gh.lazySingleton<_i1015.GetAllVehiclesUseCase>( + () => _i1015.GetAllVehiclesUseCase(gh<_i712.AuthRepo>())); gh.factory<_i940.GetCountriesUseCase>( () => _i940.GetCountriesUseCase(gh<_i712.AuthRepo>())); gh.factory<_i75.LoginUseCase>( () => _i75.LoginUseCase(gh<_i712.AuthRepo>())); gh.factory<_i14.ChangePasswordCubit>( () => _i14.ChangePasswordCubit(gh<_i991.ChangePasswordUsecase>())); + gh.factory<_i614.ForgetPasswordCubit>(() => _i614.ForgetPasswordCubit( + gh<_i769.ForgetPasswordUsecase>(), + gh<_i603.AuthStorage>(), + )); gh.factory<_i377.ApplyCubit>(() => _i377.ApplyCubit( gh<_i940.GetCountriesUseCase>(), gh<_i1015.GetAllVehiclesUseCase>(), diff --git a/lib/app/core/api_manger/api_client.dart b/lib/app/core/api_manger/api_client.dart index d4013b7..242585e 100644 --- a/lib/app/core/api_manger/api_client.dart +++ b/lib/app/core/api_manger/api_client.dart @@ -8,6 +8,12 @@ import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart' import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; import 'package:dio/dio.dart' hide Headers; import 'package:retrofit/retrofit.dart'; +import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/verifyreset_request.dart'; +import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; import '../../../features/auth/data/models/response/apply_response_model.dart'; import '../../../features/auth/data/models/request/apply_request_model.dart'; import '../../../features/auth/data/models/response/vehicles_response_model.dart'; @@ -15,10 +21,22 @@ import '../values/app_endpoint_strings.dart'; part 'api_client.g.dart'; -@RestApi() +@RestApi(baseUrl: AppEndpointString.baseUrl) abstract class ApiClient { factory ApiClient(Dio dio) = _ApiClient; + @POST(AppEndpointString.sendEmail) + Future> forgetPassword( + @Body() ForgetPasswordRequest request, + ); + @PUT(AppEndpointString.resetPassword) + Future> resetPassword( + @Body() ResetPasswordRequest request, + ); + @POST(AppEndpointString.verifyResetCode) + Future> verifyResetCode( + @Body() VerifyResetRequest request, + ); @PATCH(AppEndpointString.changePassword) Future> changePassword( @Body() Map body, diff --git a/lib/app/core/api_manger/api_client.g.dart b/lib/app/core/api_manger/api_client.g.dart index d5befe1..5f8bd72 100644 --- a/lib/app/core/api_manger/api_client.g.dart +++ b/lib/app/core/api_manger/api_client.g.dart @@ -12,12 +12,104 @@ class _ApiClient implements ApiClient { _ApiClient( this._dio, { this.baseUrl, - }); + }) { + baseUrl ??= 'https://flower.elevateegy.com/api/v1/'; + } final Dio _dio; String? baseUrl; + @override + Future> forgetPassword( + ForgetPasswordRequest request) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(request.toJson()); + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + 'drivers/forgotPassword', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ForgetpasswordResponse.fromJson(_result.data!); + final httpResponse = HttpResponse(value, _result); + return httpResponse; + } + + @override + Future> resetPassword( + ResetPasswordRequest request) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(request.toJson()); + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'PUT', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + 'drivers/resetPassword', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ResetpasswordResponse.fromJson(_result.data!); + final httpResponse = HttpResponse(value, _result); + return httpResponse; + } + + @override + Future> verifyResetCode( + VerifyResetRequest request) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(request.toJson()); + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + 'drivers/verifyResetCode', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = VerifyresetResponse.fromJson(_result.data!); + final httpResponse = HttpResponse(value, _result); + return httpResponse; + } + @override Future> changePassword( Map body) async { diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index 65aa77e..a56daf9 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -1,14 +1,21 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/di/di.dart'; import 'package:tracking_app/app/core/router/route_names.dart'; -import 'package:tracking_app/features/auth/presentation/reset_password/pages/change_password_page.dart'; import 'package:tracking_app/features/Onboarding/presentation/pages/onboardingScreen.dart'; import 'package:tracking_app/features/app_sections/presentation/pages/app_sections.dart'; +import 'package:tracking_app/features/auth/presentation/apply/view/apply_view.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/pages/forget_pass_page.dart'; import 'package:tracking_app/features/auth/presentation/login/pages/loginScreen.dart'; -import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; -import 'package:tracking_app/app/config/di/di.dart'; +import 'package:tracking_app/features/auth/presentation/reset_password/manager/reset_password_cubit.dart'; +import 'package:tracking_app/features/auth/presentation/reset_password/pages/change_password_page.dart'; +import 'package:tracking_app/features/auth/presentation/reset_password/pages/reset_password.dart'; +import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart'; +import 'package:tracking_app/features/auth/presentation/verify_reset/pages/verify_reset_page.dart'; import 'package:tracking_app/features/profile/presentation/pages/profile_page.dart'; -import 'package:tracking_app/app/core/router/route_names.dart'; -import 'package:tracking_app/features/auth/presentation/apply/view/apply_view.dart'; + final GoRouter appRouter = GoRouter( initialLocation: RouteNames.onboarding, @@ -38,6 +45,36 @@ final GoRouter appRouter = GoRouter( path: RouteNames.applyScreen, builder: (context, state) => const ApplyScreen(), ), + GoRoute( + path: RouteNames.verifyResetCode, + builder: (context, state) { + final email = state.extra as String; + + return BlocProvider( + create: (_) => getIt(param1: email), + child: VerifyResetCodePage(email: email), + ); + }, + ), + GoRoute( + path: RouteNames.forgetPassword, + builder: (context, state) => BlocProvider( + create: (_) => getIt(), + child: const ForgetPasswordPage(), + ), + ), + + GoRoute( + path: RouteNames.resetPassword, + builder: (context, state) => BlocProvider( + create: (_) => getIt(param1: state.extra as String), + child: const ResetPasswordPage(), + ), + ), + GoRoute( + path: RouteNames.profile, + builder: (context, state) => const ProfilePage(), + ), ], redirect: (context, state) async { final token = await getIt().getToken(); @@ -53,5 +90,3 @@ final GoRouter appRouter = GoRouter( return null; }, ); - - diff --git a/lib/app/core/router/route_names.dart b/lib/app/core/router/route_names.dart index bdaa721..118b723 100644 --- a/lib/app/core/router/route_names.dart +++ b/lib/app/core/router/route_names.dart @@ -1,12 +1,13 @@ abstract class RouteNames { static const signup = '/signup'; static const login = '/login'; + static const forgetPassword = '/forget-password'; + static const verifyResetCode = '/verify-reset-code'; + static const resetPassword = '/reset-password'; + static const home = '/home'; + static const profile = '/profile'; static const appStart = '/appStart'; static const changePassword = '/changePassword'; - static const profile = '/profile'; static const applyScreen = '/applyScreen'; static const onboarding = '/onboarding'; - - - } diff --git a/lib/app/core/values/app_endpoint_strings.dart b/lib/app/core/values/app_endpoint_strings.dart index b1b8663..3feb889 100644 --- a/lib/app/core/values/app_endpoint_strings.dart +++ b/lib/app/core/values/app_endpoint_strings.dart @@ -1,9 +1,9 @@ class AppEndpointString { static const String baseUrl = 'https://flower.elevateegy.com/api/v1/'; static const String loginEndpoint = 'auth/signin'; - static const String sendEmail = 'auth/forgotPassword'; - static const String verifyResetCode = 'auth/verifyResetCode'; - static const String resetPassword = 'auth/resetPassword'; + static const String sendEmail = 'drivers/forgotPassword'; + static const String verifyResetCode = 'drivers/verifyResetCode'; + static const String resetPassword = 'drivers/resetPassword'; static const String profileData = 'auth/profile-data'; static const String uploadPhoto = 'auth/upload-photo'; diff --git a/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart b/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart index 42c04ab..de20012 100644 --- a/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart +++ b/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart @@ -1,38 +1,56 @@ import 'dart:convert'; -import 'package:dio/dio.dart'; -import 'package:flutter/services.dart'; import 'package:injectable/injectable.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:dio/dio.dart'; +import 'package:dio/src/form_data.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; -import 'package:tracking_app/features/auth/data/models/response/country_model.dart'; -import 'package:tracking_app/features/auth/data/models/response/vehicles_response_model.dart'; +import 'package:tracking_app/app/core/network/safe_api_call.dart'; +import 'package:tracking_app/features/auth/data/datasource/auth_remote_datasource.dart'; +import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; +import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; +import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/verifyreset_request.dart'; import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; +import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; +import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; import 'package:tracking_app/features/auth/data/models/response/apply_response_model.dart'; - -import '../../../../app/core/api_manger/api_client.dart'; -import '../../../../app/core/network/safe_api_call.dart'; -import '../../data/datasource/auth_remote_datasource.dart'; -import '../../data/model/request/LoginRequest.dart'; -import '../../data/model/response/LoginResponse.dart'; -import '../../data/model/response/change_password_dto.dart'; +import 'package:tracking_app/features/auth/data/models/response/vehicles_response_model.dart'; +import 'package:tracking_app/features/auth/data/models/response/country_model.dart'; @Injectable(as: AuthRemoteDataSource) class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { - ApiClient apiClient; + final ApiClient apiClient; + AuthRemoteDataSourceImpl(this.apiClient); + + @override - Future> changePassword({ - String? password, - String? newPassword, - }) { - return safeApiCall( - call: () => apiClient.changePassword({ - "password": password, - "newPassword": newPassword, - }), - ); + Future> forgetPassword( + ForgetPasswordRequest request) { + return safeApiCall(call: () => apiClient.forgetPassword(request)); + } + + + @override + Future> verifyResetCode( + VerifyResetRequest request) { + return safeApiCall(call: () => apiClient.verifyResetCode(request)); } + + + @override + Future> resetPassword( + ResetPasswordRequest request) { + return safeApiCall(call: () => apiClient.resetPassword(request)); + } + + @override - Future?> login(LoginRequest loginRequest) async { + Future> login(LoginRequest loginRequest) async { try { final response = await apiClient.login(loginRequest); return SuccessApiResult(data: response); @@ -40,10 +58,9 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { String errorMessage = 'unknownError'; if (e.response?.statusCode == 401) { errorMessage = 'wrongEmailOrPassword'; - } else if (e.response != null && e.response?.data != null) { + } else if (e.response?.data != null) { if (e.response!.data is Map) { - errorMessage = - e.response!.data['message'] ?? e.message ?? 'unknownError'; + errorMessage = e.response!.data['message'] ?? e.message ?? 'unknownError'; } else { errorMessage = e.message ?? 'unknownError'; } @@ -55,15 +72,30 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { return ErrorApiResult(error: e.toString()); } } + + @override + Future> changePassword({ + String? password, + String? newPassword, + }) { + return safeApiCall( + call: () => apiClient.changePassword({ + "password": password, + "newPassword": newPassword, + }), + ); + } + + @override Future> getAllVehicle() { - return safeApiCall(call: () => apiClient.getAllVehicle()); + return safeApiCall(call: () => apiClient.getAllVehicle()); } + @override Future> apply( - ApplyRequestModel applyRequestModel, - ) { + ApplyRequestModel applyRequestModel) { return safeApiCall( call: () async { final formData = FormData.fromMap({ @@ -86,9 +118,7 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { "vehicleLicense", await MultipartFile.fromFile( applyRequestModel.vehicleLicense!.path, - filename: applyRequestModel.vehicleLicense!.path - .split('/') - .last, + filename: applyRequestModel.vehicleLicense!.path.split('/').last, ), ), ); @@ -111,11 +141,10 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { ); } + @override Future> getCountries() async { - final String response = await rootBundle.loadString( - 'assets/data/country.json', - ); + final String response = await rootBundle.loadString('assets/data/country.json'); final List data = json.decode(response); return data.map((json) => CountryModel.fromJson(json)).toList(); } diff --git a/lib/features/auth/data/datasource/auth_remote_datasource.dart b/lib/features/auth/data/datasource/auth_remote_datasource.dart index 6a6123c..4fc099c 100644 --- a/lib/features/auth/data/datasource/auth_remote_datasource.dart +++ b/lib/features/auth/data/datasource/auth_remote_datasource.dart @@ -1,3 +1,10 @@ +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/verifyreset_request.dart'; +import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; import '../../../../app/core/network/api_result.dart'; import '../models/response/country_model.dart'; import '../models/response/vehicles_response_model.dart'; @@ -21,5 +28,14 @@ abstract class AuthRemoteDataSource { String? password, String? newPassword, }); + Future?> forgetPassword( + ForgetPasswordRequest request, + ); + Future?> verifyResetCode( + VerifyResetRequest request, + ); + Future?> resetPassword( + ResetPasswordRequest request, + ); } diff --git a/lib/features/auth/data/models/request/forget_password_request.dart b/lib/features/auth/data/models/request/forget_password_request.dart new file mode 100644 index 0000000..4d62891 --- /dev/null +++ b/lib/features/auth/data/models/request/forget_password_request.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'forget_password_request.g.dart'; + +@JsonSerializable() +class ForgetPasswordRequest { + final String email; + ForgetPasswordRequest({required this.email}); + factory ForgetPasswordRequest.fromJson(Map json) => + _$ForgetPasswordRequestFromJson(json); + Map toJson() => _$ForgetPasswordRequestToJson(this); +} diff --git a/lib/features/auth/data/models/request/resetpassword_request.dart b/lib/features/auth/data/models/request/resetpassword_request.dart new file mode 100644 index 0000000..acbe38c --- /dev/null +++ b/lib/features/auth/data/models/request/resetpassword_request.dart @@ -0,0 +1,12 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'resetpassword_request.g.dart'; + +@JsonSerializable() +class ResetPasswordRequest { + final String email; + final String newPassword; + ResetPasswordRequest({required this.email, required this.newPassword}); + factory ResetPasswordRequest.fromJson(Map json) => + _$ResetPasswordRequestFromJson(json); + Map toJson() => _$ResetPasswordRequestToJson(this); +} diff --git a/lib/features/auth/data/models/request/verifyreset_request.dart b/lib/features/auth/data/models/request/verifyreset_request.dart new file mode 100644 index 0000000..0111df8 --- /dev/null +++ b/lib/features/auth/data/models/request/verifyreset_request.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'verifyreset_request.g.dart'; + +@JsonSerializable() +class VerifyResetRequest { + final String resetCode; + VerifyResetRequest({required this.resetCode}); + factory VerifyResetRequest.fromJson(Map json) => + _$VerifyResetRequestFromJson(json); + Map toJson() => _$VerifyResetRequestToJson(this); +} diff --git a/lib/features/auth/data/models/response/forgetpassword_response.dart b/lib/features/auth/data/models/response/forgetpassword_response.dart new file mode 100644 index 0000000..50e6628 --- /dev/null +++ b/lib/features/auth/data/models/response/forgetpassword_response.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'forgetpassword_response.g.dart'; + +@JsonSerializable() +class ForgetpasswordResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "info") + final String? info; + + ForgetpasswordResponse({this.message, this.info}); + + ForgetpasswordResponse copyWith({String? message, String? info}) => + ForgetpasswordResponse( + message: message ?? this.message, + info: info ?? this.info, + ); + + factory ForgetpasswordResponse.fromJson(Map json) => + _$ForgetpasswordResponseFromJson(json); + + Map toJson() => _$ForgetpasswordResponseToJson(this); +} diff --git a/lib/features/auth/data/models/response/resetpassword_response.dart b/lib/features/auth/data/models/response/resetpassword_response.dart new file mode 100644 index 0000000..0f02da4 --- /dev/null +++ b/lib/features/auth/data/models/response/resetpassword_response.dart @@ -0,0 +1,29 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'resetpassword_response.g.dart'; + +@JsonSerializable() +class ResetpasswordResponse { + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "token") + final String? token; + + ResetpasswordResponse({ + this.message, + this.token, + }); + + ResetpasswordResponse copyWith({ + String? message, + String? token, + }) => + ResetpasswordResponse( + message: message ?? this.message, + token: token ?? this.token, + ); + + factory ResetpasswordResponse.fromJson(Map json) => _$ResetpasswordResponseFromJson(json); + + Map toJson() => _$ResetpasswordResponseToJson(this); +} diff --git a/lib/features/auth/data/models/response/verifyreset_response.dart b/lib/features/auth/data/models/response/verifyreset_response.dart new file mode 100644 index 0000000..5558a51 --- /dev/null +++ b/lib/features/auth/data/models/response/verifyreset_response.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'verifyreset_response.g.dart'; + +@JsonSerializable() +class VerifyresetResponse { + @JsonKey(name: "status") + final String? status; + + VerifyresetResponse({ + this.status, + }); + + VerifyresetResponse copyWith({ + String? status, + }) => + VerifyresetResponse( + status: status ?? this.status, + ); + + factory VerifyresetResponse.fromJson(Map json) => _$VerifyresetResponseFromJson(json); + + Map toJson() => _$VerifyresetResponseToJson(this); +} diff --git a/lib/features/auth/data/repos/auth_repo_impl.dart b/lib/features/auth/data/repos/auth_repo_impl.dart index 87d2bab..55d1ee2 100644 --- a/lib/features/auth/data/repos/auth_repo_impl.dart +++ b/lib/features/auth/data/repos/auth_repo_impl.dart @@ -1,68 +1,153 @@ import 'package:injectable/injectable.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; -import 'package:tracking_app/features/auth/data/models/response/vechicles_entity.dart'; -import 'package:tracking_app/features/auth/data/models/response/vehicles_response_model.dart'; -import 'package:tracking_app/features/auth/domain/entities/country_entity.dart'; +import 'package:tracking_app/features/auth/data/datasource/auth_remote_datasource.dart'; +import 'package:tracking_app/features/auth/data/mapper/vehicles_mapper.dart'; +import 'package:tracking_app/features/auth/data/mappers/change_password_dto_mapper.dart'; +import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; +import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; +import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; +import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/verifyreset_request.dart'; import 'package:tracking_app/features/auth/data/models/response/apply_response_model.dart'; - -import '../../domain/models/change_password_model.dart'; -import '../../domain/repos/auth_repo.dart'; -import '../datasource/auth_remote_datasource.dart'; -import '../mapper/vehicles_mapper.dart'; -import '../mappers/change_password_dto_mapper.dart'; -import '../model/request/LoginRequest.dart'; -import '../model/response/LoginResponse.dart'; -import '../model/response/change_password_dto.dart'; -import '../models/response/vehicle_model.dart'; +import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/vehicle_model.dart'; +import 'package:tracking_app/features/auth/data/models/response/vehicles_response_model.dart'; +import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; +import 'package:tracking_app/features/auth/domain/entities/country_entity.dart'; +import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.dart'; +import 'package:tracking_app/features/auth/domain/models/verifyreset_entity.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; @Injectable(as: AuthRepo) -class AuthRepoImp implements AuthRepo { +class AuthRepoImpl implements AuthRepo { final AuthRemoteDataSource authDatasource; - AuthRepoImp(this.authDatasource); + + AuthRepoImpl(this.authDatasource); + + @override + Future> forgetPassword(String email) async { + final result = await authDatasource.forgetPassword( + ForgetPasswordRequest(email: email), + ); + + if (result is SuccessApiResult) { + return SuccessApiResult( + data: ForgetPasswordEntitiy( + message: result.data.message, + info: result.data.info, + ), + ); + } + + if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } + + return ErrorApiResult(error: 'Unexpected error'); + } + + @override + Future> verifyResetCode(String code) async { + final result = await authDatasource.verifyResetCode( + VerifyResetRequest(resetCode: code), + ); + + if (result is SuccessApiResult) { + return SuccessApiResult( + data: VerifyResetCodeEntity(status: result.data.status), + ); + } + + if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } + + return ErrorApiResult(error: 'Unexpected error'); + } + + + @override + Future> resetPassword( + ResetPasswordRequest request) async { + final result = await authDatasource.resetPassword(request); + + if (result is SuccessApiResult) { + return SuccessApiResult( + data: ResetPasswordEntity( + token: result.data.token, + message: result.data.message, + ), + ); + } + + if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } + + return ErrorApiResult(error: 'Unexpected error'); + } + + @override Future> login(String email, String password) async { final loginRequest = LoginRequest(email: email, password: password); final result = await authDatasource.login(loginRequest); + if (result is SuccessApiResult) { - return SuccessApiResult(data: result.data); + return SuccessApiResult(data: result.data); } + if (result is ErrorApiResult) { - return ErrorApiResult(error: result.error); + return ErrorApiResult(error: result.error); } - return ErrorApiResult(error: 'Unknown error'); + + return ErrorApiResult(error: 'Unknown error'); } + + @override Future> changePassword({ String? password, String? newPassword, }) async { - ApiResult response = await authDatasource.changePassword( + final response = await authDatasource.changePassword( password: password, newPassword: newPassword, ); - switch (response) { - case SuccessApiResult(): - ChangePasswordDto dto = response.data; - ChangePasswordModel changePassModel = dto.toChangePassModel(); - return SuccessApiResult(data: changePassModel); - case ErrorApiResult(): - return ErrorApiResult(error: response.error); + + if (response is SuccessApiResult) { + final dto = response.data; + return SuccessApiResult(data: dto.toChangePassModel()); + } + + if (response is ErrorApiResult) { + return ErrorApiResult(error: response.error); } + + return ErrorApiResult(error: 'Unknown error'); } + + @override Future>> getAllVehicles() async { final result = await authDatasource.getAllVehicle(); - switch (result) { - case SuccessApiResult(): - return SuccessApiResult( - data: result.data.vehicles.map((v) { - return v.toVehicleType(); - }).toList(), - ); - case ErrorApiResult(): - return ErrorApiResult(error: result.error); + + if (result is SuccessApiResult) { + return SuccessApiResult( + data: result.data.vehicles.map((v) => v.toVehicleType()).toList(), + ); + } + + if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); } + + return ErrorApiResult(error: 'Unknown error'); } @override @@ -75,47 +160,20 @@ class AuthRepoImp implements AuthRepo { } } + @override Future> apply( - ApplyRequestModel applyRequestModel, - ) async { - final result = await authDatasource.apply(applyRequestModel); - switch (result) { - case SuccessApiResult(): - return SuccessApiResult(data: result.data); - case ErrorApiResult(): - return ErrorApiResult(error: result.error); + ApplyRequestModel request) async { + final result = await authDatasource.apply(request); + + if (result is SuccessApiResult) { + return SuccessApiResult(data: result.data); } - } - // @override - // Future> signup({ - // String? firstName, - // String? lastName, - // String? email, - // String? password, - // String? rePassword, - // String? phone, - // String? gender, - // }) async { - // ApiResult signupResponse = await authDatasource.signUp( - // firstName: firstName, - // lastName: lastName, - // email: email, - // password: password, - // rePassword: rePassword, - // phone: phone, - // gender: gender, - // ); - // switch (signupResponse) { - // case SuccessApiResult(): - // SignupDto dto = signupResponse.data; - // SignupModel signupModel = dto.toSignupModel(); - // return SuccessApiResult(data: signupModel); - // case ErrorApiResult(): - // return ErrorApiResult( - // error: signupResponse.error.toString(), - // ); - // } - // } + if (result is ErrorApiResult) { + return ErrorApiResult(error: result.error); + } + + return ErrorApiResult(error: 'Unknown error'); + } } diff --git a/lib/features/auth/domain/models/forgetpassword_entitiy.dart b/lib/features/auth/domain/models/forgetpassword_entitiy.dart new file mode 100644 index 0000000..73a57ae --- /dev/null +++ b/lib/features/auth/domain/models/forgetpassword_entitiy.dart @@ -0,0 +1,6 @@ +class ForgetPasswordEntitiy { + final String? message; + final String? info; + + ForgetPasswordEntitiy({required this.message, required this.info}); +} diff --git a/lib/features/auth/domain/models/resetpassword_entity.dart b/lib/features/auth/domain/models/resetpassword_entity.dart new file mode 100644 index 0000000..f48719c --- /dev/null +++ b/lib/features/auth/domain/models/resetpassword_entity.dart @@ -0,0 +1,6 @@ +class ResetPasswordEntity { + final String? message; + final String? token; + + const ResetPasswordEntity({required this.message, this.token,}); +} diff --git a/lib/features/auth/domain/models/verifyreset_entity.dart b/lib/features/auth/domain/models/verifyreset_entity.dart new file mode 100644 index 0000000..2a9dd14 --- /dev/null +++ b/lib/features/auth/domain/models/verifyreset_entity.dart @@ -0,0 +1,5 @@ +class VerifyResetCodeEntity { + final String? status; + + VerifyResetCodeEntity({required this.status}); +} diff --git a/lib/features/auth/domain/repos/auth_repo.dart b/lib/features/auth/domain/repos/auth_repo.dart index f83d963..2a4338c 100644 --- a/lib/features/auth/domain/repos/auth_repo.dart +++ b/lib/features/auth/domain/repos/auth_repo.dart @@ -1,16 +1,23 @@ -import 'package:tracking_app/features/auth/data/models/response/vechicles_entity.dart'; -import 'package:tracking_app/features/auth/data/models/response/vehicles_response_model.dart'; -import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; -import 'package:tracking_app/features/auth/data/models/response/apply_response_model.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; +import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/response/apply_response_model.dart'; +import 'package:tracking_app/features/auth/data/models/response/vehicle_model.dart'; +import 'package:tracking_app/features/auth/domain/entities/country_entity.dart'; import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; -import '../../../../app/core/network/api_result.dart'; -import '../../data/models/response/vehicle_model.dart'; -import '../entities/country_entity.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.dart'; +import 'package:tracking_app/features/auth/domain/models/verifyreset_entity.dart'; abstract class AuthRepo { - Future>> getAllVehicles(); + Future> forgetPassword(String email); + Future> verifyResetCode(String code); + Future> resetPassword( + ResetPasswordRequest request, + ); + + Future>> getAllVehicles(); Future>> getCountries(); Future> apply( ApplyRequestModel applyRequestModel, @@ -22,3 +29,4 @@ abstract class AuthRepo { String? newPassword, }); } + diff --git a/lib/features/auth/domain/usecase/forgetpassword_usecase.dart b/lib/features/auth/domain/usecase/forgetpassword_usecase.dart new file mode 100644 index 0000000..87117b0 --- /dev/null +++ b/lib/features/auth/domain/usecase/forgetpassword_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; + +@injectable +class ForgetPasswordUsecase { + AuthRepo authRepo; + ForgetPasswordUsecase(this.authRepo); + Future> call(String email) { + return authRepo.forgetPassword(email); + } +} diff --git a/lib/features/auth/domain/usecase/resertpassword_usecase.dart b/lib/features/auth/domain/usecase/resertpassword_usecase.dart new file mode 100644 index 0000000..38a48cf --- /dev/null +++ b/lib/features/auth/domain/usecase/resertpassword_usecase.dart @@ -0,0 +1,14 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; + +@injectable +class ResetPasswordUsecase { + AuthRepo authRepo; + ResetPasswordUsecase(this.authRepo); + Future> call(ResetPasswordRequest request){ + return authRepo.resetPassword(request); + } +} diff --git a/lib/features/auth/domain/usecase/verifyreaset_usecase.dart b/lib/features/auth/domain/usecase/verifyreaset_usecase.dart new file mode 100644 index 0000000..5d3864e --- /dev/null +++ b/lib/features/auth/domain/usecase/verifyreaset_usecase.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/domain/models/verifyreset_entity.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; + +@injectable +class VerifyResetCodeUsecase { + AuthRepo authRepo; + VerifyResetCodeUsecase(this.authRepo); + Future >call(String code){ + return authRepo.verifyResetCode(code); + } +} diff --git a/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart b/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart new file mode 100644 index 0000000..25a51e4 --- /dev/null +++ b/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart @@ -0,0 +1,63 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/features/auth/domain/usecase/forgetpassword_usecase.dart'; + +part 'forget_pass_state.dart'; +part 'forget_pass_intents.dart'; + +@injectable +class ForgetPasswordCubit extends Cubit { + final AuthStorage _authStorage; + final ForgetPasswordUsecase _ForgetPasswordUsecase; + + ForgetPasswordCubit(this._ForgetPasswordUsecase, this._authStorage) + : super(ForgetPasswordState.initial()); + + final formKey = GlobalKey(); + final emailController = TextEditingController(); + + Future doIntent(ForgetPasswordIntents intent) async { + switch (intent) { + case FormChangedIntent(): + _validateForm(); + break; + case SubmitForgetPasswordIntent(): + _submitForgetPassword(); + break; + } + } + + void _validateForm() { + final isEmailFilled = emailController.text.trim().isNotEmpty; + emit(state.copyWith(isFormValid: isEmailFilled)); + } + + Future _submitForgetPassword() async { + // final isValid = formKey.currentState?.validate() ?? false; + if (!state.isFormValid) return; + + emit(state.copyWith(resource: Resource.loading())); + + final result = await _ForgetPasswordUsecase(emailController.text.trim()); + + if (result is SuccessApiResult) { + emit(state.copyWith(resource: Resource.success(result.data))); + } else if (result is ErrorApiResult) { + emit(state.copyWith(resource: Resource.error(result.error))); + } else { + emit(state.copyWith(resource: Resource.error('Unexpected error'))); + } + } + + @override + Future close() { + emailController.dispose(); + return super.close(); + } +} diff --git a/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_intents.dart b/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_intents.dart new file mode 100644 index 0000000..311360f --- /dev/null +++ b/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_intents.dart @@ -0,0 +1,13 @@ +part of 'forget_pass_cubit.dart'; + +sealed class ForgetPasswordIntents { + const ForgetPasswordIntents(); +} + +class FormChangedIntent extends ForgetPasswordIntents { + const FormChangedIntent(); +} + +class SubmitForgetPasswordIntent extends ForgetPasswordIntents { + const SubmitForgetPasswordIntent(); +} diff --git a/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_state.dart b/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_state.dart new file mode 100644 index 0000000..55b0958 --- /dev/null +++ b/lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_state.dart @@ -0,0 +1,27 @@ +part of 'forget_pass_cubit.dart'; + +class ForgetPasswordState extends Equatable { + final Resource resource; + final bool isFormValid; + + const ForgetPasswordState({ + required this.resource, + required this.isFormValid, + }); + + factory ForgetPasswordState.initial() => + ForgetPasswordState(resource: Resource.initial(), isFormValid: false); + + ForgetPasswordState copyWith({ + Resource? resource, + bool? isFormValid, + }) { + return ForgetPasswordState( + resource: resource ?? this.resource, + isFormValid: isFormValid ?? this.isFormValid, + ); + } + + @override + List get props => [resource, isFormValid]; +} diff --git a/lib/features/auth/presentation/forget_pass/pages/forget_pass_page.dart b/lib/features/auth/presentation/forget_pass/pages/forget_pass_page.dart new file mode 100644 index 0000000..d31c798 --- /dev/null +++ b/lib/features/auth/presentation/forget_pass/pages/forget_pass_page.dart @@ -0,0 +1,24 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart'; +import '../../../../../../../generated/locale_keys.g.dart'; + +class ForgetPasswordPage extends StatelessWidget { + const ForgetPasswordPage({super.key}); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: Text(LocaleKeys.password.tr()), + leading: IconButton( + icon: Icon(Icons.arrow_back_ios_new), + onPressed: () => context.go(RouteNames.onboarding), + ), + ), + body: ForgetPasswordForm(), + ); + } +} diff --git a/lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart b/lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart new file mode 100644 index 0000000..210ea31 --- /dev/null +++ b/lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart @@ -0,0 +1,89 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; +import '../../../../../../../generated/locale_keys.g.dart'; +import '../../../../../../app/config/base_state/base_state.dart'; +import '../../../../../../app/core/router/route_names.dart'; +import '../../../../../../app/core/utils/validators_helper.dart'; +import '../../../../../../app/core/widgets/custom_button.dart'; +import '../../../../../../app/core/widgets/custom_text_form_field.dart'; +import '../../../../../../app/core/widgets/show_app_dialog.dart'; +import '../../../../../../app/core/widgets/show_snak_bar.dart'; + +class ForgetPasswordForm extends StatelessWidget { + const ForgetPasswordForm({super.key}); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listenWhen: (previous, current) => + previous.resource.status != current.resource.status, + listener: (context, state) { + final email = context + .read() + .emailController + .text + .trim(); + if (state.resource.status == Status.success) { + showAppSnackbar( + context, + LocaleKeys.check_email_for_verification_code.tr(), + ); + context.push(RouteNames.verifyResetCode, extra: email); + } + + if (state.resource.status == Status.error) { + showAppDialog( + context, + message: state.resource.error ?? LocaleKeys.an_error_occurred.tr(), + isError: true, + ); + } + }, + builder: (context, state) { + final cubit = context.read(); + + return Form( + key: cubit.formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + const SizedBox(height: 30), + Text( + LocaleKeys.forgotPassword.tr(), + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 16), + Text( + LocaleKeys.associatedEmail.tr(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 30), + CustomTextFormField( + controller: cubit.emailController, + label: LocaleKeys.email.tr(), + hint: LocaleKeys.enterEmail.tr(), + validator: Validators.validateEmail, + onChanged: (_) => cubit.doIntent(const FormChangedIntent()), + ), + const SizedBox(height: 40), + CustomButton( + + isEnabled: state.isFormValid, + isLoading: state.resource.status == Status.loading, + text: LocaleKeys.continueTxt.tr(), + onPressed: () => + cubit.doIntent(const SubmitForgetPasswordIntent()), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/auth/presentation/login/widgets/loginScreenBody.dart b/lib/features/auth/presentation/login/widgets/loginScreenBody.dart index 14a1eb4..ac39e82 100644 --- a/lib/features/auth/presentation/login/widgets/loginScreenBody.dart +++ b/lib/features/auth/presentation/login/widgets/loginScreenBody.dart @@ -13,6 +13,7 @@ import 'package:tracking_app/app/core/widgets/show_snak_bar.dart'; import 'package:tracking_app/features/auth/presentation/login/manager/login_cubit.dart'; import 'package:tracking_app/features/auth/presentation/login/manager/login_intent.dart'; import 'package:tracking_app/features/auth/presentation/login/manager/login_states.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; class Loginscreenbody extends StatefulWidget { const Loginscreenbody({super.key}); @@ -61,7 +62,7 @@ class _LoginscreenbodyState extends State { ), onPressed: () => Navigator.of(context).pop(), ), - title: Text('login'.tr(), style: AppStyles.black24SemiBold), + title: Text(LocaleKeys.login.tr(), style: AppStyles.black24SemiBold), centerTitle: false, ), body: SafeArea( @@ -77,12 +78,12 @@ class _LoginscreenbodyState extends State { children: [ CustomTextFormField( controller: _emailController, - label: 'email'.tr(), - hint: 'enterEmail'.tr(), + label: LocaleKeys.email.tr(), + hint: LocaleKeys.enterEmail.tr(), keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || value.isEmpty) { - return 'emailRequired'.tr(); + return LocaleKeys.emailRequired.tr(); } return null; }, @@ -90,8 +91,8 @@ class _LoginscreenbodyState extends State { const SizedBox(height: 20), PasswordTextFormField( controller: _passwordController, - label: 'password'.tr(), - hint: 'enterPassword'.tr(), + label: LocaleKeys.password.tr(), + hint: LocaleKeys.enterPassword.tr(), isVisible: _isPasswordVisible, onToggleVisibility: () { setState(() { @@ -100,10 +101,10 @@ class _LoginscreenbodyState extends State { }, validator: (value) { if (value == null || value.isEmpty) { - return 'passwordRequired'.tr(); + return LocaleKeys.passwordRequired.tr(); } if (value.length < 6) { - return 'least6Characters'.tr(); + return LocaleKeys.least6Characters.tr(); } return null; }, @@ -124,17 +125,17 @@ class _LoginscreenbodyState extends State { }, ), Text( - 'rememberMe'.tr(), + LocaleKeys.rememberMe.tr(), style: AppStyles.font14Black, ), ], ), TextButton( onPressed: () { - // Navigate to Forget Password + context.go(RouteNames.forgetPassword); }, child: Text( - 'forgotPasswordTitle'.tr(), + LocaleKeys.forgotPasswordTitle.tr(), style: AppStyles.font14Black.copyWith( decoration: TextDecoration.underline, ), @@ -148,7 +149,7 @@ class _LoginscreenbodyState extends State { child: CustomButton( isEnabled: true, isLoading: state.loginResource.status == Status.loading, - text: 'continueTxt'.tr(), + text: LocaleKeys.login.tr(), color: AppColors.pink, onPressed: () { if (_formKey.currentState!.validate()) { diff --git a/lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart b/lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart new file mode 100644 index 0000000..b5b7774 --- /dev/null +++ b/lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart @@ -0,0 +1,82 @@ +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.dart'; +import 'package:tracking_app/features/auth/domain/usecase/resertpassword_usecase.dart'; +import 'package:tracking_app/features/auth/presentation/reset_password/manager/reset_password_intents.dart'; +import '../../../../../app/config/base_state/base_state.dart'; +import '../../../../../app/core/network/api_result.dart'; +import '../../../../../app/core/utils/validators_helper.dart'; + + +part 'reset_password_state.dart'; + +@injectable +class ResetPasswordCubit extends Cubit { + final ResetPasswordUsecase _resetPasswordUseCase; + final String email; + + ResetPasswordCubit(@factoryParam this.email, this._resetPasswordUseCase) + : super(ResetPasswordState.initial(email: email)); + + final formKey = GlobalKey(); + final emailController = TextEditingController(); + final newPasswordController = TextEditingController(); + + void doIntent(ChangePasswordIntent intent) { + switch (intent) { + case FormChangedIntent(): + _validateForm(); + break; + case TogglePasswordVisibilityIntent(): + _togglePasswordVisibility(); + break; + case SubmitChangePasswordIntent(): + _submitResetPassword(); + break; + } + } + + void _validateForm() { + final isValid = + newPasswordController.text.trim().isNotEmpty && + Validators.validatePassword(newPasswordController.text.trim()) == null; + + emit(state.copyWith(isFormValid: isValid)); + } + + void _togglePasswordVisibility() { + emit( + state.copyWith(togglePasswordVisibility: !state.togglePasswordVisibility), + ); + } + + Future _submitResetPassword() async { + if (!state.isFormValid) return; + + emit(state.copyWith(resource: Resource.loading())); + + final dto = ResetPasswordRequest( + email: email, // Use the stored email + newPassword: newPasswordController.text.trim(), + ); + + final result = await _resetPasswordUseCase(dto); + + if (result is SuccessApiResult) { + emit(state.copyWith(resource: Resource.success(result.data))); + } else if (result is ErrorApiResult) { + emit(state.copyWith(resource: Resource.error(result.error))); + } else { + emit(state.copyWith(resource: Resource.error('Unexpected error'))); + } + } + + @override + Future close() { + emailController.dispose(); + newPasswordController.dispose(); + return super.close(); + } +} diff --git a/lib/features/auth/presentation/reset_password/manager/reset_password_intents.dart b/lib/features/auth/presentation/reset_password/manager/reset_password_intents.dart new file mode 100644 index 0000000..d97932a --- /dev/null +++ b/lib/features/auth/presentation/reset_password/manager/reset_password_intents.dart @@ -0,0 +1,19 @@ +sealed class ChangePasswordIntent { + const ChangePasswordIntent(); + + static const formChanged = FormChangedIntent(); + static const togglePasswordVisibility = TogglePasswordVisibilityIntent(); + static const submit = SubmitChangePasswordIntent(); +} + +class FormChangedIntent extends ChangePasswordIntent { + const FormChangedIntent(); +} + +class TogglePasswordVisibilityIntent extends ChangePasswordIntent { + const TogglePasswordVisibilityIntent(); +} + +class SubmitChangePasswordIntent extends ChangePasswordIntent { + const SubmitChangePasswordIntent(); +} diff --git a/lib/features/auth/presentation/reset_password/manager/reset_password_state.dart b/lib/features/auth/presentation/reset_password/manager/reset_password_state.dart new file mode 100644 index 0000000..7fad286 --- /dev/null +++ b/lib/features/auth/presentation/reset_password/manager/reset_password_state.dart @@ -0,0 +1,37 @@ +part of 'reset_password_cubit.dart'; + +class ResetPasswordState { + final Resource resource; + final bool isFormValid; + final bool togglePasswordVisibility; + final String email; + + const ResetPasswordState({ + required this.resource, + required this.isFormValid, + required this.togglePasswordVisibility, + required this.email, + }); + + factory ResetPasswordState.initial({String email = ''}) => ResetPasswordState( + resource: Resource.initial(), + isFormValid: false, + togglePasswordVisibility: false, + email: email, + ); + + ResetPasswordState copyWith({ + Resource? resource, + bool? isFormValid, + bool? togglePasswordVisibility, + String? email, + }) { + return ResetPasswordState( + resource: resource ?? this.resource, + isFormValid: isFormValid ?? this.isFormValid, + togglePasswordVisibility: + togglePasswordVisibility ?? this.togglePasswordVisibility, + email: email ?? this.email, + ); + } +} diff --git a/lib/features/auth/presentation/reset_password/pages/reset_password.dart b/lib/features/auth/presentation/reset_password/pages/reset_password.dart new file mode 100644 index 0000000..d1e0b28 --- /dev/null +++ b/lib/features/auth/presentation/reset_password/pages/reset_password.dart @@ -0,0 +1,51 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../../../generated/locale_keys.g.dart'; +import '../../../../../app/config/base_state/base_state.dart'; +import '../../../../../app/core/router/route_names.dart'; +import '../../../../../app/core/widgets/show_app_dialog.dart'; +import '../../../../../app/core/widgets/show_snak_bar.dart'; +import '../manager/reset_password_cubit.dart'; +import '../widgets/reset_password_form.dart'; + +class ResetPasswordPage extends StatelessWidget { + const ResetPasswordPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(LocaleKeys.password.tr()), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => context.pop(), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: BlocConsumer( + listenWhen: (p, c) => p.resource.status != c.resource.status, + listener: (context, state) { + if (state.resource.status == Status.success) { + showAppSnackbar(context, LocaleKeys.passwordUpdated.tr()); + context.push(RouteNames.profile); + } + if (state.resource.status == Status.error) { + showAppDialog( + context, + message: + state.resource.error ?? LocaleKeys.an_error_occurred.tr(), + isError: true, + ); + } + }, + builder: (context, state) { + return const ResetPasswordForm(); + }, + ), + ), + ); + } +} diff --git a/lib/features/auth/presentation/reset_password/widgets/reset_password_form.dart b/lib/features/auth/presentation/reset_password/widgets/reset_password_form.dart new file mode 100644 index 0000000..61e7fd6 --- /dev/null +++ b/lib/features/auth/presentation/reset_password/widgets/reset_password_form.dart @@ -0,0 +1,68 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/features/auth/presentation/reset_password/widgets/show_user_email.dart'; +import 'package:tracking_app/generated/locale_keys.g.dart'; +import '../../../../../app/config/base_state/base_state.dart'; +import '../../../../../app/core/utils/validators_helper.dart'; +import '../../../../../app/core/widgets/custom_button.dart'; +import '../../../../../app/core/widgets/password_text_form_field.dart'; +import '../manager/reset_password_cubit.dart'; +import '../manager/reset_password_intents.dart'; + +class ResetPasswordForm extends StatelessWidget { + const ResetPasswordForm({super.key}); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + final email = cubit.email; + + return Form( + key: cubit.formKey, + onChanged: () => cubit.doIntent(ChangePasswordIntent.formChanged), + child: Column( + children: [ + const SizedBox(height: 20), + + ShowUserEmail(context, email), + + const SizedBox(height: 24), + + BlocBuilder( + buildWhen: (p, c) => + p.togglePasswordVisibility != c.togglePasswordVisibility, + builder: (context, state) { + return PasswordTextFormField( + controller: cubit.newPasswordController, + label: LocaleKeys.newPassword.tr(), + isVisible: state.togglePasswordVisibility, + onToggleVisibility: () => cubit.doIntent( + ChangePasswordIntent.togglePasswordVisibility, + ), + validator: Validators.validatePassword, + hint: LocaleKeys.enterYourPassword, + ); + }, + ), + + const SizedBox(height: 32), + + BlocBuilder( + buildWhen: (p, c) => + p.isFormValid != c.isFormValid || + p.resource.status != c.resource.status, + builder: (context, state) { + return CustomButton( + text: LocaleKeys.confirm.tr(), + isEnabled: state.isFormValid, + isLoading: state.resource.status == Status.loading, + onPressed: () => cubit.doIntent(ChangePasswordIntent.submit), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/auth/presentation/reset_password/widgets/show_user_email.dart b/lib/features/auth/presentation/reset_password/widgets/show_user_email.dart new file mode 100644 index 0000000..832928b --- /dev/null +++ b/lib/features/auth/presentation/reset_password/widgets/show_user_email.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +Widget ShowUserEmail(BuildContext context, String email) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.email_outlined), + const SizedBox(width: 12), + Expanded(child: Text(email)), + ], + ), + ); +} diff --git a/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart new file mode 100644 index 0000000..6f227b9 --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart @@ -0,0 +1,99 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/features/auth/domain/models/verifyreset_entity.dart'; +import 'package:tracking_app/features/auth/domain/usecase/forgetpassword_usecase.dart'; +import 'package:tracking_app/features/auth/domain/usecase/verifyreaset_usecase.dart'; +import '../../../../../../app/config/base_state/base_state.dart'; +import '../../../../../../app/core/network/api_result.dart'; + +part 'verify_reset_state.dart'; +part 'verify_reset_intent.dart'; + +@injectable +class VerifyResetCodeCubit extends Cubit { + final VerifyResetCodeUsecase _verifyUseCase; + final ForgetPasswordUsecase _resendUseCase; + final String email; + Timer? _cooldownTimer; + + VerifyResetCodeCubit( + this._verifyUseCase, + this._resendUseCase, + @factoryParam this.email, + ) : super(VerifyResetCodeState.initial()) { + _startCooldown(30); + } + + void doIntent(VerifyResetCodeIntents intent) { + switch (intent.runtimeType) { + case FormChangedIntent: + _validateForm((intent as FormChangedIntent).code); + break; + case SubmitVerifyCodeIntent: + _submitCode(); + break; + case ResendCodeIntent: + _resendCode(); + break; + } + } + + void _validateForm(String code) { + emit(state.copyWith(code: code, isFormValid: code.length == 6)); + } + + Future _submitCode() async { + if (!state.isFormValid) return; + + emit(state.copyWith(resource: Resource.loading())); + + final result = await _verifyUseCase(state.code); + + if (result is SuccessApiResult) { + emit(state.copyWith(resource: Resource.success(result.data))); + } else if (result is ErrorApiResult) { + emit(state.copyWith(resource: Resource.error(result.error))); + } else { + emit(state.copyWith(resource: Resource.error("Unexpected error"))); + } + } + + Future _resendCode() async { + if (!state.canResend) return; + _startCooldown(30); + emit(state.copyWith(resource: Resource.loading(), canResend: false)); + + final result = await _resendUseCase(email); + + if (result is SuccessApiResult) { + emit(state.copyWith(resource: Resource.success(result.data))); + } else if (result is ErrorApiResult) { + emit(state.copyWith(resource: Resource.error(result.error))); + } else { + emit(state.copyWith(resource: Resource.error("Unexpected error"))); + } + } + + void _startCooldown(int seconds) { + _cooldownTimer?.cancel(); + emit(state.copyWith(resendCountdown: seconds, canResend: false)); + + _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final remaining = state.resendCountdown - 1; + if (remaining <= 0) { + timer.cancel(); + emit(state.copyWith(resendCountdown: 0, canResend: true)); + } else { + emit(state.copyWith(resendCountdown: remaining)); + } + }); + } + + @override + Future close() { + _cooldownTimer?.cancel(); + return super.close(); + } +} diff --git a/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart new file mode 100644 index 0000000..532fed2 --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart @@ -0,0 +1,17 @@ +part of 'verify_reset_cubit.dart'; +sealed class VerifyResetCodeIntents { + const VerifyResetCodeIntents(); +} + +class FormChangedIntent extends VerifyResetCodeIntents { + final String code; + const FormChangedIntent(this.code); +} + +class SubmitVerifyCodeIntent extends VerifyResetCodeIntents { + const SubmitVerifyCodeIntent(); +} + +class ResendCodeIntent extends VerifyResetCodeIntents { + const ResendCodeIntent(); +} diff --git a/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_state.dart b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_state.dart new file mode 100644 index 0000000..ebf54da --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_state.dart @@ -0,0 +1,41 @@ +part of 'verify_reset_cubit.dart'; + +class VerifyResetCodeState { + final Resource resource; + final bool isFormValid; + final String code; + final int resendCountdown; + final bool canResend; + + const VerifyResetCodeState({ + required this.resource, + required this.isFormValid, + required this.code, + required this.resendCountdown, + required this.canResend, + }); + + factory VerifyResetCodeState.initial() => VerifyResetCodeState( + resource: Resource.initial(), + isFormValid: false, + code: '', + resendCountdown: 0, + canResend: true, + ); + + VerifyResetCodeState copyWith({ + Resource? resource, + bool? isFormValid, + String? code, + int? resendCountdown, + bool? canResend, + }) { + return VerifyResetCodeState( + resource: resource ?? this.resource, + isFormValid: isFormValid ?? this.isFormValid, + code: code ?? this.code, + resendCountdown: resendCountdown ?? this.resendCountdown, + canResend: canResend ?? this.canResend, + ); + } +} diff --git a/lib/features/auth/presentation/verify_reset/pages/verify_reset_page.dart b/lib/features/auth/presentation/verify_reset/pages/verify_reset_page.dart new file mode 100644 index 0000000..73f7155 --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/pages/verify_reset_page.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:go_router/go_router.dart'; +import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart'; +import '../../../../../../generated/locale_keys.g.dart'; +import '../../../../../app/config/base_state/base_state.dart'; +import '../../../../../app/core/router/route_names.dart'; +import '../../../../../app/core/widgets/show_app_dialog.dart'; +import '../../../../../app/core/widgets/show_snak_bar.dart'; +import '../widgets/verify_rest_code_form.dart'; + +class VerifyResetCodePage extends StatelessWidget { + final String email; + const VerifyResetCodePage({super.key, required this.email}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: Text(LocaleKeys.emailVerification.tr()), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.resource.status != current.resource.status, + listener: (context, state) { + if (state.resource.status == Status.success && + state.code.isNotEmpty) { + showAppSnackbar(context, LocaleKeys.yourEmailVerified.tr()); + context.push(RouteNames.resetPassword, extra: email); + } + if (state.resource.status == Status.error) { + showAppDialog( + context, + message: + state.resource.error ?? LocaleKeys.an_error_occurred.tr(), + isError: true, + ); + } + }, + builder: (context, state) { + return VerifyResetCodeForm(); + }, + ), + ), + ); + } +} diff --git a/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart b/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart new file mode 100644 index 0000000..d962750 --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart @@ -0,0 +1,71 @@ + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class CountdownTimerWidget extends StatefulWidget { + final int initialSeconds; + final VoidCallback onTimerEnd; + final Color? activeColor; + final Color? inactiveColor; + + const CountdownTimerWidget({ + super.key, + required this.initialSeconds, + required this.onTimerEnd, + this.activeColor = Colors.pink, + this.inactiveColor = Colors.grey, + }); + + @override + State createState() => _CountdownTimerWidgetState(); +} + +class _CountdownTimerWidgetState extends State { + late int _remainingSeconds; + Timer? _timer; + + @override + void initState() { + super.initState(); + _remainingSeconds = widget.initialSeconds; + _startTimer(); + } + + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + setState(() { + _remainingSeconds--; + }); + + if (_remainingSeconds <= 0) { + timer.cancel(); + widget.onTimerEnd(); + } + }); + } + + String _formatTime() { + final minutes = _remainingSeconds ~/ 60; + final seconds = _remainingSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isActive = _remainingSeconds > 0; + + return Text( + isActive ? _formatTime() : '00:00', + style: (Theme.of(context).textTheme.bodyMedium)?.copyWith( + color: isActive ? widget.activeColor : widget.inactiveColor, + ), + ); + } +} diff --git a/lib/features/auth/presentation/verify_reset/widgets/resend_action_widget.dart b/lib/features/auth/presentation/verify_reset/widgets/resend_action_widget.dart new file mode 100644 index 0000000..5e89080 --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/widgets/resend_action_widget.dart @@ -0,0 +1,82 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart'; + +import '../../../../../generated/locale_keys.g.dart'; + +Widget buildResendSectionWithCountdown( + BuildContext context, + VerifyResetCodeCubit cubit, + VerifyResetCodeState state, +) { + final canResend = state.canResend; + final cooldownSeconds = state.resendCountdown; + + return Column( + children: [ + Text( + LocaleKeys.didNotReceive.tr(), + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + + if (canResend) + InkWell( + onTap: () => cubit.doIntent(ResendCodeIntent()), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.pink.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.pink, width: 1), + ), + child: Text( + LocaleKeys.resend.tr(), + style: TextStyle( + color: Colors.pink, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ) + else + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.timer_outlined, color: Colors.pink, size: 20), + const SizedBox(width: 8), + Text( + _formatTime(cooldownSeconds), + style: TextStyle( + color: Colors.pink, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 4), + Text( + 'until you can resend', + style: TextStyle(color: Colors.grey.shade600, fontSize: 14), + ), + ], + ), + ), + ], + ); +} + +String _formatTime(int seconds) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; +} diff --git a/lib/features/auth/presentation/verify_reset/widgets/verify_rest_code_form.dart b/lib/features/auth/presentation/verify_reset/widgets/verify_rest_code_form.dart new file mode 100644 index 0000000..0cd52f9 --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/widgets/verify_rest_code_form.dart @@ -0,0 +1,93 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tracking_app/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart'; +import 'package:tracking_app/features/auth/presentation/verify_reset/widgets/resend_action_widget.dart'; +import '../../../../../../generated/locale_keys.g.dart'; +import '../../../../../app/config/base_state/base_state.dart'; +import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; + +class VerifyResetCodeForm extends StatelessWidget { + const VerifyResetCodeForm({super.key}); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + + return BlocBuilder( + buildWhen: (previous, current) => + previous.canResend != current.canResend || + previous.resendCountdown != current.resendCountdown || + previous.resource.status != current.resource.status, + builder: (context, state) { + final isLoading = state.resource.status == Status.loading; + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 40), + Text( + LocaleKeys.emailVerification.tr(), + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 16), + Text( + LocaleKeys.instruction.tr(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 48), + + OtpTextField( + numberOfFields: 6, + borderColor: Theme.of(context).colorScheme.primary, + enabledBorderColor: Theme.of(context).colorScheme.outline, + focusedBorderColor: Theme.of(context).colorScheme.primary, + showFieldAsBox: true, + fieldWidth: 52, + fieldHeight: 64, + borderRadius: BorderRadius.circular(12), + textStyle: Theme.of(context).textTheme.headlineSmall + ?.copyWith(fontWeight: FontWeight.w600), + onCodeChanged: (code) => + cubit.doIntent(FormChangedIntent(code)), + onSubmit: (code) { + cubit.doIntent(FormChangedIntent(code)); + cubit.doIntent(SubmitVerifyCodeIntent()); + }, + ), + + const SizedBox(height: 32), + + if (isLoading) + CircularProgressIndicator( + color: Theme.of(context).colorScheme.primary, + ), + + if (!isLoading) const SizedBox(height: 32), + buildResendSectionWithCountdown(context, cubit, state), + + const SizedBox(height: 20), + Text( + 'Code sent to: ${cubit.email}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.outline, + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/auth/test.dart b/lib/features/auth/test.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/auth/test.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/features/profile/presentation/pages/profile_page.dart b/lib/features/profile/presentation/pages/profile_page.dart index 2da99e0..6c970df 100644 --- a/lib/features/profile/presentation/pages/profile_page.dart +++ b/lib/features/profile/presentation/pages/profile_page.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:tracking_app/app/core/router/route_names.dart'; class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold(body: Center(child: const Text("Welcome to Profile Page"))); } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart new file mode 100644 index 0000000..1743545 --- /dev/null +++ b/lib/generated/locale_keys.g.dart @@ -0,0 +1,238 @@ +// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart + +// ignore_for_file: constant_identifier_names + +abstract class LocaleKeys { + static const firstName = 'firstName'; + static const lastName = 'lastName'; + static const email = 'email'; + static const password = 'password'; + static const confirmPassword = 'confirmPassword'; + static const phone = 'phone'; + static const gender = 'gender'; + static const enterFirstName = 'enterFirstName'; + static const enterLastName = 'enterLastName'; + static const enterEmail = 'enterEmail'; + static const enterPassword = 'enterPassword'; + static const enterPhoneNumber = 'enterPhoneNumber'; + static const enterRePassword = 'enterRePassword'; + static const femaleGender = 'femaleGender'; + static const maleGender = 'maleGender'; + static const femaleValue = 'femaleValue'; + static const maleValue = 'maleValue'; + static const createAccount = 'createAccount'; + static const termsAndConditions = 'termsAndConditions'; + static const alreadyHaveAccount = 'alreadyHaveAccount'; + static const login = 'login'; + static const signup = 'signup'; + static const emailRequired = 'emailRequired'; + static const emailInvalid = 'emailInvalid'; + static const passwordRequired = 'passwordRequired'; + static const passwordLengthInvalid = 'passwordLengthInvalid'; + static const passwordUpperLetterInvalid = 'passwordUpperLetterInvalid'; + static const passwordLowerLetterInvalid = 'passwordLowerLetterInvalid'; + static const passwordNumbersInvalid = 'passwordNumbersInvalid'; + static const passwordSpecialCharInvalid = 'passwordSpecialCharInvalid'; + static const confirmPasswordRequired = 'confirmPasswordRequired'; + static const passwordsDoNotMatch = 'passwordsDoNotMatch'; + static const phoneRequired = 'phoneRequired'; + static const phoneInvalid = 'phoneInvalid'; + static const firstNameRequired = 'firstNameRequired'; + static const lastNameRequired = 'lastNameRequired'; + static const nameInvalid = 'nameInvalid'; + static const genderRequired = 'genderRequired'; + static const loading = 'loading'; + static const registrationSuccessful = 'registrationSuccessful'; + static const ok = 'ok'; + static const error = 'error'; + static const success = 'success'; + static const emailVerification = 'emailVerification'; + static const rememberMe = 'rememberMe'; + static const forgotPassword = 'forgotPassword'; + static const forgotPasswordTitle = 'forgotPasswordTitle'; + static const continueAsGuest = 'continueAsGuest'; + static const dontHaveAnAccount = 'dontHaveAnAccount'; + static const signUp = 'signUp'; + static const enterYourEmail = 'enterYourEmail'; + static const enterYourPassword = 'enterYourPassword'; + static const associatedEmail = 'associatedEmail'; + static const userName = 'userName'; + static const newPassword = 'newPassword'; + static const confirm = 'confirm'; + static const continueTxt = 'continueTxt'; + static const instruction = 'instruction'; + static const didNotReceive = 'didNotReceive'; + static const resend = 'resend'; + static const resetPassword = 'resetPassword'; + static const yourEmailVerified = 'yourEmailVerified'; + static const check_email_for_verification_code = 'check_email_for_verification_code'; + static const passwordValidation = 'passwordValidation'; + static const connectionTimeout = 'connectionTimeout'; + static const noInternet = 'noInternet'; + static const unauthorized = 'unauthorized'; + static const serverError = 'serverError'; + static const unknownError = 'unknownError'; + static const an_error_occurred = 'an_error_occurred'; + static const weakPassword = 'weakPassword'; + static const passwordWithCapital = 'passwordWithCapital'; + static const passwordWithNumber = 'passwordWithNumber'; + static const passwordDontMatch = 'passwordDontMatch'; + static const confirmPasswordMsg = 'confirmPasswordMsg'; + static const invalidNumber = 'invalidNumber'; + static const required = 'required'; + static const least3Characters = 'least3Characters'; + static const least6Characters = 'least6Characters'; + static const invalidName = 'invalidName'; + static const phoneNumber = 'phoneNumber'; + static const passwordUpdated = 'passwordUpdated'; + static const addToCard = 'addToCard'; + static const noProductsfound = 'noProductsfound'; + static const viewAll = 'viewAll'; + static const search = 'search'; + static const categories = 'categories'; + static const bestSelling = 'bestSelling'; + static const occasions = 'occasions'; + static const allPricesIncludeTax = 'allPricesIncludeTax'; + static const productAddedToCart = 'productAddedToCart'; + static const something_went_wrong = 'something_went_wrong'; + static const cart = 'cart'; + static const items = 'items'; + static const deliverTo = 'deliverTo'; + static const egp = 'egp'; + static const subTotal = 'subTotal'; + static const deliveryFee = 'deliveryFee'; + static const total = 'total'; + static const checkout = 'checkout'; + static const productDeletedSuccessfully = 'productDeletedSuccessfully'; + static const productUpdated = 'productUpdated'; + static const currentPassword = 'currentPassword'; + static const enterCurrentPassword = 'enterCurrentPassword'; + static const enterNewPassword = 'enterNewPassword'; + static const confirmNewPassword = 'confirmNewPassword'; + static const update = 'update'; + static const changePassword = 'changePassword'; + static const no_products_found = 'no_products_found'; + static const change_language = 'change_language'; + static const arabic = 'arabic'; + static const english = 'english'; + static const initialSearchMsg = 'initialSearchMsg'; + static const welcomeMessage = 'welcomeMessage'; + static const home = 'home'; + static const profile = 'profile'; + static const defaultErrorMessage = 'defaultErrorMessage'; + static const bestseller = 'bestseller'; + static const sessionExpiredMessage = 'sessionExpiredMessage'; + static const notificationsKey = 'notificationsKey'; + static const noProfileFound = 'noProfileFound'; + static const register = 'register'; + static const pleaseLoginToAccessProfile = 'pleaseLoginToAccessProfile'; + static const aboutUs = 'aboutUs'; + static const language = 'language'; + static const notifications = 'notifications'; + static const savedAddresses = 'savedAddresses'; + static const myOrders = 'myOrders'; + static const noName = 'noName'; + static const noEmail = 'noEmail'; + static const editProfile = 'editProfile'; + static const logout = 'logout'; + static const logoutFailed = 'logoutFailed'; + static const order_success = 'order_success'; + static const failed_load_addresses = 'failed_load_addresses'; + static const no_addresses = 'no_addresses'; + static const order_status = 'order_status'; + static const delivered = 'delivered'; + static const paid = 'paid'; + static const pending = 'pending'; + static const instant_delivery_info = 'instant_delivery_info'; + static const schedule = 'schedule'; + static const delivery_address = 'delivery_address'; + static const add_new = 'add_new'; + static const payment_method = 'payment_method'; + static const cash_on_delivery = 'cash_on_delivery'; + static const credit_card = 'credit_card'; + static const it_is_a_gift = 'it_is_a_gift'; + static const recipient_name = 'recipient_name'; + static const recipient_phone = 'recipient_phone'; + static const place_order = 'place_order'; + static const instant = 'instant'; + static const arrive_by_datetime = 'arrive_by_datetime'; + static const in_cart = 'in_cart'; + static const invalidRecipientName = 'invalidRecipientName'; + static const invalidAddress = 'invalidAddress'; + static const requiredRecipientName = 'requiredRecipientName'; + static const requiredAddress = 'requiredAddress'; + static const requiredCity = 'requiredCity'; + static const requiredArea = 'requiredArea'; + static const address = 'address'; + static const enter_address = 'enter_address'; + static const phone_number = 'phone_number'; + static const enter_phone_number = 'enter_phone_number'; + static const enter_recipient_name = 'enter_recipient_name'; + static const save_address = 'save_address'; + static const area = 'area'; + static const city = 'city'; + static const location_permission = 'location_permission'; + static const location_service_off_message = 'location_service_off_message'; + static const location_permission_denied_forever_message = 'location_permission_denied_forever_message'; + static const location_permission_denied_message = 'location_permission_denied_message'; + static const open_settings = 'open_settings'; + static const open_location_settings = 'open_location_settings'; + static const allow_location = 'allow_location'; + static const move_map_to_choose_location = 'move_map_to_choose_location'; + static const address_saved_successfully = 'address_saved_successfully'; + static const failed_to_save_address = 'failed_to_save_address'; + static const addNewAddress = 'addNewAddress'; + static const savedAddress = 'savedAddress'; + static const discount = 'discount'; + static const sortBy = 'sortBy'; + static const lowestPrice = 'lowestPrice'; + static const highestPrice = 'highestPrice'; + static const newest = 'newest'; + static const oldest = 'oldest'; + static const filter = 'filter'; + static const active = 'active'; + static const completed = 'completed'; + static const no_orders_found = 'no_orders_found'; + static const track_order = 'track_order'; + static const order_number = 'order_number'; + static const all_notifications_cleared = 'all_notifications_cleared'; + static const notification_deleted_successfully = 'notification_deleted_successfully'; + static const clear_all = 'clear_all'; + static const no_notifications_yet = 'no_notifications_yet'; + static const orders = 'orders'; + static const onboardingTitle = 'onboardingTitle'; + static const onboardingDescription = 'onboardingDescription'; + static const applyNow = 'applyNow'; + static const wrongEmailOrPassword = 'wrongEmailOrPassword'; + static const apply = 'apply'; + static const welcomeApply = 'welcomeApply'; + static const joinTeamMessage = 'joinTeamMessage'; + static const country = 'country'; + static const firstLegalName = 'firstLegalName'; + static const enterFirstLegalName = 'enterFirstLegalName'; + static const secondLegalName = 'secondLegalName'; + static const enterSecondLegalName = 'enterSecondLegalName'; + static const vehicleType = 'vehicleType'; + static const vehicleNumber = 'vehicleNumber'; + static const enterVehicleNumber = 'enterVehicleNumber'; + static const vehicleLicense = 'vehicleLicense'; + static const uploadLicensePhoto = 'uploadLicensePhoto'; + static const idNumber = 'idNumber'; + static const enterNationalId = 'enterNationalId'; + static const idImage = 'idImage'; + static const uploadIdImage = 'uploadIdImage'; + static const continueValue = 'continue'; + static const requiredField = 'requiredField'; + static const licensePhotoRequired = 'licensePhotoRequired'; + static const idImageRequired = 'idImageRequired'; + static const failedToLoadCountries = 'failedToLoadCountries'; + static const failedToLoadVehicles = 'failedToLoadVehicles'; + static const applicationSubmittedSuccessfully = 'applicationSubmittedSuccessfully'; + static const submissionFailed = 'submissionFailed'; + static const applicationSubmitted = 'applicationSubmitted'; + static const congratulationsMessage = 'congratulationsMessage'; + static const reviewMessage = 'reviewMessage'; + static const backToLogin = 'backToLogin'; + static const checkEmailMessage = 'checkEmailMessage'; + +} diff --git a/pubspec.lock b/pubspec.lock index 779a2a3..a14d8be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "91.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,10 +21,18 @@ packages: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "8.4.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: @@ -77,18 +85,18 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "4.0.4" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: @@ -97,30 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.1" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.dev" - source: hosted - version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" - url: "https://pub.dev" - source: hosted - version: "2.4.13" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "2.11.1" built_collection: dependency: transitive description: @@ -237,10 +229,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "3.1.3" dbus: dependency: transitive description: @@ -653,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" html: dependency: transitive description: @@ -761,10 +761,10 @@ packages: dependency: "direct dev" description: name: injectable_generator - sha256: af403d76c7b18b4217335e0075e950cd0579fd7f8d7bd47ee7c85ada31680ba1 + sha256: "309c3f3546160dd00b575f16b341a6a3025479950441bcc7fcb2f8404a40d326" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.9.1" intl: dependency: "direct main" description: @@ -801,10 +801,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.11.2" leak_tracker: dependency: transitive description: @@ -829,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + lean_builder: + dependency: transitive + description: + name: lean_builder + sha256: "4f3d70c34c52cc5034e8cc6f53d35aa3a32fb373b78fb4c29cf45cd1dcf06942" + url: "https://pub.dev" + source: hosted + version: "0.1.5" lints: dependency: transitive description: @@ -889,10 +897,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566 url: "https://pub.dev" source: hosted - version: "5.4.4" + version: "5.6.3" mocktail: dependency: "direct dev" description: @@ -1021,6 +1029,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + url: "https://pub.dev" + source: hosted + version: "6.0.0" provider: dependency: "direct main" description: @@ -1065,10 +1081,10 @@ packages: dependency: "direct dev" description: name: retrofit_generator - sha256: "9499eb46b3657a62192ddbc208ff7e6c6b768b19e83c1ee6f6b119c864b99690" + sha256: fed2c4e4ed6dab084c00d25c739988aa3cec1acd2b168771136188cced8d967d url: "https://pub.dev" source: hosted - version: "7.0.8" + version: "10.2.1" sanitize_html: dependency: transitive description: @@ -1190,18 +1206,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "4.2.0" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.8" source_map_stack_trace: dependency: transitive description: @@ -1298,22 +1314,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: @@ -1490,6 +1490,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bb7cff1..ac0ddc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,10 +42,10 @@ dev_dependencies: bloc_test: ^10.0.0 build_runner: ^2.4.13 flutter_lints: ^6.0.0 + retrofit_generator: 10.2.1 injectable_generator: ^2.4.1 json_serializable: ^6.8.0 mockito: ^5.4.4 - retrofit_generator: 7.0.8 network_image_mock: ^2.1.1 mocktail: ^1.0.3 diff --git a/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart b/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart index 558892c..22b9953 100644 --- a/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart +++ b/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart @@ -2,141 +2,190 @@ import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:retrofit/dio.dart'; +import 'package:retrofit/retrofit.dart'; import 'package:tracking_app/app/core/api_manger/api_client.dart'; import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/api/datasource/auth_remote_datasource_impl.dart'; import 'package:tracking_app/features/auth/data/model/request/LoginRequest.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; +import 'package:tracking_app/features/auth/data/models/request/forget_password_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/request/verifyreset_request.dart'; +import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; import 'auth_remote_datasource_impl_test.mocks.dart'; @GenerateMocks([ApiClient]) void main() { late MockApiClient mockApiClient; - late AuthRemoteDataSourceImpl dataSource; - - final tLoginRequest = LoginRequest( - email: 'test@example.com', - password: 'password123', - ); - final tLoginResponse = LoginResponse(token: 'token123', message: 'Success'); + late AuthRemoteDataSourceImpl authRemoteDataSourceImpl; + late AuthRemoteDataSourceImpl dataSource; // initialize for login/change password tests setUpAll(() { mockApiClient = MockApiClient(); + authRemoteDataSourceImpl = AuthRemoteDataSourceImpl(mockApiClient); dataSource = AuthRemoteDataSourceImpl(mockApiClient); }); + final forgetPasswordRequest = ForgetPasswordRequest(email: "test@example.com"); + + group("AuthRemoteDatasourceImpl.forgetPassword()", () { + test("returns SuccessApiResult when apiClient returns valid response", () async { + final expectedResponse = ForgetpasswordResponse(message: "Password reset code sent to email"); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/forget-password'), + data: expectedResponse, + statusCode: 200, + ); + final fakeHttpResponse = HttpResponse(dioResponse.data!, dioResponse); + + when(mockApiClient.forgetPassword(any)).thenAnswer((_) async => fakeHttpResponse); + + final result = await authRemoteDataSourceImpl.forgetPassword(forgetPasswordRequest); + + expect(result, isA>()); + final successResult = result as SuccessApiResult; + expect(successResult.data.message, "Password reset code sent to email"); + verify(mockApiClient.forgetPassword(any)).called(1); + }); + + test("returns ErrorApiResult when apiClient throws Exception", () async { + when(mockApiClient.forgetPassword(any)).thenThrow(Exception("Network Error")); + + final result = await authRemoteDataSourceImpl.forgetPassword(forgetPasswordRequest); + + expect(result, isA()); + final errorResult = result as ErrorApiResult; + expect(errorResult.error, contains("Network Error")); + verify(mockApiClient.forgetPassword(any)).called(1); + }); + }); + + group("AuthRemoteDatasourceImpl.resetPassword()", () { + final resetPasswordRequest = ResetPasswordRequest(email: "test@example.com", newPassword: "12345678"); + + test("returns SuccessApiResult when apiClient returns valid response", () async { + final expectedResponse = ResetpasswordResponse(message: "Password reset successfully"); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/reset-password'), + data: expectedResponse, + statusCode: 200, + ); + final fakeHttpResponse = HttpResponse(dioResponse.data!, dioResponse); + + when(mockApiClient.resetPassword(any)).thenAnswer((_) async => fakeHttpResponse); + + final result = await authRemoteDataSourceImpl.resetPassword(resetPasswordRequest); + + expect(result, isA>()); + final successResult = result as SuccessApiResult; + expect(successResult.data.message, "Password reset successfully"); + verify(mockApiClient.resetPassword(any)).called(1); + }); + + test("returns ErrorApiResult when apiClient throws Exception", () async { + when(mockApiClient.resetPassword(any)).thenThrow(Exception("Reset failed")); + + final result = await authRemoteDataSourceImpl.resetPassword(resetPasswordRequest); + + expect(result, isA()); + final errorResult = result as ErrorApiResult; + expect(errorResult.error, contains("Reset failed")); + verify(mockApiClient.resetPassword(any)).called(1); + }); + }); + + group("AuthRemoteDatasourceImpl.verifyResetCode()", () { + final verifyResetCodeRequest = VerifyResetRequest(resetCode: "1234"); + + test("returns SuccessApiResult when apiClient returns valid response", () async { + final expectedResponse = VerifyresetResponse(status: "Code verified successfully"); + final dioResponse = Response( + requestOptions: RequestOptions(path: '/verify-reset-code'), + data: expectedResponse, + statusCode: 200, + ); + final fakeHttpResponse = HttpResponse(dioResponse.data!, dioResponse); + + when(mockApiClient.verifyResetCode(any)).thenAnswer((_) async => fakeHttpResponse); + + final result = await authRemoteDataSourceImpl.verifyResetCode(verifyResetCodeRequest); + + expect(result, isA>()); + final successResult = result as SuccessApiResult; + expect(successResult.data.status, "Code verified successfully"); + verify(mockApiClient.verifyResetCode(any)).called(1); + }); + + test("returns ErrorApiResult when apiClient throws Exception", () async { + when(mockApiClient.verifyResetCode(any)).thenThrow(Exception("Invalid code")); + + final result = await authRemoteDataSourceImpl.verifyResetCode(verifyResetCodeRequest); + + expect(result, isA()); + final errorResult = result as ErrorApiResult; + expect(errorResult.error, contains("Invalid code")); + verify(mockApiClient.verifyResetCode(any)).called(1); + }); + }); + + // ---------- login ---------- + final tLoginRequest = LoginRequest(email: 'test@example.com', password: 'password123'); + final tLoginResponse = LoginResponse(token: 'token123', message: 'Success'); + group('AuthRemoteDataSourceImpl.login', () { test('should return SuccessApiResult when login is successful', () async { - // Arrange when(mockApiClient.login(any)).thenAnswer((_) async => tLoginResponse); - - // Act final result = await dataSource.login(tLoginRequest); - - // Assert expect(result, isA>()); expect((result as SuccessApiResult).data, tLoginResponse); verify(mockApiClient.login(tLoginRequest)).called(1); }); - test( - 'should return ErrorApiResult with "wrongEmailOrPassword" on 401 error', - () async { - // Arrange - when(mockApiClient.login(any)).thenThrow( - DioException( - requestOptions: RequestOptions(path: ''), - response: Response( - requestOptions: RequestOptions(path: ''), - statusCode: 401, - ), - ), - ); - - // Act - final result = await dataSource.login(tLoginRequest); - - // Assert - expect(result, isA>()); - expect( - (result as ErrorApiResult).error, - 'wrongEmailOrPassword', - ); - }, - ); - - test( - 'should return ErrorApiResult with message from response on other DioErrors', - () async { - // Arrange - const tErrorMessage = 'Some other error'; - when(mockApiClient.login(any)).thenThrow( - DioException( - requestOptions: RequestOptions(path: ''), - response: Response( - requestOptions: RequestOptions(path: ''), - statusCode: 400, - data: {'message': tErrorMessage}, - ), - ), - ); - - // Act - final result = await dataSource.login(tLoginRequest); - - // Assert - expect(result, isA>()); - expect((result as ErrorApiResult).error, tErrorMessage); - }, - ); - - test( - 'should return ErrorApiResult with exception message on unknown error', - () async { - // Arrange - const tExceptionMessage = 'Exception: Unknown error'; - when(mockApiClient.login(any)).thenThrow(Exception('Unknown error')); - - // Act - final result = await dataSource.login(tLoginRequest); - - // Assert - expect(result, isA>()); - expect( - (result as ErrorApiResult).error, - tExceptionMessage, - ); - }, - ); + test('should return ErrorApiResult with "wrongEmailOrPassword" on 401 error', () async { + when(mockApiClient.login(any)).thenThrow( + DioException( + requestOptions: RequestOptions(path: ''), + response: Response(requestOptions: RequestOptions(path: ''), statusCode: 401), + ), + ); + final result = await dataSource.login(tLoginRequest); + expect(result, isA>()); + expect((result as ErrorApiResult).error, 'wrongEmailOrPassword'); + }); + + test('should return ErrorApiResult with message from response on other DioErrors', () async { + const tErrorMessage = 'Some other error'; + when(mockApiClient.login(any)).thenThrow( + DioException( + requestOptions: RequestOptions(path: ''), + response: Response(requestOptions: RequestOptions(path: ''), statusCode: 400, data: {'message': tErrorMessage}), + ), + ); + final result = await dataSource.login(tLoginRequest); + expect(result, isA>()); + expect((result as ErrorApiResult).error, tErrorMessage); + }); + + test('should return ErrorApiResult with exception message on unknown error', () async { + const tExceptionMessage = 'Exception: Unknown error'; + when(mockApiClient.login(any)).thenThrow(Exception('Unknown error')); + final result = await dataSource.login(tLoginRequest); + expect(result, isA>()); + expect((result as ErrorApiResult).error, tExceptionMessage); + }); }); group("AuthRemoteDatasourceImpl.changePassword()", () { test('should return ApiSuccess when change password succeeds', () async { - final fakeDto = ChangePasswordDto( - message: 'Success', - token: 'fake_token', - error: 'error', - ); - final fakeResponse = HttpResponse( - fakeDto, - Response( - requestOptions: RequestOptions(path: '/drivers/change-password'), - statusCode: 200, - ), - ); - when( - mockApiClient.changePassword(any), - ).thenAnswer((_) async => fakeResponse); + final fakeDto = ChangePasswordDto(message: 'Success', token: 'fake_token', error: 'error'); + final fakeResponse = HttpResponse(fakeDto, Response(requestOptions: RequestOptions(path: '/drivers/change-password'), statusCode: 200)); + when(mockApiClient.changePassword(any)).thenAnswer((_) async => fakeResponse); - final result = - await dataSource.changePassword( - password: 'Mm@123456', - newPassword: "Mmmmmm@1", - ) - as SuccessApiResult; + final result = await dataSource.changePassword(password: 'Mm@123456', newPassword: "Mmmmmm@1") as SuccessApiResult; expect(result, isA>()); expect(result.data.token, fakeDto.token); @@ -144,24 +193,13 @@ void main() { verify(mockApiClient.changePassword(any)).called(1); }); - test( - 'should return ApiFailure when change password throws exception', - () async { - when( - mockApiClient.changePassword(any), - ).thenThrow(Exception('Network error')); - - final result = - await dataSource.changePassword( - password: 'Mm@123456', - newPassword: "Mmmmmm@1", - ) - as ErrorApiResult; - - expect(result, isA>()); - expect(result.error.toString(), contains("Network error")); - verify(mockApiClient.changePassword(any)).called(1); - }, - ); + test('should return ApiFailure when change password throws exception', () async { + when(mockApiClient.changePassword(any)).thenThrow(Exception('Network error')); + final result = await dataSource.changePassword(password: 'Mm@123456', newPassword: "Mmmmmm@1") as ErrorApiResult; + + expect(result, isA>()); + expect(result.error.toString(), contains("Network error")); + verify(mockApiClient.changePassword(any)).called(1); + }); }); } diff --git a/test/features/auth/data/models/response/forgetpassword_response_test.dart b/test/features/auth/data/models/response/forgetpassword_response_test.dart new file mode 100644 index 0000000..10005e0 --- /dev/null +++ b/test/features/auth/data/models/response/forgetpassword_response_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; + +void main() { + group("ForgetpasswordResponse", () { + + test("fromJson should parse correctly", () { + // Arrange + final json = { + "message": "Reset email sent", + "info": "Check your inbox", + }; + + // Act + final model = ForgetpasswordResponse.fromJson(json); + + // Assert + expect(model.message, "Reset email sent"); + expect(model.info, "Check your inbox"); + }); + + test("toJson should return correct map", () { + // Arrange + final model = ForgetpasswordResponse( + message: "Reset email sent", + info: "Check your inbox", + ); + + // Act + final json = model.toJson(); + + // Assert + expect(json["message"], "Reset email sent"); + expect(json["info"], "Check your inbox"); + }); + + test("copyWith should override only provided fields", () { + // Arrange + final model = ForgetpasswordResponse( + message: "Old message", + info: "Old info", + ); + + // Act + final updatedModel = model.copyWith( + message: "New message", + ); + + // Assert + expect(updatedModel.message, "New message"); + expect(updatedModel.info, "Old info"); // unchanged + }); + + test("should handle null values correctly", () { + // Arrange + final model = ForgetpasswordResponse(); + + // Assert + expect(model.message, null); + expect(model.info, null); + + final json = model.toJson(); + expect(json.containsKey("message"), true); + expect(json.containsKey("info"), true); + }); + + }); +} diff --git a/test/features/auth/data/models/response/resetpassword_response_test.dart b/test/features/auth/data/models/response/resetpassword_response_test.dart new file mode 100644 index 0000000..febd035 --- /dev/null +++ b/test/features/auth/data/models/response/resetpassword_response_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; + +void main() { + group("ResetpasswordResponse", () { + + test("fromJson should parse correctly", () { + // Arrange + final json = { + "message": "Password reset successful", + "token": "abc123token", + }; + + // Act + final model = ResetpasswordResponse.fromJson(json); + + // Assert + expect(model.message, "Password reset successful"); + expect(model.token, "abc123token"); + }); + + test("toJson should return correct map", () { + // Arrange + final model = ResetpasswordResponse( + message: "Password reset successful", + token: "abc123token", + ); + + // Act + final json = model.toJson(); + + // Assert + expect(json["message"], "Password reset successful"); + expect(json["token"], "abc123token"); + }); + + test("copyWith should override only provided fields", () { + // Arrange + final model = ResetpasswordResponse( + message: "Old message", + token: "oldToken", + ); + + // Act + final updated = model.copyWith( + message: "New message", + ); + + // Assert + expect(updated.message, "New message"); + expect(updated.token, "oldToken"); // unchanged + }); + + test("should handle null values", () { + final model = ResetpasswordResponse(); + + expect(model.message, null); + expect(model.token, null); + + final json = model.toJson(); + expect(json.containsKey("message"), true); + expect(json.containsKey("token"), true); + }); + + }); +} diff --git a/test/features/auth/data/models/response/verifyreset_response_test.dart b/test/features/auth/data/models/response/verifyreset_response_test.dart new file mode 100644 index 0000000..5c1d76f --- /dev/null +++ b/test/features/auth/data/models/response/verifyreset_response_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; + +void main() { + group("VerifyresetResponse", () { + + test("fromJson should parse correctly", () { + // Arrange + final json = { + "status": "verified", + }; + + // Act + final model = VerifyresetResponse.fromJson(json); + + // Assert + expect(model.status, "verified"); + }); + + test("toJson should return correct map", () { + // Arrange + final model = VerifyresetResponse( + status: "verified", + ); + + // Act + final json = model.toJson(); + + // Assert + expect(json["status"], "verified"); + }); + + test("copyWith should override provided field", () { + // Arrange + final model = VerifyresetResponse( + status: "pending", + ); + + // Act + final updated = model.copyWith( + status: "verified", + ); + + // Assert + expect(updated.status, "verified"); + }); + + test("should handle null values", () { + final model = VerifyresetResponse(); + + expect(model.status, null); + + final json = model.toJson(); + expect(json.containsKey("status"), true); + }); + + }); +} diff --git a/test/features/auth/data/repos/auth_repo_impl_test.dart b/test/features/auth/data/repos/auth_repo_impl_test.dart index 7fa3eb4..3cc1078 100644 --- a/test/features/auth/data/repos/auth_repo_impl_test.dart +++ b/test/features/auth/data/repos/auth_repo_impl_test.dart @@ -1,23 +1,48 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; + import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/features/auth/data/datasource/auth_remote_datasource.dart'; import 'package:tracking_app/features/auth/data/model/response/LoginResponse.dart'; import 'package:tracking_app/features/auth/data/model/response/change_password_dto.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; +import 'package:tracking_app/features/auth/data/models/response/verifyreset_response.dart'; import 'package:tracking_app/features/auth/data/repos/auth_repo_impl.dart'; import 'package:tracking_app/features/auth/domain/models/change_password_model.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.dart'; +import 'package:tracking_app/features/auth/domain/models/verifyreset_entity.dart'; import 'auth_repo_impl_test.mocks.dart'; @GenerateMocks([AuthRemoteDataSource]) void main() { - late MockAuthRemoteDataSource mockDataSource; - late AuthRepoImp repo; + late MockAuthRemoteDataSource datasource; + late AuthRepoImpl repo; + + late MockAuthRemoteDataSource mockDataSource; // for login/changePassword tests + late AuthRepoImpl repoImp; setUpAll(() { - mockDataSource = MockAuthRemoteDataSource(); - repo = AuthRepoImp(mockDataSource); + // Provide dummy data for generics + provideDummy>( + SuccessApiResult( + data: ForgetpasswordResponse(message: '', info: ''), + ), + ); + provideDummy>( + SuccessApiResult( + data: VerifyresetResponse(status: ''), + ), + ); + provideDummy>( + SuccessApiResult( + data: ResetpasswordResponse(message: '', token: ''), + ), + ); provideDummy>( SuccessApiResult( data: LoginResponse(token: 'dummy', message: 'dummy'), @@ -28,121 +53,189 @@ void main() { ); }); + setUp(() { + datasource = MockAuthRemoteDataSource(); + repo = AuthRepoImpl(datasource); + + mockDataSource = MockAuthRemoteDataSource(); + repoImp = AuthRepoImpl(mockDataSource); + }); + + // ============================================================ + // forgetPassword + // ============================================================ + group("forgetPassword", () { + const email = "test@mail.com"; + + test("should return SuccessApiResult when datasource succeeds", () async { + final fakeDto = ForgetpasswordResponse(message: "Email sent", info: "Check inbox"); + + when(datasource.forgetPassword(any)).thenAnswer( + (_) async => SuccessApiResult(data: fakeDto), + ); + + final result = await repo.forgetPassword(email); + + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, "Email sent"); + expect(data.info, "Check inbox"); + + verify(datasource.forgetPassword(any)).called(1); + }); + + test("should return ErrorApiResult when datasource fails", () async { + when(datasource.forgetPassword(any)).thenAnswer( + (_) async => ErrorApiResult(error: "Network error"), + ); + + final result = await repo.forgetPassword(email); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Network error"); + + verify(datasource.forgetPassword(any)).called(1); + }); + }); + + // ============================================================ + // verifyResetCode + // ============================================================ + group("verifyResetCode", () { + const code = "123456"; + + test("should return SuccessApiResult when datasource succeeds", () async { + final fakeDto = VerifyresetResponse(status: "verified"); + + when(datasource.verifyResetCode(any)).thenAnswer( + (_) async => SuccessApiResult(data: fakeDto), + ); + + final result = await repo.verifyResetCode(code); + + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.status, "verified"); + + verify(datasource.verifyResetCode(any)).called(1); + }); + + test("should return ErrorApiResult when datasource fails", () async { + when(datasource.verifyResetCode(any)).thenAnswer( + (_) async => ErrorApiResult(error: "Invalid code"), + ); + + final result = await repo.verifyResetCode(code); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Invalid code"); + + verify(datasource.verifyResetCode(any)).called(1); + }); + }); + + // ============================================================ + // resetPassword + // ============================================================ + group("resetPassword", () { + final request = ResetPasswordRequest(email: "test@mail.com", newPassword: "12345678"); + + test("should return SuccessApiResult when datasource succeeds", () async { + final fakeDto = ResetpasswordResponse(message: "Password reset", token: "abc123"); + + when(datasource.resetPassword(request)).thenAnswer( + (_) async => SuccessApiResult(data: fakeDto), + ); + + final result = await repo.resetPassword(request); + + expect(result, isA>()); + final data = (result as SuccessApiResult).data; + expect(data.message, "Password reset"); + expect(data.token, "abc123"); + + verify(datasource.resetPassword(request)).called(1); + }); + + test("should return ErrorApiResult when datasource fails", () async { + when(datasource.resetPassword(request)).thenAnswer( + (_) async => ErrorApiResult(error: "Server error"), + ); + + final result = await repo.resetPassword(request); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Server error"); + + verify(datasource.resetPassword(request)).called(1); + }); + }); + + // ============================================================ + // login + // ============================================================ const tEmail = 'test@example.com'; const tPassword = 'password123'; final tLoginResponse = LoginResponse(token: 'token123', message: 'Success'); - group('AuthRepoImpl', () { - test( - 'should return SuccessApiResult when remote data source call is successful', - () async { - // Arrange - when( - mockDataSource.login(any), - ).thenAnswer((_) async => SuccessApiResult(data: tLoginResponse)); - - // Act - final result = await repo.login(tEmail, tPassword); - - // Assert - expect(result, isA>()); - expect( - (result as SuccessApiResult).data, - tLoginResponse, - ); - verify(mockDataSource.login(any)).called(1); - verifyNoMoreInteractions(mockDataSource); - }, - ); + group('AuthRepoImpl.login', () { + test('should return SuccessApiResult when remote data source call is successful', () async { + when(mockDataSource.login(any)).thenAnswer((_) async => SuccessApiResult(data: tLoginResponse)); - test( - 'should return ErrorApiResult when remote data source call fails', - () async { - // Arrange - const tErrorMessage = 'An error occurred'; - when( - mockDataSource.login(any), - ).thenAnswer((_) async => ErrorApiResult(error: tErrorMessage)); - - // Act - final result = await repo.login(tEmail, tPassword); - - // Assert - expect(result, isA>()); - expect((result as ErrorApiResult).error, tErrorMessage); - verify(mockDataSource.login(any)).called(1); - verifyNoMoreInteractions(mockDataSource); - }, - ); + final result = await repoImp.login(tEmail, tPassword); + + expect(result, isA>()); + expect((result as SuccessApiResult).data, tLoginResponse); + + verify(mockDataSource.login(any)).called(1); + verifyNoMoreInteractions(mockDataSource); + }); + + test('should return ErrorApiResult when remote data source call fails', () async { + const tErrorMessage = 'An error occurred'; + when(mockDataSource.login(any)).thenAnswer((_) async => ErrorApiResult(error: tErrorMessage)); + + final result = await repoImp.login(tEmail, tPassword); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, tErrorMessage); + + verify(mockDataSource.login(any)).called(1); + verifyNoMoreInteractions(mockDataSource); + }); }); + // ============================================================ + // changePassword + // ============================================================ group("AuthRepoImpl.changePassword()", () { - test( - 'should return ApiSuccess when changePassword datasource succeeds', - () async { - final fakeDto = ChangePasswordDto( - message: 'Success', - token: 'fake_token', - error: null, - ); - - when( - mockDataSource.changePassword( - password: anyNamed('password'), - newPassword: anyNamed('newPassword'), - ), - ).thenAnswer( - (_) async => SuccessApiResult(data: fakeDto), - ); - - final result = - await repo.changePassword( - password: 'Mm@123456', - newPassword: 'Mmmm@123', - ) - as SuccessApiResult; - - expect(result, isA>()); - expect(result.data.token, fakeDto.token); - expect(result.data.message, fakeDto.message); - verify( - mockDataSource.changePassword( - password: anyNamed('password'), - newPassword: anyNamed('newPassword'), - ), - ).called(1); - }, - ); + test('should return ApiSuccess when changePassword datasource succeeds', () async { + final fakeDto = ChangePasswordDto(message: 'Success', token: 'fake_token', error: null); - test( - 'should return ApiFailure when changePassword datasource throws exception', - () async { - when( - mockDataSource.changePassword( - password: anyNamed('password'), - newPassword: anyNamed('newPassword'), - ), - ).thenAnswer( - (_) async => - ErrorApiResult(error: 'Network error'), - ); - - final result = - await repo.changePassword( - password: 'Mm@123456', - newPassword: 'Mmmm@123', - ) - as ErrorApiResult; - - expect(result, isA>()); - expect(result.error.toString(), contains("Network error")); - verify( - mockDataSource.changePassword( - password: anyNamed('password'), - newPassword: anyNamed('newPassword'), - ), - ).called(1); - }, - ); + when(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))) + .thenAnswer((_) async => SuccessApiResult(data: fakeDto)); + + final result = await repoImp.changePassword(password: 'Mm@123456', newPassword: 'Mmmm@123') + as SuccessApiResult; + + expect(result, isA>()); + expect(result.data.token, fakeDto.token); + expect(result.data.message, fakeDto.message); + + verify(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))).called(1); + }); + + test('should return ApiFailure when changePassword datasource throws exception', () async { + when(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))) + .thenAnswer((_) async => ErrorApiResult(error: 'Network error')); + + final result = await repoImp.changePassword(password: 'Mm@123456', newPassword: 'Mmmm@123') + as ErrorApiResult; + + expect(result, isA>()); + expect(result.error.toString(), contains("Network error")); + + verify(mockDataSource.changePassword(password: anyNamed('password'), newPassword: anyNamed('newPassword'))).called(1); + }); }); } diff --git a/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart b/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart new file mode 100644 index 0000000..a3438d7 --- /dev/null +++ b/test/features/auth/domain/usecase/forgetpassword_usecase_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; +import 'package:tracking_app/features/auth/domain/usecase/forgetpassword_usecase.dart'; + +import 'forgetpassword_usecase_test.mocks.dart'; + +@GenerateMocks([AuthRepo]) +void main() { + late MockAuthRepo mockRepo; + late ForgetPasswordUsecase usecase; + + setUpAll(() { + provideDummy>( + SuccessApiResult( + data: ForgetPasswordEntitiy(message: '', info: ''), + ), + ); + }); + + setUp(() { + mockRepo = MockAuthRepo(); + usecase = ForgetPasswordUsecase(mockRepo); + }); + + group("ForgetPasswordUsecase", () { + const email = "test@mail.com"; + + test("returns SuccessApiResult when repo succeeds", () async { + final entity = + ForgetPasswordEntitiy(message: "Email sent", info: "Check inbox"); + + when(mockRepo.forgetPassword(email)).thenAnswer( + (_) async => SuccessApiResult(data: entity), + ); + + final result = await usecase.call(email); + + expect(result, isA>()); + expect((result as SuccessApiResult).data.message, "Email sent"); + + verify(mockRepo.forgetPassword(email)).called(1); + }); + + test("returns ErrorApiResult when repo fails", () async { + when(mockRepo.forgetPassword(email)).thenAnswer( + (_) async => + ErrorApiResult(error: "Network error"), + ); + + final result = await usecase.call(email); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Network error"); + + verify(mockRepo.forgetPassword(email)).called(1); + }); + }); +} diff --git a/test/features/auth/domain/usecase/resertpassword_usecase_test.dart b/test/features/auth/domain/usecase/resertpassword_usecase_test.dart new file mode 100644 index 0000000..c095518 --- /dev/null +++ b/test/features/auth/domain/usecase/resertpassword_usecase_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; +import 'package:tracking_app/features/auth/domain/usecase/resertpassword_usecase.dart'; + +import 'forgetpassword_usecase_test.mocks.dart'; + + +@GenerateMocks([AuthRepo]) +void main() { + late MockAuthRepo mockRepo; + late ResetPasswordUsecase usecase; + + setUpAll(() { + provideDummy>( + SuccessApiResult( + data: ResetPasswordEntity(token: '', message: ''), + ), + ); + }); + + setUp(() { + mockRepo = MockAuthRepo(); + usecase = ResetPasswordUsecase(mockRepo); + }); + + group("ResetPasswordUsecase", () { + final request = ResetPasswordRequest( + email: "test@mail.com", + newPassword: "12345678", + ); + + test("returns SuccessApiResult when repo succeeds", () async { + final entity = + ResetPasswordEntity(token: "abc123", message: "Password reset"); + + when(mockRepo.resetPassword(request)).thenAnswer( + (_) async => SuccessApiResult(data: entity), + ); + + final result = await usecase.call(request); + + expect(result, isA>()); + expect((result as SuccessApiResult).data.token, "abc123"); + + verify(mockRepo.resetPassword(request)).called(1); + }); + + test("returns ErrorApiResult when repo fails", () async { + when(mockRepo.resetPassword(request)).thenAnswer( + (_) async => + ErrorApiResult(error: "Server error"), + ); + + final result = await usecase.call(request); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Server error"); + + verify(mockRepo.resetPassword(request)).called(1); + }); + }); +} diff --git a/test/features/auth/domain/usecase/verifyreaset_usecase_test.dart b/test/features/auth/domain/usecase/verifyreaset_usecase_test.dart new file mode 100644 index 0000000..6831cf0 --- /dev/null +++ b/test/features/auth/domain/usecase/verifyreaset_usecase_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/domain/models/verifyreset_entity.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; +import 'package:tracking_app/features/auth/domain/usecase/verifyreaset_usecase.dart'; + +import 'forgetpassword_usecase_test.mocks.dart'; + +@GenerateMocks([AuthRepo]) +void main() { + late MockAuthRepo mockRepo; + late VerifyResetCodeUsecase usecase; + + setUpAll(() { + provideDummy>( + SuccessApiResult( + data: VerifyResetCodeEntity(status: ''), + ), + ); + }); + + setUp(() { + mockRepo = MockAuthRepo(); + usecase = VerifyResetCodeUsecase(mockRepo); + }); + + group("VerifyResetCodeUsecase", () { + const code = "123456"; + + test("returns SuccessApiResult when repo succeeds", () async { + final entity = VerifyResetCodeEntity(status: "verified"); + + when(mockRepo.verifyResetCode(code)).thenAnswer( + (_) async => SuccessApiResult(data: entity), + ); + + final result = await usecase.call(code); + + expect(result, isA>()); + expect((result as SuccessApiResult).data.status, "verified"); + + verify(mockRepo.verifyResetCode(code)).called(1); + }); + + test("returns ErrorApiResult when repo fails", () async { + when(mockRepo.verifyResetCode(code)).thenAnswer( + (_) async => + ErrorApiResult(error: "Invalid code"), + ); + + final result = await usecase.call(code); + + expect(result, isA>()); + expect((result as ErrorApiResult).error, "Invalid code"); + + verify(mockRepo.verifyResetCode(code)).called(1); + }); + }); +} diff --git a/test/features/auth/presentation/apply/view/apply_screen_test.dart b/test/features/auth/presentation/apply/view/apply_screen_test.dart index 6d36766..ae53288 100644 --- a/test/features/auth/presentation/apply/view/apply_screen_test.dart +++ b/test/features/auth/presentation/apply/view/apply_screen_test.dart @@ -1,204 +1,68 @@ -import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:tracking_app/features/auth/data/models/request/apply_request_model.dart'; -import 'package:tracking_app/features/auth/domain/entities/country_entity.dart'; -import 'package:tracking_app/features/auth/presentation/apply/manager/apply_intent.dart'; -import 'package:tracking_app/features/auth/presentation/apply/manager/apply_state.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:tracking_app/features/auth/presentation/apply/view/apply_success_view.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MockAssetLoader extends AssetLoader { + const MockAssetLoader(); + + @override + Future> load(String path, Locale locale) async { + return { + "applicationSubmitted": "Application Submitted!", + "congratulationsMessage": + "Congratulations! Your application has been submitted successfully.", + "backToLogin": "Back to Login", + }; + } +} void main() { - group('ApplySuccessScreen Widget Tests -', () { - testWidgets('should display success message', (tester) async { - // Act - await tester.pumpWidget(const MaterialApp(home: ApplySuccessScreen())); - await tester.pumpAndSettle(); - - // Assert - expect(find.text('Application Submitted!'), findsOneWidget); - expect( - find.text( - 'Congratulations! Your application has been submitted successfully.', - ), - findsOneWidget, - ); - }); - - testWidgets('should display back to login button', (tester) async { - // Act - await tester.pumpWidget(const MaterialApp(home: ApplySuccessScreen())); - await tester.pumpAndSettle(); - - // Assert - expect(find.text('Back to Login'), findsOneWidget); - expect(find.byType(ElevatedButton), findsOneWidget); - }); - - testWidgets('should display success icon', (tester) async { - // Act - await tester.pumpWidget(const MaterialApp(home: ApplySuccessScreen())); - await tester.pumpAndSettle(); - - // Assert - Check for circular container with success decoration - final container = tester.widget( - find - .descendant( - of: find.byType(Column).first, - matching: find.byType(Container), - ) - .first, - ); - - final decoration = container.decoration as BoxDecoration?; - expect(decoration?.shape, BoxShape.circle); - }); - - testWidgets('should navigate when back button is tapped', (tester) async { - // Arrange - bool navigationCalled = false; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const ApplySuccessScreen(), - ), - ).then((_) => navigationCalled = true); - }, - child: const Text('Go to Success'), - ); - }, - ), - ), - ), - ); - - // Navigate to success screen - await tester.tap(find.text('Go to Success')); - await tester.pumpAndSettle(); - - // Verify we're on success screen - expect(find.text('Application Submitted!'), findsOneWidget); - - // Tap back to login button - await tester.tap(find.text('Back to Login')); - await tester.pumpAndSettle(); - - // Assert - Should navigate back to first route - expect(find.text('Go to Success'), findsOneWidget); - }); + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); }); - group('ApplyState Tests -', () { - test('initial state should have correct default values', () { - // Act - const state = ApplyState(); - - // Assert - expect(state.status, ApplyStatus.initial); - expect(state.countries, isEmpty); - expect(state.errorMessage, isNull); - expect(state.vehiclesStatus, ApplyStatus.initial); - expect(state.vehicles, isEmpty); - expect(state.vehiclesErrorMessage, isNull); - expect(state.applyStatus, ApplyStatus.initial); - expect(state.applyErrorMessage, isNull); - }); - - test('copyWith should update only specified fields', () { - // Arrange - const initialState = ApplyState(); - final countries = [ - const CountryEntity( - name: 'Egypt', - isoCode: 'EG', - flag: '🇪🇬', - phoneCode: '20', - ), - ]; - - // Act - final newState = initialState.copyWith( - status: ApplyStatus.success, - countries: countries, - ); - - // Assert - expect(newState.status, ApplyStatus.success); - expect(newState.countries, countries); - expect(newState.vehiclesStatus, ApplyStatus.initial); // Unchanged - expect(newState.applyStatus, ApplyStatus.initial); // Unchanged - }); - - test('state should support equality comparison', () { - // Arrange - const state1 = ApplyState(); - const state2 = ApplyState(); - - // Assert - expect(state1, equals(state2)); - }); - - test('different states should not be equal', () { - // Arrange - const state1 = ApplyState(status: ApplyStatus.initial); - const state2 = ApplyState(status: ApplyStatus.loading); - - // Assert - expect(state1, isNot(equals(state2))); - }); + Widget createWidgetUnderTest() { + return EasyLocalization( + supportedLocales: const [Locale('en')], + fallbackLocale: const Locale('en'), + path: 'assets/translations', + assetLoader: const MockAssetLoader(), + child: Builder( + builder: (context) { + return MaterialApp( + locale: context.locale, + supportedLocales: context.supportedLocales, + localizationsDelegates: context.localizationDelegates, + home: const ApplySuccessScreen(), + ); + }, + ), + ); + } + + testWidgets('should display success message', (tester) async { + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + // Test for the actual text rendered, not the key + expect(find.text('Application Submitted!'), findsOneWidget); + expect( + find.text( + 'Congratulations! Your application has been submitted successfully.', + ), + findsOneWidget, + ); }); - group('ApplyIntent Tests -', () { - test('GetCountriesIntent should be created', () { - // Act - final intent = GetCountriesIntent(); - - // Assert - expect(intent, isA()); - expect(intent, isA()); - }); - - test('GetVehiclesIntent should be created', () { - // Act - final intent = GetVehiclesIntent(); - - // Assert - expect(intent, isA()); - expect(intent, isA()); - }); - - test('SubmitApplyIntent should be created with request model', () { - // Arrange - final requestModel = ApplyRequestModel( - country: 'EG', - firstName: 'John', - lastName: 'Doe', - vehicleType: '1', - vehicleNumber: 'ABC123', - email: 'john@example.com', - phone: '+201234567890', - NID: '12345678901234', - password: 'Password123!', - rePassword: 'Password123!', - gender: 'male', - vehicleLicense: null, - NIDimg: null, - ); - - // Act - final intent = SubmitApplyIntent(requestModel); + testWidgets('should display back to login button', (tester) async { + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); - // Assert - expect(intent, isA()); - expect(intent, isA()); - expect(intent.applyRequestModel, requestModel); - }); + expect(find.text('Back to Login'), findsOneWidget); + expect(find.byType(ElevatedButton), findsOneWidget); }); } diff --git a/test/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit_test.dart b/test/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit_test.dart new file mode 100644 index 0000000..c05ea88 --- /dev/null +++ b/test/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/auth/domain/usecase/forgetpassword_usecase.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy.dart'; +import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; + +class MockForgetPasswordUsecase extends Mock + implements ForgetPasswordUsecase {} + +class MockAuthStorage extends Mock implements AuthStorage {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late ForgetPasswordCubit cubit; + late MockForgetPasswordUsecase mockUsecase; + late MockAuthStorage mockAuthStorage; + + setUp(() { + mockUsecase = MockForgetPasswordUsecase(); + mockAuthStorage = MockAuthStorage(); + cubit = ForgetPasswordCubit(mockUsecase, mockAuthStorage); + }); + + tearDown(() async { + await cubit.close(); + }); + + group('ForgetPasswordCubit', () { + test('initial state is correct', () { + expect(cubit.state.isFormValid, false); + expect(cubit.state.resource.status, Status.initial); + }); + + test('FormChangedIntent updates isFormValid', () { + cubit.emailController.text = 'test@mail.com'; + cubit.doIntent(const FormChangedIntent()); + + expect(cubit.state.isFormValid, true); + }); + + test('Submit emits success', () async { + final entity = ForgetPasswordEntitiy(message: 'Reset email sent', info: 'Check your inbox'); + + cubit.emailController.text = 'test@mail.com'; + cubit.doIntent(const FormChangedIntent()); + + when(() => mockUsecase(any())) + .thenAnswer((_) async => SuccessApiResult(data: entity)); + + await cubit.doIntent(const SubmitForgetPasswordIntent()); + + expect(cubit.state.resource.status, Status.success); + expect(cubit.state.resource.data, entity); + }); + + test('Submit emits error', () async { + cubit.emailController.text = 'test@mail.com'; + cubit.doIntent(const FormChangedIntent()); + + when(() => mockUsecase(any())) + .thenAnswer((_) async => ErrorApiResult(error: 'Error')); + + await cubit.doIntent(const SubmitForgetPasswordIntent()); + + expect(cubit.state.resource.status, Status.error); + }); + }); +} \ No newline at end of file diff --git a/test/features/auth/presentation/forget_pass/pages/forget_pass_page_test.dart b/test/features/auth/presentation/forget_pass/pages/forget_pass_page_test.dart new file mode 100644 index 0000000..ed294f7 --- /dev/null +++ b/test/features/auth/presentation/forget_pass/pages/forget_pass_page_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:tracking_app/app/config/base_state/base_state.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart'; +import 'package:tracking_app/features/auth/presentation/forget_pass/pages/forget_pass_page.dart'; + +class MockForgetPasswordCubit extends Mock + implements ForgetPasswordCubit {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MockForgetPasswordCubit mockCubit; + + setUp(() { + mockCubit = MockForgetPasswordCubit(); + + when(() => mockCubit.state) + .thenReturn(ForgetPasswordState.initial()); + + when(() => mockCubit.stream) + .thenAnswer((_) => const Stream.empty()); + + when(() => mockCubit.formKey) + .thenReturn(GlobalKey()); + + when(() => mockCubit.emailController) + .thenReturn(TextEditingController()); + }); + + Widget buildTestableWidget() { + final router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (context, state) { + return BlocProvider.value( + value: mockCubit, + child: const ForgetPasswordPage(), + ); + }, + ), + GoRoute( + path: '/verify', + builder: (context, state) => const Scaffold(), + ), + ], + ); + + return MaterialApp.router( + routerConfig: router, + ); + } + + testWidgets('renders ForgetPasswordPage correctly', + (WidgetTester tester) async { + await tester.pumpWidget(buildTestableWidget()); + await tester.pumpAndSettle(); + + expect(find.byType(AppBar), findsOneWidget); + }); + + testWidgets('shows loading indicator when loading', + (WidgetTester tester) async { + when(() => mockCubit.state).thenReturn( + ForgetPasswordState( + resource: Resource.loading(), + isFormValid: true, + ), + ); + + await tester.pumpWidget(buildTestableWidget()); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); +} \ No newline at end of file diff --git a/test/features/auth/presentation/login/pages/loginScreen_test.dart b/test/features/auth/presentation/login/pages/loginScreen_test.dart index 45a98cd..d3966c7 100644 --- a/test/features/auth/presentation/login/pages/loginScreen_test.dart +++ b/test/features/auth/presentation/login/pages/loginScreen_test.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:get_it/get_it.dart'; import 'package:mockito/annotations.dart'; import 'package:tracking_app/app/config/auth_storage/auth_storage.dart'; @@ -18,14 +21,16 @@ void main() { late LoginCubit loginCubit; late GetIt getIt; - setUp(() { + setUp(() async { + // Mock shared preferences to avoid MissingPluginException + SharedPreferences.setMockInitialValues({}); + getIt = GetIt.instance; mockAuthRepo = MockAuthRepo(); mockAuthStorage = MockAuthStorage(); loginUseCase = LoginUseCase(mockAuthRepo); loginCubit = LoginCubit(loginUseCase, mockAuthStorage); - // Register LoginCubit in GetIt if (getIt.isRegistered()) { getIt.unregister(); } @@ -38,30 +43,41 @@ void main() { }); Widget createWidgetUnderTest() { - return MaterialApp(home: const LoginScreen()); + return EasyLocalization( + supportedLocales: const [Locale('en')], + path: 'assets/langs', + fallbackLocale: const Locale('en'), + child: MaterialApp( + localizationsDelegates: GlobalMaterialLocalizations.delegates, + home: const LoginScreen(), + ), + ); } testWidgets('LoginScreen renders correctly', (WidgetTester tester) async { - // Act + await EasyLocalization.ensureInitialized(); // initialize EasyLocalization await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); - // Assert expect(find.text('email'), findsOneWidget); expect(find.text('password'), findsOneWidget); - expect(find.text('continueTxt'), findsOneWidget); }); - testWidgets('Enters text into email and password fields', ( - WidgetTester tester, - ) async { - // Act + testWidgets('Enters text into email and password fields', (tester) async { + await EasyLocalization.ensureInitialized(); await tester.pumpWidget(createWidgetUnderTest()); await tester.enterText(find.byType(TextFormField).first, 'test@test.com'); await tester.enterText(find.byType(TextFormField).last, 'password123'); await tester.pump(); - // Assert - expect(find.text('test@test.com'), findsOneWidget); - expect(find.text('password123'), findsOneWidget); + final emailField = tester.widget( + find.byType(TextFormField).first, + ); + expect(emailField.controller?.text, 'test@test.com'); + + final passwordField = tester.widget( + find.byType(TextFormField).last, + ); + expect(passwordField.controller?.text, 'password123'); }); } diff --git a/test_out.json b/test_out.json new file mode 100644 index 0000000..3abc33b Binary files /dev/null and b/test_out.json differ diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000..6da05d0 Binary files /dev/null and b/test_output.txt differ