feat: appmetrica

This commit is contained in:
mso7nueh
2025-08-25 14:24:06 +03:00
parent 886cfa8f1c
commit 5a8fa7089c
7 changed files with 120 additions and 201 deletions

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');
}
}