diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 9d810d8e..4320ecc5 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -20,6 +20,7 @@ jobs: - name: Flutter build run: | + echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8 flutter clean flutter pub get flutter pub upgrade @@ -31,6 +32,8 @@ jobs: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} + FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane FASTLANE_USER: eatvio2023@icloud.com - FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} \ No newline at end of file + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} \ No newline at end of file diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 75372b09..fd291367 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.32.5" + flutter_version: "3.32.8" java_version: "21.x" flutter_channel: "stable" my_local_cache_dir: "/opt/hostedtoolcache/" diff --git a/android/app/build.gradle b/android/app/build.gradle index a5377cc6..c9fe6d27 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -19,17 +19,17 @@ android { ndkVersion = "27.0.12077973" compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = 21 + jvmTarget = JavaVersion.VERSION_11.toString() } defaultConfig { applicationId = "com.eatvioapp.app" - minSdk = flutter.minSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/gradle.properties b/android/gradle.properties index 25971708..f018a618 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f587a472..ac3b4792 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 833c476b..29eab0d3 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -22,7 +22,7 @@ plugins { // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.4.2" apply false // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "2.0.21" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d2269b71..9a27847e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -213,10 +213,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -283,10 +287,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -388,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 64; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -406,7 +414,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; @@ -538,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 64; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -556,7 +564,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; @@ -576,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 64; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -594,7 +602,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.50; + MARKETING_VERSION = 1.2.3; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6de3b03b..2341e63f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 52 + 64 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 4073b355..9340117d 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -18,9 +18,10 @@ default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do - match(type: "appstore") + match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") + app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") upload_to_testflight end end diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index c86e2b65..0d039029 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,24 @@ - + - + - + - + + + diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 9dc8612f..c691b0b2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,5 +1,3 @@ -import 'dart:io'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -121,7 +119,9 @@ class AppRouter { GoRoute( path: 'forget-password-screen', name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool,), + builder: (context, state) => ForgetPasswordScreen( + isAuth: state.extra as bool, + ), ), GoRoute( path: 'scan-qr-code-screen', @@ -205,21 +205,9 @@ class AppRouter { builder: (context, state) => const WelcomeScreen(), redirect: (context, state) async { try { - if (await TokenStorage.haveTokenInStorage()) { - final firebaseApi = sl(); - final fbToken = await firebaseApi.getToken(); - final deviceInfo = DeviceInfoPlugin(); - await sl().post( - '/api/profile/notification/token/register', - data: { - 'platform': Platform.isIOS ? 'ios' : 'android', - 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, - 'token': fbToken, - }, - ); - } final sharedPreferences = await SharedPreferences.getInstance(); final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; + await sl().registerPushToken(); if (welcomeScreenWasShowed == false) { return null; } else if (await TokenStorage.haveTokenInStorage()) { @@ -228,7 +216,7 @@ class AppRouter { return '/auth-screen'; } } catch (e) { - rethrow; + return '/feed-screen'; } }, ), diff --git a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart index deeea5ff..5506215d 100644 --- a/lib/core/comments/presentation/comments_actions_bottom_sheet.dart +++ b/lib/core/comments/presentation/comments_actions_bottom_sheet.dart @@ -23,7 +23,7 @@ void showCommentsActionsBottomSheet( isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => Container( - margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom), + margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700, @@ -189,11 +189,12 @@ class _ActionWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ - Text( - text, - style: TextStyles.textLMedium.copyWith(color: appTheme.black), + Expanded( + child: Text( + text, + style: TextStyles.textLMedium.copyWith(color: appTheme.black), + ), ), - const Spacer(), SvgPicture.asset(iconPath) ], ), diff --git a/lib/core/const.dart b/lib/core/const.dart index 31c47e9c..f22f79aa 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ String baseUrl = 'https://eatvio.ru'; -// String baseUrl = 'https://dev2.eatvio.com'; +bool hidePlans = true; const List durationValues = [7, 14, 21]; diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index fbf8e5c1..572b56f6 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -1,3 +1,10 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/token_storage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; class FirebaseApi { @@ -7,4 +14,22 @@ class FirebaseApi { await _firebaseMessaging.requestPermission(); return await _firebaseMessaging.getToken(); } + + Future registerPushToken() async { + if (await TokenStorage.haveTokenInStorage()) { + try { + final deviceInfo = DeviceInfoPlugin(); + await sl().post( + '/api/profile/notification/token/register', + data: { + 'platform': Platform.isIOS ? 'ios' : 'android', + 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, + 'token': await getToken(), + }, + ); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + } + } } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 10fb4172..f5fd7a34 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; @@ -66,6 +67,7 @@ class _AuthScreenState extends State { await sl().connectToWebSocket(); await sl().getNotifications(); await sl().connectToWebSocket(); + await sl().registerPushToken(); showSuccessToast('Успешный вход'); } catch (e) { rethrow; diff --git a/lib/features/chat/models/chat_room_model.dart b/lib/features/chat/models/chat_room_model.dart index dc091ea6..c5ee559f 100644 --- a/lib/features/chat/models/chat_room_model.dart +++ b/lib/features/chat/models/chat_room_model.dart @@ -9,7 +9,7 @@ part 'chat_room_model.g.dart'; @freezed abstract class ChatRoomModel with _$ChatRoomModel { const factory ChatRoomModel({ - required String id, + required String id, @JsonKey(name: 'chat_room_setting') required ChatRoomSettingsModel chatRoomSettings, required Users users, @JsonKey(name: 'chosen_plan_id') required int? chosenPlanId, @@ -22,13 +22,9 @@ abstract class ChatRoomModel with _$ChatRoomModel { @freezed abstract class Users with _$Users { const factory Users({ - @JsonKey(name: 'from_chat_user') required ChatUserModel sender, @JsonKey(name: 'to_chat_user') required ChatUserModel receiver, - }) = _Users; factory Users.fromJson(Map json) => _$UsersFromJson(json); } - - diff --git a/lib/features/chat/models/chat_room_settings_model.dart b/lib/features/chat/models/chat_room_settings_model.dart index fa332843..737d0c82 100644 --- a/lib/features/chat/models/chat_room_settings_model.dart +++ b/lib/features/chat/models/chat_room_settings_model.dart @@ -6,7 +6,7 @@ part 'chat_room_settings_model.g.dart'; @freezed abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel { const factory ChatRoomSettingsModel({ - required String name, + @JsonKey(name: 'room_name') required String name, required String state, }) = _ChatRoomSettingsModel; diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index f4c95d12..c4d8ed45 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -223,6 +223,7 @@ class ChatRoomCubit extends Cubit { files: compressedFiles, slug: userSlug, ); + sl().getChatRooms(); return true; } catch (e, st) { log(e.toString(), stackTrace: st); diff --git a/lib/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart index 260c051d..1c17d40e 100644 --- a/lib/features/chat/presentation/pages/photo_view_screen.dart +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:eatvio_app/core/const.dart'; @@ -11,7 +9,7 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:url_launcher/url_launcher.dart'; class PhotoViewScreen extends StatefulWidget { - final List imageUrls; + final List imageUrls; final int index; final bool isLocal; @@ -50,14 +48,12 @@ class _PhotoViewScreenState extends State { ), scrollPhysics: const BouncingScrollPhysics(), builder: (BuildContext context, int index) { + final imageProvider = widget.isLocal + ? FileImage(widget.imageUrls[index]) as ImageProvider + : CachedNetworkImageProvider(widget.imageUrls[index]) as ImageProvider; + return PhotoViewGalleryPageOptions( - imageProvider: widget.isLocal - ? FileImage( - File(widget.imageUrls[index]), - ) as ImageProvider - : CachedNetworkImageProvider( - widget.imageUrls[index], - ), + imageProvider: imageProvider, initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, heroAttributes: PhotoViewHeroAttributes( diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index d2f5f1bd..60a31c44 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -110,7 +110,7 @@ class ChatBubbleWidget extends StatelessWidget { onTap: () { context.pushTransparentRoute( PhotoViewScreen( - imageUrls: mediaData.map((e) => '$e').toList(), + imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false, ), diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index d682c9a0..b1961b2a 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -63,7 +63,8 @@ class ChatRepository { Future getChatRoom(String uuid) async { try { return (await chatService.getChatRoom(uuid: uuid)); - } catch (e) { + } catch (e, st) { + log(e.toString(), stackTrace: st); rethrow; } } diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index dee63eab..89d65749 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -13,18 +13,18 @@ part 'chat_service.g.dart'; abstract class ChatService { factory ChatService(Dio dio, {String baseUrl}) = _ChatService; - @GET('/api/chat/index') + @GET('/api/v2/chat/index') Future getChatRooms({ @Query('page') required int page, - @Query('find') String? query, + @Query('filters[data][\$conversation_name]') String? query, }); - @GET('/api/chat/show/{uuid}') + @GET('/api/v2/chat/show/{uuid}') Future getChatRoom({ @Path() required String uuid, }); - @POST('/api/chat/message/send/{slug}') + @POST('/api/v2/chat/message/send/{slug}') @MultiPart() Future sendMessage({ @Header('X-Socket-ID') required String socketId, @@ -33,13 +33,13 @@ abstract class ChatService { @Part(name: 'media_data[]', contentType: 'multipart/form-data') List? mediaData, }); - @GET('/api/chat/show/{id}/messages?page={page}') + @GET('/api/v2/chat/show/{id}/messages?page={page}') Future getChatMessages({ @Path() required String id, @Path() required int page, }); - @GET('/api/chat/get_by_user/{user}') + @GET('/api/v2/chat/get_by_user/{user}') Future getChatRoomIdByUserSlug({ @Path() required String user, }); @@ -82,7 +82,7 @@ abstract class ChatService { @Body() required PlanComplainModel body, }); - @GET('/api/v2/chat/get_by_user/{slug}') + @GET('/api/chat/get_by_user/{slug}') Future getChatByUserSlug({ @Path() required String slug, }); diff --git a/lib/features/complaining/presentation/complaining_bottom_sheet.dart b/lib/features/complaining/presentation/complaining_bottom_sheet.dart index e3f42616..aa5e1b90 100644 --- a/lib/features/complaining/presentation/complaining_bottom_sheet.dart +++ b/lib/features/complaining/presentation/complaining_bottom_sheet.dart @@ -9,6 +9,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -194,63 +195,74 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> { @override Widget build(BuildContext context) { final s = S.of(context); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const BottomSheetHandlerBar(), - const SizedBox(height: 20), - Text( - s.lawViolation, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - const SizedBox(height: 8), - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.7, - child: Text( - s.what_do_you_want_to_complaint_about, - textAlign: TextAlign.center, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - ), - const SizedBox(height: 24), - ...tags.mapIndexed((index, element) { - return Row( - children: [ - Checkbox( - activeColor: appTheme.blueA400, - side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), - value: element.$1, - onChanged: (isChecked) { - setState(() { - if (isChecked == null) return; - if (index == tags.length - 1 && !isChecked) { - context.read().toggleDescription(); - } - tags[index] = (isChecked, element.$2); - final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); - context.read().setTags(activeTags); - }); + return BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const BottomSheetHandlerBar(), + const SizedBox(height: 20), + Text( + s.lawViolation, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + const SizedBox(height: 8), + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.7, + child: Text( + s.what_do_you_want_to_complaint_about, + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + const SizedBox(height: 24), + ...tags.mapIndexed((index, element) { + return Row( + children: [ + Checkbox( + activeColor: appTheme.blueA400, + side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), + value: element.$1, + onChanged: (isChecked) { + setState(() { + if (isChecked == null) return; + if (index == tags.length - 1 && !isChecked) { + context.read().toggleDescription(); + } + tags[index] = (isChecked, element.$2); + final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); + context.read().setTags(activeTags); + }); + }, + ), + Text( + element.$2, + style: TextStyles.textLSemibold.copyWith(color: appTheme.black), + ), + ], + ); + }), + if (tags.last.$1) ...[ + const SizedBox(height: 12), + CustomTextFormField( + hintText: s.type_here_your_complaint, + hasError: (state.complainInfo.description?.length ?? 10) < 10, + onChanged: (value) { + context.read().setDescription(value); }, ), - Text( - element.$2, - style: TextStyles.textLSemibold.copyWith(color: appTheme.black), - ), + if ((state.complainInfo.description?.length ?? 10) < 10) ...[ + const SizedBox(height: 4), + const ValidationErrorWidget(error: 'Минимум 10 символов'), + ] ], - ); - }), - if (tags.last.$1) ...[ - const SizedBox(height: 12), - CustomTextFormField( - hintText: s.type_here_your_complaint, - onChanged: (value) { - context.read().setDescription(value); - }, - ), - ], - const SizedBox(height: 20), - const _ComplainButton(), - ], + const SizedBox(height: 20), + _ComplainButton( + isDisabled: (state.complainInfo.description?.length ?? 0) < 10, + ), + ], + ); + }, ); } } @@ -515,8 +527,8 @@ class _ComplaintFailedWidget extends StatelessWidget { } class _ComplainButton extends StatelessWidget { - const _ComplainButton(); - + const _ComplainButton({this.isDisabled = false}); + final bool isDisabled; @override Widget build(BuildContext context) { final s = S.of(context); @@ -534,6 +546,10 @@ class _ComplainButton extends StatelessWidget { backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), ), onPressed: () async { + if (isDisabled) { + context.read().setDescription(''); + return; + } await context.read().complain(); }, ); diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 5173ab2d..04a0b7fb 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -302,7 +302,6 @@ class _ConsultationCardWidgetState extends State { 'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1, }); - GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id); return; } showErrorDialog(context, diff --git a/lib/features/create_recipe/reporitories/recipe_repository.dart b/lib/features/create_recipe/reporitories/recipe_repository.dart index f0f860e9..289233a2 100644 --- a/lib/features/create_recipe/reporitories/recipe_repository.dart +++ b/lib/features/create_recipe/reporitories/recipe_repository.dart @@ -57,7 +57,7 @@ class RecipeRepository { 'type': recipe.type, 'state': 'active', 'title': recipe.title, - 'tags': recipe.tags, + 'tags[]': recipe.tags ?? [], 'description_md': recipe.description, 'video_url': recipe.videoUrl, 'ingredients': ingredientsMap, diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index c973bcaa..1bbfbbc7 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -179,11 +179,14 @@ class DiaryCubit extends Cubit { int? currentPlanDay; String? planName; if (diaryDay.chosenPlanId != null) { - consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!); + if (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented) { + await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id); + } + consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); planDuration = chosenPlanModel.plan.duration; planName = chosenPlanModel.plan.name; - currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1; + currentPlanDay = consultantReview.currentPlanDay; } final nutrition = await getNutrition(diaryDay.id); @@ -194,20 +197,19 @@ class DiaryCubit extends Cubit { diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; emit( DiaryState.loaded( - diary: diaryDay, - nutrition: nutrition, - selectedDay: selectedDay, - selectedMeal: selectedMeal, - isConsultantView: this.diaryId != null, - consultantReview: consultantReview, - isDiaryFinished: isDiaryFinished, - isDiaryCommented: isDiaryCommented, - chosenPlanCurrentDay: currentPlanDay, - chosenPlanDuration: planDuration, - chosenPlanName: planName, - isMoodEnabled: isMoodEnabled, - isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled - ), + diary: diaryDay, + nutrition: nutrition, + selectedDay: selectedDay, + selectedMeal: selectedMeal, + isConsultantView: this.diaryId != null, + consultantReview: consultantReview, + isDiaryFinished: isDiaryFinished, + isDiaryCommented: isDiaryCommented, + chosenPlanCurrentDay: currentPlanDay, + chosenPlanDuration: planDuration, + chosenPlanName: planName, + isMoodEnabled: isMoodEnabled, + isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled), ); } catch (e, st) { log(e.toString(), stackTrace: st); @@ -241,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/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index e807e3d0..2ac30803 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -297,4 +297,12 @@ class DiaryRepository { rethrow; } } + + Future viewCommentOfDiaryDayByConsultant({required int diaryId}) async { + try { + await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index bb6672d3..d9967200 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -150,4 +150,9 @@ abstract class DiaryService { @Path() required int planId, @Path() required int diaryId, }); + + @POST('/api/diary/{diaryId}/consultant/comment/view') + Future viewCommentOfDiaryDayByConsultant({ + @Path() required int diaryId, + }); } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index b6869040..ce29b884 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,4 +1,5 @@ import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; @@ -32,24 +33,12 @@ class FeedScreen extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider( - create: (context) => sl()..loadRecipesList(), - ), - BlocProvider( - create: (context) => sl()..getProfile(), - ), - BlocProvider( - create: (context) => sl()..loadFeedBlogPostList(), - ), - BlocProvider( - create: (context) => sl()..loadDiets(), - ), - BlocProvider( - create: (context) => sl()..loadPlans(), - ), - BlocProvider( - create: (context) => sl()..getConsultants(), - ), + BlocProvider(create: (context) => sl()..loadRecipesList()), + BlocProvider(create: (context) => sl()..getProfile()), + BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), + BlocProvider(create: (context) => sl()..loadDiets()), + if (!hidePlans) BlocProvider(create: (context) => sl()..loadPlans()), + BlocProvider(create: (context) => sl()..getConsultants()), ], child: const FeedScreenContent(), ); @@ -69,7 +58,8 @@ class _FeedScreenContentState extends State { final dietsController = TextEditingController(); final plansController = TextEditingController(); final consultantsController = TextEditingController(); - final List chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; + + late final List chipsLabelList; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); @@ -78,12 +68,13 @@ class _FeedScreenContentState extends State { final consultantsScrollController = ScrollController(); @override - void initState() { + void initState() { + // Initialize chipsLabelList based on hidePlans setting + chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; + consultantsScrollController.addListener(_onScrollConsultants); super.initState(); - } - - + } void _onScrollConsultants() { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} @@ -108,45 +99,36 @@ class _FeedScreenContentState extends State { heroTag: 'recipesTagsFilter', backgroundColor: appTheme.blueA400, shape: const CircleBorder(), - child: const Icon( - Icons.filter_list_rounded, - color: Colors.white, - ), + child: const Icon(Icons.filter_list_rounded, color: Colors.white), onPressed: () async { final recipesCubit = context.read(); await showModalBottomSheet( useRootNavigator: true, context: context, - builder: (context) => TagsModalWidget( - recipesCubit: recipesCubit, - ), + builder: (context) => TagsModalWidget(recipesCubit: recipesCubit), ); recipesCubit.loadRecipesList(); }, ) : null, - appBar: AppBar( - backgroundColor: Colors.white, - toolbarHeight: 0, - ), + appBar: AppBar(backgroundColor: Colors.white, toolbarHeight: 0), backgroundColor: Colors.grey.shade200, body: NestedScrollView( controller: switch (_selectedChipIndex) { 0 => blogPostsScrollController, 1 => recipesScrollController, 2 => dietsScrollController, - 3 => plansScrollController, - 4 => consultantsScrollController, - _ => blogPostsScrollController + 3 when !hidePlans => plansScrollController, + 3 when hidePlans => consultantsScrollController, + 4 when !hidePlans => consultantsScrollController, + _ => blogPostsScrollController, }, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ BlocBuilder( builder: (context, state) { return SliverAppBar( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), - ), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), backgroundColor: Colors.white, foregroundColor: Colors.white, automaticallyImplyLeading: false, @@ -172,78 +154,71 @@ class _FeedScreenContentState extends State { searchController: switch (_selectedChipIndex) { 1 => recipesController, 2 => dietsController, - 3 => plansController, - 4 => consultantsController, + 3 when !hidePlans => plansController, + 3 when hidePlans => consultantsController, + 4 when !hidePlans => consultantsController, _ => postsController, }, onChanged: (query) { if (query.length > 2) { - EasyDebounce.debounce( - 'searchFeed', - const Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - context.read().loadFeedBlogPostList(query: query); - break; - case 1: - context.read().loadRecipesList(query: query); - break; - case 2: - context.read().loadDiets(query: query); - break; - case 3: - context.read().loadPlans(query: query); - break; - case 4: - context.read().getConsultants(); - break; - } - }, - ); + EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () { + switch (_selectedChipIndex) { + case 0: + context.read().loadFeedBlogPostList(query: query); + break; + case 1: + context.read().loadRecipesList(query: query); + break; + case 2: + context.read().loadDiets(query: query); + break; + case 3 when !hidePlans: + context.read().loadPlans(query: query); + break; + case 3 when hidePlans: + case 4 when !hidePlans: + context.read().getConsultants(); + break; + } + }); } else { - EasyDebounce.debounce( - 'searchFeed', - const Duration(milliseconds: 500), - () { - switch (_selectedChipIndex) { - case 0: - context.read().loadFeedBlogPostList(); - break; - case 1: - context.read().loadRecipesList(); - break; - case 2: - context.read().loadDiets(); - break; - case 3: - context.read().loadPlans(); - break; - case 4: - context.read().getConsultants(); - break; - } - }, - ); + EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () { + switch (_selectedChipIndex) { + case 0: + context.read().loadFeedBlogPostList(); + break; + case 1: + context.read().loadRecipesList(); + break; + case 2: + context.read().loadDiets(); + break; + case 3 when !hidePlans: + context.read().loadPlans(); + break; + case 3 when hidePlans: + case 4 when !hidePlans: + context.read().getConsultants(); + break; + } + }); } }, ), const SizedBox(width: 12), switch (_selectedChipIndex) { - 1 => AddItemButton( - onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name), - ), + 1 => AddItemButton(onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name)), 2 => AddItemButton( - onTap: state.profile?.roles.contains('consultant') ?? false - ? () async { - await GoRouter.of(context).pushNamed(CreateDietScreen.name); - } - : null, - ), - 3 => AddItemButton( - onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, - ), - _ => const AddItemButton() + onTap: state.profile?.roles.contains('consultant') ?? false + ? () async { + await GoRouter.of(context).pushNamed(CreateDietScreen.name); + } + : null, + ), + 3 when !hidePlans => AddItemButton( + onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, + ), + _ => const AddItemButton(), }, ], ), @@ -256,8 +231,9 @@ class _FeedScreenContentState extends State { 0 => const BlogPosts(), 1 => const Recipes(), 2 => const Diets(), - 3 => const Plans(), - 4 => const Consultants(), + 3 when !hidePlans => const Plans(), + 3 when hidePlans => const Consultants(), + 4 when !hidePlans => const Consultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), }, ), diff --git a/lib/features/feed/presentation/widgets/chip_group.dart b/lib/features/feed/presentation/widgets/chip_group.dart index a8d88da1..183a5a76 100644 --- a/lib/features/feed/presentation/widgets/chip_group.dart +++ b/lib/features/feed/presentation/widgets/chip_group.dart @@ -2,12 +2,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget import 'package:flutter/widgets.dart'; class ChipGroup extends StatelessWidget { - const ChipGroup({ - super.key, - required this.selectedChipIndex, - required this.chipsLabelList, - required this.setSelectedChipIndex, - }); + const ChipGroup({super.key, required this.selectedChipIndex, required this.chipsLabelList, required this.setSelectedChipIndex}); final int selectedChipIndex; final List chipsLabelList; final void Function(int) setSelectedChipIndex; @@ -22,7 +17,7 @@ class ChipGroup extends StatelessWidget { child: Row( children: [ ...List.generate( - 5, + chipsLabelList.length, (index) => Padding( padding: const EdgeInsets.only(right: 8.0), child: ChoiceChipWidget( diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index e8489a5a..827a0904 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -106,8 +106,7 @@ class DiaryReminderNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; - - + DiaryReminderNotification({required this.type, required this.isRead}); factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { @@ -297,6 +296,25 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode } } +class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + + ChosenPlanLastDayFinishedForUser({ + required this.type, + required this.isRead, + }); + + factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead}) { + return ChosenPlanLastDayFinishedForUser( + type: type, + isRead: isRead ?? false, + ); + } +} + class UnknownNotification implements BaseNotificationModel { @override final NotificationType type; @@ -323,6 +341,7 @@ enum NotificationType { chosenPlanTomorrowReminderToConsultantNotification, planSubscriptionPaidToAuthor, chosenPlanFinishedToConsultant, + chosenPlanLastDayFinishedForUser, unknown; static NotificationType getTypeFromString(String type) => switch (type) { @@ -339,6 +358,7 @@ enum NotificationType { 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, + 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, _ => NotificationType.unknown, }; } diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 16cf1f40..61eba9a7 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -42,6 +42,7 @@ class NotificationsCubit extends Cubit { ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type), NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type), NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type), + NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(notificationJson, type), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 83157593..66b6cf18 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -53,6 +53,7 @@ class NotificationWidget extends StatelessWidget { ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget( notification: notification as ChosenPlanFinishedToConsultantNotification, ), + ChosenPlanLastDayFinishedForUser() => Text((notification as ChosenPlanLastDayFinishedForUser).type.toString()), UnknownNotification() => const SizedBox.shrink(), }; } diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index 860aab55..ec2aad74 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -40,6 +40,11 @@ class NotificationsService { PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + ), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { 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..211543a9 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -45,10 +45,7 @@ import 'package:url_launcher/url_launcher.dart'; class ProfileScreen extends StatefulWidget { final Author? author; - const ProfileScreen({ - super.key, - this.author, - }); + const ProfileScreen({super.key, this.author}); @override State createState() => _ProfileScreenState(); @@ -91,7 +88,7 @@ class _ProfileScreenState extends State { } if (isFirstFetch) { canSub = !state.profile.isEntitySubscribedByMe; - if (state.profile.roles.contains('consultant')) { + if (state.profile.roles.contains('consultant') && !hidePlans) { plansCubit.getPlans(state.profile.slug); } isFirstFetch = false; @@ -109,10 +106,7 @@ class _ProfileScreenState extends State { ), ); } else { - return Scaffold( - appBar: AppBar(), - body: const Loader(), - ); + return Scaffold(appBar: AppBar(), body: const Loader()); } }, ), @@ -130,15 +124,13 @@ class _ProfileScreenState extends State { Container( decoration: const BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(16), - ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)), ), child: Column( 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: [ @@ -147,28 +139,15 @@ class _ProfileScreenState extends State { const SizedBox(width: 16), const Text( 'Консультант', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color( - 0xFF323F4B, - ), - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFF6FAFF), - borderRadius: BorderRadius.circular(12), - ), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), child: Text( '${state.consultantInfo!.ratingPlace}-й в рейтинге', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: appTheme.blueA400, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400), ), ), ], @@ -177,17 +156,10 @@ class _ProfileScreenState extends State { const Row( children: [ SizedBox(width: 16), - Icon( - Icons.done_rounded, - color: Colors.green, - ), + Icon(Icons.done_rounded, color: Colors.green), Text( 'Медицинское образование', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600), ), ], ), @@ -202,10 +174,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 && !hidePlans) _activePlans(state), _blogPosts(state), ], ), @@ -216,7 +188,7 @@ 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, @@ -224,9 +196,7 @@ class _ProfileScreenState extends State { Container( decoration: const BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(16), - ), + borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -241,28 +211,15 @@ class _ProfileScreenState extends State { const SizedBox(width: 16), const Text( 'Консультант', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Color( - 0xFF323F4B, - ), - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFF6FAFF), - borderRadius: BorderRadius.circular(12), - ), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)), child: Text( '${state.consultantInfo!.ratingPlace}-й в рейтинге', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: appTheme.blueA400, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: appTheme.blueA400), ), ), ], @@ -271,17 +228,10 @@ class _ProfileScreenState extends State { const Row( children: [ SizedBox(width: 16), - Icon( - Icons.done_rounded, - color: Colors.green, - ), + Icon(Icons.done_rounded, color: Colors.green), Text( 'Медицинское образование', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 14, fontWeight: FontWeight.w600), ), ], ), @@ -297,7 +247,7 @@ class _ProfileScreenState extends State { if (state.profile.roles.contains('consultant')) _education(state), if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()), - if (state.profile.roles.contains('consultant')) _activePlans(state), + if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state), _newBlogPostButton(), _blogPosts(state), ], @@ -307,7 +257,7 @@ class _ProfileScreenState extends State { } Widget _activePlans(ProfileStateLoaded state) { - if (!state.profile.roles.contains('consultant')) { + if (!state.profile.roles.contains('consultant') || hidePlans) { return const SizedBox.shrink(); } return BlocBuilder( @@ -326,12 +276,7 @@ class _ProfileScreenState extends State { children: [ Text( 'Активные планы', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 24, - height: 1, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600), ), ], ), @@ -341,10 +286,7 @@ class _ProfileScreenState extends State { height: 781, child: PageView.builder( itemBuilder: (BuildContext context, int index) { - return PlanWidget( - plan: state.plansList[index], - index: index, - ); + return PlanWidget(plan: state.plansList[index], index: index); }, onPageChanged: (bullet) { setState(() { @@ -368,17 +310,11 @@ class _ProfileScreenState extends State { Container _bullets(int currentBullet, int bulletCount) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), child: Row( mainAxisSize: MainAxisSize.min, children: [ - for (int i = 0; i < bulletCount; i++) ...[ - _bullet(i == currentBullet), - i < bulletCount - 1 ? const SizedBox(width: 4) : const SizedBox.shrink(), - ], + for (int i = 0; i < bulletCount; i++) ...[_bullet(i == currentBullet), i < bulletCount - 1 ? const SizedBox(width: 4) : const SizedBox.shrink()], ], ), ); @@ -389,10 +325,7 @@ class _ProfileScreenState extends State { width: isActive ? 10 : 8, height: isActive ? 10 : 8, duration: const Duration(milliseconds: 150), - decoration: BoxDecoration( - color: isActive ? appTheme.blueA400 : const Color(0xFFD7D9DC), - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: isActive ? appTheme.blueA400 : const Color(0xFFD7D9DC), shape: BoxShape.circle), ); } @@ -409,25 +342,15 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(8), child: Ink( padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: const Color(0xFFF6FAFF), - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(8)), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.add_circle_outline_rounded, - color: Color(0xFF2F80ED), - ), + Icon(Icons.add_circle_outline_rounded, color: Color(0xFF2F80ED)), SizedBox(width: 12), Text( 'Новая запись', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF2F80ED), - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF2F80ED)), ), ], ), @@ -452,12 +375,7 @@ class _ProfileScreenState extends State { children: [ const Text( 'Дипломы\nи сертификаты', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 24, - height: 1, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, height: 1, fontWeight: FontWeight.w600), ), GestureDetector( onTap: () { @@ -465,11 +383,7 @@ class _ProfileScreenState extends State { }, child: Text( widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '', - style: const TextStyle( - color: Color(0xFF2F80ED), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500), ), ), ], @@ -489,17 +403,14 @@ class _ProfileScreenState extends State { child: Row( children: [ if (state.consultantInfo!.photos.isNotEmpty) - ...List.generate( - state.consultantInfo!.photos.length * 2 - 1, - (index) { - if (index.isEven) { - final photo = state.consultantInfo!.photos[index ~/ 2]; - return _diplomaImage(photo.url); - } else { - return const SizedBox(width: 8); - } - }, - ), + ...List.generate(state.consultantInfo!.photos.length * 2 - 1, (index) { + if (index.isEven) { + final photo = state.consultantInfo!.photos[index ~/ 2]; + return _diplomaImage(photo.url); + } else { + return const SizedBox(width: 8); + } + }), ], ), ), @@ -513,24 +424,13 @@ class _ProfileScreenState extends State { GestureDetector _diplomaImage(String url) { return GestureDetector( onTap: () { - context.pushTransparentRoute( - rootNavigator: true, - PhotoViewScreen( - imageUrls: [url], - index: 0, - ), - ); + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [url], index: 0)); }, child: Hero( tag: url, child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: url, - fit: BoxFit.fitHeight, - width: MediaQuery.of(context).size.width / 3 - (60 / 3), - height: 160, - ), + child: CachedNetworkImage(imageUrl: url, fit: BoxFit.fitHeight, width: MediaQuery.of(context).size.width / 3 - (60 / 3), height: 160), ), ), ); @@ -553,11 +453,7 @@ class _ProfileScreenState extends State { children: [ const Text( 'Образование', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 24, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600), ), GestureDetector( onTap: () { @@ -565,11 +461,7 @@ class _ProfileScreenState extends State { }, child: Text( widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '', - style: const TextStyle( - color: Color(0xFF2F80ED), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 16, fontWeight: FontWeight.w500), ), ), ], @@ -588,8 +480,10 @@ class _ProfileScreenState extends State { ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _educationBlock(state.consultantInfo!.educationModel.completedCourses![index].values.first.title, - state.consultantInfo!.educationModel.completedCourses![index].values.first.date), + itemBuilder: (context, index) => _educationBlock( + state.consultantInfo!.educationModel.completedCourses![index].values.first.title, + state.consultantInfo!.educationModel.completedCourses![index].values.first.date, + ), separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.consultantInfo!.educationModel.completedCourses!.length, ), @@ -619,22 +513,12 @@ class _ProfileScreenState extends State { children: [ Text( title, - style: const TextStyle( - height: 1, - color: Color(0xFF323F4B), - fontWeight: FontWeight.w500, - fontSize: 16, - ), + style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontWeight: FontWeight.w500, fontSize: 16), ), const SizedBox(height: 4), Text( formattedDate, - style: const TextStyle( - height: 1, - color: Color(0xFF6D6F71), - fontWeight: FontWeight.w500, - fontSize: 14, - ), + style: const TextStyle(height: 1, color: Color(0xFF6D6F71), fontWeight: FontWeight.w500, fontSize: 14), ), ], ), @@ -644,7 +528,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( @@ -658,12 +542,7 @@ class _ProfileScreenState extends State { children: [ _socialLinkButton( () { - launchUrl( - Uri.parse( - state.consultantInfo!.educationModel.vkLink ?? '', - ), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(state.consultantInfo!.educationModel.vkLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/vk.svg', state.consultantInfo!.educationModel.vkLink!.isEmpty ? true : false, @@ -671,12 +550,7 @@ class _ProfileScreenState extends State { const SizedBox(width: 12), _socialLinkButton( () { - launchUrl( - Uri.parse( - state.consultantInfo!.educationModel.tgLink ?? '', - ), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(state.consultantInfo!.educationModel.tgLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/telegram.svg', state.consultantInfo!.educationModel.tgLink!.isEmpty ? true : false, @@ -684,12 +558,7 @@ class _ProfileScreenState extends State { const SizedBox(width: 12), _socialLinkButton( () { - launchUrl( - Uri.parse( - state.consultantInfo!.educationModel.personalSiteLink ?? '', - ), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(state.consultantInfo!.educationModel.personalSiteLink ?? ''), mode: LaunchMode.externalApplication); }, 'assets/icons/web.svg', state.consultantInfo!.educationModel.personalSiteLink!.isEmpty ? true : false, @@ -707,10 +576,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(20), child: Ink( width: MediaQuery.of(context).size.width / 3 - (56 / 3), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16), child: SvgPicture.asset( @@ -732,20 +598,13 @@ class _ProfileScreenState extends State { child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Достижения', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), SingleChildScrollView( @@ -757,51 +616,25 @@ class _ProfileScreenState extends State { ? Tooltip( message: state.profile.badges[i].description, triggerMode: TooltipTriggerMode.tap, - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha((255 * 0.2).toInt()), - blurRadius: 32, - ) - ], - ), - child: SvgPicture.network( - '$baseUrl/storage/${state.profile.badges[i].image}', - width: 40, - height: 40, + boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), + child: SvgPicture.network('$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), ) : Tooltip( message: state.profile.badges[i].description, margin: const EdgeInsets.symmetric(horizontal: 32), triggerMode: TooltipTriggerMode.tap, - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w500, - ), + textStyle: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.w500), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha((255 * 0.2).toInt()), - blurRadius: 32, - ) - ], - ), - child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}', - width: 40, - height: 40, + boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), + child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), ), i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], @@ -821,21 +654,12 @@ class _ProfileScreenState extends State { child: Row( children: [ InkWell( - onTap: () => GoRouter.of(context).pushNamed( - 'subsScreen', - extra: SubType.subscriptions, - pathParameters: { - 'slug': state.profile.slug, - }, - ), + onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscriptions, pathParameters: {'slug': state.profile.slug}), borderRadius: BorderRadius.circular(12), child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width / 2 - 6, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, @@ -845,47 +669,27 @@ class _ProfileScreenState extends State { children: [ const Text( 'Подписки', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), Text( '${state.profile.subscriptions.length}', - style: const TextStyle( - color: Color(0xFF323F4B), - fontSize: 22, - fontWeight: FontWeight.w700, - ), + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700), ), ], ), - Padding( - padding: const EdgeInsets.only(right: 8), - child: _miniAvatars(state.profile.subscriptions), - ), + Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscriptions)), ], ), ), ), const SizedBox(width: 4), InkWell( - onTap: () => GoRouter.of(context).pushNamed( - 'subsScreen', - extra: SubType.subscribers, - pathParameters: { - 'slug': state.profile.slug, - }, - ), + onTap: () => GoRouter.of(context).pushNamed('subsScreen', extra: SubType.subscribers, pathParameters: {'slug': state.profile.slug}), borderRadius: BorderRadius.circular(12), child: Ink( padding: const EdgeInsets.all(8), width: MediaQuery.of(context).size.width / 2 - 6, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, @@ -895,26 +699,15 @@ class _ProfileScreenState extends State { children: [ const Text( 'Подписчики', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w600), ), Text( '${state.profile.subscribers.length}', - style: const TextStyle( - color: Color(0xFF323F4B), - fontSize: 22, - fontWeight: FontWeight.w700, - ), + style: const TextStyle(color: Color(0xFF323F4B), fontSize: 22, fontWeight: FontWeight.w700), ), ], ), - Padding( - padding: const EdgeInsets.only(right: 8), - child: _miniAvatars(state.profile.subscribers), - ), + Padding(padding: const EdgeInsets.only(right: 8), child: _miniAvatars(state.profile.subscribers)), ], ), ), @@ -932,13 +725,7 @@ class _ProfileScreenState extends State { for (int i = 0; i < maxLength; i++) Align( widthFactor: 0.8, - child: CircleAvatar( - radius: 14, - foregroundImage: NetworkImage( - urls[i], - ), - backgroundImage: const AssetImage('assets/images/no_avatar.png'), - ), + child: CircleAvatar(radius: 14, foregroundImage: NetworkImage(urls[i]), backgroundImage: const AssetImage('assets/images/no_avatar.png')), ), ], ); @@ -949,10 +736,7 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 16), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => ProfileBlogPostWidget( - index: index, - profileCubit: profileCubit, - ), + itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit), separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: state.blogPosts.length, ); @@ -966,13 +750,7 @@ class _ProfileScreenState extends State { return Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16), - child: Text( - text, - style: TextStyle( - color: Colors.black.withValues(alpha: 0.4), - height: 1, - ), - ), + child: Text(text, style: TextStyle(color: Colors.black.withValues(alpha: 0.4), height: 1)), ); } @@ -984,13 +762,7 @@ class _ProfileScreenState extends State { PopupMenuButton( onSelected: (value) async { if (value == 'view') { - context.pushTransparentRoute( - rootNavigator: true, - PhotoViewScreen( - imageUrls: [baseUrl + state.profile.profilePhotoUrl], - index: 0, - ), - ); + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0)); } if (value == 'replace') { showCupertinoModalPopup( @@ -1017,13 +789,7 @@ class _ProfileScreenState extends State { context: context, ); }, - child: const Row( - children: [ - Icon(Icons.photo_camera), - SizedBox(width: 8), - Text('Фото'), - ], - ), + child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), ), CupertinoActionSheetAction( onPressed: () async { @@ -1043,13 +809,7 @@ class _ProfileScreenState extends State { context: context, ); }, - child: const Row( - children: [ - Icon(Icons.photo), - SizedBox(width: 8), - Text('Галерея'), - ], - ), + child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]), ), ], cancelButton: CupertinoActionSheetAction( @@ -1067,20 +827,9 @@ class _ProfileScreenState extends State { } }, itemBuilder: (context) => [ - const PopupMenuItem( - value: 'view', - child: Text('Посмотреть'), - ), - if (widget.author == null || widget.author!.authorIsMe) - const PopupMenuItem( - value: 'replace', - child: Text('Заменить'), - ), - if (widget.author == null || widget.author!.authorIsMe) - const PopupMenuItem( - value: 'delete', - child: Text('Удалить'), - ), + const PopupMenuItem(value: 'view', child: Text('Посмотреть')), + if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'replace', child: Text('Заменить')), + if (widget.author == null || widget.author!.authorIsMe) const PopupMenuItem(value: 'delete', child: Text('Удалить')), ], child: Hero( tag: state.profile.profilePhotoUrl, @@ -1099,12 +848,7 @@ class _ProfileScreenState extends State { children: [ Text( '${state.profileDetails.firstName} ${state.profileDetails.lastName}', - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.w600, - fontSize: 16, - height: 1, - ), + style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600, fontSize: 16, height: 1), ), const SizedBox(height: 8), GestureDetector( @@ -1122,11 +866,7 @@ class _ProfileScreenState extends State { children: [ const Text( 'Подробнее', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: Color(0xFF323F4B), - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), _additional('Дата рожденя: ', state.profileDetails.birthdate), @@ -1146,19 +886,11 @@ class _ProfileScreenState extends State { }, child: const Row( children: [ - Icon( - Icons.info_outline, - size: 20, - color: Color(0xFF6D6F71), - ), + Icon(Icons.info_outline, size: 20, color: Color(0xFF6D6F71)), SizedBox(width: 4), Text( 'Дополнительно', - style: TextStyle( - color: Color(0xFF6D6F71), - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: Color(0xFF6D6F71), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), @@ -1171,10 +903,7 @@ class _ProfileScreenState extends State { if (value == 'share') { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( - ShareParams( - uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ), + ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), ); } else if (value == 'block') { isFirstFetch = true; @@ -1196,33 +925,15 @@ class _ProfileScreenState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (context) => ComplainingBottomSheet( - complainedContentType: ComplainedContent.profile, - slug: widget.author?.authorSlug, - ), + builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.profile, slug: widget.author?.authorSlug), ); } }, itemBuilder: (context) => [ - const PopupMenuItem( - value: 'share', - child: Text('Поделиться'), - ), - if (widget.author != null) - const PopupMenuItem( - value: 'complain', - child: Text('Пожаловаться'), - ), - if (widget.author != null && !state.profile.isBlockedByMe) - const PopupMenuItem( - value: 'block', - child: Text('Заблокировать'), - ), - if (widget.author != null && state.profile.isBlockedByMe) - const PopupMenuItem( - value: 'unblock', - child: Text('Разблокировать'), - ), + const PopupMenuItem(value: 'share', child: Text('Поделиться')), + if (widget.author != null) const PopupMenuItem(value: 'complain', child: Text('Пожаловаться')), + if (widget.author != null && !state.profile.isBlockedByMe) const PopupMenuItem(value: 'block', child: Text('Заблокировать')), + if (widget.author != null && state.profile.isBlockedByMe) const PopupMenuItem(value: 'unblock', child: Text('Разблокировать')), ], child: const Icon(Icons.more_horiz_rounded), ), @@ -1236,19 +947,11 @@ class _ProfileScreenState extends State { children: [ Text( title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF6D6F71), - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF6D6F71)), ), Text( value, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF323F4B), - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), ), ], ); @@ -1264,16 +967,8 @@ class _ProfileScreenState extends State { alignment: Alignment.center, padding: const EdgeInsets.all(8), width: (MediaQuery.of(context).size.width - 40) / 2, - decoration: BoxDecoration( - color: appTheme.blueA400.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(16), - ), - child: Text( - 'Сообщение', - style: TextStyle( - color: appTheme.blueA400, - ), - ), + decoration: BoxDecoration(color: appTheme.blueA400.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16)), + child: Text('Сообщение', style: TextStyle(color: appTheme.blueA400)), ), ), const SizedBox(width: 8), @@ -1288,16 +983,8 @@ class _ProfileScreenState extends State { alignment: Alignment.center, padding: const EdgeInsets.all(8), width: (MediaQuery.of(context).size.width - 40) / 2, - decoration: BoxDecoration( - color: !canSub ? Colors.grey.shade200 : appTheme.blueA400, - borderRadius: BorderRadius.circular(16), - ), - child: Text( - !canSub ? 'Отписаться' : 'Подписаться', - style: TextStyle( - color: !canSub ? Colors.black : Colors.white, - ), - ), + decoration: BoxDecoration(color: !canSub ? Colors.grey.shade200 : appTheme.blueA400, borderRadius: BorderRadius.circular(16)), + child: Text(!canSub ? 'Отписаться' : 'Подписаться', style: TextStyle(color: !canSub ? Colors.black : Colors.white)), ), ), const SizedBox(width: 16), @@ -1316,33 +1003,17 @@ class _ProfileScreenState extends State { SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, - child: Row( - children: [ - _choiceChipWidget(0), - const SizedBox(width: 8), - _choiceChipWidget(1), - const SizedBox(width: 8), - _choiceChipWidget(2), - ], - ), + child: Row(children: [_choiceChipWidget(0), const SizedBox(width: 8), _choiceChipWidget(1), const SizedBox(width: 8), _choiceChipWidget(2)]), ), Builder( builder: (context) { switch (_selectedChipIndex) { case 0: - return WeightChartWidget( - weightPoints: weightPoints, - profileCubit: profileCubit, - ); + return WeightChartWidget(weightPoints: weightPoints, profileCubit: profileCubit); case 1: - return CircumferenceChartWidget( - circumferencePoints: circumferencePoints, - profileCubit: profileCubit, - ); + return CircumferenceChartWidget(circumferencePoints: circumferencePoints, profileCubit: profileCubit); case 2: - return ImtChartWidget( - weightPoints: weightPoints, - ); + return ImtChartWidget(weightPoints: weightPoints); default: return const SizedBox.shrink(); } @@ -1375,16 +1046,10 @@ class _ProfileScreenState extends State { pressElevation: 0, showCheckmark: false, side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), label: Text(chipsIntervalLabelList[index]), - labelStyle: TextStyle( - color: appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedIntervalChipIndex == index, onSelected: (bool selected) { profileCubit.getMyProfile(period: chipsIntervalList[index]); @@ -1401,16 +1066,10 @@ class _ProfileScreenState extends State { pressElevation: 0, showCheckmark: false, side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), label: Text(chipsLabelList[index]), - labelStyle: TextStyle( - color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedChipIndex == index, backgroundColor: appTheme.gray100, onSelected: (bool selected) { diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 75879d15..05399457 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -203,4 +203,12 @@ class ProfileRepository { return false; } } + + Future deleteAccount() async { + try { + await profileService.deleteAccount(); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/profile/services/profile_service.dart b/lib/features/profile/services/profile_service.dart index 06af37b9..a1208526 100644 --- a/lib/features/profile/services/profile_service.dart +++ b/lib/features/profile/services/profile_service.dart @@ -119,4 +119,7 @@ abstract class ProfileService { Future tryVerifyEmailByCode({ @Body() required String code, }); + + @DELETE('/api/profile/delete_my_account') + Future deleteAccount(); } 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 83592c3b..c9426674 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -1,6 +1,8 @@ import 'dart:developer'; import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; @@ -58,9 +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 d3995d6a..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; } @@ -67,4 +71,13 @@ class SettingsCubit extends Cubit { await init(); } } + + Future deleteAccount() async { + try { + await profileRepository.deleteAccount(); + return true; + } catch (e) { + return false; + } + } } diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 67460955..af99f901 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -1,12 +1,19 @@ +import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -60,7 +67,7 @@ class _SettingsScreenState extends State { _aboutEatvio(state), const SizedBox(height: 12), _becomeConsultantButton(state), - _deleteAccountButton(state), + _deleteAccountButton(state, settingsCubit), ], ), ), @@ -428,11 +435,20 @@ class _SettingsScreenState extends State { return const SizedBox.shrink(); } - Padding _deleteAccountButton(SettingsStateLoaded state) { + Padding _deleteAccountButton(SettingsStateLoaded state, SettingsCubit settingsCubit) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: InkWell( - onTap: () {}, + onTap: () async { + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + height: null, + child: DeleteAccountBottomSheet( + cubit: settingsCubit, + ), + ); + }, borderRadius: BorderRadius.circular(20), child: Ink( height: 52, @@ -495,3 +511,73 @@ class _SettingsScreenState extends State { ); } } + +class DeleteAccountBottomSheet extends StatefulWidget { + const DeleteAccountBottomSheet({super.key, required this.cubit}); + final SettingsCubit cubit; + + @override + State createState() => _DeleteAccountBottomSheet(); +} + +class _DeleteAccountBottomSheet extends State { + bool? isFailure; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + const BottomSheetHandlerBar(), + const SizedBox( + height: 20, + ), + Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)), + const SizedBox( + height: 16, + ), + Text( + s.youShureYouWantToDeleteAccount, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 8, + ), + Text( + s.itCantBeRecovered, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 8, + ), + CustomOutlinedButton( + text: s.delete, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + final result = await widget.cubit.deleteAccount(); + if (result == true) { + AppRouter.navigatorKey.currentContext?.go('/auth-screen'); + return; + } + AppRouter.navigatorKey.currentContext?.pop(); + if (!context.mounted) return; + showErrorDialog(context, description: s.cannotDeleteAccount); + }, + ), + const SizedBox( + height: 40, + ), + ], + ), + ); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 73db6f27..360b4288 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -51,6 +51,9 @@ class MessageLookup extends MessageLookupByLibrary { "about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), "about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), "about_recipe": MessageLookupByLibrary.simpleMessage("Об этом рецепте"), + "accountDeleting": MessageLookupByLibrary.simpleMessage( + "Удаление аккаунта", + ), "active": MessageLookupByLibrary.simpleMessage("Активен"), "active_consultations_count": MessageLookupByLibrary.simpleMessage( "Кол-во активных консультаций", @@ -124,6 +127,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"), "cancel_order": MessageLookupByLibrary.simpleMessage("Отменить заказ"), + "cannotDeleteAccount": MessageLookupByLibrary.simpleMessage( + "Не получилось удалить ваш аккаунт", + ), "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), "carb_type_desc": MessageLookupByLibrary.simpleMessage( "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", @@ -256,6 +262,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "days": MessageLookupByLibrary.simpleMessage("дней"), "deception": MessageLookupByLibrary.simpleMessage("Обман"), + "delete": MessageLookupByLibrary.simpleMessage("Удалить"), "deleteComment": MessageLookupByLibrary.simpleMessage( "Удалить комментарий", ), @@ -411,6 +418,9 @@ class MessageLookup extends MessageLookupByLibrary { "invited_people_count": MessageLookupByLibrary.simpleMessage( "Количество приглашенных", ), + "itCantBeRecovered": MessageLookupByLibrary.simpleMessage( + "Его нельзя будет восстановить", + ), "label": MessageLookupByLibrary.simpleMessage("Название"), "language": MessageLookupByLibrary.simpleMessage("Язык"), "last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), @@ -780,6 +790,9 @@ class MessageLookup extends MessageLookupByLibrary { "youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage( "Вы уверены, что хотите отменить заказ?", ), + "youShureYouWantToDeleteAccount": MessageLookupByLibrary.simpleMessage( + "Вы уверены, что хотите удалить аккаунт?", + ), "you_can_not_open_this_day": MessageLookupByLibrary.simpleMessage( "Вы не можете перейти в этот день", ), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 5a4e420e..7df3cbcc 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3414,6 +3414,51 @@ class S { args: [], ); } + + /// `Удаление аккаунта` + String get accountDeleting { + return Intl.message( + 'Удаление аккаунта', + name: 'accountDeleting', + desc: '', + args: [], + ); + } + + /// `Вы уверены, что хотите удалить аккаунт?` + String get youShureYouWantToDeleteAccount { + return Intl.message( + 'Вы уверены, что хотите удалить аккаунт?', + name: 'youShureYouWantToDeleteAccount', + desc: '', + args: [], + ); + } + + /// `Его нельзя будет восстановить` + String get itCantBeRecovered { + return Intl.message( + 'Его нельзя будет восстановить', + name: 'itCantBeRecovered', + desc: '', + args: [], + ); + } + + /// `Удалить` + String get delete { + return Intl.message('Удалить', name: 'delete', desc: '', args: []); + } + + /// `Не получилось удалить ваш аккаунт` + String get cannotDeleteAccount { + return Intl.message( + 'Не получилось удалить ваш аккаунт', + name: 'cannotDeleteAccount', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 6a8fb162..452e2d5a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -405,7 +405,12 @@ "allowAccess": "Разрешить доступ" , "copy": "Скопировать", "reply": "Ответить", - "deleteComment": "Удалить комментарий" + "deleteComment": "Удалить комментарий", + "accountDeleting": "Удаление аккаунта", + "youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?", + "itCantBeRecovered": "Его нельзя будет восстановить", + "delete": "Удалить", + "cannotDeleteAccount": "Не получилось удалить ваш аккаунт" } diff --git a/lib/main.dart b/lib/main.dart index 005afaac..3fd39e7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:eatvio_app/features/internet_connection_checker/internet_connect import 'package:eatvio_app/firebase_options.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -22,8 +23,8 @@ import 'package:sentry_flutter/sentry_flutter.dart'; Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await FirebaseMessaging.instance.requestPermission(); await di.init(); await SentryFlutter.init( diff --git a/pubspec.yaml b/pubspec.yaml index 74b00964..0dd1a2da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.2+52 +version: 1.2.6+64 publish_to: 'none' environment: - sdk: '>=3.3.4 <4.0.0' + sdk: '>=3.8.0 <4.0.0' dependencies: flutter: @@ -18,28 +18,28 @@ dependencies: cached_network_image: ^3.4.1 flutter_svg: ^2.2.0 equatable: ^2.0.7 - freezed_annotation: ^3.0.0 + freezed_annotation: ^3.1.0 json_annotation: ^4.9.0 validators: ^3.0.0 path_provider: ^2.1.5 carousel_slider_plus: ^7.1.1 - flutter_form_builder: ^10.0.1 + flutter_form_builder: ^10.1.0 image_picker: ^1.1.2 flutter_typeahead: ^5.2.0 go_router: ^16.0.0 keyboard_dismisser: ^3.0.0 - retrofit: ^4.5.0 - get_it: ^8.0.3 + retrofit: ^4.7.0 + get_it: ^8.1.0 dio: ^5.8.0+1 dart_pusher_channels: ^1.2.3 photo_view: ^0.15.0 dismissible_page: ^1.0.2 - url_launcher: ^6.3.1 + url_launcher: ^6.3.2 easy_debounce: ^2.0.3 badges: ^3.1.2 google_fonts: ^6.2.1 - syncfusion_flutter_charts: ^30.1.38 - syncfusion_flutter_gauges: ^30.1.38 + syncfusion_flutter_charts: ^30.1.42 + syncfusion_flutter_gauges: ^30.1.42 table_calendar: ^3.2.0 flutter_keyboard_visibility: ^6.0.0 share_plus: ^11.0.0 @@ -58,11 +58,11 @@ dependencies: http: ^1.4.0 flutter_native_splash: ^2.4.6 webview_flutter: ^4.13.0 - sentry_flutter: ^8.14.2 + sentry_flutter: ^9.5.0 connectivity_plus: ^6.1.4 visibility_detector: ^0.4.0+2 - firebase_core: ^3.15.0 - firebase_messaging: ^15.2.8 + firebase_core: ^3.15.2 + firebase_messaging: ^15.2.10 http_parser: ^4.1.2 path: ^1.9.1 collection: ^1.19.1 @@ -70,15 +70,15 @@ dependencies: version: ^3.0.2 package_info_plus: ^8.3.0 device_info_plus: ^11.5.0 - permission_handler: ^12.0.0+1 + permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 dev_dependencies: - build_runner: ^2.5.4 - freezed: ^3.0.6 - json_serializable: ^6.9.5 - retrofit_generator: ^9.2.0 - mockito: ^5.4.6 + build_runner: ^2.6.0 + freezed: ^3.2.0 + json_serializable: ^6.10.0 + retrofit_generator: ^10.0.1 + mockito: ^5.5.0 flutter_test: sdk: flutter 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,