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

@@ -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,
));
));*/
}
}

View File

@@ -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());
}

View File

@@ -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(),
));
}
}
}

View File

@@ -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(),
));
}
}}

View File

@@ -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;

View File

@@ -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));
}
}
});
}
}

View File

@@ -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();
}
}
}