Merge pull request 'fix: profile roles' (#224) from BLoC/eatvio_app:main into main

Reviewed-on: eatvio/eatvio_app#224
This commit is contained in:
BLoC
2025-02-12 12:05:45 +00:00
17 changed files with 244 additions and 419 deletions

View File

@@ -22,6 +22,7 @@ import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
@@ -63,7 +64,17 @@ class AppRouter {
}
return null;
},
builder: (context, state) => AuthScreen.builder(context),
builder: (context, state) {
final bool needLogout;
if (state.extra == null) {
needLogout = false;
} else {
needLogout = true;
}
return AuthScreen(
needLogout: needLogout,
);
},
routes: [
GoRoute(
path: 'registration-screen',
@@ -125,6 +136,11 @@ class AppRouter {
);
},
),
GoRoute(
path: '/glossary-screen',
name: '/glossaryScreen',
builder: (context, state) => GlossaryScreen(),
),
GoRoute(
path: '/about-recipes-screen',
name: 'aboutRecipesScreen',

View File

@@ -1,5 +1,5 @@
// String baseUrl = 'https://eatvio.ru';
String baseUrl = 'https://dev2.eatvio.com';
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];

View File

@@ -0,0 +1,32 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
abstract class CredentialsStorage {
static Future<bool> haveCredentialsInStorage() async {
try {
if (await FlutterSecureStorage().read(key: 'email') == null || await FlutterSecureStorage().read(key: 'password') == null) {
return false;
} else {
return true;
}
} catch (e) {
return false;
}
}
static Future<Map<String, String>> getCredentialsFromStorage() async {
return {
'email': (await FlutterSecureStorage().read(key: 'email'))!,
'password': (await FlutterSecureStorage().read(key: 'password'))!,
};
}
static saveCredentials(String email, String password) async {
await FlutterSecureStorage().write(key: 'email', value: email);
await FlutterSecureStorage().write(key: 'password', value: password);
}
static removeCredentials() async {
await FlutterSecureStorage().delete(key: 'email');
await FlutterSecureStorage().delete(key: 'password');
}
}

View File

@@ -64,6 +64,7 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_it/get_it.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profilePlans;
@@ -155,4 +156,5 @@ Future<void> init() async {
),
),
);
sl.registerLazySingleton(() async => await FlutterSecureStorage());
}

View File

@@ -7,7 +7,7 @@ abstract class TokenStorage {
sl<Dio>().options.headers['Authorization'] = 'Bearer $token';
}
static haveTokenInStorage() async {
static Future<bool> haveTokenInStorage() async {
try {
if (await FlutterSecureStorage().read(key: 'token') == null) {
return false;

View File

@@ -1,41 +1,22 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/credentials_storage.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/const/strings.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import 'package:validators/validators.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'bloc/auth_bloc.dart';
import 'models/auth_model.dart';
class AuthScreen extends StatefulWidget {
static Widget builder(BuildContext context) {
return BlocProvider<AuthBloc>(
create: (context) => AuthBloc(
AuthState(
authModelObj: AuthModel(),
),
)..add(
AuthInitialEvent(),
),
child: AuthScreen(),
);
}
final bool needLogout;
const AuthScreen({
this.needLogout = false,
});
@override
State<AuthScreen> createState() => _AuthScreenState();
@@ -47,6 +28,16 @@ class _AuthScreenState extends State<AuthScreen> {
final baseUrlController = TextEditingController();
late final WebViewController _controller;
String email = '';
String password = '';
Future<void> logout() async {
await CredentialsStorage.removeCredentials();
await _controller.runJavaScript('''
document.querySelector('.header-list__link button[type="submit"]').click();
''');
}
@override
void initState() {
super.initState();
@@ -56,42 +47,73 @@ class _AuthScreenState extends State<AuthScreen> {
..addJavaScriptChannel(
'FlutterChannel',
onMessageReceived: (JavaScriptMessage message) {
// Handle messages from JavaScript
final data = jsonDecode(message.message);
final email = data['email'];
final password = data['password'];
print('Email: $email, Password: $password');
email = data['email'];
password = data['password'];
},
)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) async {
await _controller.runJavaScript('''
function initializeListeners() {
const emailInput = document.getElementById("inputEmail_main");
const passwordInput = document.getElementById("inputPassword_main");
if (emailInput && passwordInput) {
function sendToFlutter() {
const email = emailInput.value;
const password = passwordInput.value;
const data = JSON.stringify({ email: email, password: password });
if (window.FlutterChannel) {
window.FlutterChannel.postMessage(data);
}
onNavigationRequest: (NavigationRequest navigationRequest) async {
if (navigationRequest.url.contains('profile')) {
if (await CredentialsStorage.haveCredentialsInStorage()) {
email = (await CredentialsStorage.getCredentialsFromStorage())['email']!;
password = (await CredentialsStorage.getCredentialsFromStorage())['password']!;
} else {
if (email.isNotEmpty && password.isNotEmpty && !(await TokenStorage.haveTokenInStorage())) {
await CredentialsStorage.saveCredentials(email, password);
try {
final Map<String, String> credentials = {
'email': email,
'password': password,
};
final response = await http.post(
Uri.parse(baseUrl + '/api/login'),
headers: {'Content-Type': 'application/json'},
body: json.encode(credentials),
encoding: utf8,
);
var res = await jsonDecode(response.body);
final token = res['token'];
await TokenStorage.saveTokenInStorage(token);
GoRouter.of(context).go('/my-profile-screen');
await sl<ProfileAvatarCubit>().getMyProfile();
} catch (e) {
print(e);
}
emailInput.addEventListener("input", sendToFlutter);
passwordInput.addEventListener("input", sendToFlutter);
} else {
console.error("Email or Password input not found.");
await logout();
}
}
// Ensure DOM is loaded before attaching listeners
document.addEventListener("DOMContentLoaded", initializeListeners);
initializeListeners();
''');
}
return NavigationDecision.navigate;
},
onPageStarted: (String url) async {
final bool tokenInStorage = await TokenStorage.haveTokenInStorage();
final bool credentialsInStorage = await CredentialsStorage.haveCredentialsInStorage();
if (tokenInStorage && credentialsInStorage) {
GoRouter.of(context).go('/my-profile-screen');
} else if (!tokenInStorage && credentialsInStorage) {
final credentialsFromStorage = await CredentialsStorage.getCredentialsFromStorage();
final Map<String, String> credentials = {
'email': credentialsFromStorage['email']!,
'password': credentialsFromStorage['password']!,
};
final response = await http.post(
Uri.parse(baseUrl + '/api/login'),
headers: {'Content-Type': 'application/json'},
body: json.encode(credentials),
encoding: utf8,
);
var res = await jsonDecode(response.body);
final token = res['token'];
await TokenStorage.saveTokenInStorage(token);
GoRouter.of(context).go('/my-profile-screen');
await sl<ProfileAvatarCubit>().getMyProfile();
}
},
onPageFinished: (String url) async {
await _controller.runJavaScript(jsChannelAuthScript);
},
),
)
@@ -101,269 +123,13 @@ class _AuthScreenState extends State<AuthScreen> {
@override
Widget build(BuildContext context) {
FlutterNativeSplash.remove();
final s = S.of(context);
// return Scaffold(
// appBar: AppBar(
// toolbarHeight: 0,
// ),
// body: WebViewWidget(controller: _controller),
// );
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: GestureDetector(
onLongPress: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Base url без https:// и /api'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CustomTextFormField(
controller: baseUrlController,
hintText: 'dev2.eatvio.com',
),
const SizedBox(height: 12),
InkWell(
onTap: () async {
baseUrl = 'https://${baseUrlController.text}';
sl<Dio>().options.baseUrl = baseUrl;
},
borderRadius: BorderRadius.circular(20),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
alignment: Alignment.center,
child: Text(
'Сохранить',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
),
);
},
child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 70,
fit: BoxFit.contain,
),
),
toolbarHeight: 0,
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.email),
const SizedBox(height: 4),
BlocSelector<AuthBloc, AuthState, TextEditingController?>(
selector: (state) => state.emailController,
builder: (context, emailController) {
return CustomTextFormField(
controller: emailController,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
textInputType: TextInputType.emailAddress,
hintText: 'Введите email',
);
},
),
const SizedBox(height: 16),
Text(s.password),
const SizedBox(height: 4),
BlocSelector<AuthBloc, AuthState, TextEditingController?>(
selector: (state) => state.passwordController,
builder: (context, passwordController) {
return CustomTextFormField(
obscureText: hidePassword,
controller: passwordController,
hintText: 'Введите пароль',
suffix: IconButton(
onPressed: () {
setState(() {
hidePassword = !hidePassword;
});
},
icon: Icon(
hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
color: Colors.grey,
),
),
);
},
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Нет аккаунта? '),
GestureDetector(
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
child: Text(
'Зарегистрироваться',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
),
),
],
),
Spacer(),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Войти", Вы соглашаетесь \n',
style: TextStyle(
fontSize: 12,
color: Colors.black,
),
children: [
TextSpan(text: 'c '),
TextSpan(
text: ' Публичной офертой',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse(baseUrl + '/oferta_bs.pdf')),
),
],
),
),
),
const SizedBox(height: 16),
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
return Container(
height: 48,
width: MediaQuery.of(context).size.width - 32,
child: FloatingActionButton.extended(
heroTag: 'authButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400,
onPressed: onTapAuth(state, context),
label: Text(
'Войти',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
),
);
},
),
],
),
),
child: WebViewWidget(controller: _controller),
),
);
}
onTapAuth(AuthState state, BuildContext context) {
final s = S.of(context);
return () async {
List<String> errors = validateFields(state.emailController!.text, state.passwordController!.text);
if (errors.isEmpty) {
final Map<String, dynamic> credentials = {
'email': state.emailController!.text,
'password': state.passwordController!.text,
};
final response = await http.post(
Uri.parse(baseUrl + '/api/login'),
headers: {'Content-Type': 'application/json'},
body: json.encode(credentials),
encoding: utf8,
);
if (response.statusCode == 200) {
var res = await jsonDecode(response.body);
final token = res['token'];
await TokenStorage.saveTokenInStorage(token);
GoRouter.of(context).go('/my-profile-screen');
sl<ProfileAvatarCubit>().getMyProfile();
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Ошибка'),
content: Text(s.wrong_credentials),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
} else {
String errorMessage = '';
for (var error in errors) {
errorMessage += error + '\n\n';
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Ошибка'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
};
}
List<String> validateFields(String email, String password) {
final s = S.of(context);
List<String> errors = [];
if (!isEmail(email)) {
errors.add(s.email_invalid);
}
if (email.isEmpty) {
errors.add(s.email_empty);
}
if (password.isEmpty) {
errors.add(s.password_empty);
}
if (password.length < 6) {
errors.add(s.password_short);
}
return errors;
}
}

View File

@@ -1,26 +0,0 @@
import 'package:eatvio_app/features/auth/auth_screen/models/auth_model.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
part 'auth_event.dart';
part 'auth_state.dart';
/// A bloc that manages the state of a Auth according to the event that is dispatched to it.
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(AuthState initialState) : super(initialState) {
on<AuthInitialEvent>(_onInitialize);
}
_onInitialize(
AuthInitialEvent event,
Emitter<AuthState> emit,
) async {
emit(
state.copyWith(
emailController: TextEditingController(),
passwordController: TextEditingController(),
),
);
}
}

View File

@@ -1,16 +0,0 @@
// ignore_for_file: must_be_immutable
part of 'auth_bloc.dart';
/// Abstract class for all events that can be dispatched from the
///Credentials widget.
///
/// Events must be immutable and implement the [Equatable] interface.
@immutable
abstract class AuthEvent extends Equatable {}
/// Event that is dispatched when the Credentials widget is first created.
class AuthInitialEvent extends AuthEvent {
@override
List<Object?> get props => [];
}

View File

@@ -1,37 +0,0 @@
// ignore_for_file: must_be_immutable
part of 'auth_bloc.dart';
/// Represents the state of Auth in the application.
class AuthState extends Equatable {
AuthState({
this.emailController,
this.passwordController,
this.authModelObj,
});
TextEditingController? emailController;
TextEditingController? passwordController;
AuthModel? authModelObj;
@override
List<Object?> get props => [
emailController,
passwordController,
authModelObj,
];
AuthState copyWith({
TextEditingController? emailController,
TextEditingController? passwordController,
AuthModel? authModelObj,
}) {
return AuthState(
emailController: emailController ?? this.emailController,
passwordController: passwordController ?? this.passwordController,
authModelObj: authModelObj ?? this.authModelObj,
);
}
}

View File

@@ -1,16 +0,0 @@
// ignore_for_file: must_be_immutable
import 'package:equatable/equatable.dart';
/// This class defines the variables used in the [auth_screen],
/// and is typically used to hold data that is passed between different parts of the application.
class AuthModel extends Equatable {
AuthModel();
AuthModel copyWith() {
return AuthModel();
}
@override
List<Object?> get props => [];
}

View File

@@ -0,0 +1,22 @@
const String jsChannelAuthScript = '''
function initializeListeners() {
const emailInput = document.getElementById("inputEmail_main");
const passwordInput = document.getElementById("inputPassword_main");
if (emailInput && passwordInput) {
function sendToFlutter() {
const email = emailInput.value;
const password = passwordInput.value;
const data = JSON.stringify({ email: email, password: password });
if (window.FlutterChannel) {
window.FlutterChannel.postMessage(data);
}
}
emailInput.addEventListener("input", sendToFlutter);
passwordInput.addEventListener("input", sendToFlutter);
} else {
console.error("Email or Password input not found.");
}
}
document.addEventListener("DOMContentLoaded", initializeListeners);
initializeListeners();
''';

View File

@@ -0,0 +1,33 @@
import 'package:eatvio_app/core/const.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class GlossaryScreen extends StatefulWidget {
const GlossaryScreen({super.key});
@override
State<GlossaryScreen> createState() => _GlossaryScreenState();
}
class _GlossaryScreenState extends State<GlossaryScreen> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(baseUrl + '/product_nutritional_value'));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Глоссарий'),
),
body: WebViewWidget(controller: _controller),
);
}
}

View File

@@ -127,7 +127,7 @@ class MenuScreen extends StatelessWidget {
} catch (e) {
} finally {
await TokenStorage.removeTokenInStorage();
GoRouter.of(context).go('/auth-screen');
GoRouter.of(context).go('/auth-screen', extra: true);
}
};
}

View File

@@ -53,6 +53,7 @@ class MessageLookup extends MessageLookupByLibrary {
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"),
"by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"),
"calories": MessageLookupByLibrary.simpleMessage("Калории"),
"cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"),
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"),
"carb_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
@@ -86,10 +87,14 @@ class MessageLookup extends MessageLookupByLibrary {
"comments": MessageLookupByLibrary.simpleMessage("Комментарии"),
"comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"),
"complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"),
"confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"),
"continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"),
"continue_": MessageLookupByLibrary.simpleMessage("Продолжить"),
"copied": MessageLookupByLibrary.simpleMessage("cкопирован"),
"copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
"could_not_cancel_diet": MessageLookupByLibrary.simpleMessage(
"Не удалось отменить диету",
),
"country": MessageLookupByLibrary.simpleMessage("Страна"),
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"),
"customer_price": MessageLookupByLibrary.simpleMessage(
@@ -103,6 +108,7 @@ class MessageLookup extends MessageLookupByLibrary {
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"),
"diet_started": MessageLookupByLibrary.simpleMessage("Диета уже начата"),
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"),
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"),
"do_you_have_hypertension_tendency": MessageLookupByLibrary.simpleMessage(

View File

@@ -1799,6 +1799,41 @@ class S {
args: [],
);
}
/// `Диета уже начата`
String get diet_started {
return Intl.message(
'Диета уже начата',
name: 'diet_started',
desc: '',
args: [],
);
}
/// `Отменить диету?`
String get cancel_diet {
return Intl.message(
'Отменить диету?',
name: 'cancel_diet',
desc: '',
args: [],
);
}
/// `Подтвердить`
String get confirm {
return Intl.message('Подтвердить', name: 'confirm', desc: '', args: []);
}
/// `Не удалось отменить диету`
String get could_not_cancel_diet {
return Intl.message(
'Не удалось отменить диету',
name: 'could_not_cancel_diet',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -14,12 +14,19 @@ import 'package:flutter_localizations/flutter_localizations.dart';
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';
Future<void> main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await di.init();
runApp(MyApp());
await SentryFlutter.init(
(options) {
options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2';
options.tracesSampleRate = 1.0;
},
appRunner: () => runApp(MyApp()),
);
FlutterNativeSplash.remove();
}

View File

@@ -23,14 +23,14 @@ dependencies:
validators: ^3.0.0
path_provider: ^2.1.5
carousel_slider_plus: ^7.1.0
flutter_form_builder: ^7.0.0-alpha.3
flutter_form_builder: ^9.7.0
image_picker: ^1.1.2
flutter_typeahead: ^5.2.0
go_router: ^14.7.1
go_router: ^14.7.2
keyboard_dismisser: ^3.0.0
retrofit: ^4.4.2
get_it: ^8.0.3
dio: ^5.7.0
dio: ^5.8.0+1
dart_pusher_channels: ^1.2.3
photo_view: ^0.15.0
dismissible_page: ^1.0.2
@@ -38,14 +38,14 @@ dependencies:
easy_debounce: ^2.0.3
badges: ^3.1.2
google_fonts: ^6.2.1
syncfusion_flutter_charts: ^28.1.38
syncfusion_flutter_gauges: ^28.1.38
syncfusion_flutter_charts: ^28.2.3
syncfusion_flutter_gauges: ^28.2.3
table_calendar: ^3.2.0
flutter_keyboard_visibility: ^6.0.0
share_plus: ^10.1.4
flutter_html: ^3.0.0-beta.2
file_picker: ^8.1.7
audioplayers: ^6.1.0
file_picker: ^8.3.1
audioplayers: ^6.1.1
intl: ^0.20.2
rive: ^0.13.20
mask_text_input_formatter: ^2.9.0
@@ -59,6 +59,7 @@ dependencies:
http: ^1.3.0
flutter_native_splash: ^2.4.4
webview_flutter: ^4.10.0
sentry_flutter: ^8.13.0
connectivity_plus: ^6.1.2
dev_dependencies: