fix
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<intent>
|
||||
@@ -49,6 +50,14 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="card_binding_deep_link">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="com.behappy.app" android:host="card-result" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
||||
@@ -2,3 +2,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
yandexMapkit.variant=full
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
BIN
assets/icons/card_result_screen.png
Normal file
BIN
assets/icons/card_result_screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
BIN
assets/icons/failed.png
Normal file
BIN
assets/icons/failed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/icons/success.png
Normal file
BIN
assets/icons/success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
@@ -2,7 +2,7 @@ import '../../domain/entities/payment_card.dart';
|
||||
|
||||
class PaymentCardResponseDto {
|
||||
final int id;
|
||||
final int clientId;
|
||||
final String bindingId;
|
||||
final int expirationMonth;
|
||||
final int expirationYear;
|
||||
final String cardHolder;
|
||||
@@ -12,7 +12,7 @@ class PaymentCardResponseDto {
|
||||
|
||||
PaymentCardResponseDto({
|
||||
required this.id,
|
||||
required this.clientId,
|
||||
required this.bindingId,
|
||||
required this.expirationMonth,
|
||||
required this.expirationYear,
|
||||
required this.cardHolder,
|
||||
@@ -24,7 +24,7 @@ class PaymentCardResponseDto {
|
||||
factory PaymentCardResponseDto.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentCardResponseDto(
|
||||
id: json['id'] as int,
|
||||
clientId: json['clientId'] as int,
|
||||
bindingId: json['bindingId'] as String,
|
||||
expirationMonth: json['expirationMonth'] as int,
|
||||
expirationYear: json['expirationYear'] as int,
|
||||
cardHolder: json['cardHolder'] as String,
|
||||
@@ -37,13 +37,9 @@ class PaymentCardResponseDto {
|
||||
PaymentCard toEntity(String? fullCardNumber) {
|
||||
return PaymentCard(
|
||||
id: id,
|
||||
clientId: clientId,
|
||||
expirationMonth: expirationMonth,
|
||||
expirationYear: expirationYear,
|
||||
cardHolder: cardHolder,
|
||||
bindingId: bindingId,
|
||||
cardLastNumber: cardLastNumber,
|
||||
isMain: isMain,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,11 @@ class ApiService {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
"$baseUrl/auth/client/login",
|
||||
data: {"phone": phone, "model": model, "systemId": systemId},
|
||||
data: {
|
||||
"phone": phone,
|
||||
"model": model,
|
||||
"systemId": systemId
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
@@ -177,13 +181,9 @@ class ApiService {
|
||||
cardsJson.map((cardJson) async {
|
||||
return PaymentCard(
|
||||
id: cardJson['id'],
|
||||
clientId: cardJson['clientId'],
|
||||
expirationMonth: cardJson['expirationMonth'],
|
||||
expirationYear: cardJson['expirationYear'],
|
||||
cardHolder: cardJson['cardHolder'],
|
||||
bindingId: cardJson['bindingId'],
|
||||
cardLastNumber: cardJson['cardLastNumber'],
|
||||
isMain: cardJson['isMain'],
|
||||
type: cardJson['cardType'],
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -517,13 +517,13 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> activateSubscription({required int optionId}) async {
|
||||
Future<bool> activateSubscription({required int optionId, required int cardId}) async {
|
||||
const url = "$baseUrl/scootersubscription/client";
|
||||
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
url,
|
||||
data: {"optionId": optionId},
|
||||
data: {"optionId": optionId, "cardId": cardId},
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
@@ -539,31 +539,25 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addPaymentCard({
|
||||
required String cardNumber,
|
||||
required String cardHolder,
|
||||
required int expirationMonth,
|
||||
required int expirationYear,
|
||||
required String cvv,
|
||||
}) async {
|
||||
Future<Map<String, dynamic>> addPaymentCard() async {
|
||||
const url = "$baseUrl/client/card";
|
||||
final cleanCardNumber = cardNumber.replaceAll(' ', '');
|
||||
/*final cleanCardNumber = cardNumber.replaceAll(' ', '');*/
|
||||
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
url,
|
||||
data: {
|
||||
/*data: {
|
||||
"cardNumber": cleanCardNumber,
|
||||
"cardHolder": cardHolder,
|
||||
"expirationMonth": expirationMonth,
|
||||
"expirationYear": expirationYear,
|
||||
"cvv": cvv,
|
||||
},
|
||||
},*/
|
||||
options: await _getAuthOptions(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
|
||||
|
||||
@@ -32,25 +32,25 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void>> addPaymentCard({
|
||||
required String cardNumber,
|
||||
Future<Result<Map<String, dynamic>>> addPaymentCard(
|
||||
/*required String cardNumber,
|
||||
required String cardHolder,
|
||||
required String expiryMonth,
|
||||
required String expiryYear,
|
||||
required String cvv,
|
||||
}) async {
|
||||
required String cvv,*/
|
||||
) async {
|
||||
try {
|
||||
await apiService.addPaymentCard(
|
||||
cardNumber: cardNumber,
|
||||
final result = await apiService.addPaymentCard(
|
||||
/*cardNumber: cardNumber,
|
||||
cardHolder: cardHolder,
|
||||
expirationMonth: int.parse(expiryMonth),
|
||||
expirationYear: int.parse(expiryYear),
|
||||
cvv: cvv,
|
||||
cvv: cvv,*/
|
||||
);
|
||||
|
||||
// await securityService.saveCardFullNumber(cardId, cardNumber);
|
||||
|
||||
return Success(null);
|
||||
return Success(result);
|
||||
} on AuthException catch (e) {
|
||||
return Failure(AuthFailure(e.attemptsLeft));
|
||||
} on AuthBlockException catch (_) {
|
||||
@@ -98,9 +98,9 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool>> activateSubscription(int optionId) async {
|
||||
Future<Result<bool>> activateSubscription(int optionId, int cardId) async {
|
||||
try {
|
||||
final success = await apiService.activateSubscription(optionId: optionId);
|
||||
final success = await apiService.activateSubscription(optionId: optionId, cardId: cardId);
|
||||
return Success(success);
|
||||
} on AuthException catch (e) {
|
||||
return Failure(AuthFailure(e.attemptsLeft));
|
||||
|
||||
@@ -305,7 +305,7 @@ Future<void> setupDependencies() async {
|
||||
|
||||
getIt.registerFactory<PhoneAuthBloc>(() => PhoneAuthBloc(getIt()));
|
||||
|
||||
getIt.registerFactory<VerifyCodeBloc>(() => VerifyCodeBloc(getIt()));
|
||||
getIt.registerFactory<VerifyCodeBloc>(() => VerifyCodeBloc(getIt(), getIt()));
|
||||
|
||||
getIt.registerFactory<ProfileBloc>(() => ProfileBloc(getIt(), getIt(), getIt()));
|
||||
|
||||
@@ -337,7 +337,7 @@ Future<void> setupDependencies() async {
|
||||
|
||||
|
||||
getIt.registerFactory<PaymentMethodSheetBloc>(
|
||||
() => PaymentMethodSheetBloc(getIt()),
|
||||
() => PaymentMethodSheetBloc(getIt(), getIt()),
|
||||
);
|
||||
|
||||
getIt.registerFactory<CurrentRidesBloc>(() => CurrentRidesBloc(getIt()));
|
||||
|
||||
@@ -1,43 +1,27 @@
|
||||
class PaymentCard {
|
||||
final int id;
|
||||
final int clientId;
|
||||
final int expirationMonth;
|
||||
final int expirationYear;
|
||||
final String cardHolder;
|
||||
final String? bindingId;
|
||||
final String cardLastNumber;
|
||||
final bool isMain;
|
||||
final String type;
|
||||
|
||||
PaymentCard({
|
||||
required this.id,
|
||||
required this.clientId,
|
||||
required this.expirationMonth,
|
||||
required this.expirationYear,
|
||||
required this.cardHolder,
|
||||
required this.bindingId,
|
||||
required this.cardLastNumber,
|
||||
required this.isMain,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
PaymentCard copyWith({
|
||||
int? id,
|
||||
int? clientId,
|
||||
int? expirationMonth,
|
||||
int? expirationYear,
|
||||
String? cardHolder,
|
||||
String? bindingId,
|
||||
String? cardLastNumber,
|
||||
bool? isMain,
|
||||
String? type,
|
||||
}) {
|
||||
return PaymentCard(
|
||||
id: id ?? this.id,
|
||||
clientId: clientId ?? this.clientId,
|
||||
expirationMonth: expirationMonth ?? this.expirationMonth,
|
||||
expirationYear: expirationYear ?? this.expirationYear,
|
||||
cardHolder: cardHolder ?? this.cardHolder,
|
||||
bindingId: bindingId ?? this.bindingId,
|
||||
cardLastNumber: cardLastNumber ?? this.cardLastNumber,
|
||||
isMain: isMain ?? this.isMain,
|
||||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,17 @@ import '../entities/payment_card.dart';
|
||||
abstract class PaymentRepository {
|
||||
Future<Result<List<PaymentCard>>> getPaymentCards();
|
||||
|
||||
Future<Result<void>> addPaymentCard({
|
||||
required String cardNumber,
|
||||
Future<Result<Map<String, dynamic>>> addPaymentCard(
|
||||
/*required String cardNumber,
|
||||
required String cardHolder,
|
||||
required String expiryMonth,
|
||||
required String expiryYear,
|
||||
required String cvv,
|
||||
});
|
||||
required String cvv,*/
|
||||
);
|
||||
|
||||
Future<Result<void>> setMainPaymentCard(int cardId);
|
||||
|
||||
Future<Result<void>> removePaymentCard(int cardId);
|
||||
|
||||
Future<Result<bool>> activateSubscription(int optionId);
|
||||
Future<Result<bool>> activateSubscription(int optionId, int cardId);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ class ActivateSubscriptionUsecase {
|
||||
|
||||
ActivateSubscriptionUsecase(this.repository);
|
||||
|
||||
Future<Result<bool>> call(int optionId) {
|
||||
return repository.activateSubscription(optionId);
|
||||
Future<Result<bool>> call(int optionId, int cardId) {
|
||||
return repository.activateSubscription(optionId, cardId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,19 @@ class AddPaymentCardUsecase {
|
||||
|
||||
AddPaymentCardUsecase(this.repository);
|
||||
|
||||
Future<Result<void>> call({
|
||||
required String cardNumber,
|
||||
Future<Result<Map<String, dynamic>>> call(
|
||||
/*required String cardNumber,
|
||||
required String cardHolder,
|
||||
required String expiryMonth,
|
||||
required String expiryYear,
|
||||
required String cvv,
|
||||
}) {
|
||||
required String cvv,*/
|
||||
) {
|
||||
return repository.addPaymentCard(
|
||||
cardNumber: cardNumber,
|
||||
/*cardNumber: cardNumber,
|
||||
cardHolder: cardHolder,
|
||||
expiryMonth: expiryMonth,
|
||||
expiryYear: expiryYear,
|
||||
cvv: cvv,
|
||||
cvv: cvv,*/
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,22 @@ import 'package:flutter/material.dart';
|
||||
import 'scooter_info_item.dart';
|
||||
|
||||
class ScooterInfoSection extends StatelessWidget {
|
||||
const ScooterInfoSection({super.key});
|
||||
final double distance;
|
||||
final String location;
|
||||
const ScooterInfoSection({super.key, required this.distance, required this.location});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: const [
|
||||
children: [
|
||||
ScooterInfoItem(icon: 'assets/icons/bolt.png', text: '47 км или 4 часа 17 минут'),
|
||||
ScooterInfoItem(icon: 'assets/icons/speed.png', text: 'max = 25 км/ч'),
|
||||
ScooterInfoItem(icon: 'assets/icons/location.png', text: 'пр. Московский, 33'),
|
||||
ScooterInfoItem(icon: 'assets/icons/location.png', text: location),
|
||||
Row(
|
||||
children: [
|
||||
ScooterInfoItem(icon: 'assets/icons/person.png', text: '120 м'),
|
||||
ScooterInfoItem(icon: 'assets/icons/person.png', text: distance.toInt() >= 1000
|
||||
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
|
||||
: '${distance.toInt()} m',),
|
||||
SizedBox(width: 16),
|
||||
ScooterInfoItem(icon: 'assets/icons/time.png', text: '1 минута'),
|
||||
],
|
||||
|
||||
@@ -128,6 +128,7 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
child: _ScooterCard(
|
||||
scooter: scooter,
|
||||
isActive: isActive,
|
||||
address: state.address ?? "Unknown address",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -151,8 +152,9 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
class _ScooterCard extends StatelessWidget {
|
||||
final Scooter scooter;
|
||||
final bool isActive;
|
||||
final String address;
|
||||
|
||||
const _ScooterCard({required this.scooter, required this.isActive});
|
||||
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -186,7 +188,9 @@ class _ScooterCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"${(scooter.distance?.toInt()) ?? 0}m",
|
||||
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||
: '${scooter.distance?.toInt() ?? 0} m',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
@@ -229,7 +233,7 @@ class _ScooterCard extends StatelessWidget {
|
||||
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}');
|
||||
context.go('/home/scooter/${scooter.id}', extra: address);
|
||||
},
|
||||
child: Container(
|
||||
height: 42,
|
||||
@@ -241,7 +245,7 @@ class _ScooterCard extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
fontSize: 12,
|
||||
onTap: () {
|
||||
context.go('/home/scooter/${scooter.id}');
|
||||
context.go('/home/scooter/${scooter.id}', extra: address);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -192,13 +192,18 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: PaymentOption(
|
||||
title: card.type,
|
||||
subtitle: '****${card.cardLastNumber}',
|
||||
title: card.isMain ? "Основная" : "",
|
||||
subtitle: '${card.cardLastNumber}',
|
||||
isSelected: _selectedPaymentMethod == index,
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
_selectedPaymentMethod = index;
|
||||
});
|
||||
|
||||
context.read<PaymentMethodSheetBloc>().add(
|
||||
SelectPaymentCardRequested(card),
|
||||
);
|
||||
|
||||
Navigator.pop(context, card);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -151,6 +151,7 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
child: _ScooterCard(
|
||||
scooter: scooter,
|
||||
isActive: isActive,
|
||||
address: state.address ?? "Unknown address",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -174,9 +175,10 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
||||
|
||||
class _ScooterCard extends StatelessWidget {
|
||||
final Scooter scooter;
|
||||
final String address;
|
||||
final bool isActive;
|
||||
|
||||
const _ScooterCard({required this.scooter, required this.isActive});
|
||||
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -210,7 +212,9 @@ class _ScooterCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"${(scooter.distance?.toInt()) ?? 0}m",
|
||||
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||
: '${scooter.distance?.toInt() ?? 0} m',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
@@ -262,7 +266,7 @@ class _ScooterCard extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
fontSize: 12,
|
||||
onTap: () {
|
||||
Navigator.pop(context, scooter);
|
||||
Navigator.pop(context, [scooter, address]);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../../domain/entities/payment_card.dart';
|
||||
import '../../../domain/entities/scooter.dart';
|
||||
import '../../../domain/entities/tariff.dart';
|
||||
import '../../../domain/usecase/get_payment_cards_usecase.dart';
|
||||
import '../../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||
import '../../event/payment_method_sheet_event.dart';
|
||||
import '../../event/tariff_sheet_event.dart';
|
||||
import '../../state/tariff_sheet_state.dart';
|
||||
@@ -288,10 +289,10 @@ class _TariffSheetState extends State<TariffSheet> {
|
||||
child: PaymentOption(
|
||||
title: state.useBalance
|
||||
? 'Баланс'
|
||||
: state.selectedCard!.type,
|
||||
: "Карта",
|
||||
subtitle: state.useBalance
|
||||
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
||||
: '****${state.selectedCard!.cardLastNumber}',
|
||||
: '${state.selectedCard!.cardLastNumber}',
|
||||
isSelected: true,
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<dynamic>(
|
||||
@@ -301,6 +302,7 @@ class _TariffSheetState extends State<TariffSheet> {
|
||||
builder: (innerContext) => BlocProvider(
|
||||
create: (context) => PaymentMethodSheetBloc(
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
)..add(PaymentMethodSheetStarted()),
|
||||
child: PaymentMethodSheet(
|
||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||||
@@ -381,7 +383,6 @@ class _TariffSheetState extends State<TariffSheet> {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 КНОПКА "ЗАБРОНИРОВАТЬ"
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: GradientButton(
|
||||
@@ -494,13 +495,10 @@ class _TariffCard extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// Резервируем место под иконку инфо справа,
|
||||
// чтобы текст не залез под неё
|
||||
const SizedBox(width: 24),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Цена + текст рядом
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
@@ -540,7 +538,6 @@ class _TariffCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
|
||||
// Кнопка-иконка в верхнем правом углу
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
|
||||
@@ -121,10 +121,10 @@ class SideMenu extends StatelessWidget {
|
||||
_MenuItem(
|
||||
icon: 'assets/icons/logout_icon.png',
|
||||
title: 'Выход',
|
||||
onTap: () => {
|
||||
context.go('/login'),
|
||||
context.read<MapBloc>().stopNotificationStream(),
|
||||
context.read<MapBloc>().add(LogoutPressed()),
|
||||
onTap: () {
|
||||
context.read<MapBloc>().stopNotificationStream();
|
||||
context.read<MapBloc>().add(LogoutPressed());
|
||||
context.go('/login');
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
|
||||
sealed class PaymentMethodSheetEvent {}
|
||||
|
||||
class PaymentMethodSheetStarted extends PaymentMethodSheetEvent {}
|
||||
|
||||
class SelectPaymentCardRequested extends PaymentMethodSheetEvent {
|
||||
final PaymentCard card;
|
||||
SelectPaymentCardRequested(this.card);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ sealed class PaymentMethodsEvent {}
|
||||
|
||||
class PaymentMethodsStarted extends PaymentMethodsEvent {}
|
||||
|
||||
class AddPayemntCardPressed extends PaymentMethodsEvent {}
|
||||
|
||||
class PaymentMethodsDeleteCard extends PaymentMethodsEvent {
|
||||
final int cardId;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '../../domain/entities/payment_card.dart';
|
||||
import '../../domain/entities/subscription_period.dart';
|
||||
|
||||
abstract class SubscriptionDetailsEvent {}
|
||||
@@ -17,4 +18,10 @@ class ToggleAgreementEvent extends SubscriptionDetailsEvent {
|
||||
ToggleAgreementEvent(this.value);
|
||||
}
|
||||
|
||||
class ActivateSubscriptionPressed extends SubscriptionDetailsEvent {}
|
||||
class ActivateSubscriptionPressed extends SubscriptionDetailsEvent {}
|
||||
|
||||
class SelectCardEvent extends SubscriptionDetailsEvent {
|
||||
final PaymentCard card;
|
||||
|
||||
SelectCardEvent(this.card);
|
||||
}
|
||||
@@ -20,4 +20,6 @@ class SelectCard extends TopUpEvent {
|
||||
class ToggleAgreement extends TopUpEvent {
|
||||
final bool value;
|
||||
ToggleAgreement(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
class TopUpButtonPressed extends TopUpEvent {}
|
||||
@@ -13,6 +13,11 @@ class CodeChanged extends VerifyCodeEvent {
|
||||
CodeChanged(this.code);
|
||||
}
|
||||
|
||||
class TimerTicked extends VerifyCodeEvent {
|
||||
final int secondsLeft;
|
||||
TimerTicked(this.secondsLeft);
|
||||
}
|
||||
|
||||
class ResendCodePressed extends VerifyCodeEvent {}
|
||||
|
||||
class VerifyCodeSubmitted extends VerifyCodeEvent {}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:be_happy/presentation/screens/card_binding_result_screen.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:be_happy/core/app_colors.dart';
|
||||
import 'package:be_happy/di/service_locator.dart';
|
||||
@@ -43,6 +44,7 @@ import 'package:path/path.dart';
|
||||
|
||||
import '../../domain/entities/scooter.dart';
|
||||
import '../../domain/usecase/activate_subscription_usecase.dart';
|
||||
import '../../domain/usecase/add_payment_card_usecase.dart';
|
||||
import '../../domain/usecase/book_scooter_usecase.dart';
|
||||
import '../../domain/usecase/create_pin_usecase.dart';
|
||||
import '../../domain/usecase/get_address_by_point_usecase.dart';
|
||||
@@ -139,7 +141,8 @@ class AppRouter {
|
||||
createPinUseCase: getIt<CreatePinUseCase>(),
|
||||
verifyPinUsecase: getIt<VerifyPinUseCase>(),
|
||||
isPinSetUsecase: getIt<IsPinSetUsecase>(),
|
||||
)..add(PinScreenStarted()),
|
||||
)
|
||||
..add(PinScreenStarted()),
|
||||
child: const PinLoginScreen(),
|
||||
)
|
||||
),
|
||||
@@ -151,6 +154,26 @@ class AppRouter {
|
||||
path: '/license-agreement',
|
||||
builder: (context, state) => const LicenseAgreementScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/card-result',
|
||||
builder: (context, state) {
|
||||
final modeParam = state.uri.queryParameters['status'];
|
||||
|
||||
CardBindingStatus status = CardBindingStatus.success;
|
||||
String title = 'Карта успешно\nпривязана!';
|
||||
|
||||
if (modeParam == 'failed') {
|
||||
status = CardBindingStatus.failed;
|
||||
title = 'Извините, что-то\nпошло не так';
|
||||
}
|
||||
|
||||
return CardBindingResultScreen(
|
||||
status: status,
|
||||
title: title,
|
||||
cardImagePath: 'assets/icons/card_result_screen.png',
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
builder: (context, state) => const MapScreen(),
|
||||
@@ -237,7 +260,8 @@ class AppRouter {
|
||||
type: MaterialType.transparency,
|
||||
child: BlocProvider(
|
||||
create: (context) =>
|
||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>()),
|
||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>()),
|
||||
child: PaymentMethodSheet(),
|
||||
),
|
||||
),
|
||||
@@ -297,12 +321,14 @@ class AppRouter {
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'scooter/:id',
|
||||
builder: (context, state) =>
|
||||
BlocProvider(
|
||||
path: 'scooter/:id',
|
||||
builder: (context, state) {
|
||||
String address = state.extra as String;
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<ScooterDetailBloc>(),
|
||||
child: ScooterDetailScreen(),
|
||||
),
|
||||
child: ScooterDetailScreen(address: address),
|
||||
);
|
||||
}
|
||||
),
|
||||
GoRoute(
|
||||
path: 'order-photos/:orderId',
|
||||
@@ -371,6 +397,7 @@ class AppRouter {
|
||||
create: (context) =>
|
||||
SubscriptionDetailsBloc(
|
||||
getIt<GetSubscriptionByIdUsecase>(),
|
||||
getIt<GetPaymentCardsUsecase>(),
|
||||
getIt<ActivateSubscriptionUsecase>(),
|
||||
getIt<GetClientSubscriptionsUsecase>(),
|
||||
)
|
||||
@@ -394,24 +421,26 @@ class AppRouter {
|
||||
builder: (context, state) => const NewsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'qr-info',
|
||||
builder: (context, state) => const QRScanInfoScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'qr-scan',
|
||||
builder: (context, state) => const QrScanScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'qr-input',
|
||||
builder: (context, state) => BlocProvider(
|
||||
create: (context) =>
|
||||
ScooterCodeBloc(
|
||||
getScooterByTitleUsecase: getIt<GetScooterByTitleUsecase>(),
|
||||
),
|
||||
child: ScooterCodeInputScreen(),
|
||||
path: 'qr-info',
|
||||
builder: (context, state) => const QRScanInfoScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'qr-scan',
|
||||
builder: (context, state) => const QrScanScreen(),
|
||||
),
|
||||
),
|
||||
]
|
||||
GoRoute(
|
||||
path: 'qr-input',
|
||||
builder: (context, state) =>
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
ScooterCodeBloc(
|
||||
getScooterByTitleUsecase: getIt<
|
||||
GetScooterByTitleUsecase>(),
|
||||
),
|
||||
child: ScooterCodeInputScreen(),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
GoRoute(
|
||||
path: 'payment-methods',
|
||||
@@ -423,6 +452,7 @@ class AppRouter {
|
||||
getIt<RemovePaymentCardUsecase>(),
|
||||
getIt<SetMainPaymentCardUsecase>(),
|
||||
getIt<GetProfileUseCase>(),
|
||||
getIt<AddPaymentCardUsecase>(),
|
||||
)
|
||||
..add(PaymentMethodsStarted()),
|
||||
child: PaymentMethodsScreen(),
|
||||
@@ -454,9 +484,9 @@ class AppRouter {
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'order-history',
|
||||
builder: (context, state) => const OrderHistoryScreen(),
|
||||
routes: []
|
||||
path: 'order-history',
|
||||
builder: (context, state) => const OrderHistoryScreen(),
|
||||
routes: []
|
||||
),
|
||||
GoRoute(
|
||||
path: 'notifications',
|
||||
@@ -466,6 +496,24 @@ class AppRouter {
|
||||
),
|
||||
],
|
||||
|
||||
onException: (context, state, router) {
|
||||
final location = state.uri.toString();
|
||||
|
||||
if (location.startsWith('com.behappy.app://')) {
|
||||
|
||||
String normalizedPath = location
|
||||
.replaceFirst('com.behappy.app://', '/')
|
||||
.replaceAll('/?', '?')
|
||||
.replaceAll(' ', '');
|
||||
|
||||
print("Нормализованный путь диплинка: $normalizedPath");
|
||||
|
||||
router.go(normalizedPath);
|
||||
} else {
|
||||
print('GoRouter Exception: ${state.error}');
|
||||
}
|
||||
},
|
||||
|
||||
observers: [BotToastNavigatorObserver()],
|
||||
|
||||
redirect: (BuildContext context, GoRouterState state) {
|
||||
|
||||
165
lib/presentation/screens/card_binding_result_screen.dart
Normal file
165
lib/presentation/screens/card_binding_result_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:be_happy/domain/entities/payment_card.dart';
|
||||
import 'package:be_happy/domain/entities/subscription.dart';
|
||||
|
||||
import '../../domain/entities/subscription_period.dart';
|
||||
@@ -8,19 +9,24 @@ class DetailsLoading extends SubscriptionDetailsState {}
|
||||
|
||||
class DetailsError extends SubscriptionDetailsState {
|
||||
final String message;
|
||||
|
||||
DetailsError(this.message);
|
||||
}
|
||||
|
||||
class DetailsContentState extends SubscriptionDetailsState {
|
||||
final Subscription subscription;
|
||||
final SubscriptionPeriod selectedPeriod;
|
||||
final List<PaymentCard> cards;
|
||||
final PaymentCard selectedCard;
|
||||
final bool isAgreed;
|
||||
final bool isAlreadyPurchased; // ✅ Куплена ли эта подписка сейчас
|
||||
final bool isSuccess; // ✅ Сигнал для навигатора назад
|
||||
final bool isAlreadyPurchased;
|
||||
final bool isSuccess;
|
||||
|
||||
DetailsContentState({
|
||||
required this.subscription,
|
||||
required this.selectedPeriod,
|
||||
required this.selectedCard,
|
||||
this.cards = const [],
|
||||
this.isAgreed = false,
|
||||
this.isAlreadyPurchased = false,
|
||||
this.isSuccess = false,
|
||||
@@ -28,6 +34,7 @@ class DetailsContentState extends SubscriptionDetailsState {
|
||||
|
||||
DetailsContentState copyWith({
|
||||
SubscriptionPeriod? selectedPeriod,
|
||||
PaymentCard? selectedCard,
|
||||
bool? isAgreed,
|
||||
bool? isAlreadyPurchased,
|
||||
bool? isSuccess,
|
||||
@@ -35,9 +42,10 @@ class DetailsContentState extends SubscriptionDetailsState {
|
||||
return DetailsContentState(
|
||||
subscription: this.subscription,
|
||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||
selectedCard: selectedCard ?? this.selectedCard,
|
||||
isAgreed: isAgreed ?? this.isAgreed,
|
||||
isAlreadyPurchased: isAlreadyPurchased ?? this.isAlreadyPurchased,
|
||||
isSuccess: isSuccess ?? this.isSuccess,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,17 @@ class AddCardBloc extends Bloc<AddCardEvent, AddCardState> {
|
||||
}
|
||||
|
||||
final result = await addPaymentCardUsecase(
|
||||
cardNumber: state.cardNumber.replaceAll(' ', ''),
|
||||
/*cardNumber: state.cardNumber.replaceAll(' ', ''),
|
||||
expiryMonth: expiryParts[0],
|
||||
expiryYear: expiryParts[1],
|
||||
cardHolder: state.cardHolder.trim(),
|
||||
cvv: state.cvv,
|
||||
cvv: state.cvv,*/
|
||||
);
|
||||
|
||||
if (result is Success) {
|
||||
emit(state.copyWith(status: AddCardStatus.success));
|
||||
} else if (result is Failure) {
|
||||
final failure = result.failure;
|
||||
/*final failure = result.failure;
|
||||
|
||||
String errorMessage = 'Ошибка добавления карты';
|
||||
|
||||
@@ -72,12 +72,12 @@ class AddCardBloc extends Bloc<AddCardEvent, AddCardState> {
|
||||
errorMessage = 'Доступ заблокирован';
|
||||
} else if (failure is UnknownFailure) {
|
||||
errorMessage = failure.message ?? 'Неизвестная ошибка';
|
||||
}
|
||||
}*/
|
||||
|
||||
emit(state.copyWith(
|
||||
/* emit(state.copyWith(
|
||||
status: AddCardStatus.failure,
|
||||
errorMessage: errorMessage,
|
||||
));
|
||||
));*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -264,8 +264,9 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
||||
FutureOr<void> _onLogoutPressed(
|
||||
LogoutPressed event,
|
||||
Emitter<ScooterState> emit,
|
||||
) {
|
||||
logoutUseCase();
|
||||
) async {
|
||||
await logoutUseCase();
|
||||
stopNotificationStream();
|
||||
splashBloc.add(AuthCheckRequested());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../core/result.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 '../event/payment_method_sheet_event.dart';
|
||||
import '../state/payment_method_sheet_state.dart';
|
||||
|
||||
class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethodSheetState> {
|
||||
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
|
||||
final SetMainPaymentCardUsecase _setMainPaymentCardUsecase;
|
||||
|
||||
PaymentMethodSheetBloc(this._getPaymentCardsUsecase)
|
||||
PaymentMethodSheetBloc(this._getPaymentCardsUsecase, this._setMainPaymentCardUsecase)
|
||||
: super(PaymentMethodSheetState(status: PaymentMethodSheetStatus.initial)) {
|
||||
on<PaymentMethodSheetStarted>(_onStarted);
|
||||
}
|
||||
@@ -41,4 +43,53 @@ class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethod
|
||||
));
|
||||
}
|
||||
}
|
||||
Future<void> _onSelectCard(
|
||||
SelectPaymentCardRequested event,
|
||||
Emitter<PaymentMethodSheetState> emit,
|
||||
) async {
|
||||
// Включаем локальный лоадер на шторке, либо просто ждем ответа
|
||||
emit(state.copyWith(status: PaymentMethodSheetStatus.loading));
|
||||
|
||||
final result = await _setMainPaymentCardUsecase(event.card.id);
|
||||
|
||||
if (result is Success<void>) {
|
||||
final cardsResult = await _getPaymentCardsUsecase();
|
||||
if (cardsResult is Success<List<PaymentCard>>) {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodSheetStatus.success,
|
||||
cards: cardsResult.data ?? [],
|
||||
));
|
||||
} else {
|
||||
emit(state.copyWith(status: PaymentMethodSheetStatus.failure, errorMessage: 'Ошибка обновления списка карт'));
|
||||
}
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodSheetStatus.failure,
|
||||
errorMessage: 'Не удалось установить карту основной',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Выносим загрузку в отдельный метод для переиспользования
|
||||
Future<void> _loadCards(Emitter<PaymentMethodSheetState> emit) async {
|
||||
try {
|
||||
final result = await _getPaymentCardsUsecase();
|
||||
if (result is Success<List<PaymentCard>>) {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodSheetStatus.success,
|
||||
cards: result.data ?? [],
|
||||
));
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodSheetStatus.failure,
|
||||
errorMessage: 'Failed to load payment cards',
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodSheetStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:be_happy/domain/usecase/add_payment_card_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_profile_usecase.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../core/result.dart';
|
||||
import '../../core/failures.dart';
|
||||
@@ -15,16 +19,19 @@ class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState>
|
||||
final RemovePaymentCardUsecase _removePaymentCardUsecase;
|
||||
final SetMainPaymentCardUsecase _setMainPaymentCardUsecase;
|
||||
final GetProfileUseCase _getProfileUseCase;
|
||||
final AddPaymentCardUsecase _addPaymentCardUsecase;
|
||||
|
||||
PaymentMethodsBloc(
|
||||
this._getPaymentCardsUsecase,
|
||||
this._removePaymentCardUsecase,
|
||||
this._setMainPaymentCardUsecase,
|
||||
this._getProfileUseCase,
|
||||
this._addPaymentCardUsecase,
|
||||
) : super(PaymentMethodsState(status: PaymentMethodsStatus.initial)) {
|
||||
on<PaymentMethodsStarted>(_onStarted);
|
||||
on<PaymentMethodsDeleteCard>(_onDeleteCard);
|
||||
on<PaymentMethodsSetMainCard>(_onSetMainCard);
|
||||
on<AddPayemntCardPressed>(_onAddPayemntCardPressed);
|
||||
}
|
||||
|
||||
Future<void> _onStarted(
|
||||
@@ -136,4 +143,54 @@ class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState>
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onAddPayemntCardPressed(
|
||||
AddPayemntCardPressed event,
|
||||
Emitter<PaymentMethodsState> emit
|
||||
) async {
|
||||
emit(state.copyWith(status: PaymentMethodsStatus.loading));
|
||||
|
||||
try {
|
||||
final result = await _addPaymentCardUsecase();
|
||||
|
||||
if (result is Success<Map<String, dynamic>>) {
|
||||
final formUrlString = result.data?['formUrl'] as String?;
|
||||
|
||||
if (formUrlString != null && formUrlString.isNotEmpty) {
|
||||
final url = Uri.parse(formUrlString);
|
||||
|
||||
if (await canLaunchUrl(url)) {
|
||||
launchUrl(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
emit(state.copyWith(status: PaymentMethodsStatus.success));
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodsStatus.failure,
|
||||
errorMessage: 'Не удалось открыть ссылку для оплаты',
|
||||
));
|
||||
}
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodsStatus.failure,
|
||||
errorMessage: 'Ссылка для привязки карты не получена от сервера',
|
||||
));
|
||||
}
|
||||
|
||||
} else if (result is Failure) {
|
||||
String errorMessage = 'Ошибка при добавлении карты';
|
||||
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodsStatus.failure,
|
||||
errorMessage: errorMessage,
|
||||
));
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: PaymentMethodsStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:be_happy/core/result.dart';
|
||||
import 'package:be_happy/domain/entities/payment_card.dart';
|
||||
import 'package:be_happy/domain/entities/subscription.dart';
|
||||
import 'package:be_happy/domain/usecase/activate_subscription_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_payment_cards_usecase.dart';
|
||||
import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -12,27 +14,35 @@ import '../state/susbcription_details_state.dart';
|
||||
class SubscriptionDetailsBloc
|
||||
extends Bloc<SubscriptionDetailsEvent, SubscriptionDetailsState> {
|
||||
final GetSubscriptionByIdUsecase getSubscriptionByIdUsecase;
|
||||
final GetPaymentCardsUsecase getPaymentCardsUsecase;
|
||||
final ActivateSubscriptionUsecase activateSubscriptionUsecase;
|
||||
final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase;
|
||||
|
||||
SubscriptionDetailsBloc(
|
||||
this.getSubscriptionByIdUsecase,
|
||||
this.getPaymentCardsUsecase,
|
||||
this.activateSubscriptionUsecase,
|
||||
this.getClientSubscriptionsUsecase,
|
||||
) : super(DetailsLoading()) {
|
||||
on<LoadDetailsEvent>((event, emit) async {
|
||||
emit(DetailsLoading());
|
||||
try {
|
||||
final result = await getSubscriptionByIdUsecase(event.subscriptionId);
|
||||
final cardsResult = await getPaymentCardsUsecase();
|
||||
|
||||
final subsResult = await getSubscriptionByIdUsecase(
|
||||
event.subscriptionId,
|
||||
);
|
||||
|
||||
final clientSubsResult = await getClientSubscriptionsUsecase();
|
||||
bool isPurchased = false;
|
||||
|
||||
switch (clientSubsResult) {
|
||||
case Success<List<ClientSubscription>>():
|
||||
isPurchased = clientSubsResult.data?.any(
|
||||
isPurchased =
|
||||
clientSubsResult.data?.any(
|
||||
(element) => element.subscriptionId == event.subscriptionId,
|
||||
) ?? false;
|
||||
) ??
|
||||
false;
|
||||
break;
|
||||
|
||||
case Failure<List<ClientSubscription>>():
|
||||
@@ -40,16 +50,30 @@ class SubscriptionDetailsBloc
|
||||
break;
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
switch (subsResult) {
|
||||
case Success<Subscription>():
|
||||
final sub = result.data;
|
||||
final sub = subsResult.data;
|
||||
if (sub == null) return;
|
||||
|
||||
List<PaymentCard> loadedCards = [];
|
||||
PaymentCard? defaultCard;
|
||||
|
||||
if (cardsResult is Success<List<PaymentCard>>) {
|
||||
loadedCards = cardsResult.data ?? [];
|
||||
if (loadedCards.isNotEmpty) {
|
||||
defaultCard = loadedCards.firstWhere(
|
||||
(element) => element.isMain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
DetailsContentState(
|
||||
subscription: sub,
|
||||
selectedPeriod: sub.options.first,
|
||||
isAlreadyPurchased: isPurchased,
|
||||
cards: loadedCards,
|
||||
selectedCard: defaultCard ?? loadedCards.first,
|
||||
),
|
||||
);
|
||||
break;
|
||||
@@ -70,6 +94,12 @@ class SubscriptionDetailsBloc
|
||||
}
|
||||
});
|
||||
|
||||
on<SelectCardEvent>((event, emit) {
|
||||
if (state is DetailsContentState) {
|
||||
emit((state as DetailsContentState).copyWith(selectedCard: event.card));
|
||||
}
|
||||
});
|
||||
|
||||
on<ToggleAgreementEvent>((event, emit) {
|
||||
if (state is DetailsContentState) {
|
||||
emit((state as DetailsContentState).copyWith(isAgreed: event.value));
|
||||
@@ -79,16 +109,18 @@ class SubscriptionDetailsBloc
|
||||
on<ActivateSubscriptionPressed>((event, emit) async {
|
||||
switch (state) {
|
||||
case DetailsContentState contentState:
|
||||
if (!contentState.isAgreed) {
|
||||
if (!contentState.isAgreed || contentState.selectedCard == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await activateSubscriptionUsecase(contentState.selectedPeriod.id);
|
||||
final result = await activateSubscriptionUsecase(
|
||||
contentState.selectedPeriod.id,
|
||||
contentState.selectedCard.id,
|
||||
);
|
||||
|
||||
if (result is Success) {
|
||||
emit(contentState.copyWith(isSuccess: true));
|
||||
} else {
|
||||
}
|
||||
} else {}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -22,7 +22,6 @@ class TopUpBloc extends Bloc<TopUpEvent, TopUpState> {
|
||||
on<LoadTopUpData>((event, emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
// Запускаем оба запроса параллельно
|
||||
final results = await Future.wait([
|
||||
getCertificatesUsecase(),
|
||||
getUserCards(),
|
||||
@@ -77,5 +76,21 @@ class TopUpBloc extends Bloc<TopUpEvent, TopUpState> {
|
||||
on<ToggleAgreement>(
|
||||
(event, emit) => emit(state.copyWith(isAgreed: event.value)),
|
||||
);
|
||||
on<TopUpButtonPressed>((event, emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
if (state.isAgreed) {
|
||||
final result = await purchaseCertificateUsecase(
|
||||
certificateId: state.selectedTariff!.id,
|
||||
cardId:state.selectedCard!.id,
|
||||
);
|
||||
|
||||
if (result is Success) {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,26 @@ import '../../domain/usecase/verify_code_usecase.dart';
|
||||
import '../event/verify_code_event.dart';
|
||||
import '../state/verify_code_state.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:be_happy/core/failures.dart';
|
||||
import 'package:be_happy/core/result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../domain/usecase/verify_code_usecase.dart';
|
||||
import '../../domain/usecase/login_usecase.dart'; // 🔹 Импортируем LoginUseCase
|
||||
import '../event/verify_code_event.dart';
|
||||
import '../state/verify_code_state.dart';
|
||||
|
||||
class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
||||
final VerifyCodeUseCase _verifyCodeUseCase;
|
||||
final LoginUseCase _loginUseCase; // 🔹 Добавляем поле для юзкейса логина
|
||||
Timer? _timer;
|
||||
|
||||
VerifyCodeBloc(this._verifyCodeUseCase)
|
||||
// 🔹 Обновляем конструктор
|
||||
VerifyCodeBloc(this._verifyCodeUseCase, this._loginUseCase)
|
||||
: super(VerifyCodeState.initial()) {
|
||||
|
||||
on<VerifyCodeStarted>((event, emit) {
|
||||
emit(state.copyWith(
|
||||
phoneNumber: event.phoneNumber,
|
||||
@@ -24,56 +38,89 @@ class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
||||
});
|
||||
|
||||
on<CodeChanged>((event, emit) {
|
||||
emit(state.copyWith(code: event.code));
|
||||
emit(state.copyWith(
|
||||
code: event.code,
|
||||
error: null,
|
||||
));
|
||||
});
|
||||
|
||||
on<ResendCodePressed>((event, emit) {
|
||||
if (state.secondsLeft == 0) {
|
||||
emit(state.copyWith(secondsLeft: 60));
|
||||
_startTimer();
|
||||
on<ResendCodePressed>((event, emit) async {
|
||||
if (state.secondsLeft == 0 && !state.isSubmitting) {
|
||||
emit(state.copyWith(isSubmitting: true, error: null));
|
||||
|
||||
try {
|
||||
final newTempToken = await _loginUseCase.execute("+${state.phoneNumber}");
|
||||
|
||||
emit(state.copyWith(
|
||||
isSubmitting: false,
|
||||
tempToken: newTempToken,
|
||||
secondsLeft: 60,
|
||||
code: '',
|
||||
error: null,
|
||||
isBlocked: false,
|
||||
));
|
||||
|
||||
_startTimer();
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
isSubmitting: false,
|
||||
error: 'Не удалось повторно отправить код. Попробуйте позже.',
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on<TimerTicked>((event, emit) {
|
||||
emit(state.copyWith(secondsLeft: event.secondsLeft));
|
||||
});
|
||||
|
||||
on<VerifyCodeSubmitted>(_onVerifyCodeSubmitted);
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer?.cancel();
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (state.secondsLeft <= 1) {
|
||||
final currentSeconds = state.secondsLeft;
|
||||
if (currentSeconds <= 1) {
|
||||
timer.cancel();
|
||||
emit(state.copyWith(secondsLeft: 0));
|
||||
add(TimerTicked(0));
|
||||
} else {
|
||||
emit(state.copyWith(secondsLeft: state.secondsLeft - 1));
|
||||
add(TimerTicked(currentSeconds - 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onVerifyCodeSubmitted(VerifyCodeSubmitted event,
|
||||
Emitter<VerifyCodeState> emit,) async {
|
||||
if (state.isSubmitting || state.code.length != 6) return;
|
||||
if (state.isSubmitting || state.code.length != 6 || state.secondsLeft == 0) return;
|
||||
|
||||
emit(state.copyWith(isSubmitting: true, error: null));
|
||||
|
||||
// 🔹 Здесь автоматически подставится обновленный из стейта tempToken
|
||||
final result = await _verifyCodeUseCase.execute(state.code, state.tempToken);
|
||||
|
||||
final newState = switch (result) {
|
||||
Success() => state.copyWith(
|
||||
isSubmitting: false,
|
||||
isSuccess: true,
|
||||
),
|
||||
Success() => (() {
|
||||
_timer?.cancel();
|
||||
return state.copyWith(
|
||||
isSubmitting: false,
|
||||
isSuccess: true,
|
||||
);
|
||||
})(),
|
||||
Failure(failure: final f) => switch (f) {
|
||||
AuthFailure(attemptsLeft: final count) => state.copyWith(
|
||||
isSubmitting: false,
|
||||
attemptsLeft: count,
|
||||
error: 'Неверный код. Осталось попыток: $count',
|
||||
),
|
||||
AuthBlockFailure() => state.copyWith(
|
||||
isSubmitting: false,
|
||||
isBlocked: true,
|
||||
error: 'Вы заблокированы за слишком частые попытки',
|
||||
),
|
||||
AuthBlockFailure() => (() {
|
||||
_timer?.cancel();
|
||||
return state.copyWith(
|
||||
isSubmitting: false,
|
||||
isBlocked: true,
|
||||
error: 'Вы заблокированы за слишком частые попытки',
|
||||
);
|
||||
})(),
|
||||
_ => state.copyWith(
|
||||
isSubmitting: false,
|
||||
error: 'Произошла ошибка. Попробуйте позже',
|
||||
@@ -89,4 +136,4 @@ class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
||||
_timer?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
pubspec.lock
38
pubspec.lock
@@ -61,10 +61,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -524,26 +524,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -572,26 +572,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -817,10 +817,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -905,10 +905,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -974,5 +974,5 @@ packages:
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
sdks:
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
|
||||
@@ -2,7 +2,7 @@ name: be_happy
|
||||
description: ""
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.0+12
|
||||
version: 1.0.0+13
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
Reference in New Issue
Block a user