From 8f329f774da6d1ecc5f6ac09e92e057bbc8dd355 Mon Sep 17 00:00:00 2001 From: BisratHailu Date: Fri, 23 Jan 2026 09:14:07 +0300 Subject: [PATCH 1/5] first commit --- lib/app/app.dart | 4 + lib/app/app.locator.dart | 2 + lib/app/app.router.dart | 141 +++- lib/models/user_model.dart | 17 +- lib/models/user_model.g.dart | 6 +- lib/services/api_service.dart | 41 +- lib/services/authentication_service.dart | 58 +- lib/services/dio_service.dart | 19 +- lib/services/image_picker_service.dart | 1 + lib/ui/common/app_constants.dart | 4 +- lib/ui/common/ui_helpers.dart | 39 +- .../assessment/assessment_viewmodel.dart | 66 +- .../screens/assessment_form_screen.dart | 3 +- .../screens/assessment_intro_screen.dart | 3 +- .../screens/start_lesson_screen.dart | 22 +- lib/ui/views/failure/failure_view.dart | 96 +++ lib/ui/views/failure/failure_viewmodel.dart | 3 + lib/ui/views/home/home_view.dart | 5 +- lib/ui/views/home/home_viewmodel.dart | 51 +- lib/ui/views/learn/learn_view.dart | 7 +- lib/ui/views/learn/learn_viewmodel.dart | 7 + .../learn_module/learn_module_viewmodel.dart | 3 +- lib/ui/views/login/login_viewmodel.dart | 2 +- lib/ui/views/onboarding/onboarding_view.dart | 2 +- .../onboarding/onboarding_viewmodel.dart | 14 +- .../screens/birthday_form_screen.dart | 6 +- .../screens/learning_goal_form_screen.dart | 17 +- .../onboarding/screens/topic_form_screen.dart | 2 + lib/ui/views/profile/profile_view.dart | 26 +- lib/ui/views/profile/profile_viewmodel.dart | 5 + .../profile_detail/profile_detail_view.dart | 9 +- .../profile_detail_viewmodel.dart | 9 + lib/ui/views/register/register_viewmodel.dart | 2 +- lib/ui/widgets/large_app_bar.dart | 11 +- lib/ui/widgets/learn_app_bar.dart | 51 +- lib/ui/widgets/learn_module_tile.dart | 2 +- lib/ui/widgets/profile_image.dart | 21 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 88 +++ pubspec.yaml | 2 + test/helpers/test_helpers.dart | 85 --- test/helpers/test_helpers.mocks.dart | 684 ------------------ test/services/image_picker_service_test.dart | 11 + test/viewmodels/failure_viewmodel_test.dart | 11 + 44 files changed, 704 insertions(+), 956 deletions(-) create mode 100644 lib/services/image_picker_service.dart create mode 100644 lib/ui/views/failure/failure_view.dart create mode 100644 lib/ui/views/failure/failure_viewmodel.dart delete mode 100644 test/helpers/test_helpers.dart delete mode 100644 test/helpers/test_helpers.mocks.dart create mode 100644 test/services/image_picker_service_test.dart create mode 100644 test/viewmodels/failure_viewmodel_test.dart diff --git a/lib/app/app.dart b/lib/app/app.dart index cd406ac..938bae2 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -30,6 +30,8 @@ import 'package:yimaru_app/services/status_checker_service.dart'; import 'package:yimaru_app/ui/views/welcome/welcome_view.dart'; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; +import 'package:yimaru_app/ui/views/failure/failure_view.dart'; +import 'package:yimaru_app/services/image_picker_service.dart'; // @stacked-import @StackedApp( @@ -57,6 +59,7 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; MaterialRoute(page: WelcomeView), MaterialRoute(page: AssessmentView), MaterialRoute(page: LearnLessonView), + MaterialRoute(page: FailureView), // @stacked-route ], dependencies: [ @@ -68,6 +71,7 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; LazySingleton(classType: SecureStorageService), LazySingleton(classType: DioService), LazySingleton(classType: StatusCheckerService), + LazySingleton(classType: ImagePickerService), // @stacked-service ], bottomsheets: [ diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index 4319ba3..c289fa1 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -14,6 +14,7 @@ import 'package:stacked_shared/stacked_shared.dart'; import '../services/api_service.dart'; import '../services/authentication_service.dart'; import '../services/dio_service.dart'; +import '../services/image_picker_service.dart'; import '../services/secure_storage_service.dart'; import '../services/status_checker_service.dart'; @@ -36,4 +37,5 @@ Future setupLocator({ locator.registerLazySingleton(() => SecureStorageService()); locator.registerLazySingleton(() => DioService()); locator.registerLazySingleton(() => StatusCheckerService()); + locator.registerLazySingleton(() => ImagePickerService()); } diff --git a/lib/app/app.router.dart b/lib/app/app.router.dart index 871f60e..53e8dc1 100644 --- a/lib/app/app.router.dart +++ b/lib/app/app.router.dart @@ -5,16 +5,17 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i25; +import 'package:flutter/material.dart' as _i26; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart' as _i1; -import 'package:stacked_services/stacked_services.dart' as _i26; +import 'package:stacked_services/stacked_services.dart' as _i27; import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' as _i10; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23; import 'package:yimaru_app/ui/views/call_support/call_support_view.dart' as _i13; import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; +import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25; import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2; import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14; import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19; @@ -89,6 +90,8 @@ class Routes { static const learnLessonView = '/learn-lesson-view'; + static const failureView = '/failure-view'; + static const all = { homeView, onboardingView, @@ -113,6 +116,7 @@ class Routes { welcomeView, assessmentView, learnLessonView, + failureView, }; } @@ -210,17 +214,21 @@ class StackedRouter extends _i1.RouterBase { Routes.learnLessonView, page: _i24.LearnLessonView, ), + _i1.RouteDef( + Routes.failureView, + page: _i25.FailureView, + ), ]; final _pagesMap = { _i2.HomeView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i2.HomeView(), settings: data, ); }, _i3.OnboardingView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i3.OnboardingView(), settings: data, ); @@ -229,133 +237,141 @@ class StackedRouter extends _i1.RouterBase { final args = data.getArgs( orElse: () => const StartupViewArguments(), ); - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => _i4.StartupView(key: args.key, label: args.label), settings: data, ); }, _i5.ProfileView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i5.ProfileView(), settings: data, ); }, _i6.ProfileDetailView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i6.ProfileDetailView(), settings: data, ); }, _i7.DownloadsView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i7.DownloadsView(), settings: data, ); }, _i8.ProgressView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i8.ProgressView(), settings: data, ); }, _i9.OngoingProgressView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i9.OngoingProgressView(), settings: data, ); }, _i10.AccountPrivacyView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i10.AccountPrivacyView(), settings: data, ); }, _i11.SupportView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i11.SupportView(), settings: data, ); }, _i12.TelegramSupportView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i12.TelegramSupportView(), settings: data, ); }, _i13.CallSupportView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i13.CallSupportView(), settings: data, ); }, _i14.LanguageView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i14.LanguageView(), settings: data, ); }, _i15.PrivacyPolicyView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i15.PrivacyPolicyView(), settings: data, ); }, _i16.TermsAndConditionsView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i16.TermsAndConditionsView(), settings: data, ); }, _i17.RegisterView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i17.RegisterView(), settings: data, ); }, _i18.LoginView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i18.LoginView(), settings: data, ); }, _i19.LearnView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i19.LearnView(), settings: data, ); }, _i20.LearnLevelView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i20.LearnLevelView(), settings: data, ); }, _i21.LearnModuleView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i21.LearnModuleView(), settings: data, ); }, _i22.WelcomeView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i22.WelcomeView(), settings: data, ); }, _i23.AssessmentView: (data) { final args = data.getArgs(nullOk: false); - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => _i23.AssessmentView(key: args.key, data: args.data), settings: data, ); }, _i24.LearnLessonView: (data) { - return _i25.MaterialPageRoute( + return _i26.MaterialPageRoute( builder: (context) => const _i24.LearnLessonView(), settings: data, ); }, + _i25.FailureView: (data) { + final args = data.getArgs(nullOk: false); + return _i26.MaterialPageRoute( + builder: (context) => + _i25.FailureView(key: args.key, label: args.label), + settings: data, + ); + }, }; @override @@ -371,7 +387,7 @@ class StartupViewArguments { this.label = 'Loading', }); - final _i25.Key? key; + final _i26.Key? key; final String label; @@ -398,7 +414,7 @@ class AssessmentViewArguments { required this.data, }); - final _i25.Key? key; + final _i26.Key? key; final Map data; @@ -419,7 +435,34 @@ class AssessmentViewArguments { } } -extension NavigatorStateExtension on _i26.NavigationService { +class FailureViewArguments { + const FailureViewArguments({ + this.key, + required this.label, + }); + + final _i26.Key? key; + + final String label; + + @override + String toString() { + return '{"key": "$key", "label": "$label"}'; + } + + @override + bool operator ==(covariant FailureViewArguments other) { + if (identical(this, other)) return true; + return other.key == key && other.label == label; + } + + @override + int get hashCode { + return key.hashCode ^ label.hashCode; + } +} + +extension NavigatorStateExtension on _i27.NavigationService { Future navigateToHomeView([ int? routerId, bool preventDuplicates = true, @@ -449,7 +492,7 @@ extension NavigatorStateExtension on _i26.NavigationService { } Future navigateToStartupView({ - _i25.Key? key, + _i26.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -718,7 +761,7 @@ extension NavigatorStateExtension on _i26.NavigationService { } Future navigateToAssessmentView({ - _i25.Key? key, + _i26.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -748,6 +791,23 @@ extension NavigatorStateExtension on _i26.NavigationService { transition: transition); } + Future navigateToFailureView({ + _i26.Key? key, + required String label, + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + }) async { + return navigateTo(Routes.failureView, + arguments: FailureViewArguments(key: key, label: label), + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future replaceWithHomeView([ int? routerId, bool preventDuplicates = true, @@ -777,7 +837,7 @@ extension NavigatorStateExtension on _i26.NavigationService { } Future replaceWithStartupView({ - _i25.Key? key, + _i26.Key? key, String label = 'Loading', int? routerId, bool preventDuplicates = true, @@ -1046,7 +1106,7 @@ extension NavigatorStateExtension on _i26.NavigationService { } Future replaceWithAssessmentView({ - _i25.Key? key, + _i26.Key? key, required Map data, int? routerId, bool preventDuplicates = true, @@ -1075,4 +1135,21 @@ extension NavigatorStateExtension on _i26.NavigationService { parameters: parameters, transition: transition); } + + Future replaceWithFailureView({ + _i26.Key? key, + required String label, + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + }) async { + return replaceWith(Routes.failureView, + arguments: FailureViewArguments(key: key, label: label), + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } } diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart index a890509..de05dc5 100644 --- a/lib/models/user_model.dart +++ b/lib/models/user_model.dart @@ -4,9 +4,13 @@ part 'user_model.g.dart'; @JsonSerializable() class UserModel { + final String? firstName; + @JsonKey(name: 'user_id') final int? userId; + final String? profileImage; + final bool? profileCompleted; @JsonKey(name: 'access_token') @@ -15,11 +19,14 @@ class UserModel { @JsonKey(name: 'refresh_token') final String? refreshToken; - const UserModel( - {this.userId, - this.accessToken, - this.profileCompleted, - this.refreshToken}); + const UserModel({ + this.userId, + this.firstName, + this.accessToken, + this.profileImage, + this.refreshToken, + this.profileCompleted, + }); factory UserModel.fromJson(Map json) => _$UserModelFromJson(json); diff --git a/lib/models/user_model.g.dart b/lib/models/user_model.g.dart index ac50491..625b7b6 100644 --- a/lib/models/user_model.g.dart +++ b/lib/models/user_model.g.dart @@ -8,13 +8,17 @@ part of 'user_model.dart'; UserModel _$UserModelFromJson(Map json) => UserModel( userId: (json['user_id'] as num?)?.toInt(), + firstName: json['firstName'] as String?, accessToken: json['access_token'] as String?, - profileCompleted: json['profileCompleted'] as bool?, + profileImage: json['profileImage'] as String?, refreshToken: json['refresh_token'] as String?, + profileCompleted: json['profileCompleted'] as bool?, ); Map _$UserModelToJson(UserModel instance) => { + 'firstName': instance.firstName, 'user_id': instance.userId, + 'profileImage': instance.profileImage, 'profileCompleted': instance.profileCompleted, 'access_token': instance.accessToken, 'refresh_token': instance.refreshToken, diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index fccb70e..80fd409 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -14,7 +14,7 @@ class ApiService { Future> register(Map data) async { try { Response response = await _service.dio.post( - '$baseUrl/$userUrl/$kRegisterUrl', + '$baseUrl/$kUserUrl/$kRegisterUrl', data: data, ); @@ -69,7 +69,7 @@ class ApiService { Future> verifyOtp(Map data) async { try { Response response = await _service.dio.post( - '$baseUrl/$userUrl/$kVerifyOtpUrl', + '$baseUrl/$kUserUrl/$kVerifyOtpUrl', data: data, ); if (response.statusCode == 200) { @@ -96,7 +96,7 @@ class ApiService { Future> resendOtp(Map data) async { try { Response response = await _service.dio.post( - '$baseUrl/$userUrl/$kResendOtpUrl', + '$baseUrl/$kUserUrl/$kResendOtpUrl', data: data, ); @@ -123,7 +123,7 @@ class ApiService { Future> getProfileStatus(UserModel? user) async { try { Response response = await _service.dio.get( - '$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl', + '$baseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl', ); if (response.statusCode == 200) { @@ -146,18 +146,42 @@ class ApiService { } } + // Get profile + Future> getProfileData(int? userId) async { + try { + Response response = await _service.dio.get( + '$baseUrl/$kUserUrl/$kGetUserUrl/$userId', + ); + + if (response.statusCode == 200) { + return { + 'data': response.data['data'], + 'status': ResponseStatus.success, + 'message': 'Profile fetched successfully' + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': 'Unknown Error Occurred' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + // Update profile Future> updateProfile( {required UserModel? user, required Map data}) async { try { Response response = await _service.dio.put( - '$baseUrl/$userUrl', + '$baseUrl/$kUserUrl', data: data, ); - print(response.statusCode); - print(response.data); - if (response.statusCode == 200) { return { 'status': ResponseStatus.success, @@ -170,7 +194,6 @@ class ApiService { }; } } catch (e) { - print('Exception ${e.toString()}'); return { 'message': e.toString(), 'status': ResponseStatus.failure, diff --git a/lib/services/authentication_service.dart b/lib/services/authentication_service.dart index abd0fa6..369e1cc 100644 --- a/lib/services/authentication_service.dart +++ b/lib/services/authentication_service.dart @@ -5,6 +5,10 @@ import 'package:yimaru_app/services/secure_storage_service.dart'; class AuthenticationService { final _secureService = locator(); + UserModel? _user; + + UserModel? get user => _user; + Future userLoggedIn() async { if (await _secureService.getString('userId') != null) { return true; @@ -28,14 +32,56 @@ class AuthenticationService { await _secureService.setString('refreshToken', refresh); } - Future saveUserData(Map data) async { + Future saveUserName(Map data) async { + await _secureService.setString('firstName', data['firstName']); + _user = UserModel( + firstName: await _secureService.getString('firstName'), + userId: _user?.userId, + accessToken: _user?.accessToken, + refreshToken: _user?.refreshToken, + profileCompleted: _user?.profileCompleted); + } + + Future saveBasicUserData(Map data) async { await _secureService.setInt('userId', data['userId']); await _secureService.setString('accessToken', data['accessToken']); await _secureService.setString('refreshToken', data['refreshToken']); + + _user = UserModel( + firstName: _user?.firstName, + profileImage: _user?.profileImage, + profileCompleted: _user?.profileCompleted, + userId: await _secureService.getInt('userId'), + accessToken: await _secureService.getString('accessToken'), + refreshToken: await _secureService.getString('refreshToken'), + ); } - Future saveProfileCompleted(bool value) async { + Future saveProfileStatus(bool value) async { await _secureService.setBool('profileCompleted', value); + + _user = UserModel( + userId: _user?.userId, + firstName: _user?.firstName, + accessToken: _user?.accessToken, + refreshToken: _user?.refreshToken, + profileImage: _user?.profileImage, + profileCompleted: await _secureService.getBool('profileCompleted')); + } + + Future saveProfileImage() async {} + + Future saveFullName(Map data) async { + await _secureService.setBool('profileCompleted', true); + await _secureService.setString('firstName', data['firstName']); + _user = UserModel( + userId: _user?.userId, + accessToken: _user?.accessToken, + refreshToken: _user?.refreshToken, + profileImage: _user?.profileImage, + firstName: await _secureService.getString('firstName'), + profileCompleted: await _secureService.getBool('profileCompleted'), + ); } Future isFirstTimeInstall() async => @@ -45,14 +91,16 @@ class AuthenticationService { await _secureService.setBool('firstTimeInstall', value); } - Future getUser() async { - UserModel user = UserModel( + Future getUser() async { + _user = UserModel( userId: await _secureService.getInt('userId'), + firstName: await _secureService.getString('firstName'), accessToken: await _secureService.getString('accessToken'), refreshToken: await _secureService.getString('refreshToken'), + profileImage: await _secureService.getString('profileImage'), profileCompleted: await _secureService.getBool('profileCompleted'), ); - return user; + return _user; } Future logOut() async { diff --git a/lib/services/dio_service.dart b/lib/services/dio_service.dart index 94fbe33..3760b28 100644 --- a/lib/services/dio_service.dart +++ b/lib/services/dio_service.dart @@ -125,18 +125,17 @@ class DioService { } Future _refreshToken() async { - final UserModel user = await _authenticationService.getUser(); + final UserModel? user = await _authenticationService.getUser(); - if (user.refreshToken == null) return false; + if (user?.refreshToken == null) return false; try { Map data = { 'role': 'STUDENT', - 'user_id': user.userId, - 'access_token': user.accessToken, - 'refresh_token': user.refreshToken + 'user_id': user?.userId, + 'access_token': user?.accessToken, + 'refresh_token': user?.refreshToken }; - print(data); final response = await _refreshDio.post( '$baseUrl/$kRefreshTokenUrl', data: data, @@ -150,8 +149,6 @@ class DioService { ), ); - print('Refresh response'); - print(response.data); await _authenticationService.saveTokens( access: response.data['access_token'], refresh: response.data['refresh_token'], @@ -159,10 +156,8 @@ class DioService { return true; } catch (e) { - print('Refresh response exception'); - print(e.toString()); - // await _authenticationService.logOut(); - // await _navigationService.replaceWithLoginView(); + await _authenticationService.logOut(); + await _navigationService.replaceWithLoginView(); return false; } } diff --git a/lib/services/image_picker_service.dart b/lib/services/image_picker_service.dart new file mode 100644 index 0000000..058cde1 --- /dev/null +++ b/lib/services/image_picker_service.dart @@ -0,0 +1 @@ +class ImagePickerService {} diff --git a/lib/ui/common/app_constants.dart b/lib/ui/common/app_constants.dart index 1223651..1e5af9a 100644 --- a/lib/ui/common/app_constants.dart +++ b/lib/ui/common/app_constants.dart @@ -1,7 +1,9 @@ String baseUrl = 'http://195.35.29.82:8080'; //String baseUrl = 'https://api.yimaru.yaltopia.com'; -String userUrl = 'api/v1/user'; +String kGetUserUrl = 'single'; + +String kUserUrl = 'api/v1/user'; String kRegisterUrl = 'register'; diff --git a/lib/ui/common/ui_helpers.dart b/lib/ui/common/ui_helpers.dart index f4f72f9..1e6b738 100644 --- a/lib/ui/common/ui_helpers.dart +++ b/lib/ui/common/ui_helpers.dart @@ -171,15 +171,9 @@ PinTheme errorPinTheme = defaultPin.copyBorderWith( border: Border.all(color: Colors.red), ); -TextStyle validationStyle = const TextStyle( - fontSize: 12, - color: Colors.red, - fontWeight: FontWeight.w700, -); - -TextStyle style25DG600 = const TextStyle( - fontSize: 25, - color: kcDarkGrey, +TextStyle style18P600 = const TextStyle( + fontSize: 18, + color: kcPrimaryColor, fontWeight: FontWeight.w600, ); @@ -189,6 +183,21 @@ TextStyle style12R700 = const TextStyle( fontWeight: FontWeight.w700, ); +TextStyle style14P400 = const TextStyle( + color: kcPrimaryColor, +); + +TextStyle style14P600 = const TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, +); + +TextStyle style25DG600 = const TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, +); + TextStyle style16DG600 = const TextStyle( fontSize: 16, color: kcDarkGrey, @@ -210,13 +219,15 @@ TextStyle style14DG400 = const TextStyle( color: kcDarkGrey, ); -TextStyle style14P400 = const TextStyle( - color: kcPrimaryColor, +TextStyle style14DG600 = const TextStyle( + color: kcDarkGrey, + fontWeight: FontWeight.w600, ); -TextStyle style14P600 = const TextStyle( - color: kcPrimaryColor, - fontWeight: FontWeight.w600, +TextStyle validationStyle = const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, ); Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16)); diff --git a/lib/ui/views/assessment/assessment_viewmodel.dart b/lib/ui/views/assessment/assessment_viewmodel.dart index 4506612..5ca21d9 100644 --- a/lib/ui/views/assessment/assessment_viewmodel.dart +++ b/lib/ui/views/assessment/assessment_viewmodel.dart @@ -5,16 +5,20 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; +import '../../../app/app.dialogs.dart'; import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; import '../../../models/assessment.dart'; import '../../../models/user_model.dart'; import '../../../services/api_service.dart'; import '../../../services/authentication_service.dart'; +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; import '../home/home_view.dart'; class AssessmentViewModel extends BaseViewModel { final _apiService = locator(); + final _dialogService = locator(); final _navigationService = locator(); final _authenticationService = locator(); @@ -73,9 +77,6 @@ class AssessmentViewModel extends BaseViewModel { if (_currentQuestion == 5) { // A1 final correctCount = countCorrectAnswersUntil(5); - print('All : $_selectedAnswers'); - print('Question page : $_currentQuestion'); - print('Correct A1: $correctCount'); if (correctCount > 3) { return {'continue': true, 'level': ProficiencyLevels.a1}; @@ -86,9 +87,6 @@ class AssessmentViewModel extends BaseViewModel { // A2 final correctCount = countCorrectAnswersUntil(10); - print('All : $_selectedAnswers'); - print('Question page : $_currentQuestion'); - print('Correct A2: $correctCount'); if (correctCount > 3) { return {'continue': true, 'level': ProficiencyLevels.a2}; @@ -98,9 +96,6 @@ class AssessmentViewModel extends BaseViewModel { } else if (_currentQuestion == 16) { // B1 final correctCount = countCorrectAnswersUntil(16); - print('All : $_selectedAnswers'); - print('Question page : $_currentQuestion'); - print('Correct B1: $correctCount'); if (correctCount > 4) { return {'continue': true, 'level': ProficiencyLevels.b1}; @@ -109,9 +104,6 @@ class AssessmentViewModel extends BaseViewModel { } } else if (_currentQuestion == 22) { final correctCount = countCorrectAnswersUntil(16); - print('All : $_selectedAnswers'); - print('Question page : $_currentQuestion'); - print('Correct B2: $correctCount'); if (correctCount > 4) { return {'continue': true, 'level': ProficiencyLevels.b2}; @@ -178,17 +170,26 @@ class AssessmentViewModel extends BaseViewModel { } // Complete profile - Future completeProfile() async { - Map response = - await runBusyFuture>(_completeProfile()); + + Future saveProfileCompleted() async { + Map data = {'firstName': _userData['firstName']}; + await _authenticationService.saveFullName(data); } + Future completeProfile() async => + await runBusyFuture>(_completeProfile()); + Future> _completeProfile() async { - print(_userData); - UserModel user = await _authenticationService.getUser(); + UserModel? user = await _authenticationService.getUser(); Map response = await _apiService.updateProfile(data: _userData, user: user); - + if (response['status'] == ResponseStatus.success) { + showSuccessToast(response['message']); + await saveProfileCompleted(); + await replaceWithHome(); + } else { + showErrorToast(response['message']); + } return response; } @@ -203,8 +204,7 @@ class AssessmentViewModel extends BaseViewModel { } else { if (response['continue']) { _pageController.jumpToPage(_currentQuestion); - } - { + } else { _proficiencyLevel = response['level']; next(); } @@ -218,8 +218,6 @@ class AssessmentViewModel extends BaseViewModel { _pageController.previousPage( duration: const Duration(microseconds: 100), curve: Curves.linear); rebuildUi(); - } else { - _navigationService.back(); } } @@ -238,12 +236,34 @@ class AssessmentViewModel extends BaseViewModel { } void pop() { - if (_currentPage != 0) { + if (_currentPage == 3 /*7*/) { + _navigationService.back(); + } else if (_currentPage != 0 && _currentPage != 3) { _currentPage--; rebuildUi(); } } + Future showAbortDialog() async { + DialogResponse? response = await _dialogService.showDialog( + cancelTitle: 'No', + buttonTitle: 'Yes', + barrierDismissible: true, + title: 'Abort Assessment', + cancelTitleColor: kcDarkGrey, + buttonTitleColor: kcPrimaryColor, + description: 'Are you sure to abort the assessment ?', + ); + return response?.confirmed; + } + + Future abort() async { + bool? response = await showAbortDialog(); + if (response != null && response) { + next(page: 3); + } + } + Future navigateToLanguage() async => await _navigationService.navigateToLanguageView(); diff --git a/lib/ui/views/assessment/screens/assessment_form_screen.dart b/lib/ui/views/assessment/screens/assessment_form_screen.dart index aa88c8f..6fc8965 100644 --- a/lib/ui/views/assessment/screens/assessment_form_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_form_screen.dart @@ -45,9 +45,10 @@ class AssessmentFormScreen extends ViewModelWidget { [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( - showBackButton: true, + onClose: viewModel.abort, showLanguageSelection: false, onPop: viewModel.previousQuestion, + showBackButton: viewModel.currentQuestion == 0 ? false : true, ); Widget _buildExpandedBody(AssessmentViewModel viewModel) => diff --git a/lib/ui/views/assessment/screens/assessment_intro_screen.dart b/lib/ui/views/assessment/screens/assessment_intro_screen.dart index c93fe2e..6052440 100644 --- a/lib/ui/views/assessment/screens/assessment_intro_screen.dart +++ b/lib/ui/views/assessment/screens/assessment_intro_screen.dart @@ -58,7 +58,8 @@ class AssessmentIntroScreen extends ViewModelWidget { ]; Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( - showBackButton: false, + showBackButton: true, + onPop: viewModel.pop, showLanguageSelection: true, onLanguage: () async => await viewModel.navigateToLanguage(), ); diff --git a/lib/ui/views/assessment/screens/start_lesson_screen.dart b/lib/ui/views/assessment/screens/start_lesson_screen.dart index 719f74d..e8b89ff 100644 --- a/lib/ui/views/assessment/screens/start_lesson_screen.dart +++ b/lib/ui/views/assessment/screens/start_lesson_screen.dart @@ -7,6 +7,7 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../../../common/enmus.dart'; +import '../../../widgets/page_loading_indicator.dart'; import '../assessment_viewmodel.dart'; class StartLessonScreen extends ViewModelWidget { @@ -15,13 +16,11 @@ class StartLessonScreen extends ViewModelWidget { Future _start(AssessmentViewModel viewModel) async { if (viewModel.proficiencyLevel != ProficiencyLevels.none) { Map data = { - 'preferred_language': 'en', 'knowledge_level': viewModel.proficiencyLevel.name.toUpperCase() }; viewModel.addUserData(data); } - await viewModel.completeProfile(); } @@ -31,20 +30,24 @@ class StartLessonScreen extends ViewModelWidget { Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold( backgroundColor: kcBackgroundColor, - body: _buildScaffold(viewModel), + body: _buildScaffoldStack(viewModel), ); + Widget _buildScaffoldStack(AssessmentViewModel viewModel) => + Stack(children: [_buildScaffold(viewModel), _buildState(viewModel)]); + Widget _buildScaffold(AssessmentViewModel viewModel) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: _buildScaffoldChildren(viewModel), ); List _buildScaffoldChildren(AssessmentViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const LargeAppBar( - showBackButton: false, - showLanguageSelection: false, + Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.pop, + showLanguageSelection: true, ); Widget _buildExpandedBody(AssessmentViewModel viewModel) => @@ -114,10 +117,13 @@ class StartLessonScreen extends ViewModelWidget { Widget _buildContinueButton(AssessmentViewModel viewModel) => CustomElevatedButton( height: 55, + text: 'Finish', borderRadius: 12, - text: 'Go to My Lessons', foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, onTap: () async => await _start(viewModel), ); + + Widget _buildState(AssessmentViewModel viewModel) => + viewModel.isBusy ? const PageLoadingIndicator() : Container(); } diff --git a/lib/ui/views/failure/failure_view.dart b/lib/ui/views/failure/failure_view.dart new file mode 100644 index 0000000..e7c9c7a --- /dev/null +++ b/lib/ui/views/failure/failure_view.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stacked/stacked.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_circular_progress_indicator.dart'; +import 'failure_viewmodel.dart'; + +class FailureView extends StackedView { + final String label; + const FailureView({Key? key, required this.label}) : super(key: key); + + @override + FailureViewModel viewModelBuilder(BuildContext context) => FailureViewModel(); + + @override + Widget builder( + BuildContext context, + FailureViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(); + + Widget _buildScaffoldWrapper() => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(), + ); + + Widget _buildScaffold() => Stack( + children: _buildScaffoldChildren(), + ); + + List _buildScaffoldChildren() => [ + _buildBackground(), + _buildColumn(), + ]; + + Widget _buildBackground() => Image.asset( + 'assets/images/onboarding_1.png', + fit: BoxFit.fill, + width: double.maxFinite, + height: double.maxFinite, + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildUpperColumnChildren(), + ); + + List _buildUpperColumnChildren() => + [_buildIconWrapper(), _buildSafeWrapper()]; + + Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer()); + + Widget _buildLoadingTextContainer() => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildLoadingTextWrapper(), + ); + + Widget _buildLoadingTextWrapper() => Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: _buildLoadingTextChildren(), + ); + + List _buildLoadingTextChildren() => [ + _buildLoadingText(), + horizontalSpaceSmall, + _buildIndicatorWrapper(), + ]; + + Widget _buildLoadingText() => + Text('$label ...', style: const TextStyle(color: kcWhite, fontSize: 16)); + + Widget _buildIndicatorWrapper() => SizedBox( + width: 16, + height: 16, + child: _buildIndicator(), + ); + + Widget _buildIndicator() => + const CustomCircularProgressIndicator(color: kcWhite); + + Widget _buildIconWrapper() => Padding( + padding: const EdgeInsets.only(top: 100), + child: _buildIcon(), + ); + + Widget _buildIcon() => SvgPicture.asset( + 'assets/icons/logo.svg', + height: 50, + ); +} diff --git a/lib/ui/views/failure/failure_viewmodel.dart b/lib/ui/views/failure/failure_viewmodel.dart new file mode 100644 index 0000000..b361d69 --- /dev/null +++ b/lib/ui/views/failure/failure_viewmodel.dart @@ -0,0 +1,3 @@ +import 'package:stacked/stacked.dart'; + +class FailureViewModel extends BaseViewModel {} diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index 8ed1ff8..ecf2308 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -15,8 +15,9 @@ class HomeView extends StackedView { HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel(); @override - void onViewModelReady(HomeViewModel viewModel) { - viewModel.getProfileStatus(); + void onViewModelReady(HomeViewModel viewModel) async { + await viewModel.getProfileStatus(); + await viewModel.getProfileData(); super.onViewModelReady(viewModel); } diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index 10002ba..bd43cdb 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -7,6 +7,10 @@ import 'package:yimaru_app/services/status_checker_service.dart'; import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/failure/failure_view.dart'; +import 'package:yimaru_app/ui/views/login/login_view.dart'; +import 'package:yimaru_app/ui/views/startup/startup_view.dart'; import '../../../services/api_service.dart'; import '../../../services/authentication_service.dart'; @@ -46,28 +50,69 @@ class HomeViewModel extends BaseViewModel { ); } + Future saveFullName(String name) async { + Map data = { + 'firstName': name, + }; + await _authenticationService.saveFullName(data); + } + + Future saveProfileStatus(bool value) async => + await _authenticationService.saveProfileStatus(value); + // Navigation + Future replaceWithFailure() async => + await _navigationService.clearStackAndShowView( + const FailureView(label: 'Check your internet connection to proceed'), + ); + Future replaceWithOnboarding() async => await _navigationService.replaceWithOnboardingView(); // Remote api calls + Future getProfileData() async => + await runBusyFuture>(_getProfileData()); + + Future> _getProfileData() async { + Map response = {}; + + UserModel? user = await _authenticationService.getUser(); + + if (user?.profileCompleted != null) { + if (await _statusChecker.checkConnection()) { + response = await _apiService.getProfileData(user?.userId); + if (response['status'] == ResponseStatus.success) { + Map data = { + 'firstName': response['data']['first_name'] + }; + await _authenticationService.saveFullName(data); + } + } + } + + return response; + } + Future getProfileStatus() async { Map response = await runBusyFuture>(_getProfileStatus()); if (response['status'] == ResponseStatus.success && !response['data']) { await replaceWithOnboarding(); + } else if (response['status'] == ResponseStatus.success && + response['data']) { + await saveProfileStatus(response['data']); } } Future> _getProfileStatus() async { Map response = {}; - UserModel user = await _authenticationService.getUser(); - if (user.profileCompleted == null) { + UserModel? user = await _authenticationService.getUser(); + if (user?.profileCompleted == null) { if (await _statusChecker.checkConnection()) { response = await _apiService.getProfileStatus(user); } else { - response = {'data': false, 'status': ResponseStatus.success}; + await replaceWithFailure(); } } else { response = {'data': true, 'status': ResponseStatus.success}; diff --git a/lib/ui/views/learn/learn_view.dart b/lib/ui/views/learn/learn_view.dart index a98b2a9..4805ca8 100644 --- a/lib/ui/views/learn/learn_view.dart +++ b/lib/ui/views/learn/learn_view.dart @@ -38,12 +38,15 @@ class LearnView extends StackedView { Widget _buildColumn(LearnViewModel viewModel) => Column( children: [ verticalSpaceMedium, - _buildAppBar(), + _buildAppBar(viewModel), _buildLevelsColumnWrapper(viewModel) ], ); - Widget _buildAppBar() => const LearnAppBar(); + Widget _buildAppBar(LearnViewModel viewModel) => LearnAppBar( + name: viewModel.user?.firstName, + profileImage: viewModel.user?.profileImage, + ); Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) => Expanded(child: _buildLevelsColumnScrollView(viewModel)); diff --git a/lib/ui/views/learn/learn_viewmodel.dart b/lib/ui/views/learn/learn_viewmodel.dart index 00b418c..83260d1 100644 --- a/lib/ui/views/learn/learn_viewmodel.dart +++ b/lib/ui/views/learn/learn_viewmodel.dart @@ -1,12 +1,19 @@ 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/user_model.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; import 'package:yimaru_app/ui/common/enmus.dart'; import '../../../app/app.locator.dart'; class LearnViewModel extends BaseViewModel { final _navigationService = locator(); + final _authenticationService = locator(); + + late final UserModel? _user = _authenticationService.user; + + UserModel? get user => _user; final List> _learnLevels = [ { diff --git a/lib/ui/views/learn_module/learn_module_viewmodel.dart b/lib/ui/views/learn_module/learn_module_viewmodel.dart index 3a9bcd4..5205cb0 100644 --- a/lib/ui/views/learn_module/learn_module_viewmodel.dart +++ b/lib/ui/views/learn_module/learn_module_viewmodel.dart @@ -37,5 +37,6 @@ class LearnModuleViewModel extends BaseViewModel { void pop() => _navigationService.back(); - Future navigateToLearnLesson() async=> await _navigationService.navigateToLearnLessonView(); + Future navigateToLearnLesson() async => + await _navigationService.navigateToLearnLessonView(); } diff --git a/lib/ui/views/login/login_viewmodel.dart b/lib/ui/views/login/login_viewmodel.dart index a58582d..b41a1ee 100644 --- a/lib/ui/views/login/login_viewmodel.dart +++ b/lib/ui/views/login/login_viewmodel.dart @@ -126,7 +126,7 @@ class LoginViewModel extends FormViewModel { 'refreshToken': user.refreshToken }; - await _authenticationService.saveUserData(data); + await _authenticationService.saveBasicUserData(data); showSuccessToast(response['message']); } else { showErrorToast(response['message']); diff --git a/lib/ui/views/onboarding/onboarding_view.dart b/lib/ui/views/onboarding/onboarding_view.dart index 89a3bff..320a40a 100644 --- a/lib/ui/views/onboarding/onboarding_view.dart +++ b/lib/ui/views/onboarding/onboarding_view.dart @@ -57,7 +57,7 @@ class OnboardingView extends StackedView Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) => PopScope( - canPop: false, + canPop: viewModel.currentPage == 0 ? true : false, onPopInvokedWithResult: (value, data) => viewModel.pop(), child: _buildOnboardingScreens(viewModel)); diff --git a/lib/ui/views/onboarding/onboarding_viewmodel.dart b/lib/ui/views/onboarding/onboarding_viewmodel.dart index 7d144db..069cbb5 100644 --- a/lib/ui/views/onboarding/onboarding_viewmodel.dart +++ b/lib/ui/views/onboarding/onboarding_viewmodel.dart @@ -54,10 +54,13 @@ class OnboardingViewModel extends FormViewModel { // Age group final List _ageGroups = [ - '8-14', - '15-18', - '19-26', - '26+', + 'UNDER_13', + '13_17', + '18_24', + '25_34', + '35_44', + '45_54', + '55_PLUS' ]; List get ageGroups => _ageGroups; @@ -352,7 +355,6 @@ class OnboardingViewModel extends FormViewModel { // Add user data void addUserData(Map data) { _userData.addAll(data); - print('User data : $_userData'); } void clearUserData() { @@ -385,7 +387,7 @@ class OnboardingViewModel extends FormViewModel { } void pop() { - if (_currentPage == 8) { + if (_currentPage == 0) { _navigationService.back(); } else { _currentPage--; diff --git a/lib/ui/views/onboarding/screens/birthday_form_screen.dart b/lib/ui/views/onboarding/screens/birthday_form_screen.dart index d832eb9..270b759 100644 --- a/lib/ui/views/onboarding/screens/birthday_form_screen.dart +++ b/lib/ui/views/onboarding/screens/birthday_form_screen.dart @@ -15,11 +15,7 @@ class BirthdayFormScreen extends ViewModelWidget { Future _next(OnboardingViewModel viewModel) async { FocusManager.instance.primaryFocus?.unfocus(); - Map data = { - 'birth_day': DateFormat('yyyy-MM-dd') - .parseUTC(viewModel.selectedBirthday ?? DateTime.now().toString()) - .toIso8601String() - }; + Map data = {'birth_day': viewModel.selectedBirthday}; viewModel.addUserData(data); viewModel.next(); } diff --git a/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart b/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart index 798eda7..f634218 100644 --- a/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart +++ b/lib/ui/views/onboarding/screens/learning_goal_form_screen.dart @@ -93,13 +93,16 @@ class LearningGoalFormScreen extends ViewModelWidget { onLanguage: () async => await viewModel.navigateToLanguage(), ); - Widget _buildTitle(OnboardingViewModel viewModel) => Text( - 'Hi ${viewModel.userData['first_name']}, Choose your learning goal.', - style: const TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + Widget _buildTitle(OnboardingViewModel viewModel) => Text.rich( + TextSpan( + text: 'Hi ${viewModel.userData['first_name']},', + style: style18P600.copyWith(fontSize: 22), + children: [ + TextSpan( + text: ' Choose your learning goal.', + style: style16DG600.copyWith(fontSize: 22), + ) + ]), ); Widget _buildLearningGoals(OnboardingViewModel viewModel) => ListView.builder( diff --git a/lib/ui/views/onboarding/screens/topic_form_screen.dart b/lib/ui/views/onboarding/screens/topic_form_screen.dart index 3bc0f43..a5390a4 100644 --- a/lib/ui/views/onboarding/screens/topic_form_screen.dart +++ b/lib/ui/views/onboarding/screens/topic_form_screen.dart @@ -17,6 +17,8 @@ class TopicFormScreen extends ViewModelWidget { FocusManager.instance.primaryFocus?.unfocus(); Map data = { + 'profile_completed': true, + 'preferred_language': 'en', 'favoutite_topic': viewModel.selectedTopic ?? topicController.text, }; viewModel.addUserData(data); diff --git a/lib/ui/views/profile/profile_view.dart b/lib/ui/views/profile/profile_view.dart index fc79f97..909bd3c 100644 --- a/lib/ui/views/profile/profile_view.dart +++ b/lib/ui/views/profile/profile_view.dart @@ -47,7 +47,7 @@ class ProfileView extends StackedView { children: [ verticalSpaceMedium, _buildNotificationIconWrapper(), - _buildProfileSection(), + _buildProfileSection(viewModel), verticalSpaceSmall, _buildViewProfileButton(viewModel), verticalSpaceLarge, @@ -66,27 +66,25 @@ class ProfileView extends StackedView { color: kcDarkGrey, ); - Widget _buildProfileSection() => Column( + Widget _buildProfileSection(ProfileViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, - children: _buildProfileSectionChildren(), + children: _buildProfileSectionChildren(viewModel), ); - List _buildProfileSectionChildren() => [ - _buildProfileImage(), + List _buildProfileSectionChildren(ProfileViewModel viewModel) => [ + _buildProfileImage(viewModel), verticalSpaceSmall, - _buildProfileName(), + _buildProfileName(viewModel), ]; - Widget _buildProfileImage() => const ProfileImage(); + Widget _buildProfileImage(ProfileViewModel viewModel) => ProfileImage( + profileImage: viewModel.user?.profileImage, + ); - Widget _buildProfileName() => const Text( - 'Hi, Bisrat ๐Ÿ‘‹', - style: TextStyle( - fontSize: 25, - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), + Widget _buildProfileName(ProfileViewModel viewModel) => Text( + 'Hi, ${viewModel.user?.firstName ?? ''} ๐Ÿ‘‹', + style: style25DG600, ); Widget _buildViewProfileButton(ProfileViewModel viewModel) => diff --git a/lib/ui/views/profile/profile_viewmodel.dart b/lib/ui/views/profile/profile_viewmodel.dart index ec35e68..72f037b 100644 --- a/lib/ui/views/profile/profile_viewmodel.dart +++ b/lib/ui/views/profile/profile_viewmodel.dart @@ -3,6 +3,7 @@ import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; import '../../../app/app.locator.dart'; +import '../../../models/user_model.dart'; import '../../../services/authentication_service.dart'; class ProfileViewModel extends BaseViewModel { @@ -10,6 +11,10 @@ class ProfileViewModel extends BaseViewModel { final _authenticationService = locator(); + late final UserModel? _user = _authenticationService.user; + + UserModel? get user => _user; + Future logOut() async { await _authenticationService.logOut(); await _navigationService.replaceWithLoginView(); diff --git a/lib/ui/views/profile_detail/profile_detail_view.dart b/lib/ui/views/profile_detail/profile_detail_view.dart index 9fe7573..0f2ad5d 100644 --- a/lib/ui/views/profile_detail/profile_detail_view.dart +++ b/lib/ui/views/profile_detail/profile_detail_view.dart @@ -101,7 +101,7 @@ class ProfileDetailView extends StackedView List _buildColumnChildren(ProfileDetailViewModel viewModel) => [ verticalSpaceMedium, - _buildProfileImage(), + _buildProfileImageWrapper(viewModel), verticalSpaceMedium, _buildNameFormSection(viewModel), verticalSpaceMedium, @@ -120,9 +120,12 @@ class ProfileDetailView extends StackedView _buildLowerColumn(viewModel) ]; - Widget _buildProfileImage() => - const Align(alignment: Alignment.center, child: ProfileImage()); + Widget _buildProfileImageWrapper(ProfileDetailViewModel viewModel) => + Align(alignment: Alignment.center, child: _buildProfileImage(viewModel)); + Widget _buildProfileImage(ProfileDetailViewModel viewModel) => ProfileImage( + profileImage: viewModel.user?.profileImage, + ); Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row( crossAxisAlignment: CrossAxisAlignment.start, children: _buildNameFormChildren(viewModel), diff --git a/lib/ui/views/profile_detail/profile_detail_viewmodel.dart b/lib/ui/views/profile_detail/profile_detail_viewmodel.dart index 16769e6..a98fa4d 100644 --- a/lib/ui/views/profile_detail/profile_detail_viewmodel.dart +++ b/lib/ui/views/profile_detail/profile_detail_viewmodel.dart @@ -2,9 +2,18 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import '../../../app/app.locator.dart'; +import '../../../models/user_model.dart'; +import '../../../services/authentication_service.dart'; class ProfileDetailViewModel extends FormViewModel { final _navigationService = locator(); + + final _authenticationService = locator(); + + late final UserModel? _user = _authenticationService.user; + + UserModel? get user => _user; + // First name bool _focusFirstName = false; diff --git a/lib/ui/views/register/register_viewmodel.dart b/lib/ui/views/register/register_viewmodel.dart index decc358..c25e2ef 100644 --- a/lib/ui/views/register/register_viewmodel.dart +++ b/lib/ui/views/register/register_viewmodel.dart @@ -246,7 +246,7 @@ class RegisterViewModel extends FormViewModel { // 'refreshToken': 'refreshToken' // } - await _authenticationService.saveUserData(data); + await _authenticationService.saveBasicUserData(data); showSuccessToast(response['message']); } else { showErrorToast(response['message']); diff --git a/lib/ui/widgets/large_app_bar.dart b/lib/ui/widgets/large_app_bar.dart index d3dc9c4..c395baf 100644 --- a/lib/ui/widgets/large_app_bar.dart +++ b/lib/ui/widgets/large_app_bar.dart @@ -6,11 +6,13 @@ class LargeAppBar extends StatelessWidget { final bool showBackButton; final GestureTapCallback? onPop; final bool showLanguageSelection; + final GestureTapCallback? onClose; final GestureTapCallback? onLanguage; const LargeAppBar( {super.key, this.onPop, + this.onClose, this.onLanguage, required this.showBackButton, required this.showLanguageSelection}); @@ -53,9 +55,9 @@ class LargeAppBar extends StatelessWidget { Widget _buildRightButton() => Align( alignment: Alignment.bottomRight, - child: showLanguageSelection ? _buildLanguageSelector() : Container() - // _buildCloseButton() - ); + child: showLanguageSelection + ? _buildLanguageSelector() + : _buildCloseButton()); Widget _buildLanguageSelector() => LanguageButton( language: 'EN', @@ -63,8 +65,9 @@ class LargeAppBar extends StatelessWidget { ); Widget _buildCloseButton() => IconButton( - onPressed: () {}, + onPressed: onClose, icon: _buildCloseIcon(), + padding: const EdgeInsets.only(top: 5), ); Widget _buildCloseIcon() => const Icon( diff --git a/lib/ui/widgets/learn_app_bar.dart b/lib/ui/widgets/learn_app_bar.dart index e3e331c..252cb63 100644 --- a/lib/ui/widgets/learn_app_bar.dart +++ b/lib/ui/widgets/learn_app_bar.dart @@ -1,10 +1,15 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import '../common/app_colors.dart'; class LearnAppBar extends StatelessWidget { - const LearnAppBar({super.key}); + final String? name; + final String? profileImage; + + const LearnAppBar( + {super.key, required this.name, required this.profileImage}); @override Widget build(BuildContext context) => _buildStack(); @@ -30,9 +35,22 @@ class LearnAppBar extends StatelessWidget { List _buildProfileRowChildren() => [_buildProfileImage(), horizontalSpaceSmall, _buildGreetingTextColumn()]; - Widget _buildProfileImage() => const CircleAvatar( + Widget _buildProfileImage() => CircleAvatar( radius: 25, - backgroundImage: AssetImage('assets/images/profile.png'), + backgroundColor: kcPrimaryColor, + backgroundImage: profileImage != null + ? CachedNetworkImageProvider(profileImage!) + : null, + child: _buildImageBuilder(), + ); + + Widget? _buildImageBuilder() => + profileImage == null ? _buildPersonIcon() : null; + + Widget _buildPersonIcon() => const Icon( + Icons.person, + size: 30, + color: kcWhite, ); Widget _buildGreetingTextColumn() => Column( @@ -44,28 +62,19 @@ class LearnAppBar extends StatelessWidget { List _buildGreetingChildren() => [_buildGreetingTitle(), _buildSubTitle()]; - Widget _buildGreetingTitle() => const Text.rich( - TextSpan( - text: 'Hello,', - style: TextStyle( - color: kcDarkGrey, - fontWeight: FontWeight.w600, - ), - children: [ - TextSpan( - text: ' Bisrat!', - style: TextStyle( - color: kcPrimaryColor, - fontWeight: FontWeight.w600, - ), - ) - ]), + Widget _buildGreetingTitle() => Text.rich( + TextSpan(text: 'Hello,', style: style14DG600, children: [ + TextSpan( + text: ' $name!', + style: style14P600, + ) + ]), ); - Widget _buildSubTitle() => const Text( + Widget _buildSubTitle() => Text( 'Ready to keep learning English today?', textAlign: TextAlign.center, - style: TextStyle(color: kcMediumGrey), + style: style14DG400, ); Widget _buildNotificationIconWrapper() => diff --git a/lib/ui/widgets/learn_module_tile.dart b/lib/ui/widgets/learn_module_tile.dart index 4160154..478e261 100644 --- a/lib/ui/widgets/learn_module_tile.dart +++ b/lib/ui/widgets/learn_module_tile.dart @@ -192,7 +192,7 @@ class LearnModuleTile extends ViewModelWidget { ); Widget _buildLessonButton(LearnModuleViewModel viewModel) => - CustomElevatedButton( + CustomElevatedButton( height: 15, borderRadius: 12, text: 'View Lessons', diff --git a/lib/ui/widgets/profile_image.dart b/lib/ui/widgets/profile_image.dart index 42f61f3..7453547 100644 --- a/lib/ui/widgets/profile_image.dart +++ b/lib/ui/widgets/profile_image.dart @@ -1,8 +1,10 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; class ProfileImage extends StatelessWidget { - const ProfileImage({super.key}); + final String? profileImage; + const ProfileImage({super.key, required this.profileImage}); @override Widget build(BuildContext context) => _buildSizedBox(); @@ -22,9 +24,22 @@ class ProfileImage extends StatelessWidget { child: _buildProfileImage(), ); - Widget _buildProfileImage() => const CircleAvatar( + Widget _buildProfileImage() => CircleAvatar( radius: 50, - backgroundImage: AssetImage('assets/images/profile.png'), + backgroundColor: kcPrimaryColor, + backgroundImage: profileImage != null + ? CachedNetworkImageProvider(profileImage!) + : null, + child: _buildImageBuilder(), + ); + + Widget? _buildImageBuilder() => + profileImage == null ? _buildPersonIcon() : null; + + Widget _buildPersonIcon() => const Icon( + Icons.person, + size: 50, + color: kcWhite, ); Widget _buildCameraButtonWrapper() => Align( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a1d1dac..a9f61f7 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,9 +8,11 @@ import Foundation import battery_plus import connectivity_plus import flutter_secure_storage_darwin +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index cfad602..29c3534 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.3" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -318,6 +342,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_html: dependency: "direct main" description: @@ -704,6 +736,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.2.3" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" omni_datetime_picker: dependency: "direct main" description: @@ -917,6 +957,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -989,6 +1069,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8bb6287..8f9fb5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,8 +27,10 @@ dependencies: stacked_services: ^1.1.0 omni_datetime_picker: any json_serializable: ^6.8.0 + cached_network_image: ^3.4.1 flutter_secure_storage: ^10.0.0 flutter_timer_countdown: ^1.0.7 + internet_connection_checker_plus: ^2.9.1+2 dev_dependencies: diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart deleted file mode 100644 index df3b057..0000000 --- a/test/helpers/test_helpers.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:yimaru_app/app/app.locator.dart'; -import 'package:stacked_services/stacked_services.dart'; -// @stacked-import - -import 'test_helpers.mocks.dart'; - -@GenerateMocks( - [], - customMocks: [ - MockSpec(onMissingStub: OnMissingStub.returnDefault), - MockSpec(onMissingStub: OnMissingStub.returnDefault), - MockSpec(onMissingStub: OnMissingStub.returnDefault), - // @stacked-mock-spec - ], -) -void registerServices() { - getAndRegisterNavigationService(); - getAndRegisterBottomSheetService(); - getAndRegisterDialogService(); - // @stacked-mock-register -} - -MockNavigationService getAndRegisterNavigationService() { - _removeRegistrationIfExists(); - final service = MockNavigationService(); - locator.registerSingleton(service); - return service; -} - -MockBottomSheetService getAndRegisterBottomSheetService({ - SheetResponse? showCustomSheetResponse, -}) { - _removeRegistrationIfExists(); - final service = MockBottomSheetService(); - - when( - service.showCustomSheet( - enableDrag: anyNamed('enableDrag'), - enterBottomSheetDuration: anyNamed('enterBottomSheetDuration'), - exitBottomSheetDuration: anyNamed('exitBottomSheetDuration'), - ignoreSafeArea: anyNamed('ignoreSafeArea'), - isScrollControlled: anyNamed('isScrollControlled'), - barrierDismissible: anyNamed('barrierDismissible'), - additionalButtonTitle: anyNamed('additionalButtonTitle'), - variant: anyNamed('variant'), - title: anyNamed('title'), - hasImage: anyNamed('hasImage'), - imageUrl: anyNamed('imageUrl'), - showIconInMainButton: anyNamed('showIconInMainButton'), - mainButtonTitle: anyNamed('mainButtonTitle'), - showIconInSecondaryButton: anyNamed('showIconInSecondaryButton'), - secondaryButtonTitle: anyNamed('secondaryButtonTitle'), - showIconInAdditionalButton: anyNamed('showIconInAdditionalButton'), - takesInput: anyNamed('takesInput'), - barrierColor: anyNamed('barrierColor'), - barrierLabel: anyNamed('barrierLabel'), - customData: anyNamed('customData'), - data: anyNamed('data'), - description: anyNamed('description'), - ), - ).thenAnswer( - (realInvocation) => - Future.value(showCustomSheetResponse ?? SheetResponse()), - ); - - locator.registerSingleton(service); - return service; -} - -MockDialogService getAndRegisterDialogService() { - _removeRegistrationIfExists(); - final service = MockDialogService(); - locator.registerSingleton(service); - return service; -} - -// @stacked-mock-create - -void _removeRegistrationIfExists() { - if (locator.isRegistered()) { - locator.unregister(); - } -} diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart deleted file mode 100644 index 651298f..0000000 --- a/test/helpers/test_helpers.mocks.dart +++ /dev/null @@ -1,684 +0,0 @@ -// Mocks generated by Mockito 5.4.4 from annotations -// in yimaru_app/test/helpers/test_helpers.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:ui' as _i6; - -import 'package:flutter/material.dart' as _i4; -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i3; -import 'package:stacked_services/stacked_services.dart' as _i2; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -/// A class which mocks [NavigationService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockNavigationService extends _i1.Mock implements _i2.NavigationService { - @override - String get previousRoute => (super.noSuchMethod( - Invocation.getter(#previousRoute), - returnValue: _i3.dummyValue( - this, - Invocation.getter(#previousRoute), - ), - returnValueForMissingStub: _i3.dummyValue( - this, - Invocation.getter(#previousRoute), - ), - ) as String); - - @override - String get currentRoute => (super.noSuchMethod( - Invocation.getter(#currentRoute), - returnValue: _i3.dummyValue( - this, - Invocation.getter(#currentRoute), - ), - returnValueForMissingStub: _i3.dummyValue( - this, - Invocation.getter(#currentRoute), - ), - ) as String); - - @override - _i4.GlobalKey<_i4.NavigatorState>? nestedNavigationKey(int? index) => - (super.noSuchMethod( - Invocation.method( - #nestedNavigationKey, - [index], - ), - returnValueForMissingStub: null, - ) as _i4.GlobalKey<_i4.NavigatorState>?); - - @override - void config({ - bool? enableLog, - bool? defaultPopGesture, - bool? defaultOpaqueRoute, - Duration? defaultDurationTransition, - bool? defaultGlobalState, - _i2.Transition? defaultTransitionStyle, - String? defaultTransition, - }) => - super.noSuchMethod( - Invocation.method( - #config, - [], - { - #enableLog: enableLog, - #defaultPopGesture: defaultPopGesture, - #defaultOpaqueRoute: defaultOpaqueRoute, - #defaultDurationTransition: defaultDurationTransition, - #defaultGlobalState: defaultGlobalState, - #defaultTransitionStyle: defaultTransitionStyle, - #defaultTransition: defaultTransition, - }, - ), - returnValueForMissingStub: null, - ); - - @override - _i5.Future? navigateWithTransition( - _i4.Widget? page, { - bool? opaque, - String? transition = r'', - Duration? duration, - bool? popGesture, - int? id, - _i4.Curve? curve, - bool? fullscreenDialog = false, - bool? preventDuplicates = true, - _i2.Transition? transitionClass, - _i2.Transition? transitionStyle, - String? routeName, - }) => - (super.noSuchMethod( - Invocation.method( - #navigateWithTransition, - [page], - { - #opaque: opaque, - #transition: transition, - #duration: duration, - #popGesture: popGesture, - #id: id, - #curve: curve, - #fullscreenDialog: fullscreenDialog, - #preventDuplicates: preventDuplicates, - #transitionClass: transitionClass, - #transitionStyle: transitionStyle, - #routeName: routeName, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? replaceWithTransition( - _i4.Widget? page, { - bool? opaque, - String? transition = r'', - Duration? duration, - bool? popGesture, - int? id, - _i4.Curve? curve, - bool? fullscreenDialog = false, - bool? preventDuplicates = true, - _i2.Transition? transitionClass, - _i2.Transition? transitionStyle, - String? routeName, - }) => - (super.noSuchMethod( - Invocation.method( - #replaceWithTransition, - [page], - { - #opaque: opaque, - #transition: transition, - #duration: duration, - #popGesture: popGesture, - #id: id, - #curve: curve, - #fullscreenDialog: fullscreenDialog, - #preventDuplicates: preventDuplicates, - #transitionClass: transitionClass, - #transitionStyle: transitionStyle, - #routeName: routeName, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - bool back({ - dynamic result, - int? id, - }) => - (super.noSuchMethod( - Invocation.method( - #back, - [], - { - #result: result, - #id: id, - }, - ), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); - - @override - void popUntil( - _i4.RoutePredicate? predicate, { - int? id, - }) => - super.noSuchMethod( - Invocation.method( - #popUntil, - [predicate], - {#id: id}, - ), - returnValueForMissingStub: null, - ); - - @override - void popRepeated(int? popTimes) => super.noSuchMethod( - Invocation.method( - #popRepeated, - [popTimes], - ), - returnValueForMissingStub: null, - ); - - @override - _i5.Future? navigateTo( - String? routeName, { - dynamic arguments, - int? id, - bool? preventDuplicates = true, - Map? parameters, - _i4.RouteTransitionsBuilder? transition, - }) => - (super.noSuchMethod( - Invocation.method( - #navigateTo, - [routeName], - { - #arguments: arguments, - #id: id, - #preventDuplicates: preventDuplicates, - #parameters: parameters, - #transition: transition, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? navigateToView( - _i4.Widget? view, { - dynamic arguments, - int? id, - bool? opaque, - _i4.Curve? curve, - Duration? duration, - bool? fullscreenDialog = false, - bool? popGesture, - bool? preventDuplicates = true, - _i2.Transition? transition, - _i2.Transition? transitionStyle, - }) => - (super.noSuchMethod( - Invocation.method( - #navigateToView, - [view], - { - #arguments: arguments, - #id: id, - #opaque: opaque, - #curve: curve, - #duration: duration, - #fullscreenDialog: fullscreenDialog, - #popGesture: popGesture, - #preventDuplicates: preventDuplicates, - #transition: transition, - #transitionStyle: transitionStyle, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? replaceWith( - String? routeName, { - dynamic arguments, - int? id, - bool? preventDuplicates = true, - Map? parameters, - _i4.RouteTransitionsBuilder? transition, - }) => - (super.noSuchMethod( - Invocation.method( - #replaceWith, - [routeName], - { - #arguments: arguments, - #id: id, - #preventDuplicates: preventDuplicates, - #parameters: parameters, - #transition: transition, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? clearStackAndShow( - String? routeName, { - dynamic arguments, - int? id, - Map? parameters, - }) => - (super.noSuchMethod( - Invocation.method( - #clearStackAndShow, - [routeName], - { - #arguments: arguments, - #id: id, - #parameters: parameters, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? clearStackAndShowView( - _i4.Widget? view, { - dynamic arguments, - int? id, - }) => - (super.noSuchMethod( - Invocation.method( - #clearStackAndShowView, - [view], - { - #arguments: arguments, - #id: id, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? clearTillFirstAndShow( - String? routeName, { - dynamic arguments, - int? id, - bool? preventDuplicates = true, - Map? parameters, - }) => - (super.noSuchMethod( - Invocation.method( - #clearTillFirstAndShow, - [routeName], - { - #arguments: arguments, - #id: id, - #preventDuplicates: preventDuplicates, - #parameters: parameters, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? clearTillFirstAndShowView( - _i4.Widget? view, { - dynamic arguments, - int? id, - }) => - (super.noSuchMethod( - Invocation.method( - #clearTillFirstAndShowView, - [view], - { - #arguments: arguments, - #id: id, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); - - @override - _i5.Future? pushNamedAndRemoveUntil( - String? routeName, { - _i4.RoutePredicate? predicate, - dynamic arguments, - int? id, - }) => - (super.noSuchMethod( - Invocation.method( - #pushNamedAndRemoveUntil, - [routeName], - { - #predicate: predicate, - #arguments: arguments, - #id: id, - }, - ), - returnValueForMissingStub: null, - ) as _i5.Future?); -} - -/// A class which mocks [BottomSheetService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockBottomSheetService extends _i1.Mock - implements _i2.BottomSheetService { - @override - void setCustomSheetBuilders(Map? builders) => - super.noSuchMethod( - Invocation.method( - #setCustomSheetBuilders, - [builders], - ), - returnValueForMissingStub: null, - ); - - @override - _i5.Future<_i2.SheetResponse?> showBottomSheet({ - required String? title, - String? description, - String? confirmButtonTitle = r'Ok', - String? cancelButtonTitle, - bool? enableDrag = true, - bool? barrierDismissible = true, - bool? isScrollControlled = false, - Duration? exitBottomSheetDuration, - Duration? enterBottomSheetDuration, - bool? ignoreSafeArea, - bool? useRootNavigator = false, - double? elevation = 1.0, - }) => - (super.noSuchMethod( - Invocation.method( - #showBottomSheet, - [], - { - #title: title, - #description: description, - #confirmButtonTitle: confirmButtonTitle, - #cancelButtonTitle: cancelButtonTitle, - #enableDrag: enableDrag, - #barrierDismissible: barrierDismissible, - #isScrollControlled: isScrollControlled, - #exitBottomSheetDuration: exitBottomSheetDuration, - #enterBottomSheetDuration: enterBottomSheetDuration, - #ignoreSafeArea: ignoreSafeArea, - #useRootNavigator: useRootNavigator, - #elevation: elevation, - }, - ), - returnValue: _i5.Future<_i2.SheetResponse?>.value(), - returnValueForMissingStub: - _i5.Future<_i2.SheetResponse?>.value(), - ) as _i5.Future<_i2.SheetResponse?>); - - @override - _i5.Future<_i2.SheetResponse?> showCustomSheet({ - dynamic variant, - String? title, - String? description, - bool? hasImage = false, - String? imageUrl, - bool? showIconInMainButton = false, - String? mainButtonTitle, - bool? showIconInSecondaryButton = false, - String? secondaryButtonTitle, - bool? showIconInAdditionalButton = false, - String? additionalButtonTitle, - bool? takesInput = false, - _i6.Color? barrierColor = const _i6.Color(2315255808), - double? elevation = 1.0, - bool? barrierDismissible = true, - bool? isScrollControlled = false, - String? barrierLabel = r'', - dynamic customData, - R? data, - bool? enableDrag = true, - Duration? exitBottomSheetDuration, - Duration? enterBottomSheetDuration, - bool? ignoreSafeArea, - bool? useRootNavigator = false, - }) => - (super.noSuchMethod( - Invocation.method( - #showCustomSheet, - [], - { - #variant: variant, - #title: title, - #description: description, - #hasImage: hasImage, - #imageUrl: imageUrl, - #showIconInMainButton: showIconInMainButton, - #mainButtonTitle: mainButtonTitle, - #showIconInSecondaryButton: showIconInSecondaryButton, - #secondaryButtonTitle: secondaryButtonTitle, - #showIconInAdditionalButton: showIconInAdditionalButton, - #additionalButtonTitle: additionalButtonTitle, - #takesInput: takesInput, - #barrierColor: barrierColor, - #elevation: elevation, - #barrierDismissible: barrierDismissible, - #isScrollControlled: isScrollControlled, - #barrierLabel: barrierLabel, - #customData: customData, - #data: data, - #enableDrag: enableDrag, - #exitBottomSheetDuration: exitBottomSheetDuration, - #enterBottomSheetDuration: enterBottomSheetDuration, - #ignoreSafeArea: ignoreSafeArea, - #useRootNavigator: useRootNavigator, - }, - ), - returnValue: _i5.Future<_i2.SheetResponse?>.value(), - returnValueForMissingStub: _i5.Future<_i2.SheetResponse?>.value(), - ) as _i5.Future<_i2.SheetResponse?>); - - @override - void completeSheet(_i2.SheetResponse? response) => - super.noSuchMethod( - Invocation.method( - #completeSheet, - [response], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [DialogService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockDialogService extends _i1.Mock implements _i2.DialogService { - @override - void registerCustomDialogBuilders( - Map? builders) => - super.noSuchMethod( - Invocation.method( - #registerCustomDialogBuilders, - [builders], - ), - returnValueForMissingStub: null, - ); - - @override - void registerCustomDialogBuilder({ - required dynamic variant, - required _i4.Widget Function( - _i4.BuildContext, - _i2.DialogRequest, - dynamic Function(_i2.DialogResponse), - )? builder, - }) => - super.noSuchMethod( - Invocation.method( - #registerCustomDialogBuilder, - [], - { - #variant: variant, - #builder: builder, - }, - ), - returnValueForMissingStub: null, - ); - - @override - _i5.Future<_i2.DialogResponse?> showDialog({ - String? title, - String? description, - String? cancelTitle, - _i6.Color? cancelTitleColor, - String? buttonTitle = r'Ok', - _i6.Color? buttonTitleColor, - bool? barrierDismissible = false, - _i4.RouteSettings? routeSettings, - _i4.GlobalKey<_i4.NavigatorState>? navigatorKey, - _i2.DialogPlatform? dialogPlatform, - }) => - (super.noSuchMethod( - Invocation.method( - #showDialog, - [], - { - #title: title, - #description: description, - #cancelTitle: cancelTitle, - #cancelTitleColor: cancelTitleColor, - #buttonTitle: buttonTitle, - #buttonTitleColor: buttonTitleColor, - #barrierDismissible: barrierDismissible, - #routeSettings: routeSettings, - #navigatorKey: navigatorKey, - #dialogPlatform: dialogPlatform, - }, - ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), - returnValueForMissingStub: - _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); - - @override - _i5.Future<_i2.DialogResponse?> showCustomDialog({ - dynamic variant, - String? title, - String? description, - bool? hasImage = false, - String? imageUrl, - bool? showIconInMainButton = false, - String? mainButtonTitle, - bool? showIconInSecondaryButton = false, - String? secondaryButtonTitle, - bool? showIconInAdditionalButton = false, - String? additionalButtonTitle, - bool? takesInput = false, - _i6.Color? barrierColor = const _i6.Color(2315255808), - bool? barrierDismissible = false, - String? barrierLabel = r'', - bool? useSafeArea = true, - _i4.RouteSettings? routeSettings, - _i4.GlobalKey<_i4.NavigatorState>? navigatorKey, - _i4.RouteTransitionsBuilder? transitionBuilder, - dynamic customData, - R? data, - }) => - (super.noSuchMethod( - Invocation.method( - #showCustomDialog, - [], - { - #variant: variant, - #title: title, - #description: description, - #hasImage: hasImage, - #imageUrl: imageUrl, - #showIconInMainButton: showIconInMainButton, - #mainButtonTitle: mainButtonTitle, - #showIconInSecondaryButton: showIconInSecondaryButton, - #secondaryButtonTitle: secondaryButtonTitle, - #showIconInAdditionalButton: showIconInAdditionalButton, - #additionalButtonTitle: additionalButtonTitle, - #takesInput: takesInput, - #barrierColor: barrierColor, - #barrierDismissible: barrierDismissible, - #barrierLabel: barrierLabel, - #useSafeArea: useSafeArea, - #routeSettings: routeSettings, - #navigatorKey: navigatorKey, - #transitionBuilder: transitionBuilder, - #customData: customData, - #data: data, - }, - ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), - returnValueForMissingStub: _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); - - @override - _i5.Future<_i2.DialogResponse?> showConfirmationDialog({ - String? title, - String? description, - String? cancelTitle = r'Cancel', - _i6.Color? cancelTitleColor, - String? confirmationTitle = r'Ok', - _i6.Color? confirmationTitleColor, - bool? barrierDismissible = false, - _i4.RouteSettings? routeSettings, - _i2.DialogPlatform? dialogPlatform, - }) => - (super.noSuchMethod( - Invocation.method( - #showConfirmationDialog, - [], - { - #title: title, - #description: description, - #cancelTitle: cancelTitle, - #cancelTitleColor: cancelTitleColor, - #confirmationTitle: confirmationTitle, - #confirmationTitleColor: confirmationTitleColor, - #barrierDismissible: barrierDismissible, - #routeSettings: routeSettings, - #dialogPlatform: dialogPlatform, - }, - ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), - returnValueForMissingStub: - _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); - - @override - void completeDialog(_i2.DialogResponse? response) => - super.noSuchMethod( - Invocation.method( - #completeDialog, - [response], - ), - returnValueForMissingStub: null, - ); -} diff --git a/test/services/image_picker_service_test.dart b/test/services/image_picker_service_test.dart new file mode 100644 index 0000000..7e0b6da --- /dev/null +++ b/test/services/image_picker_service_test.dart @@ -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('ImagePickerServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/failure_viewmodel_test.dart b/test/viewmodels/failure_viewmodel_test.dart new file mode 100644 index 0000000..d06419f --- /dev/null +++ b/test/viewmodels/failure_viewmodel_test.dart @@ -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('FailureViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} From 4ef204f31b8c7e9c89316860b9d12e54f7b0a2ef Mon Sep 17 00:00:00 2001 From: BisratHailu Date: Fri, 23 Jan 2026 23:38:50 +0300 Subject: [PATCH 2/5] feat(image_picker): Add image picker both from gallery and camera --- android/app/src/main/AndroidManifest.xml | 7 + lib/app/app.dart | 2 + lib/app/app.locator.dart | 2 + lib/services/authentication_service.dart | 32 +- lib/services/dio_service.dart | 13 +- lib/services/image_picker_service.dart | 57 +- lib/services/permission_handler_service.dart | 31 + lib/ui/common/enmus.dart | 3 + lib/ui/common/ui_helpers.dart | 6 + .../assessment/assessment_viewmodel.dart | 6 - lib/ui/views/home/home_view.dart | 4 +- lib/ui/views/home/home_viewmodel.dart | 21 +- lib/ui/views/learn/learn_viewmodel.dart | 9 +- lib/ui/views/profile/profile_view.dart | 89 +- lib/ui/views/profile/profile_viewmodel.dart | 39 +- lib/ui/widgets/custom_large_radio_button.dart | 2 +- lib/ui/widgets/image_picker_option.dart | 82 ++ lib/ui/widgets/learn_app_bar.dart | 6 +- lib/ui/widgets/profile_image.dart | 35 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 160 +++ pubspec.yaml | 2 + test/helpers/test_helpers.dart | 155 +++ test/helpers/test_helpers.mocks.dart | 1238 +++++++++++++++++ .../api_service_test.dart} | 2 +- .../authentication_service_test.dart} | 2 +- test/services/dio_service_test.dart | 11 + .../permission_handler_service_test.dart | 11 + .../services/secure_storage_service_test.dart | 11 + .../services/status_checker_service_test.dart | 11 + .../account_privacy_viewmodel_test.dart | 11 + .../viewmodels/assessment_viewmodel_test.dart | 11 + .../call_support_viewmodel_test.dart | 11 + test/viewmodels/downloads_viewmodel_test.dart | 11 + .../full_name_view_viewmodel_test.dart | 11 + test/viewmodels/home_viewmodel_test.dart | 36 + test/viewmodels/language_viewmodel_test.dart | 11 + .../learn_level_viewmodel_test.dart | 11 + .../learn_module_viewmodel_test.dart | 11 + test/viewmodels/learn_viewmodel_test.dart | 11 + test/viewmodels/login_viewmodel_test.dart | 11 + .../viewmodels/onboarding_viewmodel_test.dart | 11 + .../ongoing_progress_viewmodel_test.dart | 11 + .../privacy_policy_viewmodel_test.dart | 11 + .../profile_detail_viewmodel_test.dart | 11 + test/viewmodels/profile_viewmodel_test.dart | 11 + test/viewmodels/progress_viewmodel_test.dart | 11 + test/viewmodels/register_viewmodel_test.dart | 11 + test/viewmodels/settings_viewmodel_test.dart | 11 + test/viewmodels/startup_viewmodel_test.dart | 11 + test/viewmodels/support_viewmodel_test.dart | 11 + .../telegram_support_viewmodel_test.dart | 11 + .../terms_and_conditions_viewmodel_test.dart | 11 + test/viewmodels/welcome_viewmodel_test.dart | 11 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 58 files changed, 2281 insertions(+), 72 deletions(-) create mode 100644 lib/services/permission_handler_service.dart create mode 100644 lib/ui/widgets/image_picker_option.dart create mode 100644 test/helpers/test_helpers.dart create mode 100644 test/helpers/test_helpers.mocks.dart rename test/{viewmodels/failure_viewmodel_test.dart => services/api_service_test.dart} (85%) rename test/{viewmodels/learn_lesson_viewmodel_test.dart => services/authentication_service_test.dart} (83%) create mode 100644 test/services/dio_service_test.dart create mode 100644 test/services/permission_handler_service_test.dart create mode 100644 test/services/secure_storage_service_test.dart create mode 100644 test/services/status_checker_service_test.dart create mode 100644 test/viewmodels/account_privacy_viewmodel_test.dart create mode 100644 test/viewmodels/assessment_viewmodel_test.dart create mode 100644 test/viewmodels/call_support_viewmodel_test.dart create mode 100644 test/viewmodels/downloads_viewmodel_test.dart create mode 100644 test/viewmodels/full_name_view_viewmodel_test.dart create mode 100644 test/viewmodels/home_viewmodel_test.dart create mode 100644 test/viewmodels/language_viewmodel_test.dart create mode 100644 test/viewmodels/learn_level_viewmodel_test.dart create mode 100644 test/viewmodels/learn_module_viewmodel_test.dart create mode 100644 test/viewmodels/learn_viewmodel_test.dart create mode 100644 test/viewmodels/login_viewmodel_test.dart create mode 100644 test/viewmodels/onboarding_viewmodel_test.dart create mode 100644 test/viewmodels/ongoing_progress_viewmodel_test.dart create mode 100644 test/viewmodels/privacy_policy_viewmodel_test.dart create mode 100644 test/viewmodels/profile_detail_viewmodel_test.dart create mode 100644 test/viewmodels/profile_viewmodel_test.dart create mode 100644 test/viewmodels/progress_viewmodel_test.dart create mode 100644 test/viewmodels/register_viewmodel_test.dart create mode 100644 test/viewmodels/settings_viewmodel_test.dart create mode 100644 test/viewmodels/startup_viewmodel_test.dart create mode 100644 test/viewmodels/support_viewmodel_test.dart create mode 100644 test/viewmodels/telegram_support_viewmodel_test.dart create mode 100644 test/viewmodels/terms_and_conditions_viewmodel_test.dart create mode 100644 test/viewmodels/welcome_viewmodel_test.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6561fcc..1dc40a9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,11 @@ + + + + + setupLocator({ locator.registerLazySingleton(() => SecureStorageService()); locator.registerLazySingleton(() => DioService()); locator.registerLazySingleton(() => StatusCheckerService()); + locator.registerLazySingleton(() => PermissionHandlerService()); locator.registerLazySingleton(() => ImagePickerService()); } diff --git a/lib/services/authentication_service.dart b/lib/services/authentication_service.dart index 369e1cc..d2e8693 100644 --- a/lib/services/authentication_service.dart +++ b/lib/services/authentication_service.dart @@ -1,10 +1,15 @@ +import 'package:stacked/stacked.dart'; import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/models/user_model.dart'; import 'package:yimaru_app/services/secure_storage_service.dart'; -class AuthenticationService { +class AuthenticationService with ListenableServiceMixin { final _secureService = locator(); + AuthenticationService() { + listenToReactiveValues([_user]); + } + UserModel? _user; UserModel? get user => _user; @@ -35,11 +40,12 @@ class AuthenticationService { Future saveUserName(Map data) async { await _secureService.setString('firstName', data['firstName']); _user = UserModel( - firstName: await _secureService.getString('firstName'), - userId: _user?.userId, - accessToken: _user?.accessToken, - refreshToken: _user?.refreshToken, - profileCompleted: _user?.profileCompleted); + userId: _user?.userId, + accessToken: _user?.accessToken, + refreshToken: _user?.refreshToken, + profileCompleted: _user?.profileCompleted, + firstName: await _secureService.getString('firstName'), + ); } Future saveBasicUserData(Map data) async { @@ -69,7 +75,19 @@ class AuthenticationService { profileCompleted: await _secureService.getBool('profileCompleted')); } - Future saveProfileImage() async {} + Future saveProfileImage(String image) async { + await _secureService.setString('profileImage', image); + _user = UserModel( + userId: _user?.userId, + firstName: _user?.firstName, + accessToken: _user?.accessToken, + refreshToken: _user?.refreshToken, + profileCompleted: _user?.profileCompleted, + profileImage: await _secureService.getString('profileImage'), + ); + + notifyListeners(); + } Future saveFullName(Map data) async { await _secureService.setBool('profileCompleted', true); diff --git a/lib/services/dio_service.dart b/lib/services/dio_service.dart index 3760b28..a78abb4 100644 --- a/lib/services/dio_service.dart +++ b/lib/services/dio_service.dart @@ -139,14 +139,6 @@ class DioService { final response = await _refreshDio.post( '$baseUrl/$kRefreshTokenUrl', data: data, - options: Options( - followRedirects: false, - validateStatus: (status) => true, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - ), ); await _authenticationService.saveTokens( @@ -156,8 +148,9 @@ class DioService { return true; } catch (e) { - await _authenticationService.logOut(); - await _navigationService.replaceWithLoginView(); + print('Token refresh exception ${e.toString()}'); + // await _authenticationService.logOut(); + // await _navigationService.replaceWithLoginView(); return false; } } diff --git a/lib/services/image_picker_service.dart b/lib/services/image_picker_service.dart index 058cde1..0940be6 100644 --- a/lib/services/image_picker_service.dart +++ b/lib/services/image_picker_service.dart @@ -1 +1,56 @@ -class ImagePickerService {} +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:yimaru_app/services/permission_handler_service.dart'; + +import '../app/app.locator.dart'; +import '../ui/common/ui_helpers.dart'; + +class ImagePickerService { + final _permissionHandler = locator(); + + final ImagePicker _picker = ImagePicker(); + + Future gallery() async { + try { + PermissionStatus status = + await _permissionHandler.requestPermission(Permission.mediaLibrary); + + if (status == PermissionStatus.granted) { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, maxWidth: 600, maxHeight: 600); + + if (pickedFile == null) { + showErrorToast('Please select a picture'); + return null; + } else { + return pickedFile.path; + } + } + return null; + } catch (e) { + return null; + } + } + + Future camera() async { + try { + PermissionStatus status = + await _permissionHandler.requestPermission(Permission.camera); + + if (status == PermissionStatus.granted) { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.camera, maxWidth: 600, maxHeight: 600); + + if (pickedFile == null) { + showErrorToast('Please take a picture'); + return null; + } else { + return pickedFile.path; + } + } + return null; + } catch (e) { + return null; + } + } +} diff --git a/lib/services/permission_handler_service.dart b/lib/services/permission_handler_service.dart new file mode 100644 index 0000000..4322c80 --- /dev/null +++ b/lib/services/permission_handler_service.dart @@ -0,0 +1,31 @@ +import 'package:permission_handler/permission_handler.dart'; + +import '../ui/common/ui_helpers.dart'; + +class PermissionHandlerService { + Future requestPermission( + Permission requestedPermission) async { + if (requestedPermission == Permission.camera) { + return await request(Permission.camera); + } + if (requestedPermission == Permission.storage) { + return await request(Permission.storage); + } + if (requestedPermission == Permission.mediaLibrary) { + return await request(Permission.mediaLibrary); + } + return PermissionStatus.denied; + } + + Future request(Permission permission) async { + if (await permission.isDenied) { + final PermissionStatus status = await permission.request(); + + if (status.isDenied || status.isPermanentlyDenied) { + showErrorToast('Permission Denied'); + } + return status; + } + return PermissionStatus.granted; + } +} diff --git a/lib/ui/common/enmus.dart b/lib/ui/common/enmus.dart index a289b0a..412c299 100644 --- a/lib/ui/common/enmus.dart +++ b/lib/ui/common/enmus.dart @@ -8,3 +8,6 @@ enum ProgressStatuses { pending, started, completed } // Levels enum ProficiencyLevels { a1, a2, b1, b2, none } + +// State object +enum StateObjects{profileImage} diff --git a/lib/ui/common/ui_helpers.dart b/lib/ui/common/ui_helpers.dart index 1e6b738..dcf3812 100644 --- a/lib/ui/common/ui_helpers.dart +++ b/lib/ui/common/ui_helpers.dart @@ -177,6 +177,12 @@ TextStyle style18P600 = const TextStyle( fontWeight: FontWeight.w600, ); +TextStyle style18W600 = const TextStyle( + fontSize: 18, + color: kcWhite, + fontWeight: FontWeight.w600, +); + TextStyle style12R700 = const TextStyle( fontSize: 12, color: Colors.red, diff --git a/lib/ui/views/assessment/assessment_viewmodel.dart b/lib/ui/views/assessment/assessment_viewmodel.dart index 5ca21d9..483cc9c 100644 --- a/lib/ui/views/assessment/assessment_viewmodel.dart +++ b/lib/ui/views/assessment/assessment_viewmodel.dart @@ -171,11 +171,6 @@ class AssessmentViewModel extends BaseViewModel { // Complete profile - Future saveProfileCompleted() async { - Map data = {'firstName': _userData['firstName']}; - await _authenticationService.saveFullName(data); - } - Future completeProfile() async => await runBusyFuture>(_completeProfile()); @@ -185,7 +180,6 @@ class AssessmentViewModel extends BaseViewModel { await _apiService.updateProfile(data: _userData, user: user); if (response['status'] == ResponseStatus.success) { showSuccessToast(response['message']); - await saveProfileCompleted(); await replaceWithHome(); } else { showErrorToast(response['message']); diff --git a/lib/ui/views/home/home_view.dart b/lib/ui/views/home/home_view.dart index ecf2308..ce8a1e9 100644 --- a/lib/ui/views/home/home_view.dart +++ b/lib/ui/views/home/home_view.dart @@ -27,9 +27,7 @@ class HomeView extends StackedView { _buildScaffoldWrapper(viewModel); Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy - ? const StartupView( - label: 'Checking user info', - ) + ? const StartupView(label: 'Checking user info') : _buildScaffold(viewModel); Widget _buildScaffold(HomeViewModel viewModel) => Scaffold( diff --git a/lib/ui/views/home/home_viewmodel.dart b/lib/ui/views/home/home_viewmodel.dart index bd43cdb..d1ad455 100644 --- a/lib/ui/views/home/home_viewmodel.dart +++ b/lib/ui/views/home/home_viewmodel.dart @@ -93,18 +93,10 @@ class HomeViewModel extends BaseViewModel { return response; } - Future getProfileStatus() async { - Map response = - await runBusyFuture>(_getProfileStatus()); - if (response['status'] == ResponseStatus.success && !response['data']) { - await replaceWithOnboarding(); - } else if (response['status'] == ResponseStatus.success && - response['data']) { - await saveProfileStatus(response['data']); - } - } + Future getProfileStatus() async => + await runBusyFuture(_getProfileStatus()); - Future> _getProfileStatus() async { + Future _getProfileStatus() async { Map response = {}; UserModel? user = await _authenticationService.getUser(); @@ -118,6 +110,11 @@ class HomeViewModel extends BaseViewModel { response = {'data': true, 'status': ResponseStatus.success}; } - return response; + if (response['status'] == ResponseStatus.success && !response['data']) { + await replaceWithOnboarding(); + } else if (response['status'] == ResponseStatus.success && + response['data']) { + await saveProfileStatus(response['data']); + } } } diff --git a/lib/ui/views/learn/learn_viewmodel.dart b/lib/ui/views/learn/learn_viewmodel.dart index 83260d1..af91edc 100644 --- a/lib/ui/views/learn/learn_viewmodel.dart +++ b/lib/ui/views/learn/learn_viewmodel.dart @@ -7,13 +7,16 @@ import 'package:yimaru_app/ui/common/enmus.dart'; import '../../../app/app.locator.dart'; -class LearnViewModel extends BaseViewModel { +class LearnViewModel extends ReactiveViewModel { final _navigationService = locator(); final _authenticationService = locator(); - late final UserModel? _user = _authenticationService.user; + @override + List get listenableServices => + [_authenticationService]; - UserModel? get user => _user; + // Current user + UserModel? get user => _authenticationService.user; final List> _learnLevels = [ { diff --git a/lib/ui/views/profile/profile_view.dart b/lib/ui/views/profile/profile_view.dart index 909bd3c..cfaad64 100644 --- a/lib/ui/views/profile/profile_view.dart +++ b/lib/ui/views/profile/profile_view.dart @@ -1,17 +1,37 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart'; import 'package:yimaru_app/ui/widgets/profile_card.dart'; import 'package:yimaru_app/ui/widgets/profile_image.dart'; import 'package:yimaru_app/ui/widgets/view_profile_button.dart'; import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/image_picker_option.dart'; import 'profile_viewmodel.dart'; class ProfileView extends StackedView { const ProfileView({Key? key}) : super(key: key); + Future _showImagePicker( + {required BuildContext context, + required ProfileViewModel viewModel}) async => + await showDialog( + context: context, + builder: (context) => + _showImagePickerDialog(context: context, viewModel: viewModel), + ); + + AlertDialog _showImagePickerDialog( + {required BuildContext context, + required ProfileViewModel viewModel}) => + AlertDialog( + backgroundColor: Colors.transparent, + content: _buildImagePicker(context: context, viewModel: viewModel), + ); + @override ProfileViewModel viewModelBuilder( BuildContext context, @@ -24,30 +44,45 @@ class ProfileView extends StackedView { ProfileViewModel viewModel, Widget? child, ) => - _buildScaffoldWrapper(viewModel); + _buildScaffoldWrapper(context: context, viewModel: viewModel); - Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold( + Widget _buildScaffoldWrapper( + {required BuildContext context, + required ProfileViewModel viewModel}) => + Scaffold( backgroundColor: kcBackgroundColor, - body: _buildScaffold(viewModel), + body: _buildScaffold(context: context, viewModel: viewModel), ); - Widget _buildScaffold(ProfileViewModel viewModel) => - SafeArea(child: _buildBodyWrapper(viewModel)); + Widget _buildScaffold( + {required BuildContext context, + required ProfileViewModel viewModel}) => + SafeArea( + child: _buildBodyWrapper(context: context, viewModel: viewModel)); - Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView( - child: _buildBody(viewModel), + Widget _buildBodyWrapper( + {required BuildContext context, + required ProfileViewModel viewModel}) => + SingleChildScrollView( + child: _buildBody(context: context, viewModel: viewModel), ); - Widget _buildBody(ProfileViewModel viewModel) => Padding( + Widget _buildBody( + {required BuildContext context, + required ProfileViewModel viewModel}) => + Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildColumn(viewModel), + child: _buildColumn(context: context, viewModel: viewModel), ); - Widget _buildColumn(ProfileViewModel viewModel) => Column( + Widget _buildColumn( + {required BuildContext context, + required ProfileViewModel viewModel}) => + Column( children: [ verticalSpaceMedium, _buildNotificationIconWrapper(), - _buildProfileSection(viewModel), + _buildProfileSection(context: context, viewModel: viewModel), verticalSpaceSmall, _buildViewProfileButton(viewModel), verticalSpaceLarge, @@ -66,20 +101,42 @@ class ProfileView extends StackedView { color: kcDarkGrey, ); - Widget _buildProfileSection(ProfileViewModel viewModel) => Column( + Widget _buildProfileSection( + {required BuildContext context, + required ProfileViewModel viewModel}) => + Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, - children: _buildProfileSectionChildren(viewModel), + children: _buildProfileSectionChildren( + context: context, viewModel: viewModel), ); - List _buildProfileSectionChildren(ProfileViewModel viewModel) => [ - _buildProfileImage(viewModel), + List _buildProfileSectionChildren( + {required BuildContext context, + required ProfileViewModel viewModel}) => + [ + _buildProfileImage(context: context, viewModel: viewModel), verticalSpaceSmall, _buildProfileName(viewModel), ]; - Widget _buildProfileImage(ProfileViewModel viewModel) => ProfileImage( + + Widget _buildProfileImage( + {required BuildContext context, + required ProfileViewModel viewModel}) => + ProfileImage( profileImage: viewModel.user?.profileImage, + loading: viewModel.busy(StateObjects.profileImage) ? true:false, + onTap: () async => + await _showImagePicker(context: context, viewModel: viewModel), + ); + + Widget _buildImagePicker( + {required BuildContext context, + required ProfileViewModel viewModel}) => + ImagePickerOption( + onCameraTap: () async => await viewModel.openCamera(), + onGalleryTap: () async => await viewModel.openGallery(), ); Widget _buildProfileName(ProfileViewModel viewModel) => Text( diff --git a/lib/ui/views/profile/profile_viewmodel.dart b/lib/ui/views/profile/profile_viewmodel.dart index 72f037b..b4deac6 100644 --- a/lib/ui/views/profile/profile_viewmodel.dart +++ b/lib/ui/views/profile/profile_viewmodel.dart @@ -1,25 +1,58 @@ 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/ui/common/enmus.dart'; import '../../../app/app.locator.dart'; import '../../../models/user_model.dart'; import '../../../services/authentication_service.dart'; -class ProfileViewModel extends BaseViewModel { +class ProfileViewModel extends ReactiveViewModel { final _navigationService = locator(); + final _imagePickerService = locator(); + final _authenticationService = locator(); - late final UserModel? _user = _authenticationService.user; + @override + List get listenableServices => + [_authenticationService]; - UserModel? get user => _user; + // Current user + UserModel? get user => _authenticationService.user; + // Image picker + Future openCamera() async => runBusyFuture(_openCamera(),busyObject: StateObjects.profileImage); + + Future _openCamera()async{ + String? image = await _imagePickerService.camera(); + if (image != null) { + await _authenticationService.saveProfileImage(image); + } + pop(); + } + + Future openGallery() async => runBusyFuture(_openGallery(),busyObject: StateObjects.profileImage); + + + Future _openGallery() async { + String? image = await _imagePickerService.gallery(); + if (image != null) { + await _authenticationService.saveProfileImage(image); + } + pop(); + } + + // Logout Future logOut() async { await _authenticationService.logOut(); await _navigationService.replaceWithLoginView(); } + // Navigation + void pop() => _navigationService.back(); + Future navigateToProfileDetail() async => await _navigationService.navigateToProfileDetailView(); diff --git a/lib/ui/widgets/custom_large_radio_button.dart b/lib/ui/widgets/custom_large_radio_button.dart index 8a7bbd7..1193070 100644 --- a/lib/ui/widgets/custom_large_radio_button.dart +++ b/lib/ui/widgets/custom_large_radio_button.dart @@ -82,5 +82,5 @@ class CustomLargeRadioButton extends StatelessWidget { Widget _buildSelectedCheckBox() => Checkbox( value: selected, activeColor: kcPrimaryColor, - onChanged: (value) => onTap); + onChanged: onTap != null ? (value) => onTap!() : null); } diff --git a/lib/ui/widgets/image_picker_option.dart b/lib/ui/widgets/image_picker_option.dart new file mode 100644 index 0000000..aaaef04 --- /dev/null +++ b/lib/ui/widgets/image_picker_option.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class ImagePickerOption extends StatelessWidget { + final GestureTapCallback? onCameraTap; + final GestureTapCallback? onGalleryTap; + + const ImagePickerOption({super.key, this.onCameraTap, this.onGalleryTap}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + height: 200, + decoration: const BoxDecoration( + color: kcBackgroundColor, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.all(Radius.circular(32.0)), + ), + child: _buildCameraOptionWrapper(), + ); + + Widget _buildCameraOptionWrapper() => Center( + child: _buildCameraOption(), + ); + + Widget _buildCameraOption() => Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: _buildCameraOptionChildren(), + ); + + List _buildCameraOptionChildren() => + [_buildCameraButton(), _buildGalleryButton()]; + + Widget _buildCameraButton() => GestureDetector( + onTap: onCameraTap, + child: _buildCamera(), + ); + + Widget _buildCamera() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildCameraChildren(), + ); + + List _buildCameraChildren() => + [_buildCameraIcon(), verticalSpaceTiny, _buildCameraTitle()]; + + Widget _buildCameraIcon() => const Icon( + Icons.camera_alt_rounded, + size: 60, + color: kcPrimaryColor, + ); + + Widget _buildCameraTitle() => Text( + 'Camera', + style: style18P600, + ); + + Widget _buildGalleryButton() => GestureDetector( + onTap: onGalleryTap, + child: _buildGallery(), + ); + Widget _buildGallery() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildGalleryChildren(), + ); + + Widget _buildGalleryIcon() => const Icon( + Icons.photo, + size: 60, + color: kcPrimaryColor, + ); + + Widget _buildGalleryText() => Text( + 'Gallery', + style: style18P600, + ); + + List _buildGalleryChildren() => + [_buildGalleryIcon(), verticalSpaceTiny, _buildGalleryText()]; +} diff --git a/lib/ui/widgets/learn_app_bar.dart b/lib/ui/widgets/learn_app_bar.dart index 252cb63..e84af8b 100644 --- a/lib/ui/widgets/learn_app_bar.dart +++ b/lib/ui/widgets/learn_app_bar.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; @@ -39,7 +41,9 @@ class LearnAppBar extends StatelessWidget { radius: 25, backgroundColor: kcPrimaryColor, backgroundImage: profileImage != null - ? CachedNetworkImageProvider(profileImage!) + ? FileImage( + File(profileImage!), + ) : null, child: _buildImageBuilder(), ); diff --git a/lib/ui/widgets/profile_image.dart b/lib/ui/widgets/profile_image.dart index 7453547..db9ecf5 100644 --- a/lib/ui/widgets/profile_image.dart +++ b/lib/ui/widgets/profile_image.dart @@ -1,17 +1,26 @@ +import 'dart:io'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; class ProfileImage extends StatelessWidget { + final bool loading; final String? profileImage; - const ProfileImage({super.key, required this.profileImage}); + final GestureTapCallback? onTap; + + const ProfileImage( + {super.key, + this.onTap, + this.loading = false, + required this.profileImage}); @override Widget build(BuildContext context) => _buildSizedBox(); Widget _buildSizedBox() => SizedBox( - height: 125, width: 125, + height: 125, child: _buildStack(), ); @@ -27,14 +36,21 @@ class ProfileImage extends StatelessWidget { Widget _buildProfileImage() => CircleAvatar( radius: 50, backgroundColor: kcPrimaryColor, - backgroundImage: profileImage != null - ? CachedNetworkImageProvider(profileImage!) - : null, + backgroundImage: loading + ? null + : profileImage != null + ? FileImage( + File(profileImage!), + ) + : null, child: _buildImageBuilder(), ); - Widget? _buildImageBuilder() => - profileImage == null ? _buildPersonIcon() : null; + Widget? _buildImageBuilder() => loading + ? null + : profileImage == null + ? _buildPersonIcon() + : null; Widget _buildPersonIcon() => const Icon( Icons.person, @@ -44,6 +60,11 @@ class ProfileImage extends StatelessWidget { Widget _buildCameraButtonWrapper() => Align( alignment: Alignment.bottomCenter, + child: _buildCameraTapDetector(), + ); + + Widget _buildCameraTapDetector() => GestureDetector( + onTap: onTap, child: _buildCameraButton(), ); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d0e7f79..85a2413 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b29e9ba..62e3ed5 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux flutter_secure_storage_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a9f61f7..6b29984 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,14 @@ import Foundation import battery_plus import connectivity_plus +import file_selector_macos import flutter_secure_storage_darwin import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 29c3534..82c6e02 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" crypto: dependency: transitive description: @@ -321,6 +329,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -366,6 +406,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" flutter_secure_storage: dependency: "direct main" description: @@ -552,6 +600,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "297e42bd236c4ac4b091d4277292159b3280545e030cae2be3d503f9ecf7e6a1" + url: "https://pub.dev" + source: hosted + version: "0.8.13+12" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + url: "https://pub.dev" + source: hosted + version: "0.8.13+3" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" in_app_update: dependency: "direct main" description: @@ -832,6 +944,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8f9fb5c..a88784d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: iconsax: ^0.0.8 flutter_svg: ^2.2.3 stacked_shared: any + image_picker: ^1.2.1 battery_plus: ^7.0.0 storage_info: ^1.0.0 flutter_html: ^3.0.0 @@ -27,6 +28,7 @@ dependencies: stacked_services: ^1.1.0 omni_datetime_picker: any json_serializable: ^6.8.0 + permission_handler: ^12.0.1 cached_network_image: ^3.4.1 flutter_secure_storage: ^10.0.0 flutter_timer_countdown: ^1.0.7 diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart new file mode 100644 index 0000000..84e880c --- /dev/null +++ b/test/helpers/test_helpers.dart @@ -0,0 +1,155 @@ +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:yimaru_app/app/app.locator.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; +import 'package:yimaru_app/services/api_service.dart'; +import 'package:yimaru_app/services/secure_storage_service.dart'; +import 'package:yimaru_app/services/dio_service.dart'; +import 'package:yimaru_app/services/status_checker_service.dart'; +import 'package:yimaru_app/services/permission_handler_service.dart'; +import 'package:yimaru_app/services/image_picker_service.dart'; +// @stacked-import + +import 'test_helpers.mocks.dart'; + +@GenerateMocks( + [], + customMocks: [ + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec( + onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), +// @stacked-mock-spec + ], +) +void registerServices() { + getAndRegisterNavigationService(); + getAndRegisterBottomSheetService(); + getAndRegisterDialogService(); + getAndRegisterAuthenticationService(); + getAndRegisterApiService(); + getAndRegisterSecureStorageService(); + getAndRegisterDioService(); + getAndRegisterStatusCheckerService(); + getAndRegisterPermissionHandlerService(); + getAndRegisterImagePickerService(); +// @stacked-mock-register +} + +MockNavigationService getAndRegisterNavigationService() { + _removeRegistrationIfExists(); + final service = MockNavigationService(); + locator.registerSingleton(service); + return service; +} + +MockBottomSheetService getAndRegisterBottomSheetService({ + SheetResponse? showCustomSheetResponse, +}) { + _removeRegistrationIfExists(); + final service = MockBottomSheetService(); + + when( + service.showCustomSheet( + enableDrag: anyNamed('enableDrag'), + enterBottomSheetDuration: anyNamed('enterBottomSheetDuration'), + exitBottomSheetDuration: anyNamed('exitBottomSheetDuration'), + ignoreSafeArea: anyNamed('ignoreSafeArea'), + isScrollControlled: anyNamed('isScrollControlled'), + barrierDismissible: anyNamed('barrierDismissible'), + additionalButtonTitle: anyNamed('additionalButtonTitle'), + variant: anyNamed('variant'), + title: anyNamed('title'), + hasImage: anyNamed('hasImage'), + imageUrl: anyNamed('imageUrl'), + showIconInMainButton: anyNamed('showIconInMainButton'), + mainButtonTitle: anyNamed('mainButtonTitle'), + showIconInSecondaryButton: anyNamed('showIconInSecondaryButton'), + secondaryButtonTitle: anyNamed('secondaryButtonTitle'), + showIconInAdditionalButton: anyNamed('showIconInAdditionalButton'), + takesInput: anyNamed('takesInput'), + barrierColor: anyNamed('barrierColor'), + barrierLabel: anyNamed('barrierLabel'), + customData: anyNamed('customData'), + data: anyNamed('data'), + description: anyNamed('description'), + ), + ).thenAnswer( + (realInvocation) => + Future.value(showCustomSheetResponse ?? SheetResponse()), + ); + + locator.registerSingleton(service); + return service; +} + +MockDialogService getAndRegisterDialogService() { + _removeRegistrationIfExists(); + final service = MockDialogService(); + locator.registerSingleton(service); + return service; +} + +MockAuthenticationService getAndRegisterAuthenticationService() { + _removeRegistrationIfExists(); + final service = MockAuthenticationService(); + locator.registerSingleton(service); + return service; +} + +MockApiService getAndRegisterApiService() { + _removeRegistrationIfExists(); + final service = MockApiService(); + locator.registerSingleton(service); + return service; +} + +MockSecureStorageService getAndRegisterSecureStorageService() { + _removeRegistrationIfExists(); + final service = MockSecureStorageService(); + locator.registerSingleton(service); + return service; +} + +MockDioService getAndRegisterDioService() { + _removeRegistrationIfExists(); + final service = MockDioService(); + locator.registerSingleton(service); + return service; +} + +MockStatusCheckerService getAndRegisterStatusCheckerService() { + _removeRegistrationIfExists(); + final service = MockStatusCheckerService(); + locator.registerSingleton(service); + return service; +} + +MockPermissionHandlerService getAndRegisterPermissionHandlerService() { + _removeRegistrationIfExists(); + final service = MockPermissionHandlerService(); + locator.registerSingleton(service); + return service; +} + +MockImagePickerService getAndRegisterImagePickerService() { + _removeRegistrationIfExists(); + final service = MockImagePickerService(); + locator.registerSingleton(service); + return service; +} +// @stacked-mock-create + +void _removeRegistrationIfExists() { + if (locator.isRegistered()) { + locator.unregister(); + } +} diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart new file mode 100644 index 0000000..edb1e4a --- /dev/null +++ b/test/helpers/test_helpers.mocks.dart @@ -0,0 +1,1238 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in yimaru_app/test/helpers/test_helpers.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i7; +import 'dart:ui' as _i8; + +import 'package:dio/dio.dart' as _i2; +import 'package:flutter/material.dart' as _i6; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; +import 'package:permission_handler/permission_handler.dart' as _i16; +import 'package:stacked_services/stacked_services.dart' as _i4; +import 'package:yimaru_app/models/assessment.dart' as _i12; +import 'package:yimaru_app/models/user_model.dart' as _i10; +import 'package:yimaru_app/services/api_service.dart' as _i11; +import 'package:yimaru_app/services/authentication_service.dart' as _i9; +import 'package:yimaru_app/services/dio_service.dart' as _i13; +import 'package:yimaru_app/services/image_picker_service.dart' as _i17; +import 'package:yimaru_app/services/permission_handler_service.dart' as _i15; +import 'package:yimaru_app/services/secure_storage_service.dart' as _i3; +import 'package:yimaru_app/services/status_checker_service.dart' as _i14; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeDio_0 extends _i1.SmartFake implements _i2.Dio { + _FakeDio_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSecureStorageService_1 extends _i1.SmartFake + implements _i3.SecureStorageService { + _FakeSecureStorageService_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [NavigationService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNavigationService extends _i1.Mock implements _i4.NavigationService { + @override + String get previousRoute => (super.noSuchMethod( + Invocation.getter(#previousRoute), + returnValue: _i5.dummyValue( + this, + Invocation.getter(#previousRoute), + ), + returnValueForMissingStub: _i5.dummyValue( + this, + Invocation.getter(#previousRoute), + ), + ) as String); + + @override + String get currentRoute => (super.noSuchMethod( + Invocation.getter(#currentRoute), + returnValue: _i5.dummyValue( + this, + Invocation.getter(#currentRoute), + ), + returnValueForMissingStub: _i5.dummyValue( + this, + Invocation.getter(#currentRoute), + ), + ) as String); + + @override + _i6.GlobalKey<_i6.NavigatorState>? nestedNavigationKey(int? index) => + (super.noSuchMethod( + Invocation.method( + #nestedNavigationKey, + [index], + ), + returnValueForMissingStub: null, + ) as _i6.GlobalKey<_i6.NavigatorState>?); + + @override + void config({ + bool? enableLog, + bool? defaultPopGesture, + bool? defaultOpaqueRoute, + Duration? defaultDurationTransition, + bool? defaultGlobalState, + _i4.Transition? defaultTransitionStyle, + String? defaultTransition, + }) => + super.noSuchMethod( + Invocation.method( + #config, + [], + { + #enableLog: enableLog, + #defaultPopGesture: defaultPopGesture, + #defaultOpaqueRoute: defaultOpaqueRoute, + #defaultDurationTransition: defaultDurationTransition, + #defaultGlobalState: defaultGlobalState, + #defaultTransitionStyle: defaultTransitionStyle, + #defaultTransition: defaultTransition, + }, + ), + returnValueForMissingStub: null, + ); + + @override + _i7.Future? navigateWithTransition( + _i6.Widget? page, { + bool? opaque, + String? transition = r'', + Duration? duration, + bool? popGesture, + int? id, + _i6.Curve? curve, + bool? fullscreenDialog = false, + bool? preventDuplicates = true, + _i4.Transition? transitionClass, + _i4.Transition? transitionStyle, + String? routeName, + }) => + (super.noSuchMethod( + Invocation.method( + #navigateWithTransition, + [page], + { + #opaque: opaque, + #transition: transition, + #duration: duration, + #popGesture: popGesture, + #id: id, + #curve: curve, + #fullscreenDialog: fullscreenDialog, + #preventDuplicates: preventDuplicates, + #transitionClass: transitionClass, + #transitionStyle: transitionStyle, + #routeName: routeName, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? replaceWithTransition( + _i6.Widget? page, { + bool? opaque, + String? transition = r'', + Duration? duration, + bool? popGesture, + int? id, + _i6.Curve? curve, + bool? fullscreenDialog = false, + bool? preventDuplicates = true, + _i4.Transition? transitionClass, + _i4.Transition? transitionStyle, + String? routeName, + }) => + (super.noSuchMethod( + Invocation.method( + #replaceWithTransition, + [page], + { + #opaque: opaque, + #transition: transition, + #duration: duration, + #popGesture: popGesture, + #id: id, + #curve: curve, + #fullscreenDialog: fullscreenDialog, + #preventDuplicates: preventDuplicates, + #transitionClass: transitionClass, + #transitionStyle: transitionStyle, + #routeName: routeName, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + bool back({ + dynamic result, + int? id, + }) => + (super.noSuchMethod( + Invocation.method( + #back, + [], + { + #result: result, + #id: id, + }, + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + void popUntil( + _i6.RoutePredicate? predicate, { + int? id, + }) => + super.noSuchMethod( + Invocation.method( + #popUntil, + [predicate], + {#id: id}, + ), + returnValueForMissingStub: null, + ); + + @override + void popRepeated(int? popTimes) => super.noSuchMethod( + Invocation.method( + #popRepeated, + [popTimes], + ), + returnValueForMissingStub: null, + ); + + @override + _i7.Future? navigateTo( + String? routeName, { + dynamic arguments, + int? id, + bool? preventDuplicates = true, + Map? parameters, + _i6.RouteTransitionsBuilder? transition, + }) => + (super.noSuchMethod( + Invocation.method( + #navigateTo, + [routeName], + { + #arguments: arguments, + #id: id, + #preventDuplicates: preventDuplicates, + #parameters: parameters, + #transition: transition, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? navigateToView( + _i6.Widget? view, { + dynamic arguments, + int? id, + bool? opaque, + _i6.Curve? curve, + Duration? duration, + bool? fullscreenDialog = false, + bool? popGesture, + bool? preventDuplicates = true, + _i4.Transition? transition, + _i4.Transition? transitionStyle, + }) => + (super.noSuchMethod( + Invocation.method( + #navigateToView, + [view], + { + #arguments: arguments, + #id: id, + #opaque: opaque, + #curve: curve, + #duration: duration, + #fullscreenDialog: fullscreenDialog, + #popGesture: popGesture, + #preventDuplicates: preventDuplicates, + #transition: transition, + #transitionStyle: transitionStyle, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? replaceWith( + String? routeName, { + dynamic arguments, + int? id, + bool? preventDuplicates = true, + Map? parameters, + _i6.RouteTransitionsBuilder? transition, + }) => + (super.noSuchMethod( + Invocation.method( + #replaceWith, + [routeName], + { + #arguments: arguments, + #id: id, + #preventDuplicates: preventDuplicates, + #parameters: parameters, + #transition: transition, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? clearStackAndShow( + String? routeName, { + dynamic arguments, + int? id, + Map? parameters, + }) => + (super.noSuchMethod( + Invocation.method( + #clearStackAndShow, + [routeName], + { + #arguments: arguments, + #id: id, + #parameters: parameters, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? clearStackAndShowView( + _i6.Widget? view, { + dynamic arguments, + int? id, + }) => + (super.noSuchMethod( + Invocation.method( + #clearStackAndShowView, + [view], + { + #arguments: arguments, + #id: id, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? clearTillFirstAndShow( + String? routeName, { + dynamic arguments, + int? id, + bool? preventDuplicates = true, + Map? parameters, + }) => + (super.noSuchMethod( + Invocation.method( + #clearTillFirstAndShow, + [routeName], + { + #arguments: arguments, + #id: id, + #preventDuplicates: preventDuplicates, + #parameters: parameters, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? clearTillFirstAndShowView( + _i6.Widget? view, { + dynamic arguments, + int? id, + }) => + (super.noSuchMethod( + Invocation.method( + #clearTillFirstAndShowView, + [view], + { + #arguments: arguments, + #id: id, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); + + @override + _i7.Future? pushNamedAndRemoveUntil( + String? routeName, { + _i6.RoutePredicate? predicate, + dynamic arguments, + int? id, + }) => + (super.noSuchMethod( + Invocation.method( + #pushNamedAndRemoveUntil, + [routeName], + { + #predicate: predicate, + #arguments: arguments, + #id: id, + }, + ), + returnValueForMissingStub: null, + ) as _i7.Future?); +} + +/// A class which mocks [BottomSheetService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBottomSheetService extends _i1.Mock + implements _i4.BottomSheetService { + @override + void setCustomSheetBuilders(Map? builders) => + super.noSuchMethod( + Invocation.method( + #setCustomSheetBuilders, + [builders], + ), + returnValueForMissingStub: null, + ); + + @override + _i7.Future<_i4.SheetResponse?> showBottomSheet({ + required String? title, + String? description, + String? confirmButtonTitle = r'Ok', + String? cancelButtonTitle, + bool? enableDrag = true, + bool? barrierDismissible = true, + bool? isScrollControlled = false, + Duration? exitBottomSheetDuration, + Duration? enterBottomSheetDuration, + bool? ignoreSafeArea, + bool? useRootNavigator = false, + double? elevation = 1.0, + }) => + (super.noSuchMethod( + Invocation.method( + #showBottomSheet, + [], + { + #title: title, + #description: description, + #confirmButtonTitle: confirmButtonTitle, + #cancelButtonTitle: cancelButtonTitle, + #enableDrag: enableDrag, + #barrierDismissible: barrierDismissible, + #isScrollControlled: isScrollControlled, + #exitBottomSheetDuration: exitBottomSheetDuration, + #enterBottomSheetDuration: enterBottomSheetDuration, + #ignoreSafeArea: ignoreSafeArea, + #useRootNavigator: useRootNavigator, + #elevation: elevation, + }, + ), + returnValue: _i7.Future<_i4.SheetResponse?>.value(), + returnValueForMissingStub: + _i7.Future<_i4.SheetResponse?>.value(), + ) as _i7.Future<_i4.SheetResponse?>); + + @override + _i7.Future<_i4.SheetResponse?> showCustomSheet({ + dynamic variant, + String? title, + String? description, + bool? hasImage = false, + String? imageUrl, + bool? showIconInMainButton = false, + String? mainButtonTitle, + bool? showIconInSecondaryButton = false, + String? secondaryButtonTitle, + bool? showIconInAdditionalButton = false, + String? additionalButtonTitle, + bool? takesInput = false, + _i8.Color? barrierColor = const _i8.Color(2315255808), + double? elevation = 1.0, + bool? barrierDismissible = true, + bool? isScrollControlled = false, + String? barrierLabel = r'', + dynamic customData, + R? data, + bool? enableDrag = true, + Duration? exitBottomSheetDuration, + Duration? enterBottomSheetDuration, + bool? ignoreSafeArea, + bool? useRootNavigator = false, + }) => + (super.noSuchMethod( + Invocation.method( + #showCustomSheet, + [], + { + #variant: variant, + #title: title, + #description: description, + #hasImage: hasImage, + #imageUrl: imageUrl, + #showIconInMainButton: showIconInMainButton, + #mainButtonTitle: mainButtonTitle, + #showIconInSecondaryButton: showIconInSecondaryButton, + #secondaryButtonTitle: secondaryButtonTitle, + #showIconInAdditionalButton: showIconInAdditionalButton, + #additionalButtonTitle: additionalButtonTitle, + #takesInput: takesInput, + #barrierColor: barrierColor, + #elevation: elevation, + #barrierDismissible: barrierDismissible, + #isScrollControlled: isScrollControlled, + #barrierLabel: barrierLabel, + #customData: customData, + #data: data, + #enableDrag: enableDrag, + #exitBottomSheetDuration: exitBottomSheetDuration, + #enterBottomSheetDuration: enterBottomSheetDuration, + #ignoreSafeArea: ignoreSafeArea, + #useRootNavigator: useRootNavigator, + }, + ), + returnValue: _i7.Future<_i4.SheetResponse?>.value(), + returnValueForMissingStub: _i7.Future<_i4.SheetResponse?>.value(), + ) as _i7.Future<_i4.SheetResponse?>); + + @override + void completeSheet(_i4.SheetResponse? response) => + super.noSuchMethod( + Invocation.method( + #completeSheet, + [response], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [DialogService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDialogService extends _i1.Mock implements _i4.DialogService { + @override + void registerCustomDialogBuilders( + Map? builders) => + super.noSuchMethod( + Invocation.method( + #registerCustomDialogBuilders, + [builders], + ), + returnValueForMissingStub: null, + ); + + @override + void registerCustomDialogBuilder({ + required dynamic variant, + required _i6.Widget Function( + _i6.BuildContext, + _i4.DialogRequest, + dynamic Function(_i4.DialogResponse), + )? builder, + }) => + super.noSuchMethod( + Invocation.method( + #registerCustomDialogBuilder, + [], + { + #variant: variant, + #builder: builder, + }, + ), + returnValueForMissingStub: null, + ); + + @override + _i7.Future<_i4.DialogResponse?> showDialog({ + String? title, + String? description, + String? cancelTitle, + _i8.Color? cancelTitleColor, + String? buttonTitle = r'Ok', + _i8.Color? buttonTitleColor, + bool? barrierDismissible = false, + _i6.RouteSettings? routeSettings, + _i6.GlobalKey<_i6.NavigatorState>? navigatorKey, + _i4.DialogPlatform? dialogPlatform, + }) => + (super.noSuchMethod( + Invocation.method( + #showDialog, + [], + { + #title: title, + #description: description, + #cancelTitle: cancelTitle, + #cancelTitleColor: cancelTitleColor, + #buttonTitle: buttonTitle, + #buttonTitleColor: buttonTitleColor, + #barrierDismissible: barrierDismissible, + #routeSettings: routeSettings, + #navigatorKey: navigatorKey, + #dialogPlatform: dialogPlatform, + }, + ), + returnValue: _i7.Future<_i4.DialogResponse?>.value(), + returnValueForMissingStub: + _i7.Future<_i4.DialogResponse?>.value(), + ) as _i7.Future<_i4.DialogResponse?>); + + @override + _i7.Future<_i4.DialogResponse?> showCustomDialog({ + dynamic variant, + String? title, + String? description, + bool? hasImage = false, + String? imageUrl, + bool? showIconInMainButton = false, + String? mainButtonTitle, + bool? showIconInSecondaryButton = false, + String? secondaryButtonTitle, + bool? showIconInAdditionalButton = false, + String? additionalButtonTitle, + bool? takesInput = false, + _i8.Color? barrierColor = const _i8.Color(2315255808), + bool? barrierDismissible = false, + String? barrierLabel = r'', + bool? useSafeArea = true, + _i6.RouteSettings? routeSettings, + _i6.GlobalKey<_i6.NavigatorState>? navigatorKey, + _i6.RouteTransitionsBuilder? transitionBuilder, + dynamic customData, + R? data, + }) => + (super.noSuchMethod( + Invocation.method( + #showCustomDialog, + [], + { + #variant: variant, + #title: title, + #description: description, + #hasImage: hasImage, + #imageUrl: imageUrl, + #showIconInMainButton: showIconInMainButton, + #mainButtonTitle: mainButtonTitle, + #showIconInSecondaryButton: showIconInSecondaryButton, + #secondaryButtonTitle: secondaryButtonTitle, + #showIconInAdditionalButton: showIconInAdditionalButton, + #additionalButtonTitle: additionalButtonTitle, + #takesInput: takesInput, + #barrierColor: barrierColor, + #barrierDismissible: barrierDismissible, + #barrierLabel: barrierLabel, + #useSafeArea: useSafeArea, + #routeSettings: routeSettings, + #navigatorKey: navigatorKey, + #transitionBuilder: transitionBuilder, + #customData: customData, + #data: data, + }, + ), + returnValue: _i7.Future<_i4.DialogResponse?>.value(), + returnValueForMissingStub: _i7.Future<_i4.DialogResponse?>.value(), + ) as _i7.Future<_i4.DialogResponse?>); + + @override + _i7.Future<_i4.DialogResponse?> showConfirmationDialog({ + String? title, + String? description, + String? cancelTitle = r'Cancel', + _i8.Color? cancelTitleColor, + String? confirmationTitle = r'Ok', + _i8.Color? confirmationTitleColor, + bool? barrierDismissible = false, + _i6.RouteSettings? routeSettings, + _i4.DialogPlatform? dialogPlatform, + }) => + (super.noSuchMethod( + Invocation.method( + #showConfirmationDialog, + [], + { + #title: title, + #description: description, + #cancelTitle: cancelTitle, + #cancelTitleColor: cancelTitleColor, + #confirmationTitle: confirmationTitle, + #confirmationTitleColor: confirmationTitleColor, + #barrierDismissible: barrierDismissible, + #routeSettings: routeSettings, + #dialogPlatform: dialogPlatform, + }, + ), + returnValue: _i7.Future<_i4.DialogResponse?>.value(), + returnValueForMissingStub: + _i7.Future<_i4.DialogResponse?>.value(), + ) as _i7.Future<_i4.DialogResponse?>); + + @override + void completeDialog(_i4.DialogResponse? response) => + super.noSuchMethod( + Invocation.method( + #completeDialog, + [response], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [AuthenticationService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthenticationService extends _i1.Mock + implements _i9.AuthenticationService { + @override + _i7.Future userLoggedIn() => (super.noSuchMethod( + Invocation.method( + #userLoggedIn, + [], + ), + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); + + @override + _i7.Future getAccessToken() => (super.noSuchMethod( + Invocation.method( + #getAccessToken, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future getRefreshToken() => (super.noSuchMethod( + Invocation.method( + #getRefreshToken, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future getUserId() => (super.noSuchMethod( + Invocation.method( + #getUserId, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future saveTokens({ + required String? access, + required String? refresh, + }) => + (super.noSuchMethod( + Invocation.method( + #saveTokens, + [], + { + #access: access, + #refresh: refresh, + }, + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future saveUserName(Map? data) => + (super.noSuchMethod( + Invocation.method( + #saveUserName, + [data], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future saveBasicUserData(Map? data) => + (super.noSuchMethod( + Invocation.method( + #saveBasicUserData, + [data], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future saveProfileStatus(bool? value) => (super.noSuchMethod( + Invocation.method( + #saveProfileStatus, + [value], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future saveProfileImage(String? image) => (super.noSuchMethod( + Invocation.method( + #saveProfileImage, + [image], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future saveFullName(Map? data) => + (super.noSuchMethod( + Invocation.method( + #saveFullName, + [data], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future isFirstTimeInstall() => (super.noSuchMethod( + Invocation.method( + #isFirstTimeInstall, + [], + ), + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); + + @override + _i7.Future setFirstTimeInstall(bool? value) => (super.noSuchMethod( + Invocation.method( + #setFirstTimeInstall, + [value], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future<_i10.UserModel?> getUser() => (super.noSuchMethod( + Invocation.method( + #getUser, + [], + ), + returnValue: _i7.Future<_i10.UserModel?>.value(), + returnValueForMissingStub: _i7.Future<_i10.UserModel?>.value(), + ) as _i7.Future<_i10.UserModel?>); + + @override + _i7.Future logOut() => (super.noSuchMethod( + Invocation.method( + #logOut, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); +} + +/// A class which mocks [ApiService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockApiService extends _i1.Mock implements _i11.ApiService { + @override + _i7.Future> register(Map? data) => + (super.noSuchMethod( + Invocation.method( + #register, + [data], + ), + returnValue: + _i7.Future>.value({}), + returnValueForMissingStub: + _i7.Future>.value({}), + ) as _i7.Future>); + + @override + _i7.Future> login(Map? data) => + (super.noSuchMethod( + Invocation.method( + #login, + [data], + ), + returnValue: + _i7.Future>.value({}), + returnValueForMissingStub: + _i7.Future>.value({}), + ) as _i7.Future>); + + @override + _i7.Future> verifyOtp(Map? data) => + (super.noSuchMethod( + Invocation.method( + #verifyOtp, + [data], + ), + returnValue: + _i7.Future>.value({}), + returnValueForMissingStub: + _i7.Future>.value({}), + ) as _i7.Future>); + + @override + _i7.Future> resendOtp(Map? data) => + (super.noSuchMethod( + Invocation.method( + #resendOtp, + [data], + ), + returnValue: + _i7.Future>.value({}), + returnValueForMissingStub: + _i7.Future>.value({}), + ) as _i7.Future>); + + @override + _i7.Future> getProfileStatus(_i10.UserModel? user) => + (super.noSuchMethod( + Invocation.method( + #getProfileStatus, + [user], + ), + returnValue: + _i7.Future>.value({}), + returnValueForMissingStub: + _i7.Future>.value({}), + ) as _i7.Future>); + + @override + _i7.Future> getProfileData(int? userId) => + (super.noSuchMethod( + Invocation.method( + #getProfileData, + [userId], + ), + returnValue: + _i7.Future>.value({}), + returnValueForMissingStub: + _i7.Future>.value({}), + ) as _i7.Future>); + + @override + _i7.Future> updateProfile({ + required _i10.UserModel? user, + required Map? data, + }) => + (super.noSuchMethod( + Invocation.method( + #updateProfile, + [], + { + #user: user, + #data: data, + }, + ), + returnValue: + _i7.Future>.value({}), + returnValueForMissingStub: + _i7.Future>.value({}), + ) as _i7.Future>); + + @override + _i7.Future> getAssessments() => (super.noSuchMethod( + Invocation.method( + #getAssessments, + [], + ), + returnValue: + _i7.Future>.value(<_i12.Assessment>[]), + returnValueForMissingStub: + _i7.Future>.value(<_i12.Assessment>[]), + ) as _i7.Future>); +} + +/// A class which mocks [SecureStorageService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSecureStorageService extends _i1.Mock + implements _i3.SecureStorageService { + @override + _i7.Future clear() => (super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future getBool(String? key) => (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future getString(String? key) => (super.noSuchMethod( + Invocation.method( + #getString, + [key], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future getInt(String? key) => (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future setString( + String? key, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setString, + [ + key, + value, + ], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future setInt( + String? key, + int? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setInt, + [ + key, + value, + ], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future setBool( + String? key, + bool? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setBool, + [ + key, + value, + ], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); +} + +/// A class which mocks [DioService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDioService extends _i1.Mock implements _i13.DioService { + @override + _i2.Dio get dio => (super.noSuchMethod( + Invocation.getter(#dio), + returnValue: _FakeDio_0( + this, + Invocation.getter(#dio), + ), + returnValueForMissingStub: _FakeDio_0( + this, + Invocation.getter(#dio), + ), + ) as _i2.Dio); +} + +/// A class which mocks [StatusCheckerService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockStatusCheckerService extends _i1.Mock + implements _i14.StatusCheckerService { + @override + _i3.SecureStorageService get storage => (super.noSuchMethod( + Invocation.getter(#storage), + returnValue: _FakeSecureStorageService_1( + this, + Invocation.getter(#storage), + ), + returnValueForMissingStub: _FakeSecureStorageService_1( + this, + Invocation.getter(#storage), + ), + ) as _i3.SecureStorageService); + + @override + bool get previousConnection => (super.noSuchMethod( + Invocation.getter(#previousConnection), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i7.Future getBatteryLevel() => (super.noSuchMethod( + Invocation.method( + #getBatteryLevel, + [], + ), + returnValue: _i7.Future.value(0), + returnValueForMissingStub: _i7.Future.value(0), + ) as _i7.Future); + + @override + _i7.Future userAuthenticated() => (super.noSuchMethod( + Invocation.method( + #userAuthenticated, + [], + ), + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); + + @override + _i7.Future checkConnection() => (super.noSuchMethod( + Invocation.method( + #checkConnection, + [], + ), + returnValue: _i7.Future.value(false), + returnValueForMissingStub: _i7.Future.value(false), + ) as _i7.Future); + + @override + _i7.Future getAvailableStorage() => (super.noSuchMethod( + Invocation.method( + #getAvailableStorage, + [], + ), + returnValue: _i7.Future.value(0), + returnValueForMissingStub: _i7.Future.value(0), + ) as _i7.Future); + + @override + _i7.Future checkAndUpdate() => (super.noSuchMethod( + Invocation.method( + #checkAndUpdate, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); +} + +/// A class which mocks [PermissionHandlerService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPermissionHandlerService extends _i1.Mock + implements _i15.PermissionHandlerService { + @override + _i7.Future<_i16.PermissionStatus> requestPermission( + _i16.Permission? requestedPermission) => + (super.noSuchMethod( + Invocation.method( + #requestPermission, + [requestedPermission], + ), + returnValue: _i7.Future<_i16.PermissionStatus>.value( + _i16.PermissionStatus.denied), + returnValueForMissingStub: _i7.Future<_i16.PermissionStatus>.value( + _i16.PermissionStatus.denied), + ) as _i7.Future<_i16.PermissionStatus>); + + @override + _i7.Future<_i16.PermissionStatus> request(_i16.Permission? permission) => + (super.noSuchMethod( + Invocation.method( + #request, + [permission], + ), + returnValue: _i7.Future<_i16.PermissionStatus>.value( + _i16.PermissionStatus.denied), + returnValueForMissingStub: _i7.Future<_i16.PermissionStatus>.value( + _i16.PermissionStatus.denied), + ) as _i7.Future<_i16.PermissionStatus>); +} + +/// A class which mocks [ImagePickerService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockImagePickerService extends _i1.Mock + implements _i17.ImagePickerService { + @override + _i7.Future gallery() => (super.noSuchMethod( + Invocation.method( + #gallery, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future camera() => (super.noSuchMethod( + Invocation.method( + #camera, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); +} diff --git a/test/viewmodels/failure_viewmodel_test.dart b/test/services/api_service_test.dart similarity index 85% rename from test/viewmodels/failure_viewmodel_test.dart rename to test/services/api_service_test.dart index d06419f..93e9612 100644 --- a/test/viewmodels/failure_viewmodel_test.dart +++ b/test/services/api_service_test.dart @@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart'; import '../helpers/test_helpers.dart'; void main() { - group('FailureViewModel Tests -', () { + group('ApiServiceTest -', () { setUp(() => registerServices()); tearDown(() => locator.reset()); }); diff --git a/test/viewmodels/learn_lesson_viewmodel_test.dart b/test/services/authentication_service_test.dart similarity index 83% rename from test/viewmodels/learn_lesson_viewmodel_test.dart rename to test/services/authentication_service_test.dart index 1056224..a06da40 100644 --- a/test/viewmodels/learn_lesson_viewmodel_test.dart +++ b/test/services/authentication_service_test.dart @@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart'; import '../helpers/test_helpers.dart'; void main() { - group('LearnLessonViewModel Tests -', () { + group('AuthenticationServiceTest -', () { setUp(() => registerServices()); tearDown(() => locator.reset()); }); diff --git a/test/services/dio_service_test.dart b/test/services/dio_service_test.dart new file mode 100644 index 0000000..0651d4c --- /dev/null +++ b/test/services/dio_service_test.dart @@ -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('DioServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/services/permission_handler_service_test.dart b/test/services/permission_handler_service_test.dart new file mode 100644 index 0000000..061b23b --- /dev/null +++ b/test/services/permission_handler_service_test.dart @@ -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('PermissionHandlerServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/services/secure_storage_service_test.dart b/test/services/secure_storage_service_test.dart new file mode 100644 index 0000000..d6f4985 --- /dev/null +++ b/test/services/secure_storage_service_test.dart @@ -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('SecureStorageServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/services/status_checker_service_test.dart b/test/services/status_checker_service_test.dart new file mode 100644 index 0000000..0460b20 --- /dev/null +++ b/test/services/status_checker_service_test.dart @@ -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('StatusCheckerServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/account_privacy_viewmodel_test.dart b/test/viewmodels/account_privacy_viewmodel_test.dart new file mode 100644 index 0000000..d687122 --- /dev/null +++ b/test/viewmodels/account_privacy_viewmodel_test.dart @@ -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('AccountPrivacyViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/assessment_viewmodel_test.dart b/test/viewmodels/assessment_viewmodel_test.dart new file mode 100644 index 0000000..e39ce4d --- /dev/null +++ b/test/viewmodels/assessment_viewmodel_test.dart @@ -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('AssessmentViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/call_support_viewmodel_test.dart b/test/viewmodels/call_support_viewmodel_test.dart new file mode 100644 index 0000000..52150a2 --- /dev/null +++ b/test/viewmodels/call_support_viewmodel_test.dart @@ -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('CallSupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/downloads_viewmodel_test.dart b/test/viewmodels/downloads_viewmodel_test.dart new file mode 100644 index 0000000..342bdcd --- /dev/null +++ b/test/viewmodels/downloads_viewmodel_test.dart @@ -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('DownloadsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/full_name_view_viewmodel_test.dart b/test/viewmodels/full_name_view_viewmodel_test.dart new file mode 100644 index 0000000..c33d474 --- /dev/null +++ b/test/viewmodels/full_name_view_viewmodel_test.dart @@ -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('FullNameViewViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/home_viewmodel_test.dart b/test/viewmodels/home_viewmodel_test.dart new file mode 100644 index 0000000..0c5d802 --- /dev/null +++ b/test/viewmodels/home_viewmodel_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:yimaru_app/app/app.bottomsheets.dart'; +import 'package:yimaru_app/app/app.locator.dart'; +import 'package:yimaru_app/ui/common/app_strings.dart'; +import 'package:yimaru_app/ui/views/home/home_viewmodel.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + HomeViewModel getModel() => HomeViewModel(); + + group('HomeViewmodelTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + + group('showBottomSheet -', () { + test( + 'When called, should show custom bottom sheet using notice variant', + () { + final bottomSheetService = getAndRegisterBottomSheetService(); + + final model = getModel(); + model.showBottomSheet(); + verify( + bottomSheetService.showCustomSheet( + variant: BottomSheetType.notice, + title: ksHomeBottomSheetTitle, + description: ksHomeBottomSheetDescription, + ), + ); + }, + ); + }); + }); +} diff --git a/test/viewmodels/language_viewmodel_test.dart b/test/viewmodels/language_viewmodel_test.dart new file mode 100644 index 0000000..0b8735a --- /dev/null +++ b/test/viewmodels/language_viewmodel_test.dart @@ -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('LanguageViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/learn_level_viewmodel_test.dart b/test/viewmodels/learn_level_viewmodel_test.dart new file mode 100644 index 0000000..5d97e54 --- /dev/null +++ b/test/viewmodels/learn_level_viewmodel_test.dart @@ -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('LearnLevelViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/learn_module_viewmodel_test.dart b/test/viewmodels/learn_module_viewmodel_test.dart new file mode 100644 index 0000000..0080f51 --- /dev/null +++ b/test/viewmodels/learn_module_viewmodel_test.dart @@ -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('LearnModuleViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/learn_viewmodel_test.dart b/test/viewmodels/learn_viewmodel_test.dart new file mode 100644 index 0000000..cf11c9c --- /dev/null +++ b/test/viewmodels/learn_viewmodel_test.dart @@ -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('LearnViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/login_viewmodel_test.dart b/test/viewmodels/login_viewmodel_test.dart new file mode 100644 index 0000000..8ff6466 --- /dev/null +++ b/test/viewmodels/login_viewmodel_test.dart @@ -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('LoginViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/onboarding_viewmodel_test.dart b/test/viewmodels/onboarding_viewmodel_test.dart new file mode 100644 index 0000000..4282556 --- /dev/null +++ b/test/viewmodels/onboarding_viewmodel_test.dart @@ -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('OnboardingViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/ongoing_progress_viewmodel_test.dart b/test/viewmodels/ongoing_progress_viewmodel_test.dart new file mode 100644 index 0000000..94a3ed4 --- /dev/null +++ b/test/viewmodels/ongoing_progress_viewmodel_test.dart @@ -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('OngoingProgressViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/privacy_policy_viewmodel_test.dart b/test/viewmodels/privacy_policy_viewmodel_test.dart new file mode 100644 index 0000000..81c404c --- /dev/null +++ b/test/viewmodels/privacy_policy_viewmodel_test.dart @@ -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('PrivacyPolicyViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/profile_detail_viewmodel_test.dart b/test/viewmodels/profile_detail_viewmodel_test.dart new file mode 100644 index 0000000..a1a9504 --- /dev/null +++ b/test/viewmodels/profile_detail_viewmodel_test.dart @@ -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('ProfileDetailViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/profile_viewmodel_test.dart b/test/viewmodels/profile_viewmodel_test.dart new file mode 100644 index 0000000..c72bc93 --- /dev/null +++ b/test/viewmodels/profile_viewmodel_test.dart @@ -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('ProfileViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/progress_viewmodel_test.dart b/test/viewmodels/progress_viewmodel_test.dart new file mode 100644 index 0000000..6f79cf5 --- /dev/null +++ b/test/viewmodels/progress_viewmodel_test.dart @@ -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('ProgressViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/register_viewmodel_test.dart b/test/viewmodels/register_viewmodel_test.dart new file mode 100644 index 0000000..78e03a5 --- /dev/null +++ b/test/viewmodels/register_viewmodel_test.dart @@ -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('RegisterViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/settings_viewmodel_test.dart b/test/viewmodels/settings_viewmodel_test.dart new file mode 100644 index 0000000..aa5a11d --- /dev/null +++ b/test/viewmodels/settings_viewmodel_test.dart @@ -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('SettingsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/startup_viewmodel_test.dart b/test/viewmodels/startup_viewmodel_test.dart new file mode 100644 index 0000000..e560a6c --- /dev/null +++ b/test/viewmodels/startup_viewmodel_test.dart @@ -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('StartupViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/support_viewmodel_test.dart b/test/viewmodels/support_viewmodel_test.dart new file mode 100644 index 0000000..260dc47 --- /dev/null +++ b/test/viewmodels/support_viewmodel_test.dart @@ -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('SupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/telegram_support_viewmodel_test.dart b/test/viewmodels/telegram_support_viewmodel_test.dart new file mode 100644 index 0000000..1605ed4 --- /dev/null +++ b/test/viewmodels/telegram_support_viewmodel_test.dart @@ -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('TelegramSupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/terms_and_conditions_viewmodel_test.dart b/test/viewmodels/terms_and_conditions_viewmodel_test.dart new file mode 100644 index 0000000..7da4d4a --- /dev/null +++ b/test/viewmodels/terms_and_conditions_viewmodel_test.dart @@ -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('TermsAndConditionsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/test/viewmodels/welcome_viewmodel_test.dart b/test/viewmodels/welcome_viewmodel_test.dart new file mode 100644 index 0000000..4b59f78 --- /dev/null +++ b/test/viewmodels/welcome_viewmodel_test.dart @@ -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('WelcomeViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8ac9fc6..0f5385e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,13 +8,19 @@ #include #include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { BatteryPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e1ff19b..2c88dbb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,7 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST battery_plus connectivity_plus + file_selector_windows flutter_secure_storage_windows + permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 8110e25cb98c66dd0ce120fdb1c169cf23a5a7fc Mon Sep 17 00:00:00 2001 From: BisratHailu Date: Thu, 5 Feb 2026 23:03:52 +0300 Subject: [PATCH 3/5] feat(auth): Add google sign-in option --- android/app/build.gradle.kts | 23 +- android/app/google-services.json | 51 ++ android/app/src/main/AndroidManifest.xml | 2 +- .../lms/app}/MainActivity.kt | 2 +- android/settings.gradle.kts | 6 +- assets/icons/a_1.svg | 5 + assets/icons/a_2.svg | 5 + assets/icons/b1.svg | 10 - assets/icons/b_1.svg | 5 + assets/icons/b_2.svg | 5 + firebase.json | 1 + ios/Runner.xcodeproj/project.pbxproj | 12 +- lib/app/app.dart | 4 + lib/app/app.locator.dart | 4 + lib/firebase_options.dart | 70 ++ lib/models/assessment.dart | 28 +- lib/models/assessment.g.dart | 20 +- lib/models/option.dart | 8 +- lib/models/option.g.dart | 6 +- lib/models/question.dart | 36 - lib/models/question.g.dart | 27 - lib/models/user_model.dart | 36 +- lib/models/user_model.g.dart | 26 +- lib/services/api_service.dart | 201 ++++- lib/services/authentication_service.dart | 99 ++- lib/services/dio_service.dart | 20 +- lib/services/google_auth_service.dart | 21 + lib/services/image_downloader_service.dart | 33 + lib/ui/common/app_constants.dart | 9 +- lib/ui/common/enmus.dart | 2 +- lib/ui/common/ui_helpers.dart | 14 + lib/ui/views/assessment/assessment_view.dart | 14 +- .../assessment/assessment_viewmodel.dart | 151 ++-- .../screens/assessment_form_screen.dart | 33 +- .../screens/assessment_intro_screen.dart | 14 +- .../screens/assessment_loading_screen.dart | 17 +- .../screens/assessment_result_screen.dart | 21 +- .../screens/start_lesson_screen.dart | 28 +- lib/ui/views/home/home_viewmodel.dart | 88 ++- lib/ui/views/learn/learn_view.dart | 2 +- .../views/learn_lesson/learn_lesson_view.dart | 1 - lib/ui/views/login/login_viewmodel.dart | 97 ++- .../screens/login_with_email_screen.dart | 20 +- .../onboarding/onboarding_viewmodel.dart | 50 +- .../screens/country_region_form_screen.dart | 7 +- lib/ui/views/profile/profile_view.dart | 8 +- lib/ui/views/profile/profile_viewmodel.dart | 66 +- .../profile_detail/profile_detail_view.dart | 214 ++++-- .../profile_detail_view.form.dart | 34 + .../profile_detail_viewmodel.dart | 168 ++++- lib/ui/views/progress/progress_viewmodel.dart | 6 +- lib/ui/views/register/register_viewmodel.dart | 125 ++-- lib/ui/views/startup/startup_viewmodel.dart | 7 +- lib/ui/views/welcome/welcome_viewmodel.dart | 27 +- lib/ui/widgets/birthday_selector.dart | 1 - lib/ui/widgets/custom_dropdown.dart | 37 +- lib/ui/widgets/learn_app_bar.dart | 16 +- lib/ui/widgets/learn_lesson_tile.dart | 1 - lib/ui/widgets/profile_image.dart | 5 +- lib/ui/widgets/refresh_button.dart | 19 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 84 ++- pubspec.yaml | 6 +- test/helpers/test_helpers.dart | 21 + test/helpers/test_helpers.mocks.dart | 707 +++++++++++------- test/services/google_auth_service_test.dart | 11 + .../image_downloader_service_test.dart | 11 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 69 files changed, 2067 insertions(+), 849 deletions(-) create mode 100644 android/app/google-services.json rename android/app/src/main/kotlin/com/{example/yimaru_app => yimaru/lms/app}/MainActivity.kt (75%) create mode 100644 assets/icons/a_1.svg create mode 100644 assets/icons/a_2.svg delete mode 100644 assets/icons/b1.svg create mode 100644 assets/icons/b_1.svg create mode 100644 assets/icons/b_2.svg create mode 100644 firebase.json create mode 100644 lib/firebase_options.dart delete mode 100644 lib/models/question.dart delete mode 100644 lib/models/question.g.dart create mode 100644 lib/services/google_auth_service.dart create mode 100644 lib/services/image_downloader_service.dart create mode 100644 lib/ui/widgets/refresh_button.dart create mode 100644 test/services/google_auth_service_test.dart create mode 100644 test/services/image_downloader_service_test.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 19de2ad..005d2b7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,12 +1,12 @@ plugins { - id("com.android.application") id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("com.android.application") + id("com.google.gms.google-services") id("dev.flutter.flutter-gradle-plugin") } android { - namespace = "com.example.yimaru_app" + namespace = "com.yimaru.lms.app" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -15,25 +15,24 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + } } + defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.yimaru_app" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion + applicationId = "com.yimaru.lms.app" versionCode = flutter.versionCode versionName = flutter.versionName + targetSdk = flutter.targetSdkVersion + } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") } } diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..cd2ee71 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,51 @@ +{ + "project_info": { + "project_number": "574860813475", + "project_id": "yimaru-lms-e834e", + "storage_bucket": "yimaru-lms-e834e.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:574860813475:android:cd7fa6cf3a0527d97acb16", + "android_client_info": { + "package_name": "com.yimaru.lms.app" + } + }, + "oauth_client": [ + { + "client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.yimaru.lms.app", + "certificate_hash": "fc91f52846d27c62bba3e16bc98982fb9953eca1" + } + }, + { + "client_id": "574860813475-631s3mo8ha2qc2jeb5e2aosn0967niik.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.yimaru.lms.app", + "certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a" + } + } + ], + "api_key": [ + { + "current_key": "AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1dc40a9..e39aa8a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ + + + + diff --git a/assets/icons/a_2.svg b/assets/icons/a_2.svg new file mode 100644 index 0000000..9bdbd2e --- /dev/null +++ b/assets/icons/a_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/b1.svg b/assets/icons/b1.svg deleted file mode 100644 index 4a75083..0000000 --- a/assets/icons/b1.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/b_1.svg b/assets/icons/b_1.svg new file mode 100644 index 0000000..85f988e --- /dev/null +++ b/assets/icons/b_1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/b_2.svg b/assets/icons/b_2.svg new file mode 100644 index 0000000..40dfb5d --- /dev/null +++ b/assets/icons/b_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..4d7ff66 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-lms-e834e","appId":"1:574860813475:android:cd7fa6cf3a0527d97acb16","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-lms-e834e","configurations":{"android":"1:574860813475:android:cd7fa6cf3a0527d97acb16","ios":"1:574860813475:ios:3ac9f7c4ae1771287acb16"}}}}}} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 79e3510..f72da0a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -368,7 +368,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -384,7 +384,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -401,7 +401,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -416,7 +416,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -547,7 +547,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; + PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/lib/app/app.dart b/lib/app/app.dart index 2b42eff..0130a88 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -33,6 +33,8 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; import 'package:yimaru_app/ui/views/failure/failure_view.dart'; import 'package:yimaru_app/services/permission_handler_service.dart'; import 'package:yimaru_app/services/image_picker_service.dart'; +import 'package:yimaru_app/services/google_auth_service.dart'; +import 'package:yimaru_app/services/image_downloader_service.dart'; // @stacked-import @StackedApp( @@ -74,6 +76,8 @@ import 'package:yimaru_app/services/image_picker_service.dart'; LazySingleton(classType: StatusCheckerService), LazySingleton(classType: PermissionHandlerService), LazySingleton(classType: ImagePickerService), + LazySingleton(classType: GoogleAuthService), + LazySingleton(classType: ImageDownloaderService), // @stacked-service ], bottomsheets: [ diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index ccdd10f..77ac6ff 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -14,6 +14,8 @@ import 'package:stacked_shared/stacked_shared.dart'; import '../services/api_service.dart'; import '../services/authentication_service.dart'; import '../services/dio_service.dart'; +import '../services/google_auth_service.dart'; +import '../services/image_downloader_service.dart'; import '../services/image_picker_service.dart'; import '../services/permission_handler_service.dart'; import '../services/secure_storage_service.dart'; @@ -40,4 +42,6 @@ Future setupLocator({ locator.registerLazySingleton(() => StatusCheckerService()); locator.registerLazySingleton(() => PermissionHandlerService()); locator.registerLazySingleton(() => ImagePickerService()); + locator.registerLazySingleton(() => GoogleAuthService()); + locator.registerLazySingleton(() => ImageDownloaderService()); } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..f2470b8 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,70 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk', + appId: '1:574860813475:android:cd7fa6cf3a0527d97acb16', + messagingSenderId: '574860813475', + projectId: 'yimaru-lms-e834e', + storageBucket: 'yimaru-lms-e834e.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyBBcQ17JB6RBTjD7G7mh6Xf_FMUGxP5cC8', + appId: '1:574860813475:ios:3ac9f7c4ae1771287acb16', + messagingSenderId: '574860813475', + projectId: 'yimaru-lms-e834e', + storageBucket: 'yimaru-lms-e834e.firebasestorage.app', + androidClientId: + '574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com', + iosBundleId: 'com.yimaru.lms.app', + ); +} diff --git a/lib/models/assessment.dart b/lib/models/assessment.dart index e1cc5e5..9a6da65 100644 --- a/lib/models/assessment.dart +++ b/lib/models/assessment.dart @@ -1,17 +1,35 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:yimaru_app/models/option.dart'; -import 'package:yimaru_app/models/question.dart'; part 'assessment.g.dart'; @JsonSerializable() class Assessment { - @JsonKey(name: 'Question') - final Question? question; + final int? id; + + final int? points; + + final String? status; + + @JsonKey(name: 'question_type') + final String? questionType; + + @JsonKey(name: 'question_text') + final String? questionText; + + @JsonKey(name: 'difficulty_level') + final String? difficultyLevel; - @JsonKey(name: 'Options') final List