diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index 9fd27c39..1bbfbbc7 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -243,6 +243,7 @@ class DiaryCubit extends Cubit { required int quantity, int? productNutritionalValueId, }) async { + emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true)); await diaryRepository.addPortionToMeal( diaryId: diaryId, mealId: mealId, diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart index d5e8e39d..4779e2ea 100644 --- a/lib/features/diary/presentation/bloc/diary_state.dart +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -29,6 +29,7 @@ abstract class DiaryState with _$DiaryState { @Default(false) bool isDiaryCommented, @Default(false) bool isWakeUpAndRetireEnabled, @Default(false) bool isMoodEnabled, + @Default(false) bool isLoadingAddingProductInMeal, }) = DiaryStateLoaded; const factory DiaryState.loadedTomorrowWithDiet({ diff --git a/lib/features/diary/presentation/widgets/product_loader_widget.dart b/lib/features/diary/presentation/widgets/product_loader_widget.dart new file mode 100644 index 00000000..7b679354 --- /dev/null +++ b/lib/features/diary/presentation/widgets/product_loader_widget.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ProductLoaderWidget extends StatefulWidget { + const ProductLoaderWidget({ + super.key, + }); + + @override + State createState() => _ProductLoaderWidgetState(); +} + +class _ProductLoaderWidgetState extends State { + @override + Widget build(BuildContext context) { + return ListTile( + horizontalTitleGap: 0, + contentPadding: const EdgeInsets.only(right: 24, left: 12), + title: const CupertinoActivityIndicator(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + tileColor: const Color(0xFFF6FAFF), + ); + } +} diff --git a/lib/features/diary/presentation/widgets/products_list_widget.dart b/lib/features/diary/presentation/widgets/products_list_widget.dart index 13bbe7fc..6882af08 100644 --- a/lib/features/diary/presentation/widgets/products_list_widget.dart +++ b/lib/features/diary/presentation/widgets/products_list_widget.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart'; import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -25,6 +26,11 @@ class _ProductsListWidgetState extends State { ProductWidget(portion: state.diary.meals[mealIndex].portions[i]), i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], + if (state.isLoadingAddingProductInMeal) + const Padding( + padding: EdgeInsets.only(top: 8), + child: ProductLoaderWidget(), + ), ], ); } diff --git a/lib/features/profile/models/profile_model.dart b/lib/features/profile/models/profile_model.dart index 72e91d0d..f798021f 100644 --- a/lib/features/profile/models/profile_model.dart +++ b/lib/features/profile/models/profile_model.dart @@ -14,6 +14,7 @@ abstract class ProfileModel with _$ProfileModel { @JsonKey(name: "email") required String email, @JsonKey(name: "rating_place") int? ratingPlace, @JsonKey(name: "email_verified_at") DateTime? emailVerifiedAt, + @JsonKey(name: "phone_confirmation_state") required String phoneConfirmationState, @JsonKey(name: "created_at") DateTime? createdAt, @JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 07ab1bd2..68213da5 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -138,7 +138,7 @@ class _ProfileScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ _header(state), - if (state.profile.roles.contains('consultant')) + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -202,10 +202,10 @@ class _ProfileScreenState extends State { ), _subscriptionsAndSubscribers(state), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), - if (state.profile.roles.contains('consultant')) _socialLinks(state), - if (state.profile.roles.contains('consultant')) _education(state), - if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), - if (state.profile.roles.contains('consultant')) _activePlans(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state), + if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _activePlans(state), _blogPosts(state), ], ), @@ -216,7 +216,9 @@ class _ProfileScreenState extends State { Scaffold _myProfileScreen(ProfileStateLoaded state) { return Scaffold( backgroundColor: Colors.grey.shade200, - appBar: AppBar(), + appBar: AppBar( + toolbarHeight: widget.author == null ? 0 : null, + ), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -644,7 +646,7 @@ class _ProfileScreenState extends State { } SingleChildRenderObjectWidget _socialLinks(ProfileStateLoaded state) { - if (!state.profile.roles.contains('consultant')) { + if (!state.profile.roles.contains('consultant') && state.consultantInfo != null) { return const SizedBox.shrink(); } return Padding( diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index fa2948bc..c9426674 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -60,10 +60,11 @@ class RegistrationCubit extends Cubit { ), ); await TokenStorage.saveTokenInStorage(token); - AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen'); await sl().registerPushToken(); + AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen'); } catch (e, st) { log(e.toString(), stackTrace: st); + emit(RegistrationState(sheet: state.sheet, error: e.toString())); } } diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart index e4347d20..6a280864 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_state.dart @@ -7,5 +7,6 @@ part 'registration_state.freezed.dart'; abstract class RegistrationState with _$RegistrationState { const factory RegistrationState({ required UserRegistrationSheet sheet, + String? error, }) = _RegistrationState; } diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 69ae62d5..86514be8 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -43,142 +43,161 @@ class _RegistrationScreenState extends State { Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => registrationCubit, - child: BlocBuilder( - builder: (BuildContext context, RegistrationState state) { - return Scaffold( - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: Row( - children: [ - Container( - height: 48, - width: MediaQuery.of(context).size.width * 0.2, - padding: const EdgeInsets.only(left: 16, right: 4), - child: FloatingActionButton.extended( - heroTag: 'backButtonRegistration', - highlightElevation: 0, - focusElevation: 0, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - backgroundColor: const Color(0xFFEAF2FD), - onPressed: () async { - setState(() { - pageController.previousPage( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - ); - }); - }, - label: Icon( - Icons.arrow_back_ios_new_rounded, - color: appTheme.blueA400, - ), + child: BlocListener( + listener: (context, state) { + if (state.error != null && state.error!.isNotEmpty) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Ошибка при регистрации'), + content: Text(state.error!), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), ), - ), - Container( - height: 48, - width: MediaQuery.of(context).size.width * 0.8, - padding: const EdgeInsets.only(left: 4, right: 16), - child: FloatingActionButton.extended( - heroTag: 'nextButtonRegistration', - highlightElevation: 0, - elevation: 0, - focusElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - backgroundColor: appTheme.blueA400, - onPressed: () async { - if (await canGoNext(state, context)) { + ], + ), + ); + } + }, + child: BlocBuilder( + builder: (BuildContext context, RegistrationState state) { + return Scaffold( + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + floatingActionButton: Row( + children: [ + Container( + height: 48, + width: MediaQuery.of(context).size.width * 0.2, + padding: const EdgeInsets.only(left: 16, right: 4), + child: FloatingActionButton.extended( + heroTag: 'backButtonRegistration', + highlightElevation: 0, + focusElevation: 0, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: const Color(0xFFEAF2FD), + onPressed: () async { setState(() { - pageController.nextPage( + pageController.previousPage( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, ); }); - } - }, - label: Text( - 'Продолжить', - style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), + }, + label: Icon( + Icons.arrow_back_ios_new_rounded, + color: appTheme.blueA400, + ), ), ), - ), - ], - ), - appBar: AppBar( - automaticallyImplyLeading: false, - leading: Builder( - builder: (context) => IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => GoRouter.of(context).pop(), - ), - ), - centerTitle: true, - title: SvgPicture.asset( - ImageConstant.imgLogoEatvio, - height: 48, - fit: BoxFit.contain, - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: Text('шаг $step из 15'), - ), - ], - bottom: PreferredSize( - preferredSize: Size(MediaQuery.of(context).size.width, 32), - child: Padding( - padding: const EdgeInsets.all(16), - child: TweenAnimationBuilder( - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - tween: Tween( - begin: 1 / 15, - end: progressValue, + Container( + height: 48, + width: MediaQuery.of(context).size.width * 0.8, + padding: const EdgeInsets.only(left: 4, right: 16), + child: FloatingActionButton.extended( + heroTag: 'nextButtonRegistration', + highlightElevation: 0, + elevation: 0, + focusElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: appTheme.blueA400, + onPressed: () async { + if (await canGoNext(state, context)) { + setState(() { + pageController.nextPage( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + ); + }); + } + }, + label: Text( + 'Продолжить', + style: theme.textTheme.titleSmall!.copyWith(color: Colors.white), + ), ), - builder: (context, value, _) => LinearProgressIndicator( - value: value, - borderRadius: BorderRadius.circular(16), - minHeight: 8, - backgroundColor: Colors.grey.shade100, - valueColor: AlwaysStoppedAnimation(appTheme.blueA400), + ), + ], + ), + appBar: AppBar( + automaticallyImplyLeading: false, + leading: Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => GoRouter.of(context).pop(), + ), + ), + centerTitle: true, + title: SvgPicture.asset( + ImageConstant.imgLogoEatvio, + height: 48, + fit: BoxFit.contain, + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: Text('шаг $step из 15'), + ), + ], + bottom: PreferredSize( + preferredSize: Size(MediaQuery.of(context).size.width, 32), + child: Padding( + padding: const EdgeInsets.all(16), + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + tween: Tween( + begin: 1 / 15, + end: progressValue, + ), + builder: (context, value, _) => LinearProgressIndicator( + value: value, + borderRadius: BorderRadius.circular(16), + minHeight: 8, + backgroundColor: Colors.grey.shade100, + valueColor: AlwaysStoppedAnimation(appTheme.blueA400), + ), ), ), ), ), - ), - body: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: pageController, - onPageChanged: (index) { - setState(() { - step = index + 1; - progressValue = step / 15; - }); - }, - children: [ - // LanguageSelectScreen(sheet: state.sheet), - PhoneScreen(sheet: state.sheet), - CredentialsScreen(sheet: state.sheet), - NameScreen(sheet: state.sheet), - GenderScreen(sheet: state.sheet), - DateOfBirthScreen(sheet: state.sheet), - CityOfLivingScreen(sheet: state.sheet), - BodyCircumferencesScreen(sheet: state.sheet), - WeightScreen(sheet: state.sheet), - HeightScreen(sheet: state.sheet), - BloodPressureScreen(sheet: state.sheet), - AllergiesScreen(sheet: state.sheet), - MedicineIntakeScreen(sheet: state.sheet), - FavoriteSportsScreen(sheet: state.sheet), - FavoriteFoodsScreen(sheet: state.sheet), - AdditionalInformationScreen(sheet: state.sheet), - ], - ), - ); - }, + body: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: pageController, + onPageChanged: (index) { + setState(() { + step = index + 1; + progressValue = step / 15; + }); + }, + children: [ + // LanguageSelectScreen(sheet: state.sheet), + PhoneScreen(sheet: state.sheet), + CredentialsScreen(sheet: state.sheet), + NameScreen(sheet: state.sheet), + GenderScreen(sheet: state.sheet), + DateOfBirthScreen(sheet: state.sheet), + CityOfLivingScreen(sheet: state.sheet), + BodyCircumferencesScreen(sheet: state.sheet), + WeightScreen(sheet: state.sheet), + HeightScreen(sheet: state.sheet), + BloodPressureScreen(sheet: state.sheet), + AllergiesScreen(sheet: state.sheet), + MedicineIntakeScreen(sheet: state.sheet), + FavoriteSportsScreen(sheet: state.sheet), + FavoriteFoodsScreen(sheet: state.sheet), + AdditionalInformationScreen(sheet: state.sheet), + ], + ), + ); + }, + ), ), ); } diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 7f8aede5..637dba5b 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; @@ -19,6 +20,7 @@ class VerifyScreen extends StatefulWidget { class _VerifyScreenState extends State { late final WebViewController _controller; + bool _isLoading = true; @override void initState() { @@ -27,6 +29,11 @@ class _VerifyScreenState extends State { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( + onPageFinished: (url) { + setState(() { + _isLoading = false; + }); + }, onNavigationRequest: (NavigationRequest request) async { if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) { GoRouter.of(context).pop(); @@ -105,11 +112,17 @@ class _VerifyScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - toolbarHeight: 0, - ), + appBar: AppBar(toolbarHeight: 0), body: SafeArea( - child: WebViewWidget(controller: _controller), + child: Stack( + children: [ + WebViewWidget(controller: _controller), + if (_isLoading) + const Center( + child: Loader(), + ), + ], + ), ), ); } diff --git a/lib/features/registration/repositories/registration_repository.dart b/lib/features/registration/repositories/registration_repository.dart index 0c3008e0..a34bf1b0 100644 --- a/lib/features/registration/repositories/registration_repository.dart +++ b/lib/features/registration/repositories/registration_repository.dart @@ -1,7 +1,5 @@ - import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/models/location_model.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; @@ -14,13 +12,55 @@ class RegistrationRepository { Future tryRegister(UserRegistrationSheet sheet) async { try { final response = await registrationService.register(sheet: sheet.toJson()); - try { - return (response['token']!); - } catch (e) { - showErrorToast(response.toString()); - rethrow; + if (response.containsKey('token')) { + return response['token']!; + } else { + // Если нет токена, но есть message или errors + String errorMsg = ''; + if (response.containsKey('message')) { + errorMsg += '${response['message']!}\n'; + } + if (response.containsKey('errors')) { + final errors = response['errors']; + if (errors != null && errors is Map) { + (errors as Map).forEach((key, value) { + if (value is List && value.isNotEmpty) { + errorMsg += '${value.join('\n')}\n'; + } + }); + } + } + if (errorMsg.isNotEmpty) { + throw errorMsg.trim(); + } else { + throw 'Ошибка регистрации: $response'; + } + } + } on DioException catch (e) { + if (e.response != null && e.response?.statusCode == 422) { + final data = e.response?.data; + String errorMsg = ''; + if (data is Map) { + if (data.containsKey('message')) { + errorMsg += data['message'] + '\n'; + } + if (data.containsKey('errors')) { + final errors = data['errors']; + if (errors != null && errors is Map) { + (errors).forEach((key, value) { + if (value is List && value.isNotEmpty) { + errorMsg += '${value.join('\n')}\n'; + } + }); + } + } + } + if (errorMsg.isNotEmpty) { + throw errorMsg.trim(); + } else { + throw 'Ошибка регистрации: $data'; + } } - } catch (e) { rethrow; } } diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart index 3cb047f6..ae498824 100644 --- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart +++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart @@ -22,7 +22,11 @@ class SettingsCubit extends Cubit { final profile = await profileRepository.getMyProfile(); late final ConsultationRequestModel? consultantInfo; try { - consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); + if (profile.phoneConfirmationState.contains('confirmed')) { + consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug); + } else { + consultantInfo = null; + } } catch (e) { consultantInfo = null; } diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart index b2c51b01..106da096 100644 --- a/test/features/profile/services/profile_service_test.dart +++ b/test/features/profile/services/profile_service_test.dart @@ -19,6 +19,7 @@ void main() { id: 0, name: 'Alexander Avanesyan', email: 'mso7nueh@mail.ru', + phoneConfirmationState: '', isPublicProfile: true, isEntitySubscribedByMe: false, isBlockedByMe: false,