fix: reg, verify, loaders, grey screen when like

This commit is contained in:
mso7nueh
2025-07-30 12:34:27 +03:00
parent aacbc0ce5a
commit 09a1ec0947
13 changed files with 260 additions and 144 deletions

View File

@@ -243,6 +243,7 @@ class DiaryCubit extends Cubit<DiaryState> {
required int quantity,
int? productNutritionalValueId,
}) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,

View File

@@ -29,6 +29,7 @@ abstract class DiaryState with _$DiaryState {
@Default(false) bool isDiaryCommented,
@Default(false) bool isWakeUpAndRetireEnabled,
@Default(false) bool isMoodEnabled,
@Default(false) bool isLoadingAddingProductInMeal,
}) = DiaryStateLoaded;
const factory DiaryState.loadedTomorrowWithDiet({

View File

@@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ProductLoaderWidget extends StatefulWidget {
const ProductLoaderWidget({
super.key,
});
@override
State<ProductLoaderWidget> createState() => _ProductLoaderWidgetState();
}
class _ProductLoaderWidgetState extends State<ProductLoaderWidget> {
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 0,
contentPadding: const EdgeInsets.only(right: 24, left: 12),
title: const CupertinoActivityIndicator(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: const Color(0xFFF6FAFF),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -25,6 +26,11 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
],
if (state.isLoadingAddingProductInMeal)
const Padding(
padding: EdgeInsets.only(top: 8),
child: ProductLoaderWidget(),
),
],
);
}

View File

@@ -14,6 +14,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "email") required String email,
@JsonKey(name: "rating_place") int? ratingPlace,
@JsonKey(name: "email_verified_at") DateTime? emailVerifiedAt,
@JsonKey(name: "phone_confirmation_state") required String phoneConfirmationState,
@JsonKey(name: "created_at") DateTime? createdAt,
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,

View File

@@ -138,7 +138,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_header(state),
if (state.profile.roles.contains('consultant'))
if (state.profile.roles.contains('consultant') && state.consultantInfo != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -202,10 +202,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.roles.contains('consultant')) _socialLinks(state),
if (state.profile.roles.contains('consultant')) _education(state),
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant')) _activePlans(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _activePlans(state),
_blogPosts(state),
],
),
@@ -216,7 +216,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
Scaffold _myProfileScreen(ProfileStateLoaded state) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
appBar: AppBar(),
appBar: AppBar(
toolbarHeight: widget.author == null ? 0 : null,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -644,7 +646,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
SingleChildRenderObjectWidget _socialLinks(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant')) {
if (!state.profile.roles.contains('consultant') && state.consultantInfo != null) {
return const SizedBox.shrink();
}
return Padding(

View File

@@ -60,10 +60,11 @@ class RegistrationCubit extends Cubit<RegistrationState> {
),
);
await TokenStorage.saveTokenInStorage(token);
AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen');
await sl<FirebaseApi>().registerPushToken();
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(RegistrationState(sheet: state.sheet, error: e.toString()));
}
}

View File

@@ -7,5 +7,6 @@ part 'registration_state.freezed.dart';
abstract class RegistrationState with _$RegistrationState {
const factory RegistrationState({
required UserRegistrationSheet sheet,
String? error,
}) = _RegistrationState;
}

View File

@@ -43,142 +43,161 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>(
create: (BuildContext context) => registrationCubit,
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
child: BlocListener<RegistrationCubit, RegistrationState>(
listener: (context, state) {
if (state.error != null && state.error!.isNotEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(state.error!),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
),
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
],
),
);
}
},
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.nextPage(
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 15'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 15,
end: progressValue,
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
setState(() {
pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 15'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 15,
end: progressValue,
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
),
),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
),
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
@@ -19,6 +20,7 @@ class VerifyScreen extends StatefulWidget {
class _VerifyScreenState extends State<VerifyScreen> {
late final WebViewController _controller;
bool _isLoading = true;
@override
void initState() {
@@ -27,6 +29,11 @@ class _VerifyScreenState extends State<VerifyScreen> {
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
setState(() {
_isLoading = false;
});
},
onNavigationRequest: (NavigationRequest request) async {
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
GoRouter.of(context).pop();
@@ -105,11 +112,17 @@ class _VerifyScreenState extends State<VerifyScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 0,
),
appBar: AppBar(toolbarHeight: 0),
body: SafeArea(
child: WebViewWidget(controller: _controller),
child: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(
child: Loader(),
),
],
),
),
);
}

View File

@@ -1,7 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
import 'package:eatvio_app/features/registration/models/location_model.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
@@ -14,13 +12,55 @@ class RegistrationRepository {
Future<String> tryRegister(UserRegistrationSheet sheet) async {
try {
final response = await registrationService.register(sheet: sheet.toJson());
try {
return (response['token']!);
} catch (e) {
showErrorToast(response.toString());
rethrow;
if (response.containsKey('token')) {
return response['token']!;
} else {
// Если нет токена, но есть message или errors
String errorMsg = '';
if (response.containsKey('message')) {
errorMsg += '${response['message']!}\n';
}
if (response.containsKey('errors')) {
final errors = response['errors'];
if (errors != null && errors is Map) {
(errors as Map).forEach((key, value) {
if (value is List && value.isNotEmpty) {
errorMsg += '${value.join('\n')}\n';
}
});
}
}
if (errorMsg.isNotEmpty) {
throw errorMsg.trim();
} else {
throw 'Ошибка регистрации: $response';
}
}
} on DioException catch (e) {
if (e.response != null && e.response?.statusCode == 422) {
final data = e.response?.data;
String errorMsg = '';
if (data is Map<String, dynamic>) {
if (data.containsKey('message')) {
errorMsg += data['message'] + '\n';
}
if (data.containsKey('errors')) {
final errors = data['errors'];
if (errors != null && errors is Map) {
(errors).forEach((key, value) {
if (value is List && value.isNotEmpty) {
errorMsg += '${value.join('\n')}\n';
}
});
}
}
}
if (errorMsg.isNotEmpty) {
throw errorMsg.trim();
} else {
throw 'Ошибка регистрации: $data';
}
}
} catch (e) {
rethrow;
}
}

View File

@@ -22,7 +22,11 @@ class SettingsCubit extends Cubit<SettingsState> {
final profile = await profileRepository.getMyProfile();
late final ConsultationRequestModel? consultantInfo;
try {
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
if (profile.phoneConfirmationState.contains('confirmed')) {
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
} else {
consultantInfo = null;
}
} catch (e) {
consultantInfo = null;
}

View File

@@ -19,6 +19,7 @@ void main() {
id: 0,
name: 'Alexander Avanesyan',
email: 'mso7nueh@mail.ru',
phoneConfirmationState: '',
isPublicProfile: true,
isEntitySubscribedByMe: false,
isBlockedByMe: false,