forked from eatvio/eatvio_app
Merge pull request 'Issue-691' (#719) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#719
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/other_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/core/routing/settings_routes.dart';
|
||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -50,4 +51,8 @@ class AppRouter {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {
|
||||
appRouter.goNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
|
||||
@@ -75,7 +76,7 @@ class CubitRegistrations {
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
|
||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl()));
|
||||
// Feed related cubits
|
||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
|
||||
@@ -32,6 +34,14 @@ class AuthRoutes {
|
||||
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
|
||||
),
|
||||
GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
|
||||
GoRoute(
|
||||
path: RouteConstants.registerViaSocialScreen,
|
||||
name: 'registerViaSocialScreen',
|
||||
builder: (context, state) {
|
||||
final extra = state.extra as Map<String, dynamic>?;
|
||||
return RegisterViaSocialScreen(authResult: extra?['authResult'] as AuthResultEntity?, provider: extra?['provider'] as String?);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class RouteConstants {
|
||||
static const String scanQRCodeScreen = 'scan-qr-code-screen';
|
||||
static const String verifyScreen = '/verify-screen';
|
||||
static const String verifyEmailScreen = '/verify-email-screen';
|
||||
static const String registerViaSocialScreen = '/register-via-social-screen';
|
||||
|
||||
// Main navigation routes
|
||||
static const String myProfileScreen = '/my-profile-screen';
|
||||
|
||||
24
lib/features/auth/models/oauth_model.dart
Normal file
24
lib/features/auth/models/oauth_model.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'oauth_model.freezed.dart';
|
||||
part 'oauth_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class OAuthModel with _$OAuthModel {
|
||||
const factory OAuthModel({
|
||||
required String provider,
|
||||
required String id,
|
||||
@JsonKey(name: 'access_token') required String accessToken,
|
||||
@JsonKey(name: 'refresh_token') required String refreshToken,
|
||||
@JsonKey(name: 'first_name') required String firstName,
|
||||
@JsonKey(name: 'last_name') required String lastName,
|
||||
required String email,
|
||||
required String phone,
|
||||
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
|
||||
required String gender,
|
||||
required int weight,
|
||||
required int height,
|
||||
}) = _OAuthModel;
|
||||
|
||||
factory OAuthModel.fromJson(Map<String, dynamic> json) => _$OAuthModelFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class OAuthCubit extends Cubit<OAuthState> {
|
||||
final AuthRepository authRepository;
|
||||
|
||||
OAuthCubit({required this.authRepository}) : super(const OAuthState());
|
||||
|
||||
Future<void> getOAuthToken({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
required String id,
|
||||
required String refreshToken,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
required String phone,
|
||||
required String dateOfBirth,
|
||||
required String gender,
|
||||
required int weight,
|
||||
required int height,
|
||||
}) async {
|
||||
try {
|
||||
final token = await authRepository.getOAuthToken(
|
||||
provider: provider,
|
||||
accessToken: accessToken,
|
||||
id: id,
|
||||
refreshToken: refreshToken,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
email: email,
|
||||
phone: phone,
|
||||
dateOfBirth: dateOfBirth,
|
||||
gender: gender,
|
||||
weight: weight,
|
||||
height: height,
|
||||
);
|
||||
emit(OAuthState.loaded(token: token));
|
||||
} catch (e) {
|
||||
emit(OAuthState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'oauth_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class OAuthState with _$OAuthState {
|
||||
const factory OAuthState() = _OAuthState;
|
||||
|
||||
const factory OAuthState.loading() = OAuthStateLoading;
|
||||
|
||||
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
|
||||
|
||||
const factory OAuthState.error({required String errorMessage}) = OAuthStateError;
|
||||
}
|
||||
@@ -391,10 +391,14 @@ class _SocialButton extends StatelessWidget {
|
||||
|
||||
void _onVkPressed() async {
|
||||
try {
|
||||
final accessToken = await SocialAuthFacade.signInWithVk();
|
||||
if (accessToken == null) {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
|
||||
if (authResult == null) {
|
||||
return;
|
||||
}
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'vk');
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
@@ -402,10 +406,14 @@ void _onVkPressed() async {
|
||||
|
||||
void _onOkPressed() async {
|
||||
try {
|
||||
final accessToken = await SocialAuthFacade.signInWithOk();
|
||||
if (accessToken == null) {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile();
|
||||
if (authResult == null) {
|
||||
return;
|
||||
}
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'ok');
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
@@ -413,18 +421,27 @@ void _onOkPressed() async {
|
||||
|
||||
void _onMailRuPressed() async {
|
||||
try {
|
||||
final accessToken = await SocialAuthFacade.signInWithMailRu();
|
||||
if (accessToken == null) {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
|
||||
final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
|
||||
if (authResult == null) {
|
||||
return;
|
||||
}
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'mailru');
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onYandexPressed() async {
|
||||
final accessToken = await SocialAuthFacade.signInWithYandex();
|
||||
if (accessToken == null) return;
|
||||
try {
|
||||
final authResult = await SocialAuthFacade.signInWithYandex();
|
||||
if (authResult == null) return;
|
||||
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: 'yandex');
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reconnectAfterAuth() async {
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
|
||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class RegisterViaSocialScreen extends StatefulWidget {
|
||||
final AuthResultEntity? authResult;
|
||||
final String? provider;
|
||||
|
||||
const RegisterViaSocialScreen({super.key, this.authResult, this.provider});
|
||||
|
||||
@override
|
||||
State<RegisterViaSocialScreen> createState() => _RegisterViaSocialScreenState();
|
||||
}
|
||||
|
||||
class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')});
|
||||
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')});
|
||||
|
||||
// Контроллеры для полей ввода
|
||||
final firstNameController = TextEditingController();
|
||||
final lastNameController = TextEditingController();
|
||||
final emailController = TextEditingController();
|
||||
final phoneController = TextEditingController();
|
||||
final dateOfBirthController = TextEditingController();
|
||||
final weightController = TextEditingController();
|
||||
final heightController = TextEditingController();
|
||||
|
||||
// Состояние для выбора пола
|
||||
bool isMaleSelected = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeFieldsFromAuthResult();
|
||||
}
|
||||
|
||||
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.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
|
||||
void _initializeFieldsFromAuthResult() {
|
||||
if (widget.authResult != null) {
|
||||
final authResult = widget.authResult!;
|
||||
|
||||
// Логируем полученные данные для отладки
|
||||
print('AuthResult data:');
|
||||
print('firstName: ${authResult.firstName}');
|
||||
print('lastName: ${authResult.lastName}');
|
||||
print('email: ${authResult.email}');
|
||||
print('phone: ${authResult.phone}');
|
||||
print('birthDate: ${authResult.birthDate}');
|
||||
print('gender: ${authResult.gender}');
|
||||
print('provider: ${widget.provider}');
|
||||
|
||||
// Предзаполняем поля из AuthResultEntity
|
||||
if (authResult.firstName != null) {
|
||||
firstNameController.text = authResult.firstName!;
|
||||
}
|
||||
if (authResult.lastName != null) {
|
||||
lastNameController.text = authResult.lastName!;
|
||||
}
|
||||
if (authResult.email != null) {
|
||||
emailController.text = authResult.email!;
|
||||
}
|
||||
if (authResult.phone != null) {
|
||||
phoneController.text = authResult.phone!;
|
||||
}
|
||||
if (authResult.birthDate != null) {
|
||||
dateOfBirthController.text = _formatBirthDate(authResult.birthDate!);
|
||||
}
|
||||
if (authResult.gender != null) {
|
||||
isMaleSelected = authResult.gender == GenderEnum.male;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatBirthDate(String birthDate) {
|
||||
try {
|
||||
// Пытаемся распарсить различные форматы даты
|
||||
DateTime? date;
|
||||
|
||||
// Если дата в формате ISO (YYYY-MM-DD)
|
||||
if (birthDate.contains('-')) {
|
||||
date = DateTime.tryParse(birthDate);
|
||||
}
|
||||
// Если дата в формате DD.MM.YYYY
|
||||
else if (birthDate.contains('.')) {
|
||||
final parts = birthDate.split('.');
|
||||
if (parts.length == 3) {
|
||||
final day = int.tryParse(parts[0]);
|
||||
final month = int.tryParse(parts[1]);
|
||||
final year = int.tryParse(parts[2]);
|
||||
if (day != null && month != null && year != null) {
|
||||
date = DateTime(year, month, day);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (date != null) {
|
||||
// Форматируем в DD.MM.YYYY
|
||||
final day = date.day.toString().padLeft(2, '0');
|
||||
final month = date.month.toString().padLeft(2, '0');
|
||||
final year = date.year.toString();
|
||||
return '$day.$month.$year';
|
||||
}
|
||||
} catch (e) {
|
||||
// Если не удалось распарсить, возвращаем исходную строку
|
||||
}
|
||||
|
||||
return birthDate;
|
||||
}
|
||||
|
||||
String _getProviderName(String? provider) {
|
||||
switch (provider) {
|
||||
case 'vk':
|
||||
return 'ВКонтакте';
|
||||
case 'yandex':
|
||||
return 'Яндекс';
|
||||
case 'ok':
|
||||
return 'Одноклассники';
|
||||
case 'mailru':
|
||||
return 'Mail.ru';
|
||||
default:
|
||||
return 'социальной сети';
|
||||
}
|
||||
}
|
||||
|
||||
String _getDataStatusMessage(AuthResultEntity authResult) {
|
||||
final hasEmail = authResult.email != null && authResult.email!.isNotEmpty;
|
||||
final hasPhone = authResult.phone != null && authResult.phone!.isNotEmpty;
|
||||
final hasBirthDate = authResult.birthDate != null && authResult.birthDate!.isNotEmpty;
|
||||
|
||||
if (hasEmail && hasPhone && hasBirthDate) {
|
||||
return 'Все данные успешно получены и предзаполнены.';
|
||||
} else if (hasEmail && hasPhone) {
|
||||
return 'Email и номер телефона получены. Дата рождения может быть недоступна из-за настроек приватности.';
|
||||
} else if (hasEmail) {
|
||||
return 'Email получен. Номер телефона и дата рождения могут быть недоступны из-за настроек приватности.';
|
||||
} else if (hasPhone) {
|
||||
return 'Номер телефона получен. Email и дата рождения могут быть недоступны из-за настроек приватности.';
|
||||
} else {
|
||||
// Специальное сообщение для VK
|
||||
if (widget.provider == 'vk') {
|
||||
return 'Основные данные получены. Email запрашивается автоматически. Номер телефона получается через VK API. Если номер замаскирован, это означает, что пользователь ограничил доступ к нему в настройках приватности.';
|
||||
} else {
|
||||
return 'Основные данные получены. Email, номер телефона и дата рождения могут быть недоступны из-за настроек приватности.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
firstNameController.dispose();
|
||||
lastNameController.dispose();
|
||||
emailController.dispose();
|
||||
phoneController.dispose();
|
||||
dateOfBirthController.dispose();
|
||||
weightController.dispose();
|
||||
heightController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<OAuthCubit>(
|
||||
create: (context) => CubitRegistrations.sl<OAuthCubit>(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(toolbarHeight: 0),
|
||||
body: BlocBuilder<OAuthCubit, OAuthState>(
|
||||
builder: (context, state) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Регистрация',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Имя'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: firstNameController, hintText: 'Введите имя', onChanged: (newValue) {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Фамилия'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: lastNameController, hintText: 'Введите фамилию', onChanged: (newValue) {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Почта'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: emailController,
|
||||
textInputType: TextInputType.emailAddress,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
hintText: 'Введите email',
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (newValue) {},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Номер телефона'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
textInputAction: TextInputAction.done,
|
||||
hintText: '+7 (999) 999 99 99',
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [maskFormatter],
|
||||
onChanged: (newValue) {},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Дата рождения'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
textInputAction: TextInputAction.done,
|
||||
hintText: '01.01.2000',
|
||||
controller: dateOfBirthController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [maskFormatterForBirthday],
|
||||
onChanged: (newValue) {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Пол'),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _genderButton('М', isMaleSelected)),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(child: _genderButton('Ж', !isMaleSelected)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Вес'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Рост'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => _handleRegistration(context),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(color: state is OAuthStateLoading ? Colors.grey : appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||
child: Center(
|
||||
child: state is OAuthStateLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text(
|
||||
'Зарегистрироваться',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: 'c ',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
if (!hidePlans) ...[
|
||||
TextSpan(
|
||||
text: ' Публичной офертой',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' и ',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
],
|
||||
TextSpan(
|
||||
text: 'Политикой конфиденциальности и защитой персональных данных',
|
||||
style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Показываем ошибки если есть
|
||||
if (state is OAuthStateError) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Text(
|
||||
state.errorMessage,
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Обработка успешной регистрации
|
||||
if (state is OAuthStateLoaded) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.shade200),
|
||||
),
|
||||
child: const Text(
|
||||
'Регистрация успешно завершена!',
|
||||
style: TextStyle(color: Colors.green),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
// Автоматический переход на главный экран после успешной регистрации
|
||||
Builder(
|
||||
builder: (context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_reconnectWebSocket(context);
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _genderButton(String gender, bool isActive) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isMaleSelected = gender == 'М';
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF6F6F6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: isActive ? Border.all(width: 2, color: appTheme.blueA400) : null,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
gender,
|
||||
style: TextStyle(color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleRegistration(BuildContext context) {
|
||||
// Валидация полей
|
||||
if (firstNameController.text.isEmpty ||
|
||||
lastNameController.text.isEmpty ||
|
||||
emailController.text.isEmpty ||
|
||||
phoneController.text.isEmpty ||
|
||||
dateOfBirthController.text.isEmpty ||
|
||||
weightController.text.isEmpty ||
|
||||
heightController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Пожалуйста, заполните все поля')));
|
||||
return;
|
||||
}
|
||||
|
||||
// Парсим числовые значения
|
||||
int? weight;
|
||||
int? height;
|
||||
try {
|
||||
weight = int.parse(weightController.text);
|
||||
height = int.parse(heightController.text);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Вес и рост должны быть числами')));
|
||||
return;
|
||||
}
|
||||
|
||||
// Вызываем OAuth регистрацию
|
||||
context.read<OAuthCubit>().getOAuthToken(
|
||||
provider: widget.provider ?? 'social',
|
||||
accessToken: widget.authResult?.accessToken ?? 'dummy_token',
|
||||
id: widget.authResult?.userId ?? 'dummy_id',
|
||||
refreshToken: widget.authResult?.refreshToken ?? 'dummy_refresh_token',
|
||||
firstName: firstNameController.text,
|
||||
lastName: lastNameController.text,
|
||||
email: emailController.text,
|
||||
phone: phoneController.text,
|
||||
dateOfBirth: dateOfBirthController.text,
|
||||
gender: isMaleSelected ? 'М' : 'Ж',
|
||||
weight: weight,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
||||
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
|
||||
@@ -8,26 +9,17 @@ class AuthRepository {
|
||||
|
||||
AuthRepository({required this.authService});
|
||||
|
||||
Future<TokenModel> getTokenByCredentials(
|
||||
String email,
|
||||
String password,
|
||||
) async {
|
||||
Future<TokenModel> getTokenByCredentials(String email, String password) async {
|
||||
try {
|
||||
return await authService.getTokenByCredentials(
|
||||
credentials: CredentialsModel(
|
||||
email: email,
|
||||
password: password,
|
||||
),
|
||||
credentials: CredentialsModel(email: email, password: password),
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<TokenModel> getTokenBySocial({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
}) async {
|
||||
Future<TokenModel> getTokenBySocial({required String provider, required String accessToken}) async {
|
||||
try {
|
||||
return await authService.getTokenBySocial(
|
||||
body: SocialExchangeRequest(provider: provider, accessToken: accessToken),
|
||||
@@ -36,4 +28,40 @@ class AuthRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getOAuthToken({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
required String id,
|
||||
required String refreshToken,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
required String phone,
|
||||
required String dateOfBirth,
|
||||
required String gender,
|
||||
required int weight,
|
||||
required int height,
|
||||
}) async {
|
||||
try {
|
||||
return (await authService.getOAuthToken(
|
||||
body: OAuthModel(
|
||||
provider: provider,
|
||||
accessToken: accessToken,
|
||||
id: id,
|
||||
refreshToken: refreshToken,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
email: email,
|
||||
phone: phone,
|
||||
dateOfBirth: dateOfBirth,
|
||||
gender: gender == 'М' ? 'male' : 'female',
|
||||
weight: weight,
|
||||
height: height,
|
||||
),
|
||||
))['token']! as String;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class SocialAuthFacade {
|
||||
|
||||
try {
|
||||
final controller = VkIDController(clID: int.parse(clientIdStr));
|
||||
final authUri = controller.generateAuthorizeLinkWithCodeVerifier();
|
||||
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: VkProvider.vkID, scopes: [VkScope.email, VkScope.phone]);
|
||||
|
||||
final authUrl = authUri.value.toString();
|
||||
final codeVerifier = authUri.key;
|
||||
@@ -105,18 +105,16 @@ class SocialAuthFacade {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<String?> signInWithOk() async {
|
||||
static Future<AuthResultEntity?> signInWithOkAndFetchProfile() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return null;
|
||||
final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu);
|
||||
return vkAuthRes?.accessToken;
|
||||
return await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu);
|
||||
}
|
||||
|
||||
static Future<String?> signInWithMailRu() async {
|
||||
static Future<AuthResultEntity?> signInWithMailRuAndFetchProfile() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return null;
|
||||
final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu);
|
||||
return vkAuthRes?.accessToken;
|
||||
return await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu);
|
||||
}
|
||||
|
||||
static Future<AuthResultEntity?> signInWithYandex() async {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
@@ -19,4 +20,9 @@ abstract class AuthService {
|
||||
Future<TokenModel> getTokenBySocial({
|
||||
@Body() required SocialExchangeRequest body,
|
||||
});
|
||||
|
||||
@POST('/api/oauth/callback')
|
||||
Future<Map<String, Object?>> getOAuthToken({
|
||||
@Body() required OAuthModel body,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user