From e94681266ae6ebe4bcb710bca294238d2b355651 Mon Sep 17 00:00:00 2001 From: misha Date: Tue, 14 Jul 2026 13:43:46 +0300 Subject: [PATCH] feat(sbp_payments): added new layoute for sbp banks and rederict to chosen bank, and added tests for layoute --- lib/core/app_router.dart | 1 - .../chat/presentation/pages/chat_screen.dart | 1 - .../widgets/notification_banner.dart | 29 +-- .../widgets/sbp_banks_bottom_sheet.dart | 224 ++++++++++++++++++ lib/generated/intl/messages_en.dart | 9 + lib/generated/intl/messages_ru.dart | 9 + lib/generated/l10n.dart | 30 +++ lib/l10n/intl_en.arb | 3 + lib/l10n/intl_ru.arb | 5 +- .../widgets/sbp_banks_bottom_sheet_test.dart | 187 +++++++++++++++ 10 files changed, 471 insertions(+), 27 deletions(-) create mode 100644 lib/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart create mode 100644 test/features/payment/presentation/widgets/sbp_banks_bottom_sheet_test.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 391a8650..9b6f9610 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -133,7 +133,6 @@ class AppRouter { final orderId = effectiveSegments[3]; if (orderId.isEmpty) return null; developer.log('Deep link: finance/alfa/return/$orderId', name: 'AppRouter'); - print('Deep link: finance/alfa/return/$orderId'); return '${RouteConstants.chatScreen}?orderId=$orderId'; } diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index a8415590..8a2d42e6 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -76,7 +76,6 @@ class _ChatScreenState extends State { orderId: _effectiveUuid == null ? widget.orderId : null, sendInitialMessage: widget.sendInitialMessage ?? false, ); - print('uuid: $uuid'); if (uuid != null && mounted) { setState(() => _effectiveUuid = uuid); } diff --git a/lib/features/chat/presentation/widgets/notification_banner.dart b/lib/features/chat/presentation/widgets/notification_banner.dart index c2a8069b..59650d6c 100644 --- a/lib/features/chat/presentation/widgets/notification_banner.dart +++ b/lib/features/chat/presentation/widgets/notification_banner.dart @@ -2,15 +2,14 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; -import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart'; import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,7 +28,6 @@ class NotificationBanner extends StatefulWidget { } class _NotificationBannerState extends State { - bool isLoading = false; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); @@ -54,13 +52,7 @@ class _NotificationBannerState extends State { padding: EdgeInsets.only(top: 12, right: 8, left: 12), child: _StarIcon(), ), - if (isLoading) - const Padding( - padding: EdgeInsets.only(left: 40, top: 24), - child: Loader(), - ) - else - Padding( + Padding( padding: const EdgeInsets.only(top: 16), child: Column( mainAxisSize: MainAxisSize.min, @@ -111,21 +103,10 @@ class _NotificationBannerState extends State { ), const SizedBox(height: 12), GestureDetector( - onTap: () async { + onTap: () { final planId = (context.read().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; - setState(() { - isLoading = true; - }); - final paymentLink = await context.read().getPaymentLink(); - setState(() { - isLoading = false; - }); - if (paymentLink != null && planId != null) { - if (!context.mounted) return; - GoRouter.of(context).pushReplacement( - PaymentScreen.path, - extra: {'url': paymentLink, 'planId': planId}, - ); + if (planId != null) { + showSbpBanksBottomSheet(context, planId: planId); } }, child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)), diff --git a/lib/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart b/lib/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart new file mode 100644 index 00000000..5b0f95e7 --- /dev/null +++ b/lib/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart @@ -0,0 +1,224 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart'; +import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sbp/models/c2bmembers_model.dart'; + +/// Показывает bottom sheet выбора банка СБП для оплаты консультации. +Future showSbpBanksBottomSheet(BuildContext context, {required int planId}) { + final height = MediaQuery.sizeOf(context).height * 0.55; + return showBottomSheetWithBlur( + context: context, + height: height, + child: SbpBanksBottomSheet(planId: planId), + ); +} + +/// Bottom sheet со списком установленных банков СБП. +/// +/// При открытии вызывает [ConsultationPaymentCubit.startPayment] с [planId] +/// и показывает [Loader], пока загружаются checkout_url и список банков. +class SbpBanksBottomSheet extends StatefulWidget { + const SbpBanksBottomSheet({super.key, required this.planId, this.cubit}); + + final int planId; + final ConsultationPaymentCubit? cubit; + + @override + State createState() => _SbpBanksBottomSheetState(); +} + +class _SbpBanksBottomSheetState extends State { + late final ConsultationPaymentCubit _cubit; + late final bool _ownsCubit; + + @override + void initState() { + super.initState(); + _ownsCubit = widget.cubit == null; + _cubit = widget.cubit ?? sl(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _cubit.startPayment(planId: widget.planId); + }); + } + + @override + void dispose() { + if (_ownsCubit) { + _cubit.close(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocProvider.value( + value: _cubit, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 8), + const BottomSheetHandlerBar(), + const SizedBox(height: 20), + Text( + s.choose_bank_for_payment, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Loader(); + } + if (state.errorMessage != null) { + return _ErrorContent( + message: s.sbp_payment_error, + onOk: () { + final navigator = Navigator.of(context); + if (navigator.canPop()) { + navigator.pop(); + } + }, + ); + } + if (state.banks.isEmpty) { + return Center( + child: Text( + s.no_installed_banks, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + ); + } + return ListView.separated( + itemCount: state.banks.length, + separatorBuilder: (_, _) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final bank = state.banks[index]; + return _BankTile(bank: bank, onTap: () => _onBankTap(bank)); + }, + ); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + Future _onBankTap(C2bmemberModel bank) async { + final opened = await _cubit.openBank(bank: bank); + if (opened && mounted) { + final navigator = Navigator.of(context); + if (navigator.canPop()) { + navigator.pop(); + } + } + } +} + +class _BankTile extends StatelessWidget { + const _BankTile({required this.bank, required this.onTap}); + + final C2bmemberModel bank; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final name = bank.localName ?? bank.bankName; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4), + child: Row( + children: [ + _BankIcon(bank: bank), + const SizedBox(width: 12), + Expanded( + child: Text(name, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), + ), + ], + ), + ), + ); + } +} + +class _BankIcon extends StatelessWidget { + const _BankIcon({required this.bank}); + + final C2bmemberModel bank; + + @override + Widget build(BuildContext context) { + if (bank.icon.isNotEmpty) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: bank.logoURL, + width: 40, + height: 40, + placeholder: (context, url) => _placeholder(), + errorWidget: (context, url, error) => _placeholder(), + ), + ); + } + return _placeholder(); + } + + Widget _placeholder() { + return Container( + width: 40, + height: 40, + decoration: BoxDecoration(color: appTheme.blueA400.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8)), + child: Icon(Icons.account_balance, color: appTheme.blueA400, size: 22), + ); + } +} + +class _ErrorContent extends StatelessWidget { + const _ErrorContent({required this.message, required this.onOk}); + + final String message; + final VoidCallback onOk; + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + message, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + CustomOutlinedButton( + text: s.ok, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), + onPressed: onOk, + ), + ], + ); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 096562d6..d79fe75d 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -173,6 +173,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "chest": MessageLookupByLibrary.simpleMessage("Грудь"), "chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"), + "choose_bank_for_payment": MessageLookupByLibrary.simpleMessage( + "Выберите банк", + ), "choose_date": MessageLookupByLibrary.simpleMessage("Выбрать дату"), "choose_diet_duration": MessageLookupByLibrary.simpleMessage( "Выберите срок диеты", @@ -588,6 +591,9 @@ class MessageLookup extends MessageLookupByLibrary { "no_default_cards": MessageLookupByLibrary.simpleMessage( "У вас нет привязанных карт. Что бы начать работу, привяжите карту", ), + "no_installed_banks": MessageLookupByLibrary.simpleMessage( + "Нет установленных банков", + ), "not_liked_groceries": MessageLookupByLibrary.simpleMessage( "Не любимые продукты", ), @@ -737,6 +743,9 @@ class MessageLookup extends MessageLookupByLibrary { "reply": MessageLookupByLibrary.simpleMessage("Ответить"), "resend": MessageLookupByLibrary.simpleMessage("Отправить повторно"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), + "sbp_payment_error": MessageLookupByLibrary.simpleMessage( + "Не удалось начать оплату. Попробуйте позже", + ), "send": MessageLookupByLibrary.simpleMessage("Отправить"), "send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"), "services": MessageLookupByLibrary.simpleMessage("Оказание услуг"), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 86adc84c..b9c4300d 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -62,6 +62,9 @@ class MessageLookup extends MessageLookupByLibrary { "chat": MessageLookupByLibrary.simpleMessage("Чат"), "chest": MessageLookupByLibrary.simpleMessage("Грудь"), "chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"), + "choose_bank_for_payment": MessageLookupByLibrary.simpleMessage( + "Выберите банк", + ), "choose_diet_duration": MessageLookupByLibrary.simpleMessage( "Выберите срок диеты", ), @@ -243,6 +246,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "no": MessageLookupByLibrary.simpleMessage("Нет"), "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "no_installed_banks": MessageLookupByLibrary.simpleMessage( + "Нет установленных банков", + ), "notifications": MessageLookupByLibrary.simpleMessage("Уведомления"), "one_option": MessageLookupByLibrary.simpleMessage("1 опция"), "option": MessageLookupByLibrary.simpleMessage("Опция"), @@ -314,6 +320,9 @@ class MessageLookup extends MessageLookupByLibrary { "Снять с публикации", ), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), + "sbp_payment_error": MessageLookupByLibrary.simpleMessage( + "Не удалось начать оплату. Попробуйте позже", + ), "services": MessageLookupByLibrary.simpleMessage("Оказание услуг"), "settings": MessageLookupByLibrary.simpleMessage("Настройки"), "share": MessageLookupByLibrary.simpleMessage("Поделиться"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 3bca8ece..41b13d8d 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2649,6 +2649,36 @@ class S { return Intl.message('Оплатить', name: 'pay', desc: '', args: []); } + /// `Выберите банк` + String get choose_bank_for_payment { + return Intl.message( + 'Выберите банк', + name: 'choose_bank_for_payment', + desc: '', + args: [], + ); + } + + /// `Нет установленных банков` + String get no_installed_banks { + return Intl.message( + 'Нет установленных банков', + name: 'no_installed_banks', + desc: '', + args: [], + ); + } + + /// `Не удалось начать оплату. Попробуйте позже` + String get sbp_payment_error { + return Intl.message( + 'Не удалось начать оплату. Попробуйте позже', + name: 'sbp_payment_error', + desc: '', + args: [], + ); + } + /// `Подробнее о заказе` String get more_about_order { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 27c662dd..855b25e8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -326,6 +326,9 @@ "date_has_ben_set": "Дата старта начала консультации установлена", "tap_on_link_to_pay": "Перейдите по ссылке для оплаты консультации", "pay": "Оплатить", + "choose_bank_for_payment": "Выберите банк", + "no_installed_banks": "Нет установленных банков", + "sbp_payment_error": "Не удалось начать оплату. Попробуйте позже", "more_about_order": "Подробнее о заказе", "cancel_order": "Отменить заказ", "refuse_order":"Отказаться от заказа", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 687f142c..b160ec83 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -233,5 +233,8 @@ "video_call_permission_camera_title": "Доступ к камере", "video_call_permission_camera_description": "Чтобы вас видели во время видеозвонка, разрешите доступ к камере. При отказе можно включить доступ в настройках устройства.", "video_call_permission_microphone_title": "Доступ к микрофону", - "video_call_permission_microphone_description": "Чтобы вас слышали во время видеозвонка, разрешите доступ к микрофону. При отказе можно включить доступ в настройках устройства." + "video_call_permission_microphone_description": "Чтобы вас слышали во время видеозвонка, разрешите доступ к микрофону. При отказе можно включить доступ в настройках устройства.", + "choose_bank_for_payment": "Выберите банк", + "no_installed_banks": "Нет установленных банков", + "sbp_payment_error": "Не удалось начать оплату. Попробуйте позже" } diff --git a/test/features/payment/presentation/widgets/sbp_banks_bottom_sheet_test.dart b/test/features/payment/presentation/widgets/sbp_banks_bottom_sheet_test.dart new file mode 100644 index 00000000..773a1d74 --- /dev/null +++ b/test/features/payment/presentation/widgets/sbp_banks_bottom_sheet_test.dart @@ -0,0 +1,187 @@ +import 'dart:async'; + +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/chat/models/payment_model.dart'; +import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart'; +import 'package:eatvio_app/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart'; +import 'package:eatvio_app/features/payment/repositories/payment_repository.dart'; +import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sbp/models/c2bmembers_model.dart'; + +import '../../../../fixtures/fixture_reader.dart'; +import 'sbp_banks_bottom_sheet_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SbpBanksBottomSheet', () { + late MockPaymentRepository mockPaymentRepository; + late MockSbpRepository mockSbpRepository; + late ConsultationPaymentCubit cubit; + late PaymentModel alfaPayment; + + final installedBank = C2bmemberModel.fromJson(const { + 'bankName': 'Сбербанк', + 'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png', + 'schema': 'bank100000000111', + 'package_name': 'ru.sberbankmobile', + 'icon': '', + }); + + setUp(() { + mockPaymentRepository = MockPaymentRepository(); + mockSbpRepository = MockSbpRepository(); + cubit = ConsultationPaymentCubit( + paymentRepository: mockPaymentRepository, + sbpRepository: mockSbpRepository, + ); + alfaPayment = PaymentModel.fromJson( + FixtureReader.readAsMap('chat/payments/payment_response_alfa_bank.json'), + ); + }); + + tearDown(() { + cubit.close(); + }); + + Widget createTestWidget({required int planId}) { + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: Scaffold( + body: SbpBanksBottomSheet( + planId: planId, + cubit: cubit, + ), + ), + ); + } + + testWidgets('показывает Loader пока идёт startPayment', (tester) async { + const planId = 12; + final completer = Completer(); + + when( + mockPaymentRepository.startPaymentProcess(planId: planId), + ).thenAnswer((_) => completer.future); + + await tester.pumpWidget(createTestWidget(planId: planId)); + await tester.pump(); + + expect(find.byType(Loader), findsOneWidget); + expect(find.text('Сбербанк'), findsNothing); + + completer.complete(alfaPayment); + when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]); + await tester.pumpAndSettle(); + }); + + testWidgets('вызывает startPaymentProcess с переданным planId', (tester) async { + const planId = 12; + + when( + mockPaymentRepository.startPaymentProcess(planId: planId), + ).thenAnswer((_) async => alfaPayment); + when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]); + + await tester.pumpWidget(createTestWidget(planId: planId)); + await tester.pumpAndSettle(); + + verify(mockPaymentRepository.startPaymentProcess(planId: planId)).called(1); + }); + + testWidgets('после успеха показывает список банков и скрывает Loader', (tester) async { + const planId = 12; + + when( + mockPaymentRepository.startPaymentProcess(planId: planId), + ).thenAnswer((_) async => alfaPayment); + when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]); + + await tester.pumpWidget(createTestWidget(planId: planId)); + await tester.pumpAndSettle(); + + expect(find.byType(Loader), findsNothing); + expect(find.text('Выберите банк'), findsOneWidget); + expect(find.text('Сбербанк'), findsOneWidget); + }); + + testWidgets('tap по банку вызывает openBank с checkout_url', (tester) async { + const planId = 12; + + when( + mockPaymentRepository.startPaymentProcess(planId: planId), + ).thenAnswer((_) async => alfaPayment); + when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]); + when( + mockSbpRepository.openBank( + url: alfaPayment.paymentUrl, + bank: installedBank, + ), + ).thenAnswer((_) async => true); + + await tester.pumpWidget(createTestWidget(planId: planId)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Сбербанк')); + await tester.pumpAndSettle(); + + verify( + mockSbpRepository.openBank( + url: alfaPayment.paymentUrl, + bank: installedBank, + ), + ).called(1); + }); + + testWidgets('при ошибке API показывает сообщение об ошибке', (tester) async { + const planId = 12; + + when( + mockPaymentRepository.startPaymentProcess(planId: planId), + ).thenThrow(Exception('Payment start failed')); + + await tester.pumpWidget(createTestWidget(planId: planId)); + await tester.pumpAndSettle(); + + expect(find.byType(Loader), findsNothing); + expect(find.text('Сбербанк'), findsNothing); + expect( + find.text('Не удалось начать оплату. Попробуйте позже'), + findsOneWidget, + ); + }); + + testWidgets('при пустом списке банков показывает empty-сообщение', (tester) async { + const planId = 12; + + when( + mockPaymentRepository.startPaymentProcess(planId: planId), + ).thenAnswer((_) async => alfaPayment); + when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => []); + + await tester.pumpWidget(createTestWidget(planId: planId)); + await tester.pumpAndSettle(); + + expect(find.byType(Loader), findsNothing); + expect(find.text('Нет установленных банков'), findsOneWidget); + }); + }); +}