This commit is contained in:
2026-07-08 13:09:20 +03:00
parent b9da1d3064
commit 9eda119527
42 changed files with 830 additions and 279 deletions

View File

@@ -0,0 +1,165 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../components/gradient_button.dart';
enum CardBindingStatus { success, failed }
class CardBindingResultScreen extends StatefulWidget {
final CardBindingStatus status;
final String title;
final String cardImagePath;
const CardBindingResultScreen({
super.key,
required this.status,
required this.title,
required this.cardImagePath,
});
@override
State<CardBindingResultScreen> createState() => _CardBindingResultScreenState();
}
class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
Timer? _autoPopTimer;
@override
void initState() {
super.initState();
if (widget.status == CardBindingStatus.success) {
_autoPopTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
context.go('/home/payment-methods');
}
});
}
}
@override
void dispose() {
_autoPopTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isSuccess = widget.status == CardBindingStatus.success;
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF1B284F),
Color(0xFF0F172A),
],
),
),
child: SafeArea(
child: Stack(
children: [
// Positioned(
// bottom: 50,
// left: 0,
// right: 0,
// child: Image.asset(
// 'assets/wave.png',
// fit: BoxFit.cover,
// ),
// ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// const Spacer(),
Text(
widget.title,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 40),
Image.asset(
isSuccess
? 'assets/icons/success.png'
: 'assets/icons/failed.png',
width: 140,
height: 140,
),
const SizedBox(height: 40),
if (isSuccess) ...[
Padding(
padding: const EdgeInsets.only(bottom: 40.0),
// child: Image.asset(
// widget.cardImagePath,
// width: double.infinity,
// fit: BoxFit.contain,
// ),
),
] else ...[
const Text(
'Проверьте подключение к\nинтернету и попробуйте снова',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF94A3B8),
fontSize: 16,
height: 1.4,
),
),
const SizedBox(height: 40),
GradientButton(
text: "Попробовать снова",
onTap: () {},
enabled: true,
showArrows: true,
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 56,
child: TextButton(
onPressed: () => context.go("/home/payment-methods"),
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
backgroundColor: const Color(0xFF0F172A).withOpacity(0.4),
),
child: const Text(
'Закрыть',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
],
],
),
),
],
),
),
),
);
}
}

View File

@@ -5,6 +5,7 @@ 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 '../../domain/usecase/set_main_payment_card_usecase.dart';
import '../components/custom_app_bar.dart';
import '../components/gradient_button.dart';
import '../components/payment_option.dart';
@@ -212,10 +213,10 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
(state.useBalance || state.selectedCard != null)
? Padding(padding: const EdgeInsets.symmetric(horizontal: 20),
child: PaymentOption(
title: state.useBalance ? 'Баланс' : state.selectedCard!.type,
title: state.useBalance ? 'Баланс' : "",
subtitle: state.useBalance
? '${state.userBalance.toStringAsFixed(2)} BYN'
: '****${state.selectedCard!.cardLastNumber}',
: '${state.selectedCard!.cardLastNumber}',
isSelected: true,
onTap: () async {
final result = await showModalBottomSheet<dynamic>(
@@ -225,6 +226,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
builder: (innerContext) => BlocProvider(
create: (context) => PaymentMethodSheetBloc(
getIt<GetPaymentCardsUsecase>(),
getIt<SetMainPaymentCardUsecase>(),
)..add(PaymentMethodSheetStarted()),
child: PaymentMethodSheet(
initialSelectedCard: state.useBalance ? null : state.selectedCard,

View File

@@ -184,11 +184,14 @@ class PaymentMethodsScreen extends StatelessWidget {
color: Colors.transparent,
child: InkWell(
onTap: () async {
final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
context.read<PaymentMethodsBloc>().add(AddPayemntCardPressed());
/*final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
if (isCardAdded == true && context.mounted) {
context.read<PaymentMethodsBloc>().add(PaymentMethodsStarted());
}
}*/
},
borderRadius: BorderRadius.circular(24),
child: const Padding(
@@ -264,12 +267,7 @@ class _CardItem extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
Image.asset(
_getDefaultIconPath(card.type),
width: 40,
height: 40,
fit: BoxFit.contain,
),
Icon(Icons.credit_card, color: Color(0xFFFFFFFF), size: 40),
const SizedBox(width: 12),
Expanded(
child: GestureDetector(
@@ -279,7 +277,7 @@ class _CardItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${card.type} ****${card.cardLastNumber}',
'Карта ${card.cardLastNumber}',
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),

View File

@@ -79,17 +79,18 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
if (state.isSuccess) {
context.go("/pin");
} else if (state.error != null) {
setState(() {
_isFirstTry = false;
});
setState(() { _isFirstTry = false; });
codeController.clear();
FocusScope.of(context).requestFocus(codeFocusNode);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.error!)));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.error!)));
} else if (state.isBlocked) {
context.go("/block");
}
if (state.secondsLeft == 0 && codeFocusNode.hasFocus) {
codeFocusNode.unfocus();
codeController.clear();
}
},
builder: (context, state) {
return Scaffold(
@@ -126,12 +127,21 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
),
const SizedBox(height: 35),
GestureDetector(
onTap: openKeyboard,
onTap: state.secondsLeft > 0 ? openKeyboard : null,
child: CodeDots(code: state.code, length: 6),
),
const SizedBox(height: 8),
if (!_isFirstTry)
if (state.secondsLeft == 0)
const Text(
"Время действия кода истекло. Запросите новый.",
style: TextStyle(
color: AppColors.pinError,
fontSize: 14,
fontWeight: FontWeight.bold,
),
)
else if (!_isFirstTry)
Text(
"Осталось ${state.attemptsLeft} попытк${state.attemptsLeft == 1 ? 'а' : 'и'}",
style: const TextStyle(
@@ -145,14 +155,14 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
text: state.secondsLeft == 0
? "Отправить код повторно"
: "Отправить код повторно\nчерез 00:${state.secondsLeft.toString().padLeft(2, '0')} сек.",
onTap: state.secondsLeft == 0
onTap: (state.secondsLeft == 0 && !state.isSubmitting)
? () {
context.read<VerifyCodeBloc>().add(
ResendCodePressed(),
);
}
: () {},
enabled: state.secondsLeft == 0,
context.read<VerifyCodeBloc>().add(
ResendCodePressed(),
);
}
: null,
enabled: state.secondsLeft == 0 && !state.isSubmitting,
width: 250,
),
const Spacer(),
@@ -167,6 +177,7 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
keyboardType: TextInputType.number,
maxLength: 6,
autofocus: true,
enabled: state.secondsLeft > 0,
),
),
),

View File

@@ -12,7 +12,8 @@ import '../state/scooter_detail_state.dart';
import '../viewmodel/scooter_detail_bloc.dart';
class ScooterDetailScreen extends StatelessWidget {
const ScooterDetailScreen({super.key});
final String address;
const ScooterDetailScreen({super.key, required this.address});
@override
Widget build(BuildContext context) {
@@ -83,7 +84,7 @@ class ScooterDetailScreen extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Expanded(child: ScooterInfoSection()),
Expanded(child: ScooterInfoSection(distance: scooter?.distance ?? 0, location: address)),
const SizedBox(width: 20),
],
),

View File

@@ -3,13 +3,21 @@ 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 '../../domain/usecase/set_main_payment_card_usecase.dart';
import '../components/app_checkbox.dart';
import '../components/custom_app_bar.dart';
import '../components/gradient_button.dart';
import '../components/payment_option.dart';
import '../components/period_selector.dart';
import '../components/content_parser.dart'; // ✅ Парсер контента
import '../components/sheet/payment_method_sheet.dart';
import '../event/payment_method_sheet_event.dart';
import '../event/subscription_details_event.dart';
import '../state/susbcription_details_state.dart';
import '../viewmodel/payment_method_sheet_bloc.dart';
import '../viewmodel/susbcription_details_bloc.dart';
class SubscriptionDetailsScreen extends StatelessWidget {
@@ -50,11 +58,9 @@ class SubscriptionDetailsScreen extends StatelessWidget {
),
const SizedBox(height: 20),
// 🔹 Контент
Expanded(
child: Stack(
children: [
// Волна снизу
Positioned(
bottom: 0,
left: 0,
@@ -108,10 +114,9 @@ class SubscriptionDetailsScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 Обёртка для растягивания контента на всю ширину
Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // ✅ Растягивает детей на всю ширину
mainAxisSize: MainAxisSize.min, // ✅ Не занимает лишнее место по вертикали
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: ContentParser.parseContent(
textJson: state.subscription.fullDescriptionJson,
text: state.subscription.fullDescription,
@@ -121,6 +126,14 @@ class SubscriptionDetailsScreen extends StatelessWidget {
if (isAvailableForPurchase) ...[
const SizedBox(height: 30),
_ActionCard(state: state),
const SizedBox(height: 24),
const Text(
'Способ оплаты',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(height: 12),
_buildCardSelector(state, context),
const SizedBox(height: 30),
GradientButton(
text: state.isAlreadyPurchased ? 'Продлить' : 'Активировать',
@@ -129,7 +142,7 @@ class SubscriptionDetailsScreen extends StatelessWidget {
ActivateSubscriptionPressed(),
);
},
enabled: state.isAgreed,
enabled: state.isAgreed && state.selectedCard != null,
width: double.infinity,
height: 56,
fontSize: 16,
@@ -141,8 +154,77 @@ class SubscriptionDetailsScreen extends StatelessWidget {
),
);
}
Widget _buildCardSelector(DetailsContentState state, BuildContext context) {
if (state.selectedCard != null) {
return PaymentOption(
title: "Карта",
subtitle: state.selectedCard.cardLastNumber,
isSelected: true,
onTap: () async {
final selectedCard = await showModalBottomSheet<PaymentCard>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (innerContext) => BlocProvider(
create: (context) => PaymentMethodSheetBloc(
getIt<GetPaymentCardsUsecase>(),
getIt<SetMainPaymentCardUsecase>(),
)..add(PaymentMethodSheetStarted()),
child: const PaymentMethodSheet(showBalance: false),
),
);
if (selectedCard != null && context.mounted) {
context.read<SubscriptionDetailsBloc>().add(SelectCardEvent(selectedCard));
}
},
);
}
return 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)),
],
),
),
),
),
);
}
}
class _ActionCard extends StatelessWidget {
final DetailsContentState state;
@@ -216,6 +298,7 @@ class _ActionCard extends StatelessWidget {
),
);
}
}
class _PriceRow extends StatelessWidget {

View File

@@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:path/path.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 '../../domain/usecase/set_main_payment_card_usecase.dart';
import '../components/app_checkbox.dart';
import '../components/custom_app_bar.dart'; // ✅ Уже есть
import '../components/gradient_button.dart';
@@ -22,54 +24,56 @@ class TopUpScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const SizedBox(height: 16),
const CustomAppBar(title: 'Пополнение баланса'),
const SizedBox(height: 20),
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const SizedBox(height: 16),
const CustomAppBar(title: 'Пополнение баланса'),
const SizedBox(height: 20),
BlocBuilder<TopUpBloc, TopUpState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
BlocBuilder<TopUpBloc, TopUpState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Expanded(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTariffList(state, context),
const SizedBox(height: 30),
_buildPriceInfo(state),
const SizedBox(height: 40),
const Text(
'Способ оплаты',
style: TextStyle(color: Colors.white70),
),
const SizedBox(height: 15),
_buildCardSelector(state, context),
const SizedBox(height: 20),
_buildAgreement(state, context),
const Spacer(),
_buildPayButton(state),
const SizedBox(height: 30),
],
),
],
),
);
},
),
],
return Expanded(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTariffList(state, context),
const SizedBox(height: 30),
_buildPriceInfo(state),
const SizedBox(height: 40),
const Text(
'Способ оплаты',
style: TextStyle(color: Colors.white70),
),
const SizedBox(height: 15),
_buildCardSelector(state, context),
const SizedBox(height: 20),
_buildAgreement(state, context),
const Spacer(),
_buildPayButton(state, () {
context.read<TopUpBloc>().add(TopUpButtonPressed());
}),
const SizedBox(height: 30),
],
),
],
),
);
},
),
],
),
),
),
)
)
),
);
}
@@ -158,20 +162,24 @@ class TopUpScreen extends StatelessWidget {
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: PaymentOption(
title: state.selectedCard!.type,
subtitle: '****${state.selectedCard!.cardLastNumber}',
title: "Карта",
subtitle: '${state.selectedCard!.cardLastNumber}',
isSelected: true,
onTap: () async {
final selectedCard = await showModalBottomSheet<PaymentCard>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (innerContext) => BlocProvider(
create: (context) =>
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>())
..add(PaymentMethodSheetStarted()),
child: const PaymentMethodSheet(showBalance: false,),
),
builder: (innerContext) =>
BlocProvider(
create: (context) =>
PaymentMethodSheetBloc(
getIt<GetPaymentCardsUsecase>(),
getIt<SetMainPaymentCardUsecase>(),
)
..add(PaymentMethodSheetStarted()),
child: const PaymentMethodSheet(showBalance: false,),
),
);
if (selectedCard != null) {
@@ -260,10 +268,10 @@ class TopUpScreen extends StatelessWidget {
);
}
Widget _buildPayButton(TopUpState state) {
Widget _buildPayButton(TopUpState state, VoidCallback onTap) {
return GradientButton(
text: 'Оплатить',
onTap: state.isAgreed ? () {} : null,
onTap: state.isAgreed ? onTap : null,
enabled: state.isAgreed,
showArrows: true,
height: 56,