diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d6e7401a..4447f176 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e814e8a..f0baee2b 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 69 + 70 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index ebd745be..7ccc8b7a 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -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}'" ) diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index b363ad70..a3404aeb 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -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'] ?? ''; diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 20b0e26c..73162bd2 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -47,12 +47,20 @@ class _AuthScreenState extends State { 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 _reconnectWebSocket(BuildContext context) async { try { await sl().clear(); diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index b55a979f..9b6c2536 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -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 createState() => _AddProductModalState(); @@ -58,75 +53,62 @@ class _AddProductModalState extends State { 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 { 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 { 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 { 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 { 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 { 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 { 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), ), ), ), diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index 34964d1a..d91cdd74 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -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({ diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index c9426674..ccf9cae4 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -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 { 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 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().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())); } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart index 649c1fb8..d1f547f8 100644 --- a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -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 { final ProfileRepository profileRepository; - VerifyEmailCubit({ - required this.profileRepository, - }) : super(const VerifyEmailState.loading()); + VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading()); Future sendEmailCode() async { try { @@ -24,6 +23,7 @@ class VerifyEmailCubit extends Cubit { emit(const VerifyEmailState.verifyLoading()); final result = await profileRepository.tryVerifyEmailByCode(code); if (result == true) { + RegistrationAnalyticsService.reportEmailVerification(); emit(const VerifyEmailState.verifySuccess()); } else { emit(const VerifyEmailState.verifyError()); diff --git a/lib/features/registration/presentation/pages/registration_screen.dart b/lib/features/registration/presentation/pages/registration_screen.dart index 86514be8..9f30ca33 100644 --- a/lib/features/registration/presentation/pages/registration_screen.dart +++ b/lib/features/registration/presentation/pages/registration_screen.dart @@ -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 { 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 { 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 { 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 { 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 { child: TweenAnimationBuilder( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, - tween: Tween( - begin: 1 / 15, - end: progressValue, - ), + tween: Tween(begin: 1 / 15, end: progressValue), builder: (context, value, _) => LinearProgressIndicator( value: value, borderRadius: BorderRadius.circular(16), @@ -175,6 +140,8 @@ class _RegistrationScreenState extends State { 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 { 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 { return true; } case 2: - List errors = validateCredentialsFields( - state.sheet.email, - state.sheet.password, - state.sheet.passwordConfirmation, - context, - ); + List 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 { 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 errors = validateNameFields( - state.sheet.firstName.toString(), - state.sheet.lastName.toString(), - context, - ); + List 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 { 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 { 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 { 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 { 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 { 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 { 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 { } } - List validateCircumferencesFields( - String chestCircumference, - String waistCircumference, - String hipCircumference, - BuildContext context, - ) { + List validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) { final s = S.of(context); List errors = []; @@ -406,12 +324,7 @@ class _RegistrationScreenState extends State { return errors; } - List validateCredentialsFields( - String email, - String password, - String passwordConfirmation, - BuildContext context, - ) { + List validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) { final s = S.of(context); List errors = []; @@ -435,11 +348,7 @@ class _RegistrationScreenState extends State { return errors; } - List validateNameFields( - String firstName, - String lastName, - BuildContext context, - ) { + List validateNameFields(String firstName, String lastName, BuildContext context) { List errors = []; if (firstName.isEmpty) { diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index f50d4960..e91b9e9e 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -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 { @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 diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 637dba5b..8d5df9df 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -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 createState() => _VerifyScreenState(); @@ -25,6 +23,8 @@ class _VerifyScreenState extends State { @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 { }, 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 { 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 { 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 { child: Stack( children: [ WebViewWidget(controller: _controller), - if (_isLoading) - const Center( - child: Loader(), - ), + if (_isLoading) const Center(child: Loader()), ], ), ), diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index abb6537d..05c0ffc1 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -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 { void initState() { super.initState(); authCubit = sl(); + // Report webview registration started + RegistrationAnalyticsService.reportRegistrationStep(18); // Webview registration step _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( @@ -43,10 +46,9 @@ class _WebViewRegistrationScreenState extends State { 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 { @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)), ); } } diff --git a/lib/features/registration/services/registration_analytics_service.dart b/lib/features/registration/services/registration_analytics_service.dart new file mode 100644 index 00000000..3bfc69d1 --- /dev/null +++ b/lib/features/registration/services/registration_analytics_service.dart @@ -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'); + } +} diff --git a/lib/main.dart b/lib/main.dart index 6b44348c..468de90f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 main() async { WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); @@ -38,6 +39,9 @@ Future 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; diff --git a/pubspec.yaml b/pubspec.yaml index 3b33303b..1505f232 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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