forked from eatvio/eatvio_app
feat(sbp_payments): added new layoute for sbp banks and rederict to chosen bank, and added tests for layoute
This commit is contained in:
@@ -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<void> 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<SbpBanksBottomSheet> createState() => _SbpBanksBottomSheetState();
|
||||
}
|
||||
|
||||
class _SbpBanksBottomSheetState extends State<SbpBanksBottomSheet> {
|
||||
late final ConsultationPaymentCubit _cubit;
|
||||
late final bool _ownsCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ownsCubit = widget.cubit == null;
|
||||
_cubit = widget.cubit ?? sl<ConsultationPaymentCubit>();
|
||||
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<ConsultationPaymentCubit>.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<ConsultationPaymentCubit, ConsultationPaymentState>(
|
||||
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<void> _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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user