Merge branch 'release/0.1.25'

-fix: Update profile detail section
This commit is contained in:
BisratHailu 2026-05-27 23:43:45 +03:00
commit 0842a0b357
16 changed files with 136 additions and 252 deletions

View File

@ -37,6 +37,9 @@ class User {
@JsonKey(name: 'profile_completed')
final bool? profileCompleted;
@JsonKey(name: 'subscription_status')
final String? subscriptionStatus;
@JsonKey(name: 'profile_picture_url')
final String? profilePicture;
@ -55,6 +58,7 @@ class User {
this.profilePicture,
this.userInfoLoaded,
this.profileCompleted,
this.subscriptionStatus
});
User copyWith(

View File

@ -90,6 +90,8 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setBool('userInfoLoaded', true);
await _secureService.setBool(
'profileCompleted', data.profileCompleted ?? false);
await _secureService.setString(
'subscriptionStatus', data.subscriptionStatus ?? '');
await _secureService.setString('email', data.email ?? '');
await _secureService.setString('region', data.region ?? '');
await _secureService.setString('gender', data.gender ?? '');
@ -99,6 +101,7 @@ class AuthenticationService with ListenableServiceMixin {
await _secureService.setString('firstName', data.firstName ?? '');
await _secureService.setString('occupation', data.occupation ?? '');
_user = User(
email: data.email,
gender: data.gender,
@ -113,6 +116,7 @@ class AuthenticationService with ListenableServiceMixin {
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: data.profileCompleted,
subscriptionStatus: data.subscriptionStatus,
);
notifyListeners();
@ -169,6 +173,7 @@ class AuthenticationService with ListenableServiceMixin {
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
profilePicture: await _secureService.getString('profilePicture'),
profileCompleted: await _secureService.getBool('profileCompleted'),
subscriptionStatus: await _secureService.getString('subscriptionStatus'),
);
return _user;
}

View File

@ -1,4 +1,3 @@
import 'package:http/http.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/refresh_object.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
@ -42,7 +41,7 @@ class LearnService with ListenableServiceMixin {
List<LearnLesson> get lessons => _lessons;
// Learn progress
List<ProgressSummary> _summaries = [];
final List<ProgressSummary> _summaries = [];
List<ProgressSummary> get summaries => _summaries;

View File

@ -101,4 +101,16 @@ class OnboardingService with ListenableServiceMixin {
}
return false;
}
// Profile detail fields
Future<void> getProfileDetailFields() async {
_countries = await _apiService.getCountries();
_occupations = await _apiService.getOccupations();
_regions = await _apiService.getEthiopiaRegions();
notifyListeners();
}
}

View File

@ -39,6 +39,7 @@ enum StateObjects {
learnModules,
learnCourses,
profileImage,
profileDetail,
learnPrograms,
courseLessons,
profileUpdate,

View File

@ -3,7 +3,6 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/learn_module.dart';
import 'package:yimaru_app/models/refresh_object.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import '../../../app/app.locator.dart';

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/age_group_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/challenge_form_screen.dart';
@ -13,7 +12,6 @@ import 'package:yimaru_app/ui/views/onboarding/screens/language_goal_form_screen
import 'package:yimaru_app/ui/views/onboarding/screens/learning_goal_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/occupation_form_screen.dart';
import 'package:yimaru_app/ui/views/onboarding/screens/topic_form_screen.dart';
import 'package:yimaru_app/ui/views/startup/startup_view.dart';
import '../../common/validators/form_validator.dart';
import 'onboarding_viewmodel.dart';

View File

@ -4,12 +4,9 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../../models/field_option.dart';
import '../../../services/api_service.dart';
import '../../../services/google_auth_service.dart';
import '../../../services/localization_service.dart';
import '../../../services/onboarding_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class OnboardingViewModel extends ReactiveViewModel
with FormStateHelper

View File

@ -5,7 +5,6 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';

View File

@ -5,7 +5,6 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';

View File

@ -5,7 +5,6 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart';
import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart';

View File

@ -7,10 +7,12 @@ import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
import '../../../models/field_option.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../common/validators/form_validator.dart';
import '../../widgets/custom_dropdown.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/image_picker_option.dart';
import '../../widgets/page_loading_indicator.dart';
@ -31,19 +33,30 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
with $ProfileDetailView {
const ProfileDetailView({Key? key}) : super(key: key);
void _setSelectedCountry(
{FieldOption? value, required ProfileDetailViewModel viewModel}) {
viewModel.setSelectedCountry(value);
if (viewModel.selectedCountry?.label?.toLowerCase().tr() != 'ethiopia') {
regionController.clear();
viewModel.unsetRegionFocus();
}
}
Future<void> _update(ProfileDetailViewModel viewModel) async {
Map<String, dynamic> data = {
'region': viewModel.dropdownRegion
? viewModel.selectedRegion
: regionController.text,
'gender': viewModel.selectedGender,
'region': viewModel.dropdownRegion
? viewModel.selectedRegion?.code
: regionController.text,
'last_name': lastNameController.text,
'country': viewModel.selectedCountry,
'first_name': firstNameController.text,
'occupation': viewModel.selectedOccupation,
'country': viewModel.selectedCountry?.code,
'occupation': viewModel.selectedOccupation?.code,
'birth_day': DateFormat('yyyy-MM-dd').format(DateTime.now()),
};
viewModel.addUserData(data);
await viewModel.updateProfile();
@ -68,10 +81,13 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
void _checkRegion(ProfileDetailViewModel viewModel) {
bool region = viewModel.checkRegion(
region: viewModel.user?.region ?? 'Addis Ababa',
country: viewModel.user?.country ?? 'Ethiopia');
region: viewModel.user?.region ?? '',
country: viewModel.user?.country ?? '');
if (region) {
viewModel.setSelectedRegion(viewModel.user?.region ?? 'Addis Ababa');
FieldOption? option = viewModel.regions
.where((e) => (e.code ?? '') == viewModel.user?.region)
.first;
viewModel.setSelectedRegion(option);
} else {
regionController.text = viewModel.user?.region ?? '';
}
@ -85,12 +101,17 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
_checkRegion(viewModel);
viewModel.clearUserData();
viewModel.setSelectedGender(viewModel.user?.gender ?? '');
viewModel.setSelectedOccupation(viewModel.user?.occupation ?? '');
viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia');
viewModel.setSelectedOccupation(viewModel.occupations
.where((e) => (e.code ?? '') == viewModel.user?.occupation)
.first);
viewModel.setSelectedCountry(viewModel.countries
.where((e) => (e.code ?? '') == viewModel.user?.country)
.first);
}
@override
void onViewModelReady(ProfileDetailViewModel viewModel) {
void onViewModelReady(ProfileDetailViewModel viewModel) async{
await viewModel.getProfileDetailFields();
_onModelReady(viewModel);
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
@ -113,9 +134,17 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
required ProfileDetailViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldContainer(context: context, viewModel: viewModel),
body: _buildScaffoldState(context: context, viewModel: viewModel),
);
Widget _buildScaffoldState(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
viewModel.busy(StateObjects.profileDetail)
? const PageLoadingIndicator()
: _buildScaffoldContainer(context: context, viewModel: viewModel);
Widget _buildScaffoldContainer( {required BuildContext context,
required ProfileDetailViewModel viewModel}) => Container(
decoration: bgDecoration,
@ -208,7 +237,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
verticalSpaceMedium,
_buildCountryDropdownLabel(),
verticalSpaceSmall,
// _buildCountryDropdown(viewModel),
_buildCountryDropdown(viewModel),
verticalSpaceMedium,
_buildRegionFormFieldWrapper(viewModel),
verticalSpaceMedium,
@ -533,13 +562,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
label: LocaleKeys.country.tr(),
);
// Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
// CustomDropdownPicker(
// hint: 'Select country',
// selectedItem: viewModel.selectedCountry,
// items: (value, props) => viewModel.getCountries(),
// onChanged: (value) => viewModel.setSelectedCountry(value ?? 'Ethiopia'),
// );
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
icon: _buildSearchIcon(),
hint: LocaleKeys.select_country.tr(),
selectedItem: viewModel.selectedCountry,
items: (value, props) => viewModel.countries,
onChanged: (value) =>
_setSelectedCountry(value: value, viewModel: viewModel));
Widget _buildRegionFormFieldWrapper(ProfileDetailViewModel viewModel) =>
Column(
@ -571,19 +601,17 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
);
Widget _buildRegionFormState(ProfileDetailViewModel viewModel) =>
// viewModel.dropdownRegion
// ? _buildRegionDropDown(viewModel)
// :
_buildRegionFormField(viewModel);
//
// Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) =>
// CustomDropdownPicker(
// icon: _buildSearchIcon(),
// hint:LocaleKeys.select_region.tr(),
// selectedItem: viewModel.selectedRegion,
// items: (value, props) => viewModel.getRegions(),
// onChanged: (value) =>
// viewModel.setSelectedRegion(value ?? 'Addis Ababa'));
viewModel.dropdownRegion
? _buildRegionDropDown(viewModel)
: _buildRegionFormField(viewModel);
Widget _buildRegionDropDown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
icon: _buildSearchIcon(),
hint: LocaleKeys.select_region.tr(),
selectedItem: viewModel.selectedRegion,
items: (value, props) => viewModel.regions,
onChanged: (value) => viewModel.setSelectedRegion(value));
Widget _buildRegionFormField(ProfileDetailViewModel viewModel) =>
TextFormField(
@ -618,7 +646,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
[
_buildOccupationDropdownLabel(),
verticalSpaceSmall,
// _buildOccupationDropdown(viewModel)
_buildOccupationDropdown(viewModel)
];
Widget _buildOccupationDropdownLabel() => CustomFormLabel(
@ -626,14 +654,14 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
label: LocaleKeys.occupation.tr(),
);
// Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
// CustomDropdownPicker(
// icon: _buildSearchIcon(),
// hint:LocaleKeys.select_occupation.tr(),
// selectedItem: viewModel.selectedOccupation,
// items: (value, props) => viewModel.getOccupations(),
// onChanged: (value) => viewModel.setSelectedOccupation(
// value ?? 'Students (High school & University)'));
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker(
icon: _buildSearchIcon(),
hint: LocaleKeys.select_occupation.tr(),
selectedItem: viewModel.selectedOccupation,
items: (value, props) => viewModel.occupations,
onChanged: (value) => viewModel.setSelectedOccupation(value));
Icon _buildSearchIcon() => const Icon(
Icons.search,
color: kcPrimaryColor,

View File

@ -2,10 +2,12 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../models/field_option.dart';
import '../../../models/user.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/image_picker_service.dart';
import '../../../services/onboarding_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
@ -17,6 +19,8 @@ class ProfileDetailViewModel extends ReactiveViewModel
final _statusChecker = locator<StatusCheckerService>();
final _onboardingService = locator<OnboardingService>();
final _navigationService = locator<NavigationService>();
final _imagePickerService = locator<ImagePickerService>();
@ -58,14 +62,14 @@ class ProfileDetailViewModel extends ReactiveViewModel
bool get focusEmail => _focusEmail;
// Occupation
String _selectedOccupation = 'Students (High school & University)';
FieldOption? _selectedOccupation;
String get selectedOccupation => _selectedOccupation;
FieldOption? get selectedOccupation => _selectedOccupation;
// Country
String _selectedCountry = 'Ethiopia';
FieldOption? _selectedCountry;
String get selectedCountry => _selectedCountry;
FieldOption? get selectedCountry => _selectedCountry;
// Region
bool _focusRegion = false;
@ -76,9 +80,9 @@ class ProfileDetailViewModel extends ReactiveViewModel
bool get dropdownRegion => _dropdownRegion;
String _selectedRegion = 'Addis Ababa';
FieldOption? _selectedRegion;
String get selectedRegion => _selectedRegion;
FieldOption? get selectedRegion => _selectedRegion;
// User data
final Map<String, dynamic> _userData = {};
@ -116,182 +120,26 @@ class ProfileDetailViewModel extends ReactiveViewModel
}
// 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)'
];
List<FieldOption> get _occupations => _onboardingService.occupations;
void setSelectedOccupation(String value) {
List<FieldOption> get occupations => _occupations;
void setSelectedOccupation(FieldOption? value) {
_selectedOccupation = value;
rebuildUi();
}
// Country
List<String> getCountries() => [
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Argentina",
"Armenia",
"Australia",
"Austria",
"Azerbaijan",
"Bahrain",
"Bangladesh",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bhutan",
"Bolivia",
"Bosnia and Herzegovina",
"Botswana",
"Brazil",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Chad",
"Chile",
"China",
"Colombia",
"Comoros",
"Congo",
"Costa Rica",
"Croatia",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominican Republic",
"Ecuador",
"Egypt",
"El Salvador",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Finland",
"France",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Greece",
"Guatemala",
"Guinea",
"Haiti",
"Honduras",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Israel",
"Italy",
"Jamaica",
"Japan",
"Jordan",
"Kazakhstan",
"Kenya",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Liberia",
"Libya",
"Lithuania",
"Luxembourg",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Mexico",
"Moldova",
"Monaco",
"Mongolia",
"Morocco",
"Mozambique",
"Myanmar",
"Namibia",
"Nepal",
"Netherlands",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"North Korea",
"Norway",
"Oman",
"Pakistan",
"Panama",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Qatar",
"Romania",
"Russia",
"Rwanda",
"Saudi Arabia",
"Senegal",
"Serbia",
"Singapore",
"Slovakia",
"Slovenia",
"Somalia",
"South Africa",
"South Korea",
"Spain",
"Sri Lanka",
"Sudan",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Tunisia",
"Turkey",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Venezuela",
"Vietnam",
"Yemen",
"Zambia",
"Zimbabwe"
];
List<FieldOption> get _countries => _onboardingService.countries;
void setSelectedCountry(String value) {
List<FieldOption> get countries => _countries;
void setSelectedCountry(FieldOption? value) {
_selectedCountry = value;
if (value == 'Ethiopia') {
if (value?.label?.toLowerCase().trim() == 'ethiopia') {
_dropdownRegion = true;
_selectedRegion = 'Addis Ababa';
_selectedRegion = _regions
.firstWhere((e) => e.label?.toLowerCase().trim() == 'addis ababa');
} else {
_dropdownRegion = false;
}
@ -300,34 +148,22 @@ class ProfileDetailViewModel extends ReactiveViewModel
}
// 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',
];
List<FieldOption> get _regions => _onboardingService.regions;
List<FieldOption> get regions => _regions;
void setSelectedRegion(FieldOption? value) {
_selectedRegion = value;
rebuildUi();
}
bool checkRegion({required String region, required String country}) {
if (country == 'Ethiopia') {
return getRegions().contains(region);
if (country.toLowerCase().contains('ethiopia')) {
return _regions.contains(region);
}
return false;
}
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
}
void setRegionFocus() {
_focusRegion = true;
@ -419,4 +255,12 @@ class ProfileDetailViewModel extends ReactiveViewModel
await _apiService.updateProfileImage(data: data, userId: _user?.userId);
}
}
// Profile detail fields
Future<void> getProfileDetailFields() async =>
await runBusyFuture(_getProfileDetailFields(),busyObject: StateObjects.profileDetail);
Future<void> _getProfileDetailFields() async {
await _onboardingService.getProfileDetailFields();
}
}

View File

@ -89,7 +89,7 @@ class StartupViewModel extends ReactiveViewModel {
response = {'data': true, 'status': ResponseStatus.success};
}
if (response['status'] == ResponseStatus.success && !response['data']) {
await etOnboardingFields();
await getOnboardingFields();
} else if (response['status'] == ResponseStatus.success &&
response['data']) {
await saveProfileStatus(response['data']);
@ -134,7 +134,7 @@ class StartupViewModel extends ReactiveViewModel {
// Remote api call
// Onboarding fields
Future<void> etOnboardingFields() async {
Future<void> getOnboardingFields() async {
bool response = await _onboardingService.getOnboardingFields();
if (response) {
await replaceWithOnboarding();

View File

@ -10,7 +10,6 @@ import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../common/app_colors.dart';
import '../common/helper_functions.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';

View File

@ -1,5 +1,5 @@
name: yimaru_app
version: 0.1.24+26
version: 0.1.25+27
publish_to: 'none'
description: A new Flutter project.
@ -9,6 +9,7 @@ environment:
dependencies:
flutter:
sdk: flutter
http: any
intl: any
dio: ^5.9.0