feat(auth): Add forget password feature

This commit is contained in:
BisratHailu 2026-02-09 13:15:16 +03:00
parent 8110e25cb9
commit 94c0576a87
63 changed files with 1769 additions and 524 deletions

View File

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

View File

@ -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 = <String>{
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 = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(),
settings: data,
);
},
_i3.OnboardingView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(),
settings: data,
);
@ -237,141 +246,147 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(),
);
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data,
);
},
_i5.ProfileView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(),
settings: data,
);
},
_i6.ProfileDetailView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(),
settings: data,
);
},
_i7.DownloadsView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(),
settings: data,
);
},
_i8.ProgressView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(),
settings: data,
);
},
_i9.OngoingProgressView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(),
settings: data,
);
},
_i10.AccountPrivacyView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(),
settings: data,
);
},
_i11.SupportView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(),
settings: data,
);
},
_i12.TelegramSupportView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(),
settings: data,
);
},
_i13.CallSupportView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(),
settings: data,
);
},
_i14.LanguageView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(),
settings: data,
);
},
_i15.PrivacyPolicyView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(),
settings: data,
);
},
_i16.TermsAndConditionsView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(),
settings: data,
);
},
_i17.RegisterView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(),
settings: data,
);
},
_i18.LoginView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(),
settings: data,
);
},
_i19.LearnView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(),
settings: data,
);
},
_i20.LearnLevelView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(),
settings: data,
);
},
_i21.LearnModuleView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(),
settings: data,
);
},
_i22.WelcomeView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(),
settings: data,
);
},
_i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data),
settings: data,
);
},
_i24.LearnLessonView: (data) {
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(),
settings: data,
);
},
_i25.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i26.MaterialPageRoute<dynamic>(
return _i27.MaterialPageRoute<dynamic>(
builder: (context) =>
_i25.FailureView(key: args.key, label: args.label),
settings: data,
);
},
_i26.ForgetPasswordView: (data) {
return _i27.MaterialPageRoute<dynamic>(
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<String, dynamic> 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<dynamic> navigateToHomeView([
int? routerId,
bool preventDuplicates = true,
@ -492,7 +507,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
}
Future<dynamic> 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<dynamic> navigateToAssessmentView({
_i26.Key? key,
_i27.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -792,7 +807,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
}
Future<dynamic> 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<dynamic> navigateToForgetPasswordView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.forgetPasswordView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([
int? routerId,
bool preventDuplicates = true,
@ -837,7 +866,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
}
Future<dynamic> 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<dynamic> replaceWithAssessmentView({
_i26.Key? key,
_i27.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -1137,7 +1166,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
}
Future<dynamic> 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<dynamic> replaceWithForgetPasswordView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.forgetPasswordView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
}

View File

@ -38,8 +38,6 @@ class UserModel {
@JsonKey(name: 'profile_picture_url')
final String? profilePicture;
const UserModel({
this.email,
this.region,

View File

@ -23,6 +23,11 @@ UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
);
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'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<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'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,
};

View File

@ -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<Map<String, dynamic>> emailLogin(Map<String, dynamic> 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<Map<String, dynamic>> requestResetCode(
Map<String, dynamic> 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<Map<String, dynamic>> resetPassword(Map<String, dynamic> 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<Map<String, dynamic>> updateProfile(
// Map<String, dynamic> 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<List<Assessment>> getAssessments() async {
try {

View File

@ -12,12 +12,20 @@ class ImageDownloaderService {
final _service = locator<DioService>();
Future<String> 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,

View File

@ -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';

View File

@ -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,
}

View File

@ -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<String, Style> 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,
),
);
}

View File

@ -107,11 +107,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildHeader(String title) => Text(
title,
style: const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
style: style18DG600,
);
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>

View File

@ -21,6 +21,7 @@ class AssessmentViewModel extends BaseViewModel {
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// In-app navigation
int _currentPage = 0;
int get currentPage => _currentPage;
@ -255,16 +256,17 @@ class AssessmentViewModel extends BaseViewModel {
// Complete profile
Future<void> completeProfile() async =>
await runBusyFuture(_completeProfile());
await runBusyFuture(_completeProfile(),
busyObject: StateObjects.profileCompletion);
Future<void> _completeProfile() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.completeProfile(_userData);
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}

View File

@ -61,27 +61,23 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
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(
'Were 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<AssessmentViewModel> {
height: 55,
borderRadius: 12,
text: 'View My Results',
onTap: () => viewModel.next(),
foregroundColor: kcWhite,
onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor,
);
}

View File

@ -64,25 +64,21 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
];
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
Widget _buildTitle() => const Text(
Widget _buildTitle() => Text(
'We didnt 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 wasnt 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<AssessmentViewModel> {
height: 55,
text: 'Skip',
borderRadius: 12,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -54,7 +54,7 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -69,7 +69,7 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
style: style25DG600,
);
Widget _buildSubTitle() => Text(
Widget _buildSubtitle() => Text(
'Answer a few quick questions to help us understand your English proficiency.',
style: style14MG400,
);

View File

@ -44,7 +44,5 @@ class AssessmentLoadingScreen extends StatelessWidget {
Widget _buildPageIndicator() => const PageLoadingIndicator();
Widget _buildRefreshButton() => RefreshButton(
onTap: onTap,
);
Widget _buildRefreshButton() => RefreshButton(onTap: onTap);
}

View File

@ -63,11 +63,11 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
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<AssessmentViewModel> {
textAlign: TextAlign.center,
);
Widget _buildPrimarySubTitle() => const Text(
Widget _buildPrimarySubtitle() => Text(
'Great Job! Heres 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<AssessmentViewModel> {
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<AssessmentViewModel> {
safe: false,
text: 'Continue',
borderRadius: 12,
onTap: () => viewModel.next(),
foregroundColor: kcWhite,
onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor,
);
@ -127,10 +127,10 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
text: 'Practice Speaking',
borderColor: kcPrimaryColor,
onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor,
);
}

View File

@ -48,7 +48,7 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -61,19 +61,15 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
'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(
'Were now analyzing your speaking skills',
style: style14MG400,
textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
);
}

View File

@ -57,7 +57,7 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -72,20 +72,16 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
color: kcPrimaryColor,
);
Widget _buildTitle() => const Text(
Widget _buildTitle() => Text(
'We didnt 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 wasnt 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(

View File

@ -80,21 +80,25 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
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']}!',
text: 'Welcome aboard',
style: style25DG600,
)
]),
children: [
TextSpan(
style: style25DG600,
text: ', ${viewModel.userData['first_name']}!',
),
],
),
);
Widget _buildSubTitle() => Text(
Widget _buildSubtitle() => Text(
'Youre ready to explore your personalized lessons.',
style: style14MG400,
);
@ -115,5 +119,7 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
);
Widget _buildState(AssessmentViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
viewModel.busy(StateObjects.profileCompletion)
? const PageLoadingIndicator()
: Container();
}

View File

@ -92,14 +92,10 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
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(

View File

@ -178,7 +178,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
verticalSpaceMedium,
_buildEmptyTitle(),
verticalSpaceSmall,
_buildEmptySubTitle(),
_buildEmptySubtitle(),
];
Widget _buildEmptyIcon() => const Icon(
@ -197,7 +197,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
),
);
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),

View File

@ -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<ForgetPasswordViewModel>
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<Widget> _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<Widget> _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();
}

View File

@ -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<String, TextEditingController>
_ForgetPasswordViewTextEditingControllers = {};
final Map<String, FocusNode> _ForgetPasswordViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_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),
});

View File

@ -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<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// User data
final Map<String, dynamic> _userData = {};
Map<String, dynamic> 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<String, dynamic> 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<void> replaceWithLogin() async =>
await _navigationService.clearStackAndShowView(const LoginView());
// Remote api calls
// Request reset code
Future<void> requestResetCode() async =>
await runBusyFuture(_requestResetCode(),
busyObject: StateObjects.requestResetCode);
Future<void> _requestResetCode() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.requestResetCode(_userData);
if (response['status'] == ResponseStatus.success) {
goTo(1);
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
// Request reset code
Future<void> resetPassword() async => await runBusyFuture(_resetPassword(),
busyObject: StateObjects.resetPassword);
Future<void> _resetPassword() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.resetPassword(_userData);
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
await replaceWithLogin();
} else {
showErrorToast(response['message']);
}
}
}
}

View File

@ -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<ForgetPasswordViewModel> {
final TextEditingController emailController;
const RequestCodeScreen({
super.key,
required this.emailController,
});
Future<void> _addUserData(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> 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<Widget> _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<Widget> _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),
);
}

View File

@ -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<ForgetPasswordViewModel> {
final TextEditingController resetCodeController;
final TextEditingController passwordController;
final TextEditingController confirmPasswordController;
const ResetPasswordScreen(
{super.key,
required this.resetCodeController,
required this.passwordController,
required this.confirmPasswordController});
Future<void> _reset(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> 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<Widget> _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),
);
}

View File

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

View File

@ -61,7 +61,7 @@ class LanguageView extends StackedView<LanguageViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildLanguages(viewModel)
];
@ -72,22 +72,18 @@ class LanguageView extends StackedView<LanguageViewModel> {
);
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(

View File

@ -73,20 +73,14 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
_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();

View File

@ -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<LoginViewModel> 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<LoginViewModel> 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<LoginViewModel> 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();
}

View File

@ -138,13 +138,17 @@ class LoginViewModel extends FormViewModel {
Future<void> navigateToRegister() async =>
await _navigationService.navigateToRegisterView();
Future<void> navigateToForgetPassword() async =>
await _navigationService.navigateToForgetPasswordView();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
// Remote api calls
// Login with email
Future<void> emailLogin() async => await runBusyFuture(_emailLogin());
Future<void> emailLogin() async => await runBusyFuture(_emailLogin(),
busyObject: StateObjects.loginWithEmail);
Future<void> _emailLogin() async {
if (await _statusChecker.checkConnection()) {
@ -167,7 +171,8 @@ class LoginViewModel extends FormViewModel {
}
}
Future<void> googleLogin() async => await runBusyFuture(_googleLogin());
Future<void> googleLogin() async => await runBusyFuture(_googleLogin(),
busyObject: StateObjects.loginWithGoogle);
Future<void> _googleLogin() async {
if (await _statusChecker.checkConnection()) {

View File

@ -58,7 +58,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
List<Widget> _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<LoginViewModel> {
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<LoginViewModel> {
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<LoginViewModel> {
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<LoginViewModel> {
backgroundColor: kcWhite,
leadingIcon: Icons.phone,
borderColor: kcPrimaryColor,
onTap: () => viewModel.goTo(1),
foregroundColor: kcPrimaryColor,
text: 'Login with Phone Number',
onTap: () => viewModel.goTo(1),
);
}

View File

@ -44,7 +44,7 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitleWrapper(viewModel),
_buildSubtitleWrapper(viewModel),
verticalSpaceMedium,
_buildSubtitle(),
verticalSpaceMedium,
@ -57,22 +57,18 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
_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(

View File

@ -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<OnboardingViewModel>
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<OnboardingViewModel>
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(

View File

@ -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<String, TextEditingController> _OnboardingViewTextEditingControllers =
final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
AnswerValueKey: FormValidator.validateForm,
FullNameValueKey: FormValidator.validateForm,
ChallengeValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm,
@ -34,8 +32,6 @@ final Map<String, String? Function(String?)?> _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),

View File

@ -372,16 +372,83 @@ class OnboardingViewModel extends FormViewModel {
_userData.clear();
}
// Navigation
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
// Form reset
Future<void> navigateToAssessment() async =>
await _navigationService.navigateToAssessmentView(data: _userData);
// Reset full name form screen
void resetFullNameFormScreen() {
_focusFullName = false;
rebuildUi();
}
Future<void> 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<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToAssessment() async =>
await _navigationService.navigateToAssessmentView(data: _userData);
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
}

View File

@ -10,6 +10,12 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
const AgeGroupFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetAgeGroupFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -74,24 +80,20 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
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(
'Well personalize your learning experience based on your age.',
style: TextStyle(color: kcMediumGrey),
style: style14DG400,
);
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -12,6 +12,12 @@ import '../../../widgets/birthday_selector.dart';
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
const BirthdayFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetBirthdayFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -64,30 +70,26 @@ class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
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(
'Well personalize your learning experience based on your birthday.',
style: TextStyle(color: kcMediumGrey),
style: style14MG400,
);
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>

View File

@ -13,6 +13,12 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
const ChallengeFormScreen({super.key, required this.challengeController});
void _pop(OnboardingViewModel viewModel) {
challengeController.clear();
viewModel.resetChallengeFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -74,7 +80,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildChallenges(viewModel),
if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel),
@ -90,24 +96,20 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
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, lets start fixing yours 😊',
style: TextStyle(color: kcMediumGrey),
style: style14MG400,
);
Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder(
@ -151,11 +153,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
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(

View File

@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
const CountryRegionFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetCountryRegionFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -71,7 +76,7 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildCountryDropDown(viewModel),
verticalSpaceMedium,
@ -80,24 +85,20 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
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) =>

View File

@ -11,6 +11,11 @@ class EducationalBackgroundFormScreen
extends ViewModelWidget<OnboardingViewModel> {
const EducationalBackgroundFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetEducationalBackgroundFormScreen();
viewModel.goBack();
}
Future<void> _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(),
);

View File

@ -72,7 +72,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceLarge,
_buildFullNameFormField(viewModel),
if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName)
@ -96,7 +96,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
),
);
Widget _buildSubTitle() => const Text(
Widget _buildSubtitle() => const Text(
'Well use your name to personalize your learning journey.',
style: TextStyle(color: kcMediumGrey),
);

View File

@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
const GenderFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetGenderFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -63,30 +68,26 @@ class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
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(
'Well personalize your learning experience based on your gender.',
style: TextStyle(color: kcMediumGrey),
style: style14MG400,
);
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -14,6 +14,12 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
const LanguageGoalFormScreen(
{super.key, required this.languageGoalController});
void _pop(OnboardingViewModel viewModel) {
languageGoalController.clear();
viewModel.resetLanguageGoalFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -75,7 +81,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildReasons(viewModel),
if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel),
@ -91,26 +97,20 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
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(
'Whats 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<OnboardingViewModel> {
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(

View File

@ -23,6 +23,11 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
return Icons.book;
}
void _pop(OnboardingViewModel viewModel) {
viewModel.resetLearningGoalFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -87,9 +92,9 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true,
showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(),
);

View File

@ -13,6 +13,12 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
const OccupationFormScreen({super.key, required this.occupationController});
void _pop(OnboardingViewModel viewModel) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -71,7 +77,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceLarge,
_buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage &&
@ -84,23 +90,19 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
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(
'Whats your occupation?',
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
style: style25DG600,
);
Widget _buildSubTitle() => const Text(
Widget _buildSubtitle() => Text(
'Well 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<OnboardingViewModel> {
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(

View File

@ -13,6 +13,12 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
const TopicFormScreen({super.key, required this.topicController});
void _pop(OnboardingViewModel viewModel) {
topicController.clear();
viewModel.resetTopicFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
@ -45,8 +51,8 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
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<OnboardingViewModel> {
verticalSpaceMedium,
_buildTitle(),
verticalSpaceSmall,
_buildSubTitle(),
_buildSubtitle(),
verticalSpaceMedium,
_buildTopics(viewModel),
if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel),
@ -97,18 +103,14 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
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<OnboardingViewModel> {
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(

View File

@ -164,28 +164,28 @@ class ProfileView extends StackedView<ProfileViewModel> {
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(),
);

View File

@ -700,5 +700,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
);
Widget _buildState(ProfileDetailViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
viewModel.busy(StateObjects.profileUpdate)
? const PageLoadingIndicator()
: Container();
}

View File

@ -220,7 +220,8 @@ class ProfileDetailViewModel extends ReactiveViewModel
}
// Update profile
Future<void> updateProfile() async => await runBusyFuture(_updateProfile());
Future<void> updateProfile() async => await runBusyFuture(_updateProfile(),
busyObject: StateObjects.profileUpdate);
Future<void> _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']);
}

View File

@ -101,7 +101,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
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<ProgressViewModel> {
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<ProgressViewModel> {
title: title,
color: color,
status: status,
subTitle: subTitle,
subtitle: subtitle,
isCompleted: isCompleted,
onTap: viewModel.navigateToOngoingProgress,
);

View File

@ -15,7 +15,7 @@ class ProgressViewModel extends BaseViewModel {
'isCompleted': true,
'status': 'Completed',
'icon': 'assets/icons/b_1.svg',
'subTitle': 'Youve mastered everyday English basics!',
'subtitle': 'Youve 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': 'Youve mastered everyday English basics!',
'subtitle': 'Youve mastered everyday English basics!',
},
];

View File

@ -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<RegisterViewModel> 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<RegisterViewModel> 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<RegisterViewModel> 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<RegisterViewModel> 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<RegisterViewModel> with $RegisterView {
);
Widget _buildBody(RegisterViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildRegisterWithEmailScreen(),
@ -106,6 +146,13 @@ class RegisterView extends StackedView<RegisterViewModel> 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();
}

View File

@ -22,9 +22,9 @@ class RegisterViewModel extends FormViewModel {
final _authenticationService = locator<AuthenticationService>();
// 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<void> register() async => await runBusyFuture(_register());
Future<void> register() async =>
await runBusyFuture(_register(), busyObject: StateObjects.registration);
Future<void> _register() async {
if (await _statusChecker.checkConnection()) {
@ -273,7 +300,8 @@ class RegisterViewModel extends FormViewModel {
}
}
Future<void> verifyOtp() async => await runBusyFuture(_verifyOtp());
Future<void> verifyOtp() async =>
await runBusyFuture(_verifyOtp(), busyObject: StateObjects.verifyOtp);
Future<void> _verifyOtp() async {
if (await _statusChecker.checkConnection()) {
@ -296,7 +324,8 @@ class RegisterViewModel extends FormViewModel {
}
// Resend otp
Future<void> resendOtp() async => await runBusyFuture(_resendOtp());
Future<void> resendOtp() async =>
await runBusyFuture(_resendOtp(), busyObject: StateObjects.resendOtp);
Future<void> _resendOtp() async {
if (await _statusChecker.checkConnection()) {

View File

@ -55,7 +55,7 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
List<Widget> _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<RegisterViewModel> {
),
);
Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount(
Widget _buildSubtitleWrapper(RegisterViewModel viewModel) => LoginAccount(
onTap: () async => await viewModel.replaceToLogin(),
);

View File

@ -44,7 +44,7 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget<RegisterViewModel> {
List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubTitleWrapper(viewModel),
_buildSubtitleWrapper(viewModel),
verticalSpaceMedium,
_buildSubtitle(),
verticalSpaceMedium,
@ -57,22 +57,18 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget<RegisterViewModel> {
_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(

View File

@ -126,6 +126,8 @@ class RegistrationOtpScreen extends ViewModelWidget<RegisterViewModel> {
Widget _buildResendButton(RegisterViewModel viewModel) => TextButton(
onPressed: () async => await viewModel.resendOtp(),
style:
const ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.zero)),
child: _buildResendText());
Widget _buildResendText() => Text(

View File

@ -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),
),

View File

@ -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<Widget> _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),
);

View File

@ -66,7 +66,7 @@ class LearnAppBar extends StatelessWidget {
);
List<Widget> _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,

View File

@ -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,
);
}

View File

@ -1016,6 +1016,33 @@ class MockApiService extends _i1.Mock implements _i12.ApiService {
_i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i8.Future<Map<String, dynamic>>);
@override
_i8.Future<Map<String, dynamic>> requestResetCode(
Map<String, dynamic>? data) =>
(super.noSuchMethod(
Invocation.method(
#requestResetCode,
[data],
),
returnValue:
_i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
returnValueForMissingStub:
_i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i8.Future<Map<String, dynamic>>);
@override
_i8.Future<Map<String, dynamic>> resetPassword(Map<String, dynamic>? data) =>
(super.noSuchMethod(
Invocation.method(
#resetPassword,
[data],
),
returnValue:
_i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
returnValueForMissingStub:
_i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i8.Future<Map<String, dynamic>>);
@override
_i8.Future<Map<String, dynamic>> getProfileStatus(_i11.UserModel? user) =>
(super.noSuchMethod(

View File

@ -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());
});
}