diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index f1f63162..60aebe4f 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -140,7 +140,21 @@ jobs:
run: flutter analyze
- name: Test
- run: flutter test
+ run: flutter test --coverage
+
+ - name: Test coverage
+ run: |
+ awk -F: '
+ /^LF:/ { total += $2 }
+ /^LH:/ { covered += $2 }
+ END {
+ if (total == 0) {
+ print "Coverage: 0%"
+ } else {
+ printf "Coverage: %.2f%%\n", (covered / total) * 100
+ }
+ }
+ ' coverage/lcov.info
- name: Build apk
run: |
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 8fc22d6d..79e6d71d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -19,15 +19,15 @@ def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property(
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
- ndkVersion = "27.0.12077973"
+ ndkVersion = flutter.ndkVersion
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11.toString()
+ jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index d57d24d8..04cd623d 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -406,7 +406,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- MARKETING_VERSION = 1.3.3;
+ MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -557,7 +557,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- MARKETING_VERSION = 1.3.3;
+ MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -596,7 +596,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- MARKETING_VERSION = 1.3.3;
+ MARKETING_VERSION = 1.3.4;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 15c18c06..e90493e3 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 82
+ 83
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart
index 2a63de39..0220c9fb 100644
--- a/lib/features/auth/presentation/pages/auth_screen.dart
+++ b/lib/features/auth/presentation/pages/auth_screen.dart
@@ -411,7 +411,7 @@ class _AuthScreenState extends State {
_isLoading = false;
});
},
- label: state is AuthStateLoading
+ label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
@@ -426,8 +426,8 @@ class _AuthScreenState extends State {
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
- backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
- onPressed: state is AuthStateLoading
+ backgroundColor: state is AuthStateLoading || _isLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
+ onPressed: state is AuthStateLoading || _isLoading
? null
: () async {
await showDialog(
@@ -487,7 +487,7 @@ class _AuthScreenState extends State {
},
);
},
- label: state is AuthStateLoading
+ label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),
diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
index 48536468..98cb0053 100644
--- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
+++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
+import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
@@ -620,16 +621,16 @@ class _BlogPostCreateScreenState extends State {
}
},
borderRadius: BorderRadius.circular(16),
- child: Ink(
- width: MediaQuery.of(context).size.width - 32,
- height: 52,
- decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
- child: isLoading
- ? const Center(child: CircularProgressIndicator())
- : const Center(
+ child: isLoading
+ ? const Center(child: Loader())
+ : Ink(
+ width: MediaQuery.of(context).size.width - 32,
+ height: 52,
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
+ child: const Center(
child: Text('Создать', style: TextStyle(color: Colors.white)),
),
- ),
+ ),
);
}
diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
index f9fa4143..b1529280 100644
--- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
+++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
@@ -63,25 +63,28 @@ class _ConsultationCardWidgetState extends State {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- CircleAvatar(radius: 30, backgroundImage: NetworkImage(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),
- ),
- const SizedBox(height: 4),
- Text(
- widget.consultationsModel.planTitle,
- style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
- ),
- ],
- ),
- ],
+ GestureDetector(
+ onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: widget.consultationsModel.userSlug),
+ child: Row(
+ children: [
+ CircleAvatar(radius: 30, backgroundImage: NetworkImage(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),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ widget.consultationsModel.planTitle,
+ style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
+ ),
+ ],
+ ),
+ ],
+ ),
),
const SizedBox(height: 12),
GestureDetector(
diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart
index 94b21709..23d946af 100644
--- a/lib/features/create_diet/presentation/diet_preview_screen.dart
+++ b/lib/features/create_diet/presentation/diet_preview_screen.dart
@@ -1,13 +1,8 @@
import 'dart:convert';
-import 'dart:io';
import 'dart:typed_data';
-import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
-import 'package:eatvio_app/core/utils/string_extentions.dart';
-import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
-import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
import 'package:eatvio_app/core/widgets/cpfc_container.dart';
@@ -36,20 +31,11 @@ class DietPreviewScreen extends StatefulWidget {
class _DietPreviewScreenState extends State {
final ScrollController scrollController = ScrollController();
- final textEditingController = TextEditingController();
- bool isMoreOptionsVisible = false;
late final Uint8List imageData;
@override
void initState() {
super.initState();
imageData = base64.decode(widget.diet.image);
- scrollController.addListener(() {
- if (isMoreOptionsVisible) {
- setState(() {
- isMoreOptionsVisible = false;
- });
- }
- });
}
@override
@@ -58,86 +44,6 @@ class _DietPreviewScreenState extends State {
return Scaffold(
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
- bottomSheet: Container(
- padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
- decoration: BoxDecoration(
- color: Colors.white,
- border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
- ),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- children: [
- Expanded(
- child: BookmarkButton(
- isFavorite: widget.diet.isFavorite,
- onTap: () {},
- width: 52,
- height: 52,
- size: 24,
- shape: BoxShape.circle,
- color: appTheme.gray50,
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Expanded(
- child: SizedBox(
- child: TextFormField(
- textCapitalization: TextCapitalization.sentences,
- onChanged: (_) {
- setState(() {});
- },
- controller: textEditingController,
- maxLines: 5,
- minLines: 1,
- keyboardType: TextInputType.multiline,
- style: TextStyles.textMMedium.copyWith(color: appTheme.black),
- decoration: InputDecoration(
- isDense: true,
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- hintText: s.comment.capitalize(),
- hintStyle: TextStyles.textMMedium.copyWith(color: appTheme.lightGray600),
- filled: true,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
- ),
- enabledBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
- ),
- focusedBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
- ),
- ),
- ),
- ),
- ),
- const SizedBox(width: 8),
- GestureDetector(
- onTap: () {},
- child: Container(
- decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
- height: 32,
- width: 32,
- child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
appBar: AppBar(
centerTitle: true,
title: Text(
@@ -148,15 +54,7 @@ class _DietPreviewScreenState extends State {
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
- actions: [
- CustomPopup(
- isAuthor: widget.diet.author.authorIsMe,
- path: 'diet/${widget.diet.slug}',
- complainedContentType: ComplainedContent.diet,
- userSlug: widget.diet.author.authorSlug,
- ),
- const SizedBox(width: 16),
- ],
+ actions: const [],
),
body: SingleChildScrollView(
controller: scrollController,
diff --git a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart
index c8a6d4ef..932852ac 100644
--- a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart
+++ b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart
@@ -33,7 +33,7 @@ class _RecipeCreateScreenState extends State {
late final CreateRecipeCubit createRecipeCubit;
final GlobalKey _mdKey = GlobalKey();
bool _mdToolbarVisible = false;
-
+ bool _isRecipeBeingPublished = false;
@override
void initState() {
super.initState();
@@ -58,9 +58,7 @@ class _RecipeCreateScreenState extends State {
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
- floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null
- ? _mdKey.currentState!.buildToolbar(context)
- : null,
+ floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
appBar: AppBar(
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
centerTitle: true,
@@ -83,44 +81,55 @@ class _RecipeCreateScreenState extends State {
builder: (context, state) {
return Builder(
builder: (context) {
- return InkWell(
- key: AppKeys.recipeCreateScreenPublishButton,
- onTap: () async {
- if (context.read().validateAll()) {
- final isSuccess = await context.read().publishRecipe(
- widget.recipe == null ? false : true,
- widget.recipe?.slug,
+ return _isRecipeBeingPublished
+ ? const Center(child: Loader())
+ : InkWell(
+ key: AppKeys.recipeCreateScreenPublishButton,
+ onTap: () async {
+ setState(() {
+ _isRecipeBeingPublished = true;
+ });
+ if (context.read().validateAll()) {
+ final isSuccess = await context.read().publishRecipe(
+ widget.recipe == null ? false : true,
+ widget.recipe?.slug,
+ );
+ setState(() {
+ _isRecipeBeingPublished = false;
+ });
+ if (isSuccess) {
+ Future.delayed(const Duration(milliseconds: 500), () {
+ if (!context.mounted) return;
+ context.pop();
+ showBottomSheetWithBlur(
+ userRootNavigator: true,
+ context: context,
+ child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
+ );
+ });
+ } else {
+ if (!context.mounted) return;
+ showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
+ }
+ }
+ setState(() {
+ _isRecipeBeingPublished = false;
+ });
+ },
+ borderRadius: BorderRadius.circular(20),
+ child: Ink(
+ width: MediaQuery.of(context).size.width - 32,
+ decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ alignment: Alignment.center,
+ child: Text(
+ widget.recipe != null ? 'Обновить' : 'Опубликовать',
+ style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
+ ),
+ ),
+ ),
);
- if (isSuccess) {
- Future.delayed(const Duration(milliseconds: 500), () {
- if (!context.mounted) return;
- context.pop();
- showBottomSheetWithBlur(
- userRootNavigator: true,
- context: context,
- child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
- );
- });
- } else {
- if (!context.mounted) return;
- showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
- }
- }
- },
- borderRadius: BorderRadius.circular(20),
- child: Ink(
- width: MediaQuery.of(context).size.width - 32,
- decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
- child: Container(
- padding: const EdgeInsets.all(16),
- alignment: Alignment.center,
- child: Text(
- widget.recipe != null ? 'Обновить' : 'Опубликовать',
- style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
- ),
- ),
- ),
- );
},
);
},
diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart
index ae6f9e10..607e97d2 100644
--- a/lib/features/diary/presentation/pages/diary_screen.dart
+++ b/lib/features/diary/presentation/pages/diary_screen.dart
@@ -44,16 +44,24 @@ class UserDiaryScreen extends StatelessWidget {
}
}
-class DiaryScreenContent extends StatelessWidget {
+class DiaryScreenContent extends StatefulWidget {
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
final int? diaryId;
final int? currentPlanDay;
+
+ @override
+ State createState() => _DiaryScreenContentState();
+}
+
+class _DiaryScreenContentState extends State {
+ bool _isDiaryFinishing = false;
+
@override
Widget build(BuildContext context) {
final s = S.of(context);
return MultiBlocProvider(
providers: [
- BlocProvider(create: (BuildContext context) => sl()..fetchDiaryV2(needLoading: true, diaryId: diaryId)),
+ BlocProvider(create: (BuildContext context) => sl()..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId)),
BlocProvider(create: (context) => sl()),
],
child: BlocBuilder(
@@ -63,13 +71,13 @@ class DiaryScreenContent extends StatelessWidget {
}
if (state is DiaryStateError) {
return Scaffold(
- body: DataErrorWidget(onReload: () => context.read().fetchDiaryV2(needLoading: false, diaryId: diaryId)),
+ body: DataErrorWidget(onReload: () => context.read().fetchDiaryV2(needLoading: false, diaryId: widget.diaryId)),
);
}
return Scaffold(
backgroundColor: Colors.grey.shade200,
- appBar: diaryId == null
+ appBar: widget.diaryId == null
? AppBar(toolbarHeight: 0)
: AppBar(
title: Text(
@@ -83,7 +91,7 @@ class DiaryScreenContent extends StatelessWidget {
physics: const BouncingScrollPhysics(),
child: Column(
children: [
- if (diaryId == null) const CalendarWidget(),
+ if (widget.diaryId == null) const CalendarWidget(),
if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()],
if (state is DiaryStateLoaded)
WakeupAndRetireWidget(
@@ -116,7 +124,23 @@ class DiaryScreenContent extends StatelessWidget {
if (state is DiaryStateLoaded && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented)
Padding(
padding: const EdgeInsets.only(top: 12),
- child: FinishDayButtonWidget(diaryDay: state.diary),
+ child: _isDiaryFinishing
+ ? const Center(child: Loader())
+ : FinishDayButtonWidget(
+ onPressed: () async {
+ setState(() {
+ _isDiaryFinishing = true;
+ });
+ await context.read().finishDiaryDay(
+ diaryId: state.diary.id,
+ wakeUp: state.diary.wakeup.toString(),
+ retire: state.diary.retire.toString(),
+ );
+ setState(() {
+ _isDiaryFinishing = false;
+ });
+ },
+ ),
),
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) ...[
const SizedBox(height: 16),
diff --git a/lib/features/diary/presentation/widgets/finish_day_button_widget.dart b/lib/features/diary/presentation/widgets/finish_day_button_widget.dart
index b3f170ec..af58216a 100644
--- a/lib/features/diary/presentation/widgets/finish_day_button_widget.dart
+++ b/lib/features/diary/presentation/widgets/finish_day_button_widget.dart
@@ -1,13 +1,10 @@
-import 'package:eatvio_app/features/diary/entities/diary_day_entity.dart';
-import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
class FinishDayButtonWidget extends StatefulWidget {
- final DiaryDayEntity diaryDay;
+ final Future Function() onPressed;
- const FinishDayButtonWidget({super.key, required this.diaryDay});
+ const FinishDayButtonWidget({super.key, required this.onPressed});
@override
State createState() => _FinishDayButtonWidgetState();
@@ -17,11 +14,7 @@ class _FinishDayButtonWidgetState extends State {
@override
Widget build(BuildContext context) {
return InkWell(
- onTap: () => context.read().finishDiaryDay(
- diaryId: widget.diaryDay.id,
- wakeUp: widget.diaryDay.wakeup.toString(),
- retire: widget.diaryDay.retire.toString(),
- ),
+ onTap: widget.onPressed,
borderRadius: BorderRadius.circular(20),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 8),
diff --git a/lib/features/settings/models/oauth_account_model.dart b/lib/features/settings/models/oauth_account_model.dart
new file mode 100644
index 00000000..27942119
--- /dev/null
+++ b/lib/features/settings/models/oauth_account_model.dart
@@ -0,0 +1,44 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'oauth_account_model.freezed.dart';
+part 'oauth_account_model.g.dart';
+
+@freezed
+abstract class OAuthAccountModel with _$OAuthAccountModel {
+ const factory OAuthAccountModel({
+ @JsonKey(name: 'oauth_id') required String oauthId,
+ required String name,
+ required String email,
+ required String phone,
+ @JsonKey(name: 'linked_at') required DateTime linkedAt,
+ }) = _OAuthAccountModel;
+
+ factory OAuthAccountModel.fromJson(Map json) => _$OAuthAccountModelFromJson(json);
+}
+
+@freezed
+abstract class OAuthAccountsResponseModel with _$OAuthAccountsResponseModel {
+ const factory OAuthAccountsResponseModel({required bool success, required OAuthAccountsDataModel data}) = _OAuthAccountsResponseModel;
+
+ factory OAuthAccountsResponseModel.fromJson(Map json) => _$OAuthAccountsResponseModelFromJson(json);
+}
+
+@freezed
+abstract class OAuthAccountsDataModel with _$OAuthAccountsDataModel {
+ const factory OAuthAccountsDataModel({
+ @JsonKey(fromJson: _oauthAccountFromJson) OAuthAccountModel? vk,
+ @JsonKey(fromJson: _oauthAccountFromJson) OAuthAccountModel? yandex,
+ }) = _OAuthAccountsDataModel;
+
+ factory OAuthAccountsDataModel.fromJson(Map json) => _$OAuthAccountsDataModelFromJson(json);
+}
+
+// Custom converter to handle both empty array [] and object
+OAuthAccountModel? _oauthAccountFromJson(dynamic json) {
+ if (json == null) return null;
+ if (json is List && json.isEmpty) return null;
+ if (json is Map) {
+ return OAuthAccountModel.fromJson(json);
+ }
+ return null;
+}
diff --git a/lib/features/settings/models/oauth_attach_request.dart b/lib/features/settings/models/oauth_attach_request.dart
new file mode 100644
index 00000000..600fb320
--- /dev/null
+++ b/lib/features/settings/models/oauth_attach_request.dart
@@ -0,0 +1,17 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'oauth_attach_request.freezed.dart';
+part 'oauth_attach_request.g.dart';
+
+@freezed
+abstract class OAuthAttachRequest with _$OAuthAttachRequest {
+ const factory OAuthAttachRequest({
+ required String provider,
+ @JsonKey(name: 'client_id') String? clientId,
+ @JsonKey(name: 'access_token') required String accessToken,
+ @JsonKey(name: 'refresh_token') required String refreshToken,
+ }) = _OAuthAttachRequest;
+
+ factory OAuthAttachRequest.fromJson(Map json) => _$OAuthAttachRequestFromJson(json);
+}
+
diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart
index 2a64e2e9..ed123203 100644
--- a/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart
+++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart
@@ -4,6 +4,8 @@ import 'package:eatvio_app/features/consultations/repositories/consultations_rep
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
+import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
+import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
@@ -26,6 +28,7 @@ class SettingsCubit extends Cubit {
final profile = await profileRepository.getMyProfile();
late final ConsultationRequestModel? consultantInfo;
late final bool hasActiveConsultation;
+ late final OAuthAccountsDataModel? oauthAccounts;
try {
if (profile.phoneConfirmationState.contains('confirmed')) {
@@ -43,6 +46,13 @@ class SettingsCubit extends Cubit {
hasActiveConsultation = false;
}
+ try {
+ final oauthResponse = await settingsRepository.getOAuthAccounts();
+ oauthAccounts = oauthResponse.data;
+ } catch (e) {
+ oauthAccounts = null;
+ }
+
try {
emit(
SettingsState.loaded(
@@ -51,6 +61,7 @@ class SettingsCubit extends Cubit {
profile: profile,
consultationRequestModel: consultantInfo,
hasActiveConsultation: hasActiveConsultation,
+ oauthAccounts: oauthAccounts,
),
);
} catch (e) {
@@ -87,6 +98,35 @@ class SettingsCubit extends Cubit {
}
}
+ Future linkOAuthAccount({
+ required String provider,
+ required String accessToken,
+ required String refreshToken,
+ String? clientId,
+ }) async {
+ try {
+ final request = OAuthAttachRequest(
+ provider: provider,
+ clientId: clientId,
+ accessToken: accessToken,
+ refreshToken: refreshToken,
+ );
+ await settingsRepository.linkOAuthAccount(request: request);
+ await init();
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future unlinkOAuthAccount({required String provider}) async {
+ try {
+ await settingsRepository.unlinkOAuthAccount(provider: provider);
+ await init();
+ } catch (e) {
+ rethrow;
+ }
+ }
+
Future _checkActiveConsultation() async {
try {
final userPurchases = await consultationsRepository.getUserPurchases(null, null);
diff --git a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart
index 86471f31..b8746dd9 100644
--- a/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart
+++ b/lib/features/settings/presentation/bloc/settings_cubit/settings_state.dart
@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
+import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -17,6 +18,7 @@ abstract class SettingsState with _$SettingsState {
required ProfileModel profile,
ConsultationRequestModel? consultationRequestModel,
required bool hasActiveConsultation,
+ OAuthAccountsDataModel? oauthAccounts,
}) = SettingsStateLoaded;
const factory SettingsState.error({
diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart
index ad42a7b4..d6ecfd92 100644
--- a/lib/features/settings/presentation/pages/settings_screen.dart
+++ b/lib/features/settings/presentation/pages/settings_screen.dart
@@ -6,6 +6,7 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
+import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
@@ -16,6 +17,7 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
class SettingsScreen extends StatefulWidget {
@@ -39,12 +41,7 @@ class _SettingsScreenState extends State {
return AlertDialog(
title: const Text('Предупреждение'),
content: const Text('Во время консультации этот параметр нельзя отключить'),
- actions: [
- TextButton(
- onPressed: () => Navigator.of(context).pop(),
- child: const Text('Понятно'),
- ),
- ],
+ actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Понятно'))],
);
},
);
@@ -74,6 +71,8 @@ class _SettingsScreenState extends State {
physics: const BouncingScrollPhysics(),
child: Column(
children: [
+ _oauthAccounts(state, settingsCubit),
+ const SizedBox(height: 12),
_additionalInfo(state),
const SizedBox(height: 12),
_notifications(state),
@@ -328,6 +327,326 @@ class _SettingsScreenState extends State {
),
);
}
+
+ Widget _oauthAccounts(SettingsStateLoaded state, SettingsCubit settingsCubit) {
+ final isVkLinked = state.oauthAccounts?.vk != null;
+ final isYandexLinked = state.oauthAccounts?.yandex != null;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ 'Привязанные аккаунты',
+ style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'),
+ ),
+ ),
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _OAuthAccountButton(
+ onTap: () => _onVkPressed(state, settingsCubit),
+ backgroundColor: const Color(0xFF2787F5),
+ tooltip: 'VK ID',
+ icon: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24),
+ isLinked: isVkLinked,
+ accountName: state.oauthAccounts?.vk?.name,
+ ),
+ const SizedBox(width: 12),
+ _OAuthAccountButton(
+ onTap: () => _onOkPressed(state, settingsCubit),
+ backgroundColor: const Color(0xFFF58220),
+ tooltip: 'Одноклассники',
+ icon: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24),
+ isLinked: isVkLinked,
+ accountName: state.oauthAccounts?.vk?.name,
+ ),
+ const SizedBox(width: 12),
+ _OAuthAccountButton(
+ onTap: () => _onMailRuPressed(state, settingsCubit),
+ backgroundColor: const Color(0xFF005FF9),
+ tooltip: 'Mail.ru',
+ icon: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24),
+ isLinked: isVkLinked,
+ accountName: state.oauthAccounts?.vk?.name,
+ ),
+ const SizedBox(width: 12),
+ _OAuthAccountButton(
+ onTap: () => _onYandexPressed(state, settingsCubit),
+ backgroundColor: const Color(0xFFFFCC00),
+ tooltip: 'Яндекс ID',
+ icon: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24),
+ isLinked: isYandexLinked,
+ accountName: state.oauthAccounts?.yandex?.name,
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 12),
+ ],
+ );
+ }
+
+ Future _onVkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
+ if (state.oauthAccounts?.vk != null) {
+ // Отвязываем аккаунт
+ if (!mounted) return;
+ final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'VK ID');
+ if (shouldUnlink == true) {
+ try {
+ await settingsCubit.unlinkOAuthAccount(provider: 'vk');
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
+ }
+ }
+ }
+ return;
+ }
+
+ try {
+ if (!mounted) return;
+
+ final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
+ if (authResult == null) return;
+
+ try {
+ await settingsCubit.linkOAuthAccount(
+ provider: 'vk',
+ accessToken: authResult.accessToken,
+ refreshToken: authResult.refreshToken ?? '',
+ clientId: authResult.clientId,
+ );
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт');
+ }
+ }
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт');
+ }
+ }
+ }
+
+ Future _onOkPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
+ if (state.oauthAccounts?.vk != null) {
+ // Отвязываем аккаунт (OK использует тот же VK ID провайдер)
+ if (!mounted) return;
+ final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Одноклассники');
+ if (shouldUnlink == true) {
+ try {
+ await settingsCubit.unlinkOAuthAccount(provider: 'vk');
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
+ }
+ }
+ }
+ return;
+ }
+
+ try {
+ if (!mounted) return;
+
+ final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile();
+ if (authResult == null) return;
+
+ try {
+ await settingsCubit.linkOAuthAccount(
+ provider: 'vk',
+ accessToken: authResult.accessToken,
+ refreshToken: authResult.refreshToken ?? '',
+ clientId: authResult.clientId,
+ );
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт');
+ }
+ }
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт');
+ }
+ }
+ }
+
+ Future _onMailRuPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
+ if (state.oauthAccounts?.vk != null) {
+ // Отвязываем аккаунт (Mail.ru использует тот же VK ID провайдер)
+ if (!mounted) return;
+ final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Mail.ru');
+ if (shouldUnlink == true) {
+ try {
+ await settingsCubit.unlinkOAuthAccount(provider: 'vk');
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
+ }
+ }
+ }
+ return;
+ }
+
+ try {
+ if (!mounted) return;
+
+ final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
+ if (authResult == null) return;
+
+ try {
+ await settingsCubit.linkOAuthAccount(
+ provider: 'vk',
+ accessToken: authResult.accessToken,
+ refreshToken: authResult.refreshToken ?? '',
+ clientId: authResult.clientId,
+ );
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт');
+ }
+ }
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт');
+ }
+ }
+ }
+
+ Future _onYandexPressed(SettingsStateLoaded state, SettingsCubit settingsCubit) async {
+ if (state.oauthAccounts?.yandex != null) {
+ // Отвязываем аккаунт
+ if (!mounted) return;
+ final shouldUnlink = await _showUnlinkConfirmationDialog(context, 'Яндекс ID');
+ if (shouldUnlink == true) {
+ try {
+ await settingsCubit.unlinkOAuthAccount(provider: 'yandex');
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось отвязать аккаунт');
+ }
+ }
+ }
+ return;
+ }
+
+ try {
+ if (!mounted) return;
+
+ final authResult = await SocialAuthFacade.signInWithYandex();
+ if (authResult == null) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось авторизоваться через Яндекс');
+ }
+ return;
+ }
+
+ // Проверяем, что refreshToken не пустой для Яндекс
+ if (authResult.refreshToken == null || authResult.refreshToken!.isEmpty) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось получить токен обновления от Яндекс');
+ }
+ return;
+ }
+
+ // Проверяем, что clientId не пустой для Яндекс
+ if (authResult.clientId.isEmpty) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось получить client ID от Яндекс');
+ }
+ return;
+ }
+
+ try {
+ await settingsCubit.linkOAuthAccount(
+ provider: 'yandex',
+ accessToken: authResult.accessToken,
+ refreshToken: authResult.refreshToken!,
+ clientId: authResult.clientId,
+ );
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}');
+ }
+ }
+ } catch (e) {
+ if (mounted) {
+ showErrorDialog(context, description: 'Не удалось привязать аккаунт: ${e.toString()}');
+ }
+ }
+ }
+
+ Future _showUnlinkConfirmationDialog(BuildContext context, String providerName) {
+ return showDialog(
+ context: context,
+ builder: (context) => CupertinoAlertDialog(
+ title: const Text('Отвязать аккаунт'),
+ content: Text('Вы точно хотите отвязать аккаунт $providerName?'),
+ actions: [
+ CupertinoDialogAction(
+ onPressed: () {
+ Navigator.of(context).pop(false);
+ },
+ child: const Text('Отмена'),
+ ),
+ CupertinoDialogAction(
+ onPressed: () {
+ Navigator.of(context).pop(true);
+ },
+ isDestructiveAction: true,
+ child: const Text('Отвязать'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _OAuthAccountButton extends StatelessWidget {
+ const _OAuthAccountButton({
+ required this.onTap,
+ required this.icon,
+ required this.backgroundColor,
+ required this.tooltip,
+ required this.isLinked,
+ this.accountName,
+ });
+
+ final VoidCallback onTap;
+ final Widget icon;
+ final Color backgroundColor;
+ final String tooltip;
+ final bool isLinked;
+ final String? accountName;
+
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: Opacity(
+ opacity: isLinked ? 0.5 : 1.0,
+ child: Tooltip(
+ message: tooltip,
+ child: InkWell(
+ borderRadius: BorderRadius.circular(20),
+ onTap: onTap,
+ child: Ink(
+ height: 48,
+ decoration: BoxDecoration(
+ color: Colors.transparent,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: const Color(0xFFDBDBDB), width: 1),
+ ),
+ child: Center(child: icon),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
}
class DeleteAccountBottomSheet extends StatefulWidget {
diff --git a/lib/features/settings/repositories/service_repository.dart b/lib/features/settings/repositories/service_repository.dart
index eb462de3..139ee31e 100644
--- a/lib/features/settings/repositories/service_repository.dart
+++ b/lib/features/settings/repositories/service_repository.dart
@@ -1,4 +1,6 @@
import 'package:eatvio_app/features/profile/models/profile_model.dart';
+import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
+import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
@@ -15,25 +17,41 @@ class SettingsRepository {
}
}
- Future updateProfileSettings({
- required ProfileSettingsModel profileSettingsModel,
- }) async {
+ Future updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async {
try {
- return await settingsService.updateProfileSettings(
- data: profileSettingsModel.toJson(),
- );
+ return await settingsService.updateProfileSettings(data: profileSettingsModel.toJson());
} catch (e) {
rethrow;
}
}
- Future updateProfileDetails({
- required ProfileDetailsModel profileDetailsModel,
- }) async {
+ Future updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async {
try {
- return await settingsService.updateProfileDetails(
- data: profileDetailsModel.toJson(),
- );
+ return await settingsService.updateProfileDetails(data: profileDetailsModel.toJson());
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future getOAuthAccounts() async {
+ try {
+ return await settingsService.getOAuthAccounts();
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future linkOAuthAccount({required OAuthAttachRequest request}) async {
+ try {
+ return await settingsService.linkOAuthAccount(request);
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future unlinkOAuthAccount({required String provider}) async {
+ try {
+ return await settingsService.unlinkOAuthAccount(provider);
} catch (e) {
rethrow;
}
diff --git a/lib/features/settings/services/settings_service.dart b/lib/features/settings/services/settings_service.dart
index d17b5842..c83f4c41 100644
--- a/lib/features/settings/services/settings_service.dart
+++ b/lib/features/settings/services/settings_service.dart
@@ -1,5 +1,7 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
+import 'package:eatvio_app/features/settings/models/oauth_account_model.dart';
+import 'package:eatvio_app/features/settings/models/oauth_attach_request.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
import 'package:retrofit/retrofit.dart';
@@ -13,12 +15,17 @@ abstract class SettingsService {
Future getProfileSettings();
@PUT('/api/profile/settings/update')
- Future updateProfileSettings({
- @Body() required Map data,
- });
+ Future updateProfileSettings({@Body() required Map data});
@PUT('/api/profile/update')
- Future updateProfileDetails({
- @Body() required Map data,
- });
+ Future updateProfileDetails({@Body() required Map data});
+
+ @GET('/api/oauth/accounts')
+ Future getOAuthAccounts();
+
+ @POST('/api/oauth/attach')
+ Future linkOAuthAccount(@Body() OAuthAttachRequest request);
+
+ @DELETE('/api/oauth/accounts/{provider}')
+ Future unlinkOAuthAccount(@Path('provider') String provider);
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 5e3c7d25..940fdcd6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
-version: 1.3.3+82
+version: 1.3.4+83
publish_to: 'none'
environment:
@@ -25,10 +25,10 @@ dependencies:
flutter_form_builder: ^10.2.0
image_picker: ^1.2.0
flutter_typeahead: ^5.2.0
- go_router: ^16.1.0
+ go_router: ^17.0.0
keyboard_dismisser: ^3.0.0
retrofit: ^4.7.1
- get_it: ^8.2.0
+ get_it: ^9.0.5
dio: ^5.9.0
dart_pusher_channels: ^1.2.3
photo_view: ^0.15.0
@@ -37,11 +37,11 @@ dependencies:
easy_debounce: ^2.0.3
badges: ^3.1.2
google_fonts: ^6.3.0
- syncfusion_flutter_charts: ^30.2.5
- syncfusion_flutter_gauges: ^30.2.5
+ syncfusion_flutter_charts: ^31.2.10
+ syncfusion_flutter_gauges: ^31.2.10
table_calendar: ^3.2.0
flutter_keyboard_visibility: ^6.0.0
- share_plus: ^11.1.0
+ share_plus: ^12.0.1
flutter_html: ^3.0.0
file_picker: ^10.3.1
audioplayers: ^6.5.0
@@ -49,7 +49,7 @@ dependencies:
mask_text_input_formatter: ^2.9.0
lottie: ^3.3.1
flutter_portal: ^1.1.4
- fluttertoast: ^8.2.12
+ fluttertoast: ^9.0.0
flutter_rating_bar: ^4.0.1
flutter_image_compress: ^2.4.0
image: ^4.5.4
@@ -57,8 +57,8 @@ dependencies:
http: ^1.5.0
flutter_native_splash: ^2.4.6
webview_flutter: ^4.13.0
- sentry_flutter: ^9.6.0
- connectivity_plus: ^6.1.5
+ sentry_flutter: ^9.8.0
+ connectivity_plus: ^7.0.0
visibility_detector: ^0.4.0+2
firebase_core: ^4.0.0
firebase_messaging: ^16.0.0
@@ -67,17 +67,18 @@ dependencies:
collection: ^1.19.1
shared_preferences: ^2.5.3
version: ^3.0.2
- package_info_plus: ^8.3.1
- device_info_plus: ^11.5.0
+ package_info_plus: ^9.0.0
+ device_info_plus: ^12.2.0
permission_handler: ^12.0.1
qr_code_scanner_plus: ^2.0.10+1
- flutter_dotenv: ^5.2.1
- uuid: ^4.5.1
+ flutter_dotenv: ^6.0.0
+ uuid: ^4.5.2
appmetrica_plugin: ^3.3.0
vk_id: ^1.0.3
flutter_login_yandex: ^0.0.5
markdown: ^7.3.0
gpassword: ^1.0.0
+ mobile_scanner: ^7.1.3
dev_dependencies:
build_runner: ^2.7.0
diff --git a/test/features/create_diet/integration_widget_test.dart b/test/features/create_diet/integration_widget_test.dart
index 76f7de0b..0d8c7b31 100644
--- a/test/features/create_diet/integration_widget_test.dart
+++ b/test/features/create_diet/integration_widget_test.dart
@@ -35,7 +35,7 @@ void main() {
setUp(() async {
await sl.reset();
// Ensure dotenv has required keys before AppConfig constructor reads them
- dotenv.testLoad(fileInput: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
+ dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
// Register minimal AppConfig for tests to satisfy dependencies (e.g. image URL building)
sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
mockCreateDietRepository = MockCreateDietRepository();
diff --git a/test/features/create_recipe/widget_test.dart b/test/features/create_recipe/widget_test.dart
index 1b85ac35..6a60b0b3 100644
--- a/test/features/create_recipe/widget_test.dart
+++ b/test/features/create_recipe/widget_test.dart
@@ -35,7 +35,7 @@ void main() {
setUp(() async {
await sl.reset();
- dotenv.testLoad(fileInput: 'DEV_BASE_URL=\nBASE_URL=\n');
+ dotenv.loadFromString(envString: 'DEV_BASE_URL=\nBASE_URL=\n');
mockRecipeRepository = MockRecipeRepository();
mockGlossaryRepository = MockGlossaryRepository();
createRecipeCubit = CreateRecipeCubit(recipeRepository: mockRecipeRepository, glossaryRepository: mockGlossaryRepository);
diff --git a/test/features/diary/presentation/widgets/basic_widget_test.dart b/test/features/diary/presentation/widgets/basic_widget_test.dart
index 05daded2..02c97f23 100644
--- a/test/features/diary/presentation/widgets/basic_widget_test.dart
+++ b/test/features/diary/presentation/widgets/basic_widget_test.dart
@@ -220,7 +220,7 @@ void main() {
testWidgets('should display FinishDayButtonWidget', (WidgetTester tester) async {
// Arrange
- final widget = FinishDayButtonWidget(diaryDay: testDiaryEntity);
+ final widget = FinishDayButtonWidget(onPressed: () async {});
// Act
await tester.pumpWidget(createTestWidget(widget));
@@ -234,7 +234,7 @@ void main() {
// Arrange
diaryCubit.emit(DiaryState.loaded(diary: testDiaryEntity, nutrition: testNutrition, selectedDay: DateTime(2024, 1, 15), selectedMeal: 1));
- final widget = FinishDayButtonWidget(diaryDay: testDiaryEntity);
+ final widget = FinishDayButtonWidget(onPressed: () async {});
await tester.pumpWidget(createTestWidget(widget));
await tester.pumpAndSettle();