Registration pipeline chenged - fill profile step added. Order payment fixed

This commit is contained in:
2026-09-14 19:20:31 +03:00
parent 7460408784
commit 153a763b6f
8 changed files with 1023 additions and 905 deletions

View File

@@ -30,6 +30,7 @@ class ScooterOrder {
final double mileage; final double mileage;
final double avgSpeed; final double avgSpeed;
final String payTotalPricePrint; final String payTotalPricePrint;
final double unpayTotalPrice;
final String unpayTotalPricePrint; final String unpayTotalPricePrint;
final String totalOrderPricePrint; final String totalOrderPricePrint;
final bool hasFine; final bool hasFine;
@@ -65,6 +66,7 @@ class ScooterOrder {
required this.avgSpeed, required this.avgSpeed,
required this.payTotalPricePrint, // оплачено required this.payTotalPricePrint, // оплачено
required this.unpayTotalPricePrint, //неоплачено required this.unpayTotalPricePrint, //неоплачено
required this.unpayTotalPrice, //неоплачено
required this.totalOrderPricePrint, //общая required this.totalOrderPricePrint, //общая
required this.hasFine, required this.hasFine,
}); });
@@ -115,6 +117,7 @@ class ScooterOrder {
avgSpeed: (json['avgSpeed'] as num?)?.toDouble() ?? 0.0, avgSpeed: (json['avgSpeed'] as num?)?.toDouble() ?? 0.0,
payTotalPricePrint: paymentInfo?['payTotalPricePrint'] ?? '', payTotalPricePrint: paymentInfo?['payTotalPricePrint'] ?? '',
unpayTotalPricePrint: paymentInfo?['unpayTotalPricePrint'] ?? '', unpayTotalPricePrint: paymentInfo?['unpayTotalPricePrint'] ?? '',
unpayTotalPrice: (paymentInfo?['unpayTotalPrice'] as num?)?.toDouble() ?? 0.0,
totalOrderPricePrint: paymentInfo?['totalOrderPricePrint'] ?? '', totalOrderPricePrint: paymentInfo?['totalOrderPricePrint'] ?? '',
hasFine: paymentInfo?['hasFine'] ?? false, hasFine: paymentInfo?['hasFine'] ?? false,
); );

View File

@@ -2,13 +2,15 @@ import 'package:flutter/material.dart';
class CustomAppBar extends StatelessWidget { class CustomAppBar extends StatelessWidget {
final String title; final String title;
final showBackButton;
const CustomAppBar({super.key, required this.title}); const CustomAppBar({super.key, required this.title, this.showBackButton = true});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
if (showBackButton)
InkWell( InkWell(
onTap: () => Navigator.pop(context), onTap: () => Navigator.pop(context),
child: Row( child: Row(

View File

@@ -146,11 +146,38 @@ class AppRouter {
createPinUseCase: getIt<CreatePinUseCase>(), createPinUseCase: getIt<CreatePinUseCase>(),
verifyPinUsecase: getIt<VerifyPinUseCase>(), verifyPinUsecase: getIt<VerifyPinUseCase>(),
isPinSetUsecase: getIt<IsPinSetUsecase>(), isPinSetUsecase: getIt<IsPinSetUsecase>(),
getProfileUseCase: getIt<GetProfileUseCase>(),
) )
..add(PinScreenStarted()), ..add(PinScreenStarted()),
child: const PinLoginScreen(), child: const PinLoginScreen(),
) )
), ),
GoRoute(
path: '/edit-profile',
builder: (context, state) {
final isRegistration =
state.uri.queryParameters['isRegistration'] == 'true';
final profile = state.extra as UserProfile? ??
UserProfile(
name: '',
birthDate: '',
phone: '',
balance: 0,
email: '',
);
return BlocProvider(
create: (context) =>
getIt<EditProfileBloc>()..add(EditProfileStarted()),
child: EditProfileScreen(
profile: profile,
isRegistration: isRegistration,
),
);
},
),
GoRoute( GoRoute(
path: '/privacy-policy', path: '/privacy-policy',
builder: (context, state) => const PrivacyPolicyScreen(), builder: (context, state) => const PrivacyPolicyScreen(),
@@ -634,11 +661,12 @@ class AppRouter {
return null; return null;
} }
// 3. Авторизация в процессе (переход от ввода телефона/SMS к PIN-коду) // 3. Авторизация в процессе (ввод телефона -> SMS -> PIN -> Заполнение профиля)
if (authState is AuthInProgress) { if (authState is AuthInProgress) {
final isAllowed = currentLocation == '/login' || final isAllowed = currentLocation == '/login' ||
currentLocation.startsWith('/verify') || currentLocation.startsWith('/verify') ||
currentLocation == '/pin' || // ← Теперь роутер пустит сюда! currentLocation == '/pin' ||
currentLocation.startsWith('/edit-profile') ||
currentLocation == '/privacy-policy' || currentLocation == '/privacy-policy' ||
currentLocation == '/license-agreement'; currentLocation == '/license-agreement';
@@ -662,6 +690,10 @@ class AppRouter {
// 5. Пользователь полностью авторизован и PIN успешно верифицирован // 5. Пользователь полностью авторизован и PIN успешно верифицирован
if (authState is AuthPinVerified) { if (authState is AuthPinVerified) {
if (currentLocation.startsWith('/edit-profile')) {
return null;
}
final isComingFromAuth = currentLocation == '/splash' || final isComingFromAuth = currentLocation == '/splash' ||
currentLocation == '/login' || currentLocation == '/login' ||
currentLocation.startsWith('/verify') || currentLocation.startsWith('/verify') ||

View File

@@ -9,15 +9,22 @@ import '../../domain/entities/user_profile.dart';
import '../components/custom_app_bar.dart'; import '../components/custom_app_bar.dart';
import '../components/gradient_button.dart'; import '../components/gradient_button.dart';
import '../event/edit_profile_event.dart'; import '../event/edit_profile_event.dart';
import '../state/edit_profile_state.dart';
import '../event/profile_event.dart'; import '../event/profile_event.dart';
import '../event/spalsh_event.dart';
import '../state/edit_profile_state.dart';
import '../viewmodel/edit_profile_bloc.dart'; import '../viewmodel/edit_profile_bloc.dart';
import '../viewmodel/profile_bloc.dart'; import '../viewmodel/profile_bloc.dart';
import '../viewmodel/splash_bloc.dart';
class EditProfileScreen extends StatefulWidget { class EditProfileScreen extends StatefulWidget {
final UserProfile profile; final UserProfile profile;
final bool isRegistration;
const EditProfileScreen({super.key, required this.profile}); const EditProfileScreen({
super.key,
required this.profile,
this.isRegistration = false,
});
@override @override
State<EditProfileScreen> createState() => _EditProfileScreenState(); State<EditProfileScreen> createState() => _EditProfileScreenState();
@@ -52,6 +59,36 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
} }
} }
birthDateController = TextEditingController(text: initialDateText); birthDateController = TextEditingController(text: initialDateText);
nameController.addListener(_onFieldChanged);
birthDateController.addListener(_onFieldChanged);
phoneController.addListener(_onFieldChanged);
emailController.addListener(_onFieldChanged);
}
void _onFieldChanged() {
setState(() {});
}
bool get _isFormValid {
return nameController.text.trim().isNotEmpty &&
birthDateController.text.trim().isNotEmpty &&
phoneController.text.trim().isNotEmpty &&
emailController.text.trim().isNotEmpty;
}
@override
void dispose() {
nameController.removeListener(_onFieldChanged);
birthDateController.removeListener(_onFieldChanged);
phoneController.removeListener(_onFieldChanged);
emailController.removeListener(_onFieldChanged);
nameController.dispose();
birthDateController.dispose();
phoneController.dispose();
emailController.dispose();
super.dispose();
} }
void _submit(BuildContext context) { void _submit(BuildContext context) {
@@ -61,14 +98,13 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
final String birthDateForApi = _selectedBirthDate?.toIso8601String() ?? ''; final String birthDateForApi = _selectedBirthDate?.toIso8601String() ?? '';
final updatedProfile = profileFromState.copyWith( final updatedProfile = profileFromState.copyWith(
name: nameController.text, name: nameController.text.trim(),
birthDate: birthDateForApi.isNotEmpty ? "${birthDateForApi}Z" : '', birthDate: birthDateForApi.isNotEmpty ? "${birthDateForApi}Z" : '',
email: emailController.text, email: emailController.text.trim(),
); );
context.read<EditProfileBloc>().add(EditProfileSubmitted(updatedProfile)); context.read<EditProfileBloc>().add(EditProfileSubmitted(updatedProfile));
context.read<ProfileBloc>().add(ProfileUpdated()); context.read<ProfileBloc>().add(ProfileUpdated());
} }
Future<void> _selectDate(BuildContext context) async { Future<void> _selectDate(BuildContext context) async {
@@ -90,6 +126,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isButtonEnabled = widget.isRegistration ? _isFormValid : true;
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
@@ -104,15 +142,21 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
phoneController.text = profile.phone; phoneController.text = profile.phone;
emailController.text = profile.email; emailController.text = profile.email;
birthDateController.text = profile.birthDate; birthDateController.text = profile.birthDate;
} }
if (state.isSuccess) { if (state.isSuccess) {
getIt<ProfileBloc>().add(ProfileUpdated()); getIt<ProfileBloc>().add(ProfileUpdated());
if (widget.isRegistration) {
context.read<SplashBloc>().add(PinVerificationSuccess());
context.go('/home');
} else {
context.pop(); context.pop();
} }
}
if (state.error != null) { if (state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.error!))); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error!)),
);
} }
}, },
builder: (context, state) { builder: (context, state) {
@@ -125,7 +169,10 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
return Column( return Column(
children: [ children: [
const SizedBox(height: 16), const SizedBox(height: 16),
CustomAppBar(title: 'Личные данные'), CustomAppBar(
title: widget.isRegistration ? 'Заполните профиль' : 'Личные данные',
showBackButton: !widget.isRegistration,
),
const SizedBox(height: 32), const SizedBox(height: 32),
_Field( _Field(
'Имя', 'Имя',
@@ -137,9 +184,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
birthDateController, birthDateController,
iconPath: 'assets/icons/edit.png', iconPath: 'assets/icons/edit.png',
readOnly: true, readOnly: true,
onTap: () => _selectDate( onTap: () => _selectDate(context),
context,
),
), ),
_Field( _Field(
'Телефон', 'Телефон',
@@ -156,8 +201,8 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
state.isSaving state.isSaving
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(color: Colors.white)
: GradientButton( : GradientButton(
text: 'Сохранить изменения', text: widget.isRegistration ? 'Продолжить' : 'Сохранить изменения',
onTap: () => _submit(context), onTap: isButtonEnabled ? () => _submit(context) : null,
width: double.infinity, width: double.infinity,
height: 56, height: 56,
fontSize: 16, fontSize: 16,
@@ -173,7 +218,6 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
), ),
); );
} }
} }
class _Field extends StatelessWidget { class _Field extends StatelessWidget {

View File

@@ -115,6 +115,8 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
); );
} }
final hasNoDebt = (order?.unpayTotalPrice ?? 0) == 0;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -204,11 +206,12 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
), ),
), ),
// 🔹 КАРТА ОПЛАТЫ (Отображается только если есть долг)
if (!hasNoDebt) ...[
const SizedBox(height: 40), const SizedBox(height: 40),
// 🔹 КАРТА ОПЛАТЫ
(state.useBalance || state.selectedCard != null) (state.useBalance || state.selectedCard != null)
? Padding(padding: const EdgeInsets.symmetric(horizontal: 20), ? Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: PaymentOption( child: PaymentOption(
title: state.useBalance ? 'Баланс' : "", title: state.useBalance ? 'Баланс' : "",
subtitle: state.useBalance subtitle: state.useBalance
@@ -267,7 +270,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
context.pushReplacement( context.pushReplacement(
'/home/payment-method-sheet', '/home/payment-method-sheet',
); );
}, },
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
24, 24,
@@ -313,6 +315,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
), ),
), ),
], ],
],
); );
}, },
), ),
@@ -339,16 +342,23 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
} }
}, },
builder: (context, state) { builder: (context, state) {
final hasNoDebt = (state.order?.unpayTotalPrice ?? 0) == 0;
return GradientButton( return GradientButton(
text: state.status == PaymentConfirmStatus.loading text: state.status == PaymentConfirmStatus.loading
? 'Обработка...' ? 'Обработка...'
: 'Оплатить', : (hasNoDebt ? 'Продолжить' : 'Оплатить'),
showArrows: true, showArrows: true,
height: 56, height: 56,
width: double.infinity, width: double.infinity,
onTap: (state.status == PaymentConfirmStatus.loading || (!state.useBalance && state.selectedCard == null)) onTap: (state.status == PaymentConfirmStatus.loading ||
(!hasNoDebt && !state.useBalance && state.selectedCard == null))
? null ? null
: () { : () {
if (hasNoDebt) {
context.read<MapBloc>().add(ClearMapPlacemarks());
context.go('/home');
} else {
context.read<PaymentConfirmBloc>().add( context.read<PaymentConfirmBloc>().add(
PayRide( PayRide(
cardId: state.useBalance ? null : state.selectedCard?.id, cardId: state.useBalance ? null : state.selectedCard?.id,
@@ -357,6 +367,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
photoIds: photoIds, photoIds: photoIds,
), ),
); );
}
}, },
); );
}, },
@@ -371,7 +382,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
); );
} }
// Вспомогательный метод для подзаголовков
Widget _buildInfoLabel(String text) { Widget _buildInfoLabel(String text) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
@@ -382,7 +392,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
); );
} }
// Вспомогательный метод для строк со значениями и иконками
Widget _buildValueRow(String iconName, String value) { Widget _buildValueRow(String iconName, String value) {
return Row( return Row(
children: [ children: [

View File

@@ -4,11 +4,11 @@ import 'package:go_router/go_router.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../components/code_dots.dart'; import '../components/code_dots.dart';
import '../event/map_event.dart'; import '../event/map_event.dart';
import '../event/pin_event.dart';
import '../event/spalsh_event.dart'; import '../event/spalsh_event.dart';
import '../state/pin_state.dart';
import '../viewmodel/map_bloc.dart'; import '../viewmodel/map_bloc.dart';
import '../viewmodel/pin_bloc.dart'; import '../viewmodel/pin_bloc.dart';
import '../event/pin_event.dart';
import '../state/pin_state.dart';
import '../viewmodel/splash_bloc.dart'; import '../viewmodel/splash_bloc.dart';
class PinLoginScreen extends StatefulWidget { class PinLoginScreen extends StatefulWidget {
@@ -52,8 +52,14 @@ class _PinLoginScreenState extends State<PinLoginScreen> {
return BlocListener<PinBloc, PinState>( return BlocListener<PinBloc, PinState>(
listener: (context, state) { listener: (context, state) {
if (state is PinSuccess) { if (state is PinSuccess) {
if (state.isProfileIncomplete) {
// Переходим на заполнение профиля, если хотя бы одно поле пустое
context.go('/edit-profile?isRegistration=true');
} else {
// Все поля заполнены — завершаем авторизацию и идем на главный экран
context.read<SplashBloc>().add(PinVerificationSuccess()); context.read<SplashBloc>().add(PinVerificationSuccess());
context.go("/home"); context.go('/home');
}
} }
if (state.error != null) { if (state.error != null) {
controller.clear(); controller.clear();
@@ -128,7 +134,6 @@ class _PinLoginScreenState extends State<PinLoginScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
// Блок ошибки
if (state.error != null) if (state.error != null)
Text( Text(
state.error!, state.error!,
@@ -164,7 +169,6 @@ class _PinLoginScreenState extends State<PinLoginScreen> {
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
], ],
); );
}, },

View File

@@ -23,5 +23,10 @@ class PinLoading extends PinState {
} }
class PinSuccess extends PinState { class PinSuccess extends PinState {
const PinSuccess() : super(pin: ''); final bool isProfileIncomplete;
const PinSuccess({this.isProfileIncomplete = false}) : super(pin: '');
@override
List<Object?> get props => [isProfileIncomplete];
} }

View File

@@ -1,22 +1,24 @@
import 'dart:async'; import 'dart:async';
import 'package:be_happy/domain/usecase/verify_pin_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecase/create_pin_usecase.dart';
import '../../domain/usecase/get_profile_usecase.dart';
import '../../domain/usecase/is_pin_set_usecase.dart'; import '../../domain/usecase/is_pin_set_usecase.dart';
import '../../domain/usecase/verify_pin_usecase.dart';
import '../event/pin_event.dart'; import '../event/pin_event.dart';
import '../state/pin_state.dart'; import '../state/pin_state.dart';
import '../../domain/usecase/create_pin_usecase.dart';
class PinBloc extends Bloc<PinEvent, PinState> { class PinBloc extends Bloc<PinEvent, PinState> {
final CreatePinUseCase createPinUseCase; final CreatePinUseCase createPinUseCase;
final VerifyPinUseCase verifyPinUsecase; final VerifyPinUseCase verifyPinUsecase;
final IsPinSetUsecase isPinSetUsecase; final IsPinSetUsecase isPinSetUsecase;
final GetProfileUseCase getProfileUseCase;
PinBloc({ PinBloc({
required this.createPinUseCase, required this.createPinUseCase,
required this.verifyPinUsecase, required this.verifyPinUsecase,
required this.isPinSetUsecase, required this.isPinSetUsecase,
bool isRegistration = true, required this.getProfileUseCase,
}) : super(const PinLoading()) { }) : super(const PinLoading()) {
on<PinDigitChanged>(_onPinChanged); on<PinDigitChanged>(_onPinChanged);
on<PinSubmitted>(_onPinSubmitted); on<PinSubmitted>(_onPinSubmitted);
@@ -46,35 +48,52 @@ class PinBloc extends Bloc<PinEvent, PinState> {
try { try {
if (isCreateMode) { if (isCreateMode) {
await createPinUseCase(currentPin); await createPinUseCase(currentPin);
emit(const PinSuccess());
} else { } else {
final isValid = await verifyPinUsecase(currentPin); final isValid = await verifyPinUsecase(currentPin);
if (isValid) { if (!isValid) {
emit(const PinSuccess());
} else {
throw Exception("Неверный ПИН-код"); throw Exception("Неверный ПИН-код");
} }
} }
// Проверяем заполненность профиля
bool isProfileIncomplete = true;
try {
final profile = await getProfileUseCase();
// Если хотя бы одно из обязательных полей пустое — профиль считается незаполненным
isProfileIncomplete = profile.name.trim().isEmpty ||
profile.birthDate.trim().isEmpty ||
profile.email.trim().isEmpty ||
profile.phone.trim().isEmpty;
} catch (_) {
isProfileIncomplete = true;
}
emit(PinSuccess(isProfileIncomplete: isProfileIncomplete));
} catch (e) { } catch (e) {
if (isCreateMode) { if (isCreateMode) {
emit(PinCreateInProgress(pin: '', error: e.toString())); emit(PinCreateInProgress(pin: '', error: e.toString()));
} else { } else {
emit(PinLoginInProgress(pin: '', error: e.toString().contains("Неверный") emit(PinLoginInProgress(
pin: '',
error: e.toString().contains("Неверный")
? "Неверный ПИН-код" ? "Неверный ПИН-код"
: "Ошибка при проверке")); : "Ошибка при проверке",
));
} }
} }
} }
FutureOr<void> _onPinScreenStarted(PinScreenStarted event, Emitter<PinState> emit) async { FutureOr<void> _onPinScreenStarted(
PinScreenStarted event,
Emitter<PinState> emit,
) async {
try { try {
final bool hasPin = await isPinSetUsecase(); final bool hasPin = await isPinSetUsecase();
if (hasPin) { if (hasPin) {
// Пин ЕСТЬ в базе — значит, нужно его ВВЕСТИ (Login)
emit(const PinLoginInProgress(pin: '')); emit(const PinLoginInProgress(pin: ''));
} else { } else {
// Пина НЕТ в базе — значит, нужно его СОЗДАТЬ (Create)
emit(const PinCreateInProgress(pin: '')); emit(const PinCreateInProgress(pin: ''));
} }
} catch (e) { } catch (e) {