diff --git a/lib/domain/entities/scooter_order.dart b/lib/domain/entities/scooter_order.dart index 9bdd544..d1d059d 100644 --- a/lib/domain/entities/scooter_order.dart +++ b/lib/domain/entities/scooter_order.dart @@ -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, ); diff --git a/lib/presentation/components/custom_app_bar.dart b/lib/presentation/components/custom_app_bar.dart index 814d0b7..2b3c4d3 100644 --- a/lib/presentation/components/custom_app_bar.dart +++ b/lib/presentation/components/custom_app_bar.dart @@ -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( diff --git a/lib/presentation/navigation/app_router.dart b/lib/presentation/navigation/app_router.dart index e85c0cd..bd3293d 100644 --- a/lib/presentation/navigation/app_router.dart +++ b/lib/presentation/navigation/app_router.dart @@ -1,723 +1,755 @@ -import 'dart:async'; - -import 'package:be_happy/presentation/screens/card_binding_result_screen.dart'; -import 'package:be_happy/presentation/screens/lease_agreement_screen.dart'; -import 'package:be_happy/presentation/screens/payment_rules_screen.dart'; -import 'package:bot_toast/bot_toast.dart'; -import 'package:be_happy/core/app_colors.dart'; -import 'package:be_happy/di/service_locator.dart'; -import 'package:be_happy/domain/entities/user_profile.dart'; -import 'package:be_happy/domain/usecase/get_available_subscriptions_usecase.dart'; -import 'package:be_happy/domain/usecase/get_certificates_usecase.dart'; -import 'package:be_happy/domain/usecase/get_client_subscriptions_usecase.dart'; -import 'package:be_happy/domain/usecase/get_profile_usecase.dart'; -import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart'; -import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart'; -import 'package:be_happy/domain/usecase/is_pin_set_usecase.dart'; -import 'package:be_happy/domain/usecase/purchase_certificate_usecase.dart'; -import 'package:be_happy/presentation/event/payment_confirm_event.dart'; -import 'package:be_happy/presentation/event/pin_event.dart'; -import 'package:be_happy/presentation/event/subscription_list_event.dart'; -import 'package:be_happy/presentation/screens/block_screen.dart'; -import 'package:be_happy/presentation/screens/documents_screen.dart'; -import 'package:be_happy/presentation/screens/edit_profile_screen.dart'; -import 'package:be_happy/presentation/screens/map_screen.dart'; -import 'package:be_happy/presentation/screens/news_screen.dart'; -import 'package:be_happy/presentation/screens/onboarding_screen.dart'; -import 'package:be_happy/presentation/screens/order_history_detail_screen.dart'; -import 'package:be_happy/presentation/screens/payment_confirm_screen.dart'; -import 'package:be_happy/presentation/screens/profile_screen.dart'; -import 'package:be_happy/presentation/screens/promo_code_screen.dart'; -import 'package:be_happy/presentation/screens/qr_scan_info_screen.dart'; -import 'package:be_happy/presentation/screens/scooter_code_input_screen.dart'; -import 'package:be_happy/presentation/screens/scooter_detail_screen.dart'; -import 'package:be_happy/presentation/screens/send_photo_screen.dart'; -import 'package:be_happy/presentation/screens/subscription_list_screen.dart'; -import 'package:be_happy/presentation/screens/support_screen.dart'; -import 'package:be_happy/presentation/screens/top_up_screen.dart'; -import 'package:be_happy/presentation/viewmodel/splash_bloc.dart'; -import 'package:be_happy/presentation/viewmodel/susbcription_details_bloc.dart'; -import 'package:be_happy/presentation/viewmodel/top_up_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:go_router/go_router.dart'; -import 'package:path/path.dart'; - -import '../../domain/entities/scooter.dart'; -import '../../domain/usecase/activate_subscription_usecase.dart'; -import '../../domain/usecase/add_payment_card_usecase.dart'; -import '../../domain/usecase/book_scooter_usecase.dart'; -import '../../domain/usecase/create_pin_usecase.dart'; -import '../../domain/usecase/get_address_by_point_usecase.dart'; -import '../../domain/usecase/get_available_tariffs_usecase.dart'; -import '../../domain/usecase/get_client_orders_usecase.dart'; -import '../../domain/usecase/get_map_settings_usecase.dart'; -import '../../domain/usecase/get_payment_cards_usecase.dart'; -import '../../domain/usecase/get_pedestrian_routes_usecase.dart'; -import '../../domain/usecase/get_scooter_order_by_id_usecase.dart'; -import '../../domain/usecase/get_scooter_usecase.dart'; -import '../../domain/usecase/pay_ride_usecase.dart'; -import '../../domain/usecase/remove_payment_card_usecase.dart'; -import '../../domain/usecase/save_map_settings_usecase.dart'; -import '../../domain/usecase/set_main_payment_card_usecase.dart'; -import '../../domain/usecase/verify_pin_usecase.dart'; -import '../components/scooter_bottom_sheet.dart'; -import '../components/sheet/current_rides_sheet.dart'; -import '../components/sheet/map_settings_sheet.dart'; -import '../components/sheet/payment_method_sheet.dart'; -import '../components/sheet/reserved_ride_sheet.dart'; -import '../components/sheet/tariff_sheet.dart'; -import '../event/current_rides_event.dart'; -import '../event/edit_profile_event.dart'; -import '../event/map_settings_modal_event.dart'; -import '../event/payment_methods_event.dart'; -import '../event/profile_event.dart'; -import '../event/scooter_detail_modal_event.dart'; -import '../event/subscription_details_event.dart'; -import '../event/tariff_sheet_event.dart'; -import '../event/top_up_event.dart'; -import '../screens/add_card_screen.dart'; // ← новый импорт -import '../screens/company_details_screen.dart'; -import '../screens/fine_list_screen.dart'; -import '../screens/fine_payment_screen.dart'; -import '../screens/license_agreement_screen.dart'; -import '../screens/notifications_screen.dart'; -import '../screens/order_history_screen.dart'; -import '../screens/payment_methods_screen.dart'; -import '../screens/phone_login_screen.dart'; -import '../screens/phone_screen.dart'; -import '../screens/pin_login_screen.dart'; -import '../screens/privacy_policy_screen.dart'; -import '../screens/qr_scan_screen.dart'; -import '../screens/splash_screen.dart'; -import '../screens/subscription_details_screen.dart'; -import '../state/splash_state.dart'; -import '../viewmodel/add_card_bloc.dart'; -import '../viewmodel/current_rides_bloc.dart'; -import '../viewmodel/edit_profile_bloc.dart'; -import '../viewmodel/map_settings_modal_bloc.dart'; -import '../viewmodel/payment_confirm_bloc.dart'; -import '../viewmodel/payment_method_sheet_bloc.dart'; -import '../viewmodel/payment_methods_bloc.dart'; -import '../viewmodel/pin_bloc.dart'; -import '../viewmodel/profile_bloc.dart'; -import '../viewmodel/scooter_code_bloc.dart'; -import '../viewmodel/scooter_detail_bloc.dart'; -import '../viewmodel/scooter_detail_modal_bloc.dart'; -import '../viewmodel/subscription_list_bloc.dart'; -import '../viewmodel/tariff_sheet_bloc.dart'; // ← новый импорт - -class AppRouter { - final SplashBloc splashBloc; - - AppRouter(this.splashBloc); - - late final GoRouter router = GoRouter( - debugLogDiagnostics: true, - initialLocation: '/splash', - - routes: [ - GoRoute( - path: '/splash', - builder: (context, state) => const SplashScreen(), - ), - GoRoute(path: '/login', builder: (context, state) => const PhoneScreen()), - GoRoute( - path: '/verify', - builder: (context, state) { - final phone = state.uri.queryParameters['phone']; - if (phone != null) { - return PhoneLoginScreen(phoneNumber: phone, tempToken: ''); - } - throw Exception("Incorrect phone"); - }, - ), - GoRoute( - path: '/block', - builder: (context, state) => const BlockedScreen(), - ), - GoRoute( - path: '/pin', - builder: (context, state) => - BlocProvider( - create: (context) => - PinBloc( - createPinUseCase: getIt(), - verifyPinUsecase: getIt(), - isPinSetUsecase: getIt(), - ) - ..add(PinScreenStarted()), - child: const PinLoginScreen(), - ) - ), - GoRoute( - path: '/privacy-policy', - builder: (context, state) => const PrivacyPolicyScreen(), - ), - GoRoute( - path: '/license-agreement', - builder: (context, state) => const LicenseAgreementScreen(), - ), - GoRoute( - path: '/card-result', - builder: (context, state) { - final modeParam = state.uri.queryParameters['status']; - - CardBindingStatus status = CardBindingStatus.success; - String title = 'Карта успешно\nпривязана!'; - - if (modeParam == 'failed') { - status = CardBindingStatus.failed; - title = 'Извините, что-то\nпошло не так'; - } - - return CardBindingResultScreen( - status: status, - title: title, - cardImagePath: 'assets/icons/card_result_screen.png', - ); - }, - ), - GoRoute( - path: '/home', - builder: (context, state) => const MapScreen(), - routes: [ - //Modal Bottom Sheets - GoRoute( - path: 'scooter-sheet', - pageBuilder: (context, state) { - final data = state.extra as Map; - - final scooters = data['scooters'] as List; - final location = data['currentLocation'] as Position; - - return modalPage( - state: state, - child: Material( - type: MaterialType.transparency, - child: BlocProvider( - create: (context) => - ScooterDetailModalBloc( - getIt(), - getIt(), - getIt(), - ) - ..add( - ScooterDetailModalStarted( - scooters, - location.latitude, - location.longitude, - ), - ), - child: ScooterBottomSheet(), - ), - ), - ); - }, - ), - GoRoute( - path: 'tarif-sheet', - pageBuilder: (context, state) { - final scooter = state.extra as Scooter; - - return modalPage( - state: state, - child: Material( - type: MaterialType.transparency, - child: BlocProvider( - create: (context) => - TariffSheetBloc( - getIt(), - getIt(), - getIt(), - getIt(), - ), - child: TariffSheet(scooter: scooter), - ), - ), - ); - }, - ), - GoRoute( - path: 'current-rides-sheet', - pageBuilder: (context, state) { - return modalPage( - state: state, - child: Material( - type: MaterialType.transparency, - child: BlocProvider( - create: (context) => - CurrentRidesBloc(getIt()) - ..add(LoadClientOrders()), - child: CurrentRidesSheet(), - ), - ), - ); - }, - ), - GoRoute( - path: 'payment-method-sheet', - pageBuilder: (context, state) { - return modalPage( - state: state, - child: Material( - type: MaterialType.transparency, - child: BlocProvider( - create: (context) => - PaymentMethodSheetBloc( - getIt(), - getIt(), - getIt(), - getIt(), - ), - child: PaymentMethodSheet(), - ), - ), - ); - }, - ), - - GoRoute( - path: 'map-settings-sheet', - pageBuilder: (context, state) { - return modalPage( - state: state, - child: Material( - type: MaterialType.transparency, - child: BlocProvider( - create: (context) => - MapSettingsModalBloc( - getIt(), - getIt(), - ) - ..add(MapSettingsModalStarted()), - child: MapSettingsSheet(), - ), - ), - ); - }, - ), - //Sub screens - GoRoute( - path: 'profile', - builder: (context, state) { - final bloc = getIt(); - - if (!bloc.isClosed) { - bloc.add(ProfileStarted()); - } - - return BlocProvider.value( - value: bloc, - child: const ProfileScreen(), - ); - }, - - routes: [ - GoRoute( - path: 'edit', - builder: (context, state) => - BlocProvider( - create: (context) => - getIt() - ..add(EditProfileStarted()), - child: EditProfileScreen( - profile: UserProfile( - name: '', - birthDate: '', - phone: '', - balance: 23, - email: '', - ), - ), - ), - ), - ], - ), - GoRoute( - path: 'scooter/:id', - builder: (context, state) { - List extras = []; - if (state.extra != null) { - extras = state.extra as List; - } - String address = extras[0]; - double distance = extras[1]; - - return BlocProvider( - create: (context) => getIt(), - child: ScooterDetailScreen(address: address, distance: distance), - ); - } - ), - GoRoute( - path: 'order-photos/:orderId', - builder: (context, state) { - int orderId = int.parse(state.pathParameters['orderId']!); - return SendPhotoScreen(orderId: orderId); - }, - ), - GoRoute( - path: 'checkout/:orderId', - builder: (context, state) { - int orderId = int.parse(state.pathParameters['orderId']!); - List photoIds = []; - if (state.extra != null) { - photoIds = state.extra as List; - } - - return BlocProvider( - create: (context) => - PaymentConfirmBloc( - getIt(), - getIt(), - getIt(), - getIt(), - ) - ..add(PaymentConfirmStarted(orderId)), - child: PaymentConfirmScreen( - orderId: orderId, - photoIds: photoIds, - ), - ); - }, - ), - GoRoute( - path: 'support', - builder: (context, state) => const SupportScreen(), - ), - GoRoute( - path: 'documents', - builder: (context, state) => const DocumentsScreen(), - routes: [ - GoRoute( - path: 'payment-rules', - builder: (context, state) => const PaymentRulesScreen(), - ), - GoRoute( - path: 'lease-agreement', - builder: (context, state) => const LeaseAgreementScreen(), - ), - GoRoute( - path: 'company-details', - builder: (context, state) => const CompanyDetailsScreen(), - ), - ] - ), - GoRoute( - path: 'promo', - builder: (context, state) => const PromoCodeScreen(), - ), - GoRoute( - path: 'subscriptions', - builder: (context, state) { - return BlocProvider( - create: (context) => - SubscriptionListBloc( - getAvailableSubscriptionsUsecase: - getIt(), - getClientSubscriptionsUsecase: - getIt(), - ) - ..add(LoadSubscriptionsEvent()), - child: SubscriptionsListScreen(), - ); - }, - routes: [ - GoRoute( - path: ':id', - builder: (context, state) { - return BlocProvider( - create: (context) => - SubscriptionDetailsBloc( - getIt(), - getIt(), - getIt(), - getIt(), - ) - ..add( - LoadDetailsEvent( - int.parse(state.pathParameters['id']!), - ), - ), - child: SubscriptionDetailsScreen(subscriptionId: 1), - ); - }, - ), - ], - ), - GoRoute( - path: 'rules', - builder: (context, state) => const OnboardingScreen(), - ), - GoRoute( - path: 'news', - builder: (context, state) => const NewsScreen(), - ), - GoRoute( - path: 'qr-info', - builder: (context, state) => const QRScanInfoScreen(), - routes: [ - GoRoute( - path: 'qr-scan', - builder: (context, state) => const QrScanScreen(), - ), - GoRoute( - path: 'qr-input', - builder: (context, state) => - BlocProvider( - create: (context) => - ScooterCodeBloc( - getScooterByTitleUsecase: getIt< - GetScooterByTitleUsecase>(), getAddressUsecase: getIt(), getPedestrianRoutesUsecase: getIt(), - ), - child: ScooterCodeInputScreen(), - ), - ), - ] - ), - GoRoute( - path: 'payment-methods', - builder: (context, state) { - final refreshId = state.uri.queryParameters['refreshId']; - - return BlocProvider( - key: refreshId != null ? ValueKey(refreshId) : null, - create: (context) => PaymentMethodsBloc( - getIt(), - getIt(), - getIt(), - getIt(), - getIt(), - )..add(PaymentMethodsStarted()), - child: const PaymentMethodsScreen(), - ); - }, - routes: [ - GoRoute( - path: 'add-card', - builder: (context, state) => - BlocProvider( - create: (context) => getIt(), - child: const AddCardScreen(), - ), - ), - GoRoute( - path: 'top-up', - builder: (context, state) => - BlocProvider( - create: (context) => - TopUpBloc( - getCertificatesUsecase: getIt(), - purchaseCertificateUsecase: - getIt(), - getUserCards: getIt(), - ) - ..add(LoadTopUpData()), - child: TopUpScreen(), - ), - routes: [ - GoRoute( - path: 'result/:status', - builder: (context, state) { - final modeParam = state.pathParameters['status']; - - CardBindingStatus status = CardBindingStatus.success; - String title = 'Успешно!'; - - if (modeParam == 'failed') { - status = CardBindingStatus.failed; - title = 'Извините, что-то\nпошло не так'; - } - - return CardBindingResultScreen( - status: status, - title: title, - cardImagePath: 'assets/icons/card_result_screen.png', - ); - }, - ), - ], - ), - ], - ), - GoRoute( - path: 'order-history', - builder: (context, state) => const OrderHistoryScreen(), - routes: [] - ), - GoRoute( - path: 'fine-list', - builder: (context, state) => const FineListScreen(), - routes: [] - ), - GoRoute( - path: 'fine-payment/:fineId', - builder: (context, state) { - final fineId = int.parse(state.pathParameters['fineId']!); - return FinePaymentScreen(fineId: fineId); - }, - routes: [ - GoRoute( - path: 'result/:status', - builder: (context, state) { - final modeParam = state.pathParameters['status']; - - CardBindingStatus status = CardBindingStatus.success; - String title = 'Штраф успешно\nоплачен!'; - - if (modeParam == 'failed') { - status = CardBindingStatus.failed; - title = 'Извините, что-то\nпошло не так'; - } - - return CardBindingResultScreen( - status: status, - title: title, - cardImagePath: 'assets/icons/card_result_screen.png', - redirectPath: '/home/fine-list', - ); - }, - ), - ], - ), - GoRoute( - path: 'notifications', - builder: (context, state) => const NotificationsScreen(), - ), - ], - ), - ], - - onException: (context, state, router) { - final location = state.uri.toString(); - - if (location.startsWith('com.behappy.app://')) { - - String normalizedPath = location - .replaceFirst('com.behappy.app://', '/') - .replaceAll('/?', '?') - .replaceAll(' ', ''); - - print("Нормализованный путь диплинка: $normalizedPath"); - - router.go(normalizedPath); - } else { - print('GoRouter Exception: ${state.error}'); - } - }, - - observers: [BotToastNavigatorObserver()], - - redirect: (BuildContext context, GoRouterState state) { - final authState = splashBloc.state; - final currentLocation = state.uri.toString(); - - print("inside redirect"); - print(splashBloc.state); - print(state.uri.toString()); - - if (authState is AuthInitial) { - if (currentLocation != '/splash') return '/splash'; - return null; - } - - // 2. Первый запуск или пользователь не авторизован - if (authState is AuthFirstLaunch || authState is AuthUnauthenticated) { - final isAllowed = currentLocation == '/login' || - currentLocation.startsWith('/verify') || - currentLocation == '/privacy-policy' || - currentLocation == '/license-agreement'; - - if (!isAllowed) return '/login'; - return null; - } - - // 3. Авторизация в процессе (переход от ввода телефона/SMS к PIN-коду) - if (authState is AuthInProgress) { - final isAllowed = currentLocation == '/login' || - currentLocation.startsWith('/verify') || - currentLocation == '/pin' || // ← Теперь роутер пустит сюда! - currentLocation == '/privacy-policy' || - currentLocation == '/license-agreement'; - - if (!isAllowed) return '/login'; - return null; - } - - // 4. Пользователь авторизован, но PIN еще не проверен - if (authState is AuthAuthenticated) { - final isComingFromStart = currentLocation == '/splash' || - currentLocation == '/login' || - currentLocation.startsWith('/verify') || - currentLocation == '/phone'; - - if (isComingFromStart) { - print("redirecting to pin check"); - return '/pin'; - } - return null; - } - - // 5. Пользователь полностью авторизован и PIN успешно верифицирован - if (authState is AuthPinVerified) { - final isComingFromAuth = currentLocation == '/splash' || - currentLocation == '/login' || - currentLocation.startsWith('/verify') || - currentLocation == '/pin'; - - if (isComingFromAuth) { - return '/home'; - } - return null; - } - - return null; - }, - - refreshListenable: GoRouterRefreshStream(splashBloc.stream), - ); - - CustomTransitionPage modalPage({ - required GoRouterState state, - required Widget child, - }) { - return CustomTransitionPage( - key: state.pageKey, - opaque: false, - barrierDismissible: true, - barrierColor: Colors.black54, - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return SlideTransition( - position: animation.drive( - Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).chain(CurveTween(curve: Curves.easeOutCubic)), - ), - child: child, - ); - }, - child: child, - ); - } -} - -class GoRouterRefreshStream extends ChangeNotifier { - late final StreamSubscription _subscription; - - GoRouterRefreshStream(Stream stream) { - notifyListeners(); - _subscription = stream.asBroadcastStream().listen((dynamic _) { - print("Stream updated"); - notifyListeners(); - }); - } - - @override - void dispose() { - _subscription.cancel(); - super.dispose(); - } -} +import 'dart:async'; + +import 'package:be_happy/presentation/screens/card_binding_result_screen.dart'; +import 'package:be_happy/presentation/screens/lease_agreement_screen.dart'; +import 'package:be_happy/presentation/screens/payment_rules_screen.dart'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:be_happy/core/app_colors.dart'; +import 'package:be_happy/di/service_locator.dart'; +import 'package:be_happy/domain/entities/user_profile.dart'; +import 'package:be_happy/domain/usecase/get_available_subscriptions_usecase.dart'; +import 'package:be_happy/domain/usecase/get_certificates_usecase.dart'; +import 'package:be_happy/domain/usecase/get_client_subscriptions_usecase.dart'; +import 'package:be_happy/domain/usecase/get_profile_usecase.dart'; +import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart'; +import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart'; +import 'package:be_happy/domain/usecase/is_pin_set_usecase.dart'; +import 'package:be_happy/domain/usecase/purchase_certificate_usecase.dart'; +import 'package:be_happy/presentation/event/payment_confirm_event.dart'; +import 'package:be_happy/presentation/event/pin_event.dart'; +import 'package:be_happy/presentation/event/subscription_list_event.dart'; +import 'package:be_happy/presentation/screens/block_screen.dart'; +import 'package:be_happy/presentation/screens/documents_screen.dart'; +import 'package:be_happy/presentation/screens/edit_profile_screen.dart'; +import 'package:be_happy/presentation/screens/map_screen.dart'; +import 'package:be_happy/presentation/screens/news_screen.dart'; +import 'package:be_happy/presentation/screens/onboarding_screen.dart'; +import 'package:be_happy/presentation/screens/order_history_detail_screen.dart'; +import 'package:be_happy/presentation/screens/payment_confirm_screen.dart'; +import 'package:be_happy/presentation/screens/profile_screen.dart'; +import 'package:be_happy/presentation/screens/promo_code_screen.dart'; +import 'package:be_happy/presentation/screens/qr_scan_info_screen.dart'; +import 'package:be_happy/presentation/screens/scooter_code_input_screen.dart'; +import 'package:be_happy/presentation/screens/scooter_detail_screen.dart'; +import 'package:be_happy/presentation/screens/send_photo_screen.dart'; +import 'package:be_happy/presentation/screens/subscription_list_screen.dart'; +import 'package:be_happy/presentation/screens/support_screen.dart'; +import 'package:be_happy/presentation/screens/top_up_screen.dart'; +import 'package:be_happy/presentation/viewmodel/splash_bloc.dart'; +import 'package:be_happy/presentation/viewmodel/susbcription_details_bloc.dart'; +import 'package:be_happy/presentation/viewmodel/top_up_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:go_router/go_router.dart'; +import 'package:path/path.dart'; + +import '../../domain/entities/scooter.dart'; +import '../../domain/usecase/activate_subscription_usecase.dart'; +import '../../domain/usecase/add_payment_card_usecase.dart'; +import '../../domain/usecase/book_scooter_usecase.dart'; +import '../../domain/usecase/create_pin_usecase.dart'; +import '../../domain/usecase/get_address_by_point_usecase.dart'; +import '../../domain/usecase/get_available_tariffs_usecase.dart'; +import '../../domain/usecase/get_client_orders_usecase.dart'; +import '../../domain/usecase/get_map_settings_usecase.dart'; +import '../../domain/usecase/get_payment_cards_usecase.dart'; +import '../../domain/usecase/get_pedestrian_routes_usecase.dart'; +import '../../domain/usecase/get_scooter_order_by_id_usecase.dart'; +import '../../domain/usecase/get_scooter_usecase.dart'; +import '../../domain/usecase/pay_ride_usecase.dart'; +import '../../domain/usecase/remove_payment_card_usecase.dart'; +import '../../domain/usecase/save_map_settings_usecase.dart'; +import '../../domain/usecase/set_main_payment_card_usecase.dart'; +import '../../domain/usecase/verify_pin_usecase.dart'; +import '../components/scooter_bottom_sheet.dart'; +import '../components/sheet/current_rides_sheet.dart'; +import '../components/sheet/map_settings_sheet.dart'; +import '../components/sheet/payment_method_sheet.dart'; +import '../components/sheet/reserved_ride_sheet.dart'; +import '../components/sheet/tariff_sheet.dart'; +import '../event/current_rides_event.dart'; +import '../event/edit_profile_event.dart'; +import '../event/map_settings_modal_event.dart'; +import '../event/payment_methods_event.dart'; +import '../event/profile_event.dart'; +import '../event/scooter_detail_modal_event.dart'; +import '../event/subscription_details_event.dart'; +import '../event/tariff_sheet_event.dart'; +import '../event/top_up_event.dart'; +import '../screens/add_card_screen.dart'; // ← новый импорт +import '../screens/company_details_screen.dart'; +import '../screens/fine_list_screen.dart'; +import '../screens/fine_payment_screen.dart'; +import '../screens/license_agreement_screen.dart'; +import '../screens/notifications_screen.dart'; +import '../screens/order_history_screen.dart'; +import '../screens/payment_methods_screen.dart'; +import '../screens/phone_login_screen.dart'; +import '../screens/phone_screen.dart'; +import '../screens/pin_login_screen.dart'; +import '../screens/privacy_policy_screen.dart'; +import '../screens/qr_scan_screen.dart'; +import '../screens/splash_screen.dart'; +import '../screens/subscription_details_screen.dart'; +import '../state/splash_state.dart'; +import '../viewmodel/add_card_bloc.dart'; +import '../viewmodel/current_rides_bloc.dart'; +import '../viewmodel/edit_profile_bloc.dart'; +import '../viewmodel/map_settings_modal_bloc.dart'; +import '../viewmodel/payment_confirm_bloc.dart'; +import '../viewmodel/payment_method_sheet_bloc.dart'; +import '../viewmodel/payment_methods_bloc.dart'; +import '../viewmodel/pin_bloc.dart'; +import '../viewmodel/profile_bloc.dart'; +import '../viewmodel/scooter_code_bloc.dart'; +import '../viewmodel/scooter_detail_bloc.dart'; +import '../viewmodel/scooter_detail_modal_bloc.dart'; +import '../viewmodel/subscription_list_bloc.dart'; +import '../viewmodel/tariff_sheet_bloc.dart'; // ← новый импорт + +class AppRouter { + final SplashBloc splashBloc; + + AppRouter(this.splashBloc); + + late final GoRouter router = GoRouter( + debugLogDiagnostics: true, + initialLocation: '/splash', + + routes: [ + GoRoute( + path: '/splash', + builder: (context, state) => const SplashScreen(), + ), + GoRoute(path: '/login', builder: (context, state) => const PhoneScreen()), + GoRoute( + path: '/verify', + builder: (context, state) { + final phone = state.uri.queryParameters['phone']; + if (phone != null) { + return PhoneLoginScreen(phoneNumber: phone, tempToken: ''); + } + throw Exception("Incorrect phone"); + }, + ), + GoRoute( + path: '/block', + builder: (context, state) => const BlockedScreen(), + ), + GoRoute( + path: '/pin', + builder: (context, state) => + BlocProvider( + create: (context) => + PinBloc( + createPinUseCase: getIt(), + verifyPinUsecase: getIt(), + isPinSetUsecase: getIt(), + getProfileUseCase: getIt(), + ) + ..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()..add(EditProfileStarted()), + child: EditProfileScreen( + profile: profile, + isRegistration: isRegistration, + ), + ); + }, + ), + + GoRoute( + path: '/privacy-policy', + builder: (context, state) => const PrivacyPolicyScreen(), + ), + GoRoute( + path: '/license-agreement', + builder: (context, state) => const LicenseAgreementScreen(), + ), + GoRoute( + path: '/card-result', + builder: (context, state) { + final modeParam = state.uri.queryParameters['status']; + + CardBindingStatus status = CardBindingStatus.success; + String title = 'Карта успешно\nпривязана!'; + + if (modeParam == 'failed') { + status = CardBindingStatus.failed; + title = 'Извините, что-то\nпошло не так'; + } + + return CardBindingResultScreen( + status: status, + title: title, + cardImagePath: 'assets/icons/card_result_screen.png', + ); + }, + ), + GoRoute( + path: '/home', + builder: (context, state) => const MapScreen(), + routes: [ + //Modal Bottom Sheets + GoRoute( + path: 'scooter-sheet', + pageBuilder: (context, state) { + final data = state.extra as Map; + + final scooters = data['scooters'] as List; + final location = data['currentLocation'] as Position; + + return modalPage( + state: state, + child: Material( + type: MaterialType.transparency, + child: BlocProvider( + create: (context) => + ScooterDetailModalBloc( + getIt(), + getIt(), + getIt(), + ) + ..add( + ScooterDetailModalStarted( + scooters, + location.latitude, + location.longitude, + ), + ), + child: ScooterBottomSheet(), + ), + ), + ); + }, + ), + GoRoute( + path: 'tarif-sheet', + pageBuilder: (context, state) { + final scooter = state.extra as Scooter; + + return modalPage( + state: state, + child: Material( + type: MaterialType.transparency, + child: BlocProvider( + create: (context) => + TariffSheetBloc( + getIt(), + getIt(), + getIt(), + getIt(), + ), + child: TariffSheet(scooter: scooter), + ), + ), + ); + }, + ), + GoRoute( + path: 'current-rides-sheet', + pageBuilder: (context, state) { + return modalPage( + state: state, + child: Material( + type: MaterialType.transparency, + child: BlocProvider( + create: (context) => + CurrentRidesBloc(getIt()) + ..add(LoadClientOrders()), + child: CurrentRidesSheet(), + ), + ), + ); + }, + ), + GoRoute( + path: 'payment-method-sheet', + pageBuilder: (context, state) { + return modalPage( + state: state, + child: Material( + type: MaterialType.transparency, + child: BlocProvider( + create: (context) => + PaymentMethodSheetBloc( + getIt(), + getIt(), + getIt(), + getIt(), + ), + child: PaymentMethodSheet(), + ), + ), + ); + }, + ), + + GoRoute( + path: 'map-settings-sheet', + pageBuilder: (context, state) { + return modalPage( + state: state, + child: Material( + type: MaterialType.transparency, + child: BlocProvider( + create: (context) => + MapSettingsModalBloc( + getIt(), + getIt(), + ) + ..add(MapSettingsModalStarted()), + child: MapSettingsSheet(), + ), + ), + ); + }, + ), + //Sub screens + GoRoute( + path: 'profile', + builder: (context, state) { + final bloc = getIt(); + + if (!bloc.isClosed) { + bloc.add(ProfileStarted()); + } + + return BlocProvider.value( + value: bloc, + child: const ProfileScreen(), + ); + }, + + routes: [ + GoRoute( + path: 'edit', + builder: (context, state) => + BlocProvider( + create: (context) => + getIt() + ..add(EditProfileStarted()), + child: EditProfileScreen( + profile: UserProfile( + name: '', + birthDate: '', + phone: '', + balance: 23, + email: '', + ), + ), + ), + ), + ], + ), + GoRoute( + path: 'scooter/:id', + builder: (context, state) { + List extras = []; + if (state.extra != null) { + extras = state.extra as List; + } + String address = extras[0]; + double distance = extras[1]; + + return BlocProvider( + create: (context) => getIt(), + child: ScooterDetailScreen(address: address, distance: distance), + ); + } + ), + GoRoute( + path: 'order-photos/:orderId', + builder: (context, state) { + int orderId = int.parse(state.pathParameters['orderId']!); + return SendPhotoScreen(orderId: orderId); + }, + ), + GoRoute( + path: 'checkout/:orderId', + builder: (context, state) { + int orderId = int.parse(state.pathParameters['orderId']!); + List photoIds = []; + if (state.extra != null) { + photoIds = state.extra as List; + } + + return BlocProvider( + create: (context) => + PaymentConfirmBloc( + getIt(), + getIt(), + getIt(), + getIt(), + ) + ..add(PaymentConfirmStarted(orderId)), + child: PaymentConfirmScreen( + orderId: orderId, + photoIds: photoIds, + ), + ); + }, + ), + GoRoute( + path: 'support', + builder: (context, state) => const SupportScreen(), + ), + GoRoute( + path: 'documents', + builder: (context, state) => const DocumentsScreen(), + routes: [ + GoRoute( + path: 'payment-rules', + builder: (context, state) => const PaymentRulesScreen(), + ), + GoRoute( + path: 'lease-agreement', + builder: (context, state) => const LeaseAgreementScreen(), + ), + GoRoute( + path: 'company-details', + builder: (context, state) => const CompanyDetailsScreen(), + ), + ] + ), + GoRoute( + path: 'promo', + builder: (context, state) => const PromoCodeScreen(), + ), + GoRoute( + path: 'subscriptions', + builder: (context, state) { + return BlocProvider( + create: (context) => + SubscriptionListBloc( + getAvailableSubscriptionsUsecase: + getIt(), + getClientSubscriptionsUsecase: + getIt(), + ) + ..add(LoadSubscriptionsEvent()), + child: SubscriptionsListScreen(), + ); + }, + routes: [ + GoRoute( + path: ':id', + builder: (context, state) { + return BlocProvider( + create: (context) => + SubscriptionDetailsBloc( + getIt(), + getIt(), + getIt(), + getIt(), + ) + ..add( + LoadDetailsEvent( + int.parse(state.pathParameters['id']!), + ), + ), + child: SubscriptionDetailsScreen(subscriptionId: 1), + ); + }, + ), + ], + ), + GoRoute( + path: 'rules', + builder: (context, state) => const OnboardingScreen(), + ), + GoRoute( + path: 'news', + builder: (context, state) => const NewsScreen(), + ), + GoRoute( + path: 'qr-info', + builder: (context, state) => const QRScanInfoScreen(), + routes: [ + GoRoute( + path: 'qr-scan', + builder: (context, state) => const QrScanScreen(), + ), + GoRoute( + path: 'qr-input', + builder: (context, state) => + BlocProvider( + create: (context) => + ScooterCodeBloc( + getScooterByTitleUsecase: getIt< + GetScooterByTitleUsecase>(), getAddressUsecase: getIt(), getPedestrianRoutesUsecase: getIt(), + ), + child: ScooterCodeInputScreen(), + ), + ), + ] + ), + GoRoute( + path: 'payment-methods', + builder: (context, state) { + final refreshId = state.uri.queryParameters['refreshId']; + + return BlocProvider( + key: refreshId != null ? ValueKey(refreshId) : null, + create: (context) => PaymentMethodsBloc( + getIt(), + getIt(), + getIt(), + getIt(), + getIt(), + )..add(PaymentMethodsStarted()), + child: const PaymentMethodsScreen(), + ); + }, + routes: [ + GoRoute( + path: 'add-card', + builder: (context, state) => + BlocProvider( + create: (context) => getIt(), + child: const AddCardScreen(), + ), + ), + GoRoute( + path: 'top-up', + builder: (context, state) => + BlocProvider( + create: (context) => + TopUpBloc( + getCertificatesUsecase: getIt(), + purchaseCertificateUsecase: + getIt(), + getUserCards: getIt(), + ) + ..add(LoadTopUpData()), + child: TopUpScreen(), + ), + routes: [ + GoRoute( + path: 'result/:status', + builder: (context, state) { + final modeParam = state.pathParameters['status']; + + CardBindingStatus status = CardBindingStatus.success; + String title = 'Успешно!'; + + if (modeParam == 'failed') { + status = CardBindingStatus.failed; + title = 'Извините, что-то\nпошло не так'; + } + + return CardBindingResultScreen( + status: status, + title: title, + cardImagePath: 'assets/icons/card_result_screen.png', + ); + }, + ), + ], + ), + ], + ), + GoRoute( + path: 'order-history', + builder: (context, state) => const OrderHistoryScreen(), + routes: [] + ), + GoRoute( + path: 'fine-list', + builder: (context, state) => const FineListScreen(), + routes: [] + ), + GoRoute( + path: 'fine-payment/:fineId', + builder: (context, state) { + final fineId = int.parse(state.pathParameters['fineId']!); + return FinePaymentScreen(fineId: fineId); + }, + routes: [ + GoRoute( + path: 'result/:status', + builder: (context, state) { + final modeParam = state.pathParameters['status']; + + CardBindingStatus status = CardBindingStatus.success; + String title = 'Штраф успешно\nоплачен!'; + + if (modeParam == 'failed') { + status = CardBindingStatus.failed; + title = 'Извините, что-то\nпошло не так'; + } + + return CardBindingResultScreen( + status: status, + title: title, + cardImagePath: 'assets/icons/card_result_screen.png', + redirectPath: '/home/fine-list', + ); + }, + ), + ], + ), + GoRoute( + path: 'notifications', + builder: (context, state) => const NotificationsScreen(), + ), + ], + ), + ], + + onException: (context, state, router) { + final location = state.uri.toString(); + + if (location.startsWith('com.behappy.app://')) { + + String normalizedPath = location + .replaceFirst('com.behappy.app://', '/') + .replaceAll('/?', '?') + .replaceAll(' ', ''); + + print("Нормализованный путь диплинка: $normalizedPath"); + + router.go(normalizedPath); + } else { + print('GoRouter Exception: ${state.error}'); + } + }, + + observers: [BotToastNavigatorObserver()], + + redirect: (BuildContext context, GoRouterState state) { + final authState = splashBloc.state; + final currentLocation = state.uri.toString(); + + print("inside redirect"); + print(splashBloc.state); + print(state.uri.toString()); + + if (authState is AuthInitial) { + if (currentLocation != '/splash') return '/splash'; + return null; + } + + // 2. Первый запуск или пользователь не авторизован + if (authState is AuthFirstLaunch || authState is AuthUnauthenticated) { + final isAllowed = currentLocation == '/login' || + currentLocation.startsWith('/verify') || + currentLocation == '/privacy-policy' || + currentLocation == '/license-agreement'; + + if (!isAllowed) return '/login'; + return null; + } + + // 3. Авторизация в процессе (ввод телефона -> SMS -> PIN -> Заполнение профиля) + if (authState is AuthInProgress) { + final isAllowed = currentLocation == '/login' || + currentLocation.startsWith('/verify') || + currentLocation == '/pin' || + currentLocation.startsWith('/edit-profile') || + currentLocation == '/privacy-policy' || + currentLocation == '/license-agreement'; + + if (!isAllowed) return '/login'; + return null; + } + + // 4. Пользователь авторизован, но PIN еще не проверен + if (authState is AuthAuthenticated) { + final isComingFromStart = currentLocation == '/splash' || + currentLocation == '/login' || + currentLocation.startsWith('/verify') || + currentLocation == '/phone'; + + if (isComingFromStart) { + print("redirecting to pin check"); + return '/pin'; + } + return null; + } + + // 5. Пользователь полностью авторизован и PIN успешно верифицирован + if (authState is AuthPinVerified) { + if (currentLocation.startsWith('/edit-profile')) { + return null; + } + + final isComingFromAuth = currentLocation == '/splash' || + currentLocation == '/login' || + currentLocation.startsWith('/verify') || + currentLocation == '/pin'; + + if (isComingFromAuth) { + return '/home'; + } + return null; + } + + return null; + }, + + refreshListenable: GoRouterRefreshStream(splashBloc.stream), + ); + + CustomTransitionPage modalPage({ + required GoRouterState state, + required Widget child, + }) { + return CustomTransitionPage( + key: state.pageKey, + opaque: false, + barrierDismissible: true, + barrierColor: Colors.black54, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).chain(CurveTween(curve: Curves.easeOutCubic)), + ), + child: child, + ); + }, + child: child, + ); + } +} + +class GoRouterRefreshStream extends ChangeNotifier { + late final StreamSubscription _subscription; + + GoRouterRefreshStream(Stream stream) { + notifyListeners(); + _subscription = stream.asBroadcastStream().listen((dynamic _) { + print("Stream updated"); + notifyListeners(); + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} diff --git a/lib/presentation/screens/edit_profile_screen.dart b/lib/presentation/screens/edit_profile_screen.dart index 25b0c67..31cc928 100644 --- a/lib/presentation/screens/edit_profile_screen.dart +++ b/lib/presentation/screens/edit_profile_screen.dart @@ -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 createState() => _EditProfileScreenState(); @@ -52,6 +59,36 @@ class _EditProfileScreenState extends State { } } 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 { 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().add(EditProfileSubmitted(updatedProfile)); context.read().add(ProfileUpdated()); - } Future _selectDate(BuildContext context) async { @@ -90,6 +126,8 @@ class _EditProfileScreenState extends State { @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 { phoneController.text = profile.phone; emailController.text = profile.email; birthDateController.text = profile.birthDate; - } if (state.isSuccess) { getIt().add(ProfileUpdated()); - context.pop(); + if (widget.isRegistration) { + context.read().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 { 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 { birthDateController, iconPath: 'assets/icons/edit.png', readOnly: true, - onTap: () => _selectDate( - context, - ), + onTap: () => _selectDate(context), ), _Field( 'Телефон', @@ -156,13 +201,13 @@ class _EditProfileScreenState extends State { state.isSaving ? const CircularProgressIndicator(color: Colors.white) : GradientButton( - text: 'Сохранить изменения', - onTap: () => _submit(context), - width: double.infinity, - height: 56, - fontSize: 16, - showArrows: true, - ), + text: widget.isRegistration ? 'Продолжить' : 'Сохранить изменения', + onTap: isButtonEnabled ? () => _submit(context) : null, + width: double.infinity, + height: 56, + fontSize: 16, + showArrows: true, + ), const SizedBox(height: 24), ], ); @@ -173,7 +218,6 @@ class _EditProfileScreenState extends State { ), ); } - } class _Field extends StatelessWidget { diff --git a/lib/presentation/screens/payment_confirm_screen.dart b/lib/presentation/screens/payment_confirm_screen.dart index ce2161c..6480d14 100644 --- a/lib/presentation/screens/payment_confirm_screen.dart +++ b/lib/presentation/screens/payment_confirm_screen.dart @@ -115,6 +115,8 @@ class _PaymentConfirmScreenContent extends StatelessWidget { ); } + final hasNoDebt = (order?.unpayTotalPrice ?? 0) == 0; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -141,7 +143,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget { Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ _buildInfoLabel('Стоимость поездки:'), _buildValueRow( @@ -154,7 +156,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget { Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ _buildInfoLabel('Оплачено:'), _buildValueRow( @@ -183,7 +185,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget { const SizedBox(height: 12), Row( mainAxisAlignment: - MainAxisAlignment.center, + MainAxisAlignment.center, children: [ Image.asset( 'assets/icons/money_icon.png', @@ -204,114 +206,115 @@ class _PaymentConfirmScreenContent extends StatelessWidget { ), ), - const SizedBox(height: 40), - - // 🔹 КАРТА ОПЛАТЫ - (state.useBalance || state.selectedCard != null) - ? Padding(padding: const EdgeInsets.symmetric(horizontal: 20), - child: PaymentOption( - title: state.useBalance ? 'Баланс' : "", - subtitle: state.useBalance - ? '${state.userBalance.toStringAsFixed(2)} BYN' - : '${state.selectedCard!.cardLastNumber}', - isSelected: true, - onTap: () async { - final result = await showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (innerContext) => BlocProvider( - create: (context) => PaymentMethodSheetBloc( - getIt(), - getIt(), - getIt(), - getIt(), - )..add(PaymentMethodSheetStarted()), - child: PaymentMethodSheet( - initialSelectedCard: state.useBalance ? null : state.selectedCard, - ), - ), - ); - - if (result != null) { - if (result is PaymentCard) { - context.read().add(PaymentCardChanged(result)); - } else if (result == 'balance') { - context.read().add(SelectBalancePressed()); - } - } - }, - ), - ) - : Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - child: Container( - height: 56, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 24, - ), - border: Border.all( - color: Colors.white.withOpacity( - 0.4, - ), - width: 1, + // 🔹 КАРТА ОПЛАТЫ (Отображается только если есть долг) + if (!hasNoDebt) ...[ + const SizedBox(height: 40), + (state.useBalance || state.selectedCard != null) + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: PaymentOption( + title: state.useBalance ? 'Баланс' : "", + subtitle: state.useBalance + ? '${state.userBalance.toStringAsFixed(2)} BYN' + : '${state.selectedCard!.cardLastNumber}', + isSelected: true, + onTap: () async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (innerContext) => BlocProvider( + create: (context) => PaymentMethodSheetBloc( + getIt(), + getIt(), + getIt(), + getIt(), + )..add(PaymentMethodSheetStarted()), + child: PaymentMethodSheet( + initialSelectedCard: state.useBalance ? null : state.selectedCard, ), ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - context.pushReplacement( - '/home/payment-method-sheet', - ); + ); - }, - borderRadius: BorderRadius.circular( - 24, - ), - child: Center( - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Text( - 'Способ оплаты', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: - FontWeight.w600, - ), - ), - const SizedBox(width: 8), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white - .withOpacity(0.6), - ), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white - .withOpacity(0.4), - ), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white - .withOpacity(0.2), - ), - ], + if (result != null) { + if (result is PaymentCard) { + context.read().add(PaymentCardChanged(result)); + } else if (result == 'balance') { + context.read().add(SelectBalancePressed()); + } + } + }, + ), + ) + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Container( + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 24, + ), + border: Border.all( + color: Colors.white.withOpacity( + 0.4, + ), + width: 1, + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + context.pushReplacement( + '/home/payment-method-sheet', + ); + }, + borderRadius: BorderRadius.circular( + 24, + ), + child: Center( + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + const Text( + 'Способ оплаты', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: + FontWeight.w600, + ), ), - ), + const SizedBox(width: 8), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white + .withOpacity(0.6), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white + .withOpacity(0.4), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white + .withOpacity(0.2), + ), + ], ), ), ), ), + ), + ), + ], ], ); }, @@ -322,45 +325,53 @@ class _PaymentConfirmScreenContent extends StatelessWidget { Padding( padding: const EdgeInsets.all(24.0), child: - BlocConsumer( - listener: (context, state) { - if (state.status == PaymentConfirmStatus.success && - state.paymentCompleted) { + BlocConsumer( + listener: (context, state) { + if (state.status == PaymentConfirmStatus.success && + state.paymentCompleted) { + context.go('/home'); + } else if (state.status == + PaymentConfirmStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + state.errorMessage ?? 'Ошибка оплаты', + ), + ), + ); + } + }, + 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 || + (!hasNoDebt && !state.useBalance && state.selectedCard == null)) + ? null + : () { + if (hasNoDebt) { + context.read().add(ClearMapPlacemarks()); context.go('/home'); - } else if (state.status == - PaymentConfirmStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - state.errorMessage ?? 'Ошибка оплаты', - ), + } else { + context.read().add( + PayRide( + cardId: state.useBalance ? null : state.selectedCard?.id, + isBalance: state.useBalance, + orderId: orderId, + photoIds: photoIds, ), ); } }, - builder: (context, state) { - return GradientButton( - text: state.status == PaymentConfirmStatus.loading - ? 'Обработка...' - : 'Оплатить', - showArrows: true, - height: 56, - width: double.infinity, - onTap: (state.status == PaymentConfirmStatus.loading || (!state.useBalance && state.selectedCard == null)) - ? null - : () { - context.read().add( - PayRide( - cardId: state.useBalance ? null : state.selectedCard?.id, - isBalance: state.useBalance, - orderId: orderId, - 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: [ @@ -405,4 +414,4 @@ class _PaymentConfirmScreenContent extends StatelessWidget { final m = minutes % 60; return '${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}'; } -} +} \ No newline at end of file diff --git a/lib/presentation/screens/pin_login_screen.dart b/lib/presentation/screens/pin_login_screen.dart index e6aed5d..d24938e 100644 --- a/lib/presentation/screens/pin_login_screen.dart +++ b/lib/presentation/screens/pin_login_screen.dart @@ -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 { return BlocListener( listener: (context, state) { if (state is PinSuccess) { - context.read().add(PinVerificationSuccess()); - context.go("/home"); + if (state.isProfileIncomplete) { + // Переходим на заполнение профиля, если хотя бы одно поле пустое + context.go('/edit-profile?isRegistration=true'); + } else { + // Все поля заполнены — завершаем авторизацию и идем на главный экран + context.read().add(PinVerificationSuccess()); + context.go('/home'); + } } if (state.error != null) { controller.clear(); @@ -128,7 +134,6 @@ class _PinLoginScreenState extends State { const SizedBox(height: 12), - // Блок ошибки if (state.error != null) Text( state.error!, @@ -157,14 +162,13 @@ class _PinLoginScreenState extends State { "Забыли PIN-код?", style: TextStyle( color: AppColors.whiteText, - decoration: TextDecoration.underline, + decoration: TextDecoration.underline, fontSize: 14, ), ), ), const SizedBox(height: 40), - ], ); }, diff --git a/lib/presentation/state/pin_state.dart b/lib/presentation/state/pin_state.dart index 38b5ac8..fd56d1e 100644 --- a/lib/presentation/state/pin_state.dart +++ b/lib/presentation/state/pin_state.dart @@ -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 get props => [isProfileIncomplete]; } \ No newline at end of file diff --git a/lib/presentation/viewmodel/pin_bloc.dart b/lib/presentation/viewmodel/pin_bloc.dart index 77e675b..88a96c9 100644 --- a/lib/presentation/viewmodel/pin_bloc.dart +++ b/lib/presentation/viewmodel/pin_bloc.dart @@ -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 { 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(_onPinChanged); on(_onPinSubmitted); @@ -46,35 +48,52 @@ class PinBloc extends Bloc { 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 _onPinScreenStarted(PinScreenStarted event, Emitter emit) async { + FutureOr _onPinScreenStarted( + PinScreenStarted event, + Emitter emit, + ) async { try { final bool hasPin = await isPinSetUsecase(); if (hasPin) { - // Пин ЕСТЬ в базе — значит, нужно его ВВЕСТИ (Login) emit(const PinLoginInProgress(pin: '')); } else { - // Пина НЕТ в базе — значит, нужно его СОЗДАТЬ (Create) emit(const PinCreateInProgress(pin: '')); } } catch (e) {