From f7f3face54dfebef07f8c5c2c3d7e4a7b087bda9 Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Fri, 13 Feb 2026 09:41:23 +0200 Subject: [PATCH 1/6] feat(SCRUM-78)implemented api & data layer --- .metadata | 25 +---- .vscode/settings.json | 5 + lib/app/config/di/di.config.dart | 20 ++-- lib/app/core/api_manger/api_client.dart | 28 ++++- lib/app/core/api_manger/api_client.g.dart | 102 +++++++++++++++++- lib/app/core/network/api_result.dart | 2 +- lib/app/core/values/app_endpoint_strings.dart | 6 +- .../auth_remote_datasource_impl.dart | 36 +++++++ .../datasource/auth_remote_datasource.dart | 19 ++++ .../request/forget_password_request.dart | 11 ++ .../models/request/resetpassword_request.dart | 12 +++ .../models/request/verifyreset_request.dart | 11 ++ .../response/forgetpassword_response.dart | 31 ++++++ .../response/resetpassword_response.dart | 30 ++++++ .../models/response/verifyreset_response.dart | 25 +++++ .../auth/data/repos/auth_repo_impl.dart | 31 ++++++ lib/features/auth/domain/repos/auth_repo.dart | 7 ++ lib/generated/locale_keys.g.dart | 15 ++- pubspec.lock | 16 +-- 19 files changed, 382 insertions(+), 50 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/features/auth/data/models/request/forget_password_request.dart create mode 100644 lib/features/auth/data/models/request/resetpassword_request.dart create mode 100644 lib/features/auth/data/models/request/verifyreset_request.dart create mode 100644 lib/features/auth/data/models/response/forgetpassword_response.dart create mode 100644 lib/features/auth/data/models/response/resetpassword_response.dart create mode 100644 lib/features/auth/data/models/response/verifyreset_response.dart 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..9f758fb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "Forgetpassword" + ] +} \ No newline at end of file diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index edcef9a..b733963 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -12,25 +12,33 @@ import 'package:dio/dio.dart' as _i361; import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; +import '../../../features/auth/api/datasource/auth_remote_datasource_impl.dart' + as _i777; +import '../../../features/auth/data/datasource/auth_remote_datasource.dart' + as _i708; import '../../core/api_manger/api_client.dart' as _i890; import '../auth_storage/auth_storage.dart' as _i603; import '../network/network_module.dart' as _i200; extension GetItInjectableX on _i174.GetIt { - // initializes the registration of main-scope dependencies inside of GetIt +// initializes the registration of main-scope dependencies inside of GetIt _i174.GetIt init({ String? environment, _i526.EnvironmentFilter? environmentFilter, }) { - final gh = _i526.GetItHelper(this, environment, environmentFilter); + final gh = _i526.GetItHelper( + this, + environment, + environmentFilter, + ); final networkModule = _$NetworkModule(); gh.lazySingleton<_i603.AuthStorage>(() => _i603.AuthStorage()); gh.lazySingleton<_i361.Dio>( - () => networkModule.dio(gh<_i603.AuthStorage>()), - ); + () => networkModule.dio(gh<_i603.AuthStorage>())); gh.lazySingleton<_i890.ApiClient>( - () => networkModule.authApiClient(gh<_i361.Dio>()), - ); + () => networkModule.authApiClient(gh<_i361.Dio>())); + gh.factory<_i708.AuthRemoteDatasource>( + () => _i777.AuthRemoteDatasourceImpl(gh<_i890.ApiClient>())); return this; } } diff --git a/lib/app/core/api_manger/api_client.dart b/lib/app/core/api_manger/api_client.dart index 9337139..58f0170 100644 --- a/lib/app/core/api_manger/api_client.dart +++ b/lib/app/core/api_manger/api_client.dart @@ -1,8 +1,34 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:retrofit/http.dart'; +import 'package:retrofit/dio.dart'; +import 'package:tracking_app/app/core/network/api_result.dart'; +import 'package:tracking_app/app/core/values/app_endpoint_strings.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'; + 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, + ); + @POST(AppEndpointString.resetPassword) + Future> resetPassword( + @Body() ResetPasswordRequest request + ); + + @POST(AppEndpointString.verifyResetCode) + Future> verifyResetCode( + @Body() VerifyResetRequest request + ); } diff --git a/lib/app/core/api_manger/api_client.g.dart b/lib/app/core/api_manger/api_client.g.dart index e6dac36..fece798 100644 --- a/lib/app/core/api_manger/api_client.g.dart +++ b/lib/app/core/api_manger/api_client.g.dart @@ -9,12 +9,107 @@ part of 'api_client.dart'; // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers class _ApiClient implements ApiClient { - _ApiClient(this._dio, {this.baseUrl}); + _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: 'POST', + 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; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || @@ -28,7 +123,10 @@ class _ApiClient implements ApiClient { return requestOptions; } - String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { if (baseUrl == null || baseUrl.trim().isEmpty) { return dioBaseUrl; } diff --git a/lib/app/core/network/api_result.dart b/lib/app/core/network/api_result.dart index 44f22b1..48ca88f 100644 --- a/lib/app/core/network/api_result.dart +++ b/lib/app/core/network/api_result.dart @@ -1,4 +1,4 @@ -sealed class ApiResult {} + class ApiResult {} class SuccessApiResult extends ApiResult { final T data; diff --git a/lib/app/core/values/app_endpoint_strings.dart b/lib/app/core/values/app_endpoint_strings.dart index 2e41afd..9daca93 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 8b13789..f6a3c50 100644 --- a/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart +++ b/lib/features/auth/api/datasource/auth_remote_datasource_impl.dart @@ -1 +1,37 @@ +import 'package:injectable/injectable.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/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/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'; +@Injectable(as: AuthRemoteDatasource) +class AuthRemoteDatasourceImpl implements AuthRemoteDatasource { + ApiClient apiClient; + AuthRemoteDatasourceImpl(this.apiClient); + @override + 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)); + } +} diff --git a/lib/features/auth/data/datasource/auth_remote_datasource.dart b/lib/features/auth/data/datasource/auth_remote_datasource.dart index 8b13789..916fbbd 100644 --- a/lib/features/auth/data/datasource/auth_remote_datasource.dart +++ b/lib/features/auth/data/datasource/auth_remote_datasource.dart @@ -1 +1,20 @@ +import 'package:injectable/injectable.dart'; +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'; +abstract class AuthRemoteDatasource { + 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..2249178 --- /dev/null +++ b/lib/features/auth/data/models/response/forgetpassword_response.dart @@ -0,0 +1,31 @@ + +import 'package:json_annotation/json_annotation.dart'; +import 'dart:convert'; + +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..40cf99a --- /dev/null +++ b/lib/features/auth/data/models/response/resetpassword_response.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'dart:convert'; + +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..d19aa80 --- /dev/null +++ b/lib/features/auth/data/models/response/verifyreset_response.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'dart:convert'; + +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 8b13789..05dc3e6 100644 --- a/lib/features/auth/data/repos/auth_repo_impl.dart +++ b/lib/features/auth/data/repos/auth_repo_impl.dart @@ -1 +1,32 @@ +import 'package:injectable/injectable.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/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/response/forgetpassword_response.dart'; +import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; +@Injectable(as: AuthRepo) +class AuthRepoImpl implements AuthRepo { + AuthRemoteDatasource remoteDatasource; + AuthRepoImpl(this.remoteDatasource); + + @override + Future forgetPassword(String email) async { + final result = await remoteDatasource.forgetPassword( + ForgetPasswordRequest(email: email), + ); + } + + @override + Future verifyResetCode(String code) { + // TODO: implement verifyResetCode + throw UnimplementedError(); + } + + @override + Future resetPassword(ResetPasswordRequest request) { + // TODO: implement resetPassword + throw UnimplementedError(); + } +} diff --git a/lib/features/auth/domain/repos/auth_repo.dart b/lib/features/auth/domain/repos/auth_repo.dart index 8b13789..e1b927b 100644 --- a/lib/features/auth/domain/repos/auth_repo.dart +++ b/lib/features/auth/domain/repos/auth_repo.dart @@ -1 +1,8 @@ +import 'package:tracking_app/features/auth/data/models/request/resetpassword_request.dart'; +import 'package:tracking_app/features/auth/data/models/response/resetpassword_response.dart'; +abstract class AuthRepo { + Future forgetPassword(String email); + Future verifyResetCode(String code); + Future resetPassword(ResetPasswordRequest request); +} diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 1763fd6..ccc7bdf 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const firstName = 'firstName'; static const lastName = 'lastName'; static const email = 'email'; @@ -65,8 +65,7 @@ abstract class LocaleKeys { 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 check_email_for_verification_code = 'check_email_for_verification_code'; static const passwordValidation = 'passwordValidation'; static const connectionTimeout = 'connectionTimeout'; static const noInternet = 'noInternet'; @@ -174,10 +173,8 @@ abstract class LocaleKeys { 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 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'; @@ -199,8 +196,8 @@ abstract class LocaleKeys { 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 notification_deleted_successfully = 'notification_deleted_successfully'; static const clear_all = 'clear_all'; static const no_notifications_yet = 'no_notifications_yet'; + } diff --git a/pubspec.lock b/pubspec.lock index 8f59585..779a2a3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -873,10 +873,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1270,26 +1270,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" timezone: dependency: transitive description: From 1278abde005d81b53aec27ec8247eb73149bb590 Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Fri, 13 Feb 2026 13:28:52 +0200 Subject: [PATCH 2/6] feat(SCRUM-78) Auth Repository implementation --- lib/app/core/api_manger/api_client.dart | 6 +- .../response/forgetpassword_response.dart | 32 ++++----- .../auth/data/repos/auth_repo_impl.dart | 65 ++++++++++++++++--- .../domain/models/forgetpassword_entitiy.dart | 6 ++ .../domain/models/resetpassword_entity.dart | 6 ++ .../domain/models/verifyreset_entity.dart | 5 ++ 6 files changed, 90 insertions(+), 30 deletions(-) create mode 100644 lib/features/auth/domain/models/forgetpassword_entitiy.dart create mode 100644 lib/features/auth/domain/models/resetpassword_entity.dart create mode 100644 lib/features/auth/domain/models/verifyreset_entity.dart diff --git a/lib/app/core/api_manger/api_client.dart b/lib/app/core/api_manger/api_client.dart index 58f0170..82a58c7 100644 --- a/lib/app/core/api_manger/api_client.dart +++ b/lib/app/core/api_manger/api_client.dart @@ -24,11 +24,11 @@ abstract class ApiClient { ); @POST(AppEndpointString.resetPassword) Future> resetPassword( - @Body() ResetPasswordRequest request + @Body() ResetPasswordRequest request, ); - @POST(AppEndpointString.verifyResetCode) + @POST(AppEndpointString.verifyResetCode) Future> verifyResetCode( - @Body() VerifyResetRequest request + @Body() VerifyResetRequest request, ); } diff --git a/lib/features/auth/data/models/response/forgetpassword_response.dart b/lib/features/auth/data/models/response/forgetpassword_response.dart index 2249178..3b15544 100644 --- a/lib/features/auth/data/models/response/forgetpassword_response.dart +++ b/lib/features/auth/data/models/response/forgetpassword_response.dart @@ -1,4 +1,3 @@ - import 'package:json_annotation/json_annotation.dart'; import 'dart:convert'; @@ -6,26 +5,21 @@ part 'forgetpassword_response.g.dart'; @JsonSerializable() class ForgetpasswordResponse { - @JsonKey(name: "message") - final String? message; - @JsonKey(name: "info") - final String? info; + @JsonKey(name: "message") + final String? message; + @JsonKey(name: "info") + final String? info; - ForgetpasswordResponse({ - this.message, - this.info, - }); + ForgetpasswordResponse({this.message, this.info}); - ForgetpasswordResponse copyWith({ - String? message, - String? info, - }) => - ForgetpasswordResponse( - message: message ?? this.message, - info: info ?? this.info, - ); + ForgetpasswordResponse copyWith({String? message, String? info}) => + ForgetpasswordResponse( + message: message ?? this.message, + info: info ?? this.info, + ); - factory ForgetpasswordResponse.fromJson(Map json) => _$ForgetpasswordResponseFromJson(json); + factory ForgetpasswordResponse.fromJson(Map json) => + _$ForgetpasswordResponseFromJson(json); - Map toJson() => _$ForgetpasswordResponseToJson(this); + Map toJson() => _$ForgetpasswordResponseToJson(this); } diff --git a/lib/features/auth/data/repos/auth_repo_impl.dart b/lib/features/auth/data/repos/auth_repo_impl.dart index 05dc3e6..200034a 100644 --- a/lib/features/auth/data/repos/auth_repo_impl.dart +++ b/lib/features/auth/data/repos/auth_repo_impl.dart @@ -3,7 +3,13 @@ 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/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 '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) @@ -11,22 +17,65 @@ class AuthRepoImpl implements AuthRepo { AuthRemoteDatasource remoteDatasource; AuthRepoImpl(this.remoteDatasource); - @override - Future forgetPassword(String email) async { + Future> forgetPassword(String email) async { final result = await remoteDatasource.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) { - // TODO: implement verifyResetCode - throw UnimplementedError(); + Future> verifyResetCode(String code) async { + final result = await remoteDatasource.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) { - // TODO: implement resetPassword - throw UnimplementedError(); + Future> resetPassword( + ResetPasswordRequest request, + ) async { + final result = await remoteDatasource.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'); } } 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..803031c --- /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}); +} \ No newline at end of file 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}); +} From 49447fa052019bf076f364835d82539cb8a8c9c6 Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Fri, 13 Feb 2026 14:03:52 +0200 Subject: [PATCH 3/6] feat(SCRUM-78): implement use cases --- .../data/datasource/auth_remote_datasource.dart | 1 - .../models/response/forgetpassword_response.dart | 1 - .../models/response/resetpassword_response.dart | 1 - .../data/models/response/verifyreset_response.dart | 1 - lib/features/auth/domain/repos/auth_repo.dart | 11 +++++++---- .../domain/usecase/forgetpassword_usecase.dart | 14 ++++++++++++++ .../auth/domain/usecase/login_usecase.dart | 1 - .../domain/usecase/resertpassword_usecase.dart | 14 ++++++++++++++ .../auth/domain/usecase/verifyreaset_usecase.dart | 13 +++++++++++++ 9 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 lib/features/auth/domain/usecase/forgetpassword_usecase.dart delete mode 100644 lib/features/auth/domain/usecase/login_usecase.dart create mode 100644 lib/features/auth/domain/usecase/resertpassword_usecase.dart create mode 100644 lib/features/auth/domain/usecase/verifyreaset_usecase.dart diff --git a/lib/features/auth/data/datasource/auth_remote_datasource.dart b/lib/features/auth/data/datasource/auth_remote_datasource.dart index 916fbbd..ecb9bff 100644 --- a/lib/features/auth/data/datasource/auth_remote_datasource.dart +++ b/lib/features/auth/data/datasource/auth_remote_datasource.dart @@ -1,4 +1,3 @@ -import 'package:injectable/injectable.dart'; 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'; diff --git a/lib/features/auth/data/models/response/forgetpassword_response.dart b/lib/features/auth/data/models/response/forgetpassword_response.dart index 3b15544..50e6628 100644 --- a/lib/features/auth/data/models/response/forgetpassword_response.dart +++ b/lib/features/auth/data/models/response/forgetpassword_response.dart @@ -1,5 +1,4 @@ import 'package:json_annotation/json_annotation.dart'; -import 'dart:convert'; part 'forgetpassword_response.g.dart'; diff --git a/lib/features/auth/data/models/response/resetpassword_response.dart b/lib/features/auth/data/models/response/resetpassword_response.dart index 40cf99a..0f02da4 100644 --- a/lib/features/auth/data/models/response/resetpassword_response.dart +++ b/lib/features/auth/data/models/response/resetpassword_response.dart @@ -1,5 +1,4 @@ import 'package:json_annotation/json_annotation.dart'; -import 'dart:convert'; part 'resetpassword_response.g.dart'; diff --git a/lib/features/auth/data/models/response/verifyreset_response.dart b/lib/features/auth/data/models/response/verifyreset_response.dart index d19aa80..5558a51 100644 --- a/lib/features/auth/data/models/response/verifyreset_response.dart +++ b/lib/features/auth/data/models/response/verifyreset_response.dart @@ -1,5 +1,4 @@ import 'package:json_annotation/json_annotation.dart'; -import 'dart:convert'; part 'verifyreset_response.g.dart'; diff --git a/lib/features/auth/domain/repos/auth_repo.dart b/lib/features/auth/domain/repos/auth_repo.dart index e1b927b..6556f5d 100644 --- a/lib/features/auth/domain/repos/auth_repo.dart +++ b/lib/features/auth/domain/repos/auth_repo.dart @@ -1,8 +1,11 @@ +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/data/models/response/resetpassword_response.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 forgetPassword(String email); - Future verifyResetCode(String code); - Future resetPassword(ResetPasswordRequest request); + Future> forgetPassword(String email); + Future> verifyResetCode(String code); + Future> resetPassword(ResetPasswordRequest request); } 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..03b379a --- /dev/null +++ b/lib/features/auth/domain/usecase/forgetpassword_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/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/login_usecase.dart b/lib/features/auth/domain/usecase/login_usecase.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/auth/domain/usecase/login_usecase.dart +++ /dev/null @@ -1 +0,0 @@ - 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); + } +} From 9bf99ee56d52d92e94391033b8d0cddee28fe78e Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Sat, 14 Feb 2026 03:09:11 +0200 Subject: [PATCH 4/6] feat(SCRUM-78) add forgot password presentation layer (UI, cubit, states, validation) --- lib/app/config/di/di.config.dart | 43 ++++++++ lib/app/core/api_manger/api_client.dart | 2 +- lib/app/core/api_manger/api_client.g.dart | 2 +- lib/app/core/router/app_router.dart | 45 ++++++++- lib/app/core/router/route_names.dart | 5 + .../auth/data/repos/auth_repo_impl.dart | 10 +- .../domain/models/forgetpassword_entitiy.dart | 6 +- lib/features/auth/domain/repos/auth_repo.dart | 8 +- .../usecase/forgetpassword_usecase.dart | 9 +- .../manager/cubit/forget_pass_cubit.dart | 63 ++++++++++++ .../manager/cubit/forget_pass_intents.dart | 13 +++ .../manager/cubit/forget_pass_state.dart | 27 +++++ .../forget_pass/pages/forget_pass_page.dart | 23 +++++ .../forget_pass/widgets/forget_pass_form.dart | 89 +++++++++++++++++ .../login/manager/login_cubit.dart | 1 - .../login/manager/login_intent.dart | 1 - .../login/manager/login_states.dart | 1 - .../manager/reset_password_cubit.dart | 82 +++++++++++++++ .../manager/reset_password_intents.dart | 19 ++++ .../manager/reset_password_state.dart | 37 +++++++ .../reset_password/pages/reset_password.dart | 51 ++++++++++ .../widgets/reset_password_form.dart | 68 +++++++++++++ .../widgets/show_user_email.dart | 18 ++++ .../manger/cubit/verify_reset_cubit.dart | 99 +++++++++++++++++++ .../manger/cubit/verify_reset_intent.dart | 17 ++++ .../manger/cubit/verify_reset_state.dart | 41 ++++++++ .../verify_reset/pages/verify_reset_page.dart | 55 +++++++++++ .../widgets/count_down_timer_widget.dart | 70 +++++++++++++ .../widgets/resend_action_widget.dart | 82 +++++++++++++++ .../widgets/verify_rest_code_form.dart | 93 +++++++++++++++++ lib/features/auth/test.dart | 1 - .../presentation/pages/profile_page.dart | 3 +- 32 files changed, 1059 insertions(+), 25 deletions(-) create mode 100644 lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart create mode 100644 lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_intents.dart create mode 100644 lib/features/auth/presentation/forget_pass/manager/cubit/forget_pass_state.dart create mode 100644 lib/features/auth/presentation/forget_pass/pages/forget_pass_page.dart create mode 100644 lib/features/auth/presentation/forget_pass/widgets/forget_pass_form.dart delete mode 100644 lib/features/auth/presentation/login/manager/login_cubit.dart delete mode 100644 lib/features/auth/presentation/login/manager/login_intent.dart delete mode 100644 lib/features/auth/presentation/login/manager/login_states.dart create mode 100644 lib/features/auth/presentation/reset_password/manager/reset_password_cubit.dart create mode 100644 lib/features/auth/presentation/reset_password/manager/reset_password_intents.dart create mode 100644 lib/features/auth/presentation/reset_password/manager/reset_password_state.dart create mode 100644 lib/features/auth/presentation/reset_password/pages/reset_password.dart create mode 100644 lib/features/auth/presentation/reset_password/widgets/reset_password_form.dart create mode 100644 lib/features/auth/presentation/reset_password/widgets/show_user_email.dart create mode 100644 lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_cubit.dart create mode 100644 lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_intent.dart create mode 100644 lib/features/auth/presentation/verify_reset/manger/cubit/verify_reset_state.dart create mode 100644 lib/features/auth/presentation/verify_reset/pages/verify_reset_page.dart create mode 100644 lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart create mode 100644 lib/features/auth/presentation/verify_reset/widgets/resend_action_widget.dart create mode 100644 lib/features/auth/presentation/verify_reset/widgets/verify_rest_code_form.dart delete mode 100644 lib/features/auth/test.dart diff --git a/lib/app/config/di/di.config.dart b/lib/app/config/di/di.config.dart index b733963..02b8df2 100644 --- a/lib/app/config/di/di.config.dart +++ b/lib/app/config/di/di.config.dart @@ -16,6 +16,20 @@ import '../../../features/auth/api/datasource/auth_remote_datasource_impl.dart' as _i777; import '../../../features/auth/data/datasource/auth_remote_datasource.dart' as _i708; +import '../../../features/auth/data/repos/auth_repo_impl.dart' as _i566; +import '../../../features/auth/domain/repos/auth_repo.dart' as _i712; +import '../../../features/auth/domain/usecase/forgetpassword_usecase.dart' + as _i769; +import '../../../features/auth/domain/usecase/resertpassword_usecase.dart' + as _i294; +import '../../../features/auth/domain/usecase/verifyreaset_usecase.dart' + as _i112; +import '../../../features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit.dart' + as _i614; +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; @@ -39,6 +53,35 @@ extension GetItInjectableX on _i174.GetIt { () => networkModule.authApiClient(gh<_i361.Dio>())); gh.factory<_i708.AuthRemoteDatasource>( () => _i777.AuthRemoteDatasourceImpl(gh<_i890.ApiClient>())); + gh.factory<_i712.AuthRepo>( + () => _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.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.factory<_i614.ForgetPasswordCubit>(() => _i614.ForgetPasswordCubit( + gh<_i769.ForgetPasswordUsecase>(), + gh<_i603.AuthStorage>(), + )); return this; } } diff --git a/lib/app/core/api_manger/api_client.dart b/lib/app/core/api_manger/api_client.dart index 82a58c7..8f7a1fe 100644 --- a/lib/app/core/api_manger/api_client.dart +++ b/lib/app/core/api_manger/api_client.dart @@ -22,7 +22,7 @@ abstract class ApiClient { Future> forgetPassword( @Body() ForgetPasswordRequest request, ); - @POST(AppEndpointString.resetPassword) + @PUT(AppEndpointString.resetPassword) Future> resetPassword( @Body() ResetPasswordRequest request, ); diff --git a/lib/app/core/api_manger/api_client.g.dart b/lib/app/core/api_manger/api_client.g.dart index fece798..a0bb3e8 100644 --- a/lib/app/core/api_manger/api_client.g.dart +++ b/lib/app/core/api_manger/api_client.g.dart @@ -60,7 +60,7 @@ class _ApiClient implements ApiClient { _data.addAll(request.toJson()); final _result = await _dio.fetch>( _setStreamType>(Options( - method: 'POST', + method: 'PUT', headers: _headers, extra: _extra, ) diff --git a/lib/app/core/router/app_router.dart b/lib/app/core/router/app_router.dart index 277495f..ee9e1eb 100644 --- a/lib/app/core/router/app_router.dart +++ b/lib/app/core/router/app_router.dart @@ -1,3 +1,46 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.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/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/reset_password/manager/reset_password_cubit.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'; -final GoRouter appRouter = GoRouter(routes: []); +final GoRouter appRouter = GoRouter( + initialLocation: RouteNames.forgetPassword, // start here + + routes: [ + // RouteNames.verifyResetCode, + 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()), + ], +); diff --git a/lib/app/core/router/route_names.dart b/lib/app/core/router/route_names.dart index 6a85eb6..89c3f87 100644 --- a/lib/app/core/router/route_names.dart +++ b/lib/app/core/router/route_names.dart @@ -1,4 +1,9 @@ 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'; } diff --git a/lib/features/auth/data/repos/auth_repo_impl.dart b/lib/features/auth/data/repos/auth_repo_impl.dart index 200034a..1701d36 100644 --- a/lib/features/auth/data/repos/auth_repo_impl.dart +++ b/lib/features/auth/data/repos/auth_repo_impl.dart @@ -17,26 +17,25 @@ class AuthRepoImpl implements AuthRepo { AuthRemoteDatasource remoteDatasource; AuthRepoImpl(this.remoteDatasource); - Future> forgetPassword(String email) async { + Future> forgetPassword(String email) async { final result = await remoteDatasource.forgetPassword( ForgetPasswordRequest(email: email), ); if (result is SuccessApiResult) { return SuccessApiResult( - data: ForgetpasswordEntitiy( + data: ForgetPasswordEntitiy( message: result.data.message, info: result.data.info, ), ); } if (result is ErrorApiResult) { - return ErrorApiResult(error: result.error); + return ErrorApiResult(error: result.error); } ; - return ErrorApiResult(error: 'Unexpected error'); + return ErrorApiResult(error: 'Unexpected error'); } - @override Future> verifyResetCode(String code) async { final result = await remoteDatasource.verifyResetCode( @@ -56,7 +55,6 @@ class AuthRepoImpl implements AuthRepo { return ErrorApiResult(error: 'Unexpected error'); } - @override Future> resetPassword( ResetPasswordRequest request, diff --git a/lib/features/auth/domain/models/forgetpassword_entitiy.dart b/lib/features/auth/domain/models/forgetpassword_entitiy.dart index 803031c..73a57ae 100644 --- a/lib/features/auth/domain/models/forgetpassword_entitiy.dart +++ b/lib/features/auth/domain/models/forgetpassword_entitiy.dart @@ -1,6 +1,6 @@ -class ForgetpasswordEntitiy { +class ForgetPasswordEntitiy { final String? message; final String? info; - ForgetpasswordEntitiy({required this.message, required this.info}); -} \ No newline at end of file + ForgetPasswordEntitiy({required this.message, required this.info}); +} diff --git a/lib/features/auth/domain/repos/auth_repo.dart b/lib/features/auth/domain/repos/auth_repo.dart index 6556f5d..a6cdc63 100644 --- a/lib/features/auth/domain/repos/auth_repo.dart +++ b/lib/features/auth/domain/repos/auth_repo.dart @@ -5,7 +5,9 @@ import 'package:tracking_app/features/auth/domain/models/resetpassword_entity.da import 'package:tracking_app/features/auth/domain/models/verifyreset_entity.dart'; abstract class AuthRepo { - Future> forgetPassword(String email); + Future> forgetPassword(String email); Future> verifyResetCode(String code); - Future> resetPassword(ResetPasswordRequest request); -} + Future> resetPassword( + ResetPasswordRequest request, + ); +} diff --git a/lib/features/auth/domain/usecase/forgetpassword_usecase.dart b/lib/features/auth/domain/usecase/forgetpassword_usecase.dart index 03b379a..87117b0 100644 --- a/lib/features/auth/domain/usecase/forgetpassword_usecase.dart +++ b/lib/features/auth/domain/usecase/forgetpassword_usecase.dart @@ -4,11 +4,10 @@ import 'package:tracking_app/features/auth/domain/models/forgetpassword_entitiy. import 'package:tracking_app/features/auth/domain/repos/auth_repo.dart'; @injectable -class ForgetpasswordUsecase { +class ForgetPasswordUsecase { AuthRepo authRepo; - ForgetpasswordUsecase(this.authRepo); - Future> call(String email){ - return authRepo.forgetPassword(email); + ForgetPasswordUsecase(this.authRepo); + Future> call(String email) { + return authRepo.forgetPassword(email); } - } 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..83e1b00 --- /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(); + + void doIntent(ForgetPasswordIntents intent) { + 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 (!isValid) 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..8bdaaee --- /dev/null +++ b/lib/features/auth/presentation/forget_pass/pages/forget_pass_page.dart @@ -0,0 +1,23 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.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.pop(), + ), + ), + 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/manager/login_cubit.dart b/lib/features/auth/presentation/login/manager/login_cubit.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/auth/presentation/login/manager/login_cubit.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/features/auth/presentation/login/manager/login_intent.dart b/lib/features/auth/presentation/login/manager/login_intent.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/auth/presentation/login/manager/login_intent.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/features/auth/presentation/login/manager/login_states.dart b/lib/features/auth/presentation/login/manager/login_states.dart deleted file mode 100644 index 8b13789..0000000 --- a/lib/features/auth/presentation/login/manager/login_states.dart +++ /dev/null @@ -1 +0,0 @@ - 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..00f2cba --- /dev/null +++ b/lib/features/auth/presentation/verify_reset/widgets/count_down_timer_widget.dart @@ -0,0 +1,70 @@ +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"))); } } From 7e8037410ccf6bb31d05e0fbafaac5c66125bc12 Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Sat, 14 Feb 2026 10:51:48 +0200 Subject: [PATCH 5/6] feat(SCRUM-78)unit tests for data and domain layer --- devtools_options.yaml | 3 + lib/app/core/api_manger/api_client.dart | 4 - lib/app/core/network/api_result.dart | 2 +- .../widgets/count_down_timer_widget.dart | 1 + .../auth_remote_datasource_impl_test.dart | 236 ++++++++++++++++++ .../forgetpassword_response_test.dart | 68 +++++ .../response/resetpassword_response_test.dart | 66 +++++ .../response/verifyreset_response_test.dart | 58 +++++ .../auth/data/repos/auth_repo_impl_test.dart | 184 ++++++++++++++ .../usecase/forgetpassword_usecase_test.dart | 63 +++++ .../usecase/resertpassword_usecase_test.dart | 68 +++++ .../usecase/verifyreaset_usecase_test.dart | 62 +++++ 12 files changed, 810 insertions(+), 5 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart create mode 100644 test/features/auth/data/models/response/forgetpassword_response_test.dart create mode 100644 test/features/auth/data/models/response/resetpassword_response_test.dart create mode 100644 test/features/auth/data/models/response/verifyreset_response_test.dart create mode 100644 test/features/auth/data/repos/auth_repo_impl_test.dart create mode 100644 test/features/auth/domain/usecase/forgetpassword_usecase_test.dart create mode 100644 test/features/auth/domain/usecase/resertpassword_usecase_test.dart create mode 100644 test/features/auth/domain/usecase/verifyreaset_usecase_test.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/app/core/api_manger/api_client.dart b/lib/app/core/api_manger/api_client.dart index 8f7a1fe..84b2b72 100644 --- a/lib/app/core/api_manger/api_client.dart +++ b/lib/app/core/api_manger/api_client.dart @@ -1,9 +1,6 @@ -import 'dart:io'; - import 'package:dio/dio.dart'; import 'package:retrofit/http.dart'; import 'package:retrofit/dio.dart'; -import 'package:tracking_app/app/core/network/api_result.dart'; import 'package:tracking_app/app/core/values/app_endpoint_strings.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'; @@ -26,7 +23,6 @@ abstract class ApiClient { Future> resetPassword( @Body() ResetPasswordRequest request, ); - @POST(AppEndpointString.verifyResetCode) Future> verifyResetCode( @Body() VerifyResetRequest request, diff --git a/lib/app/core/network/api_result.dart b/lib/app/core/network/api_result.dart index 48ca88f..44f22b1 100644 --- a/lib/app/core/network/api_result.dart +++ b/lib/app/core/network/api_result.dart @@ -1,4 +1,4 @@ - class ApiResult {} +sealed class ApiResult {} class SuccessApiResult extends ApiResult { final T data; 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 index 00f2cba..d962750 100644 --- 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 @@ -1,3 +1,4 @@ + import 'dart:async'; import 'package:flutter/material.dart'; 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 new file mode 100644 index 0000000..284fff1 --- /dev/null +++ b/test/features/auth/api/datasource/auth_remote_datasource_impl_test.dart @@ -0,0 +1,236 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:tracking_app/app/core/api_manger/api_client.dart'; +import 'package:mockito/mockito.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/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 authRemoteDataSourceImpl; + + setUpAll(() { + mockApiClient = MockApiClient(); + authRemoteDataSourceImpl = AuthRemoteDatasourceImpl(mockApiClient); + }); + + final forgetPasswordRequest = ForgetPasswordRequest( + email: "test@example.com", + ); + + group("AuthRemoteDatasourceImpl.forgetPassword()", () { + + test( + "returns SuccessApiResult when apiClient returns valid response", + () async { + // ARRANGE + 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); + + // ACT + final result = await authRemoteDataSourceImpl + .forgetPassword(forgetPasswordRequest); + + // ASSERT + 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 { + // ARRANGE + when(mockApiClient.forgetPassword(any)) + .thenThrow(Exception("Network Error")); + + // ACT + final result = await authRemoteDataSourceImpl + .forgetPassword(forgetPasswordRequest); + + // ASSERT + 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 { + // ARRANGE + 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); + + // ACT + final result = await authRemoteDataSourceImpl + .resetPassword(resetPasswordRequest); + + // ASSERT + 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 { + // ARRANGE + when(mockApiClient.resetPassword(any)) + .thenThrow(Exception("Reset failed")); + + // ACT + final result = await authRemoteDataSourceImpl + .resetPassword(resetPasswordRequest); + + // ASSERT + 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 { + // ARRANGE + 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); + + // ACT + final result = await authRemoteDataSourceImpl + .verifyResetCode(verifyResetCodeRequest); + + // ASSERT + 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 { + // ARRANGE + when(mockApiClient.verifyResetCode(any)) + .thenThrow(Exception("Invalid code")); + + // ACT + final result = await authRemoteDataSourceImpl + .verifyResetCode(verifyResetCodeRequest); + + // ASSERT + expect(result, isA()); + + final errorResult = result as ErrorApiResult; + expect(errorResult.error, contains("Invalid code")); + + verify(mockApiClient.verifyResetCode(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 new file mode 100644 index 0000000..ed5eb16 --- /dev/null +++ b/test/features/auth/data/repos/auth_repo_impl_test.dart @@ -0,0 +1,184 @@ +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/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/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 datasource; + late AuthRepoImpl repo; + + setUpAll(() { + provideDummy>( + SuccessApiResult( + data: ForgetpasswordResponse(message: '', info: ''), + ), + ); + + provideDummy>( + SuccessApiResult( + data: VerifyresetResponse(status: ''), + ), + ); + + provideDummy>( + SuccessApiResult( + data: ResetpasswordResponse(message: '', token: ''), + ), + ); + }); + + setUp(() { + datasource = MockAuthRemoteDatasource(); + repo = AuthRepoImpl(datasource); + }); + + // ============================================================ + // 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); + }); + }); +} 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); + }); + }); +} From 740afb14b0090632eca46f23f921acbf089ca193 Mon Sep 17 00:00:00 2001 From: Rahma Ashraf Date: Sat, 28 Feb 2026 20:31:11 +0200 Subject: [PATCH 6/6] feat(SCRUM-78)widget tests for forget password --- json_output.txt | 0 .../manager/cubit/forget_pass_cubit.dart | 6 +- pubspec.lock | 120 +++++---- pubspec.yaml | 2 +- .../apply/view/apply_screen_test.dart | 248 ++++-------------- .../manager/cubit/forget_pass_cubit_test.dart | 79 ++++++ .../pages/forget_pass_page_test.dart | 81 ++++++ .../login/pages/loginScreen_test.dart | 42 ++- test_out.json | Bin 0 -> 13856 bytes test_output.txt | Bin 0 -> 42760 bytes 10 files changed, 313 insertions(+), 265 deletions(-) create mode 100644 json_output.txt create mode 100644 test/features/auth/presentation/forget_pass/manager/cubit/forget_pass_cubit_test.dart create mode 100644 test/features/auth/presentation/forget_pass/pages/forget_pass_page_test.dart create mode 100644 test_out.json create mode 100644 test_output.txt diff --git a/json_output.txt b/json_output.txt new file mode 100644 index 0000000..e69de29 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 index 83e1b00..25a51e4 100644 --- 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 @@ -22,7 +22,7 @@ class ForgetPasswordCubit extends Cubit { final formKey = GlobalKey(); final emailController = TextEditingController(); - void doIntent(ForgetPasswordIntents intent) { + Future doIntent(ForgetPasswordIntents intent) async { switch (intent) { case FormChangedIntent(): _validateForm(); @@ -39,8 +39,8 @@ class ForgetPasswordCubit extends Cubit { } Future _submitForgetPassword() async { - final isValid = formKey.currentState?.validate() ?? false; - if (!isValid) return; + // final isValid = formKey.currentState?.validate() ?? false; + if (!state.isFormValid) return; emit(state.copyWith(resource: Resource.loading())); 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/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 0000000000000000000000000000000000000000..3abc33b92d184175ad255e4209bde898301de9ad GIT binary patch literal 13856 zcmeHO+iqJ`5FNxD68~`IzNAu>*ts?qs??MODz0i$3QDWUNlp`k>sY=tEvP>MAHk3C z!pFb^9{{X1)gDWKnsRotI^NE}%4Pw9wu@zN=_wMONj$ENXgJ-Xbs6 zoDzENN)4@d`?b2L^{Ss+mUr@@d>|Zi2mRMk%IY!2TQB!}YoML3yu=*ZXu)RjT7TJS zn0weye}qxZp=<-KcD2>E?DTQ@9)0W~t%DL>v{l2~59qBa2PjiRsg~xXI(D{_YR^5C zW!fCx*yJp0#nE9T_K(U zC(ipcJiR^Q6r)+uHk;_fSD}Pa9^rkzk3k|g13jV>r0TJJtg>O!bO7X!u!=2VdPj4% z<%MEHT)3882Yx$(9hH)nzQ06zL)YDUq5QPu32N4m>RQ%B4-Nd;IyMwvlW)&qZPU>n z*28o&&ZEpVv`0@Zk_lhyO&xEst}R5m#vZBd)JbYDM-j_WcEWX`xDo2Y4eO4Ne> z`WPH%Yt^)aC|F;%B72xKrJXIr_>nb?QL^|RqQ`ywQByc#YHbg1Htu+gK8lZZ4T$yN z-0p+>=A)4ZS@4y1=9%x0&S&g%b4H?`n&x|suM&IB>X?mWT(af!ew0~R8~^y^E=H~x z=fwzTA7dWn1w}cF){N>_SDr$v9;)2kJ&~2-)d*vWHG2LUb-Dt%A_{qZw)q83E@4Nh zV@|2=QFGW8ZU*08mYFKB9Llm(AStd@h9Gr^oKeF*oyu0m%HTfC9o(&qY*|WNNuGl< z%J{3uu580zDT8shrB?d+y+7qQNn~BQ`z?Nb4lUYKhOf(y`1W1>1@gABU#!a-B=Zx< z*6C#v9S@nZe$o=zEjtR1!8t1CvHX0p>`Y55ww+wFvUp7<_bz6**GHb`1hXWwoX~;A z#eJ&*6capk$O0{_8YCgX?;h67_mxDgn;OB- zSpx^tvKhrLH{D1C={U`nNMJ{#z$yhV)4~{?^sG*)^aRZ8{))FIJoB|jrdQL73}uM- z5yPy_aY)m$+RLY(@h6#!0%p9C;SC+R`lVVpukb6RhI_y!%GoWW-QsmdB6&oyGNn2> zT1KB{Bmz5KFQeZK%*Mg!MCNQedOWA#?EPfc35LyqZE%le-5_O-$l;ByI?k+ z4ioU*5k4FEJkYaTf_o41hS>cy&wCfUAHUw)v`Ib@FXGNBdtSah5nuNH&$J=-Pq+s8 ziP*H@|8OE+#(8+y&E6y<5q8MYo`{Lbv`WXQ?WgHQB1p$|+fO$Ev+>wZ(~C?v-Ce&l z75Ox$VW$e1g|qK}bs}DZN8$8)h0*MCzr6pK{4U}14_L$N@-4ir>+&`HhmYke`4VZ5 z;9;gW?^C2D@a%nu&kgthpW=NBWuM@k-T>b{uB8Ox$NWDB=i*bdi(n< z1fJt2eG9(56i70a$`*7O}U;s12NKc)LvB63ZUz9S+YqhkC1XjCKjxNC{I1A2&xwNJ^6ZX+OneQ=D4PCATk>t4S+!#=C z%9B$~SV^wJU=!-Qzi zkIwb-2vL)oJE$L{>sk~`bLZSxUt~?jUEsYgmze<(?U=hUxbCJ^Z`5);)%lb7pMK>XoJ8axydnxnd*%`HoEDyvloc zi$IMnko>%ZzB4}7B6ZWgO#7~pWeT!$4?K9NJh`iQl0C^DF2BY`&RZe6$3_=8t7tFs zs0ia9YR65CZs4sU<&>zANgV4eid<8RVP@=Wp4S6H!{c_&dtb>h+*(+-2#f=Pi$>t_ z#&QXNXPsbZOL4btt3_5iv1G{_X3LTlXQ5@A&?RbDk)1?q_yC$lTXq+}U!&CQC3TWE z)UYVJ^J=5V(=qae_Sn`5J6o)!<&&GURy%Wt`A%ZQc)uP;P8sSeXQDpn@yWO9k^I~P z&J%AwY{b5+4EOdd!Cx>aEqQOxa$zJidtjtFd*B&oTH>MOSopkmB+d)5f}VO6PeL)$ yem`Rf4@yWjdeo)c{;T7>QgW65N}y~nXS|vD5e?UyeaMDIYYuA73M@d6Klm3g2?dz| literal 0 HcmV?d00001 diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000000000000000000000000000000000000..6da05d0afcf0219869f9ab2e05c3dc944c18e496 GIT binary patch literal 42760 zcmeHQ+ioMr5$zyv2J!`YnOF~ABZ)4&Neu@J zNcB`-b=0}KR=4UB=MQkjmb$?CJ2k?qs|KpA-s8QmzQxfQ&JA(J2xmGtdN;najidK? z@tZEL80l8JI6Fd11AT4_M{M;9-?2qr*Tr*3`l@R@;TGSRc6#ao|9^+pyXxLM+s0jI z`flFa#@*(5o9IOc=SS)n>LcCDcwdKjavv1*aAl(cDd^7&9 z1HN>1o4-_V)gD^D!Dr{=AAa^P^>?6<57gFL33) z`bPahAD`%6lUlaQHc9n5pYS=Je2w#efcva;849%uhr-3 zYxNb*9jND;>ObOKgdY3_?{Dz!3-sg==T30^636@M7*`(a@7d~4k+khTC;Y!Y4p3%Z zL#n$NgA2%753fG-#cK0QErnf-(*UnN{vv!!d5s_wSNhyNr1*x!4vQl@Qpa()l3r{>kTV4IcNPPl5KF1ODof;^b z)K$M%JF2QGs)o;o+QoYf@1LNBt8q_mK?&vj6nD}lL}yksjz(`Xe9S2m#8k@K>)Li*=hiUah7_K;)6v846g zoWYw&J;N9nJ8bM!3-=g%)W-eE_Gk-q4$#}JIj(K%ah{~f+S*SwPd6|sZa;6)@;%Nh z)8G92)PkJtUob&OwHu%;pdjhS@bfw$CkQh5MdX?#SMG;V`Rj=4)?)1xtc%;}c~+9M7i9mhBl z|CSx?b@-(kUevY5qrC2Vw8!OuQSj5yMy^&sTl0~!J&&?i;8-7C@B!A1o=5cFm4WQI zN1I#M6p9Y*c>c4U5QlsPn(47=nK+8y;X^azvziHkqFB`E3xu{P_hU?Yhl98v4plr;~)2Ff&+mKoM zF;{L~KNjbpOIMz{<@&L*>6Yuq;{455LR~*rL@&ljMs%p`UaV3AFIGVuAsjE4rd3Mo z#kfWzTrZN1sFcKuRWNUu&cR}Mu?k{Fp|Nbb<$AF)MTiy5+=c5!(k-qh31w+Kb!9lq zx#VPoSS^9y;tXAQSuWkGdVZ@|66*S;B1VW+&p*X^=ju#;vzMypo607euFp!dQAJuA zsA9E1EJ9o|@|<^7J&%;eQ&)zc*6!Cl{}kt-OILon#l1}#<#Op(>7t{^lv6HP0gz z(e87%K*l}JyQ0XZy8>7?*_<_>%2zgBk5o+AoaK3nBAf24sEwVz4)PBEr1ygEC-#Ec z*au$sZl=vpbN7CjpThhBcNehkd7(5)T{>6smPypr5yx?7O4iQnp7$xH72?jISjuu) zA?~~i?Q#C*X@%;Z|B)Du`@hVBjMKUUX&7v#6D2|#X<6Jfwb^Vb@ zLUJZhk!DEDx)83%B_m($a=lX;PoGA&8t_B$y;Gi>&Ow*1{B(=iAwuIZGB22W5g+X~ z@-xvoD>){)o2bxA(R$4uq z)~3#wlB}eL=ab?iyuR^G^FfpOQVnmFPBI`R7n(Ve%-(%?vYa#S#xS<@vv?YwCz?j7 z$4!TyFSH~rCQ&j1i&9V8nNKK^)%1MgdbH#{ zMvxibo9cI34w?D=fHTbUroB4D`1bLisXF^?4w+k2hq%*4S#$g8&Q7fEKL-^LpzaLy zQ_NBA;dm3V#2x4!KoZlnA&D|-8Fh^nceZ`5Y{(OAZ3c6FnT^cVWvlC~0T5|g?}#kd zpXoYn>wcIEF7ZLr^AN6mwR|^T!pezzMYb|?k1@8@4Ki|u8W|Jj;B#)v93?v{^n@}h zwjV{5AFgZ(r7mwR)zi7ZoP5mBRqCWGDU8vDR{CaI_(f!ZS>^N=tcm68L#U1=k4$E| zL^HeI{J>fmcXVmF=Cil+UYDgbFuLLVj9D{A$4I-imZt8J+55(0&{wb$nDGW98G4k@ z#&vD1=nxsQGVXI`c2vhXDeI5yz58hY9DZTB{?D?Oten|HuWbypT%VV_>lwJmx@X3X z^lbEpGQ*-@<`E-gR$iMr?Upkp5&mbi7hxRydPe#>bA~pRkufVo&8V`LGgKJ|4rIZB zB-vV|M*av@%-=xkUt^W(7;??e6TGwXnU(Ti;`j|}?2pwKIL97@iuq+L<2hEd>fo*I zJ%aV1M>lhWg4Dzh=xIX$9i^FkkdAe&_1oZsYoS` z1gj@G`?T#5Bf_jK+R94OPcQRa-_Q9$Pq*1duh{O$s}N=7yqy`h;FVdYPY*-u$vD!D zm}q%<%sO||3$_-ikMS(xb=;h^(UL-Bh>z?++jxgHn=5UvPPJvXm?2hveY@q_Qc@=vBS{D?w7;~J#;;H}KEydEai&Z^ zBW+1se=S_(+<|M3tg)WNVG$0kisO_;v>4h99H3Okxe#UyagHn-bKZo;B0dhFVDH^A!e(50%Iy0PcynQD*y%$F2afoG2@bS)?i~^>Un7gGvYq> zhKc7TZ5rgb`Qw+>`edKG3fE*Gp4kevhqN~a0gy9(#;knCIfVCpk1yf2bQ(T0qv5R zp%%S(J$xlzY(TqQK69Hh0>)VZ?UHLIf`rPok)q(r2DD4gya&k2vuNh-o*jRqLiU7` zz1XCU2xylE7k)8L(u*Y_$+{JV<*J5crHAZO7pVq`JI+E`5pe?o+GTpi!Ll|hSQ?+PmzS#9MCSM+9BQBCe*b2%e*@a(`Xa=Dc4>cMZ+X3+oWrmRQ?$N_5n@2Q%+~I6E+!ICCc7WdE?14I z0@`J^x)snanX@)eyw9~X)?nJ2ferX2*E7YVIu{uyKb#@{Mw-~pa;;j`yefh?f0-dD z$SK*aQ6%lX5EnwYTEv)yn2kbVoOeZ$O?Ot5h9McyE?Kt}&@Qvo^rYWAdDWF~Qm%rmXyt8mc0@@|(Gv{5YpUugDcDYK-wzhV!D8K$=o+V{S9cB^Y5xJiWeEsF1eC+*`ODKZ|0RE!xE_pB2z9eT!g6 zB;5uJZiwvf&E_<7N_-5%fOeTDT53SMWEQQPq0&bDK^(ttv6{@fb?yXMRK9oP%5@^s z5?hMj7#th6!rJqZu2tu`EgTU8>I(m{MV>Pi?1G=dYYax!1Fo{aW#gW0<1XTbu!i?m zW3QX%ZNh_Fj6v?$_}JY6?J}TUT7=K#@R|bJWvF8QusFcl>Ud3fz^;jNq!?qKRtNuD zd(#nS76I+jqFwHT7v>iz4D!WG#4wjSUJ2Ni^nnJkGwpX2K16If{yu{H%OT4}1OeM} zI)=LYixYyy^9I^ye=Q+x6(nF=c61eLz_#RC%%%SAlcLNpU2IEQ64yK1`7e>(I5S-p z+j0^7reek}fvU1|Aj}ksF$77Tyi+>HJE(zt5tI}zRkn{Tc ga^hXi+dFcQ8Ktv1d4d@|ky-=+m#Diw-X~l5AI42pT>t<8 literal 0 HcmV?d00001