405 lines
18 KiB
Dart
405 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import '../../core/app_colors.dart';
|
||
import '../../di/service_locator.dart';
|
||
import '../../domain/entities/payment_card.dart';
|
||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||
import '../components/custom_app_bar.dart';
|
||
import '../components/gradient_button.dart';
|
||
import '../components/payment_option.dart';
|
||
import '../components/sheet/payment_method_sheet.dart';
|
||
import '../event/payment_confirm_event.dart';
|
||
import '../event/payment_method_sheet_event.dart';
|
||
import '../state/payment_confirm_state.dart';
|
||
import '../viewmodel/payment_confirm_bloc.dart';
|
||
import '../viewmodel/payment_method_sheet_bloc.dart';
|
||
|
||
class PaymentConfirmScreen extends StatelessWidget {
|
||
final int orderId;
|
||
final List<int> photoIds;
|
||
|
||
const PaymentConfirmScreen({
|
||
super.key,
|
||
required this.orderId,
|
||
required this.photoIds,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return _PaymentConfirmScreenContent(orderId: orderId, photoIds: photoIds);
|
||
}
|
||
}
|
||
|
||
class _PaymentConfirmScreenContent extends StatelessWidget {
|
||
final int orderId;
|
||
final List<int> photoIds;
|
||
|
||
const _PaymentConfirmScreenContent({
|
||
required this.orderId,
|
||
required this.photoIds,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
body: Container(
|
||
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
|
||
child: SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
Positioned(
|
||
bottom: 60,
|
||
left: 0,
|
||
right: 0,
|
||
child: Image.asset(
|
||
'assets/wave.png',
|
||
fit: BoxFit.fitWidth,
|
||
color: Colors.white.withOpacity(0.1),
|
||
),
|
||
),
|
||
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: 16),
|
||
const Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||
child: CustomAppBar(title: 'Завершение поездки'),
|
||
),
|
||
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||
child: BlocConsumer<PaymentConfirmBloc, PaymentConfirmState>(
|
||
listenWhen: (previous, current) {
|
||
return current.status != previous.status;
|
||
},
|
||
|
||
listener: (context, state) {
|
||
if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) {
|
||
context.go('/home');
|
||
} else if (state.status == PaymentConfirmStatus.failure) {
|
||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
state.errorMessage ?? 'Произошла ошибка при оплате',
|
||
style: const TextStyle(color: Colors.white),
|
||
),
|
||
backgroundColor: Colors.redAccent.withOpacity(0.9),
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
margin: const EdgeInsets.all(20),
|
||
duration: const Duration(seconds: 3),
|
||
),
|
||
);
|
||
}
|
||
},
|
||
builder: (context, state) {
|
||
final order = state.order;
|
||
|
||
if (state.status == PaymentConfirmStatus.loading &&
|
||
order == null) {
|
||
return const Center(
|
||
child: CircularProgressIndicator(),
|
||
);
|
||
}
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: 30),
|
||
|
||
_buildInfoLabel('Самокат:'),
|
||
_buildValueRow(
|
||
'qr_icon.png',
|
||
"${order?.scooter?.number}",
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
_buildInfoLabel('Расстояние:'),
|
||
_buildValueRow(
|
||
'distance_icon.png',
|
||
"${order?.mileage} km",
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
_buildInfoLabel('Начислено баллов:'),
|
||
_buildValueRow('points_icon.png', '2 балла'),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment:
|
||
CrossAxisAlignment.start,
|
||
children: [
|
||
_buildInfoLabel('Стоимость поездки:'),
|
||
_buildValueRow(
|
||
'money_icon.png',
|
||
'17,17 BYN',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment:
|
||
CrossAxisAlignment.start,
|
||
children: [
|
||
_buildInfoLabel('Оплачено:'),
|
||
_buildValueRow(
|
||
'money_icon.png',
|
||
'10,00 BYN',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
const SizedBox(height: 40),
|
||
|
||
// 🔹 БЛОК СУММЫ К ОПЛАТЕ
|
||
Center(
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
'Сумма к оплате:',
|
||
style: TextStyle(
|
||
color: Colors.white.withOpacity(0.8),
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
mainAxisAlignment:
|
||
MainAxisAlignment.center,
|
||
children: [
|
||
Image.asset(
|
||
'assets/icons/money_icon.png',
|
||
width: 40,
|
||
),
|
||
const SizedBox(width: 12),
|
||
const Text(
|
||
'7,17 BYN',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 32,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 40),
|
||
|
||
// 🔹 КАРТА ОПЛАТЫ
|
||
(state.useBalance || state.selectedCard != null)
|
||
? Padding(padding: const EdgeInsets.symmetric(horizontal: 20),
|
||
child: PaymentOption(
|
||
title: state.useBalance ? 'Баланс' : state.selectedCard!.type,
|
||
subtitle: state.useBalance
|
||
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
||
: '****${state.selectedCard!.cardLastNumber}',
|
||
isSelected: true,
|
||
onTap: () async {
|
||
final result = await showModalBottomSheet<dynamic>(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: Colors.transparent,
|
||
builder: (innerContext) => BlocProvider(
|
||
create: (context) => PaymentMethodSheetBloc(
|
||
getIt<GetPaymentCardsUsecase>(),
|
||
)..add(PaymentMethodSheetStarted()),
|
||
child: PaymentMethodSheet(
|
||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||
),
|
||
),
|
||
);
|
||
|
||
if (result != null) {
|
||
if (result is PaymentCard) {
|
||
context.read<PaymentConfirmBloc>().add(PaymentCardChanged(result));
|
||
} else if (result == 'balance') {
|
||
context.read<PaymentConfirmBloc>().add(SelectBalancePressed());
|
||
}
|
||
}
|
||
},
|
||
),
|
||
)
|
||
: Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 20,
|
||
),
|
||
child: Container(
|
||
height: 56,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(
|
||
24,
|
||
),
|
||
border: Border.all(
|
||
color: Colors.white.withOpacity(
|
||
0.4,
|
||
),
|
||
width: 1,
|
||
),
|
||
),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: () {
|
||
context.pushReplacement(
|
||
'/home/payment-method-sheet',
|
||
);
|
||
|
||
},
|
||
borderRadius: BorderRadius.circular(
|
||
24,
|
||
),
|
||
child: Center(
|
||
child: Row(
|
||
mainAxisAlignment:
|
||
MainAxisAlignment.center,
|
||
children: [
|
||
const Text(
|
||
'Способ оплаты',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 16,
|
||
fontWeight:
|
||
FontWeight.w600,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Icon(
|
||
Icons.arrow_forward_ios,
|
||
size: 12,
|
||
color: Colors.white
|
||
.withOpacity(0.6),
|
||
),
|
||
Icon(
|
||
Icons.arrow_forward_ios,
|
||
size: 12,
|
||
color: Colors.white
|
||
.withOpacity(0.4),
|
||
),
|
||
Icon(
|
||
Icons.arrow_forward_ios,
|
||
size: 12,
|
||
color: Colors.white
|
||
.withOpacity(0.2),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
|
||
Padding(
|
||
padding: const EdgeInsets.all(24.0),
|
||
child:
|
||
BlocConsumer<PaymentConfirmBloc, PaymentConfirmState>(
|
||
listener: (context, state) {
|
||
if (state.status == PaymentConfirmStatus.success &&
|
||
state.paymentCompleted) {
|
||
context.go('/home');
|
||
} else if (state.status ==
|
||
PaymentConfirmStatus.failure) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
state.errorMessage ?? 'Ошибка оплаты',
|
||
),
|
||
),
|
||
);
|
||
}
|
||
},
|
||
builder: (context, state) {
|
||
return GradientButton(
|
||
text: state.status == PaymentConfirmStatus.loading
|
||
? 'Обработка...'
|
||
: 'Оплатить',
|
||
showArrows: true,
|
||
height: 56,
|
||
width: double.infinity,
|
||
onTap: (state.status == PaymentConfirmStatus.loading || (!state.useBalance && state.selectedCard == null))
|
||
? null
|
||
: () {
|
||
context.read<PaymentConfirmBloc>().add(
|
||
PayRide(
|
||
cardId: state.useBalance ? null : state.selectedCard?.id,
|
||
isBalance: state.useBalance,
|
||
orderId: orderId,
|
||
photoIds: photoIds,
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Вспомогательный метод для подзаголовков
|
||
Widget _buildInfoLabel(String text) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 8),
|
||
child: Text(
|
||
text,
|
||
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 15),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Вспомогательный метод для строк со значениями и иконками
|
||
Widget _buildValueRow(String iconName, String value) {
|
||
return Row(
|
||
children: [
|
||
Image.asset('assets/icons/$iconName', width: 24, height: 24),
|
||
const SizedBox(width: 12),
|
||
Text(
|
||
value,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
String _formatDuration(int minutes) {
|
||
final h = minutes ~/ 60;
|
||
final m = minutes % 60;
|
||
return '${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}';
|
||
}
|
||
}
|