feat(push notifications): test implemintation of push notifcations

This commit is contained in:
Username1548
2025-03-26 19:48:02 +07:00
parent fd089e0206
commit b54deb66e7
26 changed files with 580 additions and 355 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@
/node_modules/
/android/app/release/app-release.aab
/android/app/upload-keystore.jks
/android/app/.cxx

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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
}

1
firebase.json Normal file
View File

@@ -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"}}}}}}

View File

@@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
EB9FE5381C3F532BD9A9456D /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
FEE92A0E2D772EA300E6E989 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -91,6 +93,7 @@
97C146EF1CF9000F007C117D /* Products */,
6F201E010624CF963CC3A17B /* Pods */,
AEE6B7943D197783DD1CB2EE /* Frameworks */,
EB9FE5381C3F532BD9A9456D /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -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 = "";

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ</string>
<key>GCM_SENDER_ID</key>
<string>613980912922</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.eatvioapp.app</string>
<key>PROJECT_ID</key>
<string>eatvio-3467b</string>
<key>STORAGE_BUCKET</key>
<string>eatvio-3467b.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:613980912922:ios:c557d140a1212e63ecc0a3</string>
<key>DATABASE_URL</key>
<string>https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app</string>
</dict>
</plist>

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@@ -24,6 +22,8 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
@@ -42,6 +42,7 @@
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>

View File

@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>disabled</string>
<string>development</string>
</dict>
</plist>

View File

@@ -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<int> durationValues = [7, 14, 21];

View File

@@ -0,0 +1,11 @@
import 'package:firebase_messaging/firebase_messaging.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
Future<String?> getToken() async{
await _firebaseMessaging.requestPermission();
return await _firebaseMessaging.getToken();
}
}

View File

@@ -94,13 +94,13 @@ Future<void> init() async {
// Cubit factories
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(chatRepository: sl(), wsManager: sl()));
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl()));
sl.registerFactory<profilePlans.PlansCubit>(() => profilePlans.PlansCubit(plansRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
@@ -171,15 +171,7 @@ Future<void> 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());
}

View File

@@ -25,43 +25,33 @@ class _DataErrorWidgetState extends State<DataErrorWidget> {
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;
});
},
),
),
],
);
}

View File

@@ -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<ChatRoomState> {
final ChatRepository chatRepository;
final CardsRepository cardsRepository;
final WsManager wsManager;
StreamSubscription<ChannelReadEvent>? typingSubscription;
@@ -23,34 +26,22 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
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<ChatRoomState> {
}
}
Future<void> checkCards() async {
try {
final cards = await cardsRepository.getCards();
setCard(cards: cards);
} catch (e) {
emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
}
}
void setCard({required List<CardModel> 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<ChatRoomState> {
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<ChatRoomState> {
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<void> _subscribeToChatChannel({
required String chatId,
}) async {
Future<void> _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<ChatRoomState> {
messagesSubscription = wsManager.directChatStream?.listen((event) {
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']);
List<ChatMessageModel> messagesList = [
message,
...(state as ChatRoomStateLoaded).chatMessages,
];
List<ChatMessageModel> 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<bool> sendMessage({
required String chatId,
String? data,
List<File>? files,
}) async {
Future<bool> sendMessage({required String chatId, String? data, List<File>? 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<ChatRoomState> {
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;
}

View File

@@ -13,6 +13,7 @@ abstract class ChatRoomState with _$ChatRoomState {
const factory ChatRoomState.loaded({
required ChatRoomModel chatRoom,
required List<ChatMessageModel> chatMessages,
required bool hasCard,
required bool isTyping,
required bool isLoading,
}) = ChatRoomStateLoaded;

View File

@@ -24,6 +24,7 @@ mixin _$ChatRoomState {
required TResult Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)
@@ -37,6 +38,7 @@ mixin _$ChatRoomState {
TResult? Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?
@@ -50,6 +52,7 @@ mixin _$ChatRoomState {
TResult Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?
@@ -151,6 +154,7 @@ class _$ChatRoomStateImpl implements _ChatRoomState {
required TResult Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)
@@ -168,6 +172,7 @@ class _$ChatRoomStateImpl implements _ChatRoomState {
TResult? Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?
@@ -185,6 +190,7 @@ class _$ChatRoomStateImpl implements _ChatRoomState {
TResult Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?
@@ -289,6 +295,7 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading {
required TResult Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)
@@ -306,6 +313,7 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading {
TResult? Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?
@@ -323,6 +331,7 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading {
TResult Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?
@@ -388,6 +397,7 @@ abstract class _$$ChatRoomStateLoadedImplCopyWith<$Res> {
$Res call({
ChatRoomModel chatRoom,
List<ChatMessageModel> 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<ChatMessageModel>,
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<ChatMessageModel> 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<ChatMessageModel> 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<ChatMessageModel> 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<ChatMessageModel> 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<ChatMessageModel> chatMessages,
required final bool hasCard,
required final bool isTyping,
required final bool isLoading,
}) = _$ChatRoomStateLoadedImpl;
ChatRoomModel get chatRoom;
List<ChatMessageModel> 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<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)
@@ -728,6 +755,7 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError {
TResult? Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?
@@ -745,6 +773,7 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError {
TResult Function(
ChatRoomModel chatRoom,
List<ChatMessageModel> chatMessages,
bool hasCard,
bool isTyping,
bool isLoading,
)?

View File

@@ -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<ChatScreen> createState() => _ChatScreenState();
@@ -72,14 +70,9 @@ class _ChatScreenState extends State<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
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<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy);
if (!result) return;
context
.read<ChatsCubit>()
.reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIdsCopy.length);
unreadMessagesIdsCopy.clear();
},
);
EasyDebounce.debounce('mark_as_read', Duration(milliseconds: 500), () async {
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy);
if (!result) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount(
chatId: state.chatRoom.id,
count: unreadMessagesIdsCopy.length,
);
unreadMessagesIdsCopy.clear();
});
}
},
child: ChatBubbleWidget(
@@ -219,16 +196,12 @@ class _ChatScreenState extends State<ChatScreen> {
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<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
if (!result) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
unreadMessagesIds.clear();
},
);
EasyDebounce.debounce('mark_as_read', Duration(milliseconds: 500), () async {
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
if (!result) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
unreadMessagesIds.clear();
});
}
},
child: ChatBubbleWidget(
@@ -268,6 +241,8 @@ class _ChatScreenState extends State<ChatScreen> {
},
),
),
if (bannerType != null)
Padding(padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: NotificationBanner(bannerType: bannerType)),
],
],
],
@@ -310,24 +285,14 @@ class _ChatScreenState extends State<ChatScreen> {
if (result != null) {
List<File> 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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
),
],
),
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<ChatScreen> {
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)),
),
);
}

View File

@@ -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<DevToolsScreenContent> {
@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<DevToolsScreenContent> {
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<DevToolsScreenContent> {
),
),
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'add token to server',
buttonStyle: ButtonStyle(),
onPressed: () async {
try {
final dio = sl<Dio>();
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<Dio>();
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<Dio>();
Future.delayed(Duration(seconds: 3), () async {
await dio.post('/api/profile/notification/token/test');
});
} catch (e) {
showInfoToast('Не удалось добавть токен');
}
},
),
const SizedBox(height: 300),
],
),
),

View File

@@ -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<UserCardsState> {
Future<void> 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<UserCardsState> {
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);
}
}

View File

@@ -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<CardModel> cards,
@Default(false) bool isLoading,
@Default([]) List<CardModel> cards,
@Default(null) String? error,
@Default(BaseState.initial) BaseState state,
}) = _UserCardsState;
}

View File

@@ -18,7 +18,8 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc
mixin _$UserCardsState {
List<CardModel> 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<CardModel> cards, bool isLoading});
$Res call({List<CardModel> 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<CardModel>,
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<CardModel> cards, bool isLoading});
$Res call({List<CardModel> 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<CardModel>,
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<CardModel> cards = const [],
this.isLoading = false,
this.error = null,
this.state = BaseState.initial,
}) : _cards = cards;
final List<CardModel> _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<CardModel> cards, final bool isLoading}) =
_$UserCardsStateImpl;
factory _UserCardsState({
final List<CardModel> cards,
final String? error,
final BaseState state,
}) = _$UserCardsStateImpl;
@override
List<CardModel> 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.

View File

@@ -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<UserCardsCubit, UserCardsState>(
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<UserCardsCubit>().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()));
}
},
),
),

View File

@@ -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<UserCardsCubit>().addCard();
return Stack(
children: [
GestureDetector(
onTap:
enabledAddCard
? () async {
final addCardUrl = await context.read<UserCardsCubit>().addCard();
if (addCardUrl != null) {
final uri = Uri.parse(addCardUrl.paymentUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
}
: () {
context.read<UserCardsCubit>().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<UserCardsCubit>().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<UserCardsCubit>().deleteCard(id: cardId),
),
),
],
);
}
}

70
lib/firebase_options.dart Normal file
View File

@@ -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',
);
}

View File

@@ -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<void> 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')],
),
),
),

View File

@@ -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