forked from eatvio/eatvio_app
feat(sbp_payments): QR-код оплаты СБП с другого устройства
- Новый SbpQrBottomSheet: рендер checkout_url (qr.nspk.ru/{qrcId}) через qr_flutter,
сумма, инструкция, автоопределение оплаты polling-ом статуса chosen_plan (3 сек)
и автозакрытие с успехом
- Кнопка «Оплатить по QR-коду» в шите выбора банков — первичная, под заголовком,
доступна сразу после получения checkout_url, не дожидаясь списка банков
(важно для устройств без банковских приложений, iOS в РФ)
- Блок «Симуляция оплаты» (testpay.alfabank.ru/sbp-page?qrcId=...) показывается
только когда base URL API не продуктовый: AppConfig.isProdApi/prodApiHosts
(хост целиком — стенды-поддомены eatvio.ru продом не считаются)
- showBottomSheetWithBlur стал генериком — возвращает значение pop(result)
- showConsultationPaymentFlow: опциональная прокидка суммы в QR-шит
- Локализация ru/en, тесты (16 новых, всего 34 платёжных зелёные)
This commit is contained in:
@@ -33,6 +33,20 @@ class AppConfig {
|
||||
late String _baseUrl;
|
||||
String get baseUrl => _baseUrl;
|
||||
|
||||
/// Продуктовые хосты API. Сравнение по хосту целиком: поддомены
|
||||
/// (например `*-hasan.eatvio.ru`) — это стенды разработки, не прод.
|
||||
static const List<String> prodApiHosts = ['eatvio.ru', 'eatvio.com'];
|
||||
|
||||
/// Текущий base URL указывает на продуктовый API?
|
||||
bool get isProdApi {
|
||||
final host = Uri.tryParse(baseUrl)?.host.toLowerCase() ?? '';
|
||||
if (host.isEmpty) return false;
|
||||
return prodApiHosts.any((prodHost) => host == prodHost || host == 'www.$prodHost');
|
||||
}
|
||||
|
||||
/// Работаем на стенде разработки/stage (не на продуктовом API)?
|
||||
bool get isDevOrStageStand => !isProdApi;
|
||||
|
||||
void setBaseUrl(String baseUrls) {
|
||||
_baseUrl = baseUrls;
|
||||
_persistBaseUrl(baseUrls);
|
||||
|
||||
@@ -3,7 +3,11 @@ import 'dart:ui';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> showBottomSheetWithBlur({
|
||||
/// Показывает modal bottom sheet с blur-фоном.
|
||||
///
|
||||
/// Дженерик [T] пробрасывает значение, переданное в `Navigator.pop(T)`
|
||||
/// внутри шита (по умолчанию `null`).
|
||||
Future<T?> showBottomSheetWithBlur<T>({
|
||||
required BuildContext context,
|
||||
required Widget child,
|
||||
double? height = 200,
|
||||
@@ -11,7 +15,7 @@ Future<void> showBottomSheetWithBlur({
|
||||
bool enableDrag = true,
|
||||
bool userRootNavigator = false,
|
||||
}) {
|
||||
return showModalBottomSheet(
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
barrierColor: appTheme.transparentBlack,
|
||||
|
||||
@@ -106,7 +106,11 @@ class _NotificationBannerState extends State<NotificationBanner> {
|
||||
onTap: () {
|
||||
final planId = (context.read<ChatRoomCubit>().state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
|
||||
if (planId != null) {
|
||||
showConsultationPaymentFlow(context, planId: planId);
|
||||
showConsultationPaymentFlow(
|
||||
context,
|
||||
planId: planId,
|
||||
amount: widget.planPrice,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(s.pay, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
|
||||
Future<void> showConsultationPaymentFlow(
|
||||
BuildContext context, {
|
||||
required int planId,
|
||||
double? amount,
|
||||
Future<bool?> Function(BuildContext)? showGate,
|
||||
Future<PromoCodeResult?> Function(
|
||||
BuildContext, {
|
||||
@@ -21,7 +22,7 @@ Future<void> showConsultationPaymentFlow(
|
||||
Future<bool> Function()? onRemove,
|
||||
PromoCodeModel? initialPromo,
|
||||
})? showForm,
|
||||
Future<void> Function(BuildContext, {required int planId})? showSbp,
|
||||
Future<void> Function(BuildContext, {required int planId, double? amount})? showSbp,
|
||||
Future<PromoValidateResponse> Function({
|
||||
required int planId,
|
||||
required String promoCode,
|
||||
@@ -65,7 +66,7 @@ Future<void> showConsultationPaymentFlow(
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
await sbp(context, planId: planId);
|
||||
await sbp(context, planId: planId, amount: amount);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ Future<void> showConsultationPaymentFlow(
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
await sbp(context, planId: planId);
|
||||
await sbp(context, planId: planId, amount: amount);
|
||||
}
|
||||
|
||||
Future<bool?> _showGate(BuildContext context) {
|
||||
@@ -113,8 +114,8 @@ Future<PromoCodeResult?> _showForm(
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showSbp(BuildContext context, {required int planId}) {
|
||||
return showSbpBanksBottomSheet(context, planId: planId);
|
||||
Future<void> _showSbp(BuildContext context, {required int planId, double? amount}) {
|
||||
return showSbpBanksBottomSheet(context, planId: planId, amount: amount);
|
||||
}
|
||||
|
||||
Future<PromoValidateResponse> _defaultValidatePromo({
|
||||
|
||||
@@ -8,18 +8,23 @@ 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/features/payment/presentation/widgets/sbp_qr_bottom_sheet.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<void> showSbpBanksBottomSheet(BuildContext context, {required int planId}) {
|
||||
Future<void> showSbpBanksBottomSheet(
|
||||
BuildContext context, {
|
||||
required int planId,
|
||||
double? amount,
|
||||
}) {
|
||||
final height = MediaQuery.sizeOf(context).height * 0.55;
|
||||
return showBottomSheetWithBlur(
|
||||
context: context,
|
||||
height: height,
|
||||
child: SbpBanksBottomSheet(planId: planId),
|
||||
child: SbpBanksBottomSheet(planId: planId, amount: amount),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,9 +33,13 @@ Future<void> showSbpBanksBottomSheet(BuildContext context, {required int planId}
|
||||
/// При открытии вызывает [ConsultationPaymentCubit.startPayment] с [planId]
|
||||
/// и показывает [Loader], пока загружаются checkout_url и список банков.
|
||||
class SbpBanksBottomSheet extends StatefulWidget {
|
||||
const SbpBanksBottomSheet({super.key, required this.planId, this.cubit});
|
||||
const SbpBanksBottomSheet({super.key, required this.planId, this.amount, this.cubit});
|
||||
|
||||
final int planId;
|
||||
|
||||
/// Сумма к оплате в рублях — передаётся в QR-шит для отображения.
|
||||
final double? amount;
|
||||
|
||||
final ConsultationPaymentCubit? cubit;
|
||||
|
||||
@override
|
||||
@@ -77,6 +86,32 @@ class _SbpBanksBottomSheetState extends State<SbpBanksBottomSheet> {
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// QR-кнопка доступна сразу после получения checkout_url,
|
||||
// не дожидаясь списка банков: на устройствах без банковских
|
||||
// приложений (iOS в РФ) это единственный путь оплаты.
|
||||
BlocBuilder<ConsultationPaymentCubit, ConsultationPaymentState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.checkoutUrl != current.checkoutUrl,
|
||||
builder: (context, state) {
|
||||
final checkoutUrl = state.checkoutUrl;
|
||||
if (checkoutUrl == null || checkoutUrl.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.pay_by_qr_code,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: _onShowQrTap,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: BlocBuilder<ConsultationPaymentCubit, ConsultationPaymentState>(
|
||||
@@ -122,6 +157,23 @@ class _SbpBanksBottomSheetState extends State<SbpBanksBottomSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Открывает QR-шит поверх шита банков; при подтверждённой оплате
|
||||
/// закрывает и родительский шит.
|
||||
Future<void> _onShowQrTap() async {
|
||||
final paid = await showSbpQrBottomSheet(
|
||||
context,
|
||||
planId: widget.planId,
|
||||
cubit: _cubit,
|
||||
amount: widget.amount,
|
||||
);
|
||||
if (paid == true && mounted) {
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onBankTap(C2bmemberModel bank) async {
|
||||
final opened = await _cubit.openBank(bank: bank);
|
||||
if (opened && mounted) {
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/app_config.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/features/chat/repositories/chat_repository.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:qr_flutter/qr_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// Тестовый симулятор оплаты СБП (только для стендов разработки/stage).
|
||||
const String _testPayUrlBase = 'https://testpay.alfabank.ru/sbp-page?qrcId=';
|
||||
const String _nspkUrlPrefix = 'https://qr.nspk.ru/';
|
||||
const Duration _pollInterval = Duration(seconds: 3);
|
||||
const Duration _autoCloseDelay = Duration(seconds: 2);
|
||||
|
||||
/// Показывает bottom sheet с QR-кодом СБП для оплаты консультации
|
||||
/// с другого устройства.
|
||||
///
|
||||
/// Возвращает `true`, если оплата подтверждена polling-ом статуса плана.
|
||||
Future<bool?> showSbpQrBottomSheet(
|
||||
BuildContext context, {
|
||||
required int planId,
|
||||
required ConsultationPaymentCubit cubit,
|
||||
double? amount,
|
||||
}) {
|
||||
return showBottomSheetWithBlur<bool>(
|
||||
context: context,
|
||||
height: null,
|
||||
child: SbpQrBottomSheet(
|
||||
planId: planId,
|
||||
cubit: cubit,
|
||||
amount: amount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// QR-код оплаты СБП: рендерит [ConsultationPaymentState.checkoutUrl]
|
||||
/// (payload вида `https://qr.nspk.ru/{qrcId}`), ожидает оплату через
|
||||
/// polling статуса chosen_plan и показывает успех.
|
||||
class SbpQrBottomSheet extends StatefulWidget {
|
||||
const SbpQrBottomSheet({
|
||||
super.key,
|
||||
required this.planId,
|
||||
required this.cubit,
|
||||
this.amount,
|
||||
this.chatRepository,
|
||||
this.pollInterval = _pollInterval,
|
||||
});
|
||||
|
||||
final int planId;
|
||||
final ConsultationPaymentCubit cubit;
|
||||
|
||||
/// Сумма к оплате в рублях (показывается над QR, если передана).
|
||||
final double? amount;
|
||||
|
||||
/// Переопределяется в тестах; по умолчанию берётся из DI.
|
||||
final ChatRepository? chatRepository;
|
||||
|
||||
final Duration pollInterval;
|
||||
|
||||
@override
|
||||
State<SbpQrBottomSheet> createState() => _SbpQrBottomSheetState();
|
||||
}
|
||||
|
||||
class _SbpQrBottomSheetState extends State<SbpQrBottomSheet> {
|
||||
late final ChatRepository _chatRepository;
|
||||
late final Timer _pollTimer;
|
||||
bool _paid = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_chatRepository = widget.chatRepository ?? sl<ChatRepository>();
|
||||
_pollTimer = Timer.periodic(widget.pollInterval, (_) => _pollStatus());
|
||||
_pollStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pollStatus() async {
|
||||
if (_paid || !mounted) return;
|
||||
try {
|
||||
final plan = await _chatRepository.getChosenPlan(id: widget.planId);
|
||||
if (plan.chosenPlanState.isStateAfterPaidState()) {
|
||||
_onPaid();
|
||||
}
|
||||
} catch (_) {
|
||||
// Сетевые сбои polling-а игнорируем — следующий тик повторит проверку.
|
||||
}
|
||||
}
|
||||
|
||||
void _onPaid() {
|
||||
if (_paid || !mounted) return;
|
||||
setState(() => _paid = true);
|
||||
Future.delayed(_autoCloseDelay, () {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocProvider<ConsultationPaymentCubit>.value(
|
||||
value: widget.cubit,
|
||||
child: BlocBuilder<ConsultationPaymentCubit, ConsultationPaymentState>(
|
||||
builder: (context, state) {
|
||||
final checkoutUrl = state.checkoutUrl;
|
||||
if (checkoutUrl == null || checkoutUrl.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
_paid ? s.sbp_qr_paid_title : s.sbp_qr_title,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_paid)
|
||||
_PaidContent(description: s.sbp_qr_paid_description)
|
||||
else ...[
|
||||
if (widget.amount != null) ...[
|
||||
Text(
|
||||
s.sbp_qr_amount_label,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.gray600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${widget.amount!.toStringAsFixed(0)} ₽',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_QrCodeCard(checkoutUrl: checkoutUrl),
|
||||
const SizedBox(height: 16),
|
||||
_HowToPay(
|
||||
step1: s.sbp_qr_step1,
|
||||
step2: s.sbp_qr_step2,
|
||||
step3: s.sbp_qr_step3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_TestPayButton(checkoutUrl: checkoutUrl),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
s.sbp_qr_waiting_payment,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.gray600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QrCodeCard extends StatelessWidget {
|
||||
const _QrCodeCard({required this.checkoutUrl});
|
||||
|
||||
final String checkoutUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: appTheme.blueGray300),
|
||||
),
|
||||
child: QrImageView(
|
||||
key: const ValueKey('sbp_qr_code'),
|
||||
data: checkoutUrl,
|
||||
size: 200,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HowToPay extends StatelessWidget {
|
||||
const _HowToPay({required this.step1, required this.step2, required this.step3});
|
||||
|
||||
final String step1;
|
||||
final String step2;
|
||||
final String step3;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(step1, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 4),
|
||||
Text(step2, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 4),
|
||||
Text(step3, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Кнопка открытия тестового симулятора оплаты Альфа-Банка.
|
||||
///
|
||||
/// Видна только когда QR выдан непродовым бэкендом: тестовый qrcId от
|
||||
/// продового в приложении не отличить (формат одинаковый), различает их
|
||||
/// именно то, какой бэкенд выдал checkout_url — на стендах разработки/stage
|
||||
/// Альфа подключена в sandbox и выдаёт QR тестового терминала. Поэтому
|
||||
/// критерий — текущий base URL API не продуктовый (см. [AppConfig.isProdApi]).
|
||||
class _TestPayButton extends StatelessWidget {
|
||||
const _TestPayButton({required this.checkoutUrl});
|
||||
|
||||
final String checkoutUrl;
|
||||
|
||||
bool get _visible {
|
||||
if (sl<AppConfig>().isProdApi) return false;
|
||||
return checkoutUrl.startsWith(_nspkUrlPrefix);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_visible) return const SizedBox.shrink();
|
||||
final s = S.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3E0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
s.sbp_qr_test_hint,
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => _openTestPayment(),
|
||||
child: Text(s.sbp_qr_open_test_payment),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openTestPayment() async {
|
||||
final qrcId = checkoutUrl.replaceFirst(_nspkUrlPrefix, '');
|
||||
final uri = Uri.parse('$_testPayUrlBase$qrcId');
|
||||
try {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {
|
||||
// Не смогли открыть браузер — пользователь оплатит по QR.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PaidContent extends StatelessWidget {
|
||||
const _PaidContent({required this.description});
|
||||
|
||||
final String description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(color: Color(0xFFE8F5E9), shape: BoxShape.circle),
|
||||
child: const Icon(Icons.check_rounded, color: Color(0xFF4CAF50), size: 40),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.gray600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -658,6 +658,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"В пароле должно быть минимум 6 символов",
|
||||
),
|
||||
"pay": MessageLookupByLibrary.simpleMessage("Оплатить"),
|
||||
"pay_by_qr_code": MessageLookupByLibrary.simpleMessage(
|
||||
"Оплатить по QR-коду",
|
||||
),
|
||||
"payment_failed": MessageLookupByLibrary.simpleMessage("Оплата не прошла"),
|
||||
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
|
||||
"pending_payment": MessageLookupByLibrary.simpleMessage("Ожидается оплата"),
|
||||
@@ -777,6 +780,34 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"sbp_payment_error": MessageLookupByLibrary.simpleMessage(
|
||||
"Не удалось начать оплату. Попробуйте позже",
|
||||
),
|
||||
"sbp_qr_amount_label": MessageLookupByLibrary.simpleMessage(
|
||||
"Сумма к оплате",
|
||||
),
|
||||
"sbp_qr_open_test_payment": MessageLookupByLibrary.simpleMessage(
|
||||
"Открыть тестовую оплату",
|
||||
),
|
||||
"sbp_qr_paid_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Консультация оплачена. Можно вернуться в чат.",
|
||||
),
|
||||
"sbp_qr_paid_title": MessageLookupByLibrary.simpleMessage(
|
||||
"Оплата прошла успешно!",
|
||||
),
|
||||
"sbp_qr_step1": MessageLookupByLibrary.simpleMessage(
|
||||
"Откройте приложение банка на телефоне",
|
||||
),
|
||||
"sbp_qr_step2": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите «Оплата по QR-коду»",
|
||||
),
|
||||
"sbp_qr_step3": MessageLookupByLibrary.simpleMessage(
|
||||
"Наведите камеру на код",
|
||||
),
|
||||
"sbp_qr_test_hint": MessageLookupByLibrary.simpleMessage(
|
||||
"Симуляция оплаты на тестовом стенде Альфа-Банка",
|
||||
),
|
||||
"sbp_qr_title": MessageLookupByLibrary.simpleMessage("Оплата по QR-коду"),
|
||||
"sbp_qr_waiting_payment": MessageLookupByLibrary.simpleMessage(
|
||||
"Ожидание оплаты…",
|
||||
),
|
||||
"send": MessageLookupByLibrary.simpleMessage("Отправить"),
|
||||
"send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"),
|
||||
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
||||
|
||||
@@ -297,6 +297,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"password_short": MessageLookupByLibrary.simpleMessage(
|
||||
"В пароле должно быть минимум 6 символов",
|
||||
),
|
||||
"pay_by_qr_code": MessageLookupByLibrary.simpleMessage(
|
||||
"Оплатить по QR-коду",
|
||||
),
|
||||
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
|
||||
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
|
||||
"personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"),
|
||||
@@ -354,6 +357,34 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"sbp_payment_error": MessageLookupByLibrary.simpleMessage(
|
||||
"Не удалось начать оплату. Попробуйте позже",
|
||||
),
|
||||
"sbp_qr_amount_label": MessageLookupByLibrary.simpleMessage(
|
||||
"Сумма к оплате",
|
||||
),
|
||||
"sbp_qr_open_test_payment": MessageLookupByLibrary.simpleMessage(
|
||||
"Открыть тестовую оплату",
|
||||
),
|
||||
"sbp_qr_paid_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Консультация оплачена. Можно вернуться в чат.",
|
||||
),
|
||||
"sbp_qr_paid_title": MessageLookupByLibrary.simpleMessage(
|
||||
"Оплата прошла успешно!",
|
||||
),
|
||||
"sbp_qr_step1": MessageLookupByLibrary.simpleMessage(
|
||||
"Откройте приложение банка на телефоне",
|
||||
),
|
||||
"sbp_qr_step2": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите «Оплата по QR-коду»",
|
||||
),
|
||||
"sbp_qr_step3": MessageLookupByLibrary.simpleMessage(
|
||||
"Наведите камеру на код",
|
||||
),
|
||||
"sbp_qr_test_hint": MessageLookupByLibrary.simpleMessage(
|
||||
"Симуляция оплаты на тестовом стенде Альфа-Банка",
|
||||
),
|
||||
"sbp_qr_title": MessageLookupByLibrary.simpleMessage("Оплата по QR-коду"),
|
||||
"sbp_qr_waiting_payment": MessageLookupByLibrary.simpleMessage(
|
||||
"Ожидание оплаты…",
|
||||
),
|
||||
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
||||
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
|
||||
"share": MessageLookupByLibrary.simpleMessage("Поделиться"),
|
||||
|
||||
@@ -2689,6 +2689,116 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Оплатить по QR-коду`
|
||||
String get pay_by_qr_code {
|
||||
return Intl.message(
|
||||
'Оплатить по QR-коду',
|
||||
name: 'pay_by_qr_code',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Оплата по QR-коду`
|
||||
String get sbp_qr_title {
|
||||
return Intl.message(
|
||||
'Оплата по QR-коду',
|
||||
name: 'sbp_qr_title',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Сумма к оплате`
|
||||
String get sbp_qr_amount_label {
|
||||
return Intl.message(
|
||||
'Сумма к оплате',
|
||||
name: 'sbp_qr_amount_label',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Откройте приложение банка на телефоне`
|
||||
String get sbp_qr_step1 {
|
||||
return Intl.message(
|
||||
'Откройте приложение банка на телефоне',
|
||||
name: 'sbp_qr_step1',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Выберите «Оплата по QR-коду»`
|
||||
String get sbp_qr_step2 {
|
||||
return Intl.message(
|
||||
'Выберите «Оплата по QR-коду»',
|
||||
name: 'sbp_qr_step2',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Наведите камеру на код`
|
||||
String get sbp_qr_step3 {
|
||||
return Intl.message(
|
||||
'Наведите камеру на код',
|
||||
name: 'sbp_qr_step3',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Ожидание оплаты…`
|
||||
String get sbp_qr_waiting_payment {
|
||||
return Intl.message(
|
||||
'Ожидание оплаты…',
|
||||
name: 'sbp_qr_waiting_payment',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Оплата прошла успешно!`
|
||||
String get sbp_qr_paid_title {
|
||||
return Intl.message(
|
||||
'Оплата прошла успешно!',
|
||||
name: 'sbp_qr_paid_title',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Консультация оплачена. Можно вернуться в чат.`
|
||||
String get sbp_qr_paid_description {
|
||||
return Intl.message(
|
||||
'Консультация оплачена. Можно вернуться в чат.',
|
||||
name: 'sbp_qr_paid_description',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Открыть тестовую оплату`
|
||||
String get sbp_qr_open_test_payment {
|
||||
return Intl.message(
|
||||
'Открыть тестовую оплату',
|
||||
name: 'sbp_qr_open_test_payment',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Симуляция оплаты на тестовом стенде Альфа-Банка`
|
||||
String get sbp_qr_test_hint {
|
||||
return Intl.message(
|
||||
'Симуляция оплаты на тестовом стенде Альфа-Банка',
|
||||
name: 'sbp_qr_test_hint',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Подробнее о заказе`
|
||||
String get more_about_order {
|
||||
return Intl.message(
|
||||
|
||||
@@ -330,6 +330,17 @@
|
||||
"choose_bank_for_payment": "Выберите банк",
|
||||
"no_installed_banks": "Нет установленных банков",
|
||||
"sbp_payment_error": "Не удалось начать оплату. Попробуйте позже",
|
||||
"pay_by_qr_code": "Оплатить по QR-коду",
|
||||
"sbp_qr_title": "Оплата по QR-коду",
|
||||
"sbp_qr_amount_label": "Сумма к оплате",
|
||||
"sbp_qr_step1": "Откройте приложение банка на телефоне",
|
||||
"sbp_qr_step2": "Выберите «Оплата по QR-коду»",
|
||||
"sbp_qr_step3": "Наведите камеру на код",
|
||||
"sbp_qr_waiting_payment": "Ожидание оплаты…",
|
||||
"sbp_qr_paid_title": "Оплата прошла успешно!",
|
||||
"sbp_qr_paid_description": "Консультация оплачена. Можно вернуться в чат.",
|
||||
"sbp_qr_open_test_payment": "Открыть тестовую оплату",
|
||||
"sbp_qr_test_hint": "Симуляция оплаты на тестовом стенде Альфа-Банка",
|
||||
"more_about_order": "Подробнее о заказе",
|
||||
"cancel_order": "Отменить заказ",
|
||||
"refuse_order":"Отказаться от заказа",
|
||||
|
||||
@@ -239,6 +239,17 @@
|
||||
"choose_bank_for_payment": "Выберите банк",
|
||||
"no_installed_banks": "Нет установленных банков",
|
||||
"sbp_payment_error": "Не удалось начать оплату. Попробуйте позже",
|
||||
"pay_by_qr_code": "Оплатить по QR-коду",
|
||||
"sbp_qr_title": "Оплата по QR-коду",
|
||||
"sbp_qr_amount_label": "Сумма к оплате",
|
||||
"sbp_qr_step1": "Откройте приложение банка на телефоне",
|
||||
"sbp_qr_step2": "Выберите «Оплата по QR-коду»",
|
||||
"sbp_qr_step3": "Наведите камеру на код",
|
||||
"sbp_qr_waiting_payment": "Ожидание оплаты…",
|
||||
"sbp_qr_paid_title": "Оплата прошла успешно!",
|
||||
"sbp_qr_paid_description": "Консультация оплачена. Можно вернуться в чат.",
|
||||
"sbp_qr_open_test_payment": "Открыть тестовую оплату",
|
||||
"sbp_qr_test_hint": "Симуляция оплаты на тестовом стенде Альфа-Банка",
|
||||
"yes": "Да",
|
||||
"apply": "Применить",
|
||||
"have_promo_code": "У вас есть промокод?",
|
||||
|
||||
@@ -92,6 +92,7 @@ dependencies:
|
||||
floating: ^6.0.0
|
||||
wakelock_plus: ^1.5.2
|
||||
sbp: 0.1.0
|
||||
qr_flutter: ^4.1.0
|
||||
|
||||
# html 0.15.7 removed top-level query_selector.matches used by flutter_html 3.0.0.
|
||||
dependency_overrides:
|
||||
|
||||
@@ -73,7 +73,7 @@ void main() {
|
||||
return formResult;
|
||||
}
|
||||
|
||||
Future<void> mockSbp(BuildContext context, {required int planId}) async {
|
||||
Future<void> mockSbp(BuildContext context, {required int planId, double? amount}) async {
|
||||
sbpCalled = true;
|
||||
sbpPlanId = planId;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
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/chat/repositories/chat_repository.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/presentation/widgets/sbp_qr_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:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:sbp/models/c2bmembers_model.dart';
|
||||
|
||||
import '../../../../fixtures/fixture_reader.dart';
|
||||
@@ -21,9 +29,21 @@ import 'sbp_banks_bottom_sheet_test.mocks.dart';
|
||||
@GenerateNiceMocks([
|
||||
MockSpec<PaymentRepository>(),
|
||||
MockSpec<SbpRepository>(),
|
||||
MockSpec<ChatRepository>(),
|
||||
])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
// AppConfig в конструкторе читает EnvConfig (dotenv) — инициализируем.
|
||||
setUpAll(() async {
|
||||
await dotenv.load(
|
||||
fileName: 'test.env',
|
||||
isOptional: true,
|
||||
mergeWith: const {
|
||||
'BASE_URL': 'https://eatvio.ru',
|
||||
'DEV_BASE_URL': 'https://dev2.eatvio.com',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('SbpBanksBottomSheet', () {
|
||||
late MockPaymentRepository mockPaymentRepository;
|
||||
@@ -183,5 +203,66 @@ void main() {
|
||||
expect(find.byType(Loader), findsNothing);
|
||||
expect(find.text('Нет установленных банков'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('QR-кнопка доступна до загрузки списка банков', (tester) async {
|
||||
const planId = 12;
|
||||
final banksCompleter = Completer<List<C2bmemberModel>>();
|
||||
|
||||
when(
|
||||
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||
).thenAnswer((_) async => alfaPayment);
|
||||
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) => banksCompleter.future);
|
||||
sl.registerLazySingleton<AppConfig>(
|
||||
() => AppConfig(buildType: AppBuildType.tests),
|
||||
);
|
||||
sl.registerLazySingleton<ChatRepository>(() => MockChatRepository());
|
||||
|
||||
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
// Банки ещё грузятся (Loader), а QR-кнопка уже доступна.
|
||||
expect(find.byType(Loader), findsOneWidget);
|
||||
expect(find.text('Оплатить по QR-коду'), findsOneWidget);
|
||||
|
||||
banksCompleter.complete([installedBank]);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(Loader), findsNothing);
|
||||
expect(find.text('Сбербанк'), findsOneWidget);
|
||||
expect(find.text('Оплатить по QR-коду'), findsOneWidget);
|
||||
|
||||
await GetIt.I.reset();
|
||||
});
|
||||
|
||||
testWidgets('после старта оплаты показывает кнопку QR и открывает QR-шит', (tester) async {
|
||||
const planId = 12;
|
||||
|
||||
when(
|
||||
mockPaymentRepository.startPaymentProcess(planId: planId),
|
||||
).thenAnswer((_) async => alfaPayment);
|
||||
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
|
||||
sl.registerLazySingleton<AppConfig>(
|
||||
() => AppConfig(buildType: AppBuildType.tests),
|
||||
);
|
||||
sl.registerLazySingleton<ChatRepository>(() => MockChatRepository());
|
||||
|
||||
await tester.pumpWidget(createTestWidget(planId: planId));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Оплатить по QR-коду'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Оплатить по QR-коду'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.byType(SbpQrBottomSheet), findsOneWidget);
|
||||
expect(find.byType(QrImageView), findsOneWidget);
|
||||
|
||||
// Закрываем QR-шит (останавливает polling-таймер).
|
||||
tester.state<NavigatorState>(find.byType(Navigator).first).pop();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
await GetIt.I.reset();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.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/features/payment/presentation/widgets/sbp_qr_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/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.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:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import 'sbp_qr_bottom_sheet_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([
|
||||
MockSpec<PaymentRepository>(),
|
||||
MockSpec<SbpRepository>(),
|
||||
MockSpec<ChatRepository>(),
|
||||
MockSpec<ChosenPlanModel>(),
|
||||
])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
const planId = 12;
|
||||
const checkoutUrl = 'https://qr.nspk.ru/AD10106TB0BVFTBG9DQ96NKJEIVVTKIO';
|
||||
|
||||
group('SbpQrBottomSheet', () {
|
||||
late MockPaymentRepository mockPaymentRepository;
|
||||
late MockSbpRepository mockSbpRepository;
|
||||
late MockChatRepository mockChatRepository;
|
||||
late MockChosenPlanModel mockPlan;
|
||||
late ConsultationPaymentCubit cubit;
|
||||
late Future<bool?>? sheetResult;
|
||||
|
||||
setUp(() async {
|
||||
await GetIt.I.reset();
|
||||
// AppConfig в конструкторе читает EnvConfig (dotenv) — инициализируем.
|
||||
await dotenv.load(
|
||||
fileName: 'test.env',
|
||||
isOptional: true,
|
||||
mergeWith: const {
|
||||
'BASE_URL': 'https://eatvio.ru',
|
||||
'DEV_BASE_URL': 'https://dev2.eatvio.com',
|
||||
},
|
||||
);
|
||||
sl.registerLazySingleton<AppConfig>(
|
||||
() => AppConfig(buildType: AppBuildType.tests),
|
||||
);
|
||||
sl.registerLazySingleton<ChatRepository>(() => mockChatRepository);
|
||||
|
||||
mockPaymentRepository = MockPaymentRepository();
|
||||
mockSbpRepository = MockSbpRepository();
|
||||
mockChatRepository = MockChatRepository();
|
||||
mockPlan = MockChosenPlanModel();
|
||||
cubit = ConsultationPaymentCubit(
|
||||
paymentRepository: mockPaymentRepository,
|
||||
sbpRepository: mockSbpRepository,
|
||||
);
|
||||
cubit.emit(
|
||||
ConsultationPaymentState(
|
||||
isLoading: false,
|
||||
checkoutUrl: checkoutUrl,
|
||||
),
|
||||
);
|
||||
sheetResult = null;
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await GetIt.I.reset();
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
Future<void> pumpApp(WidgetTester tester, {double? amount}) {
|
||||
return tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeHelper().themeData(),
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('ru')],
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => sheetResult = showSbpQrBottomSheet(
|
||||
context,
|
||||
planId: planId,
|
||||
cubit: cubit,
|
||||
amount: amount,
|
||||
),
|
||||
child: const Text('open'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> openSheet(WidgetTester tester, {double? amount}) async {
|
||||
await pumpApp(tester, amount: amount);
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
/// Закрывает шит, чтобы остановить polling-таймер в конце теста.
|
||||
Future<void> closeSheet(WidgetTester tester) async {
|
||||
final navigator = tester.state<NavigatorState>(
|
||||
find.byType(Navigator).first,
|
||||
);
|
||||
navigator.pop();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
void stubPlanState(ChosenPlanStateEnum state) {
|
||||
when(mockChatRepository.getChosenPlan(id: planId))
|
||||
.thenAnswer((_) async => mockPlan);
|
||||
when(mockPlan.chosenPlanState).thenReturn(state);
|
||||
}
|
||||
|
||||
/// Перерегистрирует DI с AppConfig, смотрящим на конкретный base URL.
|
||||
Future<void> useApiBaseUrl(String baseUrl) async {
|
||||
await GetIt.I.reset();
|
||||
sl.registerLazySingleton<AppConfig>(
|
||||
() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl(baseUrl),
|
||||
);
|
||||
sl.registerLazySingleton<ChatRepository>(() => mockChatRepository);
|
||||
}
|
||||
|
||||
testWidgets('рендерит QR-код из checkout_url и инструкцию', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.byKey(const ValueKey('sbp_qr_code')), findsOneWidget);
|
||||
expect(find.byType(QrImageView), findsOneWidget);
|
||||
expect(find.text('Оплата по QR-коду'), findsOneWidget);
|
||||
expect(find.text('Ожидание оплаты…'), findsOneWidget);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
|
||||
testWidgets('показывает сумму, если передана', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
|
||||
await openSheet(tester, amount: 1200);
|
||||
|
||||
expect(find.text('Сумма к оплате'), findsOneWidget);
|
||||
expect(find.text('1200 ₽'), findsOneWidget);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
|
||||
testWidgets('не показывает сумму, если не передана', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('Сумма к оплате'), findsNothing);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
|
||||
testWidgets('показывает тестовую кнопку на stage-стенде (dev2)', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
await useApiBaseUrl('https://dev2.eatvio.com');
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('Открыть тестовую оплату'), findsOneWidget);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
|
||||
testWidgets('показывает тестовую кнопку на стенде-поддомене прод-хоста', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
// Поддомен eatvio.ru — стенд (например, hasan), а не прод.
|
||||
await useApiBaseUrl('https://ubooquiequ7ohz9ias3aik4a-hasan.eatvio.ru');
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('Открыть тестовую оплату'), findsOneWidget);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
|
||||
testWidgets('скрывает тестовую кнопку на продуктовом API', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
await useApiBaseUrl('https://eatvio.ru');
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('Открыть тестовую оплату'), findsNothing);
|
||||
// Сам QR при этом работает.
|
||||
expect(find.byType(QrImageView), findsOneWidget);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
|
||||
testWidgets('скрывает тестовую кнопку на продуктовом API (eatvio.com)', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
await useApiBaseUrl('https://eatvio.com');
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('Открыть тестовую оплату'), findsNothing);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
|
||||
testWidgets('при оплате (statePaid) показывает успех и закрывает шит', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePaid);
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
// Первый poll выполняется сразу в initState.
|
||||
await tester.pump();
|
||||
expect(find.text('Оплата прошла успешно!'), findsOneWidget);
|
||||
|
||||
// Автозакрытие через 2 секунды.
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await tester.pump();
|
||||
expect(find.byType(SbpQrBottomSheet), findsNothing);
|
||||
expect(await sheetResult, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('до оплаты продолжает ждать', (tester) async {
|
||||
stubPlanState(ChosenPlanStateEnum.statePendingPayment);
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Оплата прошла успешно!'), findsNothing);
|
||||
expect(find.text('Ожидание оплаты…'), findsOneWidget);
|
||||
|
||||
await closeSheet(tester);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user