bug fixes
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,4 +29,6 @@ class UpdateValidityDuration extends BuyPassEvent {
|
||||
final int duration;
|
||||
|
||||
UpdateValidityDuration(this.duration);
|
||||
}
|
||||
}
|
||||
class AddToCartLoading extends BuyPassEvent {}
|
||||
class AddToCartDone extends BuyPassEvent {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +186,11 @@ 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),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,39 +16,53 @@ 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -654,4 +664,4 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -200,4 +204,4 @@ class PassAttractionCard extends StatelessWidget {
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,66 +215,41 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user