fix: Apply UAT fixes
This commit is contained in:
parent
2619210611
commit
2eb7e7f031
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
assessments = decodedData
|
||||
.map(
|
||||
(e) {
|
||||
return Assessment.fromJson(e);
|
||||
},
|
||||
).toList();
|
||||
)
|
||||
.toList()
|
||||
.reversed
|
||||
.toList();
|
||||
return assessments;
|
||||
}
|
||||
return [];
|
||||
|
|
|
|||
8
lib/services/phone_caller_service.dart
Normal file
8
lib/services/phone_caller_service.dart
Normal 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);
|
||||
|
||||
|
||||
}
|
||||
12
lib/services/url_launcher_service.dart
Normal file
12
lib/services/url_launcher_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
const String ksHomeBottomSheetTitle = 'Build Great Apps!';
|
||||
|
||||
const String ksSuggestion =
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? '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,
|
||||
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,8 +111,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
|||
_buildLearnPracticeCompletionScreen()
|
||||
];
|
||||
|
||||
Widget _buildLearnPracticeIntroScreen() =>
|
||||
const LearnPracticeIntroScreen();
|
||||
Widget _buildLearnPracticeIntroScreen() => const LearnPracticeIntroScreen();
|
||||
|
||||
Widget _buildLearnPracticeElementsScreen() =>
|
||||
const LearnPracticeDescriptionScreen();
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -153,8 +153,8 @@ 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) =>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
Future<void> _start(LearnPracticeViewModel viewModel) async =>
|
||||
await viewModel.playVoicePrompt(question);
|
||||
|
||||
|
||||
Future<void> _showSheet(
|
||||
{required BuildContext context,
|
||||
required LearnPracticeViewModel viewModel}) async =>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
onTap: !viewModel.dropdownRegion
|
||||
? regionController.text.isNotEmpty
|
||||
? () => _next(viewModel)
|
||||
: null
|
||||
: () => _next(viewModel),
|
||||
backgroundColor: !viewModel.dropdownRegion
|
||||
? regionController.text.isNotEmpty
|
||||
? kcPrimaryColor
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
: kcPrimaryColor.withOpacity(0.1)
|
||||
: kcPrimaryColor,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -139,11 +124,6 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
|||
text: 'Continue',
|
||||
borderRadius: 12,
|
||||
foregroundColor: kcWhite,
|
||||
onTap: occupationController.text.isNotEmpty
|
||||
? () => _next(viewModel)
|
||||
: null,
|
||||
backgroundColor: occupationController.text.isNotEmpty
|
||||
? kcPrimaryColor
|
||||
: kcPrimaryColor.withOpacity(0.1),
|
||||
);
|
||||
onTap: () => _next(viewModel),
|
||||
backgroundColor: kcPrimaryColor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,8 +560,21 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
style: style16DG600,
|
||||
);
|
||||
|
||||
Widget _buildRegionFormField(ProfileDetailViewModel viewModel) =>
|
||||
TextFormField(
|
||||
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(
|
||||
|
|
@ -567,6 +593,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
|||
style: style12R700,
|
||||
);
|
||||
|
||||
|
||||
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -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,28 +616,17 @@ 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,
|
||||
|
|
|
|||
|
|
@ -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,7 +117,24 @@ 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",
|
||||
|
|
@ -266,21 +293,53 @@ class ProfileDetailViewModel extends ReactiveViewModel
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
72
pubspec.lock
72
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>() {
|
||||
|
|
|
|||
11
test/services/phone_caller_service_test.dart
Normal file
11
test/services/phone_caller_service_test.dart
Normal 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());
|
||||
});
|
||||
}
|
||||
11
test/services/url_launcher_service_test.dart
Normal file
11
test/services/url_launcher_service_test.dart
Normal 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());
|
||||
});
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user