diff --git a/lib/presentation/pages/profile/about_app_page.dart b/lib/presentation/pages/profile/about_app_page.dart index 1518f005..19fbbc5d 100644 --- a/lib/presentation/pages/profile/about_app_page.dart +++ b/lib/presentation/pages/profile/about_app_page.dart @@ -3,7 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rtu_mirea_app/presentation/bloc/about_app_bloc/about_app_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; +import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/icon_button.dart'; +import 'package:rtu_mirea_app/presentation/widgets/feedback_modal.dart'; import 'package:rtu_mirea_app/service_locator.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:rtu_mirea_app/presentation/typography.dart'; @@ -203,6 +206,26 @@ class AboutAppPage extends StatelessWidget { ), ], ), + const SizedBox(height: 16), + SizedBox( + height: 40, + width: double.infinity, + child: ColorfulButton( + text: 'Сообщить об ошибке', + backgroundColor: AppTheme.colors.colorful07.withBlue(180), + onClick: () { + final userBloc = context.read(); + + userBloc.state.maybeMap( + logInSuccess: (value) => FeedbackBottomModalSheet.show( + context, + defaultEmail: value.user.email, + ), + orElse: () => FeedbackBottomModalSheet.show(context), + ); + }, + ), + ), const SizedBox(height: 24), Text('Контрибьюторы', style: AppTextStyle.h4), const SizedBox(height: 16), diff --git a/lib/presentation/pages/schedule/schedule_page.dart b/lib/presentation/pages/schedule/schedule_page.dart index 9f8e3d7d..36cf73cf 100644 --- a/lib/presentation/pages/schedule/schedule_page.dart +++ b/lib/presentation/pages/schedule/schedule_page.dart @@ -4,12 +4,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rtu_mirea_app/domain/entities/schedule.dart'; import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_drawer.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_modal.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/settings_switch_button.dart'; +import '../../widgets/feedback_modal.dart'; import 'widgets/schedule_page_view.dart'; import 'package:rtu_mirea_app/presentation/typography.dart'; @@ -263,6 +265,60 @@ class _SchedulePageState extends State { context.router.push(const GroupsSelectRoute()), ), ), + Material( + color: Colors.transparent, + child: InkWell( + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 20), + child: Row( + children: [ + SvgPicture.asset( + 'assets/icons/social-sharing.svg', + height: 16, + width: 16, + ), + const SizedBox(width: 20), + Text("Проблемы с расписанием", + style: AppTextStyle.buttonL), + ], + ), + ), + Opacity( + opacity: 0.05, + child: Container( + width: double.infinity, + height: 1, + color: Colors.white, + ), + ), + ], + ), + onTap: () { + final defaultText = state is ScheduleLoaded + ? 'Возникла проблема с расписанием группы ${state.activeGroup}:\n\n' + : null; + + final userBloc = context.read(); + + userBloc.state.maybeMap( + logInSuccess: (value) => + FeedbackBottomModalSheet.show( + context, + defaultText: defaultText, + defaultEmail: value.user.email, + ), + orElse: () => FeedbackBottomModalSheet.show( + context, + defaultText: defaultText, + ), + ); + }, + ), + ), + if (state is ScheduleLoaded) Expanded( child: Column( diff --git a/lib/presentation/widgets/feedback_modal.dart b/lib/presentation/widgets/feedback_modal.dart new file mode 100644 index 00000000..95f81afe --- /dev/null +++ b/lib/presentation/widgets/feedback_modal.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'package:sentry/sentry.dart'; + +import '../theme.dart'; +import '../typography.dart'; +import 'buttons/primary_button.dart'; + +class FeedbackBottomModalSheet extends StatefulWidget { + const FeedbackBottomModalSheet({ + Key? key, + this.onConfirm, + this.defaultText, + this.defaultEmail, + }) : super(key: key); + + final String? defaultEmail; + final String? defaultText; + final VoidCallback? onConfirm; + + static void show( + BuildContext context, { + String? defaultEmail, + String? defaultText, + VoidCallback? onConfirm, + }) { + showModalBottomSheet( + isDismissible: true, + isScrollControlled: true, + backgroundColor: AppTheme.colors.background02, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + context: context, + builder: (context) => SafeArea( + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: FeedbackBottomModalSheet( + defaultEmail: defaultEmail, + defaultText: defaultText, + onConfirm: () { + onConfirm?.call(); + Navigator.pop(context); + }, + ), + ), + ), + ); + } + + @override + State createState() => + _FeedbackBottomModalSheetState(); +} + +class _FeedbackBottomModalSheetState extends State { + @override + void initState() { + super.initState(); + _emailController.text = widget.defaultEmail ?? ''; + _textController.text = widget.defaultText ?? ''; + } + + final _emailController = TextEditingController(); + final _textController = TextEditingController(); + + String? _emailErrorText; + String? _textErrorText; + + final _reEmail = RegExp( + r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$', + ); + + void _sendFeedback() async { + final email = _emailController.text; + final text = _textController.text; + + if (email == null) { + setState(() { + _emailErrorText = 'Введите email'; + }); + return; + } + + if (text == null) { + setState(() { + _textErrorText = 'Введите текст'; + }); + return; + } + + if (!_reEmail.hasMatch(email)) { + setState(() { + _emailErrorText = 'Некорректный email'; + }); + return; + } + + if (text.isEmpty) { + setState(() { + _textErrorText = 'Введите текст'; + }); + return; + } + + setState(() { + _emailErrorText = null; + _textErrorText = null; + }); + + final SentryId sentryId = await Sentry.captureMessage(text); + + final userFeedback = SentryUserFeedback( + eventId: sentryId, + email: email, + comments: text, + ); + + Sentry.captureUserFeedback(userFeedback).then((value) { + final message = 'Отзыв отправлен. Код ошибки: $sentryId'; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: AppTheme.colors.primary.withOpacity(0.8), + content: Text(message, style: AppTextStyle.captionL), + duration: const Duration(seconds: 3), + ), + ); + }); + widget.onConfirm?.call(); + } + + @override + Widget build(BuildContext context) { + final border = OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.transparent, + width: 0, + ), + ); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Text( + 'Оставить отзыв', + style: AppTextStyle.h5, + ), + const SizedBox(height: 8), + Text( + 'Кажется, у вас что-то пошло не так. Пожалуйста, напишите нам, и мы постараемся исправить это. Мы свяжемся по указанному email адресу для уточнения деталей.', + style: AppTextStyle.captionL.copyWith( + color: AppTheme.colors.deactive, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ]), + Text( + 'Email', + style: AppTextStyle.chip.copyWith( + color: AppTheme.colors.deactive, + ), + ), + const SizedBox(height: 8), + TextField( + decoration: InputDecoration( + errorText: _emailErrorText, + errorStyle: AppTextStyle.captionL.copyWith( + color: AppTheme.colors.colorful07, + ), + hintText: 'Введите email', + hintStyle: AppTextStyle.titleS.copyWith( + color: AppTheme.colors.deactive, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.colors.primary, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.colors.colorful07, + ), + ), + disabledBorder: border, + enabledBorder: border, + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.colors.colorful07, + ), + ), + fillColor: AppTheme.colors.background01, + filled: true, + ), + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + style: AppTextStyle.titleS, + controller: _emailController, + ), + const SizedBox(height: 24), + Text( + 'Что случилось?', + style: AppTextStyle.chip.copyWith( + color: AppTheme.colors.deactive, + ), + ), + const SizedBox(height: 8), + TextField( + keyboardType: TextInputType.multiline, + maxLines: 5, + controller: _textController, + decoration: InputDecoration( + hintText: 'Когда я нажимаю "Х" происходит "У"', + hintStyle: AppTextStyle.bodyL.copyWith( + color: AppTheme.colors.deactive, + ), + errorText: _textErrorText, + errorStyle: AppTextStyle.captionS.copyWith( + color: AppTheme.colors.colorful07, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.colors.primary, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.colors.colorful07, + ), + ), + disabledBorder: border, + enabledBorder: border, + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.colors.colorful07, + ), + ), + fillColor: AppTheme.colors.background01, + filled: true, + ), + textInputAction: TextInputAction.done, + style: AppTextStyle.bodyL, + ), + const SizedBox(height: 24), + PrimaryButton( + text: 'Отправить', + onClick: _sendFeedback, + ), + ], + ), + ); + } +}