817 lines
34 KiB
Dart
817 lines
34 KiB
Dart
import 'package:citycards_customer/checkout/bloc/checkOut/checkout_bloc.dart';
|
|
import 'package:citycards_customer/checkout/bloc/checkOut/checkout_event.dart';
|
|
import 'package:citycards_customer/checkout/bloc/checkOut/checkout_state.dart';
|
|
import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart';
|
|
import 'package:citycards_customer/login/view/login_email_bottomsheet.dart';
|
|
import 'package:citycards_customer/common_packages/app_bar.dart';
|
|
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
|
|
import 'package:citycards_customer/common_packages/custom_text.dart';
|
|
import 'package:citycards_customer/common_packages/custom_dashed_line.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import '../../StripePayment/view/stripe_payment.dart';
|
|
import '../../add_details/add_details_view.dart';
|
|
import '../../buy_a_pass/models/checkout_model.dart';
|
|
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
|
|
import '../../localPreference/local_preference.dart';
|
|
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
|
|
import '../../my_pass/blocs/myPasses/my_passes_event.dart';
|
|
import '../widget/pass_purchase_details_bottomsheet.dart';
|
|
import '../repository/all_coupons_repository.dart';
|
|
import '../repository/checkout_repository.dart';
|
|
import '../models/all_coupons_model.dart';
|
|
|
|
class CheckoutView extends StatefulWidget {
|
|
final int bookingId;
|
|
const CheckoutView({super.key, required this.bookingId});
|
|
|
|
@override
|
|
State<CheckoutView> createState() => _CheckoutViewState();
|
|
}
|
|
|
|
class _CheckoutViewState extends State<CheckoutView> {
|
|
bool isPurchaseDetailsConfirmed = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// ✅ Receive checkout data from navigation arguments
|
|
final arguments = ModalRoute.of(context)?.settings.arguments;
|
|
|
|
CheckoutData? checkoutData;
|
|
|
|
if (arguments is CheckoutData) {
|
|
checkoutData = arguments;
|
|
print("✅ CHECKOUT DATA RECEIVED!");
|
|
print(" City: ${checkoutData.cityName}");
|
|
print(" Adults: ${checkoutData.adultCount}");
|
|
print(" Children: ${checkoutData.childCount}");
|
|
print(" Total: \$${checkoutData.totalPrice}");
|
|
} else {
|
|
print("❌ NO CHECKOUT DATA - showing error screen");
|
|
}
|
|
|
|
// ✅ If no data passed, show error or default values
|
|
if (checkoutData == null) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 60.sp, color: Colors.red),
|
|
SizedBox(height: 16.h),
|
|
CustomText(
|
|
text: "No checkout data available",
|
|
size: 16.sp,
|
|
color: Colors.red,
|
|
),
|
|
SizedBox(height: 8.h),
|
|
CustomText(
|
|
text: "Arguments type: ${arguments.runtimeType}",
|
|
size: 12.sp,
|
|
color: Colors.grey,
|
|
),
|
|
SizedBox(height: 20.h),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text("Go Back"),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return BlocProvider(
|
|
create: (context) => CheckoutBloc(
|
|
couponsRepository: AllCouponsRepository(),
|
|
checkoutRepository: CheckoutRepository(),
|
|
)..add(FetchCheckoutCouponsEvent()),
|
|
child: _CheckoutContent(
|
|
checkoutData: checkoutData,
|
|
bookingId: widget.bookingId,
|
|
isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed,
|
|
onPurchaseDetailsChanged: (value) {
|
|
setState(() {
|
|
isPurchaseDetailsConfirmed = value;
|
|
});
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CheckoutContent extends StatefulWidget {
|
|
final CheckoutData checkoutData;
|
|
final int bookingId;
|
|
final bool isPurchaseDetailsConfirmed;
|
|
final Function(bool) onPurchaseDetailsChanged;
|
|
|
|
const _CheckoutContent({
|
|
required this.checkoutData,
|
|
required this.bookingId,
|
|
required this.isPurchaseDetailsConfirmed,
|
|
required this.onPurchaseDetailsChanged,
|
|
});
|
|
|
|
@override
|
|
State<_CheckoutContent> createState() => _CheckoutContentState();
|
|
}
|
|
|
|
class _CheckoutContentState extends State<_CheckoutContent> {
|
|
bool _hasHandledPaymentResult = false;
|
|
/// 🆕 Handle payment flow with client secret
|
|
/// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION
|
|
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async {
|
|
final paymentSuccess = await StripePaymentScreen.showAsBottomSheet(
|
|
context: context,
|
|
clientSecret: clientSecret,
|
|
amount: finalTotal,
|
|
currencySymbol: '\$',
|
|
title: 'Complete Payment',
|
|
loadingMessage: 'Processing your pass payment...',
|
|
successMessage: 'Payment Successful!\nYour pass is ready.',
|
|
failureMessage: 'Payment Failed',
|
|
primaryColor: const Color(0xFFF95F62),
|
|
heightRatio: 0.5,
|
|
isDismissible: false,
|
|
enableDrag: false,
|
|
onPaymentSuccess: () {
|
|
context.read<CheckoutBloc>().add(
|
|
ConfirmPaymentEvent(
|
|
bookingId: bookingId,
|
|
stripeStatus: 'succeeded',
|
|
paymentStatus: 'success',
|
|
),
|
|
);
|
|
},
|
|
onPaymentFailure: (error) {
|
|
context.read<CheckoutBloc>().add(
|
|
ConfirmPaymentEvent(
|
|
bookingId: bookingId,
|
|
stripeStatus: 'failed',
|
|
paymentStatus: 'failed',
|
|
),
|
|
);
|
|
},
|
|
onPaymentCancelled: () {
|
|
Navigator.pop(context);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Payment cancelled'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
// ✅ USE paymentSuccess HERE
|
|
if (paymentSuccess == true && context.mounted) {
|
|
// Wait a moment for backend confirmation
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
// Navigate to home after successful payment
|
|
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
|
|
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Payment confirmed successfully!'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocConsumer<CheckoutBloc, CheckoutState>(
|
|
listener: (context, state) {
|
|
// 🆕 Listen for payment initiation success
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
// 🔒 CHECK: Prevent duplicate payment flow initiation
|
|
if (state.clientSecret != null &&
|
|
state.clientSecret!.isNotEmpty &&
|
|
!_hasHandledPaymentResult) { // 🔒 Only proceed if not already handled
|
|
|
|
// 🔒 MARK: Set flag immediately to prevent re-entry
|
|
_hasHandledPaymentResult = true;
|
|
|
|
// ✅ Calculate finalTotal here
|
|
double discountPercentage = 0.0;
|
|
if (state.appliedCoupon != null) {
|
|
discountPercentage = state.appliedCoupon!.discountPercent.toDouble();
|
|
}
|
|
|
|
final num subtotal = widget.checkoutData.totalPrice; // Changed to widget.
|
|
final double discountAmount = subtotal * (discountPercentage / 100);
|
|
final double totalBeforeTax = subtotal - discountAmount;
|
|
final double taxAmount = 2;
|
|
final double finalTotal = totalBeforeTax + taxAmount;
|
|
|
|
// ✅ Trigger payment flow with finalTotal
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_handlePaymentFlow(
|
|
context,
|
|
state.clientSecret!,
|
|
state.bookingId ?? widget.bookingId,
|
|
finalTotal, // ✅ Pass the calculated finalTotal
|
|
);
|
|
});
|
|
}
|
|
|
|
// 🆕 Listen for payment confirmation success
|
|
if (state.isPaymentConfirmed) {
|
|
// Navigate to success page or back
|
|
Future.delayed(const Duration(seconds: 2), () {
|
|
if (context.mounted) {
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 🆕 Listen for payment confirmation error
|
|
if (state.confirmationError != null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.confirmationError!),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 🆕 Handle payment initiation error
|
|
if (state is CheckoutPaymentInitiationErrorState) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.error),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🆕 Handle payment confirmation error
|
|
if (state is CheckoutPaymentConfirmationErrorState) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.error),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
// ✅ Calculate pricing
|
|
double discountPercentage = 0.0;
|
|
AllCouponsModel? appliedCoupon;
|
|
bool isInitiatingPayment = false;
|
|
bool isConfirmingPayment = false;
|
|
|
|
if (state is CheckoutCouponsLoadedState) {
|
|
appliedCoupon = state.appliedCoupon;
|
|
if (appliedCoupon != null) {
|
|
discountPercentage = appliedCoupon.discountPercent.toDouble();
|
|
}
|
|
isInitiatingPayment = state.isInitiatingPayment;
|
|
isConfirmingPayment = state.isConfirmingPayment;
|
|
}
|
|
|
|
final num subtotal = widget.checkoutData.totalPrice;
|
|
final double discountAmount = subtotal * (discountPercentage / 100);
|
|
// final double taxRate = 0.05; // 5% tax
|
|
final double totalBeforeTax = subtotal - discountAmount;
|
|
// final double taxAmount = totalBeforeTax * taxRate;
|
|
final double taxAmount = 2;
|
|
final double finalTotal = totalBeforeTax + taxAmount;
|
|
|
|
return Scaffold(
|
|
resizeToAvoidBottomInset: true,
|
|
backgroundColor: Colors.white,
|
|
body: SafeArea(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
|
child: Column(
|
|
children: [
|
|
// ✅ App Bar
|
|
CommonAppBar(
|
|
isWhiteLogo: false,
|
|
isProfilePage: false,
|
|
showCart: false,
|
|
showDivider: true,
|
|
),
|
|
|
|
// ✅ Back Button & Title
|
|
Row(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () => Navigator.pop(context),
|
|
child: const Icon(Icons.arrow_back),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
CustomText(text: "Checkout", size: 12.sp),
|
|
],
|
|
),
|
|
|
|
SizedBox(height: 22.h),
|
|
|
|
// ✅ PASS CARD SECTION
|
|
Container(
|
|
height: 140.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(
|
|
color: widget.checkoutData.themeColor.withOpacity(0.2),
|
|
),
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
// ✅ Hero Image
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(8.r),
|
|
bottomLeft: Radius.circular(8.r),
|
|
),
|
|
child: widget.checkoutData.heroImage.isNotEmpty
|
|
? Image.network(
|
|
widget.checkoutData.heroImage,
|
|
width: 105.w,
|
|
height: 140.h,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return _fallbackImage();
|
|
},
|
|
loadingBuilder:
|
|
(context, child, loadingProgress) {
|
|
if (loadingProgress == null) return child;
|
|
return Container(
|
|
width: 105.w,
|
|
height: 140.h,
|
|
color: Colors.grey[200],
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 24.w,
|
|
height: 24.w,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: widget.checkoutData.themeColor,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: _fallbackImage(),
|
|
),
|
|
|
|
SizedBox(width: 6.66.w),
|
|
|
|
// ✅ Pass Details
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// City Name
|
|
CustomText(
|
|
text: widget.checkoutData.cityName,
|
|
weight: FontWeight.w500,
|
|
size: 16.sp,
|
|
),
|
|
SizedBox(height: 5.h),
|
|
|
|
// Validity (Days or Attractions)
|
|
CustomText(
|
|
text: widget.checkoutData.validityLabel,
|
|
color: const Color(0xFF8E8E8E),
|
|
size: 12.sp,
|
|
),
|
|
SizedBox(height: 5.h),
|
|
|
|
// Adults
|
|
SizedBox(
|
|
width: MediaQuery.of(context).size.width * .5,
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// Adults
|
|
if (widget.checkoutData.adultCount > 0)
|
|
Row(
|
|
children: [
|
|
Image.asset(
|
|
'assets/icons/adult.png',
|
|
scale: 4,
|
|
),
|
|
SizedBox(width: 4.w),
|
|
CustomText(
|
|
text:
|
|
"${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
|
|
color: const Color(0xFF8E8E8E),
|
|
size: 12.sp,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 5.h),
|
|
Row(
|
|
children: [
|
|
// Children
|
|
if (widget.checkoutData.childCount > 0) ...[
|
|
Image.asset(
|
|
"assets/icons/kid.png",
|
|
scale: 4,
|
|
),
|
|
SizedBox(width: 4.w),
|
|
CustomText(
|
|
text:
|
|
"${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
|
|
color: const Color(0xFF8E8E8E),
|
|
size: 12.sp,
|
|
),
|
|
SizedBox(width: 53.w),
|
|
] else
|
|
SizedBox(width: 120.w),
|
|
|
|
// Total Price
|
|
CustomText(
|
|
text: "\$${subtotal.toStringAsFixed(2)}",
|
|
size: 24.sp,
|
|
weight: FontWeight.w500,
|
|
color: widget.checkoutData.themeColor,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
// ✅ Card Type Label (Vertical)
|
|
Container(
|
|
width: 35.w,
|
|
height: 140.h,
|
|
decoration: BoxDecoration(
|
|
color: widget.checkoutData.themeColor,
|
|
borderRadius: BorderRadius.only(
|
|
bottomRight: Radius.circular(8.r),
|
|
topRight: Radius.circular(8.r),
|
|
),
|
|
),
|
|
child: RotatedBox(
|
|
quarterTurns: -1,
|
|
child: Center(
|
|
child: Text(
|
|
widget.checkoutData.cardDisplayName,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 20.h),
|
|
|
|
// ✅ COUPON SECTION
|
|
Container(
|
|
width: double.infinity,
|
|
padding:
|
|
EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF95F62).withOpacity(0.06),
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
border: Border.all(
|
|
color: const Color(0xFFF95F62),
|
|
width: 0.3,
|
|
),
|
|
),
|
|
child: state is CheckoutCouponsLoadingState
|
|
? Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 16.w,
|
|
height: 16.w,
|
|
child: const CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Color(0xFFF95F62),
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
CustomText(
|
|
text: "Loading coupons...",
|
|
size: 12.sp,
|
|
color: Colors.grey,
|
|
),
|
|
],
|
|
)
|
|
: state is CheckoutCouponsErrorState
|
|
? CustomText(
|
|
text: "Error loading coupons",
|
|
size: 12.sp,
|
|
color: Colors.red,
|
|
)
|
|
: state is CheckoutCouponsLoadedState
|
|
? Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
/// LEFT CONTENT
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
CustomText(
|
|
text: appliedCoupon != null
|
|
? "Coupon Applied: ${appliedCoupon.couponCode}"
|
|
: state.coupons.isNotEmpty
|
|
? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}"
|
|
: "No coupons available",
|
|
color: const Color(0xFF262626),
|
|
size: 14.sp,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 7.h),
|
|
GestureDetector(
|
|
onTap: () {
|
|
// ✅ Updated: Pass callback to bottomsheet
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(12.r),
|
|
),
|
|
),
|
|
builder: (_) => AllCouponsBottomsheet(
|
|
onCouponSelected: (selectedCoupon) {
|
|
final coupon = selectedCoupon as AllCouponsModel;
|
|
// Apply the selected coupon
|
|
context.read<CheckoutBloc>().add(
|
|
ApplyCouponEvent(
|
|
coupon: selectedCoupon),
|
|
);
|
|
context.read<CheckoutBloc>().add(
|
|
ApplyCouponToBackendEvent(
|
|
bookingId: widget.bookingId,
|
|
couponCode: coupon.couponCode,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CustomText(
|
|
text: "View all coupons",
|
|
color: const Color(0xFFF95F62),
|
|
size: 12.sp,
|
|
),
|
|
SizedBox(width: 3.w),
|
|
const Icon(
|
|
Icons.arrow_right,
|
|
size: 18,
|
|
color: Color(0xFFF95F62),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
SizedBox(width: 12.w),
|
|
|
|
/// APPLY / REMOVE BUTTON
|
|
GestureDetector(
|
|
onTap: () {
|
|
if (appliedCoupon != null) {
|
|
context.read<CheckoutBloc>().add(
|
|
RemoveCouponEvent(bookingId: widget.bookingId),
|
|
);
|
|
} else if (state.coupons.isNotEmpty) {
|
|
// Apply coupon via backend API
|
|
context.read<CheckoutBloc>().add(
|
|
ApplyCouponToBackendEvent(
|
|
bookingId: widget.bookingId,
|
|
couponCode: state.coupons[0].couponCode,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 18.w,
|
|
vertical: 10.h,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: const Color(0xFFF95F62),
|
|
),
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
),
|
|
child: CustomText(
|
|
text: state.isApplyingCoupon
|
|
? "Applying..."
|
|
: (appliedCoupon != null ? "Remove" : "Apply"),
|
|
color: const Color(0xFFF95F62),
|
|
size: 14.sp,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: const SizedBox.shrink(),
|
|
),
|
|
|
|
SizedBox(height: 15.h),
|
|
|
|
// ✅ PRICING BREAKDOWN
|
|
DashedDivider(
|
|
color: const Color(0xFFACACAC),
|
|
thickness: 1.h,
|
|
dashLength: 4,
|
|
dashSpace: 4,
|
|
),
|
|
SizedBox(height: 10.h),
|
|
|
|
// Subtotal
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
CustomText(text: "Subtotal", size: 14.sp),
|
|
CustomText(
|
|
text: "\$${subtotal.toStringAsFixed(2)}",
|
|
size: 14.sp,
|
|
weight: FontWeight.w500,
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 14.h),
|
|
|
|
// Discount
|
|
if (discountPercentage > 0) ...[
|
|
Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
CustomText(text: "Discount", size: 14.sp),
|
|
CustomText(
|
|
text:
|
|
"-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)",
|
|
size: 14.sp,
|
|
weight: FontWeight.w500,
|
|
color: Colors.green,
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 14.h),
|
|
],
|
|
|
|
DashedDivider(
|
|
color: const Color(0xFFACACAC),
|
|
thickness: 1.h,
|
|
dashLength: 4,
|
|
dashSpace: 4,
|
|
),
|
|
SizedBox(height: 10.h),
|
|
|
|
// Total
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
CustomText(text: 'Total', size: 14.sp),
|
|
SizedBox(height: 4.h),
|
|
CustomText(
|
|
text:
|
|
"Including \$${taxAmount.toStringAsFixed(2)} in taxes",
|
|
size: 12.sp,
|
|
color: Colors.black.withOpacity(0.6),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
CustomText(
|
|
text: "\$${finalTotal.toStringAsFixed(2)}",
|
|
size: 24.sp,
|
|
weight: FontWeight.w500,
|
|
),
|
|
],
|
|
),
|
|
|
|
const Spacer(),
|
|
|
|
// ✅ CHECKOUT BUTTON
|
|
FutureBuilder<bool>(
|
|
future: LocalPreference.getLogin(),
|
|
builder: (context, snapshot) {
|
|
final isLoggedIn = snapshot.data ?? false;
|
|
final isDisabled = isInitiatingPayment || isConfirmingPayment;
|
|
|
|
return CustomFilledButton(
|
|
onTap: isDisabled
|
|
? () {} // Empty callback when disabled
|
|
: () async {
|
|
if (isLoggedIn) {
|
|
if (widget.isPurchaseDetailsConfirmed) {
|
|
// 🆕 Initiate payment flow
|
|
context.read<CheckoutBloc>().add(
|
|
InitiatePaymentEvent(
|
|
bookingId: widget.bookingId),
|
|
);
|
|
} else {
|
|
// Show purchase details bottom sheet
|
|
final result = await PassPurchaseBottomSheet.show(
|
|
context, bookingId: widget.bookingId);
|
|
|
|
// ✅ Handle 'Buy for Myself' - user submitted details
|
|
if (result == 'success') {
|
|
widget.onPurchaseDetailsChanged(true);
|
|
}
|
|
// ✅ Handle 'Gift the Pass' - navigate to AddDetailsView
|
|
else if (result == 'gift') {
|
|
final giftResult = await Navigator.of(context).push<String>(
|
|
MaterialPageRoute(
|
|
builder: (_) => AddDetailsView(bookingId: widget.bookingId),
|
|
),
|
|
);
|
|
|
|
// If gift details were successfully submitted, mark as confirmed
|
|
if (giftResult == 'success') {
|
|
widget.onPurchaseDetailsChanged(true);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Navigator.pop(context);
|
|
// Show login bottom sheet if not logged in
|
|
showModalBottomSheet(
|
|
backgroundColor: Colors.white,
|
|
context: context,
|
|
isScrollControlled: true,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(12.r),
|
|
),
|
|
),
|
|
builder: (_) => const LoginEmailBottomsheet(),
|
|
);
|
|
}
|
|
},
|
|
width: double.infinity,
|
|
label: isLoggedIn
|
|
? (widget.isPurchaseDetailsConfirmed
|
|
? (isInitiatingPayment || isConfirmingPayment
|
|
? "Processing..."
|
|
: "Pay \$${finalTotal.toStringAsFixed(2)}")
|
|
: "Checkout")
|
|
: "Login to Checkout",
|
|
);
|
|
},
|
|
),
|
|
SizedBox(height: 25.h),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ✅ Fallback image widget
|
|
Widget _fallbackImage() {
|
|
return Container(
|
|
width: 105.w,
|
|
height: 140.h,
|
|
color: Colors.grey[200],
|
|
child: Icon(
|
|
Icons.card_travel,
|
|
size: 40.sp,
|
|
color: Colors.grey[400],
|
|
),
|
|
);
|
|
}
|
|
} |