Merge branch 'release/0.1.13'

- fix: Apply UAT fixes
This commit is contained in:
BisratHailu 2026-05-07 17:00:43 +03:00
commit 3d9da1edaa
36 changed files with 674 additions and 426 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
.map(
(e) { (e) {
return Assessment.fromJson(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

@ -111,8 +111,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
_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) =>

View File

@ -153,8 +153,8 @@ 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) =>

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

@ -25,7 +25,6 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
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,
required LearnPracticeViewModel viewModel}) async => required LearnPracticeViewModel viewModel}) async =>

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
? () => _next(viewModel)
: null
: () => _next(viewModel),
backgroundColor: !viewModel.dropdownRegion
? regionController.text.isNotEmpty
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1), : 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(
@ -139,11 +124,6 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
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,12 +33,14 @@ 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,
'first_name': firstNameController.text, 'first_name': firstNameController.text,
'occupation': occupationController.text, 'occupation': viewModel.selectedOccupation,
'birth_day': DateFormat('yyyy-MM-dd').format(DateTime.now()), 'birth_day': DateFormat('yyyy-MM-dd').format(DateTime.now()),
}; };
@ -65,15 +66,25 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
content: _buildImagePicker(context: context, viewModel: viewModel), content: _buildImagePicker(context: context, viewModel: viewModel),
); );
void _checkRegion(ProfileDetailViewModel viewModel){
bool region = viewModel.checkRegion(region:viewModel.user?.region ?? 'Addis Ababa',country:viewModel.user?.country ?? 'Ethiopia' );
if(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 ?? ''; _checkRegion(viewModel);
viewModel.clearUserData(); viewModel.clearUserData();
viewModel.setGender(viewModel.user?.gender ?? ''); viewModel.setSelectedGender(viewModel.user?.gender ?? '');
viewModel.setSelectedOccupation(viewModel.user?.occupation ?? '');
viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia'); viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia');
} }
@ -359,7 +370,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildMaleRadioButton(ProfileDetailViewModel viewModel) => Widget _buildMaleRadioButton(ProfileDetailViewModel viewModel) =>
RadioGroup<String?>( RadioGroup<String?>(
groupValue: viewModel.selectedGender, groupValue: viewModel.selectedGender,
onChanged: (value) => viewModel.setGender(value ?? ''), onChanged: (value) => viewModel.setSelectedGender(value ?? ''),
child: _buildMaleRadioTileWrapper(viewModel)); child: _buildMaleRadioTileWrapper(viewModel));
Widget _buildMaleRadioTileWrapper(ProfileDetailViewModel viewModel) => Widget _buildMaleRadioTileWrapper(ProfileDetailViewModel viewModel) =>
@ -388,7 +399,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildFemaleRadioButton(ProfileDetailViewModel viewModel) => Widget _buildFemaleRadioButton(ProfileDetailViewModel viewModel) =>
RadioGroup<String?>( RadioGroup<String?>(
groupValue: viewModel.selectedGender, groupValue: viewModel.selectedGender,
onChanged: (value) => viewModel.setGender(value ?? ''), onChanged: (value) => viewModel.setSelectedGender(value ?? ''),
child: _buildFemaleRadioTileWrapper(viewModel)); child: _buildFemaleRadioTileWrapper(viewModel));
Widget _buildFemaleRadioTileWrapper(ProfileDetailViewModel viewModel) => Widget _buildFemaleRadioTileWrapper(ProfileDetailViewModel viewModel) =>
@ -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,8 +560,21 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
style: style16DG600, style: style16DG600,
); );
Widget _buildRegionFormField(ProfileDetailViewModel viewModel) => Widget _buildRegionFormState(ProfileDetailViewModel viewModel) =>
TextFormField( 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, controller: regionController,
onTap: viewModel.setRegionFocus, onTap: viewModel.setRegionFocus,
decoration: inputDecoration( decoration: inputDecoration(
@ -567,6 +593,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
style: style12R700, style: style12R700,
); );
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) => Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -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,28 +616,17 @@ 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,

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 = {};
@ -90,7 +100,7 @@ class ProfileDetailViewModel extends ReactiveViewModel
} }
// Gender // Gender
void setGender(String value) { void setSelectedGender(String value) {
_selectedGender = value; _selectedGender = value;
rebuildUi(); rebuildUi();
} }
@ -107,7 +117,24 @@ 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",
@ -266,21 +293,56 @@ class ProfileDetailViewModel extends ReactiveViewModel
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({required String region,required String country}){
if(country == 'Ethiopia'){
return getRegions().contains(region);
}
return false;
}
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

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

@ -1,5 +1,5 @@
name: yimaru_app name: yimaru_app
version: 0.1.12+14 version: 0.1.13+15
publish_to: 'none' publish_to: 'none'
description: A new Flutter project. description: A new Flutter project.
@ -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