261 lines
7.9 KiB
Dart
261 lines
7.9 KiB
Dart
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../../repository/all_coupons_repository.dart';
|
|
import '../../repository/checkout_repository.dart';
|
|
import 'checkout_event.dart';
|
|
import 'checkout_state.dart';
|
|
|
|
class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
|
|
final AllCouponsRepository couponsRepository;
|
|
final CheckoutRepository checkoutRepository;
|
|
|
|
CheckoutBloc({
|
|
required this.couponsRepository,
|
|
required this.checkoutRepository,
|
|
}) : super(CheckoutInitialState()) {
|
|
on<FetchCheckoutCouponsEvent>(_onFetchCheckoutCoupons);
|
|
on<ApplyCouponEvent>(_onApplyCoupon);
|
|
on<RemoveCouponEvent>(_onRemoveCoupon);
|
|
on<ApplyCouponToBackendEvent>(_onApplyCouponToBackend); // 🆕 NEW
|
|
on<InitiatePaymentEvent>(_onInitiatePayment); // 🆕 NEW
|
|
on<ConfirmPaymentEvent>(_onConfirmPayment); // 🆕 NEW
|
|
}
|
|
|
|
Future<void> _onFetchCheckoutCoupons(
|
|
FetchCheckoutCouponsEvent event,
|
|
Emitter<CheckoutState> emit,
|
|
) async {
|
|
emit(CheckoutCouponsLoadingState());
|
|
try {
|
|
final coupons = await couponsRepository.fetchAllCoupons();
|
|
emit(CheckoutCouponsLoadedState(coupons: coupons));
|
|
} catch (e) {
|
|
emit(CheckoutCouponsErrorState(error: e.toString()));
|
|
}
|
|
}
|
|
|
|
void _onApplyCoupon(
|
|
ApplyCouponEvent event,
|
|
Emitter<CheckoutState> emit,
|
|
) {
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
emit(currentState.copyWith(appliedCoupon: event.coupon));
|
|
}
|
|
}
|
|
|
|
Future<void> _onRemoveCoupon(
|
|
RemoveCouponEvent event,
|
|
Emitter<CheckoutState> emit,
|
|
) async {
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
|
|
// Show loading
|
|
emit(currentState.copyWith(isApplyingCoupon: true, couponError: null));
|
|
|
|
try {
|
|
// Call API with empty coupon code
|
|
await checkoutRepository.applyCoupon(
|
|
bookingId: event.bookingId,
|
|
couponCode: '', // Empty string to remove coupon
|
|
);
|
|
|
|
// Clear applied coupon from state
|
|
emit(currentState.copyWith(
|
|
clearAppliedCoupon: true,
|
|
isApplyingCoupon: false,
|
|
couponError: null,
|
|
));
|
|
} catch (e) {
|
|
emit(currentState.copyWith(
|
|
isApplyingCoupon: false,
|
|
couponError: e.toString(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 🆕 Apply Coupon to Backend
|
|
/// Calls the PUT /apply-coupon API
|
|
Future<void> _onApplyCouponToBackend(
|
|
ApplyCouponToBackendEvent event,
|
|
Emitter<CheckoutState> emit,
|
|
) async {
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
|
|
// Show loading
|
|
emit(currentState.copyWith(isApplyingCoupon: true, couponError: null));
|
|
|
|
try {
|
|
// Call API
|
|
final response = await checkoutRepository.applyCoupon(
|
|
bookingId: event.bookingId,
|
|
couponCode: event.couponCode,
|
|
);
|
|
|
|
// Find the coupon from the list
|
|
final appliedCoupon = currentState.coupons.firstWhere(
|
|
(c) => c.couponCode == event.couponCode,
|
|
orElse: () => currentState.coupons.first,
|
|
);
|
|
|
|
// Update state with applied coupon
|
|
emit(currentState.copyWith(
|
|
appliedCoupon: appliedCoupon,
|
|
isApplyingCoupon: false,
|
|
couponError: null,
|
|
));
|
|
|
|
// Success message will be handled in view
|
|
} catch (e) {
|
|
emit(currentState.copyWith(
|
|
isApplyingCoupon: false,
|
|
couponError: e.toString(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 🆕 Initiate Payment
|
|
/// Calls the /pay API to get clientSecret for Stripe
|
|
Future<void> _onInitiatePayment(
|
|
InitiatePaymentEvent event,
|
|
Emitter<CheckoutState> emit,
|
|
) async {
|
|
// Show loading state
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
emit(currentState.copyWith(
|
|
isInitiatingPayment: true,
|
|
paymentError: null,
|
|
clientSecret: null,
|
|
));
|
|
} else {
|
|
emit(CheckoutPaymentInitiatingState());
|
|
}
|
|
|
|
try {
|
|
// Call the /pay API
|
|
final response = await checkoutRepository.initiatePayment(
|
|
bookingId: event.bookingId,
|
|
);
|
|
|
|
// Extract clientSecret and bookingId from response
|
|
final clientSecret = response['clientSecret'] as String?;
|
|
final bookingId = response['bookingId'] as int?;
|
|
|
|
// Validate response
|
|
if (clientSecret == null || clientSecret.isEmpty) {
|
|
emit(CheckoutPaymentInitiationErrorState(
|
|
error: 'Payment initialization failed - no client secret received from server',
|
|
));
|
|
return;
|
|
}
|
|
|
|
if (bookingId == null) {
|
|
emit(CheckoutPaymentInitiationErrorState(
|
|
error: 'Payment initialization failed - no booking ID received from server',
|
|
));
|
|
return;
|
|
}
|
|
|
|
// Emit success state with clientSecret
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
emit(currentState.copyWith(
|
|
isInitiatingPayment: false,
|
|
clientSecret: clientSecret,
|
|
bookingId: bookingId,
|
|
paymentError: null,
|
|
));
|
|
} else {
|
|
emit(CheckoutPaymentInitiatedState(
|
|
clientSecret: clientSecret,
|
|
bookingId: bookingId,
|
|
));
|
|
}
|
|
} catch (e) {
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
emit(currentState.copyWith(
|
|
isInitiatingPayment: false,
|
|
paymentError: e.toString(),
|
|
));
|
|
} else {
|
|
emit(CheckoutPaymentInitiationErrorState(
|
|
error: e.toString(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 🆕 Confirm Payment
|
|
/// Called after Stripe payment succeeds or fails
|
|
/// Sends stripeStatus and paymentStatus to backend
|
|
Future<void> _onConfirmPayment(
|
|
ConfirmPaymentEvent event,
|
|
Emitter<CheckoutState> emit,
|
|
) async {
|
|
// 🔒 GUARD: Prevent duplicate confirmation calls
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
if (currentState.hasConfirmationBeenSent) {
|
|
print('⚠️ [CHECKOUT BLOC] Payment confirmation already sent. Ignoring duplicate call.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Show loading state
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
emit(currentState.copyWith(
|
|
isConfirmingPayment: true,
|
|
confirmationError: null,
|
|
isPaymentConfirmed: false,
|
|
hasConfirmationBeenSent: true, // 🔒 Mark as sent
|
|
));
|
|
} else {
|
|
emit(CheckoutPaymentConfirmingState());
|
|
}
|
|
|
|
try {
|
|
// Call the confirm-payment API
|
|
final response = await checkoutRepository.confirmPayment(
|
|
bookingId: event.bookingId,
|
|
stripeStatus: event.stripeStatus,
|
|
paymentStatus: event.paymentStatus,
|
|
);
|
|
|
|
// Emit success state with booking details
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
emit(currentState.copyWith(
|
|
isConfirmingPayment: false,
|
|
isPaymentConfirmed: true,
|
|
confirmationError: null,
|
|
bookingDetails: response,
|
|
clearClientSecret: true,
|
|
));
|
|
} else {
|
|
emit(CheckoutPaymentConfirmedState(
|
|
bookingDetails: response,
|
|
));
|
|
}
|
|
} catch (e) {
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
final currentState = state as CheckoutCouponsLoadedState;
|
|
emit(currentState.copyWith(
|
|
isConfirmingPayment: false,
|
|
isPaymentConfirmed: false,
|
|
confirmationError: e.toString(),
|
|
hasConfirmationBeenSent: false, // 🔓 Reset on error to allow retry
|
|
));
|
|
} else {
|
|
emit(CheckoutPaymentConfirmationErrorState(
|
|
error: e.toString(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
} |