fix: Apply UAT fixes

This commit is contained in:
BisratHailu 2026-05-07 16:30:16 +03:00
parent 2619210611
commit 2eb7e7f031
36 changed files with 664 additions and 419 deletions

View File

@ -53,6 +53,8 @@ import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/services/vimeo_service.dart';
import 'package:yimaru_app/services/url_launcher_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
// @stacked-import
@StackedApp(
@ -114,6 +116,8 @@ import 'package:yimaru_app/services/vimeo_service.dart';
LazySingleton(classType: VoiceRecorderService),
LazySingleton(classType: InAppUpdateService),
LazySingleton(classType: VimeoService),
LazySingleton(classType: UrlLauncherService),
LazySingleton(classType: PhoneCallerService),
// @stacked-service
],
bottomsheets: [

View File

@ -23,9 +23,11 @@ import '../services/image_picker_service.dart';
import '../services/in_app_update_service.dart';
import '../services/notification_service.dart';
import '../services/permission_handler_service.dart';
import '../services/phone_caller_service.dart';
import '../services/secure_storage_service.dart';
import '../services/smart_auth_service.dart';
import '../services/status_checker_service.dart';
import '../services/url_launcher_service.dart';
import '../services/vimeo_service.dart';
import '../services/voice_recorder_service.dart';
@ -57,4 +59,6 @@ Future<void> setupLocator(
locator.registerLazySingleton(() => VoiceRecorderService());
locator.registerLazySingleton(() => InAppUpdateService());
locator.registerLazySingleton(() => VimeoService());
locator.registerLazySingleton(() => UrlLauncherService());
locator.registerLazySingleton(() => PhoneCallerService());
}

View File

@ -366,11 +366,15 @@ class ApiService {
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['question_sets'] as List;
assessments = decodedData.map(
(e) {
return Assessment.fromJson(e);
},
).toList();
assessments = decodedData
.map(
(e) {
return Assessment.fromJson(e);
},
)
.toList()
.reversed
.toList();
return assessments;
}
return [];

View File

@ -0,0 +1,8 @@
import 'package:flutter_phone_direct_caller/flutter_phone_direct_caller.dart';
class PhoneCallerService {
Future<void> call(String phone) async =>
await FlutterPhoneDirectCaller.callNumber(phone);
}

View File

@ -0,0 +1,12 @@
import 'package:url_launcher/url_launcher.dart';
class UrlLauncherService {
Future<void> launchUri(String url) async {
Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
launchUrl(uri);
}
}
}

View File

@ -1,3 +1,4 @@
// Endpoints
String kBaseUrl = 'https://api.yimaruacademy.com';
String kApiUrl = 'api';
@ -83,3 +84,8 @@ String kSampleVideoUrl =
String kServerClientId =
'900714037062-24ria5fcfet71o3vde8f6gsvsj1n68ec.apps.googleusercontent.com';
// Other
String kPhoneSupport = '+251946396655';
String kTelegramSupport = '@yimaruacademy2026';

View File

@ -1,3 +1,4 @@
const String ksHomeBottomSheetTitle = 'Build Great Apps!';
const String ksSuggestion =

View File

@ -99,8 +99,7 @@ class AssessmentViewModel extends BaseViewModel {
count++;
}
}
print('COUNT: $count');
print('ASSESSMENT: ${_currentAssessment?.passingScore}');
if (count >= (_currentAssessment?.passingScore ?? 0)) {
return true;
}
@ -152,9 +151,7 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> nextQuestion() async {
_currentQuestionIndex++;
Map<String, dynamic> response = evaluateAssessment();
print('LEVEL: $response');
print('LENGTH: ${_assessmentQuestions.length}');
print('INDEX: $_currentQuestionIndex');
if (_currentQuestionIndex == _assessmentQuestions.length) {
_currentAssessmentIndex = _currentAssessmentIndex + 1;
if (_currentAssessmentIndex == _assessments.length) {
@ -279,7 +276,6 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) {
_assessments = await _apiService.getAssessments();
_assessments.reversed;
}
}
@ -290,6 +286,8 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> _getAssessmentQuestions(int id) async {
if (await _statusChecker.checkConnection()) {
_assessmentQuestions = await _apiService.getAssessmentQuestions(id);
_assessmentQuestions
.removeWhere((question) => question.questionType != 'MCQ');
}
}
}

View File

@ -54,57 +54,61 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
itemCount: viewModel.assessmentQuestions.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cotext, index) =>
_buildBodyScroller(index: index, viewModel: viewModel),
_buildBodyScroller( viewModel),
);
Widget _buildBodyScroller(
{required int index, required AssessmentViewModel viewModel}) =>
AssessmentViewModel viewModel) =>
SingleChildScrollView(
child: _buildBody(index: index, viewModel: viewModel),
child: _buildBody( viewModel),
);
Widget _buildBody(
{required int index, required AssessmentViewModel viewModel}) =>
AssessmentViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel: viewModel, index: index),
children: _buildBodyChildren( viewModel),
);
List<Widget> _buildBodyChildren(
{required int index, required AssessmentViewModel viewModel}) =>
[
verticalSpaceMedium,
_buildTitle(index: index, viewModel: viewModel),
verticalSpaceMedium,
_buildAnswers(index: index, viewModel: viewModel),
_buildContinueButtonWrapper(viewModel: viewModel, question: index + 1)
];
AssessmentViewModel viewModel) =>[
verticalSpaceMedium,
_buildTitleState( viewModel),
verticalSpaceMedium,
_buildAnswersState( viewModel),
_buildContinueButtonWrapper( viewModel)
];
Widget _buildTitleState( AssessmentViewModel viewModel)=> viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length ?Container(): _buildTitle(viewModel);
Widget _buildTitle(
{required int index, required AssessmentViewModel viewModel}) =>
AssessmentViewModel viewModel) =>
Text(
'Q${index + 1}. ${viewModel.assessmentQuestions[index].questionText} ',
'Q${viewModel.currentQuestionIndex + 1}. ${viewModel.assessmentQuestions[viewModel.currentQuestionIndex].questionText} ',
style: style16DG600,
);
Widget _buildAnswersState( AssessmentViewModel viewModel)=> viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length ?Container(): _buildAnswers(viewModel);
Widget _buildAnswers(
{required int index, required AssessmentViewModel viewModel}) =>
AssessmentViewModel viewModel) =>
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.assessmentQuestions[index].options?.length,
itemCount: viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?.length,
itemBuilder: (context, inner) => _buildAnswer(
onTap: () => viewModel.setSelectedAnswer(
question: index + 1,
option: viewModel.assessmentQuestions[index].options?[inner]),
question: viewModel.currentQuestionIndex + 1,
option: viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?[inner]),
title:
viewModel.assessmentQuestions[index].options?[inner].optionText ??
viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?[inner].optionText ??
'',
selected: viewModel.isSelectedAnswer(
question: index + 1,
question: viewModel.currentQuestionIndex + 1,
answer: viewModel
.assessmentQuestions[index].options?[inner].optionText ??
.assessmentQuestions[viewModel.currentQuestionIndex ].options?[inner].optionText ??
''),
),
);
@ -120,28 +124,29 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
);
Widget _buildContinueButtonWrapper(
{required int question, required AssessmentViewModel viewModel}) =>
AssessmentViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel: viewModel, question: question),
child: _buildContinueButton( viewModel),
);
Widget _buildContinueButton(
{required int question, required AssessmentViewModel viewModel}) =>
AssessmentViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor:
viewModel.selectedAnswers.containsKey(question.toString())
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers.containsKey(question.toString())
? () => viewModel.nextQuestion()
: null,
text: viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length - 1
viewModel.assessmentQuestions.length - 1
? 'Finish Level'
: 'Continue',
backgroundColor:
viewModel.selectedAnswers.containsKey('${viewModel.currentQuestionIndex + 1}')
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers.containsKey('${viewModel.currentQuestionIndex + 1}')
? () => viewModel.nextQuestion()
: null,
);
}

View File

@ -104,15 +104,14 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
List<Widget> _buildScreens(LearnPracticeViewModel viewModel) => [
_buildLearnPracticeIntroScreen(),
_buildLearnPracticeElementsScreen(),
_buildLearnPracticeElementsScreen(),
_buildLearnPracticeQuestionsScreen(),
_buildFinishLearnPracticeScreen(),
_buildLearnPracticeResultScreen(),
_buildLearnPracticeCompletionScreen()
];
Widget _buildLearnPracticeIntroScreen() =>
const LearnPracticeIntroScreen();
Widget _buildLearnPracticeIntroScreen() => const LearnPracticeIntroScreen();
Widget _buildLearnPracticeElementsScreen() =>
const LearnPracticeDescriptionScreen();

View File

@ -12,7 +12,8 @@ class FinishLearnPracticeScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const FinishLearnPracticeScreen({super.key});
Future<void> _reset(LearnPracticeViewModel viewModel)async =>await viewModel.reset();
Future<void> _reset(LearnPracticeViewModel viewModel) async =>
await viewModel.reset();
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
@ -123,6 +124,6 @@ class FinishLearnPracticeScreen
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
onTap: ()async => await _reset(viewModel) ,
onTap: () async => await _reset(viewModel),
);
}

View File

@ -153,9 +153,9 @@ class LearnPracticeDescriptionScreen
Widget _buildImage(LearnPracticeViewModel viewModel) => CachedNetworkImage(
fit: BoxFit.cover,
width: double.maxFinite,
imageUrl: getReadableUrl(viewModel.practices.first.storyImage ?? '') ?? '',
);
imageUrl:
getReadableUrl(viewModel.practices.first.storyImage ?? '') ?? '',
);
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding(

View File

@ -14,7 +14,8 @@ class LearnPracticeResultScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeResultScreen({super.key});
void _navigate(LearnPracticeViewModel viewModel) async=>await viewModel.reset();
void _navigate(LearnPracticeViewModel viewModel) async =>
await viewModel.reset();
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
await viewModel.stopRecording();

View File

@ -22,9 +22,8 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
viewModel.pop();
}
Future<void> _start(LearnPracticeViewModel viewModel)async =>
await viewModel.playVoicePrompt(question);
Future<void> _start(LearnPracticeViewModel viewModel) async =>
await viewModel.playVoicePrompt(question);
Future<void> _showSheet(
{required BuildContext context,
@ -115,7 +114,7 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
GestureDetector(
onTap: () async=>await _start(viewModel),
onTap: () async => await _start(viewModel),
child: _buildStartButton(),
);
@ -142,16 +141,16 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
startAngle: 0.0,
center: Alignment.center,
colors: [
kcPrimaryColor.withValues(alpha:0.3),
kcIndigo.withValues(alpha:0.2),
kcIndigo.withValues(alpha:0.3),
kcIndigo.withValues(alpha:0.4),
kcIndigo.withValues(alpha:0.5),
kcPrimaryColor.withValues(alpha:0.5),
kcPrimaryColor.withValues(alpha:0.4),
kcPrimaryColor.withValues(alpha:0.3),
kcPrimaryColor.withValues(alpha: 0.3),
kcIndigo.withValues(alpha: 0.2),
kcIndigo.withValues(alpha: 0.3),
kcIndigo.withValues(alpha: 0.4),
kcIndigo.withValues(alpha: 0.5),
kcPrimaryColor.withValues(alpha: 0.5),
kcPrimaryColor.withValues(alpha: 0.4),
kcPrimaryColor.withValues(alpha: 0.3),
kcPrimaryColor.withValues(alpha: 0.2),
kcPrimaryColor.withValues(alpha:0.5),
kcPrimaryColor.withValues(alpha: 0.5),
],
// quarterly spread
),

View File

@ -23,7 +23,6 @@ import 'onboarding_view.form.dart';
name: 'fullName', validator: FormValidator.validateFullNameForm),
FormTextField(name: 'region', validator: FormValidator.validateForm),
FormTextField(name: 'challenge', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
FormTextField(name: 'languageGoal', validator: FormValidator.validateForm),
])
class OnboardingView extends StackedView<OnboardingViewModel>
@ -39,7 +38,6 @@ class OnboardingView extends StackedView<OnboardingViewModel>
regionController.clear();
fullNameController.clear();
challengeController.clear();
occupationController.clear();
languageGoalController.clear();
}
@ -54,7 +52,6 @@ class OnboardingView extends StackedView<OnboardingViewModel>
} else if (viewModel.currentPage == 3) {
viewModel.resetEducationalBackgroundFormScreen();
} else if (viewModel.currentPage == 4) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
} else if (viewModel.currentPage == 5) {
viewModel.resetCountryRegionFormScreen();
@ -135,8 +132,7 @@ class OnboardingView extends StackedView<OnboardingViewModel>
Widget _buildEducationalBackgroundForm() =>
const EducationalBackgroundFormScreen();
Widget _buildOccupationForm() =>
OccupationFormScreen(occupationController: occupationController);
Widget _buildOccupationForm() => const OccupationFormScreen();
Widget _buildCountryRegionForm() => CountryRegionFormScreen(
regionController: regionController,

View File

@ -17,7 +17,6 @@ const String TopicValueKey = 'topic';
const String FullNameValueKey = 'fullName';
const String RegionValueKey = 'region';
const String ChallengeValueKey = 'challenge';
const String OccupationValueKey = 'occupation';
const String LanguageGoalValueKey = 'languageGoal';
final Map<String, TextEditingController> _OnboardingViewTextEditingControllers =
@ -30,7 +29,6 @@ final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
FullNameValueKey: FormValidator.validateFullNameForm,
RegionValueKey: FormValidator.validateForm,
ChallengeValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm,
LanguageGoalValueKey: FormValidator.validateForm,
};
@ -43,8 +41,6 @@ mixin $OnboardingView {
_getFormTextEditingController(RegionValueKey);
TextEditingController get challengeController =>
_getFormTextEditingController(ChallengeValueKey);
TextEditingController get occupationController =>
_getFormTextEditingController(OccupationValueKey);
TextEditingController get languageGoalController =>
_getFormTextEditingController(LanguageGoalValueKey);
@ -52,7 +48,6 @@ mixin $OnboardingView {
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
FocusNode get regionFocusNode => _getFormFocusNode(RegionValueKey);
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
FocusNode get languageGoalFocusNode =>
_getFormFocusNode(LanguageGoalValueKey);
@ -84,7 +79,6 @@ mixin $OnboardingView {
fullNameController.addListener(() => _updateFormData(model));
regionController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
languageGoalController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
@ -101,7 +95,6 @@ mixin $OnboardingView {
fullNameController.addListener(() => _updateFormData(model));
regionController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
languageGoalController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
@ -116,7 +109,6 @@ mixin $OnboardingView {
FullNameValueKey: fullNameController.text,
RegionValueKey: regionController.text,
ChallengeValueKey: challengeController.text,
OccupationValueKey: occupationController.text,
LanguageGoalValueKey: languageGoalController.text,
}),
);
@ -163,8 +155,6 @@ extension ValueProperties on FormStateHelper {
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
String? get regionValue => this.formValueMap[RegionValueKey] as String?;
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
String? get occupationValue =>
this.formValueMap[OccupationValueKey] as String?;
String? get languageGoalValue =>
this.formValueMap[LanguageGoalValueKey] as String?;
@ -210,17 +200,6 @@ extension ValueProperties on FormStateHelper {
}
}
set occupationValue(String? value) {
this.setData(
this.formValueMap..addAll({OccupationValueKey: value}),
);
if (_OnboardingViewTextEditingControllers.containsKey(OccupationValueKey)) {
_OnboardingViewTextEditingControllers[OccupationValueKey]?.text =
value ?? '';
}
}
set languageGoalValue(String? value) {
this.setData(
this.formValueMap..addAll({LanguageGoalValueKey: value}),
@ -245,9 +224,6 @@ extension ValueProperties on FormStateHelper {
bool get hasChallenge =>
this.formValueMap.containsKey(ChallengeValueKey) &&
(challengeValue?.isNotEmpty ?? false);
bool get hasOccupation =>
this.formValueMap.containsKey(OccupationValueKey) &&
(occupationValue?.isNotEmpty ?? false);
bool get hasLanguageGoal =>
this.formValueMap.containsKey(LanguageGoalValueKey) &&
(languageGoalValue?.isNotEmpty ?? false);
@ -260,8 +236,6 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[RegionValueKey]?.isNotEmpty ?? false;
bool get hasChallengeValidationMessage =>
this.fieldsValidationMessages[ChallengeValueKey]?.isNotEmpty ?? false;
bool get hasOccupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey]?.isNotEmpty ?? false;
bool get hasLanguageGoalValidationMessage =>
this.fieldsValidationMessages[LanguageGoalValueKey]?.isNotEmpty ?? false;
@ -273,8 +247,6 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[RegionValueKey];
String? get challengeValidationMessage =>
this.fieldsValidationMessages[ChallengeValueKey];
String? get occupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey];
String? get languageGoalValidationMessage =>
this.fieldsValidationMessages[LanguageGoalValueKey];
}
@ -288,8 +260,6 @@ extension Methods on FormStateHelper {
this.fieldsValidationMessages[RegionValueKey] = validationMessage;
void setChallengeValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ChallengeValueKey] = validationMessage;
void setOccupationValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[OccupationValueKey] = validationMessage;
void setLanguageGoalValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[LanguageGoalValueKey] = validationMessage;
@ -299,7 +269,6 @@ extension Methods on FormStateHelper {
fullNameValue = '';
regionValue = '';
challengeValue = '';
occupationValue = '';
languageGoalValue = '';
}
@ -310,7 +279,6 @@ extension Methods on FormStateHelper {
FullNameValueKey: getValidationMessage(FullNameValueKey),
RegionValueKey: getValidationMessage(RegionValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
LanguageGoalValueKey: getValidationMessage(LanguageGoalValueKey),
});
}
@ -335,6 +303,5 @@ void updateValidationData(FormStateHelper model) =>
FullNameValueKey: getValidationMessage(FullNameValueKey),
RegionValueKey: getValidationMessage(RegionValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
LanguageGoalValueKey: getValidationMessage(LanguageGoalValueKey),
});

View File

@ -91,9 +91,9 @@ class OnboardingViewModel extends ReactiveViewModel
Map<String, dynamic>? get selectedAgeGroup => _selectedAgeGroup;
// Occupation
bool _focusOccupation = false;
String _selectedOccupation = 'Students (High school & University)';
bool get focusOccupation => _focusOccupation;
String get selectedOccupation => _selectedOccupation;
// Country
String _selectedCountry = 'Ethiopia';
@ -105,6 +105,14 @@ class OnboardingViewModel extends ReactiveViewModel
bool get focusRegion => _focusRegion;
bool _dropdownRegion = true;
bool get dropdownRegion => _dropdownRegion;
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
// Learning goal
String? _selectedLearningGoal;
@ -253,8 +261,18 @@ class OnboardingViewModel extends ReactiveViewModel
_selectedAgeGroup == value;
// Occupation
void setOccupationFocus() {
_focusOccupation = true;
List<String> getOccupations() => [
'Students (High school & University)',
'Job Seekers / Fresh Graduates',
'Working Professionals (Corporate/Office)',
'Government & NGO Workers',
'Entrepreneurs & Small Business Owners',
'Hospitality & Tourism Workers',
'Freelancers / Remote Workers (Digital Economy)'
];
void setSelectedOccupation(String value) {
_selectedOccupation = value;
rebuildUi();
}
@ -415,15 +433,50 @@ class OnboardingViewModel extends ReactiveViewModel
void setSelectedCountry(String value) {
_selectedCountry = value;
if (value == 'Ethiopia') {
_dropdownRegion = true;
_selectedRegion = 'Addis Ababa';
} else {
_dropdownRegion = false;
}
rebuildUi();
}
// Region
List<String> getRegions() => [
'Addis Ababa',
'Afar',
'Amhara',
'Benishangul-Gumuz',
'Central Ethiopia',
'Dire Dawa',
'Gambela',
'Harari',
'Oromia',
'Sidama',
'Somali',
'South Ethiopia',
'South West Ethiopia Peoples',
'Tigray',
];
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
}
void setRegionFocus() {
_focusRegion = true;
rebuildUi();
}
void unsetRegionFocus() {
_focusRegion = false;
rebuildUi();
}
// Learning goal
void setSelectedLearningGoal(String value) {
_selectedLearningGoal = value;
@ -541,7 +594,7 @@ class OnboardingViewModel extends ReactiveViewModel
// Reset occupation form screen
void resetOccupationFormScreen() {
_focusOccupation = false;
_selectedOccupation = 'Students (High school & University)';
rebuildUi();
}
@ -549,6 +602,7 @@ class OnboardingViewModel extends ReactiveViewModel
void resetCountryRegionFormScreen() {
_focusRegion = false;
_selectedCountry = 'Ethiopia';
_selectedRegion = 'Addis Ababa';
rebuildUi();
}

View File

@ -12,6 +12,16 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController regionController;
const CountryRegionFormScreen({super.key, required this.regionController});
void _setSelectedCountry(
{String? value, required OnboardingViewModel viewModel}) {
viewModel.setSelectedCountry(value ?? 'Ethiopia');
if (viewModel.selectedCountry != 'Ethiopia') {
regionController.clear();
viewModel.unsetRegionFocus();
}
}
void _pop(OnboardingViewModel viewModel) {
viewModel.resetCountryRegionFormScreen();
viewModel.goBack();
@ -21,7 +31,9 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'region': regionController.text,
'region': viewModel.dropdownRegion
? viewModel.selectedRegion
: regionController.text,
'country': viewModel.selectedCountry,
};
viewModel.addUserData(data);
@ -85,10 +97,14 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium,
_buildCountryDropDown(viewModel),
verticalSpaceMedium,
_buildRegionFormField(viewModel),
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
_buildRegionFormState(viewModel),
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion)
verticalSpaceTiny,
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion)
_buildRegionValidatorWrapper(viewModel)
];
@ -116,7 +132,22 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
selectedItem: viewModel.selectedCountry,
items: (value, props) => viewModel.getCountries(),
onChanged: (value) =>
viewModel.setSelectedCountry(value ?? 'Ethiopia'));
_setSelectedCountry(value: value, viewModel: viewModel));
Widget _buildRegionFormState(OnboardingViewModel viewModel) =>
viewModel.dropdownRegion
? _buildRegionDropDown(viewModel)
: _buildRegionFormField(viewModel);
Widget _buildRegionDropDown(OnboardingViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select region',
icon: _buildSearchIcon(),
selectedItem: viewModel.selectedRegion,
items: (value, props) => viewModel.getRegions(),
onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'));
Widget _buildRegionFormField(OnboardingViewModel viewModel) => TextFormField(
controller: regionController,
onTap: viewModel.setRegionFocus,
@ -152,9 +183,15 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: regionController.text.isNotEmpty ? () => _next(viewModel) : null,
backgroundColor: regionController.text.isNotEmpty
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
onTap: !viewModel.dropdownRegion
? regionController.text.isNotEmpty
? () => _next(viewModel)
: null
: () => _next(viewModel),
backgroundColor: !viewModel.dropdownRegion
? regionController.text.isNotEmpty
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1)
: kcPrimaryColor,
);
}

View File

@ -6,15 +6,13 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../../widgets/custom_dropdown.dart';
import '../onboarding_view.form.dart';
class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
final TextEditingController occupationController;
const OccupationFormScreen({super.key, required this.occupationController});
const OccupationFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
viewModel.goBack();
}
@ -22,7 +20,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {'occupation': occupationController.text};
Map<String, dynamic> data = {'occupation': viewModel.selectedOccupation};
viewModel.addUserData(data);
viewModel.next();
@ -82,13 +80,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceSmall,
_buildSubtitle(),
verticalSpaceLarge,
_buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
verticalSpaceTiny,
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
_buildOccupationValidatorWrapper(viewModel)
_buildOccupationDropdown(viewModel),
];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
@ -108,24 +100,17 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
style: style14MG400,
);
Widget _buildOccupationFormField(OnboardingViewModel viewModel) =>
TextFormField(
controller: occupationController,
onTap: viewModel.setOccupationFocus,
decoration: inputDecoration(
hint: 'Enter Your Occupation',
focus: viewModel.focusOccupation,
filled: occupationController.text.isNotEmpty),
);
Widget _buildOccupationValidatorWrapper(OnboardingViewModel viewModel) =>
viewModel.hasOccupationValidationMessage
? _buildOccupationValidator(viewModel)
: Container();
Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text(
viewModel.occupationValidationMessage!,
style: style12R700,
Widget _buildOccupationDropdown(OnboardingViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select occupation',
icon: _buildSearchIcon(),
selectedItem: viewModel.selectedOccupation,
items: (value, props) => viewModel.getOccupations(),
onChanged: (value) => viewModel.setSelectedOccupation(
value ?? 'Students (High school & University)'));
Icon _buildSearchIcon() => const Icon(
Icons.search,
color: kcPrimaryColor,
);
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(
@ -135,15 +120,10 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildContinueButton(OnboardingViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: occupationController.text.isNotEmpty
? () => _next(viewModel)
: null,
backgroundColor: occupationController.text.isNotEmpty
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: () => _next(viewModel),
backgroundColor: kcPrimaryColor);
}

View File

@ -2,6 +2,8 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
@ -10,6 +12,7 @@ import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/status_checker_service.dart';
import '../../../services/url_launcher_service.dart';
import '../../common/app_colors.dart';
class ProfileViewModel extends ReactiveViewModel {
@ -23,6 +26,10 @@ class ProfileViewModel extends ReactiveViewModel {
final _googleAuthService = locator<GoogleAuthService>();
final _phoneCallerService = locator<PhoneCallerService>();
final _urlLauncherService = locator<UrlLauncherService>();
final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>();
@ -61,6 +68,13 @@ class ProfileViewModel extends ReactiveViewModel {
}
}
// Launch telegram
Future<void> launchTelegram() =>
_urlLauncherService.launchUri(kTelegramSupport);
// Call support
Future<void> callSupport() => _phoneCallerService.call(kPhoneSupport);
// Dialog
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(

View File

@ -26,7 +26,6 @@ import 'profile_detail_view.form.dart';
name: 'phoneNumber', validator: FormValidator.validatePhoneNumberForm),
FormTextField(name: 'lastName', validator: FormValidator.validateForm),
FormTextField(name: 'firstName', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
])
class ProfileDetailView extends StackedView<ProfileDetailViewModel>
with $ProfileDetailView {
@ -34,7 +33,9 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Future<void> _update(ProfileDetailViewModel viewModel) async {
Map<String, dynamic> data = {
'region': regionController.text,
'region': viewModel.dropdownRegion
? viewModel.selectedRegion
: regionController.text,
'gender': viewModel.selectedGender,
'last_name': lastNameController.text,
'country': viewModel.selectedCountry,
@ -65,13 +66,23 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
content: _buildImagePicker(context: context, viewModel: viewModel),
);
void _checkRegion(ProfileDetailViewModel viewModel){
if(viewModel.checkRegion(viewModel.user?.region ?? '')){
viewModel.setSelectedRegion(viewModel.user?.region ?? 'Addis Ababa');
}else{
regionController.text = viewModel.user?.region ?? '';
}
}
void _onModelReady(ProfileDetailViewModel viewModel) {
phoneNumberController.text = '251900000000';
emailController.text = viewModel.user?.email ?? '';
regionController.text = viewModel.user?.region ?? '';
lastNameController.text = viewModel.user?.lastName ?? '';
firstNameController.text = viewModel.user?.firstName ?? '';
occupationController.text = viewModel.user?.occupation ?? '';
_checkRegion(viewModel);
viewModel.clearUserData();
viewModel.setGender(viewModel.user?.gender ?? '');
viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia');
@ -535,11 +546,13 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
[
_buildRegionFormFieldLabel(),
verticalSpaceSmall,
_buildRegionFormField(viewModel),
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
verticalSpaceTiny,
if (viewModel.hasRegionValidationMessage && viewModel.focusRegion)
_buildRegionValidatorWrapper(viewModel),
_buildRegionFormState(viewModel),
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion) verticalSpaceTiny,
if (viewModel.hasRegionValidationMessage &&
!viewModel.dropdownRegion &&
viewModel.focusRegion) _buildRegionValidatorWrapper(viewModel),
];
Widget _buildRegionFormFieldLabel() => CustomFormLabel(
@ -547,15 +560,28 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
style: style16DG600,
);
Widget _buildRegionFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
controller: regionController,
onTap: viewModel.setRegionFocus,
decoration: inputDecoration(
hint: 'Enter Your City',
focus: viewModel.focusRegion,
filled: regionController.text.isNotEmpty),
);
Widget _buildRegionFormState(ProfileDetailViewModel viewModel) =>
viewModel.dropdownRegion
? _buildRegionDropDown(viewModel)
: _buildRegionFormField(viewModel);
Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select region',
icon: _buildSearchIcon(),
selectedItem: viewModel.selectedRegion,
items: (value, props) => viewModel.getRegions(),
onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'));
Widget _buildRegionFormField(ProfileDetailViewModel viewModel) => TextFormField(
controller: regionController,
onTap: viewModel.setRegionFocus,
decoration: inputDecoration(
hint: 'Enter Your City',
focus: viewModel.focusRegion,
filled: regionController.text.isNotEmpty),
);
Widget _buildRegionValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasRegionValidationMessage
@ -563,9 +589,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
: Container();
Widget _buildRegionValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.regionValidationMessage!,
style: style12R700,
);
viewModel.regionValidationMessage!,
style: style12R700,
);
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
Column(
@ -580,13 +607,8 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
[
_buildOccupationDropdownLabel(),
verticalSpaceSmall,
_buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
verticalSpaceTiny,
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
_buildOccupationValidatorWrapper(viewModel)
_buildOccupationDropdown(viewModel)
];
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
@ -594,29 +616,18 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
style: style16DG600,
);
Widget _buildOccupationFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
controller: occupationController,
onTap: viewModel.setOccupationFocus,
decoration: inputDecoration(
hint: 'Enter Your Occupation',
focus: viewModel.focusOccupation,
filled: occupationController.text.isNotEmpty),
);
Widget _buildOccupationValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasOccupationValidationMessage
? _buildOccupationValidator(viewModel)
: Container();
Widget _buildOccupationValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.occupationValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
hint: 'Select occupation',
icon: _buildSearchIcon(),
selectedItem: viewModel.selectedOccupation,
items: (value, props) => viewModel.getOccupations(),
onChanged: (value) => viewModel.setSelectedOccupation(
value ?? 'Students (High school & University)'));
Icon _buildSearchIcon() => const Icon(
Icons.search,
color: kcPrimaryColor,
);
Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel),

View File

@ -57,21 +57,31 @@ class ProfileDetailViewModel extends ReactiveViewModel
bool get focusEmail => _focusEmail;
// Occupation
String _selectedOccupation = 'Students (High school & University)';
String get selectedOccupation => _selectedOccupation;
// Country
String _selectedCountry = 'Ethiopia';
String get selectedCountry => _selectedCountry;
// Occupation
bool _focusOccupation = false;
bool get focusOccupation => _focusOccupation;
// Region
bool _focusRegion = false;
bool get focusRegion => _focusRegion;
bool _dropdownRegion = true;
bool get dropdownRegion => _dropdownRegion;
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
// User data
final Map<String, dynamic> _userData = {};
@ -107,180 +117,229 @@ class ProfileDetailViewModel extends ReactiveViewModel
rebuildUi();
}
// Country
// Occupation
List<String> getOccupations() => [
'Students (High school & University)',
'Job Seekers / Fresh Graduates',
'Working Professionals (Corporate/Office)',
'Government & NGO Workers',
'Entrepreneurs & Small Business Owners',
'Hospitality & Tourism Workers',
'Freelancers / Remote Workers (Digital Economy)'
];
void setSelectedOccupation(String value) {
_selectedOccupation = value;
rebuildUi();
}
// Country
List<String> getCountries() => [
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahrain",
"Bangladesh",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia and Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominican Republic",
"Ecuador",
"Egypt",
"El Salvador",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Guatemala",
"Guinea",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Liberia",
"Libya",
"Lithuania",
"Luxembourg",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Mexico",
"Moldova",
"Monaco",
"Mongolia",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"North Korea",
"Norway",
"Oman",
"Pakistan",
"Panama",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russia",
"Rwanda",
"Saudi Arabia",
"Senegal",
"Serbia",
"Singapore",
"Slovakia",
"Slovenia",
"Somalia",
"South Africa",
"South Korea",
"Spain",
"Sri Lanka",
"Sudan",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Tunisia",
"Turkey",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe"
];
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahrain",
"Bangladesh",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia and Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominican Republic",
"Ecuador",
"Egypt",
"El Salvador",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Guatemala",
"Guinea",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Liberia",
"Libya",
"Lithuania",
"Luxembourg",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Mexico",
"Moldova",
"Monaco",
"Mongolia",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"North Korea",
"Norway",
"Oman",
"Pakistan",
"Panama",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russia",
"Rwanda",
"Saudi Arabia",
"Senegal",
"Serbia",
"Singapore",
"Slovakia",
"Slovenia",
"Somalia",
"South Africa",
"South Korea",
"Spain",
"Sri Lanka",
"Sudan",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Tunisia",
"Turkey",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe"
];
void setSelectedCountry(String value) {
_selectedCountry = value;
rebuildUi();
}
if (value == 'Ethiopia') {
_dropdownRegion = true;
_selectedRegion = 'Addis Ababa';
} else {
_dropdownRegion = false;
}
// Occupation
void setOccupationFocus() {
_focusOccupation = true;
rebuildUi();
}
// Region
List<String> getRegions() => [
'Addis Ababa',
'Afar',
'Amhara',
'Benishangul-Gumuz',
'Central Ethiopia',
'Dire Dawa',
'Gambela',
'Harari',
'Oromia',
'Sidama',
'Somali',
'South Ethiopia',
'South West Ethiopia Peoples',
'Tigray',
];
bool checkRegion(String value){
return getRegions().contains(value);
}
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
}
void setRegionFocus() {
_focusRegion = true;
rebuildUi();
}
void unsetRegionFocus() {
_focusRegion = false;
rebuildUi();
}
// User data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);

View File

@ -49,14 +49,6 @@ class RegisterViewModel extends ReactiveViewModel
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;
@ -138,17 +130,6 @@ class RegisterViewModel extends ReactiveViewModel
_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;
@ -160,13 +141,9 @@ class RegisterViewModel extends ReactiveViewModel
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
return completed / 2; // returns 0.0 1.0
}
void setObscurePassword() {
@ -238,8 +215,6 @@ class RegisterViewModel extends ReactiveViewModel
void resetCreatePasswordScreen() {
_agree = false;
_length = false;
_number = false;
_specialChar = false;
_passwordMatch = false;
_focusPassword = false;
_focusConfirmPassword = false;

View File

@ -114,8 +114,6 @@ class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
_buildLinearProgressIndicator(viewModel),
verticalSpaceSmall,
_buildCharLengthValidator(viewModel),
_buildNumberValidator(viewModel),
_buildSymbolValidator(viewModel),
_buildPasswordMatchValidator(viewModel),
_buildCheckBox(viewModel),
verticalSpaceSmall,
@ -215,15 +213,6 @@ class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey,
label: '8 characters minimum');
Widget _buildNumberValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey,
label: 'a number');
Widget _buildSymbolValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey,
label: 'one symbol minimum');
Widget _buildPasswordMatchValidator(RegisterViewModel viewModel) =>
ValidatorListTile(
@ -265,20 +254,14 @@ class CreatePasswordScreen extends ViewModelWidget<RegisterViewModel> {
foregroundColor: kcWhite,
onTap: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch &&
viewModel.agree
? () async => await _signUp(viewModel)
: null,
backgroundColor: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch &&
viewModel.agree
? kcPrimaryColor

View File

@ -10,13 +10,11 @@ class WelcomeViewModel extends BaseViewModel {
// Dependency Injection
final _navigationService = locator<NavigationService>();
final _statusChecker = locator<StatusCheckerService>();
final _authenticationService = locator<AuthenticationService>();
// Navigation
Future<void> navigateToLogin() async =>
await _navigationService.navigateToLoginView();
await _navigationService.replaceWithLoginView();
// Remote api call

View File

@ -55,7 +55,7 @@ class LearningProgressCard extends StatelessWidget {
color: kcPrimaryColor,
);
Widget _buildTitle() => Text(
Widget _buildTitle() => Text(
'Learn English',
style: style16DG600,
);

View File

@ -11,6 +11,7 @@
#include <flutter_inappwebview_linux/flutter_inappwebview_linux_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
@ -28,4 +29,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_inappwebview_linux
flutter_secure_storage_linux
record_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -18,6 +18,7 @@ import google_sign_in_ios
import package_info_plus
import record_macos
import sqflite_darwin
import url_launcher_macos
import video_player_avfoundation
import wakelock_plus
@ -35,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}

View File

@ -662,6 +662,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.7"
flutter_phone_direct_caller:
dependency: "direct main"
description:
name: flutter_phone_direct_caller
sha256: "8f166b12391572ce5872feeac3f4b47f455ee314ef221d98ba160c296ae2fad3"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -1693,6 +1701,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
url: "https://pub.dev"
source: hosted
version: "6.3.29"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
description:

View File

@ -26,6 +26,7 @@ dependencies:
flutter_html: ^3.0.0
email_validator: any
audioplayers: ^6.6.0
url_launcher: ^6.3.2
video_player: ^2.10.1
firebase_core: ^4.4.0
in_app_update: ^4.2.5
@ -49,6 +50,7 @@ dependencies:
flutter_timer_countdown: ^1.0.7
flutter_carousel_widget: ^3.1.0
flutter_inappwebview: ^6.2.0-beta.3
flutter_phone_direct_caller: ^2.2.1
flutter_local_notifications: ^20.1.0
internet_connection_checker_plus: ^2.9.1+2

View File

@ -18,6 +18,8 @@ import 'package:yimaru_app/services/audio_player_service.dart';
import 'package:yimaru_app/services/voice_recorder_service.dart';
import 'package:yimaru_app/services/in_app_update_service.dart';
import 'package:yimaru_app/services/vimeo_service.dart';
import 'package:yimaru_app/services/url_launcher_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
// @stacked-import
import 'test_helpers.mocks.dart';
@ -47,6 +49,9 @@ import 'test_helpers.mocks.dart';
MockSpec<InAppUpdateService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<VimeoService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<VimeoService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<UrlLauncherService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<PhoneCallerService>(onMissingStub: OnMissingStub.returnDefault),
// @stacked-mock-spec
],
)
@ -71,6 +76,9 @@ void registerServices() {
getAndRegisterInAppUpdateService();
getAndRegisterVimeoService();
getAndRegisterVimeoService();
getAndRegisterUrlLauncherService();
getAndRegisterUrlLauncherService();
getAndRegisterPhoneCallerService();
// @stacked-mock-register
}
@ -239,6 +247,20 @@ MockVimeoService getAndRegisterVimeoService() {
locator.registerSingleton<VimeoService>(service);
return service;
}
MockUrlLauncherService getAndRegisterUrlLauncherService() {
_removeRegistrationIfExists<UrlLauncherService>();
final service = MockUrlLauncherService();
locator.registerSingleton<UrlLauncherService>(service);
return service;
}
MockPhoneCallerService getAndRegisterPhoneCallerService() {
_removeRegistrationIfExists<PhoneCallerService>();
final service = MockPhoneCallerService();
locator.registerSingleton<PhoneCallerService>(service);
return service;
}
// @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() {

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('PhoneCallerServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

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('UrlLauncherServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -15,6 +15,7 @@
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
@ -35,4 +36,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
permission_handler_windows
record_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST