636 lines
26 KiB
Dart
636 lines
26 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 '../../buy_a_pass/models/checkout_model.dart';
|
|
import '../../localPreference/local_preference.dart';
|
|
import '../widget/pass_purchase_details_bottomsheet.dart';
|
|
import '../repository/all_coupons_repository.dart';
|
|
import '../models/all_coupons_model.dart';
|
|
|
|
class CheckoutView extends StatefulWidget {
|
|
const CheckoutView({super.key});
|
|
|
|
@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(repository: AllCouponsRepository())
|
|
..add(FetchCheckoutCouponsEvent()),
|
|
child: _CheckoutContent(
|
|
checkoutData: checkoutData,
|
|
isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed,
|
|
onPurchaseDetailsChanged: (value) {
|
|
setState(() {
|
|
isPurchaseDetailsConfirmed = value;
|
|
});
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CheckoutContent extends StatelessWidget {
|
|
final CheckoutData checkoutData;
|
|
final bool isPurchaseDetailsConfirmed;
|
|
final Function(bool) onPurchaseDetailsChanged;
|
|
|
|
const _CheckoutContent({
|
|
required this.checkoutData,
|
|
required this.isPurchaseDetailsConfirmed,
|
|
required this.onPurchaseDetailsChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocBuilder<CheckoutBloc, CheckoutState>(
|
|
builder: (context, state) {
|
|
// ✅ Calculate pricing
|
|
double discountPercentage = 0.0;
|
|
AllCouponsModel? appliedCoupon;
|
|
|
|
if (state is CheckoutCouponsLoadedState && state.appliedCoupon != null) {
|
|
appliedCoupon = state.appliedCoupon;
|
|
discountPercentage = appliedCoupon!.discountPercent.toDouble();
|
|
}
|
|
|
|
final double subtotal = 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 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: 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: checkoutData.heroImage.isNotEmpty
|
|
? Image.network(
|
|
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: checkoutData.themeColor,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: _fallbackImage(),
|
|
),
|
|
|
|
SizedBox(width: 6.66.w),
|
|
|
|
// ✅ Pass Details
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// City Name
|
|
CustomText(
|
|
text: checkoutData.cityName,
|
|
weight: FontWeight.w500,
|
|
size: 16.sp,
|
|
),
|
|
SizedBox(height: 5.h),
|
|
|
|
// Validity (Days or Attractions)
|
|
CustomText(
|
|
text: 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 (checkoutData.adultCount > 0)
|
|
Row(
|
|
children: [
|
|
Image.asset(
|
|
'assets/icons/adult.png',
|
|
scale: 4,
|
|
),
|
|
SizedBox(width: 4.w),
|
|
CustomText(
|
|
text:
|
|
"${checkoutData.adultCount} adult${checkoutData.adultCount > 1 ? 's' : ''}",
|
|
color: const Color(0xFF8E8E8E),
|
|
size: 12.sp,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 5.h),
|
|
Row(
|
|
children: [
|
|
// Children
|
|
if (checkoutData.childCount > 0) ...[
|
|
Image.asset(
|
|
"assets/icons/kid.png",
|
|
scale: 4,
|
|
),
|
|
SizedBox(width: 4.w),
|
|
CustomText(
|
|
text:
|
|
"${checkoutData.childCount} Kid${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: checkoutData.themeColor,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
// ✅ Card Type Label (Vertical)
|
|
Container(
|
|
width: 35.w,
|
|
height: 140.h,
|
|
decoration: BoxDecoration(
|
|
color: checkoutData.themeColor,
|
|
borderRadius: BorderRadius.only(
|
|
bottomRight: Radius.circular(8.r),
|
|
topRight: Radius.circular(8.r),
|
|
),
|
|
),
|
|
child: RotatedBox(
|
|
quarterTurns: -1,
|
|
child: Center(
|
|
child: Text(
|
|
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) {
|
|
// Apply the selected coupon
|
|
context.read<CheckoutBloc>().add(
|
|
ApplyCouponEvent(
|
|
coupon: selectedCoupon),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
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());
|
|
} else if (state.coupons.isNotEmpty) {
|
|
context.read<CheckoutBloc>().add(
|
|
ApplyCouponEvent(
|
|
coupon: state.coupons[0]),
|
|
);
|
|
}
|
|
},
|
|
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:
|
|
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;
|
|
|
|
return CustomFilledButton(
|
|
onTap: () async {
|
|
if (isLoggedIn) {
|
|
if (isPurchaseDetailsConfirmed) {
|
|
// Navigate to Stripe payment directly
|
|
final paymentResult = await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const StripePaymentView(),
|
|
settings: RouteSettings(
|
|
arguments: {
|
|
'amount': finalTotal,
|
|
'currency': 'usd', // or your currency
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
// Handle payment result
|
|
if (paymentResult == true) {
|
|
// Payment successful
|
|
print("Payment successful!");
|
|
// Handle success - clear cart, show confirmation, etc.
|
|
}
|
|
} else {
|
|
// Show purchase details bottom sheet and wait for result
|
|
final result = await PassPurchaseBottomSheet.show(context);
|
|
|
|
// If user selected 'self', show purchase confirmation
|
|
if (result == 'self') {
|
|
onPurchaseDetailsChanged(true);
|
|
}
|
|
}
|
|
} else {
|
|
// 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
|
|
? (isPurchaseDetailsConfirmed
|
|
? "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],
|
|
),
|
|
);
|
|
}
|
|
} |