diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 4320ecc5..c1244bd9 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -11,6 +11,30 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Checkout env repository + uses: actions/checkout@v2 + with: + repository: mso7nueh/eatvio_app_env + path: env_repo + token: ${{ vars.MSO7NUEH_TOKEN }} + ref: main + + - name: Copy .env file + run: | + mkdir -p env + cp env_repo/.env env/.env + + - name: Load environment variables from .env + run: | + set -a + source env/.env + set +a + # Export all variables to GITHUB_ENV + while IFS= read -r line; do + if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then + echo "$line" >> $GITHUB_ENV + fi + done < env/.env - name: Unlock login keychain run: | @@ -36,4 +60,4 @@ jobs: MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane FASTLANE_USER: eatvio2023@icloud.com FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} - API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} \ No newline at end of file + API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index fd291367..c7aefd5c 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -16,6 +16,31 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Checkout env repository + uses: actions/checkout@v3 + with: + repository: mso7nueh/eatvio_app_env + path: env_repo + token: ${{ vars.MSO7NUEH_TOKEN }} + ref: main + + - name: Copy .env file + run: | + mkdir -p env + cp env_repo/.env env/.env + + - name: Load environment variables from .env + run: | + set -a + source env/.env + set +a + # Export all variables to GITHUB_ENV + while IFS= read -r line; do + if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then + echo "$line" >> $GITHUB_ENV + fi + done < env/.env + - name: Create key.properties file run: | cat > android/key.properties < diff --git a/env/.env b/env/.env new file mode 100644 index 00000000..e69de29b diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9a27847e..6388d648 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -396,7 +396,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 65; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 65; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -584,7 +584,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 64; + CURRENT_PROJECT_VERSION = 65; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2341e63f..9a16914c 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - eatvio_app + Итвио CFBundlePackageType APPL CFBundleShortVersionString @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 64 + 65 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 9340117d..7dff33a3 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -20,7 +20,13 @@ platform :ios do lane :beta do match(type: "appstore", readonly: true) increment_build_number(xcodeproj: "Runner.xcodeproj") - build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution") + build_app( + workspace: "Runner.xcworkspace", + scheme: "Runner", + export_method: "app-store", + codesigning_identity: "Apple Distribution", + build_args: "--dart-define=HIDE_PLANS=true" + ) app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8") upload_to_testflight end diff --git a/lib/core/const.dart b/lib/core/const.dart index f22f79aa..efed7d49 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,7 @@ -String baseUrl = 'https://eatvio.ru'; -bool hidePlans = true; +import 'package:eatvio_app/core/env_config.dart'; + +String baseUrl = EnvConfig.baseUrl; +bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const List durationValues = [7, 14, 21]; diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart new file mode 100644 index 00000000..23c18f62 --- /dev/null +++ b/lib/core/env_config.dart @@ -0,0 +1,43 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class EnvConfig { + static String get baseUrl => dotenv.env['BASE_URL'] ?? 'https://eatvio.ru'; + static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? 'https://dev2.eatvio.com'; + + // Sentry Configuration + static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; + + // Firebase Configuration + static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? 'eatvio-3467b'; + static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app'; + static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? 'eatvio-3467b.firebasestorage.app'; + static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '613980912922'; + + // Firebase Android Configuration + static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k'; + static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '1:613980912922:android:4de95e5f2d50ba69ecc0a3'; + + // Firebase iOS Configuration + static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ'; + static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '1:613980912922:ios:c557d140a1212e63ecc0a3'; + static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? 'com.eatvioapp.app'; + + // App Store URLs + static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? 'https://apps.apple.com/app/idYOUR_APP_ID'; + static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? 'https://www.rustore.ru/catalog/app/com.eatvioapp.app'; + + // Telegram Support + static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? 'https://t.me/eatvio'; + static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? 'help@eatv.io'; + + // Test Credentials (for development only) + static String get testEmail => dotenv.env['TEST_EMAIL'] ?? 'reshetnick.mike+user1@gmail.com'; + static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '1234567890'; + + // Email Validation API + static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? 'https://api.validate.email/validate'; + + // Connection Check URLs + static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? 'https://google.com'; + static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? 'https://yandex.ru'; +} diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart index 04a0b7fb..760a8783 100644 --- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart +++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart @@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart'; class ConsultationCardWidget extends StatefulWidget { final ConsultationModel consultationsModel; - const ConsultationCardWidget({ - super.key, - required this.consultationsModel, - }); + const ConsultationCardWidget({super.key, required this.consultationsModel}); @override State createState() => _ConsultationCardWidgetState(); @@ -32,13 +29,7 @@ class _ConsultationCardWidgetState extends State { 'Дневник заполнен, прокомментирован консультантом и после этого клиент просмотрел комментарии.', ]; - final List legendColors = const [ - Colors.white, - Color(0xFFF0C497), - Color(0xFFFF9500), - Color(0xFF2F80ED), - Color(0xFF4CD964), - ]; + final List legendColors = const [Colors.white, Color(0xFFF0C497), Color(0xFFFF9500), Color(0xFF2F80ED), Color(0xFF4CD964)]; late final List<(int, DiaryModel)> givenDaysWithDayNumber; final List> legendStates = [ @@ -46,7 +37,7 @@ class _ConsultationCardWidgetState extends State { [DiaryStateEnum.started, DiaryStateEnum.lastDayStarted], [DiaryStateEnum.finished, DiaryStateEnum.lastDayFinished], [DiaryStateEnum.commented, DiaryStateEnum.lastDayCommented], - [DiaryStateEnum.reviewedByUser] + [DiaryStateEnum.reviewedByUser], ]; @override void initState() { @@ -72,32 +63,19 @@ class _ConsultationCardWidgetState extends State { children: [ Row( children: [ - CircleAvatar( - radius: 30, - backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl), - ), + CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.consultationsModel.userName, - style: const TextStyle( - height: 1, - color: Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w600, - ), + style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 4), Text( widget.consultationsModel.planTitle, - style: const TextStyle( - height: 1, - color: Color(0xFF88949F), - fontSize: 14, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), @@ -127,12 +105,7 @@ class _ConsultationCardWidgetState extends State { children: [ Text( 'День ${widget.consultationsModel.currentDiaryDay} из ${widget.consultationsModel.duration}', - style: const TextStyle( - height: 1, - color: Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500), ), GestureDetector( onTap: () { @@ -149,12 +122,7 @@ class _ConsultationCardWidgetState extends State { children: [ const Text( 'Легенда', - style: TextStyle( - height: 1, - fontSize: 20, - fontWeight: FontWeight.w700, - color: Color(0xFF323F4B), - ), + style: TextStyle(height: 1, fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)), ), const SizedBox(height: 16), ListView.separated( @@ -170,11 +138,7 @@ class _ConsultationCardWidgetState extends State { ), ); }, - child: const Icon( - Icons.help_outline_rounded, - color: Color(0xFF323F4B), - size: 20, - ), + child: const Icon(Icons.help_outline_rounded, color: Color(0xFF323F4B), size: 20), ), ], ), @@ -185,7 +149,8 @@ class _ConsultationCardWidgetState extends State { children: List.generate(widget.consultationsModel.duration, (index) { final day = givenDaysWithDayNumber.firstWhereOrNull((e) => e.$1 == index); final hasActivity = day != null && day.$2.state != DiaryStateEnum.init; - final canConsultantOpenThisDay = day != null && + final canConsultantOpenThisDay = + day != null && (day.$2.state != DiaryStateEnum.init && day.$2.state != DiaryStateEnum.started && day.$2.state != DiaryStateEnum.lastDayStarted); return Padding( padding: const EdgeInsets.only(right: 12), @@ -195,81 +160,74 @@ class _ConsultationCardWidgetState extends State { InkWell( onTap: () { if (!hasActivity || !canConsultantOpenThisDay) { - showErrorDialog(context, - title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}'); + showErrorDialog( + context, + title: s.you_can_not_open_this_day, + description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}', + ); return; } - GoRouter.of(context).push(UserDiaryScreen.path, extra: { - 'diaryId': day.$2.id, - 'currentDiaryDay': index + 1, - }); + GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1}); }, borderRadius: BorderRadius.circular(100), child: Ink( decoration: BoxDecoration( - color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(100)), + color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(100), + ), width: 58, height: 32, child: Center( child: Text( (index + 1).toString(), - style: TextStyles.textMSemibold.copyWith( - color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4), - ), + style: TextStyles.textMSemibold.copyWith(color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4)), ), ), ), ), if (hasActivity) ...[ - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), switch (day.$2.state) { DiaryStateEnum.init => const SizedBox.shrink(), DiaryStateEnum.started => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), + ), DiaryStateEnum.lastDayStarted => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]), + ), DiaryStateEnum.finished => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), + ), DiaryStateEnum.lastDayFinished => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]), + ), DiaryStateEnum.commented => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), + ), DiaryStateEnum.lastDayCommented => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]), + ), DiaryStateEnum.reviewedByUser => Container( - width: 8, - height: 8, - decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]), - ), + width: 8, + height: 8, + decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]), + ), }, - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), ] else - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), ], ), ); @@ -280,11 +238,7 @@ class _ConsultationCardWidgetState extends State { const SizedBox(height: 12), Text( '${widget.consultationsModel.consultationOptionPrice.toStringAsFixed(0)} ₽', - style: const TextStyle( - color: Color(0xFF019A3E), - fontSize: 20, - fontWeight: FontWeight.w600, - ), + style: const TextStyle(color: Color(0xFF019A3E), fontSize: 20, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), Row( @@ -293,35 +247,26 @@ class _ConsultationCardWidgetState extends State { borderRadius: BorderRadius.circular(20), onTap: () { final lastActiveDay = givenDaysWithDayNumber - .where( - (e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished, - ) + .where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished) .lastOrNull; if (lastActiveDay != null) { - GoRouter.of(context).push(UserDiaryScreen.path, extra: { - 'diaryId': lastActiveDay.$2.id, - 'currentDiaryDay': lastActiveDay.$1, - }); + GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1}); return; } - showErrorDialog(context, - title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}'); + showErrorDialog( + context, + title: s.you_can_not_open_this_day, + description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}', + ); }, child: Ink( width: MediaQuery.of(context).size.width - 32 - 52 - 8, padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), child: const Center( child: Text( 'Комментировать отчёт', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), ), ), ), @@ -333,16 +278,8 @@ class _ConsultationCardWidgetState extends State { child: Ink( height: 52, width: 52, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), - child: const Center( - child: Icon( - Icons.chat_outlined, - size: 24, - ), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), + child: const Center(child: Icon(Icons.chat_outlined, size: 24)), ), ), ], @@ -360,21 +297,13 @@ class _ConsultationCardWidgetState extends State { Container( height: 20, width: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), + decoration: BoxDecoration(shape: BoxShape.circle, color: color), ), const SizedBox(width: 12), Flexible( child: Text( text, - style: const TextStyle( - height: 1, - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF323F4B), - ), + style: const TextStyle(height: 1, fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)), ), ), ], diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index a4ff7730..4922a87d 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -9,6 +9,7 @@ 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:eatvio_app/core/env_config.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -97,10 +98,15 @@ class _DevToolsScreenContentState extends State { padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( children: [ - Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')])), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row(children: [Text('BaseUrl: $baseUrlForTest')]), + ), Row( children: [ - Expanded(child: CustomTextFormField(controller: controller, hintText: 'type new base url')), + Expanded( + child: CustomTextFormField(controller: controller, hintText: 'type new base url'), + ), IconButton( onPressed: () { setState(() { @@ -116,13 +122,13 @@ class _DevToolsScreenContentState extends State { ), const SizedBox(height: 10), CustomOutlinedButton( - text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url', + text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', onPressed: () { setState(() { - if (baseUrl == 'https://eatvio.ru') { - baseUrlForTest = 'https://dev2.eatvio.com'; + if (baseUrl == EnvConfig.baseUrl) { + baseUrlForTest = EnvConfig.devBaseUrl; } else { - baseUrlForTest = 'https://eatvio.ru'; + baseUrlForTest = EnvConfig.baseUrl; } baseUrl = baseUrlForTest.toLowerCase(); sl().options.baseUrl = baseUrl; @@ -132,7 +138,7 @@ class _DevToolsScreenContentState extends State { buttonStyle: const ButtonStyle(), ), SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), - if (baseUrl.toLowerCase() != 'https://eatvio.ru') + if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) ListView.builder( shrinkWrap: true, itemCount: testCases.length, @@ -175,12 +181,7 @@ class _DevToolsScreenContentState extends State { builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } @@ -197,12 +198,7 @@ class _DevToolsScreenContentState extends State { builder: (context) => AlertDialog( title: const Text('Test Failed'), content: SingleChildScrollView(child: Text('$e\n$stack')), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], + actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], ), ); } @@ -216,10 +212,10 @@ class _DevToolsScreenContentState extends State { TestState.fail => Container(width: 20, height: 20, color: Colors.red), TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), TestState.loading => const SizedBox( - width: 20, - height: 20, - child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), - ), + width: 20, + height: 20, + child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), + ), }, ], ), diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart index eb72a1e7..3d43bd2a 100644 --- a/lib/features/dev_tools/test_cases.dart +++ b/lib/features/dev_tools/test_cases.dart @@ -35,6 +35,7 @@ import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart'; +import 'package:eatvio_app/core/env_config.dart'; class TestResult { final bool success; @@ -477,7 +478,9 @@ class AuthServiceTest implements TestCases { final dio = sl(); final authService = AuthService(dio, baseUrl: baseUrl); // Use dummy credentials, expecting failure but no crash - await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890')); + await authService.getTokenByCredentials( + credentials: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword), + ); return TestResult(false, 'Should not succeed'); } catch (e) { return TestResult(true, 'Exception expected for dummy credentials'); diff --git a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart index 3aa1bc34..576eb891 100644 --- a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart +++ b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart' as http; +import 'package:eatvio_app/core/env_config.dart'; class InternetConnectionCheckerScreen extends StatefulWidget { const InternetConnectionCheckerScreen({super.key, required this.child}); @@ -66,12 +67,7 @@ class _InternetConnectionCheckerScreenState extends State GoRouter.of(context).push(MyPlansScreen.path)), - _divider(context), - _tile( - leading: 'assets/icons/finance.svg', - title: 'Мои финансы', - onTap: () { - if (profileModel.roles.contains('consultant')) { - GoRouter.of(context).push(ConsultantFinancesScreen.path); - return; - } - GoRouter.of(context).push(UserPurchasesScreen.path); - }, - ), + if (!hidePlans) ...[ + _divider(context), + _tile( + leading: 'assets/icons/finance.svg', + title: 'Мои финансы', + onTap: () { + if (profileModel.roles.contains('consultant')) { + GoRouter.of(context).push(ConsultantFinancesScreen.path); + return; + } + GoRouter.of(context).push(UserPurchasesScreen.path); + }, + ), + ], _divider(context), _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), diff --git a/lib/features/notifications/models/notification_model.dart b/lib/features/notifications/models/notification_model.dart index 827a0904..96626d5d 100644 --- a/lib/features/notifications/models/notification_model.dart +++ b/lib/features/notifications/models/notification_model.dart @@ -1,8 +1,7 @@ -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; - sealed class BaseNotificationModel { abstract final NotificationType type; abstract final bool isRead; + abstract final DateTime createdAt; } class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel { @@ -13,6 +12,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod final String planName; final String customerName; final String chatId; + @override + final DateTime createdAt; PlanSubscriptionStartedToAuthorNotification({ required this.type, @@ -20,15 +21,17 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod required this.customerName, required this.chatId, required this.isRead, + required this.createdAt, }); - factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionStartedToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], chatId: json['chat_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -40,20 +43,24 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; PlanSubscriptionCanceledToAuthorNotification({ required this.type, required this.planName, required this.customerName, required this.isRead, + required this.createdAt, }); - factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToAuthorNotification( type: type, planName: json['plan_name'], customerName: json['user_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -65,20 +72,18 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel { final bool isRead; final String planName; final int planId; + @override + final DateTime createdAt; - ChosenPlanStartedToClientNotification({ - required this.type, - required this.planName, - required this.planId, - required this.isRead, - }); + ChosenPlanStartedToClientNotification({required this.type, required this.planName, required this.planId, required this.isRead, required this.createdAt}); - factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanStartedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanStartedToClientNotification( type: type, planName: json['plan_name'], planId: json['plan_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -89,15 +94,18 @@ class ChosenPlanTomorrowReminderToClientNotification implements BaseNotification @override final bool isRead; final String planName; + @override + final DateTime createdAt; - ChosenPlanTomorrowReminderToClientNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + ChosenPlanTomorrowReminderToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { - return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false); + factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanTomorrowReminderToClientNotification( + type: type, + planName: json['plan_name'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); } } @@ -106,11 +114,17 @@ class DiaryReminderNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - DiaryReminderNotification({required this.type, required this.isRead}); + DiaryReminderNotification({required this.type, required this.isRead, required this.createdAt}); - factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { - return DiaryReminderNotification(type: type, isRead: isRead ?? false); + factory DiaryReminderNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return DiaryReminderNotification( + type: type, + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); } } @@ -120,18 +134,17 @@ class ConsultantLeftCommentNotification implements BaseNotificationModel { @override final bool isRead; final DateTime date; + @override + final DateTime createdAt; - ConsultantLeftCommentNotification({ - required this.type, - required this.date, - required this.isRead, - }); + ConsultantLeftCommentNotification({required this.type, required this.date, required this.isRead, required this.createdAt}); - factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ConsultantLeftCommentNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ConsultantLeftCommentNotification( type: type, - date: parseDate(json['diary_date']), + date: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()), isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -141,16 +154,16 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - PersonalizedDietIsDefinedNotification({ - required this.type, - required this.isRead, - }); + PersonalizedDietIsDefinedNotification({required this.type, required this.isRead, required this.createdAt}); - factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PersonalizedDietIsDefinedNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PersonalizedDietIsDefinedNotification( type: type, isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -161,17 +174,16 @@ class ChosenPlanFinishedToClientNotification implements BaseNotificationModel { @override final bool isRead; final String planName; - ChosenPlanFinishedToClientNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + @override + final DateTime createdAt; + ChosenPlanFinishedToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanFinishedToClientNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToClientNotification( type: type, planName: json['plan_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -182,17 +194,16 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification @override final bool isRead; final String planName; - PlanSubscriptionCanceledToCustomerNotification({ - required this.type, - required this.planName, - required this.isRead, - }); + @override + final DateTime createdAt; + PlanSubscriptionCanceledToCustomerNotification({required this.type, required this.planName, required this.isRead, required this.createdAt}); - factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionCanceledToCustomerNotification( type: type, planName: json['plan_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -204,19 +215,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; ChosenPlanStartedToConsultantNotification({ required this.type, required this.isRead, required this.planName, required this.customerName, + required this.createdAt, }); - factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanStartedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanStartedToConsultantNotification( type: type, isRead: isRead ?? false, planName: json['plan_name'], customerName: json['client_name'], + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -228,19 +243,23 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; ChosenPlanTomorrowReminderToConsultantNotification({ required this.type, required this.planName, required this.customerName, required this.isRead, + required this.createdAt, }); - factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanTomorrowReminderToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -252,19 +271,23 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel final bool isRead; final String planName; final String customerName; + @override + final DateTime createdAt; PlanSubscriptionPaidToAuthorNotification({ required this.type, required this.isRead, required this.planName, required this.customerName, + required this.createdAt, }); - factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return PlanSubscriptionPaidToAuthorNotification( type: type, isRead: isRead ?? false, planName: json['plan_name'], customerName: json['user_name'], + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -277,21 +300,25 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode final String planName; final String customerName; final int planId; + @override + final DateTime createdAt; ChosenPlanFinishedToConsultantNotification({ required this.type, required this.planName, required this.customerName, required this.planId, required this.isRead, + required this.createdAt, }); - factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanFinishedToConsultantNotification.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanFinishedToConsultantNotification( type: type, planName: json['plan_name'], customerName: json['client_name'], planId: json['plan_id'], isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -301,16 +328,212 @@ class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - ChosenPlanLastDayFinishedForUser({ - required this.type, - required this.isRead, - }); + ChosenPlanLastDayFinishedForUser({required this.type, required this.isRead, required this.createdAt}); - factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead}) { + factory ChosenPlanLastDayFinishedForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { return ChosenPlanLastDayFinishedForUser( type: type, isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ChosenPlanConsultantLastDayStarted implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String planName; + final int planId; + final String chatId; + final String chatUrl; + @override + final DateTime createdAt; + + ChosenPlanConsultantLastDayStarted({ + required this.type, + required this.planName, + required this.planId, + required this.chatId, + required this.chatUrl, + required this.isRead, + required this.createdAt, + }); + + factory ChosenPlanConsultantLastDayStarted.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ChosenPlanConsultantLastDayStarted( + type: type, + planName: json['plan_name'], + planId: json['plan_id'], + chatId: json['chat_id'], + chatUrl: json['chat_url'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ComplaintToModerationForUser implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String message; + final int entityId; + final String entityType; + @override + final DateTime createdAt; + + ComplaintToModerationForUser({ + required this.type, + required this.message, + required this.entityId, + required this.entityType, + required this.isRead, + required this.createdAt, + }); + + factory ComplaintToModerationForUser.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ComplaintToModerationForUser( + type: type, + message: json['message'], + entityId: json['entity_id'], + entityType: json['entity_type'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class CommentReplied implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String entityName; + final int commentedEntityId; + final String commentedEntityType; + final String author; + final int authorId; + final int parentCommentId; + @override + final DateTime createdAt; + + CommentReplied({ + required this.type, + required this.entityName, + required this.commentedEntityId, + required this.commentedEntityType, + required this.author, + required this.authorId, + required this.parentCommentId, + required this.isRead, + required this.createdAt, + }); + + factory CommentReplied.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return CommentReplied( + type: type, + entityName: json['entity_name'], + commentedEntityId: json['commented_entity_id'], + commentedEntityType: json['commented_entity_type'], + author: json['author'], + authorId: json['author_id'], + parentCommentId: json['parrent_comment_id'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class ScoreAdded implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String scoreKey; + final int scorableId; + final String scorableType; + final String scorableName; + final String scorableSlug; + final int scoreValue; + @override + final DateTime createdAt; + + ScoreAdded({ + required this.type, + required this.scoreKey, + required this.scorableId, + required this.scorableType, + required this.scorableName, + required this.scorableSlug, + required this.scoreValue, + required this.isRead, + required this.createdAt, + }); + + factory ScoreAdded.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return ScoreAdded( + type: type, + scoreKey: json['score_key'], + scorableId: json['scorable_id'], + scorableType: json['scorable_type'], + scorableName: json['scorable_name'] ?? '', + scorableSlug: json['scorable_slug'] ?? '', + scoreValue: json['score_value'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), + ); + } +} + +class EntityCommented implements BaseNotificationModel { + @override + final NotificationType type; + @override + final bool isRead; + final String entityName; + final int entityId; + final String entityType; + final String entityTranslatedType; + final String entityUrl; + final String authorName; + final int authorId; + final String authorSlug; + @override + final DateTime createdAt; + + EntityCommented({ + required this.type, + required this.entityName, + required this.entityId, + required this.entityType, + required this.entityTranslatedType, + required this.entityUrl, + required this.authorName, + required this.authorId, + required this.authorSlug, + required this.isRead, + required this.createdAt, + }); + + factory EntityCommented.fromJson(Map json, NotificationType type, {bool? isRead, String? createdAt}) { + return EntityCommented( + type: type, + entityName: json['entity_name'], + entityId: json['entity_id'], + entityType: json['entity_type'], + entityTranslatedType: json['entity_traslated_type'], + entityUrl: json['entity_url'], + authorName: json['author_name'], + authorId: json['author_id'], + authorSlug: json['author_slug'], + isRead: isRead ?? false, + createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()), ); } } @@ -320,11 +543,10 @@ class UnknownNotification implements BaseNotificationModel { final NotificationType type; @override final bool isRead; + @override + final DateTime createdAt; - UnknownNotification({ - required this.type, - this.isRead = true, - }); + UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0); } enum NotificationType { @@ -342,23 +564,37 @@ enum NotificationType { planSubscriptionPaidToAuthor, chosenPlanFinishedToConsultant, chosenPlanLastDayFinishedForUser, + // Новые типы + chosenPlanConsultantLastDayStarted, + complaintToModerationForUser, + commentReplied, + scoreAdded, + entityCommented, unknown; static NotificationType getTypeFromString(String type) => switch (type) { - 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, - 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, - 'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, - 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, - 'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification, - 'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment, - 'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined, - 'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient, - 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer, - 'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification, - 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, - 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, - 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, - 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, - _ => NotificationType.unknown, - }; + 'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor, + 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor, + 'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, + 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification, + 'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification, + 'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment, + 'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined, + 'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient, + 'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer, + 'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification, + 'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification, + 'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor, + 'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant, + 'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser, + // Новые маппинги + 'App\\Notifications\\ChosenPlan\\LastDayStartedForConsultant' => NotificationType.chosenPlanConsultantLastDayStarted, + 'App\\Notifications\\ComplaintToModerationNotify' => NotificationType.complaintToModerationForUser, + 'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied, + 'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded, + 'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented, + _ => NotificationType.unknown, + }; } + +DateTime parseDate(String date) => DateTime.parse(date).toLocal(); diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart index 61eba9a7..d4e6e17d 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart @@ -13,10 +13,7 @@ class NotificationsCubit extends Cubit { final WsManager wsManager; final NotificationsRepository notificationsRepository; StreamSubscription? _newNotificationsSubscription; - NotificationsCubit({ - required this.wsManager, - required this.notificationsRepository, - }) : super(const NotificationsState.empty()); + NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty()); Future connectToWebSocket() async { await _newNotificationsSubscription?.cancel(); @@ -27,22 +24,95 @@ class NotificationsCubit extends Cubit { BaseNotificationModel _parseNotification(Map notificationJson, NotificationType type) { try { + final createdAt = notificationJson['created_at']; + final isRead = notificationJson['read_at'] != null; + return switch (type) { - NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type), - NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type), - NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type), - NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanTomorrowReminderToConsultantNotification => - ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type), - NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type), - NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(notificationJson, type), + NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( + notificationJson, + type, + isRead: isRead, + createdAt: createdAt, + ), + // New notification types + NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), + NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.unknown => UnknownNotification(type: type), }; } catch (e) { @@ -56,32 +126,80 @@ class NotificationsCubit extends Cubit { final type = NotificationType.getTypeFromString(stringifiedType); final notification = _parseNotification(json, type); _notifications.insert(0, notification); - emit( - NotificationsState.loaded( - notifications: _notifications, - notificationsCount: _notifications.length, - hasUnreadNotifications: true, - ), - ); + + final currentState = state; + if (currentState is NotificationsStateLoaded) { + emit(currentState.copyWith(notifications: _notifications, notificationsCount: _notifications.length, hasUnreadNotifications: true)); + } } Future getNotifications() async { try { - final notifications = await notificationsRepository.getNotifications(page: 1); - _notifications.clear(); - _notifications.addAll(notifications ?? []); - emit( - NotificationsState.loaded( - notifications: _notifications, - notificationsCount: _notifications.length, - hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty, - ), - ); + final result = await notificationsRepository.getNotifications(page: 1); + if (result != null) { + final notifications = result['notifications'] as List; + final pagination = result['pagination'] as Map; + + _notifications.clear(); + _notifications.addAll(notifications); + + final currentPage = pagination['current_page'] as int; + final lastPage = pagination['last_page'] as int; + final hasNextPage = currentPage < lastPage; + + emit( + NotificationsState.loaded( + notifications: _notifications, + notificationsCount: _notifications.length, + hasUnreadNotifications: notifications.where((elem) => elem.isRead == false).isNotEmpty, + currentPage: currentPage, + hasNextPage: hasNextPage, + ), + ); + } } catch (e, st) { log(e.toString(), stackTrace: st); } } + Future loadMoreNotifications() async { + final currentState = state; + if (currentState is NotificationsStateLoaded && currentState.hasNextPage && !currentState.isLoadingMore) { + try { + emit(currentState.copyWith(isLoadingMore: true)); + + final nextPage = currentState.currentPage + 1; + final result = await notificationsRepository.getNotifications(page: nextPage); + + if (result != null) { + final newNotifications = result['notifications'] as List; + final pagination = result['pagination'] as Map; + + _notifications.addAll(newNotifications); + + final currentPage = pagination['current_page'] as int; + final lastPage = pagination['last_page'] as int; + final hasNextPage = currentPage < lastPage; + + emit( + currentState.copyWith( + notifications: _notifications, + notificationsCount: _notifications.length, + currentPage: currentPage, + hasNextPage: hasNextPage, + isLoadingMore: false, + ), + ); + } else { + emit(currentState.copyWith(isLoadingMore: false)); + } + } catch (e, st) { + log(e.toString(), stackTrace: st); + emit(currentState.copyWith(isLoadingMore: false)); + } + } + } + Future markNotificationsAsRead() async { final currentState = state; if (currentState is NotificationsStateLoaded) { diff --git a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart index 9c221b31..391e199e 100644 --- a/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart +++ b/lib/features/notifications/presentation/notifications_cubit/notifications_state.dart @@ -14,6 +14,9 @@ abstract class NotificationsState with _$NotificationsState { required int notificationsCount, @Default(false) bool isLoading, @Default(false) bool hasUnreadNotifications, + @Default(1) int currentPage, + @Default(true) bool hasNextPage, + @Default(false) bool isLoadingMore, }) = NotificationsStateLoaded; const factory NotificationsState.error({ diff --git a/lib/features/notifications/presentation/notifications_screen.dart b/lib/features/notifications/presentation/notifications_screen.dart index ff48617f..58e3c65f 100644 --- a/lib/features/notifications/presentation/notifications_screen.dart +++ b/lib/features/notifications/presentation/notifications_screen.dart @@ -10,9 +10,38 @@ import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class NotificationsScreen extends StatelessWidget { +class NotificationsScreen extends StatefulWidget { const NotificationsScreen({super.key}); + @override + State createState() => _NotificationsScreenState(); +} + +class _NotificationsScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { + final cubit = context.read(); + final state = cubit.state; + if (state is NotificationsStateLoaded && state.hasNextPage && !state.isLoadingMore) { + cubit.loadMoreNotifications(); + } + } + } + @override Widget build(BuildContext context) { final s = S.of(context); @@ -20,62 +49,65 @@ class NotificationsScreen extends StatelessWidget { builder: (BuildContext context, NotificationsState state) { return Scaffold( backgroundColor: appTheme.gray100, - appBar: AppBar( - title: const Text('Уведомления'), - ), + appBar: AppBar(title: const Text('Уведомления')), body: state is NotificationsStateLoaded - ? Stack(children: [ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Container( - padding: const EdgeInsets.only(top: 16), - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), + ? Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Container( + padding: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), + ), + child: state.isLoading + ? const Center(child: Loader()) + : CustomScrollView( + controller: _scrollController, + slivers: [ + SliverList.separated( + separatorBuilder: (context, index) => + const Padding(padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: CustomDivider()), + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: NotificationWidget(notification: state.notifications[index]), + ); + }, + itemCount: state.notifications.length, + ), + if (state.isLoadingMore) + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Center(child: Loader()), + ), + ), + ], + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: CustomOutlinedButton( + text: s.markAsRead, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: const WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + onPressed: () async { + context.read().markNotificationsAsRead(); + }, ), ), - child: state.isLoading - ? const Center(child: Loader()) - : CustomScrollView( - slivers: [ - SliverList.separated( - separatorBuilder: (context, index) => const Padding( - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: CustomDivider(), - ), - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: NotificationWidget( - notification: state.notifications[index], - ), - ); - }, - itemCount: state.notifications.length, - ) - ], - ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - child: CustomOutlinedButton( - text: s.markAsRead, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: - ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), - onPressed: () async { - context.read().markNotificationsAsRead(); - }, - ), - ), - ) - ]) + ], + ) : const Placeholder(), ); }, diff --git a/lib/features/notifications/presentation/widgets/notification_widget.dart b/lib/features/notifications/presentation/widgets/notification_widget.dart index 66b6cf18..18c02a13 100644 --- a/lib/features/notifications/presentation/widgets/notification_widget.dart +++ b/lib/features/notifications/presentation/widgets/notification_widget.dart @@ -15,46 +15,39 @@ class NotificationWidget extends StatelessWidget { Widget build(BuildContext context) { return switch (notification) { PlanSubscriptionStartedToAuthorNotification() => _PlanSubscriptionStartedToAuthorWidget( - notification: notification as PlanSubscriptionStartedToAuthorNotification, - ), + notification: notification as PlanSubscriptionStartedToAuthorNotification, + ), PlanSubscriptionCanceledToAuthorNotification() => _PlanSubscriptionCanceledToAuthorWidget( - notification: notification as PlanSubscriptionCanceledToAuthorNotification, - ), - ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget( - notification: notification as ChosenPlanStartedToClientNotification, - ), + notification: notification as PlanSubscriptionCanceledToAuthorNotification, + ), + ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget(notification: notification as ChosenPlanStartedToClientNotification), ChosenPlanTomorrowReminderToClientNotification() => _ChosenPlanTomorrowReminderToClientWidget( - notification: notification as ChosenPlanTomorrowReminderToClientNotification, - ), - DiaryReminderNotification() => _DiaryReminderWidget( - notification: notification as DiaryReminderNotification, - ), - ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget( - notification: notification as ConsultantLeftCommentNotification, - ), - PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget( - notification: notification as PersonalizedDietIsDefinedNotification, - ), - ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget( - notification: notification as ChosenPlanFinishedToClientNotification, - ), + notification: notification as ChosenPlanTomorrowReminderToClientNotification, + ), + DiaryReminderNotification() => _DiaryReminderWidget(notification: notification as DiaryReminderNotification), + ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(notification: notification as ConsultantLeftCommentNotification), + PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget(notification: notification as PersonalizedDietIsDefinedNotification), + ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget(notification: notification as ChosenPlanFinishedToClientNotification), PlanSubscriptionCanceledToCustomerNotification() => _PlanSubscriptionCanceledToCustomerWidget( - notification: notification as PlanSubscriptionCanceledToCustomerNotification, - ), + notification: notification as PlanSubscriptionCanceledToCustomerNotification, + ), ChosenPlanStartedToConsultantNotification() => _ChosenPlanStartedToConsultantWidget( - notification: notification as ChosenPlanStartedToConsultantNotification, - ), + notification: notification as ChosenPlanStartedToConsultantNotification, + ), ChosenPlanTomorrowReminderToConsultantNotification() => _ChosenPlanTomorrowReminderToConsultantWidget( - notification: notification as ChosenPlanTomorrowReminderToConsultantNotification, - ), - PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget( - notification: notification as PlanSubscriptionPaidToAuthorNotification, - ), + notification: notification as ChosenPlanTomorrowReminderToConsultantNotification, + ), + PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(notification: notification as PlanSubscriptionPaidToAuthorNotification), ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget( - notification: notification as ChosenPlanFinishedToConsultantNotification, - ), - ChosenPlanLastDayFinishedForUser() => Text((notification as ChosenPlanLastDayFinishedForUser).type.toString()), - UnknownNotification() => const SizedBox.shrink(), + notification: notification as ChosenPlanFinishedToConsultantNotification, + ), + ChosenPlanLastDayFinishedForUser() => _ChosenPlanLastDayFinishedForUserWidget(notification: notification as ChosenPlanLastDayFinishedForUser), + ChosenPlanConsultantLastDayStarted() => _ChosenPlanConsultantLastDayStartedWidget(notification: notification as ChosenPlanConsultantLastDayStarted), + ComplaintToModerationForUser() => _ComplaintToModerationForUserWidget(notification: notification as ComplaintToModerationForUser), + CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied), + ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded), + EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented), + UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification), }; } } @@ -69,6 +62,7 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -83,6 +77,7 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -94,10 +89,7 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget { Widget build(BuildContext context) { final s = S.of(context); - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: s.planStarted(notification.planName), - ); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.planStarted(notification.planName), createdAt: notification.createdAt); } } @@ -111,6 +103,7 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.tomorrowYourPlanWillStart(notification.planName), + createdAt: notification.createdAt, ); } } @@ -121,10 +114,7 @@ class _DiaryReminderWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return _BaseNotificationWidget( - isRead: notification.isRead, - notificationsText: s.fillYasterday, - ); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.fillYasterday, createdAt: notification.createdAt); } } @@ -138,6 +128,7 @@ class _ConsultantLeftCommentWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')), + createdAt: notification.createdAt, ); } } @@ -147,7 +138,7 @@ class _PersonalizedDietIsDefinedWidget extends StatelessWidget { final PersonalizedDietIsDefinedNotification notification; @override Widget build(BuildContext context) { - return Container(); + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Персональная диета определена', createdAt: notification.createdAt); } } @@ -161,6 +152,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', + createdAt: notification.createdAt, ); } } @@ -175,6 +167,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.youCanceledSubscriptionToPlan(notification.planName), + createdAt: notification.createdAt, ); } } @@ -189,6 +182,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.planWithUserStarted(notification.planName, notification.customerName), + createdAt: notification.createdAt, ); } } @@ -203,6 +197,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName), + createdAt: notification.createdAt, ); } } @@ -217,6 +212,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget { return _BaseNotificationWidget( isRead: notification.isRead, notificationsText: s.userPaidThePlan(notification.customerName, notification.planName), + createdAt: notification.createdAt, ); } } @@ -227,58 +223,223 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - return Column(children: [ - Text( - '${s.congratsYouHaveFinishedPlan} «${notification.planName}»', - style: TextStyles.textMSemibold.copyWith(color: appTheme.black), - ), - const SizedBox(height: 12), - Text( - s.userConfirmedPlanCompletionWithoutSum, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), - const SizedBox(height: 12), - CustomOutlinedButton( - text: s.collectCharge, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), - onPressed: () { - GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId); - }, - ), - const SizedBox(height: 12), - ]); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${s.congratsYouHaveFinishedPlan} «${notification.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + Text(s.userConfirmedPlanCompletionWithoutSum, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), + const SizedBox(height: 12), + CustomOutlinedButton( + text: s.collectCharge, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)), + onPressed: () { + GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId); + }, + ), + ], + ), + ), + if (!notification.isRead) + Container( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + width: 8, + height: 8, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${notification.createdAt.day} ${_getMonthName(notification.createdAt.month)} ${notification.createdAt.hour.toString().padLeft(2, '0')}:${notification.createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), + ), + ], + ); + } + + String _getMonthName(int month) { + switch (month) { + case 1: + return 'янв'; + case 2: + return 'фев'; + case 3: + return 'мар'; + case 4: + return 'апр'; + case 5: + return 'май'; + case 6: + return 'июн'; + case 7: + return 'июл'; + case 8: + return 'авг'; + case 9: + return 'сен'; + case 10: + return 'окт'; + case 11: + return 'ноя'; + case 12: + return 'дек'; + default: + return ''; + } } } // CardSettingsScreen class _BaseNotificationWidget extends StatelessWidget { - const _BaseNotificationWidget({required this.notificationsText, required this.isRead}); + const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt}); final String notificationsText; final bool isRead; + final DateTime createdAt; @override Widget build(BuildContext context) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: MediaQuery.sizeOf(context).width * 0.6, - child: Text( - notificationsText, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - ), - if (!isRead) ...[ - const Spacer(), - Container( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, + Row( + children: [ + Expanded( + child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)), ), - width: 8, - height: 8, - ) - ] + if (!isRead) + Container( + decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), + width: 8, + height: 8, + ), + ], + ), + const SizedBox(height: 4), + Text( + '${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}', + style: TextStyles.textSMedium.copyWith(color: appTheme.gray600), + ), ], ); } + + String _getMonthName(int month) { + switch (month) { + case 1: + return 'янв'; + case 2: + return 'фев'; + case 3: + return 'мар'; + case 4: + return 'апр'; + case 5: + return 'май'; + case 6: + return 'июн'; + case 7: + return 'июл'; + case 8: + return 'авг'; + case 9: + return 'сен'; + case 10: + return 'окт'; + case 11: + return 'ноя'; + case 12: + return 'дек'; + default: + return ''; + } + } +} + +class _ChosenPlanLastDayFinishedForUserWidget extends StatelessWidget { + const _ChosenPlanLastDayFinishedForUserWidget({required this.notification}); + final ChosenPlanLastDayFinishedForUser notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Последний день плана завершен', createdAt: notification.createdAt); + } +} + +class _ChosenPlanConsultantLastDayStartedWidget extends StatelessWidget { + const _ChosenPlanConsultantLastDayStartedWidget({required this.notification}); + final ChosenPlanConsultantLastDayStarted notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Последний день плана начался: ${notification.planName}', + createdAt: notification.createdAt, + ); + } +} + +class _ComplaintToModerationForUserWidget extends StatelessWidget { + const _ComplaintToModerationForUserWidget({required this.notification}); + final ComplaintToModerationForUser notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Жалоба отправлена на модерацию', createdAt: notification.createdAt); + } +} + +class _CommentRepliedWidget extends StatelessWidget { + const _CommentRepliedWidget({required this.notification}); + final CommentReplied notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Ответ на комментарий: ${notification.entityName}', + createdAt: notification.createdAt, + ); + } +} + +class _ScoreAddedWidget extends StatelessWidget { + const _ScoreAddedWidget({required this.notification}); + final ScoreAdded notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Добавлены баллы: ${notification.scoreValue}', + createdAt: notification.createdAt, + ); + } +} + +class _EntityCommentedWidget extends StatelessWidget { + const _EntityCommentedWidget({required this.notification}); + final EntityCommented notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Комментарий к ${notification.entityTranslatedType}: ${notification.entityName}', + createdAt: notification.createdAt, + ); + } +} + +class _UnknownNotificationWidget extends StatelessWidget { + const _UnknownNotificationWidget({required this.notification}); + final UnknownNotification notification; + @override + Widget build(BuildContext context) { + return _BaseNotificationWidget( + isRead: notification.isRead, + notificationsText: 'Неизвестное уведомление: ${notification.type.toString()}', + createdAt: notification.createdAt, + ); + } } diff --git a/lib/features/notifications/repository/notifications_repository.dart b/lib/features/notifications/repository/notifications_repository.dart index beef645f..267faa89 100644 --- a/lib/features/notifications/repository/notifications_repository.dart +++ b/lib/features/notifications/repository/notifications_repository.dart @@ -1,14 +1,15 @@ -import 'package:eatvio_app/features/notifications/models/notification_model.dart'; +import 'dart:developer'; import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; class NotificationsRepository { final NotificationsService notificationsService; NotificationsRepository({required this.notificationsService}); - Future?> getNotifications({required int page}) { + Future?> getNotifications({required int page}) { try { return notificationsService.getNotifications(page: page); } catch (e) { + log(e.toString()); rethrow; } } diff --git a/lib/features/notifications/service/notifications_service.dart b/lib/features/notifications/service/notifications_service.dart index ec2aad74..61f545ab 100644 --- a/lib/features/notifications/service/notifications_service.dart +++ b/lib/features/notifications/service/notifications_service.dart @@ -5,53 +5,137 @@ class NotificationsService { final Dio dio; NotificationsService(this.dio); - Future?> getNotifications({required int page}) async { + Future?> getNotifications({required int page}) async { try { final response = await dio.get('/api/profile/notifications?page=$page'); if (response.statusCode == 200) { final data = response.data['data'] as List; + final notifications = data.map((notificationJson) { try { final stringifiedType = notificationJson['data']['notification_class_name']; final readDate = notificationJson['read_at']; + final createdAt = notificationJson['created_at']; final type = NotificationType.getTypeFromString(stringifiedType); + return switch (type) { - NotificationType.planSubscriptionStartedToAuthor => - PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionCanceledToAuthor => - PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanStartedToClientNotification => - ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanTomorrowReminderToClientNotification => - ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.personalizedDietIsDefined => - PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanFinishedToClient => - ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionCanceledToCustomer => - PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanStartedToConsultantNotification => - ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanTomorrowReminderToConsultantNotification => - ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.planSubscriptionPaidToAuthor => - PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), - NotificationType.chosenPlanFinishedToConsultant => - ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null), + NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( - notificationJson['data'], - type, - isRead: readDate != null, - ), - NotificationType.unknown => UnknownNotification(type: type), + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + // Новые типы уведомлений + NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson( + notificationJson['data'], + type, + isRead: readDate != null, + createdAt: createdAt, + ), + NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), + NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)), }; } catch (e) { - return UnknownNotification(type: NotificationType.unknown); + return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now()); } }).toList(); - return notifications.where((notification) => notification.type != NotificationType.unknown).toList(); + + // Извлекаем информацию о пагинации из корня ответа + final currentPage = response.data['current_page'] as int; + final lastPage = response.data['last_page'] as int; + final perPage = response.data['per_page'] as int; + final total = response.data['total'] as int; + + return { + 'notifications': notifications, + 'pagination': {'current_page': currentPage, 'last_page': lastPage, 'per_page': perPage, 'total': total}, + }; } return null; } catch (e) { @@ -67,3 +151,5 @@ class NotificationsService { } } } + +DateTime parseDate(String date) => DateTime.parse(date).toLocal(); diff --git a/lib/features/registration/repositories/registration_repository.dart b/lib/features/registration/repositories/registration_repository.dart index a34bf1b0..548684b9 100644 --- a/lib/features/registration/repositories/registration_repository.dart +++ b/lib/features/registration/repositories/registration_repository.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/models/location_model.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; +import 'package:eatvio_app/core/env_config.dart'; class RegistrationRepository { final RegistrationService registrationService; @@ -83,7 +84,7 @@ class RegistrationRepository { Future emailIsReachable(String email) async { try { - final response = await sl().getUri(Uri.parse('https://api.validate.email/validate?email=$email')); + final response = await sl().getUri(Uri.parse('${EnvConfig.emailValidationApiUrl}?email=$email')); if (response.data['result']['reachable'] != 'invalid') { return true; } else { diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index b550040f..c6f7719a 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:eatvio_app/core/env_config.dart'; class SupportScreen extends StatelessWidget { const SupportScreen({super.key}); @@ -15,48 +16,28 @@ class SupportScreen extends StatelessWidget { final s = S.of(context); return Scaffold( - appBar: AppBar( - title: const Text('Поддержка'), - ), + appBar: AppBar(title: const Text('Поддержка')), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.all(16), - child: Text( - 'Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:', style: TextStyle(fontSize: 16)), ), InkWell( onTap: () { - launchUrl( - Uri.parse('https://t.me/eatvio'), - mode: LaunchMode.externalApplication, - ); + launchUrl(Uri.parse(EnvConfig.telegramSupportUrl), mode: LaunchMode.externalApplication); }, borderRadius: BorderRadius.circular(16), child: Ink( width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.all(16), child: Row( children: [ - SvgPicture.asset( - 'assets/icons/telegram.svg', - colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), - ), + SvgPicture.asset('assets/icons/telegram.svg', colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn)), const SizedBox(width: 12), - const Text( - 'Telegram: t.me/eatvio', - style: TextStyle( - fontSize: 16, - ), - ), + Text('Telegram: ${EnvConfig.telegramSupportUrl.replaceFirst('https://', '')}', style: const TextStyle(fontSize: 16)), ], ), ), @@ -64,26 +45,19 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 8), InkWell( onTap: () { - Clipboard.setData(const ClipboardData(text: 'help@eatv.io')); + Clipboard.setData(ClipboardData(text: EnvConfig.supportEmail)); showInfoToast(s.mail_was_copied); }, borderRadius: BorderRadius.circular(16), child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)), width: double.infinity, padding: const EdgeInsets.all(16), - child: const Row( + child: Row( children: [ - Icon(Icons.alternate_email_rounded), - SizedBox(width: 12), - Text( - 'E-mail: help@eatv.io', - style: TextStyle( - fontSize: 16, - ), - ), + const Icon(Icons.alternate_email_rounded), + const SizedBox(width: 12), + Text('E-mail: ${EnvConfig.supportEmail}', style: const TextStyle(fontSize: 16)), ], ), ), @@ -92,13 +66,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Правовые документы', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - ), - ), + child: Text('Правовые документы', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), ), const SizedBox(height: 12), InkWell( @@ -106,12 +74,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'Публичная офёрта', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Публичная офёрта', style: TextStyle(fontSize: 16)), ), ), const SizedBox(height: 8), @@ -120,12 +83,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'Политика конфиденциальности', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)), ), ), const SizedBox(height: 8), @@ -134,12 +92,7 @@ class SupportScreen extends StatelessWidget { borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - 'О компании', - style: TextStyle( - fontSize: 16, - ), - ), + child: Text('О компании', style: TextStyle(fontSize: 16)), ), ), ], diff --git a/lib/features/update_screen/presentation/update_screen.dart b/lib/features/update_screen/presentation/update_screen.dart index 186987c2..68ec9640 100644 --- a/lib/features/update_screen/presentation/update_screen.dart +++ b/lib/features/update_screen/presentation/update_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../core/theme/theme_helper.dart'; +import '../../../../core/env_config.dart'; class UpdateScreen extends StatelessWidget { const UpdateScreen({super.key}); @@ -17,16 +18,7 @@ class UpdateScreen extends StatelessWidget { body: Stack( clipBehavior: Clip.none, children: [ - Positioned( - left: -20, - top: -20, - right: -20, - bottom: -20, - child: Image.asset( - 'assets/images/bg_welcome.png', - fit: BoxFit.cover, - ), - ), + Positioned(left: -20, top: -20, right: -20, bottom: -20, child: Image.asset('assets/images/bg_welcome.png', fit: BoxFit.cover)), Positioned.fill( child: SafeArea( child: Padding( @@ -48,47 +40,26 @@ class UpdateScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), - const Text( - 'Пора обновиться! ⚡', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - height: 1, - ), - ), + const Text('Пора обновиться! ⚡', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, height: 1)), const SizedBox(height: 20), const Text( 'Мы выпустили важное обновление\nс новыми функциями и исправлениями.\nЧтобы продолжить пользоваться\nприложением без сбоев, обновите\nего прямо сейчас.', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.15, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.15), ), const SizedBox(height: 32), InkWell( onTap: () async { - final Uri url = Platform.isAndroid - ? Uri.parse('https://www.rustore.ru/catalog/app/com.eatvioapp.app') - : Uri.parse('https://apps.apple.com/app/idYOUR_APP_ID'); + final Uri url = Platform.isAndroid ? Uri.parse(EnvConfig.rustoreUrl) : Uri.parse(EnvConfig.appStoreUrl); await launchUrl(url); }, borderRadius: BorderRadius.circular(8), child: Ink( height: 52, - decoration: BoxDecoration( - color: appTheme.blueA400, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(8)), child: const Center( child: Text( 'Обновить', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - height: 1, - color: Colors.white, - ), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1, color: Colors.white), ), ), ), diff --git a/lib/features/user_details/presentation/cubit/user_details_cubit.dart b/lib/features/user_details/presentation/cubit/user_details_cubit.dart index c6eb36b7..0ce0bd93 100644 --- a/lib/features/user_details/presentation/cubit/user_details_cubit.dart +++ b/lib/features/user_details/presentation/cubit/user_details_cubit.dart @@ -20,19 +20,11 @@ class UserDetailsCubit extends Cubit { } } - void getChartData({ - String period = '1', - required String slug, - }) async { + void getChartData({String period = '1', required String slug}) async { try { - final weightPoints = await profileRepository.getWeight(period: period); - final circumferencePoints = await profileRepository.getCircumferences(period: period); - emit( - state.copyWith( - weightPoints: weightPoints, - circumferencePoints: circumferencePoints, - ), - ); + final weightPoints = await profileRepository.getWeight(period: period, slug: slug); + final circumferencePoints = await profileRepository.getCircumferences(period: period, slug: slug); + emit(state.copyWith(weightPoints: weightPoints, circumferencePoints: circumferencePoints)); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); emit(state.copyWith(error: e.toString(), isLoading: false)); diff --git a/lib/features/user_details/presentation/user_details_screen.dart b/lib/features/user_details/presentation/user_details_screen.dart index f79b990e..4907e3dd 100644 --- a/lib/features/user_details/presentation/user_details_screen.dart +++ b/lib/features/user_details/presentation/user_details_screen.dart @@ -31,9 +31,7 @@ class UserDetailsScreen extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => sl()..getUserDetails(slug), - child: UserDetailsContent( - slug: slug, - ), + child: UserDetailsContent(slug: slug), ); } } @@ -79,27 +77,15 @@ class _UserDetailsContentState extends State { actions: [ PortalTarget( visible: isMoreOptionsVisible, - portalFollower: MoreOptions( - path: 'profile/${widget.slug}', - complainedContentType: ComplainedContent.profile, - slug: widget.slug, - ), - anchor: const Aligned( - shiftToWithinBound: AxisFlag(x: true, y: true), - follower: Alignment.topRight, - target: Alignment.bottomRight, - ), + portalFollower: MoreOptions(path: 'profile/${widget.slug}', complainedContentType: ComplainedContent.profile, slug: widget.slug), + anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomRight), child: IconButton( onPressed: () { setState(() { isMoreOptionsVisible = !isMoreOptionsVisible; }); }, - icon: SvgPicture.asset( - 'assets/icons/more_button.svg', - width: 20, - height: 20, - ), + icon: SvgPicture.asset('assets/icons/more_button.svg', width: 20, height: 20), ), ), ], @@ -124,25 +110,16 @@ class _UserDetailsContentState extends State { initiallyExpanded: true, title: s.general_info, children: [ - Text( - s.date_of_birth, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.date_of_birth, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), CustomTextFormField( readOnly: true, controller: TextEditingController(text: DateTime.parse(profileDetails.birthdate).format('dd.MM.yyyy')), ), const SizedBox(height: 16), - Text( - s.sex, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.sex, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.gender.parseGender()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.gender.parseGender())), const SizedBox(height: 16), ], ), @@ -155,15 +132,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.height, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.height, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.height.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.height.toString())), ], ), ), @@ -172,15 +143,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.weight, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.weight, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0)), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0))), ], ), ), @@ -193,15 +158,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.chest_circumference, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.chest_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.chestCircumference.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.chestCircumference.toString())), ], ), ), @@ -210,15 +169,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.waist_circumference, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.waist_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.waistCircumference.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.waistCircumference.toString())), ], ), ), @@ -227,15 +180,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.hip_circumference, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.hip_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.hipCircumference.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hipCircumference.toString())), ], ), ), @@ -253,15 +200,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.upper, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.upper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.bloodPressureUp.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.bloodPressureUp.toString())), ], ), ), @@ -270,10 +211,7 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.lower, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.lower, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), CustomTextFormField( readOnly: true, @@ -291,15 +229,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.has_hypo, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.has_hypo, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.hasHypo.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo.toString())), ], ), ), @@ -308,15 +240,9 @@ class _UserDetailsContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - s.has_hyper, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.has_hyper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.hasHyper.toString()), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper.toString())), ], ), ), @@ -328,60 +254,27 @@ class _UserDetailsContentState extends State { ExpandableBouble( title: s.other_info, children: [ - Text( - s.has_alergy, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.has_alergy, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.aboutAlergy), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutAlergy)), const SizedBox(height: 16), - Text( - s.takes_pills, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.takes_pills, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.aboutTakingPils), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutTakingPils)), const SizedBox(height: 16), - Text( - s.sport_hobbies, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.sport_hobbies, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.aboutSportHobbies), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutSportHobbies)), const SizedBox(height: 16), - Text( - s.liked_groceries, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.liked_groceries, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.favoriteFoods), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.favoriteFoods)), const SizedBox(height: 16), - Text( - s.not_liked_groceries, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.not_liked_groceries, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), - CustomTextFormField( - readOnly: true, - controller: TextEditingController(text: profileDetails.unfavoriteFoods), - ), + CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.unfavoriteFoods)), const SizedBox(height: 16), - Text( - s.additional_information, - style: TextStyles.textSMedium.copyWith(color: appTheme.black), - ), + Text(s.additional_information, style: TextStyles.textSMedium.copyWith(color: appTheme.black)), const SizedBox(height: 4), SizedBox( height: 112, @@ -413,10 +306,7 @@ class _UserDetailsContentState extends State { ), ); } - return Scaffold( - appBar: AppBar(), - body: const Loader(), - ); + return Scaffold(appBar: AppBar(), body: const Loader()); }, ); } @@ -432,31 +322,17 @@ class _UserDetailsContentState extends State { SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, - child: Row( - children: [ - _choiceChipWidget(0), - const SizedBox(width: 8), - _choiceChipWidget(1), - const SizedBox(width: 8), - _choiceChipWidget(2), - ], - ), + child: Row(children: [_choiceChipWidget(0), const SizedBox(width: 8), _choiceChipWidget(1), const SizedBox(width: 8), _choiceChipWidget(2)]), ), Builder( builder: (context) { switch (_selectedChipIndex) { case 0: - return WeightChartWidget( - weightPoints: weightPoints, - ); + return WeightChartWidget(weightPoints: weightPoints); case 1: - return CircumferenceChartWidget( - circumferencePoints: circumferencePoints, - ); + return CircumferenceChartWidget(circumferencePoints: circumferencePoints); case 2: - return ImtChartWidget( - weightPoints: weightPoints, - ); + return ImtChartWidget(weightPoints: weightPoints); default: return const SizedBox.shrink(); } @@ -489,16 +365,10 @@ class _UserDetailsContentState extends State { pressElevation: 0, showCheckmark: false, side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), label: Text(chipsIntervalLabelList[index]), - labelStyle: TextStyle( - color: appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedIntervalChipIndex == index, onSelected: (bool selected) { context.read().getChartData(period: chipsIntervalList[index], slug: widget.slug); @@ -515,16 +385,10 @@ class _UserDetailsContentState extends State { pressElevation: 0, showCheckmark: false, side: BorderSide.none, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), label: Text(chipsLabelList[index]), - labelStyle: TextStyle( - color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + labelStyle: TextStyle(color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12), selected: _selectedChipIndex == index, backgroundColor: appTheme.gray100, onSelected: (bool selected) { diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index dcf61a5d..6450332e 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -1,8 +1,8 @@ // 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; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:eatvio_app/core/env_config.dart'; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -43,28 +43,26 @@ class DefaultFirebaseOptions { 'you can reconfigure this by running the FlutterFire CLI again.', ); default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); + 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 FirebaseOptions get android => FirebaseOptions( + apiKey: EnvConfig.firebaseAndroidApiKey, + appId: EnvConfig.firebaseAndroidAppId, + messagingSenderId: EnvConfig.firebaseMessagingSenderId, + projectId: EnvConfig.firebaseProjectId, + databaseURL: EnvConfig.firebaseDatabaseUrl, + storageBucket: EnvConfig.firebaseStorageBucket, ); - 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', + static FirebaseOptions get ios => FirebaseOptions( + apiKey: EnvConfig.firebaseIosApiKey, + appId: EnvConfig.firebaseIosAppId, + messagingSenderId: EnvConfig.firebaseMessagingSenderId, + projectId: EnvConfig.firebaseProjectId, + databaseURL: EnvConfig.firebaseDatabaseUrl, + storageBucket: EnvConfig.firebaseStorageBucket, + iosBundleId: EnvConfig.firebaseIosBundleId, ); } diff --git a/lib/main.dart b/lib/main.dart index 3fd39e7f..6b44348c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:io' as io; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; @@ -12,6 +13,7 @@ import 'package:eatvio_app/firebase_options.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -19,22 +21,28 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/env_config.dart'; Future main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + + await dotenv.load(fileName: "env/.env"); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await FirebaseMessaging.instance.requestPermission(); await di.init(); - await SentryFlutter.init( - (options) { - options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2'; - options.tracesSampleRate = 1.0; - options.debug = false; - }, - appRunner: () => runApp(const MyApp()), - ); + if (kDebugMode) { + io.HttpClient.enableTimelineLogging = true; + } + + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.tracesSampleRate = 1.0; + options.debug = false; + }, appRunner: () => runApp(const MyApp())); } class MyApp extends StatelessWidget { diff --git a/pubspec.yaml b/pubspec.yaml index 0dd1a2da..fefe422b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.6+64 +version: 1.2.7+66 publish_to: 'none' environment: @@ -72,6 +72,7 @@ dependencies: device_info_plus: ^11.5.0 permission_handler: ^12.0.1 qr_code_scanner_plus: ^2.0.10+1 + flutter_dotenv: ^5.1.0 dev_dependencies: build_runner: ^2.6.0 @@ -88,6 +89,7 @@ dev_dependencies: flutter: uses-material-design: true assets: + - env/.env - assets/ - assets/images/ - assets/audio/