Files
be_happy_public/lib/presentation/navigation/app_router.dart

756 lines
28 KiB
Dart

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<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(),
),
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<String, dynamic>;
final scooters = data['scooters'] as List<Scooter>;
final location = data['currentLocation'] as Position;
return modalPage(
state: state,
child: Material(
type: MaterialType.transparency,
child: BlocProvider(
create: (context) =>
ScooterDetailModalBloc(
getIt<GetAddressByPointUsecase>(),
getIt<GetScooterUsecase>(),
getIt<GetPedestrianRoutesUsecase>(),
)
..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<GetAvailableTariffsUsecase>(),
getIt<GetProfileUseCase>(),
getIt<GetPaymentCardsUsecase>(),
getIt<BookScooterUsecase>(),
),
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<GetClientOrdersUsecase>())
..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<GetPaymentCardsUsecase>(),
getIt<SetMainPaymentCardUsecase>(),
getIt<GetProfileUseCase>(),
getIt<AddPaymentCardUsecase>(),
),
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<GetMapSettingsUsecase>(),
getIt<SaveMapSettingsUsecase>(),
)
..add(MapSettingsModalStarted()),
child: MapSettingsSheet(),
),
),
);
},
),
//Sub screens
GoRoute(
path: 'profile',
builder: (context, state) {
final bloc = getIt<ProfileBloc>();
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<EditProfileBloc>()
..add(EditProfileStarted()),
child: EditProfileScreen(
profile: UserProfile(
name: '',
birthDate: '',
phone: '',
balance: 23,
email: '',
),
),
),
),
],
),
GoRoute(
path: 'scooter/:id',
builder: (context, state) {
List<dynamic> extras = [];
if (state.extra != null) {
extras = state.extra as List<dynamic>;
}
String address = extras[0];
double distance = extras[1];
return BlocProvider(
create: (context) => getIt<ScooterDetailBloc>(),
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<int> photoIds = [];
if (state.extra != null) {
photoIds = state.extra as List<int>;
}
return BlocProvider(
create: (context) =>
PaymentConfirmBloc(
getIt<PayRideUsecase>(),
getIt<GetScooterOrderByIdUsecase>(),
getIt<GetPaymentCardsUsecase>(),
getIt<GetProfileUseCase>(),
)
..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<GetAvailableSubscriptionsUsecase>(),
getClientSubscriptionsUsecase:
getIt<GetClientSubscriptionsUsecase>(),
)
..add(LoadSubscriptionsEvent()),
child: SubscriptionsListScreen(),
);
},
routes: [
GoRoute(
path: ':id',
builder: (context, state) {
return BlocProvider(
create: (context) =>
SubscriptionDetailsBloc(
getIt<GetSubscriptionByIdUsecase>(),
getIt<GetPaymentCardsUsecase>(),
getIt<ActivateSubscriptionUsecase>(),
getIt<GetClientSubscriptionsUsecase>(),
)
..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<GetAddressByPointUsecase>(), getPedestrianRoutesUsecase: getIt<GetPedestrianRoutesUsecase>(),
),
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<GetPaymentCardsUsecase>(),
getIt<RemovePaymentCardUsecase>(),
getIt<SetMainPaymentCardUsecase>(),
getIt<GetProfileUseCase>(),
getIt<AddPaymentCardUsecase>(),
)..add(PaymentMethodsStarted()),
child: const PaymentMethodsScreen(),
);
},
routes: [
GoRoute(
path: 'add-card',
builder: (context, state) =>
BlocProvider(
create: (context) => getIt<AddCardBloc>(),
child: const AddCardScreen(),
),
),
GoRoute(
path: 'top-up',
builder: (context, state) =>
BlocProvider(
create: (context) =>
TopUpBloc(
getCertificatesUsecase: getIt<GetCertificatesUsecase>(),
purchaseCertificateUsecase:
getIt<PurchaseCertificateUsecase>(),
getUserCards: getIt<GetPaymentCardsUsecase>(),
)
..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<dynamic> _subscription;
GoRouterRefreshStream(Stream<dynamic> stream) {
notifyListeners();
_subscription = stream.asBroadcastStream().listen((dynamic _) {
print("Stream updated");
notifyListeners();
});
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}