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/image_picker_service.dart';
import 'package:yimaru_app/services/google_auth_service.dart'; import 'package:yimaru_app/services/google_auth_service.dart';
import 'package:yimaru_app/services/image_downloader_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 // @stacked-import
@StackedApp( @StackedApp(
@ -63,6 +64,7 @@ import 'package:yimaru_app/services/image_downloader_service.dart';
MaterialRoute(page: AssessmentView), MaterialRoute(page: AssessmentView),
MaterialRoute(page: LearnLessonView), MaterialRoute(page: LearnLessonView),
MaterialRoute(page: FailureView), MaterialRoute(page: FailureView),
MaterialRoute(page: ForgetPasswordView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [

View File

@ -5,10 +5,10 @@
// ************************************************************************** // **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes // 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:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1; 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' import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i10; as _i10;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23; 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; as _i13;
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; 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/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/home/home_view.dart' as _i2;
import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14; import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19; 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 failureView = '/failure-view';
static const forgetPasswordView = '/forget-password-view';
static const all = <String>{ static const all = <String>{
homeView, homeView,
onboardingView, onboardingView,
@ -117,6 +121,7 @@ class Routes {
assessmentView, assessmentView,
learnLessonView, learnLessonView,
failureView, failureView,
forgetPasswordView,
}; };
} }
@ -218,17 +223,21 @@ class StackedRouter extends _i1.RouterBase {
Routes.failureView, Routes.failureView,
page: _i25.FailureView, page: _i25.FailureView,
), ),
_i1.RouteDef(
Routes.forgetPasswordView,
page: _i26.ForgetPasswordView,
),
]; ];
final _pagesMap = <Type, _i1.StackedRouteFactory>{ final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) { _i2.HomeView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(), builder: (context) => const _i2.HomeView(),
settings: data, settings: data,
); );
}, },
_i3.OnboardingView: (data) { _i3.OnboardingView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(), builder: (context) => const _i3.OnboardingView(),
settings: data, settings: data,
); );
@ -237,141 +246,147 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>( final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(), orElse: () => const StartupViewArguments(),
); );
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label), builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data, settings: data,
); );
}, },
_i5.ProfileView: (data) { _i5.ProfileView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(), builder: (context) => const _i5.ProfileView(),
settings: data, settings: data,
); );
}, },
_i6.ProfileDetailView: (data) { _i6.ProfileDetailView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(), builder: (context) => const _i6.ProfileDetailView(),
settings: data, settings: data,
); );
}, },
_i7.DownloadsView: (data) { _i7.DownloadsView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(), builder: (context) => const _i7.DownloadsView(),
settings: data, settings: data,
); );
}, },
_i8.ProgressView: (data) { _i8.ProgressView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(), builder: (context) => const _i8.ProgressView(),
settings: data, settings: data,
); );
}, },
_i9.OngoingProgressView: (data) { _i9.OngoingProgressView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(), builder: (context) => const _i9.OngoingProgressView(),
settings: data, settings: data,
); );
}, },
_i10.AccountPrivacyView: (data) { _i10.AccountPrivacyView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(), builder: (context) => const _i10.AccountPrivacyView(),
settings: data, settings: data,
); );
}, },
_i11.SupportView: (data) { _i11.SupportView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(), builder: (context) => const _i11.SupportView(),
settings: data, settings: data,
); );
}, },
_i12.TelegramSupportView: (data) { _i12.TelegramSupportView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(), builder: (context) => const _i12.TelegramSupportView(),
settings: data, settings: data,
); );
}, },
_i13.CallSupportView: (data) { _i13.CallSupportView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(), builder: (context) => const _i13.CallSupportView(),
settings: data, settings: data,
); );
}, },
_i14.LanguageView: (data) { _i14.LanguageView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(), builder: (context) => const _i14.LanguageView(),
settings: data, settings: data,
); );
}, },
_i15.PrivacyPolicyView: (data) { _i15.PrivacyPolicyView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(), builder: (context) => const _i15.PrivacyPolicyView(),
settings: data, settings: data,
); );
}, },
_i16.TermsAndConditionsView: (data) { _i16.TermsAndConditionsView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(), builder: (context) => const _i16.TermsAndConditionsView(),
settings: data, settings: data,
); );
}, },
_i17.RegisterView: (data) { _i17.RegisterView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(), builder: (context) => const _i17.RegisterView(),
settings: data, settings: data,
); );
}, },
_i18.LoginView: (data) { _i18.LoginView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(), builder: (context) => const _i18.LoginView(),
settings: data, settings: data,
); );
}, },
_i19.LearnView: (data) { _i19.LearnView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(), builder: (context) => const _i19.LearnView(),
settings: data, settings: data,
); );
}, },
_i20.LearnLevelView: (data) { _i20.LearnLevelView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(), builder: (context) => const _i20.LearnLevelView(),
settings: data, settings: data,
); );
}, },
_i21.LearnModuleView: (data) { _i21.LearnModuleView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(), builder: (context) => const _i21.LearnModuleView(),
settings: data, settings: data,
); );
}, },
_i22.WelcomeView: (data) { _i22.WelcomeView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(), builder: (context) => const _i22.WelcomeView(),
settings: data, settings: data,
); );
}, },
_i23.AssessmentView: (data) { _i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false); final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data), _i23.AssessmentView(key: args.key, data: args.data),
settings: data, settings: data,
); );
}, },
_i24.LearnLessonView: (data) { _i24.LearnLessonView: (data) {
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(), builder: (context) => const _i24.LearnLessonView(),
settings: data, settings: data,
); );
}, },
_i25.FailureView: (data) { _i25.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false); final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i26.MaterialPageRoute<dynamic>( return _i27.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i25.FailureView(key: args.key, label: args.label), _i25.FailureView(key: args.key, label: args.label),
settings: data, settings: data,
); );
}, },
_i26.ForgetPasswordView: (data) {
return _i27.MaterialPageRoute<dynamic>(
builder: (context) => const _i26.ForgetPasswordView(),
settings: data,
);
},
}; };
@override @override
@ -387,7 +402,7 @@ class StartupViewArguments {
this.label = 'Loading', this.label = 'Loading',
}); });
final _i26.Key? key; final _i27.Key? key;
final String label; final String label;
@ -414,7 +429,7 @@ class AssessmentViewArguments {
required this.data, required this.data,
}); });
final _i26.Key? key; final _i27.Key? key;
final Map<String, dynamic> data; final Map<String, dynamic> data;
@ -441,7 +456,7 @@ class FailureViewArguments {
required this.label, required this.label,
}); });
final _i26.Key? key; final _i27.Key? key;
final String label; final String label;
@ -462,7 +477,7 @@ class FailureViewArguments {
} }
} }
extension NavigatorStateExtension on _i27.NavigationService { extension NavigatorStateExtension on _i28.NavigationService {
Future<dynamic> navigateToHomeView([ Future<dynamic> navigateToHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -492,7 +507,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
} }
Future<dynamic> navigateToStartupView({ Future<dynamic> navigateToStartupView({
_i26.Key? key, _i27.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -761,7 +776,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
} }
Future<dynamic> navigateToAssessmentView({ Future<dynamic> navigateToAssessmentView({
_i26.Key? key, _i27.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -792,7 +807,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
} }
Future<dynamic> navigateToFailureView({ Future<dynamic> navigateToFailureView({
_i26.Key? key, _i27.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -808,6 +823,20 @@ extension NavigatorStateExtension on _i27.NavigationService {
transition: transition); 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([ Future<dynamic> replaceWithHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -837,7 +866,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
} }
Future<dynamic> replaceWithStartupView({ Future<dynamic> replaceWithStartupView({
_i26.Key? key, _i27.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1106,7 +1135,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
} }
Future<dynamic> replaceWithAssessmentView({ Future<dynamic> replaceWithAssessmentView({
_i26.Key? key, _i27.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1137,7 +1166,7 @@ extension NavigatorStateExtension on _i27.NavigationService {
} }
Future<dynamic> replaceWithFailureView({ Future<dynamic> replaceWithFailureView({
_i26.Key? key, _i27.Key? key,
required String label, required String label,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1152,4 +1181,18 @@ extension NavigatorStateExtension on _i27.NavigationService {
parameters: parameters, parameters: parameters,
transition: transition); 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') @JsonKey(name: 'profile_picture_url')
final String? profilePicture; final String? profilePicture;
const UserModel({ const UserModel({
this.email, this.email,
this.region, this.region,

View File

@ -23,6 +23,11 @@ UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
); );
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{ 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, 'user_id': instance.userId,
'last_name': instance.lastName, 'last_name': instance.lastName,
'birth_day': instance.birthday, 'birth_day': instance.birthday,
@ -31,9 +36,4 @@ Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'refresh_token': instance.refreshToken, 'refresh_token': instance.refreshToken,
'profile_completed': instance.profileCompleted, 'profile_completed': instance.profileCompleted,
'profile_picture_url': instance.profilePicture, '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' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
// Login // Email Login
Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async { Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
@ -57,10 +57,10 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}' 'message': '${response.data['message']}, ${response.data['error']}'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -85,10 +85,10 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}' 'message': '${response.data['message']}, ${response.data['error']}'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -112,10 +112,10 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}' 'message': '${response.data['message']}, ${response.data['error']}'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -139,10 +139,65 @@ class ApiService {
'message': 'Unknown Error Occurred' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, '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']}' 'message': '${response.data['message']}, ${response.data['error']}'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -193,10 +248,10 @@ class ApiService {
'message': 'Unknown Error Occurred' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -221,10 +276,10 @@ class ApiService {
'message': 'Unknown Error Occurred' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -273,105 +328,14 @@ class ApiService {
'message': 'Unknown Error Occurred' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, '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 // Assessments
Future<List<Assessment>> getAssessments() async { Future<List<Assessment>> getAssessments() async {
try { try {

View File

@ -12,12 +12,20 @@ class ImageDownloaderService {
final _service = locator<DioService>(); final _service = locator<DioService>();
Future<String> downloader(String? networkImage) async { Future<String> downloader(String? networkImage) async {
final Directory appDir = await getApplicationDocumentsDirectory();
late File image; 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( final Response profileImageResponse = await _service.dio.get(
'$kBaseUrl$networkImage', profileImage,
options: Options( options: Options(
followRedirects: false, followRedirects: false,
responseType: ResponseType.bytes, responseType: ResponseType.bytes,

View File

@ -11,6 +11,10 @@ String kVerifyOtpUrl = 'verify-otp';
String kResendOtpUrl = 'resend-otp'; String kResendOtpUrl = 'resend-otp';
String kResetPassword = 'resetPassword';
String kRequestResetCode = 'sendResetCode';
String kUpdateProfileImage = 'profile-picture'; String kUpdateProfileImage = 'profile-picture';
String kRefreshTokenUrl = 'api/v1/auth/refresh'; String kRefreshTokenUrl = 'api/v1/auth/refresh';

View File

@ -10,4 +10,15 @@ enum ProgressStatuses { pending, started, completed }
enum ProficiencyLevels { a1, a2, b1, b2, none } enum ProficiencyLevels { a1, a2, b1, b2, none }
// State object // 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, fontWeight: FontWeight.w600,
); );
TextStyle style18DG500 = const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w500,
);
TextStyle style18DG600 = const TextStyle( TextStyle style18DG600 = const TextStyle(
fontSize: 18, fontSize: 18,
color: kcDarkGrey, color: kcDarkGrey,
@ -234,6 +240,8 @@ TextStyle style14LG400 = const TextStyle(
TextStyle style14MG400 = const TextStyle( TextStyle style14MG400 = const TextStyle(
color: kcMediumGrey, color: kcMediumGrey,
); );
TextStyle style14DG500 =
const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500);
TextStyle style14DG400 = const TextStyle( TextStyle style14DG400 = const TextStyle(
color: kcDarkGrey, color: kcDarkGrey,
@ -274,24 +282,27 @@ Map<String, Style> htmlStyle = {
Widget buildToastDescription(String message) => Text( Widget buildToastDescription(String message) => Text(
message, message,
maxLines: 4, maxLines: 4,
style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500), style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500),
); );
void showErrorToast(String message) { void showErrorToast(String message) {
toastification.show( toastification.show(
showIcon: true, showIcon: true,
dragToClose: true, dragToClose: true,
primaryColor: kcRed,
showProgressBar: false, showProgressBar: false,
applyBlurEffect: false, applyBlurEffect: false,
icon: const Icon(Icons.check), alignment: Alignment.topCenter,
primaryColor: kcBackgroundColor,
type: ToastificationType.success, type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored, style: ToastificationStyle.fillColored,
description: buildToastDescription(message), description: buildToastDescription(message),
borderSide: const BorderSide(color: kcWhite), autoCloseDuration: const Duration(seconds: 3),
autoCloseDuration: const Duration(seconds: 5),
margin: const EdgeInsets.symmetric(horizontal: 15), 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, dragToClose: true,
showProgressBar: false, showProgressBar: false,
applyBlurEffect: false, applyBlurEffect: false,
icon: const Icon(Icons.check), alignment: Alignment.topCenter,
primaryColor: kcPrimaryColor, primaryColor: kcBackgroundColor,
type: ToastificationType.success, type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored, style: ToastificationStyle.fillColored,
description: buildToastDescription(message), description: buildToastDescription(message),
borderSide: const BorderSide(color: kcWhite), autoCloseDuration: const Duration(seconds: 3),
autoCloseDuration: const Duration(seconds: 5),
margin: const EdgeInsets.symmetric(horizontal: 15), 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( Widget _buildHeader(String title) => Text(
title, title,
style: const TextStyle( style: style18DG600,
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) => Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>

View File

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

View File

@ -61,27 +61,23 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/complete.svg', 'assets/icons/complete.svg',
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Assessment complete!', 'Assessment complete!',
style: style25DG600,
textAlign: TextAlign.center, 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', 'Were now analyzing your speaking skills',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
@ -94,8 +90,8 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'View My Results', text: 'View My Results',
onTap: () => viewModel.next(),
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
} }

View File

@ -64,25 +64,21 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg'); Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'We didnt get enough from your assessment', 'We didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center, 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 ', 'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -117,9 +113,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
text: 'Skip', text: 'Skip',
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

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

View File

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

View File

@ -63,11 +63,11 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceLarge, verticalSpaceLarge,
_buildTitle(viewModel), _buildTitle(viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildPrimarySubTitle(), _buildPrimarySubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildIconWrapper(viewModel), _buildIconWrapper(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildSecondarySubTitle() _buildSecondarySubtitle()
]; ];
Widget _buildTitle(AssessmentViewModel viewModel) => Text( Widget _buildTitle(AssessmentViewModel viewModel) => Text(
@ -76,10 +76,10 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
Widget _buildPrimarySubTitle() => const Text( Widget _buildPrimarySubtitle() => Text(
'Great Job! Heres your next step to keep improving.', 'Great Job! Heres your next step to keep improving.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildIconWrapper(AssessmentViewModel viewModel) => Widget _buildIconWrapper(AssessmentViewModel viewModel) =>
@ -90,7 +90,7 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset( Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset(
'assets/icons/${viewModel.proficiencyLevel.name.substring(0, 1)}_${viewModel.proficiencyLevel.name.substring(1)}.svg'); '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', 'Let\'s start your practice',
style: style14DG400, style: style14DG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -113,8 +113,8 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
safe: false, safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
onTap: () => viewModel.next(),
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
@ -127,10 +127,10 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite,
text: 'Practice Speaking', text: 'Practice Speaking',
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

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

View File

@ -57,7 +57,7 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -72,20 +72,16 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
color: kcPrimaryColor, color: kcPrimaryColor,
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'We didnt get enough from your assessment', 'We didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center, 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 ', '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, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
); );
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(

View File

@ -80,21 +80,25 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(viewModel), _buildTitle(viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg'); Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg');
Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich( Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich(
TextSpan(text: 'Welcome aboard', style: style25DG600, children: [ TextSpan(
TextSpan( text: 'Welcome aboard',
text: ', ${viewModel.userData['first_name']}!', style: style25DG600,
style: style25DG600, children: [
) TextSpan(
]), style: style25DG600,
text: ', ${viewModel.userData['first_name']}!',
),
],
),
); );
Widget _buildSubTitle() => Text( Widget _buildSubtitle() => Text(
'Youre ready to explore your personalized lessons.', 'Youre ready to explore your personalized lessons.',
style: style14MG400, style: style14MG400,
); );
@ -115,5 +119,7 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
); );
Widget _buildState(AssessmentViewModel viewModel) => 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() => Widget _buildIcon() =>
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor); 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', 'Call our support team between 9 AM - 6 PM',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle(String title) => Text( Widget _buildSubTitle(String title) => Text(

View File

@ -178,7 +178,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildEmptyTitle(), _buildEmptyTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildEmptySubTitle(), _buildEmptySubtitle(),
]; ];
Widget _buildEmptyIcon() => const Icon( 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.', 'Start by exploring your learning materials and save them for offline access.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), 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}; response = {'data': true, 'status': ResponseStatus.success};
} }
if (response['status'] == ResponseStatus.success && !response['data']) { if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding(); await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success && } else if (response['status'] == ResponseStatus.success &&

View File

@ -61,7 +61,7 @@ class LanguageView extends StackedView<LanguageViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildLanguages(viewModel) _buildLanguages(viewModel)
]; ];
@ -72,22 +72,18 @@ class LanguageView extends StackedView<LanguageViewModel> {
); );
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar( Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
title: 'Language Preference',
onTap: viewModel.pop, onTap: viewModel.pop,
title: 'Language Preference',
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Choose your language', 'Choose your language',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'You can switch languages anytime', 'You can switch languages anytime',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder( Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(

View File

@ -73,20 +73,14 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
_buildListView(viewModel) _buildListView(viewModel)
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'A1 - Beginner', 'A1 - Beginner',
style: TextStyle( style: style18P600,
fontSize: 18,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubTitle() => Text(
'Your Current Level', 'Your Current Level',
style: TextStyle( style: style14DG400,
color: kcDarkGrey,
),
); );
Widget _buildOverallProgress() => const OverallLearnProgress(); Widget _buildOverallProgress() => const OverallLearnProgress();

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.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_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_email_screen.dart';
import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_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 @override
void onViewModelReady(LoginViewModel viewModel) { void onViewModelReady(LoginViewModel viewModel) {
_clearData();
syncFormWithViewModel(viewModel); syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
void _clearData() {
otpController.clear();
emailController.clear();
passwordController.clear();
phoneNumberController.clear();
}
@override @override
LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel(); LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel();
@ -52,8 +61,11 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
body: _buildScaffoldStack(viewModel), body: _buildScaffoldStack(viewModel),
); );
Widget _buildScaffoldStack(LoginViewModel viewModel) => Widget _buildScaffoldStack(LoginViewModel viewModel) => Stack(children: [
Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]); _buildScaffold(viewModel),
_buildLoginWithEmailState(viewModel),
_buildLoginWithGoogleState(viewModel)
]);
Widget _buildScaffold(LoginViewModel viewModel) => Column( Widget _buildScaffold(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -94,6 +106,13 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
otpController: otpController, otpController: otpController,
phoneNumberController: phoneNumberController); phoneNumberController: phoneNumberController);
Widget _buildBusyLogin(LoginViewModel viewModel) => Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container(); 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 => Future<void> navigateToRegister() async =>
await _navigationService.navigateToRegisterView(); await _navigationService.navigateToRegisterView();
Future<void> navigateToForgetPassword() async =>
await _navigationService.navigateToForgetPasswordView();
Future<void> replaceWithHome() async => Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView()); await _navigationService.clearStackAndShowView(const HomeView());
// Remote api calls // Remote api calls
// Login with email // Login with email
Future<void> emailLogin() async => await runBusyFuture(_emailLogin()); Future<void> emailLogin() async => await runBusyFuture(_emailLogin(),
busyObject: StateObjects.loginWithEmail);
Future<void> _emailLogin() async { Future<void> _emailLogin() async {
if (await _statusChecker.checkConnection()) { 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 { Future<void> _googleLogin() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {

View File

@ -58,7 +58,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubTitleWrapper(viewModel), _buildSubtitleWrapper(viewModel),
verticalSpaceLarge, verticalSpaceLarge,
_buildEmailFormField(viewModel), _buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
@ -71,19 +71,15 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
verticalSpaceTiny, verticalSpaceTiny,
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
_buildPasswordValidationWrapper(viewModel), _buildPasswordValidationWrapper(viewModel),
_buildForgetPasswordTextButtonWrapper(), _buildForgetPasswordTextButtonWrapper(viewModel),
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Welcome Back', 'Welcome Back',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(), onTap: () async => await viewModel.navigateToRegister(),
); );
@ -104,11 +100,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildEmailValidator(LoginViewModel viewModel) => Text( Widget _buildEmailValidator(LoginViewModel viewModel) => Text(
viewModel.emailValidationMessage!, viewModel.emailValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField( Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField(
@ -135,26 +127,23 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildPasswordValidator(LoginViewModel viewModel) => Text( Widget _buildPasswordValidator(LoginViewModel viewModel) => Text(
viewModel.passwordValidationMessage!, viewModel.passwordValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildForgetPasswordTextButtonWrapper() => Align( Widget _buildForgetPasswordTextButtonWrapper(LoginViewModel viewModel) =>
Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: _buildForgetPasswordTextButton(), child: _buildForgetPasswordTextButton(viewModel),
); );
Widget _buildForgetPasswordTextButton() => TextButton( Widget _buildForgetPasswordTextButton(LoginViewModel viewModel) => TextButton(
onPressed: () {}, onPressed: () async => await viewModel.navigateToForgetPassword(),
child: _buildForgetPasswordText(), child: _buildForgetPasswordText(),
); );
Widget _buildForgetPasswordText() => const Text( Widget _buildForgetPasswordText() => Text(
'Forget Password?', 'Forget Password?',
style: TextStyle(color: kcPrimaryColor), style: style14P400,
); );
Widget _buildLowerColumn(LoginViewModel viewModel) => Column( Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
@ -207,8 +196,8 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
backgroundColor: kcWhite, backgroundColor: kcWhite,
leadingIcon: Icons.phone, leadingIcon: Icons.phone,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.goTo(1),
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
text: 'Login with Phone Number', 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) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubTitleWrapper(viewModel), _buildSubtitleWrapper(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildSubtitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
@ -57,22 +57,18 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
_buildPhoneNumberValidatorWrapper(viewModel), _buildPhoneNumberValidatorWrapper(viewModel),
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Welcome Back', 'Welcome Back',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(), onTap: () async => await viewModel.navigateToRegister(),
); );
Widget _buildSubtitle() => const Text( Widget _buildSubtitle() => Text(
'Enter your phone number. We will send you a confirmation code there', 'Enter your phone number. We will send you a confirmation code there',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row( Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row(

View File

@ -19,7 +19,6 @@ import 'onboarding_viewmodel.dart';
import 'onboarding_view.form.dart'; import 'onboarding_view.form.dart';
@FormView(fields: [ @FormView(fields: [
FormTextField(name: 'answer', validator: FormValidator.validateForm),
FormTextField(name: 'fullName', validator: FormValidator.validateForm), FormTextField(name: 'fullName', validator: FormValidator.validateForm),
FormTextField(name: 'challenge', validator: FormValidator.validateForm), FormTextField(name: 'challenge', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm), FormTextField(name: 'occupation', validator: FormValidator.validateForm),
@ -30,13 +29,55 @@ class OnboardingView extends StackedView<OnboardingViewModel>
with $OnboardingView { with $OnboardingView {
const OnboardingView({Key? key}) : super(key: key); const OnboardingView({Key? key}) : super(key: key);
void _initFormFields() { void _initClearData() {
answerController.text = 'Book'; 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 @override
void onViewModelReady(OnboardingViewModel viewModel) { void onViewModelReady(OnboardingViewModel viewModel) {
_initFormFields(); _initClearData();
syncFormWithViewModel(viewModel); syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -58,7 +99,7 @@ class OnboardingView extends StackedView<OnboardingViewModel>
Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) => Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) =>
PopScope( PopScope(
canPop: viewModel.currentPage == 0 ? true : false, canPop: viewModel.currentPage == 0 ? true : false,
onPopInvokedWithResult: (value, data) => viewModel.pop(), onPopInvokedWithResult: (value, data) => _pop(viewModel),
child: _buildOnboardingScreens(viewModel)); child: _buildOnboardingScreens(viewModel));
Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack( 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 bool _autoTextFieldValidation = true;
const String AnswerValueKey = 'answer';
const String FullNameValueKey = 'fullName'; const String FullNameValueKey = 'fullName';
const String ChallengeValueKey = 'challenge'; const String ChallengeValueKey = 'challenge';
const String OccupationValueKey = 'occupation'; const String OccupationValueKey = 'occupation';
@ -25,7 +24,6 @@ final Map<String, TextEditingController> _OnboardingViewTextEditingControllers =
final Map<String, FocusNode> _OnboardingViewFocusNodes = {}; final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = { final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
AnswerValueKey: FormValidator.validateForm,
FullNameValueKey: FormValidator.validateForm, FullNameValueKey: FormValidator.validateForm,
ChallengeValueKey: FormValidator.validateForm, ChallengeValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm, OccupationValueKey: FormValidator.validateForm,
@ -34,8 +32,6 @@ final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
}; };
mixin $OnboardingView { mixin $OnboardingView {
TextEditingController get answerController =>
_getFormTextEditingController(AnswerValueKey);
TextEditingController get fullNameController => TextEditingController get fullNameController =>
_getFormTextEditingController(FullNameValueKey); _getFormTextEditingController(FullNameValueKey);
TextEditingController get challengeController => TextEditingController get challengeController =>
@ -47,7 +43,6 @@ mixin $OnboardingView {
TextEditingController get topicController => TextEditingController get topicController =>
_getFormTextEditingController(TopicValueKey); _getFormTextEditingController(TopicValueKey);
FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey);
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey); FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey); FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey); FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
@ -79,7 +74,6 @@ mixin $OnboardingView {
/// Registers a listener on every generated controller that calls [model.setData()] /// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values /// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) { void syncFormWithViewModel(FormStateHelper model) {
answerController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model)); fullNameController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model)); challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model)); occupationController.addListener(() => _updateFormData(model));
@ -96,7 +90,6 @@ mixin $OnboardingView {
'This feature was deprecated after 3.1.0.', 'This feature was deprecated after 3.1.0.',
) )
void listenToFormUpdated(FormViewModel model) { void listenToFormUpdated(FormViewModel model) {
answerController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model)); fullNameController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model)); challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model)); occupationController.addListener(() => _updateFormData(model));
@ -111,7 +104,6 @@ mixin $OnboardingView {
model.setData( model.setData(
model.formValueMap model.formValueMap
..addAll({ ..addAll({
AnswerValueKey: answerController.text,
FullNameValueKey: fullNameController.text, FullNameValueKey: fullNameController.text,
ChallengeValueKey: challengeController.text, ChallengeValueKey: challengeController.text,
OccupationValueKey: occupationController.text, OccupationValueKey: occupationController.text,
@ -158,7 +150,6 @@ extension ValueProperties on FormStateHelper {
return !hasAnyValidationMessage; return !hasAnyValidationMessage;
} }
String? get answerValue => this.formValueMap[AnswerValueKey] as String?;
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?; String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?; String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
String? get occupationValue => String? get occupationValue =>
@ -167,16 +158,6 @@ extension ValueProperties on FormStateHelper {
this.formValueMap[LanguageGoalValueKey] as String?; this.formValueMap[LanguageGoalValueKey] as String?;
String? get topicValue => this.formValueMap[TopicValueKey] 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) { set fullNameValue(String? value) {
this.setData( this.setData(
this.formValueMap..addAll({FullNameValueKey: value}), 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 => bool get hasFullName =>
this.formValueMap.containsKey(FullNameValueKey) && this.formValueMap.containsKey(FullNameValueKey) &&
(fullNameValue?.isNotEmpty ?? false); (fullNameValue?.isNotEmpty ?? false);
@ -251,8 +229,6 @@ extension ValueProperties on FormStateHelper {
this.formValueMap.containsKey(TopicValueKey) && this.formValueMap.containsKey(TopicValueKey) &&
(topicValue?.isNotEmpty ?? false); (topicValue?.isNotEmpty ?? false);
bool get hasAnswerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false;
bool get hasFullNameValidationMessage => bool get hasFullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false; this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false;
bool get hasChallengeValidationMessage => bool get hasChallengeValidationMessage =>
@ -264,8 +240,6 @@ extension ValueProperties on FormStateHelper {
bool get hasTopicValidationMessage => bool get hasTopicValidationMessage =>
this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false; this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false;
String? get answerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey];
String? get fullNameValidationMessage => String? get fullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey]; this.fieldsValidationMessages[FullNameValueKey];
String? get challengeValidationMessage => String? get challengeValidationMessage =>
@ -279,8 +253,6 @@ extension ValueProperties on FormStateHelper {
} }
extension Methods on FormStateHelper { extension Methods on FormStateHelper {
setAnswerValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[AnswerValueKey] = validationMessage;
setFullNameValidationMessage(String? validationMessage) => setFullNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FullNameValueKey] = validationMessage; this.fieldsValidationMessages[FullNameValueKey] = validationMessage;
setChallengeValidationMessage(String? validationMessage) => setChallengeValidationMessage(String? validationMessage) =>
@ -294,7 +266,6 @@ extension Methods on FormStateHelper {
/// Clears text input fields on the Form /// Clears text input fields on the Form
void clearForm() { void clearForm() {
answerValue = '';
fullNameValue = ''; fullNameValue = '';
challengeValue = ''; challengeValue = '';
occupationValue = ''; occupationValue = '';
@ -305,7 +276,6 @@ extension Methods on FormStateHelper {
/// Validates text input fields on the Form /// Validates text input fields on the Form
void validateForm() { void validateForm() {
this.setValidationMessages({ this.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey), FullNameValueKey: getValidationMessage(FullNameValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey), ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey), OccupationValueKey: getValidationMessage(OccupationValueKey),
@ -330,7 +300,6 @@ String? getValidationMessage(String key) {
/// Updates the fieldsValidationMessages on the FormViewModel /// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) => void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({ model.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey), FullNameValueKey: getValidationMessage(FullNameValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey), ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey), OccupationValueKey: getValidationMessage(OccupationValueKey),

View File

@ -372,16 +372,83 @@ class OnboardingViewModel extends FormViewModel {
_userData.clear(); _userData.clear();
} }
// Navigation // Form reset
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToAssessment() async => // Reset full name form screen
await _navigationService.navigateToAssessmentView(data: _userData); void resetFullNameFormScreen() {
_focusFullName = false;
rebuildUi();
}
Future<void> replaceWithHome() async => // Reset gender form screen
await _navigationService.clearStackAndShowView(const HomeView()); 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 { void next({int? page}) async {
if (page == null) { if (page == null) {
if (_previousPage != 0) { if (_previousPage != 0) {
@ -396,7 +463,7 @@ class OnboardingViewModel extends FormViewModel {
rebuildUi(); rebuildUi();
} }
void pop() { void goBack() {
if (_currentPage == 0) { if (_currentPage == 0) {
_navigationService.back(); _navigationService.back();
} else { } else {
@ -405,4 +472,14 @@ class OnboardingViewModel extends FormViewModel {
rebuildUi(); 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> { class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
const AgeGroupFormScreen({super.key}); const AgeGroupFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetAgeGroupFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -74,24 +80,20 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Which age range are you in?', 'Which age range are you in?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubTitle() => Text(
'Well personalize your learning experience based on your age.', 'Well personalize your learning experience based on your age.',
style: TextStyle(color: kcMediumGrey), style: style14DG400,
); );
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder( Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -12,6 +12,12 @@ import '../../../widgets/birthday_selector.dart';
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> { class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
const BirthdayFormScreen({super.key}); const BirthdayFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetBirthdayFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -64,30 +70,26 @@ class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildBirthdayFormField(viewModel) _buildBirthdayFormField(viewModel)
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Pick your birthday?', 'Pick your birthday?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your birthday.', 'Well personalize your learning experience based on your birthday.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) => Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>

View File

@ -13,6 +13,12 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
const ChallengeFormScreen({super.key, required this.challengeController}); const ChallengeFormScreen({super.key, required this.challengeController});
void _pop(OnboardingViewModel viewModel) {
challengeController.clear();
viewModel.resetChallengeFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -74,7 +80,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildChallenges(viewModel), _buildChallenges(viewModel),
if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel), if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel),
@ -90,24 +96,20 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'What challenge do you face most with English?', 'What challenge do you face most with English?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Everyone has struggles, lets start fixing yours 😊', 'Everyone has struggles, lets start fixing yours 😊',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder( Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder(
@ -151,11 +153,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text( Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text(
viewModel.challengeValidationMessage!, viewModel.challengeValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( 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> { class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
const CountryRegionFormScreen({super.key}); const CountryRegionFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetCountryRegionFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -71,7 +76,7 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildCountryDropDown(viewModel), _buildCountryDropDown(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
@ -80,24 +85,20 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Where are you from?', 'Where are you from?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Select your country and region from the dropdown', 'Select your country and region from the dropdown',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildCountryDropDown(OnboardingViewModel viewModel) => Widget _buildCountryDropDown(OnboardingViewModel viewModel) =>

View File

@ -11,6 +11,11 @@ class EducationalBackgroundFormScreen
extends ViewModelWidget<OnboardingViewModel> { extends ViewModelWidget<OnboardingViewModel> {
const EducationalBackgroundFormScreen({super.key}); const EducationalBackgroundFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetEducationalBackgroundFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -77,9 +82,9 @@ class EducationalBackgroundFormScreen
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );

View File

@ -72,7 +72,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceLarge, verticalSpaceLarge,
_buildFullNameFormField(viewModel), _buildFullNameFormField(viewModel),
if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName) 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.', 'Well use your name to personalize your learning journey.',
style: TextStyle(color: kcMediumGrey), 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> { class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
const GenderFormScreen({super.key}); const GenderFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetGenderFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -63,30 +68,26 @@ class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildAgeGroups(viewModel) _buildAgeGroups(viewModel)
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Choose your gender?', 'Choose your gender?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your gender.', 'Well personalize your learning experience based on your gender.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder( Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -14,6 +14,12 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
const LanguageGoalFormScreen( const LanguageGoalFormScreen(
{super.key, required this.languageGoalController}); {super.key, required this.languageGoalController});
void _pop(OnboardingViewModel viewModel) {
languageGoalController.clear();
viewModel.resetLanguageGoalFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -75,7 +81,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildReasons(viewModel), _buildReasons(viewModel),
if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel), if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel),
@ -91,26 +97,20 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Whats your main goal for improving your English?', 'Whats your main goal for improving your English?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Your goal helps us tailor your learning journey.', 'Your goal helps us tailor your learning journey.',
style: TextStyle( style: style14MG400,
color: kcMediumGrey,
),
); );
Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder( Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder(
@ -154,11 +154,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text( Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text(
viewModel.languageGoalValidationMessage!, viewModel.languageGoalValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

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

View File

@ -13,6 +13,12 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
const OccupationFormScreen({super.key, required this.occupationController}); const OccupationFormScreen({super.key, required this.occupationController});
void _pop(OnboardingViewModel viewModel) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -71,7 +77,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceLarge, verticalSpaceLarge,
_buildOccupationFormField(viewModel), _buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage && if (viewModel.hasOccupationValidationMessage &&
@ -84,23 +90,19 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Whats your occupation?', 'Whats your occupation?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your occupation.', 'Well personalize your learning experience based on your occupation.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildOccupationFormField(OnboardingViewModel viewModel) => Widget _buildOccupationFormField(OnboardingViewModel viewModel) =>
@ -120,11 +122,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text( Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text(
viewModel.occupationValidationMessage!, viewModel.occupationValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -13,6 +13,12 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
const TopicFormScreen({super.key, required this.topicController}); const TopicFormScreen({super.key, required this.topicController});
void _pop(OnboardingViewModel viewModel) {
topicController.clear();
viewModel.resetTopicFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -45,8 +51,8 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
@ -82,7 +88,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildTopics(viewModel), _buildTopics(viewModel),
if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel), if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel),
@ -97,18 +103,14 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Which topics interest you most?', 'Which topics interest you most?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Your favorite topics help us create fun, relatable lessons.', 'Your favorite topics help us create fun, relatable lessons.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder( Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder(
@ -150,11 +152,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text( Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text(
viewModel.topicValidationMessage!, viewModel.topicValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -164,28 +164,28 @@ class ProfileView extends StackedView<ProfileViewModel> {
Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard(
icon: Icons.download, icon: Icons.download,
title: 'My Downloads', title: 'My Downloads',
subTitle: 'Access offline lessons and saved videos', subtitle: 'Access offline lessons and saved videos',
onTap: () async => await viewModel.navigateToDownloads(), onTap: () async => await viewModel.navigateToDownloads(),
); );
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
title: 'My Progress', title: 'My Progress',
icon: Icons.stacked_bar_chart, icon: Icons.stacked_bar_chart,
subTitle: 'Track your achievements and learning streak', subtitle: 'Track your achievements and learning streak',
onTap: () async => await viewModel.navigateToProgress(), onTap: () async => await viewModel.navigateToProgress(),
); );
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Account & Privacy', title: 'Account & Privacy',
icon: Icons.privacy_tip_outlined, icon: Icons.privacy_tip_outlined,
subTitle: 'Manage setting and app preference', subtitle: 'Manage setting and app preference',
onTap: () async => await viewModel.navigateToAccountPrivacy(), onTap: () async => await viewModel.navigateToAccountPrivacy(),
); );
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Support', title: 'Support',
icon: Icons.headphones, icon: Icons.headphones,
subTitle: 'Get help through phone or Telegram', subtitle: 'Get help through phone or Telegram',
onTap: () async => await viewModel.navigateToSupport(), onTap: () async => await viewModel.navigateToSupport(),
); );

View File

@ -700,5 +700,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
); );
Widget _buildState(ProfileDetailViewModel viewModel) => 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 // Update profile
Future<void> updateProfile() async => await runBusyFuture(_updateProfile()); Future<void> updateProfile() async => await runBusyFuture(_updateProfile(),
busyObject: StateObjects.profileUpdate);
Future<void> _updateProfile() async { Future<void> _updateProfile() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
@ -228,8 +229,8 @@ class ProfileDetailViewModel extends ReactiveViewModel
await _apiService.completeProfile(_userData); await _apiService.completeProfile(_userData);
if (response['status'] == ResponseStatus.success) { if (response['status'] == ResponseStatus.success) {
await _authenticationService.updateUserData(_userData); await _authenticationService.updateUserData(_userData);
showSuccessToast(response['message']);
pop(); pop();
showSuccessToast(response['message']);
} else { } else {
showErrorToast(response['message']); showErrorToast(response['message']);
} }

View File

@ -101,7 +101,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
title: viewModel.progresses[index]['title'], title: viewModel.progresses[index]['title'],
color: viewModel.progresses[index]['color'], color: viewModel.progresses[index]['color'],
status: viewModel.progresses[index]['status'], status: viewModel.progresses[index]['status'],
subTitle: viewModel.progresses[index]['subTitle'], subtitle: viewModel.progresses[index]['subtitle'],
isCompleted: viewModel.progresses[index]['isCompleted'], isCompleted: viewModel.progresses[index]['isCompleted'],
), ),
); );
@ -111,7 +111,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
required String title, required String title,
required String icon, required String icon,
required String status, required String status,
required String subTitle, required String subtitle,
required bool isCompleted, required bool isCompleted,
required ProgressViewModel viewModel}) => required ProgressViewModel viewModel}) =>
CourseLevelCard( CourseLevelCard(
@ -119,7 +119,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
title: title, title: title,
color: color, color: color,
status: status, status: status,
subTitle: subTitle, subtitle: subtitle,
isCompleted: isCompleted, isCompleted: isCompleted,
onTap: viewModel.navigateToOngoingProgress, onTap: viewModel.navigateToOngoingProgress,
); );

View File

@ -15,7 +15,7 @@ class ProgressViewModel extends BaseViewModel {
'isCompleted': true, 'isCompleted': true,
'status': 'Completed', 'status': 'Completed',
'icon': 'assets/icons/b_1.svg', 'icon': 'assets/icons/b_1.svg',
'subTitle': 'Youve mastered everyday English basics!', 'subtitle': 'Youve mastered everyday English basics!',
}, },
{ {
'title': 'Elementary', 'title': 'Elementary',
@ -23,7 +23,7 @@ class ProgressViewModel extends BaseViewModel {
'status': 'In Progress', 'status': 'In Progress',
'color': kcPrimaryColor, 'color': kcPrimaryColor,
'icon': 'assets/icons/b_1.svg', 'icon': 'assets/icons/b_1.svg',
'subTitle': 'Continue improving your conversations and fluency.', 'subtitle': 'Continue improving your conversations and fluency.',
}, },
{ {
'title': 'Beginner', 'title': 'Beginner',
@ -31,7 +31,7 @@ class ProgressViewModel extends BaseViewModel {
'status': 'In Progress', 'status': 'In Progress',
'color': kcPrimaryColor, 'color': kcPrimaryColor,
'icon': 'assets/icons/b_1.svg', '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:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.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/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_email_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_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 { class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
const RegisterView({Key? key}) : super(key: key); 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 @override
void onViewModelReady(RegisterViewModel viewModel) { void onViewModelReady(RegisterViewModel viewModel) {
_initClearData();
syncFormWithViewModel(viewModel); syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -44,10 +83,8 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope( Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope(
canPop: false, canPop: false,
onPopInvokedWithResult: (value, data) { onPopInvokedWithResult: (value, data) =>
if (value) return; _pop(value: value, viewModel: viewModel),
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel)); child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold(
@ -55,8 +92,11 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
body: _buildScaffoldStack(viewModel), body: _buildScaffoldStack(viewModel),
); );
Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack( Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(children: [
children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]); _buildScaffold(viewModel),
_buildRegistrationState(viewModel),
_buildVerityOtpState(viewModel)
]);
Widget _buildScaffold(RegisterViewModel viewModel) => Column( Widget _buildScaffold(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -68,8 +108,8 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar( Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
); );
Widget _buildExpandedBody(RegisterViewModel viewModel) => Widget _buildExpandedBody(RegisterViewModel viewModel) =>
@ -81,7 +121,7 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
); );
Widget _buildBody(RegisterViewModel viewModel) => Widget _buildBody(RegisterViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildRegisterWithEmailScreen(), _buildRegisterWithEmailScreen(),
@ -106,6 +146,13 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
passwordController: passwordController, passwordController: passwordController,
confirmPasswordController: confirmPasswordController); confirmPasswordController: confirmPasswordController);
Widget _buildBusyRegistration(RegisterViewModel viewModel) => Widget _buildRegistrationState(RegisterViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container(); 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>(); final _authenticationService = locator<AuthenticationService>();
// Navigation // Navigation
int _currentIndex = 0; int _currentPage = 0;
int get currentIndex => _currentIndex; int get currentPage => _currentPage;
// Email // Email
bool _focusEmail = false; bool _focusEmail = false;
@ -213,9 +213,35 @@ class RegisterViewModel extends FormViewModel {
_userData.clear(); _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 // In-app navigation
void goTo({required int page, RegistrationType? type}) { void goTo({required int page, RegistrationType? type}) {
_currentIndex = page; _currentPage = page;
if (type != null) { if (type != null) {
_registrationType = type; _registrationType = type;
} }
@ -223,17 +249,17 @@ class RegisterViewModel extends FormViewModel {
} }
void goBack() { void goBack() {
if (_currentIndex == 1) { if (_currentPage == 1) {
_currentIndex = 0; _currentPage = 0;
rebuildUi(); rebuildUi();
} else if (_currentIndex == 2) { } else if (_currentPage == 2) {
_currentIndex = 0; _currentPage = 0;
rebuildUi(); rebuildUi();
} else if (_currentIndex == 3) { } else if (_currentPage == 3) {
if (_registrationType == RegistrationType.phone) { if (_registrationType == RegistrationType.phone) {
_currentIndex = 1; _currentPage = 1;
} else { } else {
_currentIndex = 2; _currentPage = 2;
} }
rebuildUi(); rebuildUi();
@ -258,7 +284,8 @@ class RegisterViewModel extends FormViewModel {
// Remote api calls // Remote api calls
// Register // Register
Future<void> register() async => await runBusyFuture(_register()); Future<void> register() async =>
await runBusyFuture(_register(), busyObject: StateObjects.registration);
Future<void> _register() async { Future<void> _register() async {
if (await _statusChecker.checkConnection()) { 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 { Future<void> _verifyOtp() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
@ -296,7 +324,8 @@ class RegisterViewModel extends FormViewModel {
} }
// Resend otp // Resend otp
Future<void> resendOtp() async => await runBusyFuture(_resendOtp()); Future<void> resendOtp() async =>
await runBusyFuture(_resendOtp(), busyObject: StateObjects.resendOtp);
Future<void> _resendOtp() async { Future<void> _resendOtp() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {

View File

@ -55,7 +55,7 @@ class RegisterWithEmailScreen extends ViewModelWidget<RegisterViewModel> {
List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubTitleWrapper(viewModel), _buildSubtitleWrapper(viewModel),
verticalSpaceLarge, verticalSpaceLarge,
_buildEmailFormField(viewModel), _buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) 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(), onTap: () async => await viewModel.replaceToLogin(),
); );

View File

@ -44,7 +44,7 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget<RegisterViewModel> {
List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(RegisterViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubTitleWrapper(viewModel), _buildSubtitleWrapper(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildSubtitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
@ -57,22 +57,18 @@ class RegisterWithPhoneNumberScreen extends ViewModelWidget<RegisterViewModel> {
_buildPhoneNumberValidatorWrapper(viewModel), _buildPhoneNumberValidatorWrapper(viewModel),
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Create an Account', 'Create an Account',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount( Widget _buildSubtitleWrapper(RegisterViewModel viewModel) => LoginAccount(
onTap: () async => await viewModel.replaceToLogin(), onTap: () async => await viewModel.replaceToLogin(),
); );
Widget _buildSubtitle() => const Text( Widget _buildSubtitle() => Text(
'Enter your phone number. We will send you a confirmation code there', 'Enter your phone number. We will send you a confirmation code there',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildPhoneNumberWrapper(RegisterViewModel viewModel) => Row( Widget _buildPhoneNumberWrapper(RegisterViewModel viewModel) => Row(

View File

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

View File

@ -11,7 +11,7 @@ class CourseLevelCard extends StatelessWidget {
final String icon; final String icon;
final String title; final String title;
final String status; final String status;
final String subTitle; final String subtitle;
final bool isCompleted; final bool isCompleted;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
@ -22,7 +22,7 @@ class CourseLevelCard extends StatelessWidget {
required this.title, required this.title,
required this.color, required this.color,
required this.status, required this.status,
required this.subTitle, required this.subtitle,
required this.isCompleted, required this.isCompleted,
}); });
@ -91,7 +91,7 @@ class CourseLevelCard extends StatelessWidget {
Widget _buildSubTitle() => Expanded( Widget _buildSubTitle() => Expanded(
child: Text( child: Text(
subTitle, subtitle,
maxLines: 3, maxLines: 3,
style: const TextStyle(color: kcMediumGrey), style: const TextStyle(color: kcMediumGrey),
), ),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
class CustomLargeRadioButton extends StatelessWidget { class CustomLargeRadioButton extends StatelessWidget {
final String title; final String title;
@ -49,7 +50,7 @@ class CustomLargeRadioButton extends StatelessWidget {
); );
List<Widget> _buildButtonRowChildren() => List<Widget> _buildButtonRowChildren() =>
[_buildIconSectionWrapper(), _buildTitle(), _buildSubTitle()]; [_buildIconSectionWrapper(), _buildTitle(), _buildSubtitle()];
Widget _buildIconSectionWrapper() => Row( Widget _buildIconSectionWrapper() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -67,14 +68,10 @@ class CustomLargeRadioButton extends StatelessWidget {
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, title,
style: const TextStyle( style: style18DG600,
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w500,
),
); );
Widget _buildSubTitle() => Text( Widget _buildSubtitle() => Text(
subtitle, subtitle,
style: const TextStyle(color: kcMediumGrey), style: const TextStyle(color: kcMediumGrey),
); );

View File

@ -66,7 +66,7 @@ class LearnAppBar extends StatelessWidget {
); );
List<Widget> _buildGreetingChildren() => List<Widget> _buildGreetingChildren() =>
[_buildGreetingTitle(), _buildSubTitle()]; [_buildGreetingTitle(), _buildSubtitle()];
Widget _buildGreetingTitle() => Text.rich( Widget _buildGreetingTitle() => Text.rich(
TextSpan(text: 'Hello,', style: style14DG600, children: [ 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?', 'Ready to keep learning English today?',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: style14DG400, style: style14DG400,

View File

@ -5,7 +5,7 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
class ProfileCard extends StatelessWidget { class ProfileCard extends StatelessWidget {
final String title; final String title;
final IconData icon; final IconData icon;
final String subTitle; final String subtitle;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
const ProfileCard( const ProfileCard(
@ -13,7 +13,7 @@ class ProfileCard extends StatelessWidget {
this.onTap, this.onTap,
required this.icon, required this.icon,
required this.title, required this.title,
required this.subTitle}); required this.subtitle});
@override @override
Widget build(BuildContext context) => _buildContainerWrapper(); Widget build(BuildContext context) => _buildContainerWrapper();
@ -43,7 +43,7 @@ class ProfileCard extends StatelessWidget {
verticalSpaceSmall, verticalSpaceSmall,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle() _buildSubtitle()
]; ];
Widget _buildIcon() => Icon( Widget _buildIcon() => Icon(
@ -54,15 +54,11 @@ class ProfileCard extends StatelessWidget {
Widget _buildTitle() => Text( Widget _buildTitle() => Text(
title, title,
style: const TextStyle( style: style16DG600,
fontSize: 16,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => Text( Widget _buildSubtitle() => Text(
subTitle, subtitle,
style: const TextStyle(color: kcMediumGrey), style: style14MG400,
); );
} }

View File

@ -1016,6 +1016,33 @@ class MockApiService extends _i1.Mock implements _i12.ApiService {
_i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}), _i8.Future<Map<String, dynamic>>.value(<String, dynamic>{}),
) as _i8.Future<Map<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 @override
_i8.Future<Map<String, dynamic>> getProfileStatus(_i11.UserModel? user) => _i8.Future<Map<String, dynamic>> getProfileStatus(_i11.UserModel? user) =>
(super.noSuchMethod( (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());
});
}