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/learn_course/learn_course_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_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/vimeo_service.dart';
import 'package:yimaru_app/services/url_launcher_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -114,6 +116,8 @@ import 'package:yimaru_app/services/vimeo_service.dart';
LazySingleton(classType: VoiceRecorderService), LazySingleton(classType: VoiceRecorderService),
LazySingleton(classType: InAppUpdateService), LazySingleton(classType: InAppUpdateService),
LazySingleton(classType: VimeoService), LazySingleton(classType: VimeoService),
LazySingleton(classType: UrlLauncherService),
LazySingleton(classType: PhoneCallerService),
// @stacked-service // @stacked-service
], ],
bottomsheets: [ bottomsheets: [

View File

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

View File

@ -366,11 +366,15 @@ class ApiService {
if (response.statusCode == 200) { if (response.statusCode == 200) {
var data = response.data; var data = response.data;
var decodedData = data['data']['question_sets'] as List; var decodedData = data['data']['question_sets'] as List;
assessments = decodedData.map( assessments = decodedData
(e) { .map(
return Assessment.fromJson(e); (e) {
}, return Assessment.fromJson(e);
).toList(); },
)
.toList()
.reversed
.toList();
return assessments; return assessments;
} }
return []; 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 kBaseUrl = 'https://api.yimaruacademy.com';
String kApiUrl = 'api'; String kApiUrl = 'api';
@ -83,3 +84,8 @@ String kSampleVideoUrl =
String kServerClientId = String kServerClientId =
'900714037062-24ria5fcfet71o3vde8f6gsvsj1n68ec.apps.googleusercontent.com'; '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 ksHomeBottomSheetTitle = 'Build Great Apps!';
const String ksSuggestion = const String ksSuggestion =

View File

@ -99,8 +99,7 @@ class AssessmentViewModel extends BaseViewModel {
count++; count++;
} }
} }
print('COUNT: $count');
print('ASSESSMENT: ${_currentAssessment?.passingScore}');
if (count >= (_currentAssessment?.passingScore ?? 0)) { if (count >= (_currentAssessment?.passingScore ?? 0)) {
return true; return true;
} }
@ -152,9 +151,7 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> nextQuestion() async { Future<void> nextQuestion() async {
_currentQuestionIndex++; _currentQuestionIndex++;
Map<String, dynamic> response = evaluateAssessment(); Map<String, dynamic> response = evaluateAssessment();
print('LEVEL: $response');
print('LENGTH: ${_assessmentQuestions.length}');
print('INDEX: $_currentQuestionIndex');
if (_currentQuestionIndex == _assessmentQuestions.length) { if (_currentQuestionIndex == _assessmentQuestions.length) {
_currentAssessmentIndex = _currentAssessmentIndex + 1; _currentAssessmentIndex = _currentAssessmentIndex + 1;
if (_currentAssessmentIndex == _assessments.length) { if (_currentAssessmentIndex == _assessments.length) {
@ -279,7 +276,6 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> _getAssessments() async { Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
_assessments = await _apiService.getAssessments(); _assessments = await _apiService.getAssessments();
_assessments.reversed;
} }
} }
@ -290,6 +286,8 @@ class AssessmentViewModel extends BaseViewModel {
Future<void> _getAssessmentQuestions(int id) async { Future<void> _getAssessmentQuestions(int id) async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
_assessmentQuestions = await _apiService.getAssessmentQuestions(id); _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, itemCount: viewModel.assessmentQuestions.length,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cotext, index) => itemBuilder: (cotext, index) =>
_buildBodyScroller(index: index, viewModel: viewModel), _buildBodyScroller( viewModel),
); );
Widget _buildBodyScroller( Widget _buildBodyScroller(
{required int index, required AssessmentViewModel viewModel}) => AssessmentViewModel viewModel) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildBody(index: index, viewModel: viewModel), child: _buildBody( viewModel),
); );
Widget _buildBody( Widget _buildBody(
{required int index, required AssessmentViewModel viewModel}) => AssessmentViewModel viewModel) =>
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel: viewModel, index: index), children: _buildBodyChildren( viewModel),
); );
List<Widget> _buildBodyChildren( List<Widget> _buildBodyChildren(
{required int index, required AssessmentViewModel viewModel}) => AssessmentViewModel viewModel) =>[
[
verticalSpaceMedium,
_buildTitle(index: index, viewModel: viewModel),
verticalSpaceMedium,
_buildAnswers(index: index, viewModel: viewModel),
_buildContinueButtonWrapper(viewModel: viewModel, question: index + 1)
];
verticalSpaceMedium,
_buildTitleState( viewModel),
verticalSpaceMedium,
_buildAnswersState( viewModel),
_buildContinueButtonWrapper( viewModel)
];
Widget _buildTitleState( AssessmentViewModel viewModel)=> viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length ?Container(): _buildTitle(viewModel);
Widget _buildTitle( Widget _buildTitle(
{required int index, required AssessmentViewModel viewModel}) => AssessmentViewModel viewModel) =>
Text( Text(
'Q${index + 1}. ${viewModel.assessmentQuestions[index].questionText} ', 'Q${viewModel.currentQuestionIndex + 1}. ${viewModel.assessmentQuestions[viewModel.currentQuestionIndex].questionText} ',
style: style16DG600, style: style16DG600,
); );
Widget _buildAnswersState( AssessmentViewModel viewModel)=> viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length ?Container(): _buildAnswers(viewModel);
Widget _buildAnswers( Widget _buildAnswers(
{required int index, required AssessmentViewModel viewModel}) => AssessmentViewModel viewModel) =>
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: viewModel.assessmentQuestions[index].options?.length, itemCount: viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?.length,
itemBuilder: (context, inner) => _buildAnswer( itemBuilder: (context, inner) => _buildAnswer(
onTap: () => viewModel.setSelectedAnswer( onTap: () => viewModel.setSelectedAnswer(
question: index + 1, question: viewModel.currentQuestionIndex + 1,
option: viewModel.assessmentQuestions[index].options?[inner]), option: viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?[inner]),
title: title:
viewModel.assessmentQuestions[index].options?[inner].optionText ?? viewModel.assessmentQuestions[viewModel.currentQuestionIndex].options?[inner].optionText ??
'', '',
selected: viewModel.isSelectedAnswer( selected: viewModel.isSelectedAnswer(
question: index + 1, question: viewModel.currentQuestionIndex + 1,
answer: viewModel answer: viewModel
.assessmentQuestions[index].options?[inner].optionText ?? .assessmentQuestions[viewModel.currentQuestionIndex ].options?[inner].optionText ??
''), ''),
), ),
); );
@ -120,28 +124,29 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
); );
Widget _buildContinueButtonWrapper( Widget _buildContinueButtonWrapper(
{required int question, required AssessmentViewModel viewModel}) => AssessmentViewModel viewModel) =>
Padding( Padding(
padding: const EdgeInsets.only(bottom: 50), padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel: viewModel, question: question), child: _buildContinueButton( viewModel),
); );
Widget _buildContinueButton( Widget _buildContinueButton(
{required int question, required AssessmentViewModel viewModel}) => AssessmentViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite, 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 == text: viewModel.currentQuestionIndex ==
viewModel.assessmentQuestions.length - 1 viewModel.assessmentQuestions.length - 1
? 'Finish Level' ? 'Finish Level'
: 'Continue', : '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) => [ List<Widget> _buildScreens(LearnPracticeViewModel viewModel) => [
_buildLearnPracticeIntroScreen(), _buildLearnPracticeIntroScreen(),
_buildLearnPracticeElementsScreen(), _buildLearnPracticeElementsScreen(),
_buildLearnPracticeQuestionsScreen(), _buildLearnPracticeQuestionsScreen(),
_buildFinishLearnPracticeScreen(), _buildFinishLearnPracticeScreen(),
_buildLearnPracticeResultScreen(), _buildLearnPracticeResultScreen(),
_buildLearnPracticeCompletionScreen() _buildLearnPracticeCompletionScreen()
]; ];
Widget _buildLearnPracticeIntroScreen() => Widget _buildLearnPracticeIntroScreen() => const LearnPracticeIntroScreen();
const LearnPracticeIntroScreen();
Widget _buildLearnPracticeElementsScreen() => Widget _buildLearnPracticeElementsScreen() =>
const LearnPracticeDescriptionScreen(); const LearnPracticeDescriptionScreen();

View File

@ -12,7 +12,8 @@ class FinishLearnPracticeScreen
extends ViewModelWidget<LearnPracticeViewModel> { extends ViewModelWidget<LearnPracticeViewModel> {
const FinishLearnPracticeScreen({super.key}); const FinishLearnPracticeScreen({super.key});
Future<void> _reset(LearnPracticeViewModel viewModel)async =>await viewModel.reset(); Future<void> _reset(LearnPracticeViewModel viewModel) async =>
await viewModel.reset();
@override @override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) => Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
@ -123,6 +124,6 @@ class FinishLearnPracticeScreen
backgroundColor: kcWhite, backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
foregroundColor: 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( Widget _buildImage(LearnPracticeViewModel viewModel) => CachedNetworkImage(
fit: BoxFit.cover, fit: BoxFit.cover,
width: double.maxFinite, width: double.maxFinite,
imageUrl: getReadableUrl(viewModel.practices.first.storyImage ?? '') ?? '', imageUrl:
getReadableUrl(viewModel.practices.first.storyImage ?? '') ?? '',
); );
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) => Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding( Padding(

View File

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

View File

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

View File

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

View File

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

View File

@ -91,9 +91,9 @@ class OnboardingViewModel extends ReactiveViewModel
Map<String, dynamic>? get selectedAgeGroup => _selectedAgeGroup; Map<String, dynamic>? get selectedAgeGroup => _selectedAgeGroup;
// Occupation // Occupation
bool _focusOccupation = false; String _selectedOccupation = 'Students (High school & University)';
bool get focusOccupation => _focusOccupation; String get selectedOccupation => _selectedOccupation;
// Country // Country
String _selectedCountry = 'Ethiopia'; String _selectedCountry = 'Ethiopia';
@ -105,6 +105,14 @@ class OnboardingViewModel extends ReactiveViewModel
bool get focusRegion => _focusRegion; bool get focusRegion => _focusRegion;
bool _dropdownRegion = true;
bool get dropdownRegion => _dropdownRegion;
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
// Learning goal // Learning goal
String? _selectedLearningGoal; String? _selectedLearningGoal;
@ -253,8 +261,18 @@ class OnboardingViewModel extends ReactiveViewModel
_selectedAgeGroup == value; _selectedAgeGroup == value;
// Occupation // Occupation
void setOccupationFocus() { List<String> getOccupations() => [
_focusOccupation = true; '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(); rebuildUi();
} }
@ -415,15 +433,50 @@ class OnboardingViewModel extends ReactiveViewModel
void setSelectedCountry(String value) { void setSelectedCountry(String value) {
_selectedCountry = value; _selectedCountry = value;
if (value == 'Ethiopia') {
_dropdownRegion = true;
_selectedRegion = 'Addis Ababa';
} else {
_dropdownRegion = false;
}
rebuildUi(); rebuildUi();
} }
// Region // 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() { void setRegionFocus() {
_focusRegion = true; _focusRegion = true;
rebuildUi(); rebuildUi();
} }
void unsetRegionFocus() {
_focusRegion = false;
rebuildUi();
}
// Learning goal // Learning goal
void setSelectedLearningGoal(String value) { void setSelectedLearningGoal(String value) {
_selectedLearningGoal = value; _selectedLearningGoal = value;
@ -541,7 +594,7 @@ class OnboardingViewModel extends ReactiveViewModel
// Reset occupation form screen // Reset occupation form screen
void resetOccupationFormScreen() { void resetOccupationFormScreen() {
_focusOccupation = false; _selectedOccupation = 'Students (High school & University)';
rebuildUi(); rebuildUi();
} }
@ -549,6 +602,7 @@ class OnboardingViewModel extends ReactiveViewModel
void resetCountryRegionFormScreen() { void resetCountryRegionFormScreen() {
_focusRegion = false; _focusRegion = false;
_selectedCountry = 'Ethiopia'; _selectedCountry = 'Ethiopia';
_selectedRegion = 'Addis Ababa';
rebuildUi(); rebuildUi();
} }

View File

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

View File

@ -2,6 +2,8 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/image_picker_service.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 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
@ -10,6 +12,7 @@ import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart'; import '../../../services/authentication_service.dart';
import '../../../services/google_auth_service.dart'; import '../../../services/google_auth_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../../services/url_launcher_service.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
class ProfileViewModel extends ReactiveViewModel { class ProfileViewModel extends ReactiveViewModel {
@ -23,6 +26,10 @@ class ProfileViewModel extends ReactiveViewModel {
final _googleAuthService = locator<GoogleAuthService>(); final _googleAuthService = locator<GoogleAuthService>();
final _phoneCallerService = locator<PhoneCallerService>();
final _urlLauncherService = locator<UrlLauncherService>();
final _imagePickerService = locator<ImagePickerService>(); final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>(); 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 // Dialog
Future<bool?> showAbortDialog() async { Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog( DialogResponse? response = await _dialogService.showDialog(

View File

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

View File

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

View File

@ -49,14 +49,6 @@ class RegisterViewModel extends ReactiveViewModel
bool get length => _length; bool get length => _length;
bool _number = false;
bool get number => _number;
bool _specialChar = false;
bool get specialChar => _specialChar;
bool _focusPassword = false; bool _focusPassword = false;
bool get focusPassword => _focusPassword; bool get focusPassword => _focusPassword;
@ -138,17 +130,6 @@ class RegisterViewModel extends ReactiveViewModel
_length = false; _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) { if (password == confirmPassword) {
_passwordMatch = true; _passwordMatch = true;
@ -160,13 +141,9 @@ class RegisterViewModel extends ReactiveViewModel
double validationProgress() { double validationProgress() {
int completed = 0; int completed = 0;
if (_length) completed++; if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++; if (_passwordMatch) completed++;
return completed / 2; // returns 0.0 1.0
return completed / 4; // returns 0.0 1.0
} }
void setObscurePassword() { void setObscurePassword() {
@ -238,8 +215,6 @@ class RegisterViewModel extends ReactiveViewModel
void resetCreatePasswordScreen() { void resetCreatePasswordScreen() {
_agree = false; _agree = false;
_length = false; _length = false;
_number = false;
_specialChar = false;
_passwordMatch = false; _passwordMatch = false;
_focusPassword = false; _focusPassword = false;
_focusConfirmPassword = false; _focusConfirmPassword = false;

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@
#include <flutter_inappwebview_linux/flutter_inappwebview_linux_plugin.h> #include <flutter_inappwebview_linux/flutter_inappwebview_linux_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <record_linux/record_linux_plugin.h> #include <record_linux/record_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
@ -28,4 +29,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) record_linux_registrar = g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar); 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_inappwebview_linux
flutter_secure_storage_linux flutter_secure_storage_linux
record_linux record_linux
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

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

View File

@ -662,6 +662,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.7" 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: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -1693,6 +1701,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" 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: uuid:
dependency: transitive dependency: transitive
description: description:

View File

@ -26,6 +26,7 @@ dependencies:
flutter_html: ^3.0.0 flutter_html: ^3.0.0
email_validator: any email_validator: any
audioplayers: ^6.6.0 audioplayers: ^6.6.0
url_launcher: ^6.3.2
video_player: ^2.10.1 video_player: ^2.10.1
firebase_core: ^4.4.0 firebase_core: ^4.4.0
in_app_update: ^4.2.5 in_app_update: ^4.2.5
@ -49,6 +50,7 @@ dependencies:
flutter_timer_countdown: ^1.0.7 flutter_timer_countdown: ^1.0.7
flutter_carousel_widget: ^3.1.0 flutter_carousel_widget: ^3.1.0
flutter_inappwebview: ^6.2.0-beta.3 flutter_inappwebview: ^6.2.0-beta.3
flutter_phone_direct_caller: ^2.2.1
flutter_local_notifications: ^20.1.0 flutter_local_notifications: ^20.1.0
internet_connection_checker_plus: ^2.9.1+2 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/voice_recorder_service.dart';
import 'package:yimaru_app/services/in_app_update_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/vimeo_service.dart';
import 'package:yimaru_app/services/url_launcher_service.dart';
import 'package:yimaru_app/services/phone_caller_service.dart';
// @stacked-import // @stacked-import
import 'test_helpers.mocks.dart'; import 'test_helpers.mocks.dart';
@ -47,6 +49,9 @@ import 'test_helpers.mocks.dart';
MockSpec<InAppUpdateService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<InAppUpdateService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<VimeoService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<VimeoService>(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 // @stacked-mock-spec
], ],
) )
@ -71,6 +76,9 @@ void registerServices() {
getAndRegisterInAppUpdateService(); getAndRegisterInAppUpdateService();
getAndRegisterVimeoService(); getAndRegisterVimeoService();
getAndRegisterVimeoService(); getAndRegisterVimeoService();
getAndRegisterUrlLauncherService();
getAndRegisterUrlLauncherService();
getAndRegisterPhoneCallerService();
// @stacked-mock-register // @stacked-mock-register
} }
@ -239,6 +247,20 @@ MockVimeoService getAndRegisterVimeoService() {
locator.registerSingleton<VimeoService>(service); locator.registerSingleton<VimeoService>(service);
return 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 // @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() { 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 <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h> #include <record_windows/record_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar( AudioplayersWindowsPluginRegisterWithRegistrar(
@ -35,4 +36,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar( RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

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