feat(auth): Issue-691

This commit is contained in:
mso7nueh
2025-10-09 17:24:36 +03:00
parent 79de6ee6fa
commit 183fa012d2
11 changed files with 637 additions and 218 deletions

View File

@@ -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 {

View File

@@ -1,13 +1,29 @@
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 {
const RegisterViaSocialScreen({super.key});
final AuthResultEntity? authResult;
final String? provider;
const RegisterViaSocialScreen({super.key, this.authResult, this.provider});
@override
State<RegisterViaSocialScreen> createState() => _RegisterViaSocialScreenState();
@@ -17,201 +33,421 @@ 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 Scaffold(
appBar: AppBar(toolbarHeight: 0),
body: 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: TextEditingController(), hintText: 'Введите имя', onChanged: (newValue) {}),
],
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(width: 12),
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 24),
Row(
children: [
const Text('Фамилия'),
const SizedBox(height: 4),
CustomTextFormField(controller: TextEditingController(), hintText: 'Введите фамилию', onChanged: (newValue) {}),
],
),
),
],
),
const SizedBox(height: 12),
const Text('Почта'),
const SizedBox(height: 4),
CustomTextFormField(
controller: TextEditingController(),
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: TextEditingController(),
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: TextEditingController(),
keyboardType: TextInputType.number,
inputFormatters: [maskFormatterForBirthday],
onChanged: (newValue) {},
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(width: 12),
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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: [
const Text('Пол'),
const SizedBox(height: 4),
Row(
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: [
Expanded(child: _genderButton('М', true)),
const SizedBox(width: 4),
Expanded(child: _genderButton('Ж', false)),
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')),
),
],
),
],
),
),
],
),
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: TextEditingController(), 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: TextEditingController(), 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: () {},
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: const Center(
child: 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')),
),
// Показываем ошибки если есть
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),
),
const TextSpan(
text: ' и ',
style: TextStyle(fontSize: 16, color: Colors.black),
child: Text(
state.errorMessage,
style: TextStyle(color: Colors.red.shade700),
textAlign: TextAlign.center,
),
],
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 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();
},
),
],
],
),
),
],
);
},
),
),
);
@@ -219,29 +455,67 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
Widget _genderButton(String gender, bool isActive) {
return InkWell(
onTap: () {},
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,
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,
),
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,
);
}
}