fix: Apply UAT fixes

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -111,8 +111,7 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
_buildLearnPracticeCompletionScreen()
];
Widget _buildLearnPracticeIntroScreen() =>
const LearnPracticeIntroScreen();
Widget _buildLearnPracticeIntroScreen() => const LearnPracticeIntroScreen();
Widget _buildLearnPracticeElementsScreen() =>
const LearnPracticeDescriptionScreen();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,21 +57,31 @@ class ProfileDetailViewModel extends ReactiveViewModel
bool get focusEmail => _focusEmail;
// Occupation
String _selectedOccupation = 'Students (High school & University)';
String get selectedOccupation => _selectedOccupation;
// Country
String _selectedCountry = 'Ethiopia';
String get selectedCountry => _selectedCountry;
// Occupation
bool _focusOccupation = false;
bool get focusOccupation => _focusOccupation;
// Region
bool _focusRegion = false;
bool get focusRegion => _focusRegion;
bool _dropdownRegion = true;
bool get dropdownRegion => _dropdownRegion;
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
// User data
final Map<String, dynamic> _userData = {};
@ -107,7 +117,24 @@ class ProfileDetailViewModel extends ReactiveViewModel
rebuildUi();
}
// Country
// Occupation
List<String> getOccupations() => [
'Students (High school & University)',
'Job Seekers / Fresh Graduates',
'Working Professionals (Corporate/Office)',
'Government & NGO Workers',
'Entrepreneurs & Small Business Owners',
'Hospitality & Tourism Workers',
'Freelancers / Remote Workers (Digital Economy)'
];
void setSelectedOccupation(String value) {
_selectedOccupation = value;
rebuildUi();
}
// Country
List<String> getCountries() => [
"Afghanistan",
@ -266,21 +293,53 @@ class ProfileDetailViewModel extends ReactiveViewModel
void setSelectedCountry(String value) {
_selectedCountry = value;
rebuildUi();
if (value == 'Ethiopia') {
_dropdownRegion = true;
_selectedRegion = 'Addis Ababa';
} else {
_dropdownRegion = false;
}
// Occupation
void setOccupationFocus() {
_focusOccupation = true;
rebuildUi();
}
// Region
List<String> getRegions() => [
'Addis Ababa',
'Afar',
'Amhara',
'Benishangul-Gumuz',
'Central Ethiopia',
'Dire Dawa',
'Gambela',
'Harari',
'Oromia',
'Sidama',
'Somali',
'South Ethiopia',
'South West Ethiopia Peoples',
'Tigray',
];
bool checkRegion(String value){
return getRegions().contains(value);
}
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
}
void setRegionFocus() {
_focusRegion = true;
rebuildUi();
}
void unsetRegionFocus() {
_focusRegion = false;
rebuildUi();
}
// User data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);

View File

@ -49,14 +49,6 @@ class RegisterViewModel extends ReactiveViewModel
bool get length => _length;
bool _number = false;
bool get number => _number;
bool _specialChar = false;
bool get specialChar => _specialChar;
bool _focusPassword = false;
bool get focusPassword => _focusPassword;
@ -138,17 +130,6 @@ class RegisterViewModel extends ReactiveViewModel
_length = false;
}
if (RegExp(r'\d').hasMatch(password)) {
_number = true;
} else {
_number = false;
}
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
_specialChar = true;
} else {
_specialChar = false;
}
if (password == confirmPassword) {
_passwordMatch = true;
@ -160,13 +141,9 @@ class RegisterViewModel extends ReactiveViewModel
double validationProgress() {
int completed = 0;
if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++;
return completed / 4; // returns 0.0 1.0
return completed / 2; // returns 0.0 1.0
}
void setObscurePassword() {
@ -238,8 +215,6 @@ class RegisterViewModel extends ReactiveViewModel
void resetCreatePasswordScreen() {
_agree = false;
_length = false;
_number = false;
_specialChar = false;
_passwordMatch = false;
_focusPassword = false;
_focusConfirmPassword = false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('PhoneCallerServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('UrlLauncherServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

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

View File

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