Files
CityCards_Customer_Flutter/lib/checkout/view/checkout_view.dart
2026-02-05 19:35:01 +05:30

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