bug fixes

This commit is contained in:
2026-02-26 15:54:57 +05:30
parent 77aba2f1a0
commit 60486e737a
32 changed files with 1115 additions and 1013 deletions

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -42,12 +43,13 @@ class AttractionCard extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: imageUrl.isNotEmpty
? Image.network(
imageUrl,
? CachedNetworkImage(
imageUrl: imageUrl,
height: 94.h,
width: 94.w,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _imageFallback(),
placeholder: (context, url) => _imageFallback(),
errorWidget: (_, __, ___) => _imageFallback(),
)
: _imageFallback(),
),

View File

@@ -20,7 +20,18 @@ class BuyPassBloc extends Bloc<BuyPassEvent, BuyPassState> {
on<UpdateChildCount>(_onUpdateChildCount);
/// Handle update validity duration event
on<UpdateValidityDuration>(_onUpdateValidityDuration); // ✅ Added
on<UpdateValidityDuration>(_onUpdateValidityDuration);
on<AddToCartLoading>((event, emit) {
if (state is BuyPassLoaded) {
emit((state as BuyPassLoaded).copyWith(isAddingToCart: true));
}
});
on<AddToCartDone>((event, emit) {
if (state is BuyPassLoaded) {
emit((state as BuyPassLoaded).copyWith(isAddingToCart: false));
}
});// ✅ Added
}
/// Fetch buy pass data from repository

View File

@@ -30,3 +30,5 @@ class UpdateValidityDuration extends BuyPassEvent {
UpdateValidityDuration(this.duration);
}
class AddToCartLoading extends BuyPassEvent {}
class AddToCartDone extends BuyPassEvent {}

View File

@@ -14,15 +14,17 @@ class BuyPassLoaded extends BuyPassState {
final int selectedCardIndex;
final int adultCount;
final int childCount;
final int validityDuration; // ✅ Added
final int validityDuration;
final bool isAddingToCart;
BuyPassLoaded({
required this.data,
this.selectedCardIndex = 0,
this.adultCount = 1,
this.childCount = 1,
int? validityDuration, // ✅ Added as optional parameter
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber; // ✅ Initialize with minNumber
int? validityDuration,
this.isAddingToCart = false, // ✅ default false, NOT required
}) : validityDuration = validityDuration ?? data.cards[selectedCardIndex].minNumber;
/// Method to copy state with updated values
BuyPassLoaded copyWith({
@@ -30,14 +32,16 @@ class BuyPassLoaded extends BuyPassState {
int? selectedCardIndex,
int? adultCount,
int? childCount,
int? validityDuration, // ✅ Added
int? validityDuration,
bool? isAddingToCart,
}) {
return BuyPassLoaded(
data: data ?? this.data,
selectedCardIndex: selectedCardIndex ?? this.selectedCardIndex,
adultCount: adultCount ?? this.adultCount,
childCount: childCount ?? this.childCount,
validityDuration: validityDuration ?? this.validityDuration, // ✅ Added
validityDuration: validityDuration ?? this.validityDuration,
isAddingToCart: isAddingToCart ?? this.isAddingToCart,
);
}
@@ -47,7 +51,8 @@ class BuyPassLoaded extends BuyPassState {
/// Calculate total price
double get totalPrice {
final card = selectedCard;
return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) * validityDuration.toDouble(); // ✅ Multiply by validityDuration
return ((card.adultPrice * adultCount) + (card.childPrice * childCount)) *
validityDuration.toDouble();
}
}

View File

@@ -1,14 +1,17 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../localPreference/local_preference.dart';
import '../bloc/buy_pass_bloc.dart';
import '../bloc/buy_pass_event.dart';
import '../bloc/buy_pass_state.dart';
import '../models/checkout_model.dart';
import '../../checkout/view/checkout_view.dart';
import '../repository/buy_pass_repository.dart'; // ✅ Import repository
class PaymentCard extends StatelessWidget {
class PaymentCard extends StatefulWidget {
final String city;
final String heroImage;
final String cardType;
@@ -56,10 +59,16 @@ class PaymentCard extends StatelessWidget {
required this.cardXid, // ✅ NEW
});
@override
State<PaymentCard> createState() => _PaymentCardState();
}
class _PaymentCardState extends State<PaymentCard> {
bool _isLoading = false;
@override
Widget build(BuildContext context) {
final bool isUnlimitedCard = cardType == "unlimited_card";
final bool isSelectivePass = cardType == "selective_pass";
final bool isUnlimitedCard = widget.cardType == "unlimited_card";
final bool isSelectivePass = widget.cardType == "selective_pass";
return Padding(
padding: const EdgeInsets.all(12.0),
@@ -83,7 +92,7 @@ class PaymentCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomText(
text: city,
text: widget.city,
size: 20.sp,
weight: FontWeight.bold,
),
@@ -91,32 +100,32 @@ class PaymentCard extends StatelessWidget {
Container(
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
decoration: BoxDecoration(
color: themeColor.withValues(alpha: 0.3),
color: widget.themeColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20.r),
),
child: CustomText(
text: cardDisplayName,
text: widget.cardDisplayName,
size: 12.sp,
color: themeColor,
color: widget.themeColor,
weight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
_buildCounterRow("No. of Adults", adults, onAdultChanged, context, minValue: 1),
_buildCounterRow("No. of Adults", widget.adults, widget.onAdultChanged, context, minValue: 1),
SizedBox(height: 10.h),
_buildCounterRow("No. of Children", children, onChildChanged, context),
_buildCounterRow("No. of Children", widget.children, widget.onChildChanged, context),
SizedBox(height: 10.h),
if (isUnlimitedCard)
_buildDropdownRow(
label: "No. of Days",
value: selectedValue,
onChanged: onValidityChanged,
value: widget.selectedValue,
onChanged: widget.onValidityChanged,
)
else if (isSelectivePass)
_buildDropdownRow(
label: "No. of Attractions",
value: selectedValue,
onChanged: onValidityChanged,
value: widget.selectedValue,
onChanged: widget.onValidityChanged,
),
Divider(height: 30.h, thickness: 1),
Row(
@@ -128,7 +137,7 @@ class PaymentCard extends StatelessWidget {
weight: FontWeight.w500,
),
CustomText(
text: "\$${totalPrice.toStringAsFixed(0)}",
text: "\$${widget.totalPrice.toStringAsFixed(0)}",
size: 18.sp,
color: Color(0xFFF95F62),
weight: FontWeight.bold,
@@ -136,115 +145,111 @@ class PaymentCard extends StatelessWidget {
],
),
SizedBox(height: 20.h),
CustomFilledButton(
onTap: () async {
try {
// ✅ Check login status first
final bool isLoggedIn = await LocalPreference.getLogin();
BlocBuilder<BuyPassBloc, BuyPassState>(
builder: (context, state) {
final isLoading = state is BuyPassLoaded && state.isAddingToCart;
// ✅ Create checkout data (needed for both cases)
final checkoutData = CheckoutData(
cityName: city,
heroImage: heroImage,
cardTypeName: cardType,
cardDisplayName: cardDisplayName,
themeColor: themeColor,
adultCount: adults,
childCount: children,
adultPrice: adultPrice,
childPrice: childPrice,
validityDuration: selectedValue,
totalPrice: totalPrice,
description: description,
);
return CustomFilledButton(
onTap: isLoading
? null
: () async {
final bloc = context.read<BuyPassBloc>();
bloc.add(AddToCartLoading());
try {
// ✅ Check login status first
final bool isLoggedIn = await LocalPreference.getLogin();
// ✅ Save to local preference (for both logged in and guest users)
// await LocalPreference.setPassCart(
// cityName: city,
// heroImage: heroImage,
// cardTypeName: cardType,
// cardDisplayName: cardDisplayName,
// themeColor: themeColor.value,
// adultCount: adults,
// childCount: children,
// adultPrice: adultPrice,
// childPrice: childPrice,
// validityDuration: selectedValue,
// totalPrice: totalPrice,
// description: description,
// );
if (isLoggedIn) {
// ✅ User is logged in - hit API
final repository = BuyPassRepository();
final response = await repository.addToCartPasses(
cityXid: cityXid,
cardTypeXid: cardTypeXid,
cardXid: cardXid,
cardMode: isSelectivePass ? 'flexi' : 'unlimited',
totalAdult: adults,
totalChild: children,
noOfAttractions: isSelectivePass ? selectedValue : 0,
noOfDays: isUnlimitedCard ? selectedValue : 0,
baseAmount: totalPrice,
);
// ✅ Extract bookingId from response
final int bookingId = response['id'];
// ✅ Navigate to checkout with bookingId
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: bookingId),
settings: RouteSettings(
arguments: checkoutData,
),
),
// ✅ Create checkout data (needed for both cases)
final checkoutData = CheckoutData(
cityName: widget.city,
heroImage: widget.heroImage,
cardTypeName: widget.cardType,
cardDisplayName: widget.cardDisplayName,
themeColor: widget.themeColor,
adultCount: widget.adults,
childCount: widget.children,
adultPrice: widget.adultPrice,
childPrice: widget.childPrice,
validityDuration: widget.selectedValue,
totalPrice: widget.totalPrice,
description: widget.description,
);
}
} else {
// ✅ User is NOT logged in - skip API, navigate directly
await LocalPreference.setPassCart(
cityName: city,
heroImage: heroImage,
cardTypeName: cardType,
cardDisplayName: cardDisplayName,
themeColor: themeColor.value,
adultCount: adults,
childCount: children,
adultPrice: adultPrice,
childPrice: childPrice,
validityDuration: selectedValue,
totalPrice: totalPrice,
description: description,
);
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: 0), // or 0, depending on your CheckoutView implementation
settings: RouteSettings(
arguments: checkoutData,
if (isLoggedIn) {
// ✅ User is logged in - hit API
final repository = BuyPassRepository();
final response = await repository.addToCartPasses(
cityXid: widget.cityXid,
cardTypeXid: widget.cardTypeXid,
cardXid: widget.cardXid,
cardMode: isSelectivePass ? 'flexi' : 'unlimited',
totalAdult: widget.adults,
totalChild: widget.children,
noOfAttractions: isSelectivePass ? widget.selectedValue : 0,
noOfDays: isUnlimitedCard ? widget.selectedValue : 0,
baseAmount: widget.totalPrice,
);
// ✅ Extract bookingId from response
final int bookingId = response['id'];
// ✅ Navigate to checkout with bookingId
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: bookingId),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
}
} else {
// ✅ User is NOT logged in - skip API, navigate directly
await LocalPreference.setPassCart(
cityName: widget.city,
heroImage: widget.heroImage,
cardTypeName: widget.cardType,
cardDisplayName: widget.cardDisplayName,
themeColor: widget.themeColor.value,
adultCount: widget.adults,
childCount: widget.children,
adultPrice: widget.adultPrice,
childPrice: widget.childPrice,
validityDuration: widget.selectedValue,
totalPrice: widget.totalPrice,
description: widget.description,
);
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CheckoutView(bookingId: 0),
settings: RouteSettings(
arguments: checkoutData,
),
),
);
}
}
} catch (e) {
// ✅ Show error message
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to proceed: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
),
),
);
);
}
} finally {
bloc.add(AddToCartDone()); // ✅ stop loading
}
}
} catch (e) {
// ✅ Show error message
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to proceed: ${e.toString()}'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 3),
),
);
}
}
},
label: isLoading ? "Please wait..." : "Proceed to Pay",
);
},
label: "Proceed to Pay",
),
],
),
@@ -258,8 +263,8 @@ class PaymentCard extends StatelessWidget {
required Function(int) onChanged,
}) {
List<int> numbersList = List.generate(
maxNumber - minNumber + 1,
(index) => minNumber + index,
widget.maxNumber - widget.minNumber + 1,
(index) => widget.minNumber + index,
);
return Row(

View File

@@ -22,25 +22,18 @@ class _MyCartPageState extends State<MyCartPage> {
@override
void initState() {
super.initState();
// ✅ Trigger fetch on the GLOBAL bloc instances (provided in main.dart)
// Do NOT create new blocs here — that was causing the refresh bug.
context.read<MyPassCartBloc>().add(const CheckLoginAndFetchEvent());
context.read<MyPostCardsCartBloc>().add(CheckLoginAndFetchPostcardsCart());
}
@override
Widget build(BuildContext context) {
// ✅ NO MultiBlocProvider here — we use the global blocs from main.dart.
// Creating new BlocProviders here was shadowing the global instances,
// so refresh events fired from VerifyOtpBottomsheet were hitting the
// global blocs but the UI was listening to the local (dead) ones.
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Fixed header ─────────────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Column(
@@ -54,7 +47,6 @@ class _MyCartPageState extends State<MyCartPage> {
),
backWidget(context, "Your Cart", Colors.black),
SizedBox(height: 24.h),
// ── Tab switcher ────────────────────────────────────
Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
@@ -72,8 +64,6 @@ class _MyCartPageState extends State<MyCartPage> {
],
),
),
// ✅ Expanded gives IndexedStack a FINITE height.
Expanded(
child: IndexedStack(
index: selectedTab,
@@ -94,8 +84,7 @@ class _MyCartPageState extends State<MyCartPage> {
return Expanded(
child: GestureDetector(
onTap: () => setState(() => selectedTab = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/cart/views/view_pass_page_view.dart';
import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart';
import 'package:citycards_customer/common_packages/custom_dashed_line.dart';
@@ -305,20 +306,25 @@ class _CartItemCard extends StatelessWidget {
bottomLeft: Radius.circular(8.r),
),
child: heroImage.isNotEmpty
? Image.network(
heroImage,
? CachedNetworkImage(
imageUrl: heroImage,
width: 105.w,
height: 130.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
);
},
errorWidget: (context, url, error) => Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
placeholder: (context, url) => Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
)
: Image.asset(
"assets/images/card_banner.png",

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -33,13 +34,14 @@ class TicketCard extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: cartItem.pcImagePath.isNotEmpty
? Image.network(
? CachedNetworkImage(
imageUrl:
'${ApiUrls.baseUrl}${cartItem.pcImagePath}',
width: 210.w,
height: 170.h,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
progressIndicatorBuilder:
(context, url, progress) {
return Container(
width: 210.w,
height: 170.h,
@@ -47,18 +49,14 @@ class TicketCard extends StatelessWidget {
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(16.r),
),
child: Center(
child: CircularProgressIndicator(
color: const Color(0xffF95F62),
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
alignment: Alignment.center,
child: CircularProgressIndicator(
color: const Color(0xffF95F62),
value: progress.progress,
),
);
},
errorBuilder: (_, __, ___) => _placeholderImage(),
errorWidget: (_, __, ___) => _placeholderImage(),
)
: _placeholderImage(),
),

View File

@@ -13,6 +13,7 @@ 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 '../../common_packages/custom_snackbar.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
import '../../localPreference/local_preference.dart';
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
@@ -128,9 +129,15 @@ class _CheckoutContent extends StatefulWidget {
class _CheckoutContentState extends State<_CheckoutContent> {
bool _hasHandledPaymentResult = false;
bool _hasAutoAppliedCoupon = 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 {
Future<void> _handlePaymentFlow(
BuildContext context,
String clientSecret,
int bookingId,
double finalTotal,
) async {
final paymentSuccess = await StripePaymentScreen.showAsBottomSheet(
context: context,
clientSecret: clientSecret,
@@ -184,11 +191,11 @@ class _CheckoutContentState extends State<_CheckoutContent> {
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Payment confirmed successfully!'),
backgroundColor: Colors.green,
),
);
const SnackBar(
content: Text('Payment confirmed successfully!'),
backgroundColor: Colors.green,
),
);
}
}
@@ -206,7 +213,8 @@ class _CheckoutContentState extends State<_CheckoutContent> {
double discountPercentage = 0.0;
if (state.appliedCoupon != null) {
discountPercentage = state.appliedCoupon!.discountPercent.toDouble();
discountPercentage = state.appliedCoupon!.discountPercent
.toDouble();
}
final num subtotal = widget.checkoutData.totalPrice;
@@ -230,14 +238,18 @@ class _CheckoutContentState extends State<_CheckoutContent> {
widget.couponId != null &&
state.appliedCoupon == null &&
state.coupons.isNotEmpty) {
final matchedCoupon = state.coupons.cast<AllCouponsModel?>().firstWhere(
final matchedCoupon = state.coupons
.cast<AllCouponsModel?>()
.firstWhere(
(c) => c?.id == widget.couponId,
orElse: () => null,
);
orElse: () => null,
);
if (matchedCoupon != null) {
_hasAutoAppliedCoupon = true; // ✅ Set flag before async call
_hasAutoAppliedCoupon = true; // ✅ Set flag before async call
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<CheckoutBloc>().add(ApplyCouponEvent(coupon: matchedCoupon));
context.read<CheckoutBloc>().add(
ApplyCouponEvent(coupon: matchedCoupon),
);
context.read<CheckoutBloc>().add(
ApplyCouponToBackendEvent(
bookingId: widget.bookingId,
@@ -271,20 +283,14 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// 🆕 Handle payment initiation error
if (state is CheckoutPaymentInitiationErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.error),
backgroundColor: Colors.red,
),
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,
),
SnackBar(content: Text(state.error), backgroundColor: Colors.red),
);
}
},
@@ -367,30 +373,37 @@ class _CheckoutContentState extends State<_CheckoutContent> {
),
child: widget.checkoutData.heroImage.isNotEmpty
? Image.network(
widget.checkoutData.heroImage,
width: 105.w,
height: 140.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _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(
color: const Color(0xffF95F62),
strokeWidth: 2,
),
),
),
);
},
)
widget.checkoutData.heroImage,
width: 105.w,
height: 140.h,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
_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(
color: const Color(
0xffF95F62,
),
strokeWidth: 2,
),
),
),
);
},
)
: _fallbackImage(),
),
@@ -419,10 +432,14 @@ class _CheckoutContentState extends State<_CheckoutContent> {
if (widget.checkoutData.adultCount > 0)
Row(
children: [
Image.asset('assets/icons/adult.png', scale: 4),
Image.asset(
'assets/icons/adult.png',
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text: "${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
text:
"${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
@@ -433,15 +450,20 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// Kids + Price row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
if (widget.checkoutData.childCount > 0)
Row(
children: [
Image.asset("assets/icons/kid.png", scale: 4),
Image.asset(
"assets/icons/kid.png",
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text: "${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
text:
"${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
@@ -452,7 +474,8 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// Price
CustomText(
text: "\$${subtotal.toStringAsFixed(2)}",
text:
"\$${subtotal.toStringAsFixed(2)}",
size: 20.sp,
weight: FontWeight.w500,
color: widget.checkoutData.themeColor,
@@ -502,8 +525,10 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// ✅ COUPON SECTION
Container(
width: double.infinity,
padding:
EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 16.h,
),
decoration: BoxDecoration(
color: const Color(0xFFF95F62).withOpacity(0.06),
borderRadius: BorderRadius.circular(8.r),
@@ -514,142 +539,160 @@ class _CheckoutContentState extends State<_CheckoutContent> {
),
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,
),
],
)
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,
)
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),
),
/// 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,
),
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,
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),
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),
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,
/// APPLY / REMOVE BUTTON
GestureDetector(
onTap: () async {
final isLogin =
await LocalPreference.getLogin();
if (isLogin == true) {
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,
),
);
}
} else {
CustomSnackbar.showWarning(
context,
message: 'Please login to apply coupon',
useOverlay: true,
);
}
},
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,
),
),
);
}
},
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(),
),
@@ -681,13 +724,12 @@ class _CheckoutContentState extends State<_CheckoutContent> {
// Discount
if (discountPercentage > 0) ...[
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Discount", size: 14.sp),
CustomText(
text:
"-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)",
"-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)",
size: 14.sp,
weight: FontWeight.w500,
color: Colors.green,
@@ -711,14 +753,13 @@ class _CheckoutContentState extends State<_CheckoutContent> {
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: 'Total', size: 14.sp),
SizedBox(height: 4.h),
CustomText(
text:
"Including \$${taxAmount.toStringAsFixed(2)} in taxes",
"Including \$${taxAmount.toStringAsFixed(2)} in taxes",
size: 12.sp,
color: Colors.black.withOpacity(0.6),
),
@@ -740,65 +781,76 @@ class _CheckoutContentState extends State<_CheckoutContent> {
future: LocalPreference.getLogin(),
builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false;
final isDisabled = isInitiatingPayment || isConfirmingPayment;
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);
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),
),
);
// ✅ 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);
// 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(),
);
}
}
}
} 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")
? (isInitiatingPayment || isConfirmingPayment
? "Processing..."
: "Pay \$${finalTotal.toStringAsFixed(2)}")
: "Checkout")
: "Login to Checkout",
);
},
@@ -819,11 +871,7 @@ class _CheckoutContentState extends State<_CheckoutContent> {
width: 105.w,
height: 140.h,
color: Colors.grey[200],
child: Icon(
Icons.card_travel,
size: 40.sp,
color: Colors.grey[400],
),
child: Icon(Icons.card_travel, size: 40.sp, color: Colors.grey[400]),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -39,47 +40,52 @@ class CommonAppBar extends StatelessWidget {
GestureDetector(
onTap: isSelectCity
? () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const CitySelectionBottomSheet(),
);
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const CitySelectionBottomSheet(),
);
}
: null,
child: FutureBuilder<String?>(
future: LocalPreference.getSelectedCityLogo(),
builder: (context, snapshot) {
final String? logoPath = snapshot.data;
final bool hasLogo = snapshot.hasData &&
final bool hasLogo =
snapshot.hasData &&
logoPath != null &&
logoPath.isNotEmpty;
final String? fullLogoUrl =
hasLogo ? "${ApiUrls.baseUrl}$logoPath" : null;
final String? fullLogoUrl = hasLogo
? "${ApiUrls.baseUrl}$logoPath"
: null;
return SizedBox(
height: hasLogo ? 40.h : 32.h,
child: hasLogo && fullLogoUrl != null
? Image.network(
fullLogoUrl,
fit: BoxFit.contain,
errorBuilder:
(context, error, stackTrace) {
return Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
);
},
)
? CachedNetworkImage(
imageUrl: fullLogoUrl,
fit: BoxFit.contain,
errorWidget: (context, url, error) => Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
),
placeholder: (context, url) => Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
),
)
: Image.asset(
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
),
isWhiteLogo
? "assets/logo/logo_city_cards_white.png"
: "assets/logo/logo_city_cards.png",
fit: BoxFit.contain,
),
);
},
),
@@ -93,8 +99,7 @@ class CommonAppBar extends StatelessWidget {
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) =>
const CitySelectionBottomSheet(),
builder: (_) => const CitySelectionBottomSheet(),
);
},
icon: Icon(
@@ -147,30 +152,25 @@ class CommonAppBar extends StatelessWidget {
String? imagePath;
if (state is ProfileLoaded) {
imagePath =
state.profile.profileImage;
imagePath = state.profile.profileImage;
}
final String? imageUrl =
(imagePath != null &&
imagePath.isNotEmpty)
(imagePath != null && imagePath.isNotEmpty)
? "${ApiUrls.baseUrl}$imagePath"
: null;
return CircleAvatar(
radius: 20.r,
backgroundColor:
const Color(0xffFFDFDF),
backgroundColor: const Color(0xffFFDFDF),
backgroundImage:
(imageUrl != null &&
imageUrl.isNotEmpty)
(imageUrl != null && imageUrl.isNotEmpty)
? NetworkImage(imageUrl)
: null,
child: (imageUrl == null ||
imageUrl.isEmpty)
child: (imageUrl == null || imageUrl.isEmpty)
? Image.asset(
"assets/images/profile_default_img.png",
)
"assets/images/profile_default_img.png",
)
: null,
);
},
@@ -186,10 +186,7 @@ class CommonAppBar extends StatelessWidget {
Column(
children: [
SizedBox(height: 12.h),
const Divider(
height: 1,
color: Color(0xFFD9D9D9),
),
const Divider(height: 1, color: Color(0xFFD9D9D9)),
SizedBox(height: 22.h),
],
),

View File

@@ -6,8 +6,9 @@ class CustomFilledButton extends StatelessWidget {
final double? width;
final String label;
final bool? showArrow;
final GestureTapCallback onTap;
final GestureTapCallback? onTap; // ✅ Made nullable
final double? height;
final bool isLoading; // ✅ NEW
const CustomFilledButton({
super.key,
@@ -15,35 +16,49 @@ class CustomFilledButton extends StatelessWidget {
required this.onTap,
required this.label,
this.showArrow = false,
this.height
this.height,
this.isLoading = false, // ✅ NEW
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
onTap: isLoading ? null : onTap, // ✅ Disabled when loading
child: Container(
height: height ?? 42.h, // ✅ SAFE
height: height ?? 42.h,
width: width ?? 266.w,
decoration: BoxDecoration(
color: Color(0xFFF95F62),
color: isLoading
? Color(0xFFF95F62).withOpacity(0.6) // ✅ Dimmed when loading
: Color(0xFFF95F62),
borderRadius: BorderRadius.circular(38.r),
),
child: Center(
child: Row(
child: isLoading
? SizedBox(
height: 20.sp,
width: 20.sp,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomText(
text: label,
color: Colors.white,
size: 16.sp ,
size: 16.sp,
weight: FontWeight.w500,
),
if(showArrow!)
SizedBox(width: 8,),
if(showArrow!)
Icon(Icons.arrow_forward_ios_rounded,size: 18.sp, color: Colors.white,)
if (showArrow!) SizedBox(width: 8),
if (showArrow!)
Icon(
Icons.arrow_forward_ios_rounded,
size: 18.sp,
color: Colors.white,
),
],
),
),

View File

@@ -8,25 +8,25 @@ class CityList {
if (json['cities'] != null) {
cities = <Cities>[];
json['cities'].forEach((v) {
cities!.add(new Cities.fromJson(v));
cities!.add(Cities.fromJson(v));
});
}
if (json['upcomingCities'] != null) {
upcomingCities = <UpcomingCities>[];
json['upcomingCities'].forEach((v) {
upcomingCities!.add(new UpcomingCities.fromJson(v));
upcomingCities!.add(UpcomingCities.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.cities != null) {
data['cities'] = this.cities!.map((v) => v.toJson()).toList();
final Map<String, dynamic> data = {};
if (cities != null) {
data['cities'] = cities!.map((v) => v.toJson()).toList();
}
if (this.upcomingCities != null) {
if (upcomingCities != null) {
data['upcomingCities'] =
this.upcomingCities!.map((v) => v.toJson()).toList();
upcomingCities!.map((v) => v.toJson()).toList();
}
return data;
}
@@ -41,18 +41,27 @@ class Cities {
int? cityCardTicketAmt;
int? saveAmount;
String? saveLabel;
// ✅ added safely
String? cityIconPath;
CityIcon? icon;
// ✅ kept to avoid breaking existing usage
List<UpcomingCities>? upcomingCities;
Cities(
{this.id,
this.cityName,
this.tagLine,
this.bannerImage,
this.indivisualTicketAmt,
this.cityCardTicketAmt,
this.saveAmount,
this.saveLabel,
this.upcomingCities});
Cities({
this.id,
this.cityName,
this.tagLine,
this.bannerImage,
this.indivisualTicketAmt,
this.cityCardTicketAmt,
this.saveAmount,
this.saveLabel,
this.cityIconPath,
this.icon,
this.upcomingCities,
});
Cities.fromJson(Map<String, dynamic> json) {
id = json['id'];
@@ -63,32 +72,55 @@ class Cities {
cityCardTicketAmt = json['cityCardTicketAmt'];
saveAmount = json['saveAmount'];
saveLabel = json['saveLabel'];
cityIconPath = json['cityIconPath'];
icon = json['icon'] != null ? CityIcon.fromJson(json['icon']) : null;
if (json['upcomingCities'] != null) {
upcomingCities = <UpcomingCities>[];
json['upcomingCities'].forEach((v) {
upcomingCities!.add(new UpcomingCities.fromJson(v));
upcomingCities!.add(UpcomingCities.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['cityName'] = this.cityName;
data['tagLine'] = this.tagLine;
data['bannerImage'] = this.bannerImage;
data['indivisualTicketAmt'] = this.indivisualTicketAmt;
data['cityCardTicketAmt'] = this.cityCardTicketAmt;
data['saveAmount'] = this.saveAmount;
data['saveLabel'] = this.saveLabel;
if (this.upcomingCities != null) {
final Map<String, dynamic> data = {};
data['id'] = id;
data['cityName'] = cityName;
data['tagLine'] = tagLine;
data['bannerImage'] = bannerImage;
data['indivisualTicketAmt'] = indivisualTicketAmt;
data['cityCardTicketAmt'] = cityCardTicketAmt;
data['saveAmount'] = saveAmount;
data['saveLabel'] = saveLabel;
data['cityIconPath'] = cityIconPath;
data['icon'] = icon?.toJson();
if (upcomingCities != null) {
data['upcomingCities'] =
this.upcomingCities!.map((v) => v.toJson()).toList();
upcomingCities!.map((v) => v.toJson()).toList();
}
return data;
}
}
class CityIcon {
String? svg;
CityIcon({this.svg});
CityIcon.fromJson(Map<String, dynamic> json) {
svg = json['svg'];
}
Map<String, dynamic> toJson() {
return {
'svg': svg,
};
}
}
class UpcomingCities {
int? id;
String? cityName;
@@ -103,10 +135,10 @@ class UpcomingCities {
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['cityName'] = this.cityName;
data['imgPathName'] = this.imgPathName;
return data;
return {
'id': id,
'cityName': cityName,
'imgPathName': imgPathName,
};
}
}

View File

@@ -208,6 +208,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
onTap: () async {
await LocalPreference.updateOnboardingPage(2);
await LocalPreference.setSelectedCityId(city.id!);
await LocalPreference.setSelectedCityLogo(city.cityIconPath??"");
Navigator.pushReplacementNamed(
context,
RouteConstants.home,
@@ -320,8 +321,7 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
separatorBuilder: (_, __) => SizedBox(width: 16.w),
itemBuilder: (context, index) {
final city = upcomingCities[index];
final imageUrl =
'${ApiUrls.baseUrl}${city.imgPathName}';
final imageUrl ='${ApiUrls.baseUrl}${city.imgPathName}';
return Column(
children: [

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/home/widgets/e_sim_offer_section.dart';
import 'package:citycards_customer/home/widgets/hotel_offers_section.dart';
import 'package:flutter/material.dart';
@@ -448,27 +449,30 @@ class _RegisteredUserHomePageState extends State<RegisteredUserHomePage> {
return SizedBox(
height: 350.h,
width: double.infinity,
child: imageUrl == null || imageUrl.isEmpty
child: (imageUrl == null || imageUrl.isEmpty)
? Image.asset(
"assets/images/chicago.png",
fit: BoxFit.cover,
)
: Image.network(
imageUrl,
: CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[300],
child: const Center(child: CircularProgressIndicator(color: Color(0xffF95F62))),
);
},
errorBuilder: (context, error, stackTrace) {
return Image.asset(
"assets/images/chicago.png",
fit: BoxFit.cover,
);
},
// 🔄 Loader (same as your loadingBuilder)
placeholder: (context, url) => Container(
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
),
),
),
// ❌ Error fallback (same as errorBuilder)
errorWidget: (context, url, error) => Image.asset(
"assets/images/chicago.png",
fit: BoxFit.cover,
),
),
);
}

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -114,28 +115,17 @@ class _AttractionsListViewState extends State<AttractionsListView> {
if (imageUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: Image.network(
imageUrl,
child: CachedNetworkImage(
imageUrl: imageUrl,
height: 232.h,
width: 161.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholder();
},
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(color: Color(0xffF95F62),
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
memCacheWidth: 400,
memCacheHeight: 600,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
errorWidget: (context, url, error) => _buildPlaceholder(),
),
)
else

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -38,15 +39,22 @@ class ExploreCitiesCard extends StatelessWidget {
children: [
/// Background Image with fallback
_isNetworkImage
? Image.network(
imageUrl,
? CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/city_sydney.png',
fit: BoxFit.cover,
);
},
placeholder: (context, url) => Container(
color: Colors.grey.shade200,
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
),
),
errorWidget: (context, url, error) => Image.asset(
'assets/images/city_sydney.png',
fit: BoxFit.cover,
),
)
: Image.asset(
'assets/images/city_sydney.png',

View File

@@ -72,15 +72,15 @@ class _CitySelectionView extends StatelessWidget {
if (cityId == 0) {
return SizedBox(width: 60.w); // Empty space to maintain layout
}
return Row(
children: [
InkWell(
onTap: () => Navigator.pop(context),
child: const Icon(Icons.arrow_back, size: 18),
),
SizedBox(width: 4.w),
CustomText(text: "Back", size: 12.sp),
],
return GestureDetector(
onTap: () => Navigator.pop(context),
child: Row(
children: [
const Icon(Icons.arrow_back, size: 18),
SizedBox(width: 4.w),
CustomText(text: "Back", size: 12.sp),
],
),
);
},
),

View File

@@ -283,7 +283,7 @@ class PassAttractionDetailsView extends StatelessWidget {
Text(
"Having problems redeeming the pass? ",
style: TextStyle(
fontSize: 12.sp,
fontSize: 11.sp,
color: Colors.black54,
),
),
@@ -294,7 +294,7 @@ class PassAttractionDetailsView extends StatelessWidget {
child: Text(
"Click Here",
style: TextStyle(
fontSize: 12.sp,
fontSize: 11.sp,
color: Color(0xFFF95F62),
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -37,7 +38,9 @@ class _PassDetailsViewState extends State<PassDetailsView> {
if (state is MyPassesDetailsLoading) {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator(color: Color(0xffF95F62))),
body: Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
);
}
@@ -90,7 +93,9 @@ class _PassDetailsViewState extends State<PassDetailsView> {
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 18.w, vertical: 18.h),
horizontal: 18.w,
vertical: 18.h,
),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withOpacity(0.08),
borderRadius: BorderRadius.circular(20.r),
@@ -100,9 +105,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
children: [
/// Title
Text(
'${(city?.cardMode ?? '').isNotEmpty
? city!.cardMode![0].toUpperCase() + city.cardMode!.substring(1)
: ''} Card',
'${(city?.cardMode ?? '').isNotEmpty ? city!.cardMode![0].toUpperCase() + city.cardMode!.substring(1) : ''} Card',
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
@@ -130,22 +133,26 @@ class _PassDetailsViewState extends State<PassDetailsView> {
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [
/// Adults + Kids always in a Row
Row(
children: [
Expanded(
child: _infoChip(
imagePath: "assets/icons/person.png",
text: "Adults-${city?.totalAdult ?? 0}",
imagePath:
"assets/icons/person.png",
text:
"Adults-${city?.totalAdult ?? 0}",
),
),
SizedBox(width: 8.w),
Expanded(
child: _infoChip(
imagePath: "assets/icons/person.png",
text: "Kids-${city?.totalChild ?? 0}",
imagePath:
"assets/icons/person.png",
text:
"Kids-${city?.totalChild ?? 0}",
),
),
],
@@ -213,27 +220,31 @@ class _PassDetailsViewState extends State<PassDetailsView> {
/// Display attractions from API
if (attractions.isNotEmpty) ...[
...attractions.take(2).map((attraction) =>
Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.passAttractionDetails,
arguments: attraction.id,
);
},
child: _attractionCard(
title: attraction.title,
description: attraction.description,
image: attraction.image,
ticketPriceAdult: attraction.ticketPriceAdult,
ticketPriceChild: attraction.ticketPriceChild,
bookingEmail: attraction.bookingEmail,
bookingPhoneNumber: attraction.bookingPhoneNumber,
...attractions
.take(2)
.map(
(attraction) => Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.passAttractionDetails,
arguments: attraction.id,
);
},
child: _attractionCard(
title: attraction.title,
description: attraction.description,
image: attraction.image,
ticketPriceAdult: attraction.ticketPriceAdult,
ticketPriceChild: attraction.ticketPriceChild,
bookingEmail: attraction.bookingEmail,
bookingPhoneNumber:
attraction.bookingPhoneNumber,
),
),
),
)),
),
] else ...[
_attractionCard(
title: 'No attractions available',
@@ -246,19 +257,13 @@ class _PassDetailsViewState extends State<PassDetailsView> {
),
],
SizedBox(height: 16.h),
_outlineButton(
"View all Attractions",
() {
Navigator.pushNamed(
context,
RouteConstants.passAttractionsPage,
arguments: {
'cityId': city?.id,
'source': 'my_passes',
},
);
},
),
_outlineButton("View all Attractions", () {
Navigator.pushNamed(
context,
RouteConstants.passAttractionsPage,
arguments: {'cityId': city?.id, 'source': 'my_passes'},
);
}),
SizedBox(height: 24.h),
@@ -329,24 +334,21 @@ class _PassDetailsViewState extends State<PassDetailsView> {
SizedBox(height: 16.h),
_outlineButton(
"View all Offers",
() {
Navigator.pushNamed(
context,
RouteConstants.searchPassOffer,
arguments: city?.id ??"",
);
},
),
_outlineButton("View all Offers", () {
Navigator.pushNamed(
context,
RouteConstants.searchPassOffer,
arguments: city?.id ?? "",
);
}),
SizedBox(height: 20.h),
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.privacyPolicy,
);
Navigator.of(
context,
).pushNamed(RouteConstants.privacyPolicy);
},
child: Center(
child: Text(
@@ -370,7 +372,9 @@ class _PassDetailsViewState extends State<PassDetailsView> {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator(color: Color(0xffF95F62))),
body: Center(
child: CircularProgressIndicator(color: Color(0xffF95F62)),
),
);
},
);
@@ -379,10 +383,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
Widget _sectionTitle(String title) {
return Text(
title,
style: GoogleFonts.poppins(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
style: GoogleFonts.poppins(fontSize: 16.sp, fontWeight: FontWeight.w600),
);
}
@@ -419,7 +420,8 @@ class _PassDetailsViewState extends State<PassDetailsView> {
String? bookingPhoneNumber,
}) {
// Check if booking is required (both email and phone are empty/null)
final bool isBookingRequired = (bookingEmail == null || bookingEmail.isEmpty) &&
final bool isBookingRequired =
(bookingEmail == null || bookingEmail.isEmpty) &&
(bookingPhoneNumber == null || bookingPhoneNumber.isEmpty);
// Format the price display
@@ -439,26 +441,32 @@ class _PassDetailsViewState extends State<PassDetailsView> {
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: image.isNotEmpty
? Image.network(
image,
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
);
},
)
? CachedNetworkImage(
imageUrl: image,
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
placeholder: (context, url) => Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
errorWidget: (context, url, error) => Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
)
: Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
),
SizedBox(width: 12.w),
@@ -546,7 +554,6 @@ class _PassDetailsViewState extends State<PassDetailsView> {
);
}
Widget _infoChip({
required String imagePath,
required String text,
@@ -602,29 +609,34 @@ class _PassDetailsViewState extends State<PassDetailsView> {
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: image.isNotEmpty
? Image.network(
image,
height: 120.h,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
"assets/images/aa4.png",
height: 120.h,
width: double.infinity,
fit: BoxFit.cover,
);
},
)
: Image.asset(
"assets/images/aa4.png",
height: 120.h,
width: double.infinity,
fit: BoxFit.cover,
),
),
? CachedNetworkImage(
imageUrl: image,
height: 120.h,
width: double.infinity,
fit: BoxFit.cover,
SizedBox(height: 12.h),
placeholder: (context, url) => Image.asset(
"assets/images/aa4.png",
height: 120.h,
width: double.infinity,
fit: BoxFit.cover,
),
errorWidget: (context, url, error) => Image.asset(
"assets/images/aa4.png",
height: 120.h,
width: double.infinity,
fit: BoxFit.cover,
),
)
: Image.asset(
"assets/images/aa4.png",
height: 120.h,
width: double.infinity,
fit: BoxFit.cover,
),
),
SizedBox(height: 2.h),
/// 🔥 Title
Text(
@@ -637,8 +649,6 @@ class _PassDetailsViewState extends State<PassDetailsView> {
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 6.h),
/// 🔥 Description
Text(
description,

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_search_field.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
@@ -256,54 +257,39 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(8.sp),
borderRadius: BorderRadius.circular(8.sp),
child: offer.mobileBannerImage != null &&
offer.mobileBannerImage!
.isNotEmpty
? Image.network(
offer.mobileBannerImage!.isNotEmpty
? CachedNetworkImage(
imageUrl:
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
width: double.infinity,
height: 120.5.h,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
progressIndicatorBuilder: (context, url, progress) {
return Container(
width: double.infinity,
height: 120.5.h,
color: Color(0xFFFEE7E7),
child: Icon(
Icons.local_offer,
size: 40.sp,
color: Color(0xFFF95F62)
.withOpacity(.6),
color: const Color(0xFFFEE7E7),
alignment: Alignment.center,
child: CircularProgressIndicator(
value: progress.progress,
strokeWidth: 2,
color: const Color(0xFFF95F62),
),
);
},
loadingBuilder: (context, child,
loadingProgress) {
if (loadingProgress == null) {
return child;
}
errorWidget: (context, url, error) {
return Container(
width: double.infinity,
height: 120.5.h,
color: Color(0xFFFEE7E7),
child: Center(
child:
CircularProgressIndicator(
value: loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
strokeWidth: 2,
color:
Color(0xFFF95F62),
),
color: const Color(0xFFFEE7E7),
alignment: Alignment.center,
child: Icon(
Icons.local_offer,
size: 40.sp,
color:
const Color(0xFFF95F62).withOpacity(.6),
),
);
},
@@ -311,12 +297,13 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
: Container(
width: double.infinity,
height: 120.5.h,
color: Color(0xFFFEE7E7),
color: const Color(0xFFFEE7E7),
alignment: Alignment.center,
child: Icon(
Icons.local_offer,
size: 40.sp,
color: Color(0xFFF95F62)
.withOpacity(.6),
color:
const Color(0xFFF95F62).withOpacity(.6),
),
),
),

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -23,7 +24,8 @@ class PassAttractionCard extends StatelessWidget {
/// Show "Booking Required" when both email and phone are empty/null
final bool showBookingRequired =
(attraction.bookingEmail.isEmpty || attraction.bookingEmail == null) ||
(attraction.bookingPhoneNumber.isEmpty || attraction.bookingPhoneNumber == null);
(attraction.bookingPhoneNumber.isEmpty ||
attraction.bookingPhoneNumber == null);
/// Format the price display
String priceText = attraction.ticketPriceAdult != null
@@ -50,15 +52,15 @@ class PassAttractionCard extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: imageUrl.isNotEmpty
? Image.network(
imageUrl,
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _imageFallback();
},
)
? CachedNetworkImage(
imageUrl: imageUrl,
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
placeholder: (context, url) => _imageFallback(),
errorWidget: (context, url, error) => _imageFallback(),
)
: _imageFallback(),
),
@@ -106,63 +108,65 @@ class PassAttractionCard extends StatelessWidget {
/// TAGS (CARD TITLES) OR BOOKING REQUIRED
showBookingRequired
? Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color: const Color(0xffC1D2F8),
border: Border.all(
color: const Color(0xff2563EB),
),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
"Booking Required",
style: GoogleFonts.poppins(
fontSize: 11.sp,
color: const Color(0xff1A1A1A),
fontWeight: FontWeight.w400,
),
),
)
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color: const Color(0xffC1D2F8),
border: Border.all(color: const Color(0xff2563EB)),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
"Booking Required",
style: GoogleFonts.poppins(
fontSize: 11.sp,
color: const Color(0xff1A1A1A),
fontWeight: FontWeight.w400,
),
),
)
: Wrap(
spacing: 6.w,
runSpacing: 6.h,
children: tags
.map(
(tag) => Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.h,
spacing: 6.w,
runSpacing: 6.h,
children: tags
.map(
(tag) => Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color:
tag ==
"${CommonAppText.selectiveCard} Card"
? const Color(
0xffF95FAF,
).withOpacity(0.1)
: const Color(
0xffF95F62,
).withOpacity(0.1),
border: Border.all(
color:
tag ==
"${CommonAppText.selectiveCard} Card"
? const Color(0xffF95FAF)
: const Color(0xffF95F62),
),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
tag,
style: GoogleFonts.poppins(
fontSize: 11.sp,
color: const Color(0xff1A1A1A),
fontWeight: FontWeight.w400,
),
),
),
)
.toList(),
),
decoration: BoxDecoration(
color: tag ==
"${CommonAppText.selectiveCard} Card"
? const Color(0xffF95FAF)
.withOpacity(0.1)
: const Color(0xffF95F62)
.withOpacity(0.1),
border: Border.all(
color: tag ==
"${CommonAppText.selectiveCard} Card"
? const Color(0xffF95FAF)
: const Color(0xffF95F62),
),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
tag,
style: GoogleFonts.poppins(
fontSize: 11.sp,
color: const Color(0xff1A1A1A),
fontWeight: FontWeight.w400,
),
),
),
)
.toList(),
),
],
),
),

View File

@@ -1,8 +1,8 @@
class ApiUrls {
// static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API
static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
// static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
// static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API
static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API
static const refreshToken = "$baseUrl/auth/refresh";

View File

@@ -19,7 +19,7 @@ class PostcardCreationBloc
// ✅ OPTIMIZATION: Pre-processed filter cache
final Map<String, String> _filterCache = {};
static const int maxImageSizeInBytes = 10 * 1024 * 1024; // 10 MB
static const int maxImageSizeInBytes = 5 * 1024 * 1024; // 10 MB
PostcardCreationBloc()
: super(
@@ -65,7 +65,7 @@ class PostcardCreationBloc
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB.",
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB.",
));
return;
}
@@ -94,7 +94,7 @@ class PostcardCreationBloc
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB. Please select a smaller image.",
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB. Please select a smaller image.",
));
return;
}
@@ -124,7 +124,7 @@ class PostcardCreationBloc
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB. Please try taking a photo with lower quality.",
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 5 MB. Please try taking a photo with lower quality.",
));
return;
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/postcard/blocs/edit_image_filter/edit_image_filter_bloc.dart';
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
@@ -247,55 +248,33 @@ class _EditPostcardViewState extends State<EditPostcardView> {
)
: Stack(
children: [
Image.network(
'${ApiUrls.baseUrl}${postCard!.pcImagePath}',
CachedNetworkImage(
imageUrl: '${ApiUrls.baseUrl}${postCard!.pcImagePath}',
height: size.width * 0.45,
width: size.width,
fit: BoxFit.cover,
loadingBuilder:
(
context,
child,
loadingProgress,
) {
if (loadingProgress ==
null) {
return child;
}
progressIndicatorBuilder: (context, url, progress) {
return Container(
height:
size.width *
0.45,
height: size.width * 0.45,
width: size.width,
color: Colors
.grey[300],
child: const Center(
child:
CircularProgressIndicator(
color: Color(0xffF95F62,),
strokeWidth:2,
),
color: Colors.grey[300],
alignment: Alignment.center,
child: CircularProgressIndicator(
color: const Color(0xffF95F62),
strokeWidth: 2,
value: progress.progress,
),
);
},
errorBuilder:
(
context,
error,
stackTrace,
) {
errorWidget: (context, url, error) {
return Container(
height:
size.width *
0.45,
height: size.width * 0.45,
width: size.width,
color: Colors
.grey[300],
color: Colors.grey[300],
alignment: Alignment.center,
child: const Icon(
Icons
.image_not_supported,
color:
Colors.grey,
Icons.image_not_supported,
color: Colors.grey,
),
);
},

View File

@@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart';
import 'package:citycards_customer/postcard/blocs/pick_images/pick_images_bloc.dart';
import 'package:citycards_customer/postcard/views/edit_postcard_view.dart';
@@ -404,36 +405,35 @@ class _MyPostCardDraftViewState extends State<MyPostCardDraftView> {
/// LEFT IMAGE
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
child: CachedNetworkImage(
imageUrl: '${ApiUrls.baseUrl}${postcard.pcImagePath}',
height: 72,
width: 72,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
height: 72,
width: 72,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
height: 72,
width: 72,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
);
},
imageBuilder: (context, imageProvider) => ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image(
image: imageProvider,
fit: BoxFit.cover,
),
),
placeholder: (context, url) => Container(
height: 72,
width: 72,
color: Colors.grey.shade300,
alignment: Alignment.center,
child: const CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xffF95F62),
),
),
errorWidget: (context, url, error) => Container(
height: 72,
width: 72,
color: Colors.grey.shade300,
alignment: Alignment.center,
child: const Icon(Icons.broken_image, color: Colors.grey),
),
),
),

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -335,42 +336,40 @@ class _MyPostCardOrdersViewState extends State<MyPostCardOrdersView> {
// Postcard Image
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(
image: NetworkImage(
'${ApiUrls.baseUrl}${postcard.pcImagePath}',
),
child: CachedNetworkImage(
imageUrl: '${ApiUrls.baseUrl}${postcard.pcImagePath}',
height: 70.h,
width: 70.w,
fit: BoxFit.cover,
// Loading indicator
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
height: 70.h,
width: 70.w,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
),
);
},
imageBuilder: (context, imageProvider) => Image(
image: imageProvider,
height: 70.h,
width: 70.w,
fit: BoxFit.cover,
),
// Error UI
errorBuilder: (context, error, stackTrace) {
return Container(
height: 70.h,
width: 70.w,
color: Colors.grey[300],
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
);
},
placeholder: (context, url) => Container(
height: 70.h,
width: 70.w,
color: Colors.grey.shade300,
alignment: Alignment.center,
child: const CircularProgressIndicator(
color: Color(0xffF95F62),
strokeWidth: 2,
),
),
errorWidget: (context, url, error) => Container(
height: 70.h,
width: 70.w,
color: Colors.grey.shade300,
alignment: Alignment.center,
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
),
),
),
const SizedBox(width: 20),

View File

@@ -54,13 +54,19 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},
];
// Calculate the actual line height used by the TextField
// fontSize (14sp) * Flutter default line height multiplier (1.5) gives us
// the real pixel height per line so the painter lines align with text.
final double fontSize = 14.sp;
final double lineHeight = fontSize * 1.5;
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true),
StepProgressBar(totalSteps: 4, currentStep: 3),
GestureDetector(
onTap: () {
@@ -70,9 +76,9 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
const Text(
"Back",
style: TextStyle(
fontSize: 16,
@@ -83,9 +89,10 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
),
),
),
Text("Write a message",
style:
TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
const Text(
"Write a message",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
Text(
"Design your own unique postcards to cherish your unforgettable moments.",
@@ -105,7 +112,14 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
borderRadius: BorderRadius.circular(12),
),
child: CustomPaint(
painter: LinedPaperPainter(),
// Pass lineHeight and topOffset so lines align perfectly with text
painter: LinedPaperPainter(
lineHeight: lineHeight,
// horizontal padding inside the container is 8 (symmetric),
// TextField has no extra top padding with border: InputBorder.none
// so we start the first line at lineHeight itself.
topOffset: lineHeight,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
@@ -115,6 +129,11 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
maxLength: 400,
cursorColor: const Color(0xffF95F62),
style: _getTextFieldStyle(state.selectedFont, fonts),
strutStyle: StrutStyle(
fontSize: fontSize,
height: 1.5,
forceStrutHeight: true, // ensures every line is exactly lineHeight tall
),
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Add Your Message Here",
@@ -123,6 +142,9 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
fontSize: 14.sp,
),
counterText: "",
// Remove all default content padding so text starts at top-left
contentPadding: EdgeInsets.zero,
isDense: true,
),
onChanged: (val) => bloc.add(WriteMessage(val)),
),
@@ -180,20 +202,24 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Aa",
style: fontStyle.copyWith(
fontSize: 24.sp,
color: const Color(0xff1A1A1A),
)),
Text(
"Aa",
style: fontStyle.copyWith(
fontSize: 24.sp,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 4),
Text(fontName,
textAlign: TextAlign.center,
style: fontStyle.copyWith(
fontSize: 11.sp,
color: isSelected
? const Color(0xffF95F62)
: const Color(0xff2D3134),
)),
Text(
fontName,
textAlign: TextAlign.center,
style: fontStyle.copyWith(
fontSize: 11.sp,
color: isSelected
? const Color(0xffF95F62)
: const Color(0xff2D3134),
),
),
],
),
),
@@ -237,26 +263,57 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
);
}
// Helper method to get the correct font style for the text field
TextStyle _getTextFieldStyle(String? selectedFont, List<Map<String, dynamic>> fonts) {
if (selectedFont == null || selectedFont.isEmpty) {
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
// Find matching font by cleanName
for (var font in fonts) {
if (font['cleanName'] == selectedFont) {
final TextStyle fontStyle = font['font'] as TextStyle;
return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black);
}
}
// Default fallback to Poppins
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
}
// Custom Painter for Dotted Border
// ─────────────────────────────────────────────
// LinedPaperPainter — lines aligned to text rows
// ─────────────────────────────────────────────
class LinedPaperPainter extends CustomPainter {
/// The pixel height of one text line (fontSize * lineHeightMultiplier).
final double lineHeight;
/// Where the first line should be drawn (matches where the first text
/// baseline sits). Equals [lineHeight] when contentPadding is zero.
final double topOffset;
const LinedPaperPainter({
required this.lineHeight,
required this.topOffset,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFFE0E0E0)
..strokeWidth = 1;
double y = topOffset;
while (y <= size.height) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
y += lineHeight;
}
}
@override
bool shouldRepaint(LinedPaperPainter oldDelegate) =>
oldDelegate.lineHeight != lineHeight || oldDelegate.topOffset != topOffset;
}
// ─────────────────────────────────────────────
// DottedBorderPainter (unchanged)
// ─────────────────────────────────────────────
class DottedBorderPainter extends CustomPainter {
final Color color;
final double strokeWidth;
@@ -285,7 +342,6 @@ class DottedBorderPainter extends CustomPainter {
Radius.circular(borderRadius),
));
// Create dashed path
final dashPath = _createDashedPath(path, dashWidth, dashSpace);
canvas.drawPath(dashPath, paint);
}
@@ -321,32 +377,9 @@ class DottedBorderPainter extends CustomPainter {
}
@override
bool shouldRepaint(DottedBorderPainter oldDelegate) {
return oldDelegate.color != color ||
oldDelegate.strokeWidth != strokeWidth ||
oldDelegate.dashWidth != dashWidth ||
oldDelegate.dashSpace != dashSpace;
}
}
// Lined Paper Painter (assuming this exists in your original code)
class LinedPaperPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFFE0E0E0)
..strokeWidth = 1;
const lineSpacing = 30.0;
for (double i = lineSpacing; i < size.height; i += lineSpacing) {
canvas.drawLine(
Offset(0, i),
Offset(size.width, i),
paint,
);
}
}
@override
bool shouldRepaint(LinedPaperPainter oldDelegate) => false;
bool shouldRepaint(DottedBorderPainter oldDelegate) =>
oldDelegate.color != color ||
oldDelegate.strokeWidth != strokeWidth ||
oldDelegate.dashWidth != dashWidth ||
oldDelegate.dashSpace != dashSpace;
}

View File

@@ -38,18 +38,22 @@ class _EditMessageState extends State<EditMessage> {
@override
void initState() {
super.initState();
final parsedMessage = _parseHtmlMessage(widget.text);
final messageText = parsedMessage['text'] ?? '';
final fontFamily = parsedMessage['fontFamily'] ?? '';
setState(() {
_controller.text = messageText;
selectedFont = fontFamily;
selectedFont = fontFamily.isNotEmpty ? fontFamily : "Poppins";
});
super.initState();
}
@override
Widget build(BuildContext context) {
// Calculate exact line height to match TextField rows
final double fontSize = 14.sp;
final double lineHeight = fontSize * 1.5;
return Column(
children: [
Container(
@@ -59,7 +63,10 @@ class _EditMessageState extends State<EditMessage> {
borderRadius: BorderRadius.circular(12),
),
child: CustomPaint(
painter: LinedPaperPainter(),
painter: LinedPaperPainter(
lineHeight: lineHeight,
topOffset: lineHeight,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextFormField(
@@ -68,6 +75,11 @@ class _EditMessageState extends State<EditMessage> {
maxLength: 400,
cursorColor: const Color(0xffF95F62),
style: _getTextFieldStyle(selectedFont, fonts),
strutStyle: StrutStyle(
fontSize: fontSize,
height: 1.5,
forceStrutHeight: true, // forces every line to be exactly lineHeight tall
),
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Add Your Message Here",
@@ -76,8 +88,12 @@ class _EditMessageState extends State<EditMessage> {
fontSize: 14.sp,
),
counterText: "",
// Remove default padding so first text line aligns with first drawn rule
contentPadding: EdgeInsets.zero,
isDense: true,
),
onChanged: (val) {
setState(() {}); // rebuild to update character counter
widget.onChange(val, selectedFont);
},
validator: (value) {
@@ -177,14 +193,13 @@ class _EditMessageState extends State<EditMessage> {
}
TextStyle _getTextFieldStyle(
String? selectedFont,
List<Map<String, dynamic>> fonts,
) {
String? selectedFont,
List<Map<String, dynamic>> fonts,
) {
if (selectedFont == null || selectedFont.isEmpty) {
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
// Find matching font by cleanName
for (var font in fonts) {
if (font['cleanName'] == selectedFont) {
final TextStyle fontStyle = font['font'] as TextStyle;
@@ -192,7 +207,6 @@ class _EditMessageState extends State<EditMessage> {
}
}
// Default fallback to Poppins
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
}
@@ -201,65 +215,40 @@ class _EditMessageState extends State<EditMessage> {
return {'text': '', 'fontFamily': ''};
}
// Check if message contains HTML tags
if (!htmlMessage.contains('<span') && !htmlMessage.contains('style=')) {
// Plain text message - no font specified
return {'text': htmlMessage, 'fontFamily': ''};
}
try {
// Parse HTML
final document = html_parser.parse(htmlMessage);
final spanElement = document.querySelector('span');
if (spanElement != null) {
// Extract text content
final text = spanElement.text;
// Extract font-family from style attribute
final style = spanElement.attributes['style'] ?? '';
final fontFamilyMatch = RegExp(
r'font-family:\s*([^;]+)',
).firstMatch(style);
final fontFamily = fontFamilyMatch?.group(1)?.trim() ?? '';
return {'text': text, 'fontFamily': fontFamily};
}
// Fallback: return plain text
return {'text': document.body?.text ?? htmlMessage, 'fontFamily': ''};
} catch (e) {
// If parsing fails, return original message
return {'text': htmlMessage, 'fontFamily': ''};
}
}
// Get TextStyle with any Google Font
TextStyle _getFontStyle(String fontFamily, double fontSize, double height) {
// If no font family specified, use default Caveat
if (fontFamily.isEmpty) {
return GoogleFonts.caveat(fontSize: fontSize, height: height);
}
try {
// Normalize font name: remove extra spaces, handle common variations
final normalizedFont = fontFamily.trim().replaceAll(
RegExp(r'\s+'),
' ',
); // Replace multiple spaces with single space
// Try to get the font from Google Fonts
// GoogleFonts.getFont() can load ANY Google Font dynamically
return GoogleFonts.getFont(
normalizedFont,
fontSize: fontSize,
height: height,
);
final normalizedFont = fontFamily.trim().replaceAll(RegExp(r'\s+'), ' ');
return GoogleFonts.getFont(normalizedFont, fontSize: fontSize, height: height);
} catch (e) {
// If font not found in Google Fonts, fallback to default
debugPrint(
'⚠️ Font "$fontFamily" not found in Google Fonts. Using default Caveat font.',
);
debugPrint('⚠️ Font "$fontFamily" not found in Google Fonts. Using default Caveat font.');
return GoogleFonts.caveat(fontSize: fontSize, height: height);
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -54,31 +55,31 @@ class FrontCardWidget extends StatelessWidget {
}
if (_isNetworkImage) {
return Image.network(
imageUrl,
return CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
progressIndicatorBuilder: (context, url, progress) {
return Container(
color: Colors.grey.shade200,
child: const Center(
child: Icon(Icons.broken_image, size: 40, color: Colors.grey),
),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey.shade200,
child: Center(
child: CircularProgressIndicator(color: Color(0xffF95F62),
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
alignment: Alignment.center,
child: CircularProgressIndicator(
color: const Color(0xffF95F62),
value: progress.progress, // 👈 percentage
strokeWidth: 2,
),
);
},
errorWidget: (context, url, error) => Container(
color: Colors.grey.shade200,
alignment: Alignment.center,
child: const Icon(
Icons.broken_image,
size: 40,
color: Colors.grey,
),
),
);
} else {
return Image.file(

View File

@@ -423,7 +423,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
isProfilePage: true,
showDivider: true,
),
backWidget(context, "Edit Profile", Colors.black),

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/common_bloc/language_selection_bloc.dart';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/back_widget.dart';
@@ -377,12 +378,11 @@ class _ProfilePageState extends State<ProfilePage> {
child: ClipOval(
child: Container(
color: const Color(0xFFFCE4E5),
child: profileImageUrl != null
? Image.network(
profileImageUrl,
child: profileImageUrl != null && profileImageUrl!.isNotEmpty
? CachedNetworkImage(
imageUrl: profileImageUrl!,
fit: BoxFit.cover,
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
progressIndicatorBuilder: (context, url, progress) {
return const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
@@ -390,7 +390,7 @@ class _ProfilePageState extends State<ProfilePage> {
),
);
},
errorBuilder: (_, __, ___) {
errorWidget: (_, __, ___) {
return Padding(
padding: EdgeInsets.all(16.w),
child: Image.asset(

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_search_field.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
@@ -242,54 +243,40 @@ class _OffersScreenState extends State<OffersScreen> {
child: Column(
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(8.sp),
borderRadius: BorderRadius.circular(8.sp),
child: offer.mobileBannerImage != null &&
offer.mobileBannerImage!
.isNotEmpty
? Image.network(
offer.mobileBannerImage!.isNotEmpty
? CachedNetworkImage(
imageUrl:
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
width: double.infinity,
height: 120.5.h,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) {
progressIndicatorBuilder:
(context, url, progress) {
return Container(
width: double.infinity,
height: 120.5.h,
color: Color(0xFFFEE7E7),
child: Icon(
Icons.local_offer,
size: 40.sp,
color: Color(0xFFF95F62)
.withOpacity(.6),
color: const Color(0xFFFEE7E7),
alignment: Alignment.center,
child: CircularProgressIndicator(
value: progress.progress,
strokeWidth: 2,
color: const Color(0xFFF95F62),
),
);
},
loadingBuilder: (context, child,
loadingProgress) {
if (loadingProgress == null) {
return child;
}
errorWidget: (context, url, error) {
return Container(
width: double.infinity,
height: 120.5.h,
color: Color(0xFFFEE7E7),
child: Center(
child:
CircularProgressIndicator(
value: loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
strokeWidth: 2,
color:
Color(0xFFF95F62),
),
color: const Color(0xFFFEE7E7),
alignment: Alignment.center,
child: Icon(
Icons.local_offer,
size: 40.sp,
color:
const Color(0xFFF95F62).withOpacity(.6),
),
);
},
@@ -297,12 +284,13 @@ class _OffersScreenState extends State<OffersScreen> {
: Container(
width: double.infinity,
height: 120.5.h,
color: Color(0xFFFEE7E7),
color: const Color(0xFFFEE7E7),
alignment: Alignment.center,
child: Icon(
Icons.local_offer,
size: 40.sp,
color: Color(0xFFF95F62)
.withOpacity(.6),
color:
const Color(0xFFF95F62).withOpacity(.6),
),
),
),