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

View File

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

View File

@@ -146,11 +146,38 @@ class AppRouter {
createPinUseCase: getIt<CreatePinUseCase>(),
verifyPinUsecase: getIt<VerifyPinUseCase>(),
isPinSetUsecase: getIt<IsPinSetUsecase>(),
getProfileUseCase: getIt<GetProfileUseCase>(),
)
..add(PinScreenStarted()),
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(
path: '/privacy-policy',
builder: (context, state) => const PrivacyPolicyScreen(),
@@ -634,11 +661,12 @@ class AppRouter {
return null;
}
// 3. Авторизация в процессе (переход от ввода телефона/SMS к PIN-коду)
// 3. Авторизация в процессе (ввод телефона -> SMS -> PIN -> Заполнение профиля)
if (authState is AuthInProgress) {
final isAllowed = currentLocation == '/login' ||
currentLocation.startsWith('/verify') ||
currentLocation == '/pin' || // ← Теперь роутер пустит сюда!
currentLocation == '/pin' ||
currentLocation.startsWith('/edit-profile') ||
currentLocation == '/privacy-policy' ||
currentLocation == '/license-agreement';
@@ -662,6 +690,10 @@ class AppRouter {
// 5. Пользователь полностью авторизован и PIN успешно верифицирован
if (authState is AuthPinVerified) {
if (currentLocation.startsWith('/edit-profile')) {
return null;
}
final isComingFromAuth = currentLocation == '/splash' ||
currentLocation == '/login' ||
currentLocation.startsWith('/verify') ||

View File

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

View File

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

View File

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

View File

@@ -23,5 +23,10 @@ class PinLoading 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 'package:be_happy/domain/usecase/verify_pin_usecase.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/verify_pin_usecase.dart';
import '../event/pin_event.dart';
import '../state/pin_state.dart';
import '../../domain/usecase/create_pin_usecase.dart';
class PinBloc extends Bloc<PinEvent, PinState> {
final CreatePinUseCase createPinUseCase;
final VerifyPinUseCase verifyPinUsecase;
final IsPinSetUsecase isPinSetUsecase;
final GetProfileUseCase getProfileUseCase;
PinBloc({
required this.createPinUseCase,
required this.verifyPinUsecase,
required this.isPinSetUsecase,
bool isRegistration = true,
required this.getProfileUseCase,
}) : super(const PinLoading()) {
on<PinDigitChanged>(_onPinChanged);
on<PinSubmitted>(_onPinSubmitted);
@@ -46,35 +48,52 @@ class PinBloc extends Bloc<PinEvent, PinState> {
try {
if (isCreateMode) {
await createPinUseCase(currentPin);
emit(const PinSuccess());
} else {
final isValid = await verifyPinUsecase(currentPin);
if (isValid) {
emit(const PinSuccess());
} else {
if (!isValid) {
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) {
if (isCreateMode) {
emit(PinCreateInProgress(pin: '', error: e.toString()));
} 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 {
final bool hasPin = await isPinSetUsecase();
if (hasPin) {
// Пин ЕСТЬ в базе — значит, нужно его ВВЕСТИ (Login)
emit(const PinLoginInProgress(pin: ''));
} else {
// Пина НЕТ в базе — значит, нужно его СОЗДАТЬ (Create)
emit(const PinCreateInProgress(pin: ''));
}
} catch (e) {