Merge pull request 'main' (#661) from main into test-flight

Reviewed-on: eatvio/eatvio_app#661
This commit is contained in:
mso7nueh
2025-08-25 14:42:16 +00:00
16 changed files with 178 additions and 296 deletions

View File

@@ -396,7 +396,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 69;
CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -546,7 +546,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 69;
CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -584,7 +584,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 69;
CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>69</string>
<string>70</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -54,7 +54,6 @@ platform :ios do
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'"
)

View File

@@ -7,6 +7,9 @@ class EnvConfig {
// Sentry Configuration
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
// AppMetrica Configuration
static String get appMetricaApiKey => dotenv.env['APPMETRICA_KEY'] ?? '';
// Firebase Configuration
static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? '';
static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? '';

View File

@@ -47,12 +47,20 @@ class _AuthScreenState extends State<AuthScreen> {
super.initState();
var keyboardVisibilityController = KeyboardVisibilityController();
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
setState(() {
isFocused = visible;
});
if (mounted) {
setState(() {
isFocused = visible;
});
}
});
}
@override
void dispose() {
keyboardSubscription.cancel();
super.dispose();
}
Future<void> _reconnectWebSocket(BuildContext context) async {
try {
await sl<WsManager>().clear();

View File

@@ -14,12 +14,7 @@ class AddProductModal extends StatefulWidget {
final DiaryStateLoaded state;
final DiaryCubit diaryCubit;
const AddProductModal({
super.key,
this.portion,
required this.state,
required this.diaryCubit,
});
const AddProductModal({super.key, this.portion, required this.state, required this.diaryCubit});
@override
State<AddProductModal> createState() => _AddProductModalState();
@@ -58,75 +53,62 @@ class _AddProductModalState extends State<AddProductModal> {
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
return Padding(
padding: const EdgeInsets.all(16) +
EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Добавление блюда',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 32,
width: MediaQuery.of(context).size.width * 0.76 - 32,
child: CustomTextFormField(
controller: productNameController,
autofocus: true,
hintText: 'Введите название продукта/блюда',
onChanged: (query) {
EasyDebounce.debounce(
'glossary-search',
const Duration(milliseconds: 250),
() async {
selectedGlossary = null;
if (query.isEmpty) {
glossariesList = [];
setState(() {});
return;
}
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
selectedGlossary = null;
if (query.isEmpty) {
glossariesList = [];
setState(() {});
},
);
return;
}
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
setState(() {});
});
},
),
),
const SizedBox(width: 4),
SizedBox(
width: MediaQuery.of(context).size.width * 0.2 - 4,
width: MediaQuery.of(context).size.width * 0.24 - 4,
child: CustomTextFormField(
controller: quantityController,
keyboardType: TextInputType.number,
hintText: selectedGlossary == null
? '100'
: selectedGlossary!.grInPortion == 0
? '100'
: '1',
? '100'
: '1',
suffixText: selectedGlossary == null
? 'гр'
: selectedGlossary?.grInPortion == 0
? 'гр'
: 'шт',
? 'гр'
: 'шт',
),
),
],
),
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
Container(
constraints: const BoxConstraints(
maxHeight: 256,
),
constraints: const BoxConstraints(maxHeight: 256),
child: ListView.separated(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
@@ -141,28 +123,12 @@ class _AddProductModalState extends State<AddProductModal> {
setState(() {});
FocusManager.instance.primaryFocus?.nextFocus();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text(
glossariesList[i].name,
style: const TextStyle(
height: 1,
),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
subtitle: glossariesList[i].grInPortion == 0
? null
: Text(
'${glossariesList[i].grInPortion} г',
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
),
),
trailing: Icon(
Icons.add_rounded,
color: Colors.grey.shade400,
),
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
contentPadding: const EdgeInsets.only(left: 12),
);
},
@@ -188,14 +154,8 @@ class _AddProductModalState extends State<AddProductModal> {
child: Ink(
width: 52,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.red,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
),
),
if (widget.portion != null) const SizedBox(width: 8),
@@ -209,8 +169,8 @@ class _AddProductModalState extends State<AddProductModal> {
portion: selectedGlossary == null
? 'gr'
: selectedGlossary!.grInPortion == 0
? 'gr'
: 'pcs',
? 'gr'
: 'pcs',
quantity: int.parse(quantityController.text),
productNutritionalValueId: selectedGlossary?.id,
);
@@ -223,8 +183,8 @@ class _AddProductModalState extends State<AddProductModal> {
portion: selectedGlossary == null
? 'gr'
: selectedGlossary!.grInPortion == 0
? 'gr'
: 'pcs',
? 'gr'
: 'pcs',
quantity: int.parse(quantityController.text),
productNutritionalValueId: selectedGlossary?.id,
portionState: widget.portion!.portionState,
@@ -237,13 +197,8 @@ class _AddProductModalState extends State<AddProductModal> {
useRootNavigator: true,
context: context,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: GlossaryModerationRequestModalWidget(
productName: productNameController.text,
diaryCubit: widget.diaryCubit,
),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: widget.diaryCubit),
),
);
}
@@ -252,18 +207,11 @@ class _AddProductModalState extends State<AddProductModal> {
child: Ink(
width: MediaQuery.of(context).size.width - (widget.portion == null ? 32 : 92),
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: Center(
child: Text(
widget.portion == null ? 'Добавить' : 'Сохранить',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
),
),
),

View File

@@ -17,7 +17,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class DietsWidget extends StatefulWidget {
class DietsWidget extends StatefulWidget {
final Diet diet;
final int index;
const DietsWidget({

View File

@@ -8,62 +8,61 @@ import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class RegistrationCubit extends Cubit<RegistrationState> {
final RegistrationRepository registrationRepository;
RegistrationCubit({
required this.registrationRepository,
}) : super(
const RegistrationState(
sheet: UserRegistrationSheet(
firstName: '',
lastName: '',
email: '',
password: '',
passwordConfirmation: '',
birthdate: '',
gender: '',
location: '',
responseBody: '{}',
lang: 'Русский',
weight: 0,
height: 0,
chestCircumference: 0,
waistCircumference: 0,
hipCircumference: 0,
bloodPressureUp: 0,
bloodPressureDown: 0,
hasHypo: 0,
hasHyper: 0,
aboutAllergy: '',
aboutTakingPils: '',
aboutSportHobbies: '',
favoriteFoods: '',
unfavoriteFoods: '',
additionalInfo: '',
phone: '',
),
RegistrationCubit({required this.registrationRepository})
: super(
const RegistrationState(
sheet: UserRegistrationSheet(
firstName: '',
lastName: '',
email: '',
password: '',
passwordConfirmation: '',
birthdate: '',
gender: '',
location: '',
responseBody: '{}',
lang: 'Русский',
weight: 0,
height: 0,
chestCircumference: 0,
waistCircumference: 0,
hipCircumference: 0,
bloodPressureUp: 0,
bloodPressureDown: 0,
hasHypo: 0,
hasHyper: 0,
aboutAllergy: '',
aboutTakingPils: '',
aboutSportHobbies: '',
favoriteFoods: '',
unfavoriteFoods: '',
additionalInfo: '',
phone: '',
),
);
),
);
void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet));
Future<void> tryRegistration() async {
try {
final token = await registrationRepository.tryRegister(
state.sheet.copyWith(
phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''),
responseBody: '{}',
),
state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'),
);
await TokenStorage.saveTokenInStorage(token);
await sl<FirebaseApi>().registerPushToken();
RegistrationAnalyticsService.reportRegistrationComplete();
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
} catch (e, st) {
log(e.toString(), stackTrace: st);
RegistrationAnalyticsService.reportRegistrationError(e.toString());
emit(RegistrationState(sheet: state.sheet, error: e.toString()));
}
}

View File

@@ -1,13 +1,12 @@
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class VerifyEmailCubit extends Cubit<VerifyEmailState> {
final ProfileRepository profileRepository;
VerifyEmailCubit({
required this.profileRepository,
}) : super(const VerifyEmailState.loading());
VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading());
Future<void> sendEmailCode() async {
try {
@@ -24,6 +23,7 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
emit(const VerifyEmailState.verifyLoading());
final result = await profileRepository.tryVerifyEmailByCode(code);
if (result == true) {
RegistrationAnalyticsService.reportEmailVerification();
emit(const VerifyEmailState.verifySuccess());
} else {
emit(const VerifyEmailState.verifyError());

View File

@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/registration/presentation/pages/medicine_int
import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -51,12 +52,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(state.error!),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
}
@@ -76,22 +72,14 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
),
),
Container(
@@ -103,24 +91,16 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
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,
);
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
}
},
label: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
],
@@ -128,23 +108,11 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
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'),
),
],
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(
@@ -152,10 +120,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 15,
end: progressValue,
),
tween: Tween<double>(begin: 1 / 15, end: progressValue),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
@@ -175,6 +140,8 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
step = index + 1;
progressValue = step / 15;
});
// Report analytics for step completion
RegistrationAnalyticsService.reportRegistrationStep(step);
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
@@ -211,12 +178,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Введите корректный номер телефона'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
@@ -224,12 +186,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
return true;
}
case 2:
List<String> errors = validateCredentialsFields(
state.sheet.email,
state.sheet.password,
state.sheet.passwordConfirmation,
context,
);
List<String> errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context);
if (errors.isEmpty) {
return true;
} else {
@@ -244,22 +201,13 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
}
case 3:
List<String> errors = validateNameFields(
state.sheet.firstName.toString(),
state.sheet.lastName.toString(),
context,
);
List<String> errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context);
if (errors.isEmpty) {
return true;
} else {
@@ -274,12 +222,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
@@ -293,12 +236,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Выберете пол'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
@@ -312,12 +250,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Заполните все поля'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
@@ -343,12 +276,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
return false;
@@ -359,10 +287,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
barrierDismissible: false,
useSafeArea: false,
context: context,
builder: (context) => const PopScope(
canPop: false,
child: Loader(),
),
builder: (context) => const PopScope(canPop: false, child: Loader()),
);
await registrationCubit.tryRegistration();
if (context.mounted) {
@@ -373,9 +298,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
Navigator.pop(context);
showDialog(
context: context,
builder: (context) => const AlertDialog(
title: Text('Ошибка при попытке регистрации'),
),
builder: (context) => const AlertDialog(title: Text('Ошибка при попытке регистрации')),
);
}
}
@@ -385,12 +308,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
}
}
List<String> validateCircumferencesFields(
String chestCircumference,
String waistCircumference,
String hipCircumference,
BuildContext context,
) {
List<String> validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
@@ -406,12 +324,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
return errors;
}
List<String> validateCredentialsFields(
String email,
String password,
String passwordConfirmation,
BuildContext context,
) {
List<String> validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) {
final s = S.of(context);
List<String> errors = [];
@@ -435,11 +348,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
return errors;
}
List<String> validateNameFields(
String firstName,
String lastName,
BuildContext context,
) {
List<String> validateNameFields(String firstName, String lastName, BuildContext context) {
List<String> errors = [];
if (firstName.isEmpty) {

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -29,8 +30,10 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
@override
void initState() {
super.initState();
goRouter = GoRouter.of(context);
goRouter = GoRouter.of(context);
_startTimer();
// Report email verification screen opened
RegistrationAnalyticsService.reportRegistrationStep(16); // Email verification step
}
@override

View File

@@ -1,6 +1,7 @@
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:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
@@ -9,10 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart';
class VerifyScreen extends StatefulWidget {
final String passKey;
const VerifyScreen({
super.key,
required this.passKey,
});
const VerifyScreen({super.key, required this.passKey});
@override
State<VerifyScreen> createState() => _VerifyScreenState();
@@ -25,6 +23,8 @@ class _VerifyScreenState extends State<VerifyScreen> {
@override
void initState() {
super.initState();
// Report phone verification screen opened
RegistrationAnalyticsService.reportRegistrationStep(17); // Phone verification step
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
@@ -36,6 +36,8 @@ class _VerifyScreenState extends State<VerifyScreen> {
},
onNavigationRequest: (NavigationRequest request) async {
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
// Report phone verification success
RegistrationAnalyticsService.reportPhoneVerification();
GoRouter.of(context).pop();
await showModalBottomSheet(
useRootNavigator: true,
@@ -50,27 +52,15 @@ class _VerifyScreenState extends State<VerifyScreen> {
const Text(
'Номер телефона\nуспешно привязан',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
Lottie.asset(
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160),
const SizedBox(height: 16),
const Text(
'Теперь Вам доступен весь функционал!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
),
const SizedBox(height: 16),
InkWell(
@@ -80,18 +70,11 @@ class _VerifyScreenState extends State<VerifyScreen> {
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Закрыть',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
),
),
@@ -117,10 +100,7 @@ class _VerifyScreenState extends State<VerifyScreen> {
child: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(
child: Loader(),
),
if (_isLoading) const Center(child: Loader()),
],
),
),

View File

@@ -4,6 +4,7 @@ import 'dart:developer';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -24,6 +25,8 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
void initState() {
super.initState();
authCubit = sl<AuthCubit>();
// Report webview registration started
RegistrationAnalyticsService.reportRegistrationStep(18); // Webview registration step
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
@@ -43,10 +46,9 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('profile')) {
authCubit.getTokenByCredentials(
email!,
password!,
);
// Report webview registration completion
RegistrationAnalyticsService.reportRegistrationComplete();
authCubit.getTokenByCredentials(email!, password!);
}
return NavigationDecision.navigate;
},
@@ -86,12 +88,8 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 0,
),
body: SafeArea(
child: WebViewWidget(controller: _controller),
),
appBar: AppBar(toolbarHeight: 0),
body: SafeArea(child: WebViewWidget(controller: _controller)),
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
class RegistrationAnalyticsService {
static const String _eventPrefix = 'registration_step';
/// Reports registration step completion
static void reportRegistrationStep(int stepNumber) {
AppMetrica.reportEvent('$_eventPrefix $stepNumber');
}
/// Reports email verification completion
static void reportEmailVerification() {
AppMetrica.reportEvent('registration_email_verify');
}
/// Reports phone verification completion
static void reportPhoneVerification() {
AppMetrica.reportEvent('registration_phone_verify');
}
/// Reports registration completion
static void reportRegistrationComplete() {
AppMetrica.reportEvent('registration_complete');
}
/// Reports registration error
static void reportRegistrationError(String error) {
AppMetrica.reportEvent('registration_error');
}
}

View File

@@ -23,6 +23,7 @@ import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
Future<void> main() async {
WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
@@ -38,6 +39,9 @@ Future<void> main() async {
io.HttpClient.enableTimelineLogging = true;
}
AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey));
AppMetrica.reportEvent('Application started');
await SentryFlutter.init((options) {
options.dsn = EnvConfig.sentryDsn;
options.tracesSampleRate = 1.0;

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.2.10+70
version: 1.2.11+71
publish_to: 'none'
environment:
@@ -73,6 +73,7 @@ dependencies:
qr_code_scanner_plus: ^2.0.10+1
flutter_dotenv: ^5.2.1
uuid: ^4.5.1
appmetrica_plugin: ^3.3.0
dev_dependencies:
build_runner: ^2.7.0