Merge remote-tracking branch 'upstream/main'

This commit is contained in:
misha
2026-02-11 16:26:08 +03:00
5 changed files with 81 additions and 16 deletions

View File

@@ -36,9 +36,29 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final emailErrors = errors?['email'] as List<dynamic>?;
final phoneErrors = errors?['phone'] as List<dynamic>?;
if ((emailErrors != null && emailErrors.isNotEmpty) || (phoneErrors != null && phoneErrors.isNotEmpty)) {
// Это ошибка валидации - нужно показать экран подтверждения
emit(const OAuthState.error(errorMessage: 'validation_error'));
final errorsList = <RegistrationValidationErrors>[];
if (emailErrors != null && emailErrors.isNotEmpty) {
for (final error in emailErrors) {
final errorStr = error.toString();
if (errorStr.contains('Некорректный формат email')) {
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
}
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
errorsList.add(RegistrationValidationErrors.emailExist);
}
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
for (final error in phoneErrors) {
final errorStr = error.toString();
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
errorsList.add(RegistrationValidationErrors.phoneExist);
}
}
}
if (errorsList.isNotEmpty) {
emit(OAuthState.error(errorMessage: 'validation_error', registrationValidationErrors: errorsList));
} else {
emit(OAuthState.error(errorMessage: e.toString()));
}
@@ -126,7 +146,7 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
try {
emit(const OAuthState.loading());
log('OAuthCubit: Requesting verification code for email: $email, phone: $phone');
final response = await authRepository.requestOAuthVerification(email: null, phone: phone);
final response = await authRepository.requestOAuthVerification(email: email, phone: phone);
if (response != null) {
log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}');
}
@@ -188,4 +208,3 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
}
}
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }

View File

@@ -10,5 +10,10 @@ abstract class OAuthState with _$OAuthState {
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
const factory OAuthState.error({required String errorMessage}) = OAuthStateError;
const factory OAuthState.error({
required String errorMessage,
List<RegistrationValidationErrors>? registrationValidationErrors,
}) = OAuthStateError;
}
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }

View File

@@ -58,6 +58,39 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
final GlobalKey _nameKey = GlobalKey();
final GlobalKey _durationKey = GlobalKey();
final GlobalKey _statusKey = GlobalKey();
final GlobalKey _typeKey = GlobalKey();
final GlobalKey _imageKey = GlobalKey();
final GlobalKey _planKey = GlobalKey();
void _scrollToFirstError(CreateDietState state) {
GlobalKey? key;
if (state.dietNameError != null) {
key = _nameKey;
} else if (state.dietDescriptionError != null) {
key = _mdKey;
} else if (state.dietDurationError != null) {
key = _durationKey;
} else if (state.dietStateError != null) {
key = _statusKey;
} else if (state.dietImageError != null) {
key = _imageKey;
} else if (state.dailyScheduleErrors.any((e) => e.isNotEmpty)) {
key = _planKey;
}
if (key != null && key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
alignment: 0.1,
);
}
}
@override
void initState() {
super.initState();
@@ -118,6 +151,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
children: [
const SizedBox(height: 16),
CustomTextFormField(
key: _nameKey,
hintText: s.enter_diet_name,
controller: nameController,
hasError: state.dietNameError != null,
@@ -143,19 +177,20 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
),
if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')],
const SizedBox(height: 12),
const DietDurationDropDownWidget(),
DietDurationDropDownWidget(key: _durationKey),
const SizedBox(height: 12),
StatusDropDownWidget(dietState: widget.diet?.state),
StatusDropDownWidget(key: _statusKey, dietState: widget.diet?.state),
const SizedBox(height: 16),
const DietTypeChoiceWidget(),
DietTypeChoiceWidget(key: _typeKey),
const SizedBox(height: 16),
const DietImagePickerWidget(),
DietImagePickerWidget(key: _imageKey),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 16),
MainBackgroundBouble(
key: _planKey,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
@@ -244,7 +279,11 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
? () {}
: () async {
final diet = await context.read<CreateDietCubit>().getDietForPreview();
if (diet == null) return;
if (diet == null) {
// ignore: use_build_context_synchronously
_scrollToFirstError(context.read<CreateDietCubit>().state);
return;
}
if (!context.mounted) return;
GoRouter.of(
context,
@@ -283,7 +322,10 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
}
} else {
final result = await context.read<CreateDietCubit>().createDiet();
if (result == 'error') {
if (result == 'validation') {
// ignore: use_build_context_synchronously
_scrollToFirstError(context.read<CreateDietCubit>().state);
} else if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_publish_diet} ${state.dietData.name}');
}

View File

@@ -436,11 +436,11 @@ class CreateDietCubit extends Cubit<CreateDietState> {
result = false;
emit(state.copyWith(dietStateError: 'Статус диеты не выбран'));
}
if (state.dietData.image == null) {
if (state.dietData.image == null && state.dietData.imageUrl == null) {
result = false;
emit(state.copyWith(dietImageError: 'Не добавлена обложка'));
}
result = _isDailyScheduleValid();
result = _isDailyScheduleValid() && result;
return result;
}

View File

@@ -287,8 +287,7 @@ void main() {
const verificationResponse = OAuthVerificationRequestResponse(email: 't***@example.com', status: 'sent');
// OAuthCubit всегда передает email: null в репозиторий, независимо от переданного параметра
when(mockAuthRepository.requestOAuthVerification(email: null, phone: null)).thenAnswer((_) async => verificationResponse);
when(mockAuthRepository.requestOAuthVerification(email: anyNamed('email'), phone: anyNamed('phone'))).thenAnswer((_) async => verificationResponse);
final result = await oauthCubit!.requestOAuthVerification(email: 'test@example.com');