diff --git a/.gitignore b/.gitignore index 5bb96d46..bb936737 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /node_modules/ /android/app/release/app-release.aab /android/app/upload-keystore.jks +/android/app/.cxx diff --git a/android/app/build.gradle b/android/app/build.gradle index 2869498c..4df5da7f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,8 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } @@ -13,7 +16,7 @@ if (keystorePropertiesFile.exists()) { android { namespace = "com.eatvioapp.app" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_21 diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 00000000..8b0acfc2 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,30 @@ +{ + "project_info": { + "project_number": "613980912922", + "firebase_url": "https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app", + "project_id": "eatvio-3467b", + "storage_bucket": "eatvio-3467b.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:613980912922:android:4de95e5f2d50ba69ecc0a3", + "android_client_info": { + "package_name": "com.eatvioapp.app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index a9f0eb13..849ba91f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,6 +19,9 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.7.3" apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "2.0.21" apply false } diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..f87cfe20 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"eatvio-3467b","appId":"1:613980912922:android:4de95e5f2d50ba69ecc0a3","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"eatvio-3467b","appId":"1:613980912922:ios:c557d140a1212e63ecc0a3","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"eatvio-3467b","configurations":{"android":"1:613980912922:android:4de95e5f2d50ba69ecc0a3","ios":"1:613980912922:ios:c557d140a1212e63ecc0a3"}}}}}} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8e714119..38d0543a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + ACF3142BB74D6F5F3C98AD82 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EB9FE5381C3F532BD9A9456D /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -47,6 +48,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D8D6181877DE17809AFF936 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; DF0C4038A57E19D3ABA03C15 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + EB9FE5381C3F532BD9A9456D /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; FEE92A0E2D772EA300E6E989 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ @@ -91,6 +93,7 @@ 97C146EF1CF9000F007C117D /* Products */, 6F201E010624CF963CC3A17B /* Pods */, AEE6B7943D197783DD1CB2EE /* Frameworks */, + EB9FE5381C3F532BD9A9456D /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -141,6 +144,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 1D324C14EB43C988423BC1AD /* [CP] Embed Pods Frameworks */, + DEAA66E178B81C074317BF9A /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -194,6 +198,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ACF3142BB74D6F5F3C98AD82 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -270,6 +275,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + DEAA66E178B81C074317BF9A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -363,9 +385,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -383,7 +406,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.48; + MARKETING_VERSION = 1.0.49; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -512,9 +535,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -532,7 +556,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.48; + MARKETING_VERSION = 1.0.49; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -549,9 +573,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 48; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -569,7 +594,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.48; + MARKETING_VERSION = 1.0.49; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..3f42d802 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,32 @@ + + + + + API_KEY + AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ + GCM_SENDER_ID + 613980912922 + PLIST_VERSION + 1 + BUNDLE_ID + com.eatvioapp.app + PROJECT_ID + eatvio-3467b + STORAGE_BUCKET + eatvio-3467b.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:613980912922:ios:c557d140a1212e63ecc0a3 + DATABASE_URL + https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index cab95de7..b6470ce4 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,8 +2,6 @@ - ITSAppUsesNonExemptEncryption - CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -24,6 +22,8 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity @@ -42,6 +42,7 @@ UIBackgroundModes fetch + remote-notification UILaunchStoryboardName LaunchScreen diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index a9bc4abe..903def2a 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -3,6 +3,6 @@ aps-environment - disabled + development diff --git a/lib/core/const.dart b/lib/core/const.dart index 31c47e9c..4f38a54e 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'; +// String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart new file mode 100644 index 00000000..f83d47d6 --- /dev/null +++ b/lib/core/firebase_api.dart @@ -0,0 +1,11 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; + +class FirebaseApi { + + final _firebaseMessaging = FirebaseMessaging.instance; + + Future getToken() async{ + await _firebaseMessaging.requestPermission(); + return await _firebaseMessaging.getToken(); + } +} \ No newline at end of file diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 3a3f5342..4320423a 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -94,13 +94,13 @@ Future init() async { // Cubit factories sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); - sl.registerFactory(() => ChatRoomCubit(chatRepository: sl(), wsManager: sl())); + sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); sl.registerFactory(() => profilePlans.PlansCubit(plansRepository: sl())); sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); - sl.registerFactory(() => CommentsCubit(feedRepository: sl())); + sl.registerFactory(() => CommentsCubit(feedRepository: sl())); sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl())); sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); @@ -171,15 +171,7 @@ Future init() async { // External sl.registerLazySingleton( - () => Dio( - BaseOptions( - baseUrl: baseUrl, - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ), - )..interceptors.add(TokenInterceptor()), + () => Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))..interceptors.add(TokenInterceptor()), ); sl.registerLazySingleton(() async => await FlutterSecureStorage()); } diff --git a/lib/core/widgets/data_error_widget.dart b/lib/core/widgets/data_error_widget.dart index 1ce1c110..e7330506 100644 --- a/lib/core/widgets/data_error_widget.dart +++ b/lib/core/widgets/data_error_widget.dart @@ -25,43 +25,33 @@ class _DataErrorWidgetState extends State { children: [ SvgPicture.asset('assets/icons/cloud.svg'), SizedBox(height: 30), - Text( - s.something_went_wrong, - style: TextStyles.textLSemibold.copyWith(color: appTheme.black), - ), + Text(s.something_went_wrong, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), - child: Text( - s.can_not_load_data_please_try_again, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - textAlign: TextAlign.center, - ), + child: Text(s.can_not_load_data_please_try_again, style: TextStyles.textSMedium.copyWith(color: appTheme.black), textAlign: TextAlign.center), ), SizedBox(height: 20), isLoading ? Loader() : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: CustomOutlinedButton( - text: s.try_again, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - onPressed: () async { - setState(() { - isLoading = true; - }); - await widget.onReload(); - setState(() { - isLoading = false; - }); - }, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomOutlinedButton( + text: s.try_again, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: () async { + setState(() { + isLoading = true; + }); + await widget.onReload(); + setState(() { + isLoading = false; + }); + }, ), + ), ], ); } 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 1e456fc6..c6e44f76 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 @@ -11,10 +11,13 @@ import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/my_finance/models/card_model.dart'; +import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ChatRoomCubit extends Cubit { final ChatRepository chatRepository; + final CardsRepository cardsRepository; final WsManager wsManager; StreamSubscription? typingSubscription; @@ -23,34 +26,22 @@ class ChatRoomCubit extends Cubit { late final int lastPage; bool isLoading = false; - ChatRoomCubit({ - required this.chatRepository, - required this.wsManager, - }) : super(ChatRoomState.loading()); + ChatRoomCubit({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(ChatRoomState.loading()); loadNextPage(String chatId) async { if (currentPage < lastPage && !isLoading) { isLoading = true; - emit( - ChatRoomState.loaded( - chatRoom: (state as ChatRoomStateLoaded).chatRoom, - chatMessages: (state as ChatRoomStateLoaded).chatMessages, - isTyping: (state as ChatRoomStateLoaded).isTyping, - isLoading: isLoading, - ), - ); + emit((state as ChatRoomStateLoaded).copyWith(isLoading: isLoading)); try { final chatMessages = await chatRepository.getChatMessages(chatId, currentPage + 1); isLoading = false; emit( - ChatRoomState.loaded( - chatRoom: (state as ChatRoomStateLoaded).chatRoom, - chatMessages: [...(state as ChatRoomStateLoaded).chatMessages, ...chatMessages.messagesList], - isTyping: (state as ChatRoomStateLoaded).isTyping, + (state as ChatRoomStateLoaded).copyWith( isLoading: isLoading, + chatMessages: [...(state as ChatRoomStateLoaded).chatMessages, ...chatMessages.messagesList], ), ); @@ -61,6 +52,24 @@ class ChatRoomCubit extends Cubit { } } + Future checkCards() async { + try { + final cards = await cardsRepository.getCards(); + setCard(cards: cards); + } catch (e) { + emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); + } + } + + void setCard({required List cards}) { + final currentState = state; + if (currentState is ChatRoomStateLoaded) { + if (cards.where((card) => card.isDefault).isNotEmpty) { + emit(currentState.copyWith(hasCard: true)); + } + } + } + initChat(String chatId) async { try { final chatRoom = await chatRepository.getChatRoom(chatId); @@ -70,14 +79,9 @@ class ChatRoomCubit extends Cubit { await _subscribeToChatChannel(chatId: chatId); - emit( - ChatRoomState.loaded( - chatRoom: chatRoom, - chatMessages: chatMessages.messagesList, - isTyping: false, - isLoading: isLoading, - ), - ); + emit(ChatRoomState.loaded(chatRoom: chatRoom, chatMessages: chatMessages.messagesList, isTyping: false, isLoading: isLoading, hasCard: false)); + final cards = await cardsRepository.getCards(); + setCard(cards: cards); } catch (e) { typingSubscription?.cancel(); messagesSubscription?.cancel(); @@ -90,17 +94,13 @@ class ChatRoomCubit extends Cubit { try { await chatRepository.cancelChosenPlan(id: planId); final chatRoom = await chatRepository.getChatRoom(chatId); - emit( - (state as ChatRoomStateLoaded).copyWith(chatRoom: chatRoom), - ); + emit((state as ChatRoomStateLoaded).copyWith(chatRoom: chatRoom)); } catch (e) { emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); } } - Future _subscribeToChatChannel({ - required String chatId, - }) async { + Future _subscribeToChatChannel({required String chatId}) async { final client = wsManager.client; if (client == null) return; await wsManager.initPrivateDirectChatChannel(chatId); @@ -108,56 +108,20 @@ class ChatRoomCubit extends Cubit { messagesSubscription = wsManager.directChatStream?.listen((event) { AudioPlayer().play((AssetSource('audio/recive_message.wav'))); final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']); - List messagesList = [ - message, - ...(state as ChatRoomStateLoaded).chatMessages, - ]; + List messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages]; chatRepository.emitNewMessageToChats(message); - emit( - ChatRoomState.loaded( - chatRoom: (state as ChatRoomStateLoaded).chatRoom, - chatMessages: messagesList, - isTyping: (state as ChatRoomStateLoaded).isTyping, - isLoading: isLoading, - ), - ); + emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading)); }); typingSubscription = wsManager.clientTypingStream?.listen((event) { - emit( - ChatRoomState.loaded( - chatRoom: (state as ChatRoomStateLoaded).chatRoom, - chatMessages: (state as ChatRoomStateLoaded).chatMessages, - isTyping: true, - isLoading: isLoading, - ), - ); - EasyDebounce.debounce( - 'client-typing', - Duration(seconds: 3), - () => emit( - ChatRoomState.loaded( - chatRoom: (state as ChatRoomStateLoaded).chatRoom, - chatMessages: (state as ChatRoomStateLoaded).chatMessages, - isTyping: false, - isLoading: isLoading, - ), - ), - ); + emit((state as ChatRoomStateLoaded).copyWith(isTyping: true, isLoading: isLoading)); + EasyDebounce.debounce('client-typing', Duration(seconds: 3), () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading))); }); } - Future sendMessage({ - required String chatId, - String? data, - List? files, - }) async { + Future sendMessage({required String chatId, String? data, List? files}) async { try { - await chatRepository.sendMessage( - chatId: chatId, - data: data, - files: files != null ? await ImageCompressUtility.compressFilesList(files) : null, - ); + await chatRepository.sendMessage(chatId: chatId, data: data, files: files != null ? await ImageCompressUtility.compressFilesList(files) : null); AudioPlayer().play((AssetSource('audio/send_message.wav'))); return true; } catch (e, st) { @@ -176,9 +140,7 @@ class ChatRoomCubit extends Cubit { currentMessages[elem.index] = currentMessages[elem.index].copyWith(isRead: true); }); if (!isClosed) { - emit( - (state as ChatRoomStateLoaded).copyWith(chatMessages: currentMessages), - ); + emit((state as ChatRoomStateLoaded).copyWith(chatMessages: currentMessages)); } return true; } diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart index 24113e62..36ca4f35 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart @@ -13,6 +13,7 @@ abstract class ChatRoomState with _$ChatRoomState { const factory ChatRoomState.loaded({ required ChatRoomModel chatRoom, required List chatMessages, + required bool hasCard, required bool isTyping, required bool isLoading, }) = ChatRoomStateLoaded; diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.freezed.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.freezed.dart index 52b11c34..eb7ab711 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.freezed.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.freezed.dart @@ -24,6 +24,7 @@ mixin _$ChatRoomState { required TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, ) @@ -37,6 +38,7 @@ mixin _$ChatRoomState { TResult? Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -50,6 +52,7 @@ mixin _$ChatRoomState { TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -151,6 +154,7 @@ class _$ChatRoomStateImpl implements _ChatRoomState { required TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, ) @@ -168,6 +172,7 @@ class _$ChatRoomStateImpl implements _ChatRoomState { TResult? Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -185,6 +190,7 @@ class _$ChatRoomStateImpl implements _ChatRoomState { TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -289,6 +295,7 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading { required TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, ) @@ -306,6 +313,7 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading { TResult? Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -323,6 +331,7 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading { TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -388,6 +397,7 @@ abstract class _$$ChatRoomStateLoadedImplCopyWith<$Res> { $Res call({ ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, }); @@ -411,6 +421,7 @@ class __$$ChatRoomStateLoadedImplCopyWithImpl<$Res> $Res call({ Object? chatRoom = null, Object? chatMessages = null, + Object? hasCard = null, Object? isTyping = null, Object? isLoading = null, }) { @@ -426,6 +437,11 @@ class __$$ChatRoomStateLoadedImplCopyWithImpl<$Res> ? _value._chatMessages : chatMessages // ignore: cast_nullable_to_non_nullable as List, + hasCard: + null == hasCard + ? _value.hasCard + : hasCard // ignore: cast_nullable_to_non_nullable + as bool, isTyping: null == isTyping ? _value.isTyping @@ -457,6 +473,7 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { const _$ChatRoomStateLoadedImpl({ required this.chatRoom, required final List chatMessages, + required this.hasCard, required this.isTyping, required this.isLoading, }) : _chatMessages = chatMessages; @@ -471,6 +488,8 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { return EqualUnmodifiableListView(_chatMessages); } + @override + final bool hasCard; @override final bool isTyping; @override @@ -478,7 +497,7 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { @override String toString() { - return 'ChatRoomState.loaded(chatRoom: $chatRoom, chatMessages: $chatMessages, isTyping: $isTyping, isLoading: $isLoading)'; + return 'ChatRoomState.loaded(chatRoom: $chatRoom, chatMessages: $chatMessages, hasCard: $hasCard, isTyping: $isTyping, isLoading: $isLoading)'; } @override @@ -492,6 +511,7 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { other._chatMessages, _chatMessages, ) && + (identical(other.hasCard, hasCard) || other.hasCard == hasCard) && (identical(other.isTyping, isTyping) || other.isTyping == isTyping) && (identical(other.isLoading, isLoading) || @@ -503,6 +523,7 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { runtimeType, chatRoom, const DeepCollectionEquality().hash(_chatMessages), + hasCard, isTyping, isLoading, ); @@ -526,13 +547,14 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { required TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, ) loaded, required TResult Function(String errorMessage) error, }) { - return loaded(chatRoom, chatMessages, isTyping, isLoading); + return loaded(chatRoom, chatMessages, hasCard, isTyping, isLoading); } @override @@ -543,13 +565,14 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { TResult? Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? loaded, TResult? Function(String errorMessage)? error, }) { - return loaded?.call(chatRoom, chatMessages, isTyping, isLoading); + return loaded?.call(chatRoom, chatMessages, hasCard, isTyping, isLoading); } @override @@ -560,6 +583,7 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -568,7 +592,7 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded { required TResult orElse(), }) { if (loaded != null) { - return loaded(chatRoom, chatMessages, isTyping, isLoading); + return loaded(chatRoom, chatMessages, hasCard, isTyping, isLoading); } return orElse(); } @@ -615,12 +639,14 @@ abstract class ChatRoomStateLoaded implements ChatRoomState { const factory ChatRoomStateLoaded({ required final ChatRoomModel chatRoom, required final List chatMessages, + required final bool hasCard, required final bool isTyping, required final bool isLoading, }) = _$ChatRoomStateLoadedImpl; ChatRoomModel get chatRoom; List get chatMessages; + bool get hasCard; bool get isTyping; bool get isLoading; @@ -711,6 +737,7 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError { required TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, ) @@ -728,6 +755,7 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError { TResult? Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? @@ -745,6 +773,7 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError { TResult Function( ChatRoomModel chatRoom, List chatMessages, + bool hasCard, bool isTyping, bool isLoading, )? diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 5d76bce4..745bb46b 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -13,6 +13,7 @@ import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/order_card.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; @@ -26,10 +27,7 @@ import 'package:visibility_detector/visibility_detector.dart'; class ChatScreen extends StatefulWidget { final String chatId; - const ChatScreen({ - required this.chatId, - super.key, - }); + const ChatScreen({required this.chatId, super.key}); @override State createState() => _ChatScreenState(); @@ -72,14 +70,9 @@ class _ChatScreenState extends State { if (state is ChatRoomStateLoaded) { return _buildChat(context, state); } else if (state is ChatRoomStateError) { - return Scaffold( - appBar: AppBar(), - body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)), - ); + return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId))); } else { - return Scaffold( - body: const Loader(), - ); + return Scaffold(body: const Loader()); } }, ), @@ -90,6 +83,14 @@ class _ChatScreenState extends State { final chosenPlan = state.chatRoom.chosenPlan; final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id; final showAboutOrderButton = showableChatBadgeStates.contains(chosenPlan?.chosenPlanState); + BannerType? bannerType; + if (amIConsultant && !state.hasCard) { + bannerType = BannerType.card; + } else if (amIConsultant && chosenPlan?.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) { + bannerType = BannerType.date; + } else if (!amIConsultant && chosenPlan?.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet) { + bannerType = BannerType.payment; + } return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( @@ -99,43 +100,25 @@ class _ChatScreenState extends State { title: Row( children: [ if (state.chatRoom.users.receiver.profilePhotoUrl != null) - CircleAvatar( - radius: 20, - foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!), - ), + CircleAvatar(radius: 20, foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!)), if (state.chatRoom.users.receiver.profilePhotoUrl == null) Container( width: 40, height: 40, alignment: Alignment.center, - decoration: BoxDecoration( - color: Color(0xFFEBF4FF), - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle), child: Text( state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0], - style: TextStyle( - color: Color(0xFF7F9CF5), - fontWeight: FontWeight.w400, - fontSize: 20, - ), + style: TextStyle(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20), ), ), SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - state.chatRoom.users.receiver.name, - style: CustomTextStyles.titleSmallBold.copyWith( - color: PrimaryColors().black, - ), - ), + Text(state.chatRoom.users.receiver.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black)), AnimatedCrossFade( - firstChild: Text( - 'Печатает...', - style: CustomTextStyles.bodySmallMulishPrimary, - ), + firstChild: Text('Печатает...', style: CustomTextStyles.bodySmallMulishPrimary), firstCurve: Curves.easeIn, secondCurve: Curves.easeOut, secondChild: const SizedBox.shrink(), @@ -170,10 +153,7 @@ class _ChatScreenState extends State { shrinkWrap: true, itemBuilder: (context, index) { if (index == state.chatMessages.length) { - return Padding( - padding: const EdgeInsets.all(8), - child: const Loader(), - ); + return Padding(padding: const EdgeInsets.all(8), child: const Loader()); } if ((index + 1 != state.chatMessages.length && @@ -188,18 +168,15 @@ class _ChatScreenState extends State { onVisibilityChanged: (VisibilityInfo info) { if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { unreadMessagesIdsCopy.add((id: state.chatMessages[index].id, index: index)); - EasyDebounce.debounce( - 'mark_as_read', - Duration(milliseconds: 500), - () async { - final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy); - if (!result) return; - context - .read() - .reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIdsCopy.length); - unreadMessagesIdsCopy.clear(); - }, - ); + EasyDebounce.debounce('mark_as_read', Duration(milliseconds: 500), () async { + final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy); + if (!result) return; + context.read().reduceUnreadMessagesCount( + chatId: state.chatRoom.id, + count: unreadMessagesIdsCopy.length, + ); + unreadMessagesIdsCopy.clear(); + }); } }, child: ChatBubbleWidget( @@ -219,16 +196,12 @@ class _ChatScreenState extends State { onVisibilityChanged: (VisibilityInfo info) { if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) { unreadMessagesIds.add((id: state.chatMessages[index].id, index: index)); - EasyDebounce.debounce( - 'mark_as_read', - Duration(milliseconds: 500), - () async { - final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds); - if (!result) return; - context.read().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length); - unreadMessagesIds.clear(); - }, - ); + EasyDebounce.debounce('mark_as_read', Duration(milliseconds: 500), () async { + final result = await context.read().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds); + if (!result) return; + context.read().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length); + unreadMessagesIds.clear(); + }); } }, child: ChatBubbleWidget( @@ -268,6 +241,8 @@ class _ChatScreenState extends State { }, ), ), + if (bannerType != null) + Padding(padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: NotificationBanner(bannerType: bannerType)), ], ], ], @@ -310,24 +285,14 @@ class _ChatScreenState extends State { if (result != null) { List files = result.paths.map((path) => File(path!)).toList(); - await chatRoomCubit.sendMessage( - chatId: widget.chatId, - files: files, - ); + await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files); } }, customBorder: CircleBorder(), child: Ink( height: 40, width: 40, - child: Transform.rotate( - angle: pi / 4, - child: Icon( - Icons.attach_file_rounded, - size: 30, - color: Color(0xFF858E99), - ), - ), + child: Transform.rotate(angle: pi / 4, child: Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))), ), ), SizedBox(width: 8), @@ -345,33 +310,16 @@ class _ChatScreenState extends State { isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10), hintText: 'Сообщение', - hintStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1, - color: Color(0xFFAEAEB2), - ), + hintStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)), filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(100), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(100), borderSide: BorderSide(color: Color(0xFFD1D1D6), width: 1)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(100), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: BorderSide(color: Color(0xFFD1D1D6), width: 1), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(100), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), + borderSide: BorderSide(color: Color(0xFFD1D1D6), width: 1), ), ), ), @@ -382,27 +330,17 @@ class _ChatScreenState extends State { customBorder: CircleBorder(), onTap: () { if (textEditingController.text.isNotEmpty) { - chatRoomCubit.sendMessage( - chatId: widget.chatId, - data: textEditingController.text.trim(), - ); + chatRoomCubit.sendMessage(chatId: widget.chatId, data: textEditingController.text.trim()); setState(() { textEditingController.clear(); }); } }, child: Ink( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), height: 40, width: 40, - child: Icon( - Icons.arrow_upward_rounded, - size: 24, - color: Colors.white, - ), + child: Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white), ), ), SizedBox(width: 8), @@ -411,13 +349,7 @@ class _ChatScreenState extends State { ), ], ), - if (showAboutOrderButton) - Positioned( - top: 8, - right: 15, - left: 16, - child: AboutOrderButton(), - ), + if (showAboutOrderButton) Positioned(top: 8, right: 15, left: 16, child: AboutOrderButton()), ], ), ), @@ -428,16 +360,7 @@ class _ChatScreenState extends State { return Center( child: Padding( padding: const EdgeInsets.only(top: 32), - child: Text( - DateFormat.yMMMMd('RU').format( - date, - ), - style: const TextStyle( - height: 1, - color: Color(0xFF8393A1), - fontWeight: FontWeight.w500, - ), - ), + child: Text(DateFormat.yMMMMd('RU').format(date), style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500)), ), ); } diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index 3b1067f7..32de0606 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -1,9 +1,15 @@ +import 'dart:io'; + +import 'package:dio/dio.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/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/dev_tools/test_cases.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class DevToolsScreen extends StatelessWidget { const DevToolsScreen({super.key}); @@ -61,9 +67,7 @@ class _DevToolsScreenContentState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text('Для разработчиков'), - ), + appBar: AppBar(title: Text('Для разработчиков')), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), @@ -99,6 +103,7 @@ class _DevToolsScreenContentState extends State { ListView.builder( shrinkWrap: true, itemCount: testCases.length, + physics: NeverScrollableScrollPhysics(), itemBuilder: (context, index) => Padding( padding: const EdgeInsets.only(bottom: 10.0), @@ -151,6 +156,80 @@ class _DevToolsScreenContentState extends State { ), ), ), + const SizedBox(height: 10), + CustomOutlinedButton( + text: 'add token to server', + buttonStyle: ButtonStyle(), + onPressed: () async { + try { + final dio = sl(); + final String platform; + if (Platform.isAndroid) { + platform = 'android'; + } else { + platform = 'ios'; + } + await dio.post('/api/profile/notification/token/register', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); + showInfoToast('Токен добавлен'); + } catch (e) { + showInfoToast('Не удалось добавть токен'); + } + }, + ), + const SizedBox(height: 10), + CustomOutlinedButton( + text: 'copy token', + buttonStyle: ButtonStyle(), + onPressed: () async { + try { + final token = await FirebaseApi().getToken(); + if (token != null) { + Clipboard.setData(ClipboardData(text: token)); + showInfoToast('Токен скопирован'); + } else { + showInfoToast('Token is null'); + } + } catch (e) { + showInfoToast('Не получилось скопировать токен'); + } + }, + ), + const SizedBox(height: 10), + CustomOutlinedButton( + text: 'remove token from server', + buttonStyle: ButtonStyle(), + onPressed: () async { + try { + final dio = sl(); + final String platform; + if (Platform.isAndroid) { + platform = 'android'; + } else { + platform = 'ios'; + } + await dio.post('/api/profile/notification/token/unregister', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); + showInfoToast('Токен удалён'); + } catch (e) { + showInfoToast('Не удалось удалить токен'); + } + }, + ), + const SizedBox(height: 10), + CustomOutlinedButton( + text: 'send notification', + buttonStyle: ButtonStyle(), + onPressed: () async { + try { + final dio = sl(); + Future.delayed(Duration(seconds: 3), () async { + await dio.post('/api/profile/notification/token/test'); + }); + } catch (e) { + showInfoToast('Не удалось добавть токен'); + } + }, + ), + const SizedBox(height: 300), ], ), ), diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart index a2c22389..c35b8ed1 100644 --- a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/enums/base_state.dart'; import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart'; import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; @@ -22,10 +23,12 @@ class UserCardsCubit extends Cubit { Future getCards() async { try { if (isClosed) return; + emit(state.copyWith(state: BaseState.loading)); final cards = await cardsRepository.getCards(); - emit(state.copyWith(cards: cards)); + emit(state.copyWith(cards: cards, state: BaseState.success)); } catch (e, st) { log(e.toString(), stackTrace: st); + emit(state.copyWith(state: BaseState.error, error: e.toString())); } } @@ -59,7 +62,7 @@ class UserCardsCubit extends Cubit { emit(state.copyWith(cards: cards)); await cardsRepository.deleteCard(id: id); } catch (e, st) { - emit(state.copyWith(cards: oldCards)); + emit(state.copyWith(cards: oldCards, error: e.toString(), state: BaseState.error)); log(e.toString(), stackTrace: st); } } diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart index d9fcd376..93e57dd7 100644 --- a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/enums/base_state.dart'; import 'package:eatvio_app/features/my_finance/models/card_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -7,8 +8,9 @@ part 'user_cards_state.freezed.dart'; class UserCardsState with _$UserCardsState { factory UserCardsState({ - @Default([]) List cards, - @Default(false) bool isLoading, + @Default([]) List cards, + @Default(null) String? error, + @Default(BaseState.initial) BaseState state, }) = _UserCardsState; } \ No newline at end of file diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart index 6af8348f..b8b630e9 100644 --- a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart @@ -18,7 +18,8 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$UserCardsState { List get cards => throw _privateConstructorUsedError; - bool get isLoading => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + BaseState get state => throw _privateConstructorUsedError; /// Create a copy of UserCardsState /// with the given fields replaced by the non-null parameter values. @@ -34,7 +35,7 @@ abstract class $UserCardsStateCopyWith<$Res> { $Res Function(UserCardsState) then, ) = _$UserCardsStateCopyWithImpl<$Res, UserCardsState>; @useResult - $Res call({List cards, bool isLoading}); + $Res call({List cards, String? error, BaseState state}); } /// @nodoc @@ -51,7 +52,11 @@ class _$UserCardsStateCopyWithImpl<$Res, $Val extends UserCardsState> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? cards = null, Object? isLoading = null}) { + $Res call({ + Object? cards = null, + Object? error = freezed, + Object? state = null, + }) { return _then( _value.copyWith( cards: @@ -59,11 +64,16 @@ class _$UserCardsStateCopyWithImpl<$Res, $Val extends UserCardsState> ? _value.cards : cards // ignore: cast_nullable_to_non_nullable as List, - isLoading: - null == isLoading - ? _value.isLoading - : isLoading // ignore: cast_nullable_to_non_nullable - as bool, + error: + freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + state: + null == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as BaseState, ) as $Val, ); @@ -79,7 +89,7 @@ abstract class _$$UserCardsStateImplCopyWith<$Res> ) = __$$UserCardsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({List cards, bool isLoading}); + $Res call({List cards, String? error, BaseState state}); } /// @nodoc @@ -95,7 +105,11 @@ class __$$UserCardsStateImplCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override - $Res call({Object? cards = null, Object? isLoading = null}) { + $Res call({ + Object? cards = null, + Object? error = freezed, + Object? state = null, + }) { return _then( _$UserCardsStateImpl( cards: @@ -103,11 +117,16 @@ class __$$UserCardsStateImplCopyWithImpl<$Res> ? _value._cards : cards // ignore: cast_nullable_to_non_nullable as List, - isLoading: - null == isLoading - ? _value.isLoading - : isLoading // ignore: cast_nullable_to_non_nullable - as bool, + error: + freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + state: + null == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as BaseState, ), ); } @@ -118,7 +137,8 @@ class __$$UserCardsStateImplCopyWithImpl<$Res> class _$UserCardsStateImpl implements _UserCardsState { _$UserCardsStateImpl({ final List cards = const [], - this.isLoading = false, + this.error = null, + this.state = BaseState.initial, }) : _cards = cards; final List _cards; @@ -132,11 +152,14 @@ class _$UserCardsStateImpl implements _UserCardsState { @override @JsonKey() - final bool isLoading; + final String? error; + @override + @JsonKey() + final BaseState state; @override String toString() { - return 'UserCardsState(cards: $cards, isLoading: $isLoading)'; + return 'UserCardsState(cards: $cards, error: $error, state: $state)'; } @override @@ -145,15 +168,16 @@ class _$UserCardsStateImpl implements _UserCardsState { (other.runtimeType == runtimeType && other is _$UserCardsStateImpl && const DeepCollectionEquality().equals(other._cards, _cards) && - (identical(other.isLoading, isLoading) || - other.isLoading == isLoading)); + (identical(other.error, error) || other.error == error) && + (identical(other.state, state) || other.state == state)); } @override int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_cards), - isLoading, + error, + state, ); /// Create a copy of UserCardsState @@ -169,13 +193,18 @@ class _$UserCardsStateImpl implements _UserCardsState { } abstract class _UserCardsState implements UserCardsState { - factory _UserCardsState({final List cards, final bool isLoading}) = - _$UserCardsStateImpl; + factory _UserCardsState({ + final List cards, + final String? error, + final BaseState state, + }) = _$UserCardsStateImpl; @override List get cards; @override - bool get isLoading; + String? get error; + @override + BaseState get state; /// Create a copy of UserCardsState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/my_finance/presentation/pages/card_settings.dart b/lib/features/my_finance/presentation/pages/card_settings.dart index caa6a6e0..d9b981b0 100644 --- a/lib/features/my_finance/presentation/pages/card_settings.dart +++ b/lib/features/my_finance/presentation/pages/card_settings.dart @@ -1,6 +1,10 @@ +import 'package:eatvio_app/core/enums/base_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart'; import 'package:eatvio_app/features/my_finance/presentation/widgets/card_widget.dart'; @@ -35,9 +39,9 @@ class CardsSettingsContent extends StatelessWidget { body: Column( children: [ const SizedBox(height: 16), - Material( - borderRadius: BorderRadius.circular(12), - color: Colors.white, + Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white), + width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -64,21 +68,39 @@ class CardsSettingsContent extends StatelessWidget { ), const SizedBox(height: 16), SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, child: BlocBuilder( builder: (context, state) { - if (state.isLoading) {} - return Row( - spacing: 12, - children: List.generate(state.cards.length + 1, (index) { - if (index < state.cards.length) { - final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7); - return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber); - } - return CardWidget(enabledAddCard: true); - }), - ); + switch (state.state) { + case BaseState.error: + return SizedBox( + width: MediaQuery.sizeOf(context).width, + child: Center( + child: DataErrorWidget( + onReload: () async { + context.read().getCards(); + }, + ), + ), + ); + case BaseState.success: + return Padding( + padding: EdgeInsets.only(left: 12), + child: Row( + spacing: 12, + children: List.generate(state.cards.length + 1, (index) { + if (index < state.cards.length) { + final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7); + return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber); + } + return CardWidget(enabledAddCard: true); + }), + ), + ); + + default: + return SizedBox(width: MediaQuery.sizeOf(context).width, height: 200, child: Center(child: Loader())); + } }, ), ), diff --git a/lib/features/my_finance/presentation/widgets/card_widget.dart b/lib/features/my_finance/presentation/widgets/card_widget.dart index 22789c21..7d26342f 100644 --- a/lib/features/my_finance/presentation/widgets/card_widget.dart +++ b/lib/features/my_finance/presentation/widgets/card_widget.dart @@ -16,53 +16,68 @@ class CardWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return GestureDetector( - onTap: enabledAddCard - ? () async { - final addCardUrl = await context.read().addCard(); + return Stack( + children: [ + GestureDetector( + onTap: + enabledAddCard + ? () async { + final addCardUrl = await context.read().addCard(); - if (addCardUrl != null) { - final uri = Uri.parse(addCardUrl.paymentUrl); - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } - } - : () { - context.read().markAsDefault(id: cardId); - }, - child: Container( - padding: EdgeInsets.all(4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: isSelected ? appTheme.blueA400 : Color(0xFFF1F2F6), width: 2), + if (addCardUrl != null) { + final uri = Uri.parse(addCardUrl.paymentUrl); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + } + : () { + context.read().markAsDefault(id: cardId); + }, + child: Container( + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: isSelected ? appTheme.blueA400 : Color(0xFFF1F2F6), width: 2), + ), + child: + enabledAddCard + ? Container( + width: 148, + padding: EdgeInsets.all(10), + decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)), + child: Column( + children: [ + SvgPicture.asset('assets/icons/add_card.svg'), + const SizedBox(height: 24), + Text(s.new_card, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)), + ], + ), + ) + : Container( + width: 148, + padding: EdgeInsets.all(10), + decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)), + child: Column( + children: [ + SvgPicture.asset('assets/icons/card.svg'), + const SizedBox(height: 24), + Text(cardNumber, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)), + ], + ), + ), + ), ), - child: enabledAddCard - ? Container( - width: 148, - padding: EdgeInsets.all(10), - decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)), - child: Column( - children: [ - SvgPicture.asset('assets/icons/add_card.svg'), - const SizedBox(height: 24), - Text(s.new_card, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)), - ], - ), - ) - : Container( - width: 148, - padding: EdgeInsets.all(10), - decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)), - child: Column( - children: [ - SvgPicture.asset('assets/icons/card.svg'), - const SizedBox(height: 24), - Text(cardNumber, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)), - ], - ), - ), - ), + if (!enabledAddCard) + Positioned( + top: 12, + right: 12, + child: GestureDetector( + child: SvgPicture.asset('assets/images/img_close_blue_gray_900.svg'), + onTap: () => context.read().deleteCard(id: cardId), + ), + ), + ], ); } } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 00000000..dcf61a5d --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,70 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k', + appId: '1:613980912922:android:4de95e5f2d50ba69ecc0a3', + messagingSenderId: '613980912922', + projectId: 'eatvio-3467b', + databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'eatvio-3467b.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ', + appId: '1:613980912922:ios:c557d140a1212e63ecc0a3', + messagingSenderId: '613980912922', + projectId: 'eatvio-3467b', + databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'eatvio-3467b.firebasestorage.app', + iosBundleId: 'com.eatvioapp.app', + ); +} diff --git a/lib/main.dart b/lib/main.dart index 7cdeb5fa..e7ece612 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -8,7 +9,9 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/prof import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; +import 'package:eatvio_app/firebase_options.dart'; import 'package:eatvio_app/generated/l10n.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -20,14 +23,12 @@ import 'package:sentry_flutter/sentry_flutter.dart'; Future main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await di.init(); - await SentryFlutter.init( - (options) { - options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; - options.tracesSampleRate = 1.0; - }, - appRunner: () => runApp(MyApp()), - ); + await SentryFlutter.init((options) { + options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; + options.tracesSampleRate = 1.0; + }, appRunner: () => runApp(MyApp())); FlutterNativeSplash.remove(); } @@ -55,9 +56,7 @@ class MyApp extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: [ - Locale.fromSubtags(languageCode: 'ru'), - ], + supportedLocales: [Locale.fromSubtags(languageCode: 'ru')], ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index a6ab85d4..faef1c98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.0.48+48 +version: 1.0.50+2 publish_to: 'none' environment: @@ -62,6 +62,8 @@ dependencies: sentry_flutter: ^8.13.0 connectivity_plus: ^6.1.3 visibility_detector: ^0.4.0+2 + firebase_core: ^3.12.1 + firebase_messaging: ^15.2.4 dependency_overrides: intl: ^0.19.0