diff --git a/lib/app/app.dart b/lib/app/app.dart index 0130a88..df4c512 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -35,6 +35,7 @@ import 'package:yimaru_app/services/permission_handler_service.dart'; import 'package:yimaru_app/services/image_picker_service.dart'; import 'package:yimaru_app/services/google_auth_service.dart'; import 'package:yimaru_app/services/image_downloader_service.dart'; +import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'; // @stacked-import @StackedApp( @@ -63,6 +64,7 @@ import 'package:yimaru_app/services/image_downloader_service.dart'; MaterialRoute(page: AssessmentView), MaterialRoute(page: LearnLessonView), MaterialRoute(page: FailureView), + MaterialRoute(page: ForgetPasswordView), // @stacked-route ], dependencies: [ diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index 53e8dc1..3afd53d 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -5,10 +5,10 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i26; +import 'package:flutter/material.dart' as _i27; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart' as _i1; -import 'package:stacked_services/stacked_services.dart' as _i27; +import 'package:stacked_services/stacked_services.dart' as _i28; import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' as _i10; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23; @@ -16,6 +16,8 @@ import 'package:yimaru_app/ui/views/call_support/call_support_view.dart' as _i13; import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25; +import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart' + as _i26; import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2; import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14; import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19; @@ -92,6 +94,8 @@ class Routes { static const failureView = '/failure-view'; + static const forgetPasswordView = '/forget-password-view'; + static const all = { homeView, onboardingView, @@ -117,6 +121,7 @@ class Routes { assessmentView, learnLessonView, failureView, + forgetPasswordView, }; } @@ -218,17 +223,21 @@ class StackedRouter extends _i1.RouterBase { Routes.failureView, page: _i25.FailureView, ), + _i1.RouteDef( + Routes.forgetPasswordView, + page: _i26.ForgetPasswordView, + ), ]; final _pagesMap = { _i2.HomeView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i2.HomeView(), settings: data, ); }, _i3.OnboardingView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i3.OnboardingView(), settings: data, ); @@ -237,141 +246,147 @@ class StackedRouter extends _i1.RouterBase { final args = data.getArgs( orElse: () => const StartupViewArguments(), ); - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => _i4.StartupView(key: args.key, label: args.label), settings: data, ); }, _i5.ProfileView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i5.ProfileView(), settings: data, ); }, _i6.ProfileDetailView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i6.ProfileDetailView(), settings: data, ); }, _i7.DownloadsView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i7.DownloadsView(), settings: data, ); }, _i8.ProgressView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i8.ProgressView(), settings: data, ); }, _i9.OngoingProgressView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i9.OngoingProgressView(), settings: data, ); }, _i10.AccountPrivacyView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i10.AccountPrivacyView(), settings: data, ); }, _i11.SupportView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i11.SupportView(), settings: data, ); }, _i12.TelegramSupportView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i12.TelegramSupportView(), settings: data, ); }, _i13.CallSupportView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i13.CallSupportView(), settings: data, ); }, _i14.LanguageView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i14.LanguageView(), settings: data, ); }, _i15.PrivacyPolicyView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i15.PrivacyPolicyView(), settings: data, ); }, _i16.TermsAndConditionsView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i16.TermsAndConditionsView(), settings: data, ); }, _i17.RegisterView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i17.RegisterView(), settings: data, ); }, _i18.LoginView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i18.LoginView(), settings: data, ); }, _i19.LearnView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i19.LearnView(), settings: data, ); }, _i20.LearnLevelView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i20.LearnLevelView(), settings: data, ); }, _i21.LearnModuleView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i21.LearnModuleView(), settings: data, ); }, _i22.WelcomeView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i22.WelcomeView(), settings: data, ); }, _i23.AssessmentView: (data) { final args = data.getArgs(nullOk: false); - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => _i23.AssessmentView(key: args.key, data: args.data), settings: data, ); }, _i24.LearnLessonView: (data) { - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => const _i24.LearnLessonView(), settings: data, ); }, _i25.FailureView: (data) { final args = data.getArgs(nullOk: false); - return _i26.MaterialPageRoute( + return _i27.MaterialPageRoute( builder: (context) => _i25.FailureView(key: args.key, label: args.label), settings: data, ); }, + _i26.ForgetPasswordView: (data) { + return _i27.MaterialPageRoute( + builder: (context) => const _i26.ForgetPasswordView(), + settings: data, + ); + }, }; @override @@ -387,7 +402,7 @@ class StartupViewArguments { this.label = 'Loading', }); - final _i26.Key? key; + final _i27.Key? key; final String label; @@ -414,7 +429,7 @@ class AssessmentViewArguments { required this.data, }); - final _i26.Key? key; + final _i27.Key? key; final Map data; @@ -441,7 +456,7 @@ class FailureViewArguments { required this.label, }); - final _i26.Key? key; + final _i27.Key? key; final String label; @@ -462,7 +477,7 @@ class FailureViewArguments { } } -extension NavigatorStateExtension on _i27.NavigationService { +extension NavigatorStateExtension on _i28.NavigationService { Future navigateToHomeView([ int? routerId, bool preventDuplicates = true, @@ -492,7 +507,7 @@ extension NavigatorStateExtension on _i27.NavigationService { } Future navigateToStartupView({ - _i26.Key? key, + _i27.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -761,7 +776,7 @@ extension NavigatorStateExtension on _i27.NavigationService { } Future navigateToAssessmentView({ - _i26.Key? key, + _i27.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -792,7 +807,7 @@ extension NavigatorStateExtension on _i27.NavigationService { } Future navigateToFailureView({ - _i26.Key? key, + _i27.Key? key, required String label, int? routerId, bool preventDuplicates = true, @@ -808,6 +823,20 @@ extension NavigatorStateExtension on _i27.NavigationService { transition: transition); } + Future navigateToForgetPasswordView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.forgetPasswordView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future replaceWithHomeView([ int? routerId, bool preventDuplicates = true, @@ -837,7 +866,7 @@ extension NavigatorStateExtension on _i27.NavigationService { } Future replaceWithStartupView({ - _i26.Key? key, + _i27.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -1106,7 +1135,7 @@ extension NavigatorStateExtension on _i27.NavigationService { } Future replaceWithAssessmentView({ - _i26.Key? key, + _i27.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -1137,7 +1166,7 @@ extension NavigatorStateExtension on _i27.NavigationService { } Future replaceWithFailureView({ - _i26.Key? key, + _i27.Key? key, required String label, int? routerId, bool preventDuplicates = true, @@ -1152,4 +1181,18 @@ extension NavigatorStateExtension on _i27.NavigationService { parameters: parameters, transition: transition); } + + Future replaceWithForgetPasswordView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.forgetPasswordView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } } diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart index 80e2e6f..20b6c43 100644 --- a/lib/models/user_model.dart +++ b/lib/models/user_model.dart @@ -38,8 +38,6 @@ class UserModel { @JsonKey(name: 'profile_picture_url') final String? profilePicture; - - const UserModel({ this.email, this.region, diff --git a/lib/models/user_model.g.dart b/lib/models/user_model.g.dart index f8b439d..ab72b2c 100644 --- a/lib/models/user_model.g.dart +++ b/lib/models/user_model.g.dart @@ -23,6 +23,11 @@ UserModel _$UserModelFromJson(Map json) => UserModel( ); Map _$UserModelToJson(UserModel instance) => { + 'email': instance.email, + 'gender': instance.gender, + 'region': instance.region, + 'country': instance.country, + 'occupation': instance.occupation, 'user_id': instance.userId, 'last_name': instance.lastName, 'birth_day': instance.birthday, @@ -31,9 +36,4 @@ Map _$UserModelToJson(UserModel instance) => { 'refresh_token': instance.refreshToken, 'profile_completed': instance.profileCompleted, 'profile_picture_url': instance.profilePicture, - 'email': instance.email, - 'gender': instance.gender, - 'region': instance.region, - 'country': instance.country, - 'occupation': instance.occupation, }; diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 462cfcb..6037dea 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -29,15 +29,15 @@ class ApiService { 'message': 'Unknown Error Occurred' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } - // Login + // Email Login Future> emailLogin(Map data) async { try { Response response = await _service.dio.post( @@ -57,10 +57,10 @@ class ApiService { 'message': '${response.data['message']}, ${response.data['error']}' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } @@ -85,10 +85,10 @@ class ApiService { 'message': '${response.data['message']}, ${response.data['error']}' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } @@ -112,10 +112,10 @@ class ApiService { 'message': '${response.data['message']}, ${response.data['error']}' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } @@ -139,10 +139,65 @@ class ApiService { 'message': 'Unknown Error Occurred' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), + }; + } + } + + // Request reset code + Future> requestResetCode( + Map data) async { + try { + Response response = await _service.dio.post( + '$kBaseUrl/$kUserUrl/$kRequestResetCode', + data: data, + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Reset code sent successfully', + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } on DioException catch (e) { + return { + 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), + }; + } + } + + // Reset password + Future> resetPassword(Map data) async { + try { + Response response = await _service.dio.post( + '$kBaseUrl/$kUserUrl/$kResetPassword', + data: data, + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Password reset successfully', + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } on DioException catch (e) { + return { + 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } @@ -166,10 +221,10 @@ class ApiService { 'message': '${response.data['message']}, ${response.data['error']}' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } @@ -193,10 +248,10 @@ class ApiService { 'message': 'Unknown Error Occurred' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } @@ -221,10 +276,10 @@ class ApiService { 'message': 'Unknown Error Occurred' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } @@ -273,105 +328,14 @@ class ApiService { 'message': 'Unknown Error Occurred' }; } - } catch (e) { + } on DioException catch (e) { return { - 'message': e.toString(), 'status': ResponseStatus.failure, + 'message': e.response?.data.toString(), }; } } - // - // // Update profile - // Future> updateProfile( - // Map data) async { - // try { - // late FormData formData; - // - // if (data['profile_picture_url'] - // .toString() - // .contains('com.ke.wede.customer.app/')) { - // formData = FormData.fromMap({ - // 'gender': data['gender'], - // 'region': data['region'], - // 'country': data['country'], - // 'last_name': data['last_name'], - // 'nick_name': data['nick_name'], - // 'birth_day': data['birth_day'], - // 'age_group': data['age_group'], - // 'occupation': data['occupation'], - // 'first_name': data['first_name'], - // 'learning_goal': data['learning_goal'], - // 'language_goal': data['language_goal'], - // 'education_level': data['education_level'], - // 'favoutite_topic': data['favoutite_topic'], - // 'knowledge_level': data['knowledge_level'], - // 'profile_completed': data['profile_completed'], - // 'preferred_language': data['preferred_language'], - // 'language_challange': data['language_challange'], - // 'profile_picture_url': data['profile_picture_url'] - // .toString() - // .isNotEmpty - // ? MultipartFile.fromFileSync( - // data['profile_picture_url'], - // filename: - // data['profile_picture_url'].toString().split('/').last, - // ) - // : null, - // }); - // } else { - // formData = FormData.fromMap({ - // 'gender': data['gender'], - // 'region': data['region'], - // 'country': data['country'], - // 'last_name': data['last_name'], - // 'nick_name': data['nick_name'], - // 'birth_day': data['birth_day'], - // 'age_group': data['age_group'], - // 'occupation': data['occupation'], - // 'first_name': data['first_name'], - // 'learning_goal': data['learning_goal'], - // 'language_goal': data['language_goal'], - // 'education_level': data['education_level'], - // 'favoutite_topic': data['favoutite_topic'], - // 'knowledge_level': data['knowledge_level'], - // 'profile_completed': data['profile_completed'], - // 'preferred_language': data['preferred_language'], - // 'language_challange': data['language_challange'], - // 'profile_picture_url': data['profile_picture_url'] - // .toString() - // .isNotEmpty - // ? MultipartFile.fromFileSync( - // data['profile_picture_url'], - // filename: - // data['profile_picture_url'].toString().split('/').last, - // ) - // : null, - // }); - // } - // Response response = await _service.dio.put( - // '$baseUrl/$kUserUrl', - // data: formData, - // ); - // - // if (response.statusCode == 200) { - // return { - // 'status': ResponseStatus.success, - // 'message': 'Profile updated successfully' - // }; - // } else { - // return { - // 'status': ResponseStatus.failure, - // 'message': 'Unknown Error Occurred' - // }; - // } - // } catch (e) { - // return { - // 'message': e.toString(), - // 'status': ResponseStatus.failure, - // }; - // } - // } // Assessments Future> getAssessments() async { try { diff --git a/lib/services/image_downloader_service.dart b/lib/services/image_downloader_service.dart index c8d5340..308446a 100644 --- a/lib/services/image_downloader_service.dart +++ b/lib/services/image_downloader_service.dart @@ -12,12 +12,20 @@ class ImageDownloaderService { final _service = locator(); Future downloader(String? networkImage) async { - final Directory appDir = await getApplicationDocumentsDirectory(); - late File image; + late String profileImage; + + final Directory appDir = await getApplicationDocumentsDirectory(); + + if (networkImage != null) { + profileImage = networkImage.contains('https://lh3.googleusercontent.com') + ? networkImage + : '$kBaseUrl$networkImage'; + } + final Response profileImageResponse = await _service.dio.get( - '$kBaseUrl$networkImage', + profileImage, options: Options( followRedirects: false, responseType: ResponseType.bytes, diff --git a/lib/ui/common/app_constants.dart b/lib/ui/common/app_constants.dart index d34c1d6..0cf9552 100644 --- a/lib/ui/common/app_constants.dart +++ b/lib/ui/common/app_constants.dart @@ -11,6 +11,10 @@ String kVerifyOtpUrl = 'verify-otp'; String kResendOtpUrl = 'resend-otp'; +String kResetPassword = 'resetPassword'; + +String kRequestResetCode = 'sendResetCode'; + String kUpdateProfileImage = 'profile-picture'; String kRefreshTokenUrl = 'api/v1/auth/refresh'; diff --git a/lib/ui/common/enmus.dart b/lib/ui/common/enmus.dart index 1a4a5d8..3fbcf65 100644 --- a/lib/ui/common/enmus.dart +++ b/lib/ui/common/enmus.dart @@ -10,4 +10,15 @@ enum ProgressStatuses { pending, started, completed } enum ProficiencyLevels { a1, a2, b1, b2, none } // State object -enum StateObjects { profileImage } +enum StateObjects { + verifyOtp, + resendOtp, + profileImage, + registration, + profileUpdate, + resetPassword, + loginWithEmail, + loginWithGoogle, + requestResetCode, + profileCompletion, +} diff --git a/lib/ui/common/ui_helpers.dart b/lib/ui/common/ui_helpers.dart index bedbb4a..01a96ee 100644 --- a/lib/ui/common/ui_helpers.dart +++ b/lib/ui/common/ui_helpers.dart @@ -216,6 +216,12 @@ TextStyle style16DG600 = const TextStyle( fontWeight: FontWeight.w600, ); +TextStyle style18DG500 = const TextStyle( + fontSize: 18, + color: kcDarkGrey, + fontWeight: FontWeight.w500, +); + TextStyle style18DG600 = const TextStyle( fontSize: 18, color: kcDarkGrey, @@ -234,6 +240,8 @@ TextStyle style14LG400 = const TextStyle( TextStyle style14MG400 = const TextStyle( color: kcMediumGrey, ); +TextStyle style14DG500 = + const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500); TextStyle style14DG400 = const TextStyle( color: kcDarkGrey, @@ -274,24 +282,27 @@ Map htmlStyle = { Widget buildToastDescription(String message) => Text( message, maxLines: 4, - style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500), + style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500), ); void showErrorToast(String message) { toastification.show( showIcon: true, dragToClose: true, - primaryColor: kcRed, showProgressBar: false, applyBlurEffect: false, - icon: const Icon(Icons.check), + alignment: Alignment.topCenter, + primaryColor: kcBackgroundColor, type: ToastificationType.success, - alignment: Alignment.bottomCenter, style: ToastificationStyle.fillColored, description: buildToastDescription(message), - borderSide: const BorderSide(color: kcWhite), - autoCloseDuration: const Duration(seconds: 5), + autoCloseDuration: const Duration(seconds: 3), margin: const EdgeInsets.symmetric(horizontal: 15), + borderSide: const BorderSide(color: kcPrimaryColor), + icon: const Icon( + Icons.close, + color: kcPrimaryColor, + ), ); } @@ -301,14 +312,17 @@ void showSuccessToast(String message) { dragToClose: true, showProgressBar: false, applyBlurEffect: false, - icon: const Icon(Icons.check), - primaryColor: kcPrimaryColor, + alignment: Alignment.topCenter, + primaryColor: kcBackgroundColor, type: ToastificationType.success, - alignment: Alignment.bottomCenter, style: ToastificationStyle.fillColored, description: buildToastDescription(message), - borderSide: const BorderSide(color: kcWhite), - autoCloseDuration: const Duration(seconds: 5), + autoCloseDuration: const Duration(seconds: 3), margin: const EdgeInsets.symmetric(horizontal: 15), + borderSide: const BorderSide(color: kcPrimaryColor), + icon: const Icon( + Icons.check, + color: kcPrimaryColor, + ), ); } diff --git a/lib/ui/views/account_privacy/account_privacy_view.dart b/lib/ui/views/account_privacy/account_privacy_view.dart index 9599dc7..a9cfbbc 100644 --- a/lib/ui/views/account_privacy/account_privacy_view.dart +++ b/lib/ui/views/account_privacy/account_privacy_view.dart @@ -107,11 +107,7 @@ class AccountPrivacyView extends StackedView { Widget _buildHeader(String title) => Text( title, - style: const TextStyle( - fontSize: 18, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style18DG600, ); Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) => diff --git a/lib/ui/views/assessment/assessment_viewmodel.dart b/lib/ui/views/assessment/assessment_viewmodel.dart index 3db5f31..52dd973 100644 --- a/lib/ui/views/assessment/assessment_viewmodel.dart +++ b/lib/ui/views/assessment/assessment_viewmodel.dart @@ -21,6 +21,7 @@ class AssessmentViewModel extends BaseViewModel { final _statusChecker = locator(); final _navigationService = locator(); + // In-app navigation int _currentPage = 0; int get currentPage => _currentPage; @@ -255,16 +256,17 @@ class AssessmentViewModel extends BaseViewModel { // Complete profile Future completeProfile() async => - await runBusyFuture(_completeProfile()); + await runBusyFuture(_completeProfile(), + busyObject: StateObjects.profileCompletion); Future _completeProfile() async { if (await _statusChecker.checkConnection()) { Map response = await _apiService.completeProfile(_userData); if (response['status'] == ResponseStatus.success) { - showSuccessToast(response['message']); clearUserData(); await replaceWithHome(); + showSuccessToast(response['message']); } else { showErrorToast(response['message']); } diff --git a/lib/ui/views/assessment/screens/assessment_completion_screen.dart b/lib/ui/views/assessment/screens/assessment_completion_screen.dart index 0170cd9..561d2a3 100644 --- a/lib/ui/views/assessment/screens/assessment_completion_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_completion_screen.dart @@ -61,27 +61,23 @@ class AssessmentCompletionScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), ]; Widget _buildIcon() => SvgPicture.asset( 'assets/icons/complete.svg', ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Assessment complete!', + style: style25DG600, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'We’re now analyzing your speaking skills', textAlign: TextAlign.center, - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding( @@ -94,8 +90,8 @@ class AssessmentCompletionScreen extends ViewModelWidget { height: 55, borderRadius: 12, text: 'View My Results', - onTap: () => viewModel.next(), foregroundColor: kcWhite, + onTap: () => viewModel.next(), backgroundColor: kcPrimaryColor, ); } diff --git a/lib/ui/views/assessment/screens/assessment_failure_screen.dart b/lib/ui/views/assessment/screens/assessment_failure_screen.dart index 39a6617..aa3ab35 100644 --- a/lib/ui/views/assessment/screens/assessment_failure_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_failure_screen.dart @@ -64,25 +64,21 @@ class AssessmentFailureScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), ]; Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg'); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'We didn’t get enough from your assessment', + style: style25DG600, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ', textAlign: TextAlign.center, - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( @@ -117,9 +113,9 @@ class AssessmentFailureScreen extends ViewModelWidget { height: 55, text: 'Skip', borderRadius: 12, + backgroundColor: kcWhite, borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/lib/ui/views/assessment/screens/assessment_intro_screen.dart b/lib/ui/views/assessment/screens/assessment_intro_screen.dart index b0106f9..a04f699 100644 --- a/lib/ui/views/assessment/screens/assessment_intro_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_intro_screen.dart @@ -54,7 +54,7 @@ class AssessmentIntroScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), ]; Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( @@ -69,7 +69,7 @@ class AssessmentIntroScreen extends ViewModelWidget { style: style25DG600, ); - Widget _buildSubTitle() => Text( + Widget _buildSubtitle() => Text( 'Answer a few quick questions to help us understand your English proficiency.', style: style14MG400, ); diff --git a/lib/ui/views/assessment/screens/assessment_loading_screen.dart b/lib/ui/views/assessment/screens/assessment_loading_screen.dart index 179cf33..f5e90f7 100644 --- a/lib/ui/views/assessment/screens/assessment_loading_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_loading_screen.dart @@ -44,7 +44,5 @@ class AssessmentLoadingScreen extends StatelessWidget { Widget _buildPageIndicator() => const PageLoadingIndicator(); - Widget _buildRefreshButton() => RefreshButton( - onTap: onTap, - ); + Widget _buildRefreshButton() => RefreshButton(onTap: onTap); } diff --git a/lib/ui/views/assessment/screens/assessment_result_screen.dart b/lib/ui/views/assessment/screens/assessment_result_screen.dart index 7124dcb..3c9a614 100644 --- a/lib/ui/views/assessment/screens/assessment_result_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_result_screen.dart @@ -63,11 +63,11 @@ class AssessmentResultScreen extends ViewModelWidget { verticalSpaceLarge, _buildTitle(viewModel), verticalSpaceSmall, - _buildPrimarySubTitle(), + _buildPrimarySubtitle(), verticalSpaceMedium, _buildIconWrapper(viewModel), verticalSpaceMedium, - _buildSecondarySubTitle() + _buildSecondarySubtitle() ]; Widget _buildTitle(AssessmentViewModel viewModel) => Text( @@ -76,10 +76,10 @@ class AssessmentResultScreen extends ViewModelWidget { textAlign: TextAlign.center, ); - Widget _buildPrimarySubTitle() => const Text( + Widget _buildPrimarySubtitle() => Text( 'Great Job! Here’s your next step to keep improving.', textAlign: TextAlign.center, - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildIconWrapper(AssessmentViewModel viewModel) => @@ -90,7 +90,7 @@ class AssessmentResultScreen extends ViewModelWidget { Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset( 'assets/icons/${viewModel.proficiencyLevel.name.substring(0, 1)}_${viewModel.proficiencyLevel.name.substring(1)}.svg'); - Widget _buildSecondarySubTitle() => Text( + Widget _buildSecondarySubtitle() => Text( 'Let\'s start your practice', style: style14DG400, textAlign: TextAlign.center, @@ -113,8 +113,8 @@ class AssessmentResultScreen extends ViewModelWidget { safe: false, text: 'Continue', borderRadius: 12, - onTap: () => viewModel.next(), foregroundColor: kcWhite, + onTap: () => viewModel.next(), backgroundColor: kcPrimaryColor, ); @@ -127,10 +127,10 @@ class AssessmentResultScreen extends ViewModelWidget { CustomElevatedButton( height: 55, borderRadius: 12, + backgroundColor: kcWhite, text: 'Practice Speaking', borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/lib/ui/views/assessment/screens/result_analysis_screen.dart b/lib/ui/views/assessment/screens/result_analysis_screen.dart index f3442a6..32dbc67 100644 --- a/lib/ui/views/assessment/screens/result_analysis_screen.dart +++ b/lib/ui/views/assessment/screens/result_analysis_screen.dart @@ -48,7 +48,7 @@ class ResultAnalysisScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), ]; Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( @@ -61,19 +61,15 @@ class ResultAnalysisScreen extends ViewModelWidget { 'assets/icons/progress_indicator.svg', ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Analyzing your results…', + style: style25DG600, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'We’re now analyzing your speaking skills', + style: style14MG400, textAlign: TextAlign.center, - style: TextStyle(color: kcMediumGrey), ); } diff --git a/lib/ui/views/assessment/screens/retake_assessment_screen.dart b/lib/ui/views/assessment/screens/retake_assessment_screen.dart index 2ce9b1f..5c2cbfe 100644 --- a/lib/ui/views/assessment/screens/retake_assessment_screen.dart +++ b/lib/ui/views/assessment/screens/retake_assessment_screen.dart @@ -57,7 +57,7 @@ class RetakeAssessmentScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), ]; Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( @@ -72,20 +72,16 @@ class RetakeAssessmentScreen extends ViewModelWidget { color: kcPrimaryColor, ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'We didn’t get enough from your assessment', + style: style25DG600, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'Your assessment wasn’t long enough for us to analyze your speaking level. You can retake the call to get accurate results ', + style: style14MG400, textAlign: TextAlign.center, - style: TextStyle(color: kcMediumGrey), ); Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( diff --git a/lib/ui/views/assessment/screens/start_lesson_screen.dart b/lib/ui/views/assessment/screens/start_lesson_screen.dart index c2bfff4..9e5b2cc 100644 --- a/lib/ui/views/assessment/screens/start_lesson_screen.dart +++ b/lib/ui/views/assessment/screens/start_lesson_screen.dart @@ -80,21 +80,25 @@ class StartLessonScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(viewModel), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), ]; Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg'); Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich( - TextSpan(text: 'Welcome aboard', style: style25DG600, children: [ - TextSpan( - text: ', ${viewModel.userData['first_name']}!', - style: style25DG600, - ) - ]), + TextSpan( + text: 'Welcome aboard', + style: style25DG600, + children: [ + TextSpan( + style: style25DG600, + text: ', ${viewModel.userData['first_name']}!', + ), + ], + ), ); - Widget _buildSubTitle() => Text( + Widget _buildSubtitle() => Text( 'You’re ready to explore your personalized lessons.', style: style14MG400, ); @@ -115,5 +119,7 @@ class StartLessonScreen extends ViewModelWidget { ); Widget _buildState(AssessmentViewModel viewModel) => - viewModel.isBusy ? const PageLoadingIndicator() : Container(); + viewModel.busy(StateObjects.profileCompletion) + ? const PageLoadingIndicator() + : Container(); } diff --git a/lib/ui/views/call_support/call_support_view.dart b/lib/ui/views/call_support/call_support_view.dart index ccb5e99..68d3cc3 100644 --- a/lib/ui/views/call_support/call_support_view.dart +++ b/lib/ui/views/call_support/call_support_view.dart @@ -92,14 +92,10 @@ class CallSupportView extends StackedView { Widget _buildIcon() => const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Call our support team between 9 AM - 6 PM', + style: style25DG600, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), ); Widget _buildSubTitle(String title) => Text( diff --git a/lib/ui/views/downloads/downloads_view.dart b/lib/ui/views/downloads/downloads_view.dart index 27d95ac..ec37b00 100644 --- a/lib/ui/views/downloads/downloads_view.dart +++ b/lib/ui/views/downloads/downloads_view.dart @@ -178,7 +178,7 @@ class DownloadsView extends StackedView { verticalSpaceMedium, _buildEmptyTitle(), verticalSpaceSmall, - _buildEmptySubTitle(), + _buildEmptySubtitle(), ]; Widget _buildEmptyIcon() => const Icon( @@ -197,7 +197,7 @@ class DownloadsView extends StackedView { ), ); - Widget _buildEmptySubTitle() => const Text( + Widget _buildEmptySubtitle() => const Text( 'Start by exploring your learning materials and save them for offline access.', textAlign: TextAlign.center, style: TextStyle(color: kcMediumGrey), diff --git a/lib/ui/views/forget_password/forget_password_view.dart b/lib/ui/views/forget_password/forget_password_view.dart new file mode 100644 index 0000000..3283360 --- /dev/null +++ b/lib/ui/views/forget_password/forget_password_view.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/views/forget_password/forget_password_view.form.dart'; +import 'package:yimaru_app/ui/views/forget_password/screens/request_reset_code_screen.dart'; +import 'package:yimaru_app/ui/views/forget_password/screens/reset_password_screen.dart'; + +import '../../common/app_colors.dart'; +import '../../common/validators/form_validator.dart'; +import '../../widgets/large_app_bar.dart'; +import '../../widgets/page_loading_indicator.dart'; +import 'forget_password_viewmodel.dart'; + +@FormView(fields: [ + FormTextField(name: 'email', validator: FormValidator.validateEmail), + FormTextField(name: 'resetCode', validator: FormValidator.validateForm), + FormTextField(name: 'password', validator: FormValidator.validateForm), + FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm) +]) +class ForgetPasswordView extends StackedView + with $ForgetPasswordView { + const ForgetPasswordView({Key? key}) : super(key: key); + + void _initClearData() { + emailController.clear(); + passwordController.clear(); + resetCodeController.clear(); + confirmPasswordController.clear(); + } + + void _inAppPop(ForgetPasswordViewModel viewModel) { + _clearDataOnNavigation(viewModel); + viewModel.goBack(); + } + + void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) { + if (viewModel.currentPage == 0) { + emailController.clear(); + viewModel.resetRequestResetCodeScreen(); + } else { + passwordController.clear(); + resetCodeController.clear(); + confirmPasswordController.clear(); + viewModel.resetResetPasswordScreen(); + } + } + + void _pop({required bool value, required ForgetPasswordViewModel viewModel}) { + { + if (!value) return; + _clearDataOnNavigation(viewModel); + WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); + } + } + + @override + void onViewModelReady(ForgetPasswordViewModel viewModel) { + _initClearData(); + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + ForgetPasswordViewModel viewModelBuilder(BuildContext context) => + ForgetPasswordViewModel(); + + @override + Widget builder( + BuildContext context, + ForgetPasswordViewModel viewModel, + Widget? child, + ) => + _buildLoginScreensWrapper(viewModel); + + Widget _buildLoginScreensWrapper(ForgetPasswordViewModel viewModel) => + PopScope( + canPop: true, + onPopInvokedWithResult: (value, data) => + _pop(value: value, viewModel: viewModel), + child: _buildScaffoldWrapper(viewModel)); + + Widget _buildScaffoldWrapper(ForgetPasswordViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffoldStack(viewModel), + ); + + Widget _buildScaffoldStack(ForgetPasswordViewModel viewModel) => + Stack(children: [ + _buildScaffold(viewModel), + _buildRequestResetCodeState(viewModel), + _buildResetPasswordState(viewModel) + ]); + + Widget _buildScaffold(ForgetPasswordViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildScaffoldChildren(viewModel), + ); + + List _buildScaffoldChildren(ForgetPasswordViewModel viewModel) => + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; + + Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar( + showBackButton: true, + showLanguageSelection: true, + onPop: () => _inAppPop(viewModel), + ); + + Widget _buildExpandedBody(ForgetPasswordViewModel viewModel) => + Expanded(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(ForgetPasswordViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), + ); + + Widget _buildBody(ForgetPasswordViewModel viewModel) => + IndexedStack(index: viewModel.currentPage, children: _buildScreens()); + + List _buildScreens() => [ + _buildRequestCodeScreen(), + _buildResetPasswordScreen(), + ]; + + Widget _buildRequestCodeScreen() => + RequestCodeScreen(emailController: emailController); + + Widget _buildResetPasswordScreen() => ResetPasswordScreen( + passwordController: passwordController, + resetCodeController: resetCodeController, + confirmPasswordController: confirmPasswordController); + + Widget _buildRequestResetCodeState(ForgetPasswordViewModel viewModel) => + viewModel.busy(StateObjects.requestResetCode) + ? const PageLoadingIndicator() + : Container(); + + Widget _buildResetPasswordState(ForgetPasswordViewModel viewModel) => + viewModel.busy(StateObjects.resetPassword) + ? const PageLoadingIndicator() + : Container(); +} diff --git a/lib/ui/views/forget_password/forget_password_view.form.dart b/lib/ui/views/forget_password/forget_password_view.form.dart new file mode 100644 index 0000000..2d0d39c --- /dev/null +++ b/lib/ui/views/forget_password/forget_password_view.form.dart @@ -0,0 +1,281 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; + +const bool _autoTextFieldValidation = true; + +const String EmailValueKey = 'email'; +const String ResetCodeValueKey = 'resetCode'; +const String PasswordValueKey = 'password'; +const String ConfirmPasswordValueKey = 'confirmPassword'; + +final Map + _ForgetPasswordViewTextEditingControllers = {}; + +final Map _ForgetPasswordViewFocusNodes = {}; + +final Map + _ForgetPasswordViewTextValidations = { + EmailValueKey: FormValidator.validateEmail, + ResetCodeValueKey: FormValidator.validateForm, + PasswordValueKey: FormValidator.validateForm, + ConfirmPasswordValueKey: FormValidator.validateForm, +}; + +mixin $ForgetPasswordView { + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); + TextEditingController get resetCodeController => + _getFormTextEditingController(ResetCodeValueKey); + TextEditingController get passwordController => + _getFormTextEditingController(PasswordValueKey); + TextEditingController get confirmPasswordController => + _getFormTextEditingController(ConfirmPasswordValueKey); + + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); + FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey); + FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey); + FocusNode get confirmPasswordFocusNode => + _getFormFocusNode(ConfirmPasswordValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_ForgetPasswordViewTextEditingControllers.containsKey(key)) { + return _ForgetPasswordViewTextEditingControllers[key]!; + } + + _ForgetPasswordViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _ForgetPasswordViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_ForgetPasswordViewFocusNodes.containsKey(key)) { + return _ForgetPasswordViewFocusNodes[key]!; + } + _ForgetPasswordViewFocusNodes[key] = FocusNode(); + return _ForgetPasswordViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + emailController.addListener(() => _updateFormData(model)); + resetCodeController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + confirmPasswordController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + emailController.addListener(() => _updateFormData(model)); + resetCodeController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + confirmPasswordController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + EmailValueKey: emailController.text, + ResetCodeValueKey: resetCodeController.text, + PasswordValueKey: passwordController.text, + ConfirmPasswordValueKey: confirmPasswordController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _ForgetPasswordViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _ForgetPasswordViewFocusNodes.values) { + focusNode.dispose(); + } + + _ForgetPasswordViewTextEditingControllers.clear(); + _ForgetPasswordViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get emailValue => this.formValueMap[EmailValueKey] as String?; + String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?; + String? get passwordValue => this.formValueMap[PasswordValueKey] as String?; + String? get confirmPasswordValue => + this.formValueMap[ConfirmPasswordValueKey] as String?; + + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) { + _ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text = + value ?? ''; + } + } + + set resetCodeValue(String? value) { + this.setData( + this.formValueMap..addAll({ResetCodeValueKey: value}), + ); + + if (_ForgetPasswordViewTextEditingControllers.containsKey( + ResetCodeValueKey)) { + _ForgetPasswordViewTextEditingControllers[ResetCodeValueKey]?.text = + value ?? ''; + } + } + + set passwordValue(String? value) { + this.setData( + this.formValueMap..addAll({PasswordValueKey: value}), + ); + + if (_ForgetPasswordViewTextEditingControllers.containsKey( + PasswordValueKey)) { + _ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text = + value ?? ''; + } + } + + set confirmPasswordValue(String? value) { + this.setData( + this.formValueMap..addAll({ConfirmPasswordValueKey: value}), + ); + + if (_ForgetPasswordViewTextEditingControllers.containsKey( + ConfirmPasswordValueKey)) { + _ForgetPasswordViewTextEditingControllers[ConfirmPasswordValueKey]?.text = + value ?? ''; + } + } + + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); + bool get hasResetCode => + this.formValueMap.containsKey(ResetCodeValueKey) && + (resetCodeValue?.isNotEmpty ?? false); + bool get hasPassword => + this.formValueMap.containsKey(PasswordValueKey) && + (passwordValue?.isNotEmpty ?? false); + bool get hasConfirmPassword => + this.formValueMap.containsKey(ConfirmPasswordValueKey) && + (confirmPasswordValue?.isNotEmpty ?? false); + + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; + bool get hasResetCodeValidationMessage => + this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false; + bool get hasPasswordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false; + bool get hasConfirmPasswordValidationMessage => + this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ?? + false; + + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; + String? get resetCodeValidationMessage => + this.fieldsValidationMessages[ResetCodeValueKey]; + String? get passwordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]; + String? get confirmPasswordValidationMessage => + this.fieldsValidationMessages[ConfirmPasswordValueKey]; +} + +extension Methods on FormStateHelper { + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; + setResetCodeValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage; + setPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PasswordValueKey] = validationMessage; + setConfirmPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[ConfirmPasswordValueKey] = + validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + emailValue = ''; + resetCodeValue = ''; + passwordValue = ''; + confirmPasswordValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + EmailValueKey: getValidationMessage(EmailValueKey), + ResetCodeValueKey: getValidationMessage(ResetCodeValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _ForgetPasswordViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _ForgetPasswordViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + EmailValueKey: getValidationMessage(EmailValueKey), + ResetCodeValueKey: getValidationMessage(ResetCodeValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), + }); diff --git a/lib/ui/views/forget_password/forget_password_viewmodel.dart b/lib/ui/views/forget_password/forget_password_viewmodel.dart new file mode 100644 index 0000000..320c2fd --- /dev/null +++ b/lib/ui/views/forget_password/forget_password_viewmodel.dart @@ -0,0 +1,231 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/ui/views/login/login_view.dart'; + +import '../../../app/app.locator.dart'; +import '../../../services/api_service.dart'; +import '../../../services/status_checker_service.dart'; +import '../../common/enmus.dart'; +import '../../common/ui_helpers.dart'; + +class ForgetPasswordViewModel extends FormViewModel { + final _apiService = locator(); + + final _statusChecker = locator(); + + final _navigationService = locator(); + + // User data + final Map _userData = {}; + + Map get userData => _userData; + + // Navigation + int _currentPage = 0; + + int get currentPage => _currentPage; + + // Email + bool _focusEmail = false; + + bool get focusEmail => _focusEmail; + + // Reset code + bool _focusResetCode = false; + + bool get focusResetCode => _focusResetCode; + + // Password + bool _length = false; + + bool get length => _length; + + bool _number = false; + + bool get number => _number; + + bool _specialChar = false; + + bool get specialChar => _specialChar; + + bool _focusPassword = false; + + bool get focusPassword => _focusPassword; + + bool _obscurePassword = true; + + bool get obscurePassword => _obscurePassword; + + bool _passwordMatch = false; + + bool get passwordMatch => _passwordMatch; + + // Confirm password + bool _focusConfirmPassword = false; + + bool get focusConfirmPassword => _focusConfirmPassword; + + bool _obscureConfirmPassword = true; + + bool get obscureConfirmPassword => _obscureConfirmPassword; + + // Add user data + void addUserData(Map data) { + _userData.addAll(data); + } + + void clearUserData() { + _userData.clear(); + } + + // Email + void setEmailFocus() { + _focusEmail = true; + rebuildUi(); + } + + // Reset code + void setResetCodeFocus() { + _focusResetCode = true; + rebuildUi(); + } + + // Password + void setPasswordFocus() { + _focusPassword = true; + rebuildUi(); + } + + void validatePassword( + {required String password, required String confirmPassword}) { + if (password.length > 8) { + _length = true; + } else { + _length = false; + } + + if (RegExp(r'\d').hasMatch(password)) { + _number = true; + } else { + _number = false; + } + + if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) { + _specialChar = true; + } else { + _specialChar = false; + } + + if (password == confirmPassword) { + _passwordMatch = true; + } else { + _passwordMatch = false; + } + rebuildUi(); + } + + double validationProgress() { + int completed = 0; + + if (_length) completed++; + if (_number) completed++; + if (_specialChar) completed++; + if (_passwordMatch) completed++; + + return completed / 4; // returns 0.0 → 1.0 + } + + void setObscurePassword() { + _obscurePassword = !_obscurePassword; + rebuildUi(); + } + + // Confirm password + void setConfirmPasswordFocus() { + _focusConfirmPassword = true; + rebuildUi(); + } + + void setObscureConfirmPassword() { + _obscureConfirmPassword = !_obscureConfirmPassword; + rebuildUi(); + } + + // Form reset + + // Reset reset password screen + void resetResetPasswordScreen() { + _length = false; + _number = false; + _specialChar = false; + _passwordMatch = false; + _focusPassword = false; + _focusResetCode = false; + _focusConfirmPassword = false; + rebuildUi(); + } + + // Reset reset password screen + void resetRequestResetCodeScreen() { + _focusEmail = false; + rebuildUi(); + } + + // In-app navigation + void goTo(int page) { + _currentPage = page; + rebuildUi(); + } + + void goBack() { + if (_currentPage == 1) { + _currentPage = 0; + rebuildUi(); + } else { + _navigationService.back(); + } + } + + // Navigation + void pop() => _navigationService.back(); + + Future replaceWithLogin() async => + await _navigationService.clearStackAndShowView(const LoginView()); + + // Remote api calls + + // Request reset code + Future requestResetCode() async => + await runBusyFuture(_requestResetCode(), + busyObject: StateObjects.requestResetCode); + + Future _requestResetCode() async { + if (await _statusChecker.checkConnection()) { + Map response = + await _apiService.requestResetCode(_userData); + if (response['status'] == ResponseStatus.success) { + goTo(1); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + } + + // Request reset code + Future resetPassword() async => await runBusyFuture(_resetPassword(), + busyObject: StateObjects.resetPassword); + + Future _resetPassword() async { + if (await _statusChecker.checkConnection()) { + Map response = + await _apiService.resetPassword(_userData); + if (response['status'] == ResponseStatus.success) { + showSuccessToast(response['message']); + await replaceWithLogin(); + } else { + showErrorToast(response['message']); + } + } + } +} diff --git a/lib/ui/views/forget_password/screens/request_reset_code_screen.dart b/lib/ui/views/forget_password/screens/request_reset_code_screen.dart new file mode 100644 index 0000000..2f614cd --- /dev/null +++ b/lib/ui/views/forget_password/screens/request_reset_code_screen.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/views/forget_password/forget_password_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/login_account.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/option_text_divider.dart'; +import '../forget_password_view.form.dart'; + +class RequestCodeScreen extends ViewModelWidget { + final TextEditingController emailController; + + const RequestCodeScreen({ + super.key, + required this.emailController, + }); + + Future _addUserData(ForgetPasswordViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'email': emailController.text, + }; + viewModel.addUserData(data); + + await viewModel.requestResetCode(); + } + + @override + Widget build(BuildContext context, ForgetPasswordViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(ForgetPasswordViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(ForgetPasswordViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(ForgetPasswordViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(ForgetPasswordViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubtitle(), + verticalSpaceLarge, + _buildEmailFormField(viewModel), + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + verticalSpaceTiny, + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + _buildEmailValidatorWrapper(viewModel), + ]; + + Widget _buildTitle() => Text( + 'Reset password', + style: style25DG600, + ); + + Widget _buildSubtitle() => Text( + 'Enter your email, we will send you a reset code.', + style: style14DG400, + ); + + Widget _buildEmailFormField(ForgetPasswordViewModel viewModel) => + TextFormField( + controller: emailController, + onTap: viewModel.setEmailFocus, + keyboardType: TextInputType.emailAddress, + decoration: inputDecoration( + hint: 'Email', + focus: viewModel.focusEmail, + filled: emailController.text.isNotEmpty), + ); + + Widget _buildEmailValidatorWrapper(ForgetPasswordViewModel viewModel) => + viewModel.hasEmailValidationMessage + ? _buildEmailValidator(viewModel) + : Container(); + + Widget _buildEmailValidator(ForgetPasswordViewModel viewModel) => Text( + viewModel.emailValidationMessage!, + style: style12R700, + ); + + Widget _buildContinueButtonWrapper(ForgetPasswordViewModel viewModel) => + Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildContinueButton(viewModel), + ); + + Widget _buildContinueButton(ForgetPasswordViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: emailController.text.isNotEmpty && + !viewModel.hasEmailValidationMessage + ? () => _addUserData(viewModel) + : null, + backgroundColor: emailController.text.isNotEmpty && + !viewModel.hasEmailValidationMessage + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); +} diff --git a/lib/ui/views/forget_password/screens/reset_password_screen.dart b/lib/ui/views/forget_password/screens/reset_password_screen.dart new file mode 100644 index 0000000..0eb6051 --- /dev/null +++ b/lib/ui/views/forget_password/screens/reset_password_screen.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/custom_form_label.dart'; +import '../../../widgets/custom_linear_progress_indicator.dart'; +import '../../../widgets/obscure_password.dart'; +import '../../../widgets/validator_list_tile.dart'; +import '../forget_password_viewmodel.dart'; +import '../forget_password_view.form.dart'; + +class ResetPasswordScreen extends ViewModelWidget { + final TextEditingController resetCodeController; + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + + const ResetPasswordScreen( + {super.key, + required this.resetCodeController, + required this.passwordController, + required this.confirmPasswordController}); + + Future _reset(ForgetPasswordViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'otp': resetCodeController.text, + 'password': passwordController.text, + }; + viewModel.addUserData(data); + + await viewModel.resetPassword(); + } + + @override + Widget build(BuildContext context, ForgetPasswordViewModel viewModel) => + _buildBodyChildren(viewModel); + + Widget _buildBodyChildren(ForgetPasswordViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyColumn(viewModel), + ); + + Widget _buildBodyColumn(ForgetPasswordViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyColumnChildren(viewModel), + ); + + List _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildFormLabel('Reset code'), + verticalSpaceSmall, + _buildResetCodeFormField(viewModel), + if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode) + verticalSpaceTiny, + if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode) + _buildResetCodeValidationWrapper(viewModel), + verticalSpaceMedium, + _buildFormLabel('New Password'), + verticalSpaceSmall, + _buildPasswordFormField(viewModel), + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + verticalSpaceTiny, + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + _buildPasswordValidationWrapper(viewModel), + verticalSpaceMedium, + _buildFormLabel('Confirm Password'), + verticalSpaceSmall, + _buildConfirmPasswordFormField(viewModel), + if (viewModel.hasConfirmPasswordValidationMessage && + viewModel.focusConfirmPassword) + verticalSpaceTiny, + if (viewModel.hasConfirmPasswordValidationMessage && + viewModel.focusConfirmPassword) + _buildConfirmPasswordValidationWrapper(viewModel), + verticalSpaceMedium, + _buildLinearProgressIndicator(viewModel), + verticalSpaceSmall, + _buildCharLengthValidator(viewModel), + _buildNumberValidator(viewModel), + _buildSymbolValidator(viewModel), + _buildPasswordMatchValidator(viewModel), + verticalSpaceSmall, + _buildSignUpButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildTitle() => Text( + 'Reset password', + style: style25DG600, + ); + + Widget _buildFormLabel(String label) => CustomFormLabel( + label: label, + style: style14DG400, + ); + + Widget _buildResetCodeFormField(ForgetPasswordViewModel viewModel) => + TextFormField( + controller: resetCodeController, + onTap: viewModel.setResetCodeFocus, + decoration: inputDecoration( + hint: 'Reset code', + focus: viewModel.focusResetCode, + filled: passwordController.text.isNotEmpty), + ); + + Widget _buildResetCodeValidationWrapper(ForgetPasswordViewModel viewModel) => + viewModel.hasResetCodeValidationMessage + ? _buildResetCodeValidator(viewModel) + : Container(); + + Widget _buildResetCodeValidator(ForgetPasswordViewModel viewModel) => Text( + viewModel.resetCodeValidationMessage!, + style: style12R700, + ); + + Widget _buildPasswordFormField(ForgetPasswordViewModel viewModel) => + TextFormField( + controller: passwordController, + onTap: viewModel.setPasswordFocus, + obscureText: viewModel.obscurePassword, + decoration: inputDecoration( + hint: 'Password', + focus: viewModel.focusPassword, + suffix: _buildObscurePassword(viewModel), + filled: passwordController.text.isNotEmpty), + onChanged: (value) => viewModel.validatePassword( + password: passwordController.text, + confirmPassword: confirmPasswordController.text), + ); + + Widget _buildObscurePassword(ForgetPasswordViewModel viewModel) => + ObscurePassword( + focus: viewModel.focusPassword, + obscure: viewModel.obscurePassword, + onTap: viewModel.setObscurePassword, + ); + + Widget _buildPasswordValidationWrapper(ForgetPasswordViewModel viewModel) => + viewModel.hasPasswordValidationMessage + ? _buildPasswordValidator(viewModel) + : Container(); + + Widget _buildPasswordValidator(ForgetPasswordViewModel viewModel) => Text( + viewModel.passwordValidationMessage!, + style: style12R700, + ); + + Widget _buildConfirmPasswordFormField(ForgetPasswordViewModel viewModel) => + TextFormField( + controller: confirmPasswordController, + onTap: viewModel.setConfirmPasswordFocus, + obscureText: viewModel.obscureConfirmPassword, + onChanged: (value) => viewModel.validatePassword( + password: passwordController.text, + confirmPassword: confirmPasswordController.text), + decoration: inputDecoration( + hint: 'Confirm Password', + focus: viewModel.focusConfirmPassword, + suffix: _buildObscureConfirmPassword(viewModel), + filled: confirmPasswordController.text.isNotEmpty), + ); + + Widget _buildObscureConfirmPassword(ForgetPasswordViewModel viewModel) => + ObscurePassword( + focus: viewModel.focusConfirmPassword, + obscure: viewModel.obscureConfirmPassword, + onTap: viewModel.setObscureConfirmPassword, + ); + + Widget _buildConfirmPasswordValidationWrapper( + ForgetPasswordViewModel viewModel) => + viewModel.hasConfirmPasswordValidationMessage + ? _buildConfirmPasswordValidator(viewModel) + : Container(); + + Widget _buildConfirmPasswordValidator(ForgetPasswordViewModel viewModel) => + Text( + viewModel.confirmPasswordValidationMessage!, + style: style12R700, + ); + + Widget _buildLinearProgressIndicator(ForgetPasswordViewModel viewModel) => + CustomLinearProgressIndicator( + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + progress: viewModel.validationProgress(), + ); + + Widget _buildCharLengthValidator(ForgetPasswordViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey, + label: '8 characters minimum'); + + Widget _buildNumberValidator(ForgetPasswordViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey, + label: 'a number'); + + Widget _buildSymbolValidator(ForgetPasswordViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey, + label: 'one symbol minimum'); + + Widget _buildPasswordMatchValidator(ForgetPasswordViewModel viewModel) => + ValidatorListTile( + backgroundColor: + viewModel.passwordMatch ? kcPrimaryColor : kcLightGrey, + label: 'password match'); + + Widget _buildSignUpButton(ForgetPasswordViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: passwordController.text.isNotEmpty && + confirmPasswordController.text.isNotEmpty && + resetCodeController.text.isNotEmpty && + viewModel.number && + viewModel.length && + viewModel.specialChar && + viewModel.specialChar && + viewModel.passwordMatch + ? () async => await _reset(viewModel) + : null, + backgroundColor: passwordController.text.isNotEmpty && + confirmPasswordController.text.isNotEmpty && + resetCodeController.text.isNotEmpty && + viewModel.number && + viewModel.length && + viewModel.specialChar && + viewModel.specialChar && + viewModel.passwordMatch + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); +} diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index 4269a53..7a03dd4 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -117,6 +117,7 @@ class HomeViewModel extends ReactiveViewModel { response = {'data': true, 'status': ResponseStatus.success}; } + if (response['status'] == ResponseStatus.success && !response['data']) { await replaceWithOnboarding(); } else if (response['status'] == ResponseStatus.success && diff --git a/lib/ui/views/language/language_view.dart b/lib/ui/views/language/language_view.dart index 100f9ae..4db2aec 100644 --- a/lib/ui/views/language/language_view.dart +++ b/lib/ui/views/language/language_view.dart @@ -61,7 +61,7 @@ class LanguageView extends StackedView { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceMedium, _buildLanguages(viewModel) ]; @@ -72,22 +72,18 @@ class LanguageView extends StackedView { ); Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar( - title: 'Language Preference', onTap: viewModel.pop, + title: 'Language Preference', ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Choose your language', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'You can switch languages anytime', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder( diff --git a/lib/ui/views/learn_module/learn_module_view.dart b/lib/ui/views/learn_module/learn_module_view.dart index 918f09b..e0afc81 100644 --- a/lib/ui/views/learn_module/learn_module_view.dart +++ b/lib/ui/views/learn_module/learn_module_view.dart @@ -73,20 +73,14 @@ class LearnModuleView extends StackedView { _buildListView(viewModel) ]; - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'A1 - Beginner', - style: TextStyle( - fontSize: 18, - color: kcPrimaryColor, - fontWeight: FontWeight.w600, - ), + style: style18P600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubTitle() => Text( 'Your Current Level', - style: TextStyle( - color: kcDarkGrey, - ), + style: style14DG400, ); Widget _buildOverallProgress() => const OverallLearnProgress(); diff --git a/lib/ui/views/login/login_view.dart b/lib/ui/views/login/login_view.dart index 068a0f4..da14855 100644 --- a/lib/ui/views/login/login_view.dart +++ b/lib/ui/views/login/login_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart'; import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart'; import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart'; @@ -24,10 +25,18 @@ class LoginView extends StackedView with $LoginView { @override void onViewModelReady(LoginViewModel viewModel) { + _clearData(); syncFormWithViewModel(viewModel); super.onViewModelReady(viewModel); } + void _clearData() { + otpController.clear(); + emailController.clear(); + passwordController.clear(); + phoneNumberController.clear(); + } + @override LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel(); @@ -52,8 +61,11 @@ class LoginView extends StackedView with $LoginView { body: _buildScaffoldStack(viewModel), ); - Widget _buildScaffoldStack(LoginViewModel viewModel) => - Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]); + Widget _buildScaffoldStack(LoginViewModel viewModel) => Stack(children: [ + _buildScaffold(viewModel), + _buildLoginWithEmailState(viewModel), + _buildLoginWithGoogleState(viewModel) + ]); Widget _buildScaffold(LoginViewModel viewModel) => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -94,6 +106,13 @@ class LoginView extends StackedView with $LoginView { otpController: otpController, phoneNumberController: phoneNumberController); - Widget _buildBusyLogin(LoginViewModel viewModel) => - viewModel.isBusy ? const PageLoadingIndicator() : Container(); + Widget _buildLoginWithEmailState(LoginViewModel viewModel) => + viewModel.busy(StateObjects.loginWithEmail) + ? const PageLoadingIndicator() + : Container(); + + Widget _buildLoginWithGoogleState(LoginViewModel viewModel) => + viewModel.busy(StateObjects.loginWithGoogle) + ? const PageLoadingIndicator() + : Container(); } diff --git a/lib/ui/views/login/login_viewmodel.dart b/lib/ui/views/login/login_viewmodel.dart index fd23a12..0c971dd 100644 --- a/lib/ui/views/login/login_viewmodel.dart +++ b/lib/ui/views/login/login_viewmodel.dart @@ -138,13 +138,17 @@ class LoginViewModel extends FormViewModel { Future navigateToRegister() async => await _navigationService.navigateToRegisterView(); + Future navigateToForgetPassword() async => + await _navigationService.navigateToForgetPasswordView(); + Future replaceWithHome() async => await _navigationService.clearStackAndShowView(const HomeView()); // Remote api calls // Login with email - Future emailLogin() async => await runBusyFuture(_emailLogin()); + Future emailLogin() async => await runBusyFuture(_emailLogin(), + busyObject: StateObjects.loginWithEmail); Future _emailLogin() async { if (await _statusChecker.checkConnection()) { @@ -167,7 +171,8 @@ class LoginViewModel extends FormViewModel { } } - Future googleLogin() async => await runBusyFuture(_googleLogin()); + Future googleLogin() async => await runBusyFuture(_googleLogin(), + busyObject: StateObjects.loginWithGoogle); Future _googleLogin() async { if (await _statusChecker.checkConnection()) { diff --git a/lib/ui/views/login/screens/login_with_email_screen.dart b/lib/ui/views/login/screens/login_with_email_screen.dart index ac0dcf1..3572948 100644 --- a/lib/ui/views/login/screens/login_with_email_screen.dart +++ b/lib/ui/views/login/screens/login_with_email_screen.dart @@ -58,7 +58,7 @@ class LoginWithEmailScreen extends ViewModelWidget { List _buildUpperColumnChildren(LoginViewModel viewModel) => [ verticalSpaceMedium, _buildTitle(), - _buildSubTitleWrapper(viewModel), + _buildSubtitleWrapper(viewModel), verticalSpaceLarge, _buildEmailFormField(viewModel), if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) @@ -71,19 +71,15 @@ class LoginWithEmailScreen extends ViewModelWidget { verticalSpaceTiny, if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) _buildPasswordValidationWrapper(viewModel), - _buildForgetPasswordTextButtonWrapper(), + _buildForgetPasswordTextButtonWrapper(viewModel), ]; - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Welcome Back', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( + Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount( onTap: () async => await viewModel.navigateToRegister(), ); @@ -104,11 +100,7 @@ class LoginWithEmailScreen extends ViewModelWidget { Widget _buildEmailValidator(LoginViewModel viewModel) => Text( viewModel.emailValidationMessage!, - style: const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w700, - ), + style: style12R700, ); Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField( @@ -135,26 +127,23 @@ class LoginWithEmailScreen extends ViewModelWidget { Widget _buildPasswordValidator(LoginViewModel viewModel) => Text( viewModel.passwordValidationMessage!, - style: const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w700, - ), + style: style12R700, ); - Widget _buildForgetPasswordTextButtonWrapper() => Align( + Widget _buildForgetPasswordTextButtonWrapper(LoginViewModel viewModel) => + Align( alignment: Alignment.centerRight, - child: _buildForgetPasswordTextButton(), + child: _buildForgetPasswordTextButton(viewModel), ); - Widget _buildForgetPasswordTextButton() => TextButton( - onPressed: () {}, + Widget _buildForgetPasswordTextButton(LoginViewModel viewModel) => TextButton( + onPressed: () async => await viewModel.navigateToForgetPassword(), child: _buildForgetPasswordText(), ); - Widget _buildForgetPasswordText() => const Text( + Widget _buildForgetPasswordText() => Text( 'Forget Password?', - style: TextStyle(color: kcPrimaryColor), + style: style14P400, ); Widget _buildLowerColumn(LoginViewModel viewModel) => Column( @@ -207,8 +196,8 @@ class LoginWithEmailScreen extends ViewModelWidget { backgroundColor: kcWhite, leadingIcon: Icons.phone, borderColor: kcPrimaryColor, + onTap: () => viewModel.goTo(1), foregroundColor: kcPrimaryColor, text: 'Login with Phone Number', - onTap: () => viewModel.goTo(1), ); } diff --git a/lib/ui/views/login/screens/login_with_phone_number_screen.dart b/lib/ui/views/login/screens/login_with_phone_number_screen.dart index e37ea46..075b90f 100644 --- a/lib/ui/views/login/screens/login_with_phone_number_screen.dart +++ b/lib/ui/views/login/screens/login_with_phone_number_screen.dart @@ -44,7 +44,7 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget { List _buildUpperColumnChildren(LoginViewModel viewModel) => [ verticalSpaceMedium, _buildTitle(), - _buildSubTitleWrapper(viewModel), + _buildSubtitleWrapper(viewModel), verticalSpaceMedium, _buildSubtitle(), verticalSpaceMedium, @@ -57,22 +57,18 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget { _buildPhoneNumberValidatorWrapper(viewModel), ]; - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Welcome Back', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( + Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount( onTap: () async => await viewModel.navigateToRegister(), ); - Widget _buildSubtitle() => const Text( + Widget _buildSubtitle() => Text( 'Enter your phone number. We will send you a confirmation code there', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row( diff --git a/lib/ui/views/onboarding/onboarding_view.dart b/lib/ui/views/onboarding/onboarding_view.dart index 320a40a..74b970b 100644 --- a/lib/ui/views/onboarding/onboarding_view.dart +++ b/lib/ui/views/onboarding/onboarding_view.dart @@ -19,7 +19,6 @@ import 'onboarding_viewmodel.dart'; import 'onboarding_view.form.dart'; @FormView(fields: [ - FormTextField(name: 'answer', validator: FormValidator.validateForm), FormTextField(name: 'fullName', validator: FormValidator.validateForm), FormTextField(name: 'challenge', validator: FormValidator.validateForm), FormTextField(name: 'occupation', validator: FormValidator.validateForm), @@ -30,13 +29,55 @@ class OnboardingView extends StackedView with $OnboardingView { const OnboardingView({Key? key}) : super(key: key); - void _initFormFields() { - answerController.text = 'Book'; + void _initClearData() { + topicController.clear(); + fullNameController.clear(); + challengeController.clear(); + occupationController.clear(); + languageGoalController.clear(); + } + + void _clearDataOnNavigation(OnboardingViewModel viewModel) { + if (viewModel.currentPage == 0) { + fullNameController.clear(); + viewModel.resetFullNameFormScreen(); + } else if (viewModel.currentPage == 1) { + viewModel.resetGenderFormScreen(); + } else if (viewModel.currentPage == 2) { + viewModel.resetBirthdayFormScreen(); + } else if (viewModel.currentPage == 3) { + viewModel.resetAgeGroupFormScreen(); + } else if (viewModel.currentPage == 4) { + viewModel.resetEducationalBackgroundFormScreen(); + } else if (viewModel.currentPage == 5) { + occupationController.clear(); + viewModel.resetOccupationFormScreen(); + } else if (viewModel.currentPage == 6) { + viewModel.resetCountryRegionFormScreen(); + } else if (viewModel.currentPage == 7) { + viewModel.resetLearningGoalFormScreen(); + } else if (viewModel.currentPage == 8) { + languageGoalController.clear(); + viewModel.resetLanguageGoalFormScreen(); + } else if (viewModel.currentPage == 9) { + challengeController.clear(); + viewModel.resetChallengeFormScreen(); + } else if (viewModel.currentPage == 10) { + topicController.clear(); + viewModel.resetTopicFormScreen(); + } + } + + void _pop(OnboardingViewModel viewModel) { + { + _clearDataOnNavigation(viewModel); + viewModel.goBack(); + } } @override void onViewModelReady(OnboardingViewModel viewModel) { - _initFormFields(); + _initClearData(); syncFormWithViewModel(viewModel); super.onViewModelReady(viewModel); } @@ -58,7 +99,7 @@ class OnboardingView extends StackedView Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) => PopScope( canPop: viewModel.currentPage == 0 ? true : false, - onPopInvokedWithResult: (value, data) => viewModel.pop(), + onPopInvokedWithResult: (value, data) => _pop(viewModel), child: _buildOnboardingScreens(viewModel)); Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack( diff --git a/lib/ui/views/onboarding/onboarding_view.form.dart b/lib/ui/views/onboarding/onboarding_view.form.dart index 3cf07c4..3c7d903 100644 --- a/lib/ui/views/onboarding/onboarding_view.form.dart +++ b/lib/ui/views/onboarding/onboarding_view.form.dart @@ -12,7 +12,6 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart'; const bool _autoTextFieldValidation = true; -const String AnswerValueKey = 'answer'; const String FullNameValueKey = 'fullName'; const String ChallengeValueKey = 'challenge'; const String OccupationValueKey = 'occupation'; @@ -25,7 +24,6 @@ final Map _OnboardingViewTextEditingControllers = final Map _OnboardingViewFocusNodes = {}; final Map _OnboardingViewTextValidations = { - AnswerValueKey: FormValidator.validateForm, FullNameValueKey: FormValidator.validateForm, ChallengeValueKey: FormValidator.validateForm, OccupationValueKey: FormValidator.validateForm, @@ -34,8 +32,6 @@ final Map _OnboardingViewTextValidations = { }; mixin $OnboardingView { - TextEditingController get answerController => - _getFormTextEditingController(AnswerValueKey); TextEditingController get fullNameController => _getFormTextEditingController(FullNameValueKey); TextEditingController get challengeController => @@ -47,7 +43,6 @@ mixin $OnboardingView { TextEditingController get topicController => _getFormTextEditingController(TopicValueKey); - FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey); FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey); FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey); FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey); @@ -79,7 +74,6 @@ mixin $OnboardingView { /// Registers a listener on every generated controller that calls [model.setData()] /// with the latest textController values void syncFormWithViewModel(FormStateHelper model) { - answerController.addListener(() => _updateFormData(model)); fullNameController.addListener(() => _updateFormData(model)); challengeController.addListener(() => _updateFormData(model)); occupationController.addListener(() => _updateFormData(model)); @@ -96,7 +90,6 @@ mixin $OnboardingView { 'This feature was deprecated after 3.1.0.', ) void listenToFormUpdated(FormViewModel model) { - answerController.addListener(() => _updateFormData(model)); fullNameController.addListener(() => _updateFormData(model)); challengeController.addListener(() => _updateFormData(model)); occupationController.addListener(() => _updateFormData(model)); @@ -111,7 +104,6 @@ mixin $OnboardingView { model.setData( model.formValueMap ..addAll({ - AnswerValueKey: answerController.text, FullNameValueKey: fullNameController.text, ChallengeValueKey: challengeController.text, OccupationValueKey: occupationController.text, @@ -158,7 +150,6 @@ extension ValueProperties on FormStateHelper { return !hasAnyValidationMessage; } - String? get answerValue => this.formValueMap[AnswerValueKey] as String?; String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?; String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?; String? get occupationValue => @@ -167,16 +158,6 @@ extension ValueProperties on FormStateHelper { this.formValueMap[LanguageGoalValueKey] as String?; String? get topicValue => this.formValueMap[TopicValueKey] as String?; - set answerValue(String? value) { - this.setData( - this.formValueMap..addAll({AnswerValueKey: value}), - ); - - if (_OnboardingViewTextEditingControllers.containsKey(AnswerValueKey)) { - _OnboardingViewTextEditingControllers[AnswerValueKey]?.text = value ?? ''; - } - } - set fullNameValue(String? value) { this.setData( this.formValueMap..addAll({FullNameValueKey: value}), @@ -232,9 +213,6 @@ extension ValueProperties on FormStateHelper { } } - bool get hasAnswer => - this.formValueMap.containsKey(AnswerValueKey) && - (answerValue?.isNotEmpty ?? false); bool get hasFullName => this.formValueMap.containsKey(FullNameValueKey) && (fullNameValue?.isNotEmpty ?? false); @@ -251,8 +229,6 @@ extension ValueProperties on FormStateHelper { this.formValueMap.containsKey(TopicValueKey) && (topicValue?.isNotEmpty ?? false); - bool get hasAnswerValidationMessage => - this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false; bool get hasFullNameValidationMessage => this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false; bool get hasChallengeValidationMessage => @@ -264,8 +240,6 @@ extension ValueProperties on FormStateHelper { bool get hasTopicValidationMessage => this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false; - String? get answerValidationMessage => - this.fieldsValidationMessages[AnswerValueKey]; String? get fullNameValidationMessage => this.fieldsValidationMessages[FullNameValueKey]; String? get challengeValidationMessage => @@ -279,8 +253,6 @@ extension ValueProperties on FormStateHelper { } extension Methods on FormStateHelper { - setAnswerValidationMessage(String? validationMessage) => - this.fieldsValidationMessages[AnswerValueKey] = validationMessage; setFullNameValidationMessage(String? validationMessage) => this.fieldsValidationMessages[FullNameValueKey] = validationMessage; setChallengeValidationMessage(String? validationMessage) => @@ -294,7 +266,6 @@ extension Methods on FormStateHelper { /// Clears text input fields on the Form void clearForm() { - answerValue = ''; fullNameValue = ''; challengeValue = ''; occupationValue = ''; @@ -305,7 +276,6 @@ extension Methods on FormStateHelper { /// Validates text input fields on the Form void validateForm() { this.setValidationMessages({ - AnswerValueKey: getValidationMessage(AnswerValueKey), FullNameValueKey: getValidationMessage(FullNameValueKey), ChallengeValueKey: getValidationMessage(ChallengeValueKey), OccupationValueKey: getValidationMessage(OccupationValueKey), @@ -330,7 +300,6 @@ String? getValidationMessage(String key) { /// Updates the fieldsValidationMessages on the FormViewModel void updateValidationData(FormStateHelper model) => model.setValidationMessages({ - AnswerValueKey: getValidationMessage(AnswerValueKey), FullNameValueKey: getValidationMessage(FullNameValueKey), ChallengeValueKey: getValidationMessage(ChallengeValueKey), OccupationValueKey: getValidationMessage(OccupationValueKey), diff --git a/lib/ui/views/onboarding/onboarding_viewmodel.dart b/lib/ui/views/onboarding/onboarding_viewmodel.dart index 852f601..61953d6 100644 --- a/lib/ui/views/onboarding/onboarding_viewmodel.dart +++ b/lib/ui/views/onboarding/onboarding_viewmodel.dart @@ -372,16 +372,83 @@ class OnboardingViewModel extends FormViewModel { _userData.clear(); } - // Navigation - Future navigateToLanguage() async => - await _navigationService.navigateToLanguageView(); + // Form reset - Future navigateToAssessment() async => - await _navigationService.navigateToAssessmentView(data: _userData); + // Reset full name form screen + void resetFullNameFormScreen() { + _focusFullName = false; + rebuildUi(); + } - Future replaceWithHome() async => - await _navigationService.clearStackAndShowView(const HomeView()); + // Reset gender form screen + void resetGenderFormScreen() { + _selectedGender = null; + rebuildUi(); + } + // Reset birthday form screen + void resetBirthdayFormScreen() { + _selectedBirthday = null; + rebuildUi(); + } + + // Reset age group form screen + void resetAgeGroupFormScreen() { + _selectedAgeGroup = null; + rebuildUi(); + } + + // Reset educational background form screen + void resetEducationalBackgroundFormScreen() { + _selectedEducationalBackground = null; + rebuildUi(); + } + + // Reset occupation form screen + void resetOccupationFormScreen() { + _focusOccupation = false; + rebuildUi(); + } + + // Reset country region form screen + void resetCountryRegionFormScreen() { + _selectedCountry = 'Ethiopia'; + _selectedRegion = 'Addis Ababa'; + rebuildUi(); + } + + // Reset learning goal form screen + void resetLearningGoalFormScreen() { + _selectedLearningGoal = null; + rebuildUi(); + } + + // Reset language goal form screen + void resetLanguageGoalFormScreen() { + _focusLanguageGoal = false; + _selectedLanguageGoal = null; + _showLanguageGoalTextBox = false; + rebuildUi(); + } + + // Reset challenge form screen + void resetChallengeFormScreen() { + _focusChallenge = false; + _selectedChallenge = null; + _showChallengeTextBox = false; + + rebuildUi(); + } + + // Reset topic form screen + void resetTopicFormScreen() { + _focusTopic = false; + _selectedTopic = null; + _showTopicTextBox = false; + rebuildUi(); + } + + // In-app navigation void next({int? page}) async { if (page == null) { if (_previousPage != 0) { @@ -396,7 +463,7 @@ class OnboardingViewModel extends FormViewModel { rebuildUi(); } - void pop() { + void goBack() { if (_currentPage == 0) { _navigationService.back(); } else { @@ -405,4 +472,14 @@ class OnboardingViewModel extends FormViewModel { rebuildUi(); } } + + // Navigation + Future navigateToLanguage() async => + await _navigationService.navigateToLanguageView(); + + Future navigateToAssessment() async => + await _navigationService.navigateToAssessmentView(data: _userData); + + Future replaceWithHome() async => + await _navigationService.clearStackAndShowView(const HomeView()); } diff --git a/lib/ui/views/onboarding/screens/age_group_form_screen.dart b/lib/ui/views/onboarding/screens/age_group_form_screen.dart index 9957d6f..36ed774 100644 --- a/lib/ui/views/onboarding/screens/age_group_form_screen.dart +++ b/lib/ui/views/onboarding/screens/age_group_form_screen.dart @@ -10,6 +10,12 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; class AgeGroupFormScreen extends ViewModelWidget { const AgeGroupFormScreen({super.key}); + void _pop(OnboardingViewModel viewModel) { + viewModel.resetAgeGroupFormScreen(); + + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -74,24 +80,20 @@ class AgeGroupFormScreen extends ViewModelWidget { ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Which age range are you in?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubTitle() => Text( 'We’ll personalize your learning experience based on your age.', - style: TextStyle(color: kcMediumGrey), + style: style14DG400, ); Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder( diff --git a/lib/ui/views/onboarding/screens/birthday_form_screen.dart b/lib/ui/views/onboarding/screens/birthday_form_screen.dart index 270b759..7d067a8 100644 --- a/lib/ui/views/onboarding/screens/birthday_form_screen.dart +++ b/lib/ui/views/onboarding/screens/birthday_form_screen.dart @@ -12,6 +12,12 @@ import '../../../widgets/birthday_selector.dart'; class BirthdayFormScreen extends ViewModelWidget { const BirthdayFormScreen({super.key}); + void _pop(OnboardingViewModel viewModel) { + viewModel.resetBirthdayFormScreen(); + + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -64,30 +70,26 @@ class BirthdayFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceMedium, _buildBirthdayFormField(viewModel) ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Pick your birthday?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'We’ll personalize your learning experience based on your birthday.', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildBirthdayFormField(OnboardingViewModel viewModel) => diff --git a/lib/ui/views/onboarding/screens/challenge_form_screen.dart b/lib/ui/views/onboarding/screens/challenge_form_screen.dart index 020a802..f9569f6 100644 --- a/lib/ui/views/onboarding/screens/challenge_form_screen.dart +++ b/lib/ui/views/onboarding/screens/challenge_form_screen.dart @@ -13,6 +13,12 @@ class ChallengeFormScreen extends ViewModelWidget { const ChallengeFormScreen({super.key, required this.challengeController}); + void _pop(OnboardingViewModel viewModel) { + challengeController.clear(); + viewModel.resetChallengeFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -74,7 +80,7 @@ class ChallengeFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceMedium, _buildChallenges(viewModel), if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel), @@ -90,24 +96,20 @@ class ChallengeFormScreen extends ViewModelWidget { ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'What challenge do you face most with English?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'Everyone has struggles, let’s start fixing yours 😊', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder( @@ -151,11 +153,7 @@ class ChallengeFormScreen extends ViewModelWidget { Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text( viewModel.challengeValidationMessage!, - style: const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w700, - ), + style: style12R700, ); Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( diff --git a/lib/ui/views/onboarding/screens/country_region_form_screen.dart b/lib/ui/views/onboarding/screens/country_region_form_screen.dart index af02f17..a168878 100644 --- a/lib/ui/views/onboarding/screens/country_region_form_screen.dart +++ b/lib/ui/views/onboarding/screens/country_region_form_screen.dart @@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; class CountryRegionFormScreen extends ViewModelWidget { const CountryRegionFormScreen({super.key}); + void _pop(OnboardingViewModel viewModel) { + viewModel.resetCountryRegionFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -71,7 +76,7 @@ class CountryRegionFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceMedium, _buildCountryDropDown(viewModel), verticalSpaceMedium, @@ -80,24 +85,20 @@ class CountryRegionFormScreen extends ViewModelWidget { ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Where are you from?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'Select your country and region from the dropdown', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildCountryDropDown(OnboardingViewModel viewModel) => diff --git a/lib/ui/views/onboarding/screens/educational_background_form_screen.dart b/lib/ui/views/onboarding/screens/educational_background_form_screen.dart index bd9d4dc..da39e88 100644 --- a/lib/ui/views/onboarding/screens/educational_background_form_screen.dart +++ b/lib/ui/views/onboarding/screens/educational_background_form_screen.dart @@ -11,6 +11,11 @@ class EducationalBackgroundFormScreen extends ViewModelWidget { const EducationalBackgroundFormScreen({super.key}); + void _pop(OnboardingViewModel viewModel) { + viewModel.resetEducationalBackgroundFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -77,9 +82,9 @@ class EducationalBackgroundFormScreen ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); diff --git a/lib/ui/views/onboarding/screens/full_name_form_screen.dart b/lib/ui/views/onboarding/screens/full_name_form_screen.dart index 0df2e13..d8fff29 100644 --- a/lib/ui/views/onboarding/screens/full_name_form_screen.dart +++ b/lib/ui/views/onboarding/screens/full_name_form_screen.dart @@ -72,7 +72,7 @@ class FullNameFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceLarge, _buildFullNameFormField(viewModel), if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName) @@ -96,7 +96,7 @@ class FullNameFormScreen extends ViewModelWidget { ), ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => const Text( 'We’ll use your name to personalize your learning journey.', style: TextStyle(color: kcMediumGrey), ); diff --git a/lib/ui/views/onboarding/screens/gender_form_screen.dart b/lib/ui/views/onboarding/screens/gender_form_screen.dart index e292d71..46d41a5 100644 --- a/lib/ui/views/onboarding/screens/gender_form_screen.dart +++ b/lib/ui/views/onboarding/screens/gender_form_screen.dart @@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; class GenderFormScreen extends ViewModelWidget { const GenderFormScreen({super.key}); + void _pop(OnboardingViewModel viewModel) { + viewModel.resetGenderFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -63,30 +68,26 @@ class GenderFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceMedium, _buildAgeGroups(viewModel) ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Choose your gender?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'We’ll personalize your learning experience based on your gender.', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder( diff --git a/lib/ui/views/onboarding/screens/language_goal_form_screen.dart b/lib/ui/views/onboarding/screens/language_goal_form_screen.dart index ae8b086..cfbd302 100644 --- a/lib/ui/views/onboarding/screens/language_goal_form_screen.dart +++ b/lib/ui/views/onboarding/screens/language_goal_form_screen.dart @@ -14,6 +14,12 @@ class LanguageGoalFormScreen extends ViewModelWidget { const LanguageGoalFormScreen( {super.key, required this.languageGoalController}); + void _pop(OnboardingViewModel viewModel) { + languageGoalController.clear(); + viewModel.resetLanguageGoalFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -75,7 +81,7 @@ class LanguageGoalFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceMedium, _buildReasons(viewModel), if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel), @@ -91,26 +97,20 @@ class LanguageGoalFormScreen extends ViewModelWidget { ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'What’s your main goal for improving your English?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'Your goal helps us tailor your learning journey.', - style: TextStyle( - color: kcMediumGrey, - ), + style: style14MG400, ); Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder( @@ -154,11 +154,7 @@ class LanguageGoalFormScreen extends ViewModelWidget { Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text( viewModel.languageGoalValidationMessage!, - style: const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w700, - ), + style: style12R700, ); Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( diff --git a/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart b/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart index f634218..8716c8a 100644 --- a/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart +++ b/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart @@ -23,6 +23,11 @@ class LearningGoalFormScreen extends ViewModelWidget { return Icons.book; } + void _pop(OnboardingViewModel viewModel) { + viewModel.resetLearningGoalFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -87,9 +92,9 @@ class LearningGoalFormScreen extends ViewModelWidget { ]; Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( - onPop: viewModel.pop, showBackButton: true, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); diff --git a/lib/ui/views/onboarding/screens/occupation_form_screen.dart b/lib/ui/views/onboarding/screens/occupation_form_screen.dart index 9e1db8a..2198914 100644 --- a/lib/ui/views/onboarding/screens/occupation_form_screen.dart +++ b/lib/ui/views/onboarding/screens/occupation_form_screen.dart @@ -13,6 +13,12 @@ class OccupationFormScreen extends ViewModelWidget { const OccupationFormScreen({super.key, required this.occupationController}); + void _pop(OnboardingViewModel viewModel) { + occupationController.clear(); + viewModel.resetOccupationFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -71,7 +77,7 @@ class OccupationFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceLarge, _buildOccupationFormField(viewModel), if (viewModel.hasOccupationValidationMessage && @@ -84,23 +90,19 @@ class OccupationFormScreen extends ViewModelWidget { Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( showBackButton: true, - onPop: viewModel.pop, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'What’s your occupation?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'We’ll personalize your learning experience based on your occupation.', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildOccupationFormField(OnboardingViewModel viewModel) => @@ -120,11 +122,7 @@ class OccupationFormScreen extends ViewModelWidget { Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text( viewModel.occupationValidationMessage!, - style: const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w700, - ), + style: style12R700, ); Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( diff --git a/lib/ui/views/onboarding/screens/topic_form_screen.dart b/lib/ui/views/onboarding/screens/topic_form_screen.dart index a5390a4..11fa713 100644 --- a/lib/ui/views/onboarding/screens/topic_form_screen.dart +++ b/lib/ui/views/onboarding/screens/topic_form_screen.dart @@ -13,6 +13,12 @@ class TopicFormScreen extends ViewModelWidget { const TopicFormScreen({super.key, required this.topicController}); + void _pop(OnboardingViewModel viewModel) { + topicController.clear(); + viewModel.resetTopicFormScreen(); + viewModel.goBack(); + } + Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); @@ -45,8 +51,8 @@ class TopicFormScreen extends ViewModelWidget { Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( showBackButton: true, - onPop: viewModel.pop, showLanguageSelection: true, + onPop: () => _pop(viewModel), onLanguage: () async => await viewModel.navigateToLanguage(), ); @@ -82,7 +88,7 @@ class TopicFormScreen extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), verticalSpaceSmall, - _buildSubTitle(), + _buildSubtitle(), verticalSpaceMedium, _buildTopics(viewModel), if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel), @@ -97,18 +103,14 @@ class TopicFormScreen extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Which topics interest you most?', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitle() => const Text( + Widget _buildSubtitle() => Text( 'Your favorite topics help us create fun, relatable lessons.', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder( @@ -150,11 +152,7 @@ class TopicFormScreen extends ViewModelWidget { Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text( viewModel.topicValidationMessage!, - style: const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w700, - ), + style: style12R700, ); Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( diff --git a/lib/ui/views/profile/profile_view.dart b/lib/ui/views/profile/profile_view.dart index 5f2b878..0ab096c 100644 --- a/lib/ui/views/profile/profile_view.dart +++ b/lib/ui/views/profile/profile_view.dart @@ -164,28 +164,28 @@ class ProfileView extends StackedView { Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard( icon: Icons.download, title: 'My Downloads', - subTitle: 'Access offline lessons and saved videos', + subtitle: 'Access offline lessons and saved videos', onTap: () async => await viewModel.navigateToDownloads(), ); Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard( title: 'My Progress', icon: Icons.stacked_bar_chart, - subTitle: 'Track your achievements and learning streak', + subtitle: 'Track your achievements and learning streak', onTap: () async => await viewModel.navigateToProgress(), ); Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard( title: 'Account & Privacy', icon: Icons.privacy_tip_outlined, - subTitle: 'Manage setting and app preference', + subtitle: 'Manage setting and app preference', onTap: () async => await viewModel.navigateToAccountPrivacy(), ); Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard( title: 'Support', icon: Icons.headphones, - subTitle: 'Get help through phone or Telegram', + subtitle: 'Get help through phone or Telegram', onTap: () async => await viewModel.navigateToSupport(), ); diff --git a/lib/ui/views/profile_detail/profile_detail_view.dart b/lib/ui/views/profile_detail/profile_detail_view.dart index e230086..bb5305a 100644 --- a/lib/ui/views/profile_detail/profile_detail_view.dart +++ b/lib/ui/views/profile_detail/profile_detail_view.dart @@ -700,5 +700,7 @@ class ProfileDetailView extends StackedView ); Widget _buildState(ProfileDetailViewModel viewModel) => - viewModel.isBusy ? const PageLoadingIndicator() : Container(); + viewModel.busy(StateObjects.profileUpdate) + ? const PageLoadingIndicator() + : Container(); } diff --git a/lib/ui/views/profile_detail/profile_detail_viewmodel.dart b/lib/ui/views/profile_detail/profile_detail_viewmodel.dart index 62c5185..fb73aa4 100644 --- a/lib/ui/views/profile_detail/profile_detail_viewmodel.dart +++ b/lib/ui/views/profile_detail/profile_detail_viewmodel.dart @@ -220,7 +220,8 @@ class ProfileDetailViewModel extends ReactiveViewModel } // Update profile - Future updateProfile() async => await runBusyFuture(_updateProfile()); + Future updateProfile() async => await runBusyFuture(_updateProfile(), + busyObject: StateObjects.profileUpdate); Future _updateProfile() async { if (await _statusChecker.checkConnection()) { @@ -228,8 +229,8 @@ class ProfileDetailViewModel extends ReactiveViewModel await _apiService.completeProfile(_userData); if (response['status'] == ResponseStatus.success) { await _authenticationService.updateUserData(_userData); - showSuccessToast(response['message']); pop(); + showSuccessToast(response['message']); } else { showErrorToast(response['message']); } diff --git a/lib/ui/views/progress/progress_view.dart b/lib/ui/views/progress/progress_view.dart index 4b90527..4b42ac7 100644 --- a/lib/ui/views/progress/progress_view.dart +++ b/lib/ui/views/progress/progress_view.dart @@ -101,7 +101,7 @@ class ProgressView extends StackedView { title: viewModel.progresses[index]['title'], color: viewModel.progresses[index]['color'], status: viewModel.progresses[index]['status'], - subTitle: viewModel.progresses[index]['subTitle'], + subtitle: viewModel.progresses[index]['subtitle'], isCompleted: viewModel.progresses[index]['isCompleted'], ), ); @@ -111,7 +111,7 @@ class ProgressView extends StackedView { required String title, required String icon, required String status, - required String subTitle, + required String subtitle, required bool isCompleted, required ProgressViewModel viewModel}) => CourseLevelCard( @@ -119,7 +119,7 @@ class ProgressView extends StackedView { title: title, color: color, status: status, - subTitle: subTitle, + subtitle: subtitle, isCompleted: isCompleted, onTap: viewModel.navigateToOngoingProgress, ); diff --git a/lib/ui/views/progress/progress_viewmodel.dart b/lib/ui/views/progress/progress_viewmodel.dart index 7ac29a4..c019acf 100644 --- a/lib/ui/views/progress/progress_viewmodel.dart +++ b/lib/ui/views/progress/progress_viewmodel.dart @@ -15,7 +15,7 @@ class ProgressViewModel extends BaseViewModel { 'isCompleted': true, 'status': 'Completed', 'icon': 'assets/icons/b_1.svg', - 'subTitle': 'You’ve mastered everyday English basics!', + 'subtitle': 'You’ve mastered everyday English basics!', }, { 'title': 'Elementary', @@ -23,7 +23,7 @@ class ProgressViewModel extends BaseViewModel { 'status': 'In Progress', 'color': kcPrimaryColor, 'icon': 'assets/icons/b_1.svg', - 'subTitle': 'Continue improving your conversations and fluency.', + 'subtitle': 'Continue improving your conversations and fluency.', }, { 'title': 'Beginner', @@ -31,7 +31,7 @@ class ProgressViewModel extends BaseViewModel { 'status': 'In Progress', 'color': kcPrimaryColor, 'icon': 'assets/icons/b_1.svg', - 'subTitle': 'You’ve mastered everyday English basics!', + 'subtitle': 'You’ve mastered everyday English basics!', }, ]; diff --git a/lib/ui/views/register/register_view.dart b/lib/ui/views/register/register_view.dart index e84a96b..6465c6a 100644 --- a/lib/ui/views/register/register_view.dart +++ b/lib/ui/views/register/register_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart'; import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart'; import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart'; @@ -24,8 +25,46 @@ import 'register_view.form.dart'; class RegisterView extends StackedView with $RegisterView { const RegisterView({Key? key}) : super(key: key); + void _initClearData() { + otpController.clear(); + emailController.clear(); + passwordController.clear(); + phoneNumberController.clear(); + confirmPasswordController.clear(); + } + + void _inAppPop(RegisterViewModel viewModel) { + print('OnPop'); + print(viewModel.currentPage); + _clearDataOnNavigation(viewModel); + viewModel.goBack(); + } + + void _clearDataOnNavigation(RegisterViewModel viewModel) { + if (viewModel.currentPage == 0) { + emailController.clear(); + viewModel.resetRegisterWithEmailScreen(); + } else if (viewModel.currentPage == 2) { + passwordController.clear(); + confirmPasswordController.clear(); + viewModel.resetCreatePasswordScreen(); + } else if (viewModel.currentPage == 3) { + otpController.clear(); + viewModel.resetRegistrationOtpScreen(); + } + } + + void _pop({required bool value, required RegisterViewModel viewModel}) { + { + if (!value) return; + _clearDataOnNavigation(viewModel); + WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); + } + } + @override void onViewModelReady(RegisterViewModel viewModel) { + _initClearData(); syncFormWithViewModel(viewModel); super.onViewModelReady(viewModel); } @@ -44,10 +83,8 @@ class RegisterView extends StackedView with $RegisterView { Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope( canPop: false, - onPopInvokedWithResult: (value, data) { - if (value) return; - WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); - }, + onPopInvokedWithResult: (value, data) => + _pop(value: value, viewModel: viewModel), child: _buildScaffoldWrapper(viewModel)); Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold( @@ -55,8 +92,11 @@ class RegisterView extends StackedView with $RegisterView { body: _buildScaffoldStack(viewModel), ); - Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack( - children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]); + Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(children: [ + _buildScaffold(viewModel), + _buildRegistrationState(viewModel), + _buildVerityOtpState(viewModel) + ]); Widget _buildScaffold(RegisterViewModel viewModel) => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -68,8 +108,8 @@ class RegisterView extends StackedView with $RegisterView { Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar( showBackButton: true, - onPop: viewModel.goBack, showLanguageSelection: true, + onPop: () => _inAppPop(viewModel), ); Widget _buildExpandedBody(RegisterViewModel viewModel) => @@ -81,7 +121,7 @@ class RegisterView extends StackedView with $RegisterView { ); Widget _buildBody(RegisterViewModel viewModel) => - IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); + IndexedStack(index: viewModel.currentPage, children: _buildScreens()); List _buildScreens() => [ _buildRegisterWithEmailScreen(), @@ -106,6 +146,13 @@ class RegisterView extends StackedView with $RegisterView { passwordController: passwordController, confirmPasswordController: confirmPasswordController); - Widget _buildBusyRegistration(RegisterViewModel viewModel) => - viewModel.isBusy ? const PageLoadingIndicator() : Container(); + Widget _buildRegistrationState(RegisterViewModel viewModel) => + viewModel.busy(StateObjects.registration) + ? const PageLoadingIndicator() + : Container(); + + Widget _buildVerityOtpState(RegisterViewModel viewModel) => + viewModel.busy(StateObjects.verifyOtp) + ? const PageLoadingIndicator() + : Container(); } diff --git a/lib/ui/views/register/register_viewmodel.dart b/lib/ui/views/register/register_viewmodel.dart index f799c10..72adb44 100644 --- a/lib/ui/views/register/register_viewmodel.dart +++ b/lib/ui/views/register/register_viewmodel.dart @@ -22,9 +22,9 @@ class RegisterViewModel extends FormViewModel { final _authenticationService = locator(); // Navigation - int _currentIndex = 0; + int _currentPage = 0; - int get currentIndex => _currentIndex; + int get currentPage => _currentPage; // Email bool _focusEmail = false; @@ -213,9 +213,35 @@ class RegisterViewModel extends FormViewModel { _userData.clear(); } + // Form reset + + // Reset register with email screen + void resetRegisterWithEmailScreen() { + _focusEmail = false; + rebuildUi(); + } + + // Reset create password screen + void resetCreatePasswordScreen() { + _agree = false; + _length = false; + _number = false; + _specialChar = false; + _passwordMatch = false; + _focusPassword = false; + _focusConfirmPassword = false; + rebuildUi(); + } + + // Reset registration otp screen + void resetRegistrationOtpScreen() { + _focusOtp = false; + rebuildUi(); + } + // In-app navigation void goTo({required int page, RegistrationType? type}) { - _currentIndex = page; + _currentPage = page; if (type != null) { _registrationType = type; } @@ -223,17 +249,17 @@ class RegisterViewModel extends FormViewModel { } void goBack() { - if (_currentIndex == 1) { - _currentIndex = 0; + if (_currentPage == 1) { + _currentPage = 0; rebuildUi(); - } else if (_currentIndex == 2) { - _currentIndex = 0; + } else if (_currentPage == 2) { + _currentPage = 0; rebuildUi(); - } else if (_currentIndex == 3) { + } else if (_currentPage == 3) { if (_registrationType == RegistrationType.phone) { - _currentIndex = 1; + _currentPage = 1; } else { - _currentIndex = 2; + _currentPage = 2; } rebuildUi(); @@ -258,7 +284,8 @@ class RegisterViewModel extends FormViewModel { // Remote api calls // Register - Future register() async => await runBusyFuture(_register()); + Future register() async => + await runBusyFuture(_register(), busyObject: StateObjects.registration); Future _register() async { if (await _statusChecker.checkConnection()) { @@ -273,7 +300,8 @@ class RegisterViewModel extends FormViewModel { } } - Future verifyOtp() async => await runBusyFuture(_verifyOtp()); + Future verifyOtp() async => + await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp); Future _verifyOtp() async { if (await _statusChecker.checkConnection()) { @@ -296,7 +324,8 @@ class RegisterViewModel extends FormViewModel { } // Resend otp - Future resendOtp() async => await runBusyFuture(_resendOtp()); + Future resendOtp() async => + await runBusyFuture(_resendOtp(), busyObject: StateObjects.resendOtp); Future _resendOtp() async { if (await _statusChecker.checkConnection()) { diff --git a/lib/ui/views/register/screens/register_with_email_screen.dart b/lib/ui/views/register/screens/register_with_email_screen.dart index fd6aadc..1b67670 100644 --- a/lib/ui/views/register/screens/register_with_email_screen.dart +++ b/lib/ui/views/register/screens/register_with_email_screen.dart @@ -55,7 +55,7 @@ class RegisterWithEmailScreen extends ViewModelWidget { List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ verticalSpaceMedium, _buildTitle(), - _buildSubTitleWrapper(viewModel), + _buildSubtitleWrapper(viewModel), verticalSpaceLarge, _buildEmailFormField(viewModel), if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) @@ -73,7 +73,7 @@ class RegisterWithEmailScreen extends ViewModelWidget { ), ); - Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount( + Widget _buildSubtitleWrapper(RegisterViewModel viewModel) => LoginAccount( onTap: () async => await viewModel.replaceToLogin(), ); diff --git a/lib/ui/views/register/screens/register_with_phone_number_screen.dart b/lib/ui/views/register/screens/register_with_phone_number_screen.dart index 7d3499a..8ab26a5 100644 --- a/lib/ui/views/register/screens/register_with_phone_number_screen.dart +++ b/lib/ui/views/register/screens/register_with_phone_number_screen.dart @@ -44,7 +44,7 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget { List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ verticalSpaceMedium, _buildTitle(), - _buildSubTitleWrapper(viewModel), + _buildSubtitleWrapper(viewModel), verticalSpaceMedium, _buildSubtitle(), verticalSpaceMedium, @@ -57,22 +57,18 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget { _buildPhoneNumberValidatorWrapper(viewModel), ]; - Widget _buildTitle() => const Text( + Widget _buildTitle() => Text( 'Create an Account', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style25DG600, ); - Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount( + Widget _buildSubtitleWrapper(RegisterViewModel viewModel) => LoginAccount( onTap: () async => await viewModel.replaceToLogin(), ); - Widget _buildSubtitle() => const Text( + Widget _buildSubtitle() => Text( 'Enter your phone number. We will send you a confirmation code there', - style: TextStyle(color: kcMediumGrey), + style: style14MG400, ); Widget _buildPhoneNumberWrapper(RegisterViewModel viewModel) => Row( diff --git a/lib/ui/views/register/screens/registration_otp_screen.dart b/lib/ui/views/register/screens/registration_otp_screen.dart index 8076159..7a8f547 100644 --- a/lib/ui/views/register/screens/registration_otp_screen.dart +++ b/lib/ui/views/register/screens/registration_otp_screen.dart @@ -126,6 +126,8 @@ class RegistrationOtpScreen extends ViewModelWidget { Widget _buildResendButton(RegisterViewModel viewModel) => TextButton( onPressed: () async => await viewModel.resendOtp(), + style: + const ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.zero)), child: _buildResendText()); Widget _buildResendText() => Text( diff --git a/lib/ui/widgets/course_level_card.dart b/lib/ui/widgets/course_level_card.dart index b3e1074..c49e5fb 100644 --- a/lib/ui/widgets/course_level_card.dart +++ b/lib/ui/widgets/course_level_card.dart @@ -11,7 +11,7 @@ class CourseLevelCard extends StatelessWidget { final String icon; final String title; final String status; - final String subTitle; + final String subtitle; final bool isCompleted; final GestureTapCallback? onTap; @@ -22,7 +22,7 @@ class CourseLevelCard extends StatelessWidget { required this.title, required this.color, required this.status, - required this.subTitle, + required this.subtitle, required this.isCompleted, }); @@ -91,7 +91,7 @@ class CourseLevelCard extends StatelessWidget { Widget _buildSubTitle() => Expanded( child: Text( - subTitle, + subtitle, maxLines: 3, style: const TextStyle(color: kcMediumGrey), ), diff --git a/lib/ui/widgets/custom_large_radio_button.dart b/lib/ui/widgets/custom_large_radio_button.dart index 1193070..4b3644c 100644 --- a/lib/ui/widgets/custom_large_radio_button.dart +++ b/lib/ui/widgets/custom_large_radio_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; class CustomLargeRadioButton extends StatelessWidget { final String title; @@ -49,7 +50,7 @@ class CustomLargeRadioButton extends StatelessWidget { ); List _buildButtonRowChildren() => - [_buildIconSectionWrapper(), _buildTitle(), _buildSubTitle()]; + [_buildIconSectionWrapper(), _buildTitle(), _buildSubtitle()]; Widget _buildIconSectionWrapper() => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -67,14 +68,10 @@ class CustomLargeRadioButton extends StatelessWidget { Widget _buildTitle() => Text( title, - style: const TextStyle( - fontSize: 18, - color: kcDarkGrey, - fontWeight: FontWeight.w500, - ), + style: style18DG600, ); - Widget _buildSubTitle() => Text( + Widget _buildSubtitle() => Text( subtitle, style: const TextStyle(color: kcMediumGrey), ); diff --git a/lib/ui/widgets/learn_app_bar.dart b/lib/ui/widgets/learn_app_bar.dart index e5ec115..2d56ccf 100644 --- a/lib/ui/widgets/learn_app_bar.dart +++ b/lib/ui/widgets/learn_app_bar.dart @@ -66,7 +66,7 @@ class LearnAppBar extends StatelessWidget { ); List _buildGreetingChildren() => - [_buildGreetingTitle(), _buildSubTitle()]; + [_buildGreetingTitle(), _buildSubtitle()]; Widget _buildGreetingTitle() => Text.rich( TextSpan(text: 'Hello,', style: style14DG600, children: [ @@ -77,7 +77,7 @@ class LearnAppBar extends StatelessWidget { ]), ); - Widget _buildSubTitle() => Text( + Widget _buildSubtitle() => Text( 'Ready to keep learning English today?', textAlign: TextAlign.center, style: style14DG400, diff --git a/lib/ui/widgets/profile_card.dart b/lib/ui/widgets/profile_card.dart index 6563a8a..d7b44f7 100644 --- a/lib/ui/widgets/profile_card.dart +++ b/lib/ui/widgets/profile_card.dart @@ -5,7 +5,7 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; class ProfileCard extends StatelessWidget { final String title; final IconData icon; - final String subTitle; + final String subtitle; final GestureTapCallback? onTap; const ProfileCard( @@ -13,7 +13,7 @@ class ProfileCard extends StatelessWidget { this.onTap, required this.icon, required this.title, - required this.subTitle}); + required this.subtitle}); @override Widget build(BuildContext context) => _buildContainerWrapper(); @@ -43,7 +43,7 @@ class ProfileCard extends StatelessWidget { verticalSpaceSmall, _buildTitle(), verticalSpaceSmall, - _buildSubTitle() + _buildSubtitle() ]; Widget _buildIcon() => Icon( @@ -54,15 +54,11 @@ class ProfileCard extends StatelessWidget { Widget _buildTitle() => Text( title, - style: const TextStyle( - fontSize: 16, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + style: style16DG600, ); - Widget _buildSubTitle() => Text( - subTitle, - style: const TextStyle(color: kcMediumGrey), + Widget _buildSubtitle() => Text( + subtitle, + style: style14MG400, ); } diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart index 6117c43..3cfd350 100644 --- a/test/helpers/test_helpers.mocks.dart +++ b/test/helpers/test_helpers.mocks.dart @@ -1016,6 +1016,33 @@ class MockApiService extends _i1.Mock implements _i12.ApiService { _i8.Future>.value({}), ) as _i8.Future>); + @override + _i8.Future> requestResetCode( + Map? data) => + (super.noSuchMethod( + Invocation.method( + #requestResetCode, + [data], + ), + returnValue: + _i8.Future>.value({}), + returnValueForMissingStub: + _i8.Future>.value({}), + ) as _i8.Future>); + + @override + _i8.Future> resetPassword(Map? data) => + (super.noSuchMethod( + Invocation.method( + #resetPassword, + [data], + ), + returnValue: + _i8.Future>.value({}), + returnValueForMissingStub: + _i8.Future>.value({}), + ) as _i8.Future>); + @override _i8.Future> getProfileStatus(_i11.UserModel? user) => (super.noSuchMethod( diff --git a/test/viewmodels/forget_password_viewmodel_test.dart b/test/viewmodels/forget_password_viewmodel_test.dart new file mode 100644 index 0000000..a089b2f --- /dev/null +++ b/test/viewmodels/forget_password_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ForgetPasswordViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +}