Files
eatvio_app/lib/features/auth/presentation/pages/auth_screen.dart
2025-09-17 17:00:44 +03:00

454 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ignore_for_file: unused_element, duplicate_ignore
import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.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:url_launcher/url_launcher.dart';
import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
final emailController = TextEditingController();
final passwordController = TextEditingController();
bool checkboxState = false;
bool hidePassword = true;
final authCubit = sl<AuthCubit>();
late StreamSubscription<bool> keyboardSubscription;
bool isFocused = false;
int tapsCount = 0;
@override
void initState() {
super.initState();
var keyboardVisibilityController = KeyboardVisibilityController();
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
if (mounted) {
setState(() {
isFocused = visible;
});
}
});
}
@override
void dispose() {
keyboardSubscription.cancel();
super.dispose();
}
Future<void> _reconnectWebSocket(BuildContext context) async {
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
if (context.mounted) {
try {
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход');
} catch (e) {
rethrow;
}
}
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
@override
Widget build(BuildContext context) {
var shortestSide = MediaQuery.sizeOf(context).shortestSide;
final bool useMobileLayout = shortestSide < 600;
if (!useMobileLayout) isFocused = true;
final s = S.of(context);
FlutterNativeSplash.remove();
return BlocProvider<AuthCubit>(
create: (BuildContext context) => authCubit,
child: BlocBuilder<AuthCubit, AuthState>(
builder: (BuildContext context, AuthState state) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
toolbarHeight: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
title: GestureDetector(
onTap: () {
tapsCount++;
if (tapsCount == 7) {
tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
},
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.email),
const SizedBox(height: 4),
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),
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),
Center(
child: GestureDetector(
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
child: Text(
'Забыли пароль?',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Нет аккаунта? '),
GestureDetector(
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
child: Text(
'Зарегистрироваться',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
],
),
// const SizedBox(height: 16),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// _SocialButton(
// onTap: _onVkPressed,
// backgroundColor: const Color(0xFF2787F5),
// tooltip: 'VK ID',
// child: SvgPicture.asset(
// 'assets/icons/vk.svg',
// height: 24,
// width: 24,
// colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
// ),
// ),
// _SocialButton(
// onTap: _onOkPressed,
// backgroundColor: const Color(0xFFF58220),
// tooltip: 'Одноклассники',
// child: SvgPicture.asset(
// 'assets/icons/ok.svg',
// height: 24,
// width: 24,
// colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
// ),
// ),
// const _SocialButton(
// onTap: _onMailRuPressed,
// backgroundColor: Color(0xFF005FF9),
// tooltip: 'Mail.ru',
// child: Text(
// 'Mail',
// style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
// ),
// ),
// const _SocialButton(
// onTap: _onYandexPressed,
// backgroundColor: Color(0xFFFFCC00),
// tooltip: 'Яндекс ID',
// child: Text(
// 'Yandex',
// style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700),
// ),
// ),
// ],
// ),
const Spacer(),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
style: const TextStyle(fontSize: 12, color: Colors.black),
children: [
const TextSpan(text: 'c '),
if (!hidePlans) ...[
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 TextSpan(text: ' и '),
],
TextSpan(
text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
),
],
),
),
),
const SizedBox(height: 16),
Row(
children: [
SizedBox(
height: 48,
width: MediaQuery.of(context).size.width - 88,
child: FloatingActionButton.extended(
heroTag: 'authButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
}
}
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
const SizedBox(width: 8),
SizedBox(
height: 48,
width: 48,
child: FloatingActionButton.extended(
heroTag: 'scanQrCodeButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
await showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
Text(
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
AppRouter.navigatorKey.currentContext?.pop();
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: const Text(
'Отмена',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
),
),
),
const SizedBox(width: 8),
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
AppRouter.navigatorKey.currentContext?.pop();
AppRouter.navigatorKey.currentContext?.pushNamed('scanQRCodeScreen');
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Text(
'Перейти',
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
),
),
),
],
),
],
),
),
);
},
);
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),
),
],
),
],
),
),
),
);
},
),
);
}
}
// ignore: unused_element
class _SocialButton extends StatelessWidget {
const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip});
final VoidCallback onTap;
final Widget child;
final Color backgroundColor;
final String tooltip;
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Ink(
height: 48,
width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4,
decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)),
child: Center(child: child),
),
),
);
}
}
// ignore: unused_element
void _onVkPressed() async {
final accessToken = await SocialAuthFacade.signInWithVk();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'vk', accessToken: accessToken);
if (isAuthenticated) {
if (AppRouter.navigatorKey.currentContext != null) {
// Reuse success flow
// ignore: use_build_context_synchronously
await _reconnectAfterAuth();
}
}
}
void _onOkPressed() async {
final accessToken = await SocialAuthFacade.signInWithOk();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'ok', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onMailRuPressed() async {
final accessToken = await SocialAuthFacade.signInWithMailRu();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'mailru', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onYandexPressed() async {
final accessToken = await SocialAuthFacade.signInWithYandex();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'yandex', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
Future<void> _reconnectAfterAuth() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход');
} catch (_) {}
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}