added my passes and more chnages

This commit is contained in:
mystery012728
2026-02-13 15:27:14 +05:30
parent 5d08e07de3
commit b08e2699e9
85 changed files with 5036 additions and 1453 deletions

BIN
assets/icons/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

BIN
assets/icons/person.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/time.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -9,6 +9,9 @@ import 'stripe_payment_state.dart';
class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
final StripeService _stripeService;
// 🔒 Flag to prevent re-initialization after success
bool _paymentCompleted = false;
StripePaymentBloc({
StripeService? stripeService,
}) : _stripeService = stripeService ?? StripeService(),
@@ -24,6 +27,12 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
InitiatePayment event,
Emitter<StripePaymentState> emit,
) async {
// 🛑 Prevent re-initialization if payment already completed
if (_paymentCompleted) {
debugPrint('⚠️ Payment already completed. Ignoring re-initialization.');
return;
}
try {
emit(const StripePaymentLoading(
message: 'Creating payment intent...',
@@ -61,7 +70,8 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
// 3⃣ Show Payment Sheet
await Stripe.instance.presentPaymentSheet();
// ✅ SUCCESS
// ✅ SUCCESS - Mark as completed
_paymentCompleted = true;
emit(const StripePaymentSuccess());
} on StripeException catch (e) {
_handleStripeException(e, emit);
@@ -78,6 +88,12 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
InitiatePaymentWithClientSecret event,
Emitter<StripePaymentState> emit,
) async {
// 🛑 Prevent re-initialization if payment already completed
if (_paymentCompleted) {
debugPrint('⚠️ Payment already completed. Ignoring re-initialization.');
return;
}
try {
emit(const StripePaymentLoading(
message: 'Initializing payment...',
@@ -101,7 +117,8 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
// 2⃣ Show Payment Sheet
await Stripe.instance.presentPaymentSheet();
// ✅ SUCCESS
// ✅ SUCCESS - Mark as completed
_paymentCompleted = true;
emit(const StripePaymentSuccess());
} on StripeException catch (e) {
_handleStripeException(e, emit);
@@ -118,9 +135,12 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
CancelPaymentEvent event,
Emitter<StripePaymentState> emit,
) {
emit(const StripePaymentCancelled(
message: 'Payment cancelled by user',
));
// Only emit cancelled if not already completed
if (!_paymentCompleted) {
emit(const StripePaymentCancelled(
message: 'Payment cancelled by user',
));
}
}
/// Handle payment retry
@@ -128,6 +148,9 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
RetryPaymentEvent event,
Emitter<StripePaymentState> emit,
) async {
// 🔄 Reset completion flag for retry
_paymentCompleted = false;
// Reset state first
emit(const StripePaymentInitial());
@@ -142,6 +165,8 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
ResetPaymentState event,
Emitter<StripePaymentState> emit,
) {
// 🔄 Reset completion flag
_paymentCompleted = false;
emit(const StripePaymentInitial());
}
@@ -199,4 +224,11 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
return !nonRetryableErrors.contains(errorCode);
}
@override
Future<void> close() {
// Reset flag on bloc disposal
_paymentCompleted = false;
return super.close();
}
}

View File

@@ -199,8 +199,18 @@ class StripePaymentScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocConsumer<StripePaymentBloc, StripePaymentState>(
// 🔒 CRITICAL: Only listen when state actually changes to prevent duplicate triggers
listenWhen: (previous, current) {
// Don't re-trigger if both states are the same success state
if (previous is StripePaymentSuccess && current is StripePaymentSuccess) {
debugPrint('⚠️ Preventing duplicate success listener');
return false;
}
return true;
},
listener: (context, state) {
if (state is StripePaymentSuccess) {
debugPrint('✅ Payment Success - Calling callback');
// ✅ Call the callback first
onPaymentSuccess?.call();
// ✅ Then auto-close and return true after 1.5 seconds
@@ -210,6 +220,7 @@ class StripePaymentScreen extends StatelessWidget {
}
});
} else if (state is StripePaymentFailure) {
debugPrint('❌ Payment Failure - ${state.error}');
onPaymentFailure?.call(state.error);
// Auto-close after 2 seconds on failure
Future.delayed(const Duration(seconds: 2), () {
@@ -218,10 +229,18 @@ class StripePaymentScreen extends StatelessWidget {
}
});
} else if (state is StripePaymentCancelled) {
debugPrint('🚫 Payment Cancelled');
onPaymentCancelled?.call();
Navigator.of(context).pop(false);
}
},
buildWhen: (previous, current) {
// 🔒 Prevent unnecessary rebuilds on duplicate success states
if (previous is StripePaymentSuccess && current is StripePaymentSuccess) {
return false;
}
return true;
},
builder: (context, state) {
return Container(
height: heightRatio == 1.0
@@ -394,7 +413,7 @@ class StripePaymentScreen extends StatelessWidget {
onPressed: () {
// Retry payment
context.read<StripePaymentBloc>().add(
InitiatePaymentWithClientSecret(
RetryPaymentEvent(
clientSecret: clientSecret,
),
);

View File

@@ -81,12 +81,12 @@ class _AddDetailsViewState extends State<AddDetailsView> {
// Handle API submission success
if (state is PurchaseDetailsSubmitted) {
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Gift details submitted successfully!'),
backgroundColor: Color(0xffF95F62),
),
);
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Gift details submitted successfully!'),
// backgroundColor: Color(0xffF95F62),
// ),
// );
// Navigate back
Navigator.of(context).pop('success');
@@ -231,7 +231,7 @@ class _AddDetailsViewState extends State<AddDetailsView> {
selectedCountry = value;
});
},
items: ["India", "USA", "UK", "Canada"]
items: ["Australia"]
.map((value) {
return DropdownMenuItem<String>(
value: value,

View File

@@ -26,15 +26,18 @@ class ShareBottomSheet extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// drag handle
Container(
height: 4.h,
width: 47.w,
margin: EdgeInsets.only(bottom: 16),
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: Color(0xFF222222),
color: const Color(0xFF222222),
borderRadius: BorderRadius.circular(8),
),
),
// link field
TextField(
readOnly: true,
decoration: InputDecoration(
@@ -51,7 +54,10 @@ class ShareBottomSheet extends StatelessWidget {
),
),
),
SizedBox(height: 20.h),
// grid
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -67,7 +73,16 @@ class ShareBottomSheet extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(item['icon']!, width: 55.w),
// FIXED SIZE ICON CONTAINER
Container(
width: 55.w,
height: 55.w,
alignment: Alignment.center,
child: Image.asset(
item['icon']!,
fit: BoxFit.contain,
),
),
SizedBox(height: 8.h),
Text(
item['title']!,
@@ -78,26 +93,32 @@ class ShareBottomSheet extends StatelessWidget {
);
},
),
const SizedBox(height: 20),
// page indicator
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
4,
(index) => Container(
(index) => Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
width: 8.w,
height: 8.h,
decoration: BoxDecoration(
color: index == 0 ? Color(0xFF676363) : Colors.white,
border: Border.all(color: Color(0xFF676363)),
color: index == 0
? const Color(0xFF676363)
: Colors.white,
border: Border.all(color: const Color(0xFF676363)),
shape: BoxShape.circle,
),
),
),
),
SizedBox(height: 10.h),
],
),
);
}
}
}

View File

@@ -37,9 +37,9 @@ class Attraction {
final String title;
final String description;
final String urlSlug;
final int cityXid;
final int cardTypeXid;
final int partnerXid;
final num cityXid;
final num cardTypeXid;
final num partnerXid;
final String productCode;
final bool isBookingRequired;
@@ -47,14 +47,14 @@ class Attraction {
final String bookingEmail;
final String bookingPhoneNumber;
final double latitudeCoordinate;
final double longitudeCoordinate;
final num latitudeCoordinate;
final num longitudeCoordinate;
final String address;
final double? ticketPriceAdult;
final double? ticketPriceChild;
final int durations;
final int groupSize;
final num? ticketPriceAdult;
final num? ticketPriceChild;
final num durations;
final num groupSize;
final String ageRange;
final String seoTitle;
@@ -115,13 +115,11 @@ class Attraction {
isPartnerAccess: json['isPartnerAccess'] ?? false,
bookingEmail: json['bookingEmail'] ?? '',
bookingPhoneNumber: json['bookingPhonenumber'] ?? '',
latitudeCoordinate:
(json['latitudeCoordinate'] as num?)?.toDouble() ?? 0.0,
longitudeCoordinate:
(json['longitudeCoordinate'] as num?)?.toDouble() ?? 0.0,
latitudeCoordinate: (json['latitudeCoordinate'] as num?) ?? 0,
longitudeCoordinate: (json['longitudeCoordinate'] as num?) ?? 0,
address: json['address'] ?? '',
ticketPriceAdult: (json['ticketPriceAdult'] as num?)?.toDouble(),
ticketPriceChild: (json['ticketPriceChild'] as num?)?.toDouble(),
ticketPriceAdult: json['ticketPriceAdult'] as num?,
ticketPriceChild: json['ticketPriceChild'] as num?,
durations: json['durations'] ?? 0,
groupSize: json['groupSize'] ?? 0,
ageRange: json['ageRange'] ?? '',
@@ -197,9 +195,9 @@ class Attraction {
class CardModel {
final int id;
final String title;
final int cardTypeXid;
final int adultPrice;
final int childPrice;
final num cardTypeXid;
final num adultPrice;
final num childPrice;
final String cardStatus;
CardModel({
@@ -234,7 +232,6 @@ class CardModel {
}
}
/* -------------------- GALLERY -------------------- */
class Gallery {
@@ -275,7 +272,6 @@ class Gallery {
bool get hasImage => filePathUrl.isNotEmpty;
}
/* -------------------- CATEGORY -------------------- */
class Category {
@@ -300,5 +296,4 @@ class Category {
'categoryName': categoryName,
};
}
}
}

View File

@@ -61,6 +61,8 @@ class AttractionCard extends StatelessWidget {
children: [
Text(
attraction.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
@@ -71,6 +73,8 @@ class AttractionCard extends StatelessWidget {
Text(
attraction.address,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontWeight: FontWeight.w400,
@@ -104,10 +108,8 @@ class AttractionCard extends StatelessWidget {
),
SizedBox(height: 6.h),
/// TAGS (CARD TITLES)
attraction.isBookingRequired == false
? Wrap(
Wrap(
spacing: 6.w,
runSpacing: 6.h,
children: tags
@@ -145,27 +147,6 @@ class AttractionCard extends StatelessWidget {
)
.toList(),
)
: 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,
),
),
),
],
),
),

View File

@@ -8,10 +8,10 @@ String buyPassModelToJson(BuyPassModel data) =>
json.encode(data.toJson());
class BuyPassModel {
final City city;
final List<Offer> offers;
final List<CardPass> cards;
final List<Attraction> attractions;
City city;
List<Offer> offers;
List<CardPass> cards;
List<Attraction> attractions;
BuyPassModel({
required this.city,
@@ -20,41 +20,49 @@ class BuyPassModel {
required this.attractions,
});
factory BuyPassModel.fromJson(Map<String, dynamic> json) {
factory BuyPassModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
return BuyPassModel(
city: City.fromJson(json['city']),
offers: List<Offer>.from(
json['offers'].map((x) => Offer.fromJson(x)),
),
cards: List<CardPass>.from(
json['cards'].map((x) => CardPass.fromJson(x)),
),
attractions: List<Attraction>.from(
json['attractions'].map((x) => Attraction.fromJson(x)),
),
offers: json['offers'] == null
? []
: List<Map<String, dynamic>>.from(json['offers'])
.map((e) => Offer.fromJson(e))
.toList(),
cards: json['cards'] == null
? []
: List<Map<String, dynamic>>.from(json['cards'])
.map((e) => CardPass.fromJson(e))
.toList(),
attractions: json['attractions'] == null
? []
: List<Map<String, dynamic>>.from(json['attractions'])
.map((e) => Attraction.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
"city": city.toJson(),
"offers": offers.map((x) => x.toJson()).toList(),
"cards": cards.map((x) => x.toJson()).toList(),
"attractions": attractions.map((x) => x.toJson()).toList(),
"offers": offers.map((e) => e.toJson()).toList(),
"cards": cards.map((e) => e.toJson()).toList(),
"attractions": attractions.map((e) => e.toJson()).toList(),
};
}
/// ---------- CITY ----------
class City {
final int id;
final String name;
final String slug;
final String tagLine;
final String description;
final String bestTimeToVisit;
final String priceRange;
final num individualTicketAmount; // Changed from int to num
final num cityCardTicketAmount; // Changed from int to num
final HeroBanner heroBanner;
int id;
String name;
String slug;
String tagLine;
String description;
String bestTimeToVisit;
String priceRange;
num individualTicketAmount;
num cityCardTicketAmount;
HeroBanner heroBanner;
City({
required this.id,
@@ -69,17 +77,19 @@ class City {
required this.heroBanner,
});
factory City.fromJson(Map<String, dynamic> json) {
factory City.fromJson(Map<String, dynamic>? json) {
json ??= {};
return City(
id: json['id'],
name: json['name'],
slug: json['slug'],
tagLine: json['tagLine'],
description: json['description'],
bestTimeToVisit: json['bestTimeToVisit'],
priceRange: json['priceRange'],
individualTicketAmount: json['individualTicketAmount'],
cityCardTicketAmount: json['cityCardTicketAmount'],
id: (json['id'] as num?)?.toInt() ?? 0,
name: json['name']?.toString() ?? "",
slug: json['slug']?.toString() ?? "",
tagLine: json['tagLine']?.toString() ?? "",
description: json['description']?.toString() ?? "",
bestTimeToVisit: json['bestTimeToVisit']?.toString() ?? "",
priceRange: json['priceRange']?.toString() ?? "",
individualTicketAmount: json['individualTicketAmount'] ?? 0,
cityCardTicketAmount: json['cityCardTicketAmount'] ?? 0,
heroBanner: HeroBanner.fromJson(json['heroBanner']),
);
}
@@ -100,18 +110,20 @@ class City {
/// ---------- HERO BANNER ----------
class HeroBanner {
final String title;
final String image;
String title;
String image;
HeroBanner({
required this.title,
required this.image,
});
factory HeroBanner.fromJson(Map<String, dynamic> json) {
factory HeroBanner.fromJson(Map<String, dynamic>? json) {
json ??= {};
return HeroBanner(
title: json['title'],
image: json['image'],
title: json['title']?.toString() ?? "",
image: json['image']?.toString() ?? "",
);
}
@@ -123,25 +135,25 @@ class HeroBanner {
/// ---------- OFFER ----------
class Offer {
final int id;
final String title;
final String offerCode;
final String? description; // ✅ optional
final String? redemptionLink; // ✅ optional
final String websiteBannerImage;
final String mobileBannerImage;
final String passType;
final DateTime startDateTime;
final DateTime endDateTime;
final String offerStatus;
final bool applyToPasses;
int id;
String title;
String offerCode;
String description;
String redemptionLink;
String websiteBannerImage;
String mobileBannerImage;
String passType;
DateTime startDateTime;
DateTime endDateTime;
String offerStatus;
bool applyToPasses;
Offer({
required this.id,
required this.title,
required this.offerCode,
this.description,
this.redemptionLink,
required this.description,
required this.redemptionLink,
required this.websiteBannerImage,
required this.mobileBannerImage,
required this.passType,
@@ -151,20 +163,24 @@ class Offer {
required this.applyToPasses,
});
factory Offer.fromJson(Map<String, dynamic> json) {
factory Offer.fromJson(Map<String, dynamic>? json) {
json ??= {};
return Offer(
id: json['id'],
title: json['title'],
offerCode: json['offerCode'],
description: json['description'], // ✅
redemptionLink: json['redemptionLink'], // ✅
websiteBannerImage: json['websiteBannerImage'],
mobileBannerImage: json['mobileBannerImage'],
passType: json['passType'],
startDateTime: DateTime.parse(json['startDateTime']),
endDateTime: DateTime.parse(json['endDateTime']),
offerStatus: json['offerStatus'],
applyToPasses: json['applyToPasses'],
id: (json['id'] as num?)?.toInt() ?? 0,
title: json['title']?.toString() ?? "",
offerCode: json['offerCode']?.toString() ?? "",
description: json['description']?.toString() ?? "",
redemptionLink: json['redemptionLink']?.toString() ?? "",
websiteBannerImage: json['websiteBannerImage']?.toString() ?? "",
mobileBannerImage: json['mobileBannerImage']?.toString() ?? "",
passType: json['passType']?.toString() ?? "",
startDateTime: DateTime.tryParse(json['startDateTime'] ?? "") ??
DateTime.fromMillisecondsSinceEpoch(0),
endDateTime: DateTime.tryParse(json['endDateTime'] ?? "") ??
DateTime.fromMillisecondsSinceEpoch(0),
offerStatus: json['offerStatus']?.toString() ?? "",
applyToPasses: json['applyToPasses'] ?? false,
);
}
@@ -186,16 +202,16 @@ class Offer {
/// ---------- CARD PASS ----------
class CardPass {
final int id;
final String title;
final String description;
final int validityDuration;
final num adultPrice; // Changed from int to num
final num childPrice; // Changed from int to num
final int minNumber; // ✅ NEW
final int maxNumber; // ✅ NEW
final CardType cardType;
final List<Offer> offers;
int id;
String title;
String description;
int validityDuration;
num adultPrice;
num childPrice;
int minNumber;
int maxNumber;
CardType cardType;
List<Offer> offers;
CardPass({
required this.id,
@@ -210,20 +226,24 @@ class CardPass {
required this.offers,
});
factory CardPass.fromJson(Map<String, dynamic> json) {
factory CardPass.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CardPass(
id: json['id'],
title: json['title'],
description: json['description'],
validityDuration: json['validityDuration'],
adultPrice: json['adultPrice'],
childPrice: json['childPrice'],
minNumber: json['minNumber'], // ✅
maxNumber: json['maxNumber'], // ✅
id: (json['id'] as num?)?.toInt() ?? 0,
title: json['title']?.toString() ?? "",
description: json['description']?.toString() ?? "",
validityDuration: (json['validityDuration'] as num?)?.toInt() ?? 0,
adultPrice: json['adultPrice'] ?? 0,
childPrice: json['childPrice'] ?? 0,
minNumber: (json['minNumber'] as num?)?.toInt() ?? 0,
maxNumber: (json['maxNumber'] as num?)?.toInt() ?? 0,
cardType: CardType.fromJson(json['cardType']),
offers: List<Offer>.from(
json['offers'].map((x) => Offer.fromJson(x)),
),
offers: json['offers'] == null
? []
: List<Map<String, dynamic>>.from(json['offers'])
.map((e) => Offer.fromJson(e))
.toList(),
);
}
@@ -237,15 +257,15 @@ class CardPass {
"minNumber": minNumber,
"maxNumber": maxNumber,
"cardType": cardType.toJson(),
"offers": offers.map((x) => x.toJson()).toList(),
"offers": offers.map((e) => e.toJson()).toList(),
};
}
/// ---------- CARD TYPE ----------
class CardType {
final int id;
final String name;
final String displayName;
int id;
String name;
String displayName;
CardType({
required this.id,
@@ -253,11 +273,13 @@ class CardType {
required this.displayName,
});
factory CardType.fromJson(Map<String, dynamic> json) {
factory CardType.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CardType(
id: json['id'],
name: json['name'],
displayName: json['displayName'],
id: (json['id'] as num?)?.toInt() ?? 0,
name: json['name']?.toString() ?? "",
displayName: json['displayName']?.toString() ?? "",
);
}
@@ -270,27 +292,29 @@ class CardType {
/// ---------- ATTRACTION ----------
class Attraction {
final int id;
final String title;
final String slug;
final String thumbnail;
final num? startingFrom; // Changed from int? to num?
int id;
String title;
String slug;
String thumbnail;
num startingFrom;
Attraction({
required this.id,
required this.title,
required this.slug,
required this.thumbnail,
this.startingFrom,
required this.startingFrom,
});
factory Attraction.fromJson(Map<String, dynamic> json) {
factory Attraction.fromJson(Map<String, dynamic>? json) {
json ??= {};
return Attraction(
id: json['id'],
title: json['title'],
slug: json['slug'],
thumbnail: json['thumbnail'],
startingFrom: json['startingFrom'],
id: (json['id'] as num?)?.toInt() ?? 0,
title: json['title']?.toString() ?? "",
slug: json['slug']?.toString() ?? "",
thumbnail: json['thumbnail']?.toString() ?? "",
startingFrom: json['startingFrom'] ?? 0,
);
}
@@ -301,4 +325,4 @@ class Attraction {
"thumbnail": thumbnail,
"startingFrom": startingFrom,
};
}
}

View File

@@ -95,7 +95,7 @@ class PaymentCard extends StatelessWidget {
borderRadius: BorderRadius.circular(20.r),
),
child: CustomText(
text: "$cardDisplayName Card",
text: cardDisplayName,
size: 12.sp,
color: Colors.white,
weight: FontWeight.w500,

View File

@@ -8,18 +8,122 @@ class MyPassCartBloc extends Bloc<MyPassCartEvent, MyPassCartState> {
final MyPassCartRepository repository;
MyPassCartBloc({required this.repository}) : super(const MyPassCartInitial()) {
on<CheckLoginAndFetchEvent>(_onCheckLoginAndFetch);
on<FetchPassCartEvent>(_onFetchPassCart);
on<ClearPassCartEvent>(_onClearPassCart);
}
/// Handle fetching pass cart data
/// Handle checking login status and fetching cart data accordingly
Future<void> _onCheckLoginAndFetch(
CheckLoginAndFetchEvent event,
Emitter<MyPassCartState> emit,
) async {
try {
if (kDebugMode) {
print('🔍 [BLOC] Checking login status and fetching cart...');
}
emit(const MyPassCartLoading());
// Check if user is logged in
final isLoggedIn = await repository.isUserLoggedIn();
if (kDebugMode) {
print('🔐 [BLOC] User logged in: $isLoggedIn');
}
if (isLoggedIn) {
// User is logged in - fetch from API
if (kDebugMode) {
print('🌐 [BLOC] Fetching cart data from API...');
}
try {
final apiCartData = await repository.fetchMyPassesCart();
// Check if API data is empty
if (apiCartData.cartItems.isEmpty) {
if (kDebugMode) {
print('⚠️ [BLOC] API returned empty cart, checking local data...');
}
// Try to fetch from local if API is empty
final localCartData = await repository.fetchPassesCartByLocal();
if (localCartData != null) {
if (kDebugMode) {
print('✅ [BLOC] Using local cart data as fallback');
}
emit(MyPassCartLoaded(cartData: localCartData));
} else {
if (kDebugMode) {
print(' [BLOC] No local data available, cart is empty');
}
emit(const MyPassCartEmpty());
}
} else {
// API has cart items
if (kDebugMode) {
print('✅ [BLOC] API cart data loaded successfully with ${apiCartData.cartItems.length} items');
}
emit(MyPassCartApiLoaded(apiCartData: apiCartData));
}
} catch (apiError) {
if (kDebugMode) {
print('❌ [BLOC] API error: $apiError, trying local data...');
}
// API failed, try local data as fallback
final localCartData = await repository.fetchPassesCartByLocal();
if (localCartData != null) {
if (kDebugMode) {
print('✅ [BLOC] Using local cart data after API failure');
}
emit(MyPassCartLoaded(cartData: localCartData));
} else {
if (kDebugMode) {
print('❌ [BLOC] No local data available after API failure');
}
emit(MyPassCartError(message: 'Failed to load cart data: ${apiError.toString()}'));
}
}
} else {
// User is not logged in - fetch from local only
if (kDebugMode) {
print('📱 [BLOC] User not logged in, fetching from local storage...');
}
final localCartData = await repository.fetchPassesCartByLocal();
if (localCartData != null) {
if (kDebugMode) {
print('✅ [BLOC] Local cart data loaded successfully');
}
emit(MyPassCartLoaded(cartData: localCartData));
} else {
if (kDebugMode) {
print(' [BLOC] No local cart data available');
}
emit(const MyPassCartEmpty());
}
}
} catch (e) {
if (kDebugMode) {
print('❌ [BLOC] Error in CheckLoginAndFetch: $e');
}
emit(MyPassCartError(message: e.toString()));
}
}
/// Handle fetching pass cart data from local storage
Future<void> _onFetchPassCart(
FetchPassCartEvent event,
Emitter<MyPassCartState> emit,
) async {
try {
if (kDebugMode) {
print('🔄 [BLOC] Fetching pass cart...');
print('📄 [BLOC] Fetching pass cart from local...');
}
emit(const MyPassCartLoading());
@@ -52,7 +156,7 @@ class MyPassCartBloc extends Bloc<MyPassCartEvent, MyPassCartState> {
) async {
try {
if (kDebugMode) {
print('🔄 [BLOC] Clearing pass cart...');
print('📄 [BLOC] Clearing pass cart...');
}
// You can add clearPassCart method to repository if needed

View File

@@ -7,6 +7,14 @@ abstract class MyPassCartEvent extends Equatable {
List<Object?> get props => [];
}
/// Event to check login status and fetch pass cart data accordingly
/// - If logged in: fetch from API
/// - If not logged in: fetch from local
/// - If API returns empty and local data exists: use local data
class CheckLoginAndFetchEvent extends MyPassCartEvent {
const CheckLoginAndFetchEvent();
}
/// Event to fetch pass cart data from local database
class FetchPassCartEvent extends MyPassCartEvent {
const FetchPassCartEvent();

View File

@@ -1,5 +1,7 @@
import 'package:equatable/equatable.dart';
import '../../model/my_passes_cart_mode.dart';
abstract class MyPassCartState extends Equatable {
const MyPassCartState();
@@ -17,7 +19,7 @@ class MyPassCartLoading extends MyPassCartState {
const MyPassCartLoading();
}
/// Loaded state with cart data
/// Loaded state with cart data from local storage
class MyPassCartLoaded extends MyPassCartState {
final Map<String, dynamic> cartData;
@@ -27,6 +29,16 @@ class MyPassCartLoaded extends MyPassCartState {
List<Object?> get props => [cartData];
}
/// Loaded state with cart data from API
class MyPassCartApiLoaded extends MyPassCartState {
final MyPassesCartModel apiCartData;
const MyPassCartApiLoaded({required this.apiCartData});
@override
List<Object?> get props => [apiCartData];
}
/// Empty state when no cart data exists
class MyPassCartEmpty extends MyPassCartState {
const MyPassCartEmpty();

View File

@@ -1,40 +1,40 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../model/pass_model.dart';
abstract class PassEvent {}
class LoadPasses extends PassEvent {}
abstract class PassState {}
class PassLoading extends PassState {}
class PassLoaded extends PassState {
final List<PassModel> passes;
final double subtotal;
final double discountPercent;
final double total;
PassLoaded(this.passes, this.subtotal, this.discountPercent, this.total);
}
class PassBloc extends Bloc<PassEvent, PassState> {
PassBloc() : super(PassLoading()) {
on<LoadPasses>((event, emit) {
final passes = [
PassModel(
title: "Melbourne",
imageUrl: "assets/images/city_melbourne.png",
duration: "2 days",
adults: 3,
kids: 3,
quantity: 2,
price: 49.50,
discount: 7.2,
),
];
final subtotal = passes.fold(0.0, (sum, item) => sum + item.price);
final discountPercent = passes.first.discount;
final total = subtotal - (subtotal * discountPercent / 100);
emit(PassLoaded(passes, subtotal, discountPercent, total));
});
}
}
// import 'package:flutter_bloc/flutter_bloc.dart';
// import '../model/pass_model.dart';
//
// abstract class PassEvent {}
// class LoadPasses extends PassEvent {}
//
// abstract class PassState {}
// class PassLoading extends PassState {}
// class PassLoaded extends PassState {
// final List<PassModel> passes;
// final double subtotal;
// final double discountPercent;
// final double total;
//
// PassLoaded(this.passes, this.subtotal, this.discountPercent, this.total);
// }
//
// class PassBloc extends Bloc<PassEvent, PassState> {
// PassBloc() : super(PassLoading()) {
// on<LoadPasses>((event, emit) {
// final passes = [
// PassModel(
// title: "Melbourne",
// imageUrl: "assets/images/city_melbourne.png",
// duration: "2 days",
// adults: 3,
// kids: 3,
// quantity: 2,
// price: 49.50,
// discount: 7.2,
// ),
// ];
//
// final subtotal = passes.fold(0.0, (sum, item) => sum + item.price);
// final discountPercent = passes.first.discount;
// final total = subtotal - (subtotal * discountPercent / 100);
// emit(PassLoaded(passes, subtotal, discountPercent, total));
// });
// }
// }

View File

@@ -0,0 +1,207 @@
import 'dart:convert';
/// ---------- MAIN RESPONSE ----------
MyPassesCartModel myPassesCartModelFromJson(String str) =>
MyPassesCartModel.fromJson(json.decode(str));
String myPassesCartModelToJson(MyPassesCartModel data) =>
json.encode(data.toJson());
class MyPassesCartModel {
CartCity city;
List<CartItem> cartItems;
MyPassesCartModel({
required this.city,
required this.cartItems,
});
factory MyPassesCartModel.fromJson(Map<String, dynamic>? json) {
json ??= {};
return MyPassesCartModel(
city: CartCity.fromJson(json['city']),
cartItems: json['cartItems'] == null
? []
: List<Map<String, dynamic>>.from(json['cartItems'])
.map((e) => CartItem.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
"city": city.toJson(),
"cartItems": cartItems.map((e) => e.toJson()).toList(),
};
}
/// ---------- CITY ----------
class CartCity {
int id;
String name;
CartCity({
required this.id,
required this.name,
});
factory CartCity.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CartCity(
id: (json['id'] as num?)?.toInt() ?? 0,
name: json['name']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
};
}
/// ---------- CART ITEM ----------
class CartItem {
int id;
String bookingNumber;
String cardMode;
int noOfDays;
int noOfAttractions;
int totalAdult;
int totalChild;
num baseAmount;
num totalTaxAmount;
num totalAmount;
String bookingStatus;
bool isForSelf;
String recipientFirstName;
String recipientLastName;
String recipientEmail;
String recipientPhone;
String recipientCity;
String recipientCountry;
String giftMessage;
bool isPaymentRequired;
int couponXid;
num couponDiscountAmount;
num couponDiscountPercent;
String paymentStatus;
String createdAt;
ItemCity city;
CartItem({
required this.id,
required this.bookingNumber,
required this.cardMode,
required this.noOfDays,
required this.noOfAttractions,
required this.totalAdult,
required this.totalChild,
required this.baseAmount,
required this.totalTaxAmount,
required this.totalAmount,
required this.bookingStatus,
required this.isForSelf,
required this.recipientFirstName,
required this.recipientLastName,
required this.recipientEmail,
required this.recipientPhone,
required this.recipientCity,
required this.recipientCountry,
required this.giftMessage,
required this.isPaymentRequired,
required this.couponXid,
required this.couponDiscountAmount,
required this.couponDiscountPercent,
required this.paymentStatus,
required this.createdAt,
required this.city,
});
factory CartItem.fromJson(Map<String, dynamic>? json) {
json ??= {};
return CartItem(
id: (json['id'] as num?)?.toInt() ?? 0,
bookingNumber: json['bookingNumber']?.toString() ?? "",
cardMode: json['cardMode']?.toString() ?? "",
noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0,
noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0,
totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0,
totalChild: (json['totalChild'] as num?)?.toInt() ?? 0,
baseAmount: json['baseAmount'] ?? 0,
totalTaxAmount: json['totalTaxAmount'] ?? 0,
totalAmount: json['totalAmount'] ?? 0,
bookingStatus: json['bookingStatus']?.toString() ?? "",
isForSelf: json['isForSelf'] ?? false,
recipientFirstName: json['recipientFirstName']?.toString() ?? "",
recipientLastName: json['recipientLastName']?.toString() ?? "",
recipientEmail: json['recipientEmail']?.toString() ?? "",
recipientPhone: json['recipientPhone']?.toString() ?? "",
recipientCity: json['recipientCity']?.toString() ?? "",
recipientCountry: json['recipientCountry']?.toString() ?? "",
giftMessage: json['giftMessage']?.toString() ?? "",
isPaymentRequired: json['isPaymentRequired'] ?? false,
couponXid: (json['couponXid'] as num?)?.toInt() ?? 0,
couponDiscountAmount: json['couponDiscountAmount'] ?? 0,
couponDiscountPercent: json['couponDiscountPercent'] ?? 0,
paymentStatus: json['paymentStatus']?.toString() ?? "",
createdAt: json['createdAt']?.toString() ?? "",
city: ItemCity.fromJson(json['city']),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"bookingNumber": bookingNumber,
"cardMode": cardMode,
"noOfDays": noOfDays,
"noOfAttractions": noOfAttractions,
"totalAdult": totalAdult,
"totalChild": totalChild,
"baseAmount": baseAmount,
"totalTaxAmount": totalTaxAmount,
"totalAmount": totalAmount,
"bookingStatus": bookingStatus,
"isForSelf": isForSelf,
"recipientFirstName": recipientFirstName,
"recipientLastName": recipientLastName,
"recipientEmail": recipientEmail,
"recipientPhone": recipientPhone,
"recipientCity": recipientCity,
"recipientCountry": recipientCountry,
"giftMessage": giftMessage,
"isPaymentRequired": isPaymentRequired,
"couponXid": couponXid,
"couponDiscountAmount": couponDiscountAmount,
"couponDiscountPercent": couponDiscountPercent,
"paymentStatus": paymentStatus,
"createdAt": createdAt,
"city": city.toJson(),
};
}
/// ---------- ITEM CITY ----------
class ItemCity {
int id;
String cityName;
ItemCity({
required this.id,
required this.cityName,
});
factory ItemCity.fromJson(Map<String, dynamic>? json) {
json ??= {};
return ItemCity(
id: (json['id'] as num?)?.toInt() ?? 0,
cityName: json['cityName']?.toString() ?? "",
);
}
Map<String, dynamic> toJson() => {
"id": id,
"cityName": cityName,
};
}

View File

@@ -1,18 +1,39 @@
import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
import '../model/my_passes_cart_mode.dart';
class MyPassCartRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Check if user is logged in
Future<bool> isUserLoggedIn() async {
try {
final isLogin = await LocalPreference.getLogin();
if (kDebugMode) {
print('🔐 [REPO] User login status: $isLogin');
}
return isLogin;
} catch (e) {
if (kDebugMode) {
print('❌ [REPO] Error checking login status: $e');
}
return false;
}
}
/// Fetch pass cart data from local database
Future<Map<String, dynamic>?> fetchPassesCartByLocal() async {
try {
if (kDebugMode) {
print('🔄 [REPO] Fetching pass cart from local database...');
print('📄 [REPO] Fetching pass cart from local database...');
}
final passCartData = await LocalPreference.getPassCart();
if (passCartData != null) {
if (kDebugMode) {
print('✅ [REPO] Pass cart retrieved successfully');
@@ -32,4 +53,31 @@ class MyPassCartRepository {
rethrow;
}
}
/// Fetch pass cart data from API
Future<MyPassesCartModel> fetchMyPassesCart() async {
try {
if (kDebugMode) {
print('🌐 [REPO] Fetching pass cart from API...');
}
final cityID = await LocalPreference.getSelectedCityId();
final response = await _apiService.getApi(
url: '${ApiUrls.myPassesCart}?cityXid=$cityID',
);
if (kDebugMode) {
print('✅ [REPO] API response received');
}
return MyPassesCartModel.fromJson(response.data);
} catch (e) {
if (kDebugMode) {
print('❌ [REPO] Error fetching pass cart from API: $e');
}
rethrow;
}
}
}

View File

@@ -6,6 +6,8 @@ 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 '../../add_details/add_details_view.dart';
import '../../checkout/widget/pass_purchase_details_bottomsheet.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../../common_packages/common_app_texts.dart';
import '../../localPreference/local_preference.dart';
@@ -24,12 +26,13 @@ class _MyPassesPageState extends State<MyPassesPage> {
// For coupon/discount management
String? appliedCouponCode;
double discountPercentage = 0.0;
bool isPurchaseDetailsConfirmed = false;
@override
void initState() {
super.initState();
// Fetch cart data when page loads
context.read<MyPassCartBloc>().add(const FetchPassCartEvent());
context.read<MyPassCartBloc>().add(const CheckLoginAndFetchEvent());
}
@override
@@ -38,36 +41,42 @@ class _MyPassesPageState extends State<MyPassesPage> {
builder: (context, state) {
if (state is MyPassCartLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is MyPassCartLoaded) {
final cartData = state.cartData;
}
// Extract data from cart
final String cityName = cartData['city_name'] as String? ?? '';
final String heroImage = cartData['hero_image'] as String? ?? '';
final String cardTypeName = cartData['card_type_name'] as String? ?? '';
final String cardDisplayName = cartData['card_display_name'] as String? ?? '';
final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF;
final int adultCount = cartData['adult_count'] as int? ?? 0;
final int childCount = cartData['child_count'] as int? ?? 0;
final double adultPrice = (cartData['adult_price'] as num?)?.toDouble() ?? 0.0;
final double childPrice = (cartData['child_price'] as num?)?.toDouble() ?? 0.0;
final int validityDuration = cartData['validity_duration'] as int? ?? 0;
final double totalPrice = (cartData['total_price'] as num?)?.toDouble() ?? 0.0;
final String? description = cartData['description'] as String?;
// ========== HANDLE API DATA (LOGGED IN USER) ==========
else if (state is MyPassCartApiLoaded) {
final apiCartData = state.apiCartData;
if (apiCartData.cartItems.isEmpty) {
return const Center(child: Text('Your cart is empty'));
}
// Get first cart item (you can modify to handle multiple items)
final cartItem = apiCartData.cartItems.first;
// Extract data from API cart item
final String cityName = cartItem.city.cityName;
final String heroImage = ''; // API doesn't have hero_image
final String cardTypeName = cartItem.cardMode;
final String cardDisplayName = cartItem.cardMode;
final int themeColor = 0xFFF95FAF;
final int adultCount = cartItem.totalAdult;
final int childCount = cartItem.totalChild;
final int validityDuration = cartItem.noOfDays;
final double totalPrice = cartItem.totalAmount.toDouble();
// Calculate pricing
final double subtotal = totalPrice;
final double discountAmount = subtotal * (discountPercentage / 100);
final double taxRate = 0.05; // 5% tax
final double subtotal = cartItem.baseAmount.toDouble();
final double discountAmount = cartItem.couponDiscountAmount.toDouble();
final double totalBeforeTax = subtotal - discountAmount;
final double taxAmount = totalBeforeTax * taxRate;
final double finalTotal = totalBeforeTax + taxAmount;
final double taxAmount = cartItem.totalTaxAmount.toDouble();
final double finalTotal = totalPrice;
// Determine if unlimited card
final bool isUnlimitedCard = cardTypeName == "unlimited_card";
final bool isUnlimitedCard = cardTypeName.toLowerCase().contains("unlimited");
final String validityLabel = isUnlimitedCard
? "$validityDuration Days"
: "$validityDuration Attractions";
: "${cartItem.noOfAttractions} Attractions";
return Column(
children: [
@@ -90,23 +99,7 @@ class _MyPassesPageState extends State<MyPassesPage> {
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: heroImage.isNotEmpty
? Image.network(
heroImage,
width: 105.w,
height: 123.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,
);
},
)
: Image.asset(
child: Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
@@ -133,8 +126,460 @@ class _MyPassesPageState extends State<MyPassesPage> {
SizedBox(
width: MediaQuery.of(context).size.width * .5,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Image.asset(
'assets/icons/adult.png',
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text: "$adultCount ${adultCount == 1 ? 'adult' : 'adults'}",
color: Color(0xFF8E8E8E),
size: 12.sp,
),
],
),
Row(
children: [
Image.asset(
'assets/icons/qty.png',
scale: 4,
),
SizedBox(width: 4.w),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "Qty:",
style: TextStyle(
color: Color(0xFF8E8E8E),
fontSize: 12.sp,
),
),
TextSpan(
text: " ${adultCount + childCount}",
style: TextStyle(
color: Color(0xFF000000),
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
],
),
),
SizedBox(height: 5.h),
Row(
children: [
Image.asset(
"assets/icons/kid.png",
scale: 4,
),
SizedBox(width: 4.w),
CustomText(
text: "$childCount ${childCount == 1 ? 'Kid' : 'Kids'}",
color: Color(0xFF8E8E8E),
size: 12.sp,
),
SizedBox(width: 53.w),
CustomText(
text: "\$${totalPrice.toStringAsFixed(2)}",
size: 24.sp,
weight: FontWeight.w500,
color: Color(0xFFF95F62),
),
],
),
],
),
],
),
Container(
width: 35.w,
height: 123.h,
decoration: BoxDecoration(
color: Color(themeColor),
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(8.r),
topRight: Radius.circular(8.r),
),
),
child: RotatedBox(
quarterTurns: -1,
child: Center(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: "$cardDisplayName ",
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
),
],
),
),
),
),
),
],
),
),
SizedBox(height: 15.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 12.h,
),
decoration: BoxDecoration(
color: Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: Color(0xFFBB474A).withOpacity(0.4),
width: 0.8,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: (cartItem.couponDiscountAmount > 0 || appliedCouponCode != null)
? "Coupon Applied (${(cartItem.couponDiscountAmount > 0 ? cartItem.couponDiscountPercent : discountPercentage).toStringAsFixed(0)}% off)"
: "Get 10% off on your first trip",
color: Color(0xFF262626),
size: 14.sp,
),
SizedBox(height: 7.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => AllCouponsBottomsheet(),
);
},
child: CustomText(
text: "View all coupons",
color: Color(0xFFF95F62),
size: 12,
),
),
SizedBox(width: 3.w),
Icon(Icons.arrow_right, color: Color(0xFFF95F62)),
],
),
],
),
const Spacer(),
// Only show Apply/Remove button if no API coupon is applied
if (cartItem.couponDiscountAmount == 0)
GestureDetector(
onTap: () {
setState(() {
if (appliedCouponCode == null) {
appliedCouponCode = "FIRST10";
discountPercentage = 10.0;
} else {
appliedCouponCode = null;
discountPercentage = 0.0;
}
});
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 10.h,
),
decoration: BoxDecoration(
border: Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(8.r),
),
child: CustomText(
text: appliedCouponCode != null ? "Remove" : "Apply",
color: Color(0xFFF95F62),
size: 14.sp,
),
),
),
],
),
),
SizedBox(height: 15.h),
DashedDivider(
color: Color(0xFFACACAC),
thickness: 1.h,
dashLength: 4,
dashSpace: 4,
),
SizedBox(height: 10.h),
// Calculate final discount and totals
Builder(
builder: (context) {
// Use API discount if available, otherwise use local discount
final effectiveDiscountAmount = cartItem.couponDiscountAmount > 0
? cartItem.couponDiscountAmount
: (subtotal * (discountPercentage / 100));
final effectiveDiscountPercent = cartItem.couponDiscountAmount > 0
? cartItem.couponDiscountPercent
: discountPercentage;
// Calculate tax on subtotal after discount
final subtotalAfterDiscount = subtotal - effectiveDiscountAmount;
final calculatedTax = subtotalAfterDiscount * 0.01; // 1% tax
final calculatedTotal = subtotalAfterDiscount + calculatedTax;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Subtotal", size: 14.sp),
CustomText(
text: "\$${subtotal.toStringAsFixed(2)}",
size: 14.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 14.h),
if (effectiveDiscountAmount > 0) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomText(text: "Discount", size: 14.sp),
CustomText(
text: "-\$${effectiveDiscountAmount.toStringAsFixed(2)} (${effectiveDiscountPercent.toStringAsFixed(0)}%)",
size: 14.sp,
weight: FontWeight.w500,
color: Colors.green,
),
],
),
SizedBox(height: 14.h),
],
DashedDivider(
color: Color(0xFFACACAC),
thickness: 1.h,
dashLength: 4,
dashSpace: 4,
),
SizedBox(height: 10.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: 'Total', size: 14.sp),
SizedBox(height: 4.h),
CustomText(
text: "Including \$${calculatedTax.toStringAsFixed(2)} in taxes",
size: 12.sp,
color: Colors.black.withOpacity(0.6),
),
],
),
),
CustomText(
text: "\$${calculatedTotal.toStringAsFixed(2)}",
size: 24.sp,
weight: FontWeight.w500,
),
],
),
SizedBox(height: 150.h),
FutureBuilder<bool>(
future: LocalPreference.getLogin(),
builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false;
return CustomFilledButton(
onTap: () async {
if (isLoggedIn) {
if (isPurchaseDetailsConfirmed) {
print("✅ Ready to pay: \$${calculatedTotal.toStringAsFixed(2)}");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Payment integration pending'),
backgroundColor: Colors.orange,
),
);
} else {
final result = await PassPurchaseBottomSheet.show(
context,
bookingId: cartItem.id,
);
if (result == 'success') {
setState(() {
isPurchaseDetailsConfirmed = true;
});
} else if (result == 'gift') {
final giftResult = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (_) => AddDetailsView(bookingId: cartItem.id),
),
);
if (giftResult == 'success') {
setState(() {
isPurchaseDetailsConfirmed = true;
});
}
}
}
} else {
Navigator.pop(context);
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
}
},
width: double.infinity,
label: isLoggedIn
? (isPurchaseDetailsConfirmed
? "Pay \$${calculatedTotal.toStringAsFixed(2)}"
: "Checkout")
: "Login to Checkout",
);
},
),
SizedBox(height: 25.h),
],
);
},
),
],
);
}
// ========== HANDLE LOCAL DATA (NOT LOGGED IN) ==========
else if (state is MyPassCartLoaded) {
final cartData = state.cartData;
// Extract data from cart
final String cityName = cartData['city_name'] as String? ?? '';
final String heroImage = cartData['hero_image'] as String? ?? '';
final String cardTypeName = cartData['card_type_name'] as String? ?? '';
final String cardDisplayName = cartData['card_display_name'] as String? ?? '';
final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF;
final int adultCount = cartData['adult_count'] as int? ?? 0;
final int childCount = cartData['child_count'] as int? ?? 0;
final double adultPrice = (cartData['adult_price'] as num?)?.toDouble() ?? 0.0;
final double childPrice = (cartData['child_price'] as num?)?.toDouble() ?? 0.0;
final int validityDuration = cartData['validity_duration'] as int? ?? 0;
final double totalPrice = (cartData['total_price'] as num?)?.toDouble() ?? 0.0;
final String? description = cartData['description'] as String?;
// Calculate pricing
final double subtotal = totalPrice;
final double discountAmount = subtotal * (discountPercentage / 100);
final double totalBeforeTax = subtotal - discountAmount;
final double taxAmount = 2;
final double finalTotal = totalBeforeTax + taxAmount;
// Determine if unlimited card
final bool isUnlimitedCard = cardTypeName == "unlimited_card";
final String validityLabel = isUnlimitedCard
? "$validityDuration Days"
: "$validityDuration Attractions";
return Column(
children: [
SizedBox(height: 22.h),
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Color(themeColor).withOpacity(0.2),
),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: heroImage.isNotEmpty
? Image.network(
heroImage,
width: 105.w,
height: 123.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,
);
},
)
: Image.asset(
"assets/images/card_banner.png",
scale: 4,
width: 105.w,
height: 123.h,
fit: BoxFit.cover,
),
),
SizedBox(width: 6.66.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(
text: cityName,
weight: FontWeight.w500,
size: 16.sp,
),
SizedBox(height: 5.h),
CustomText(
text: validityLabel,
color: Color(0xFF8E8E8E),
size: 12.sp,
),
SizedBox(height: 5.h),
SizedBox(
width: MediaQuery.of(context).size.width * .5,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
@@ -232,13 +677,6 @@ class _MyPassesPageState extends State<MyPassesPage> {
fontSize: 16.sp,
),
),
// TextSpan(
// text: "Card",
// style: TextStyle(
// color: Colors.white,
// fontSize: 12.sp,
// ),
// ),
],
),
),
@@ -402,42 +840,10 @@ class _MyPassesPageState extends State<MyPassesPage> {
],
),
SizedBox(height: 150.h),
// FutureBuilder for login check
FutureBuilder<bool>(
future: LocalPreference.getLogin(),
builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false;
return CustomFilledButton(
onTap: () {
if (!isLoggedIn) {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
} else {
// Handle checkout logic for logged in user
// You can navigate to checkout or payment screen
print("✅ User is logged in, proceed to checkout");
}
},
width: double.infinity,
label: isLoggedIn ? "Checkout" : "Login to Checkout",
);
},
),
SizedBox(height: 25.h),
],
);
} else if (state is MyPassCartEmpty) {
}
else if (state is MyPassCartEmpty) {
return Center(
child: Column(
children: [

View File

@@ -197,6 +197,15 @@ class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
ConfirmPaymentEvent event,
Emitter<CheckoutState> emit,
) async {
// 🔒 GUARD: Prevent duplicate confirmation calls
if (state is CheckoutCouponsLoadedState) {
final currentState = state as CheckoutCouponsLoadedState;
if (currentState.hasConfirmationBeenSent) {
print('⚠️ [CHECKOUT BLOC] Payment confirmation already sent. Ignoring duplicate call.');
return;
}
}
// Show loading state
if (state is CheckoutCouponsLoadedState) {
final currentState = state as CheckoutCouponsLoadedState;
@@ -204,6 +213,7 @@ class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
isConfirmingPayment: true,
confirmationError: null,
isPaymentConfirmed: false,
hasConfirmationBeenSent: true, // 🔒 Mark as sent
));
} else {
emit(CheckoutPaymentConfirmingState());
@@ -239,6 +249,7 @@ class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
isConfirmingPayment: false,
isPaymentConfirmed: false,
confirmationError: e.toString(),
hasConfirmationBeenSent: false, // 🔓 Reset on error to allow retry
));
} else {
emit(CheckoutPaymentConfirmationErrorState(

View File

@@ -25,6 +25,7 @@ class CheckoutCouponsLoadedState extends CheckoutState {
final bool isPaymentConfirmed;
final String? confirmationError;
final Map<String, dynamic>? bookingDetails; // Full booking response after confirmation
final bool hasConfirmationBeenSent; // 🔒 Prevent duplicate confirmation calls
CheckoutCouponsLoadedState({
required this.coupons,
@@ -39,6 +40,7 @@ class CheckoutCouponsLoadedState extends CheckoutState {
this.isPaymentConfirmed = false,
this.confirmationError,
this.bookingDetails,
this.hasConfirmationBeenSent = false,
});
CheckoutCouponsLoadedState copyWith({
@@ -56,6 +58,7 @@ class CheckoutCouponsLoadedState extends CheckoutState {
String? confirmationError,
bool clearClientSecret = false,
Map<String, dynamic>? bookingDetails,
bool? hasConfirmationBeenSent,
}) {
return CheckoutCouponsLoadedState(
coupons: coupons ?? this.coupons,
@@ -70,6 +73,7 @@ class CheckoutCouponsLoadedState extends CheckoutState {
confirmationError: confirmationError,
clientSecret: clearClientSecret ? null : (clientSecret ?? this.clientSecret),
bookingDetails: bookingDetails ?? this.bookingDetails,
hasConfirmationBeenSent: hasConfirmationBeenSent ?? this.hasConfirmationBeenSent,
);
}
}

View File

@@ -34,12 +34,12 @@ class PassPurchaseDetailsRepository {
// Request body
final requestBody = {
'isForSelf': isForSelf,
'recipientName': recipientFirstName ?? '',
// 'recipientLastName': recipientLastName ?? '',
'recipientFirstName': recipientFirstName ?? '',
'recipientLastName': recipientLastName ?? '',
'recipientEmail': recipientEmail ?? '',
'recipientPhone': recipientPhone ?? '',
// 'city': city ?? '',
// 'country': country ?? '',
'recipientCity': city ?? '',
'recipientCountry': country ?? '',
};
log('📦 Request Body: $requestBody');

View File

@@ -13,7 +13,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../StripePayment/view/stripe_payment.dart';
import '../../add_details/add_details_view.dart';
import '../../buy_a_pass/models/checkout_model.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
import '../../localPreference/local_preference.dart';
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
import '../../my_pass/blocs/myPasses/my_passes_event.dart';
import '../widget/pass_purchase_details_bottomsheet.dart';
import '../repository/all_coupons_repository.dart';
import '../repository/checkout_repository.dart';
@@ -101,7 +104,7 @@ class _CheckoutViewState extends State<CheckoutView> {
}
}
class _CheckoutContent extends StatelessWidget {
class _CheckoutContent extends StatefulWidget {
final CheckoutData checkoutData;
final int bookingId;
final bool isPurchaseDetailsConfirmed;
@@ -114,6 +117,12 @@ class _CheckoutContent extends StatelessWidget {
required this.onPurchaseDetailsChanged,
});
@override
State<_CheckoutContent> createState() => _CheckoutContentState();
}
class _CheckoutContentState extends State<_CheckoutContent> {
bool _hasHandledPaymentResult = false;
/// 🆕 Handle payment flow with client secret
/// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async {
@@ -165,7 +174,10 @@ class _CheckoutContent extends StatelessWidget {
await Future.delayed(const Duration(milliseconds: 500));
// Navigate to home after successful payment
Navigator.of(context).popUntil((route) => route.isFirst);
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Payment confirmed successfully!'),
@@ -181,15 +193,21 @@ class _CheckoutContent extends StatelessWidget {
listener: (context, state) {
// 🆕 Listen for payment initiation success
if (state is CheckoutCouponsLoadedState) {
// Check if clientSecret is available (payment initiated)
if (state.clientSecret != null && state.clientSecret!.isNotEmpty) {
// 🔒 CHECK: Prevent duplicate payment flow initiation
if (state.clientSecret != null &&
state.clientSecret!.isNotEmpty &&
!_hasHandledPaymentResult) { // 🔒 Only proceed if not already handled
// 🔒 MARK: Set flag immediately to prevent re-entry
_hasHandledPaymentResult = true;
// ✅ Calculate finalTotal here
double discountPercentage = 0.0;
if (state.appliedCoupon != null) {
discountPercentage = state.appliedCoupon!.discountPercent.toDouble();
}
final num subtotal = checkoutData.totalPrice;
final num subtotal = widget.checkoutData.totalPrice; // Changed to widget.
final double discountAmount = subtotal * (discountPercentage / 100);
final double totalBeforeTax = subtotal - discountAmount;
final double taxAmount = 2;
@@ -200,7 +218,7 @@ class _CheckoutContent extends StatelessWidget {
_handlePaymentFlow(
context,
state.clientSecret!,
state.bookingId ?? bookingId,
state.bookingId ?? widget.bookingId,
finalTotal, // ✅ Pass the calculated finalTotal
);
});
@@ -263,7 +281,7 @@ class _CheckoutContent extends StatelessWidget {
isConfirmingPayment = state.isConfirmingPayment;
}
final num subtotal = checkoutData.totalPrice;
final num subtotal = widget.checkoutData.totalPrice;
final double discountAmount = subtotal * (discountPercentage / 100);
// final double taxRate = 0.05; // 5% tax
final double totalBeforeTax = subtotal - discountAmount;
@@ -307,7 +325,7 @@ class _CheckoutContent extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: checkoutData.themeColor.withOpacity(0.2),
color: widget.checkoutData.themeColor.withOpacity(0.2),
),
borderRadius: BorderRadius.circular(8.r),
),
@@ -322,9 +340,9 @@ class _CheckoutContent extends StatelessWidget {
topLeft: Radius.circular(8.r),
bottomLeft: Radius.circular(8.r),
),
child: checkoutData.heroImage.isNotEmpty
child: widget.checkoutData.heroImage.isNotEmpty
? Image.network(
checkoutData.heroImage,
widget.checkoutData.heroImage,
width: 105.w,
height: 140.h,
fit: BoxFit.cover,
@@ -344,7 +362,7 @@ class _CheckoutContent extends StatelessWidget {
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2,
color: checkoutData.themeColor,
color: widget.checkoutData.themeColor,
),
),
),
@@ -363,7 +381,7 @@ class _CheckoutContent extends StatelessWidget {
children: [
// City Name
CustomText(
text: checkoutData.cityName,
text: widget.checkoutData.cityName,
weight: FontWeight.w500,
size: 16.sp,
),
@@ -371,7 +389,7 @@ class _CheckoutContent extends StatelessWidget {
// Validity (Days or Attractions)
CustomText(
text: checkoutData.validityLabel,
text: widget.checkoutData.validityLabel,
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
@@ -385,7 +403,7 @@ class _CheckoutContent extends StatelessWidget {
MainAxisAlignment.spaceBetween,
children: [
// Adults
if (checkoutData.adultCount > 0)
if (widget.checkoutData.adultCount > 0)
Row(
children: [
Image.asset(
@@ -395,7 +413,7 @@ class _CheckoutContent extends StatelessWidget {
SizedBox(width: 4.w),
CustomText(
text:
"${checkoutData.adultCount} adult${checkoutData.adultCount > 1 ? 's' : ''}",
"${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
@@ -408,7 +426,7 @@ class _CheckoutContent extends StatelessWidget {
Row(
children: [
// Children
if (checkoutData.childCount > 0) ...[
if (widget.checkoutData.childCount > 0) ...[
Image.asset(
"assets/icons/kid.png",
scale: 4,
@@ -416,7 +434,7 @@ class _CheckoutContent extends StatelessWidget {
SizedBox(width: 4.w),
CustomText(
text:
"${checkoutData.childCount} Kid${checkoutData.childCount > 1 ? 's' : ''}",
"${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}",
color: const Color(0xFF8E8E8E),
size: 12.sp,
),
@@ -429,7 +447,7 @@ class _CheckoutContent extends StatelessWidget {
text: "\$${subtotal.toStringAsFixed(2)}",
size: 24.sp,
weight: FontWeight.w500,
color: checkoutData.themeColor,
color: widget.checkoutData.themeColor,
),
],
),
@@ -443,7 +461,7 @@ class _CheckoutContent extends StatelessWidget {
width: 35.w,
height: 140.h,
decoration: BoxDecoration(
color: checkoutData.themeColor,
color: widget.checkoutData.themeColor,
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(8.r),
topRight: Radius.circular(8.r),
@@ -453,7 +471,7 @@ class _CheckoutContent extends StatelessWidget {
quarterTurns: -1,
child: Center(
child: Text(
checkoutData.cardDisplayName,
widget.checkoutData.cardDisplayName,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
@@ -550,7 +568,7 @@ class _CheckoutContent extends StatelessWidget {
);
context.read<CheckoutBloc>().add(
ApplyCouponToBackendEvent(
bookingId: bookingId,
bookingId: widget.bookingId,
couponCode: coupon.couponCode,
),
);
@@ -586,13 +604,13 @@ class _CheckoutContent extends StatelessWidget {
onTap: () {
if (appliedCoupon != null) {
context.read<CheckoutBloc>().add(
RemoveCouponEvent(bookingId: bookingId),
RemoveCouponEvent(bookingId: widget.bookingId),
);
} else if (state.coupons.isNotEmpty) {
// Apply coupon via backend API
context.read<CheckoutBloc>().add(
ApplyCouponToBackendEvent(
bookingId: bookingId,
bookingId: widget.bookingId,
couponCode: state.coupons[0].couponCode,
),
);
@@ -717,32 +735,32 @@ class _CheckoutContent extends StatelessWidget {
? () {} // Empty callback when disabled
: () async {
if (isLoggedIn) {
if (isPurchaseDetailsConfirmed) {
if (widget.isPurchaseDetailsConfirmed) {
// 🆕 Initiate payment flow
context.read<CheckoutBloc>().add(
InitiatePaymentEvent(
bookingId: bookingId),
bookingId: widget.bookingId),
);
} else {
// Show purchase details bottom sheet
final result = await PassPurchaseBottomSheet.show(
context, bookingId: bookingId);
context, bookingId: widget.bookingId);
// ✅ Handle 'Buy for Myself' - user submitted details
if (result == 'success') {
onPurchaseDetailsChanged(true);
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: bookingId),
builder: (_) => AddDetailsView(bookingId: widget.bookingId),
),
);
// If gift details were successfully submitted, mark as confirmed
if (giftResult == 'success') {
onPurchaseDetailsChanged(true);
widget.onPurchaseDetailsChanged(true);
}
}
}
@@ -764,7 +782,7 @@ class _CheckoutContent extends StatelessWidget {
},
width: double.infinity,
label: isLoggedIn
? (isPurchaseDetailsConfirmed
? (widget.isPurchaseDetailsConfirmed
? (isInitiatingPayment || isConfirmingPayment
? "Processing..."
: "Pay \$${finalTotal.toStringAsFixed(2)}")

View File

@@ -47,12 +47,12 @@ class _PassPurchaseContent extends StatelessWidget {
Navigator.of(context).pop('success');
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Details submitted successfully!'),
backgroundColor: Color(0xffF95F62),
),
);
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Details submitted successfully!'),
// backgroundColor: Color(0xffF95F62),
// ),
// );
}
// Handle API submission error

View File

@@ -1,3 +1,3 @@
class CommonAppText {
static const String selectiveCard = "Selective";
static const String selectiveCard = "Flexi";
}

View File

@@ -30,6 +30,10 @@ import '../cart/views/my_cart_view_page.dart';
import '../common_bloc/bottom_navigation_bloc.dart';
import '../home/views/home_page_view.dart';
import '../home/views/registered_user_home_page.dart';
import '../my_pass/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart';
import '../my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart';
import '../my_pass/repository/my_passes_attractions_repository.dart';
import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/pass_attraction_details_view.dart';
import '../profile/view/contact_us/contact_us_view.dart';
import '../profile/view/edit_profile/edit_profile_view.dart';
@@ -74,8 +78,23 @@ class AppRouter {
final args = settings.arguments as String;
return MaterialPageRoute(builder: (_) => AttractionsPage(source: args));
case RouteConstants.passAttractionsPage:
final args = settings.arguments as String;
return MaterialPageRoute(builder: (_) => PassAttractionsPage(source: args));
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
final int cityId = args['cityId'] as int;
final String source = args['source'] as String;
return MaterialPageRoute(
builder: (_) {
return BlocProvider(
create: (_) => MyPassesAttractionsBloc(
repository: MyPassesAttractionsRepository(),
),
child: PassAttractionsPage(
cityXid: cityId,
source: source,
),
);
},
);
case RouteConstants.profile:
return MaterialPageRoute(
builder: (_) {
@@ -205,11 +224,12 @@ class AppRouter {
},
);
case RouteConstants.searchPassOffer:
final int cityId = settings.arguments as int;
return MaterialPageRoute(
builder: (_) {
return BlocProvider(
create: (_) => OffersBloc(OffersRepository()),
child: PassOffersScreen(),
create: (_) => MyPassesOffersBloc(MyPassesOffersRepository()),
child: PassOffersScreen(cityId: cityId),
);
},
);

View File

@@ -19,6 +19,12 @@ import '../itinerary_creation/bloc/itinerary_detail_bloc.dart';
import '../itinerary_creation/bloc/itinerary_steps_selection_bloc.dart';
import '../itinerary_creation/views/itinerary_creation_view.dart';
import '../itinerary_creation/views/magic_itinerary_view.dart';
import '../my_pass/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart';
import '../my_pass/blocs/myPassesDetails/my_passes_details_bloc.dart';
import '../my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart';
import '../my_pass/repository/my_passes_attractions_repository.dart';
import '../my_pass/repository/my_passes_details_repository.dart';
import '../my_pass/repository/my_passes_offers_repository.dart';
import '../my_pass/views/booking_page_view.dart';
import '../my_pass/views/booking_successful_page_view.dart';
import '../my_pass/views/pass_details_page_view.dart';
@@ -59,9 +65,22 @@ Widget buildOffstageNavigator(
builder: (_) => AttractionsPage(source: args),
);
case RouteConstants.passAttractionsPage:
final args = settings.arguments as String;
final Map<String, dynamic> args = settings.arguments as Map<String, dynamic>;
final int cityId = args['cityId'] as int;
final String source = args['source'] as String;
return MaterialPageRoute(
builder: (_) => PassAttractionsPage(source: args),
builder: (_) {
return BlocProvider(
create: (_) => MyPassesAttractionsBloc(
repository: MyPassesAttractionsRepository(),
),
child: PassAttractionsPage(
cityXid: cityId,
source: source,
),
);
},
);
case RouteConstants.attractionDetails:
@@ -117,11 +136,12 @@ Widget buildOffstageNavigator(
},
);
case RouteConstants.searchPassOffer:
final int cityId = settings.arguments as int;
return MaterialPageRoute(
builder: (_) {
return BlocProvider(
create: (_) => OffersBloc(OffersRepository()),
child: PassOffersScreen(),
create: (_) => MyPassesOffersBloc(MyPassesOffersRepository()),
child: PassOffersScreen(cityId: cityId),
);
},
);
@@ -157,12 +177,14 @@ Widget buildOffstageNavigator(
);
case RouteConstants.qrPage:
final bookingId = settings.arguments as int;
return MaterialPageRoute(
builder: (context) {
final previousBloc = BlocProvider.of<MyPassBloc>(context);
return BlocProvider.value(
value: previousBloc,
child: const PassDetailsView(),
return BlocProvider(
create: (context) => MyPassesDetailsBloc(
repository: MyPassesDetailsRepository(),
),
child: PassDetailsView(bookingId: bookingId),
);
},
);

View File

@@ -6,8 +6,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/route_constants.dart';
import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
import '../../localPreference/local_preference.dart';
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../../profile/bloc/profile/profile_bloc.dart';
@@ -17,20 +19,26 @@ import '../bloc/create_account_event.dart';
import '../bloc/create_account_state.dart';
import '../repository/create_account_repository.dart';
class CreateAccountView extends StatelessWidget {
class CreateAccountView extends StatefulWidget {
final String email;
CreateAccountView({super.key, required this.email});
const CreateAccountView({super.key, required this.email});
@override
State<CreateAccountView> createState() => _CreateAccountViewState();
}
class _CreateAccountViewState extends State<CreateAccountView> {
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController addressController = TextEditingController();
final TextEditingController cityController = TextEditingController();
final TextEditingController stateController = TextEditingController();
final TextEditingController countryController = TextEditingController();
final TextEditingController postalController = TextEditingController();
String? selectedState;
String? selectedCountry;
void _submitForm(BuildContext context) {
if (firstNameController.text.trim().isEmpty ||
lastNameController.text.trim().isEmpty ||
@@ -38,8 +46,8 @@ class CreateAccountView extends StatelessWidget {
phoneController.text.trim().isEmpty ||
addressController.text.trim().isEmpty ||
cityController.text.trim().isEmpty ||
stateController.text.trim().isEmpty ||
countryController.text.trim().isEmpty ||
selectedState == null ||
selectedCountry == null ||
postalController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please fill all fields')),
@@ -56,16 +64,28 @@ class CreateAccountView extends StatelessWidget {
address1: addressController.text.trim(),
address2: '',
city: cityController.text.trim(),
state: stateController.text.trim(),
country: countryController.text.trim(),
state: selectedState!,
country: selectedCountry!,
postalCode: postalController.text.trim(),
),
);
}
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
phoneController.dispose();
addressController.dispose();
cityController.dispose();
postalController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
emailController.text = email;
emailController.text = widget.email;
return BlocProvider(
create: (context) =>
CreateAccountBloc(repository: CreateAccountRepository()),
@@ -81,6 +101,7 @@ class CreateAccountView extends StatelessWidget {
// context.read<MyPostCardBloc>().add(FetchDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
Navigator.pop(context);
ScaffoldMessenger.of(
context,
@@ -202,22 +223,134 @@ class CreateAccountView extends StatelessWidget {
controller: cityController,
),
),
// State Dropdown
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "State",
hint: "Enter your state",
controller: stateController,
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "State", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedState,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select state",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: (value) {
setState(() {
selectedState = value;
});
},
items: [
"New South Wales",
"Victoria",
"Queensland",
"South Australia",
"Western Australia",
"Tasmania",
"Northern Territory",
"Australian Capital Territory"
].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
),
),
// Country Dropdown
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Country",
hint: "Enter your country",
controller: countryController,
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCountry,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select country",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: (value) {
setState(() {
selectedCountry = value;
});
},
items: ["Australia"].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
@@ -257,4 +390,4 @@ class CreateAccountView extends StatelessWidget {
),
);
}
}
}

View File

@@ -6,7 +6,8 @@ class CitySelectionResponse {
factory CitySelectionResponse.fromJson(Map<String, dynamic> json) {
return CitySelectionResponse(
cities: (json['cities'] as List<dynamic>?)
?.map((city) => CitySelection.fromJson(city as Map<String, dynamic>))
?.map((city) =>
CitySelection.fromJson(city as Map<String, dynamic>))
.toList() ??
[],
);
@@ -20,33 +21,54 @@ class CitySelectionResponse {
}
class CitySelection {
// 🔹 EXISTING FIELDS (UNCHANGED)
final int id;
final String cityName;
final String bannerImage;
// 🔹 NEW FIELDS (ADDED ONLY)
final String cityIconPath;
final CityIcon? icon;
CitySelection({
required this.id,
required this.cityName,
required this.bannerImage,
// 🔹 ADDED
required this.cityIconPath,
required this.icon,
});
factory CitySelection.fromJson(Map<String, dynamic> json) {
return CitySelection(
// 🔹 EXISTING
id: json['id'] as int? ?? 0,
cityName: json['cityName'] as String? ?? '',
bannerImage: json['bannerImage'] as String? ?? '',
// 🔹 ADDED
cityIconPath: json['cityIconPath'] as String? ?? '',
icon: json['icon'] != null
? CityIcon.fromJson(json['icon'] as Map<String, dynamic>)
: null,
);
}
Map<String, dynamic> toJson() {
return {
// 🔹 EXISTING
'id': id,
'cityName': cityName,
'bannerImage': bannerImage,
// 🔹 ADDED
'cityIconPath': cityIconPath,
'icon': icon?.toJson(),
};
}
// Helper method to get the image URL with fallback
// 🔹 EXISTING METHODS (UNCHANGED)
String getImageUrl() {
if (bannerImage.isEmpty || !bannerImage.startsWith('http')) {
return 'assets/images/card_banner.png';
@@ -54,8 +76,26 @@ class CitySelection {
return bannerImage;
}
// Helper method to check if image is network image
bool isNetworkImage() {
return bannerImage.isNotEmpty && bannerImage.startsWith('http');
}
}
// 🔹 NEW MODEL (REQUIRED FOR icon.svg)
class CityIcon {
final String svg;
CityIcon({required this.svg});
factory CityIcon.fromJson(Map<String, dynamic> json) {
return CityIcon(
svg: json['svg'] as String? ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'svg': svg,
};
}
}

View File

@@ -238,6 +238,7 @@ class _CitySelectionView extends StatelessWidget {
city.cityName,
city.isNetworkImage(),
selectedCityId,
city.cityIconPath,
);
},
);
@@ -260,12 +261,15 @@ class _CitySelectionView extends StatelessWidget {
String imageUrl,
String name,
bool isNetwork,
int selectedCityId, // Add this parameter
int selectedCityId,
String? svgIcon,
// Add this parameter
) {
final bool isSelected = cityId == selectedCityId; // Check if selected
return InkWell(
onTap: () async {
await LocalPreference.setSelectedCityId(cityId);
await LocalPreference.setSelectedCityLogo(svgIcon!);
Navigator.pop(context);
context.read<HomeBloc>().add(FetchHomeData());
debugPrint("Selected City ID: $cityId");

View File

@@ -1,3 +1,77 @@
// import 'package:bloc/bloc.dart';
// import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
// import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
// import 'package:citycards_customer/localPreference/local_preference.dart';
// import 'package:equatable/equatable.dart';
// part 'get_itinerary_event.dart';
// part 'get_itinerary_state.dart';
//
// class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
// final ItineraryRepository _repository;
//
// GetItineraryBloc({ItineraryRepository? repository})
// : _repository = repository ?? ItineraryRepository(),
// super(GetItineraryInitial()) {
// on<CheckLoginAndFetchItinerary>(_onCheckLoginAndFetch);
// on<GetIiterary>(_onGetItinerary);
// }
//
// Future<void> _onCheckLoginAndFetch(
// CheckLoginAndFetchItinerary event,
// Emitter<GetItineraryState> emit,
// ) async {
// try {
// emit(GetItineraryLoading());
//
// final isLoggedIn = await LocalPreference.getLogin();
//
// if (!isLoggedIn) {
// emit(GetItineraryNotLoggedIn());
// return;
// }
//
// final response = await _repository.fetchMyItineraries();
//
// // Check if user has unlimited pass
// if (!response.isUnlimitedPass) {
// emit(GetItineraryRequiresPass(itineraries: response.itineraries));
// return;
// }
//
// emit(GetItinerarySuccessfully(itineraries: response.itineraries));
// } catch (e) {
// emit(GetItineraryFailed(
// error: e.toString().contains('Exception')
// ? e.toString().replaceAll('Exception: ', '')
// : "Failed to load itineraries. Please try again."));
// }
// }
//
// Future<void> _onGetItinerary(
// GetIiterary event,
// Emitter<GetItineraryState> emit,
// ) async {
// try {
// emit(GetItineraryLoading());
//
// final response = await _repository.fetchMyItineraries();
//
// // Check if user has unlimited pass
// if (!response.isUnlimitedPass) {
// emit(GetItineraryRequiresPass(itineraries: response.itineraries));
// return;
// }
//
// emit(GetItinerarySuccessfully(itineraries: response.itineraries));
// } catch (e) {
// emit(GetItineraryFailed(
// error: e.toString().contains('Exception')
// ? e.toString().replaceAll('Exception: ', '')
// : "Failed to load itineraries. Please try again."));
// }
// }
// }
import 'package:bloc/bloc.dart';
import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart';
import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart';
@@ -32,13 +106,19 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
final response = await _repository.fetchMyItineraries();
// Add static itinerary to the list
final itinerariesWithStatic = [
_createStaticItinerary(),
...response.itineraries,
];
// Check if user has unlimited pass
if (!response.isUnlimitedPass) {
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
return;
}
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
} catch (e) {
emit(GetItineraryFailed(
error: e.toString().contains('Exception')
@@ -56,13 +136,19 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
final response = await _repository.fetchMyItineraries();
// Add static itinerary to the list
final itinerariesWithStatic = [
_createStaticItinerary(),
...response.itineraries,
];
// Check if user has unlimited pass
if (!response.isUnlimitedPass) {
emit(GetItineraryRequiresPass(itineraries: response.itineraries));
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
return;
}
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
} catch (e) {
emit(GetItineraryFailed(
error: e.toString().contains('Exception')
@@ -70,4 +156,85 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
: "Failed to load itineraries. Please try again."));
}
}
// Helper method to create static/temporary itinerary
MyItinerary _createStaticItinerary() {
return MyItinerary(
id: -1, // Negative ID to identify as static data
userXid: 0,
cityXid: 1,
address: "Sample Location, City Center",
latitude: 40.7128,
longitude: -74.0060,
tripEnergy: "Relaxed",
travelingWithKids: false,
dietaryPreferences: ["Vegetarian"],
preferences: Preferences(
shopping: 3,
wildlife: 2,
landmarks: 5,
scenicViews: 4,
artAndMuseums: 5,
),
totalDays: 2,
aiModel: "static-v1",
promptVersion: "1.0",
isActive: true,
createdAt: DateTime.now().toIso8601String(),
updatedAt: DateTime.now().toIso8601String(),
days: [
ItineraryDay(
id: -1,
itineraryXid: -1,
dayNumber: 1,
title: "Day 1: City Exploration",
summary: "Explore the main attractions and local cuisine",
items: [
DayItem(
id: -1,
itineraryDayXid: -1,
timeSlot: "09:00 AM",
title: "Morning Coffee",
description: "Start your day with a cup of local coffee",
locationName: "Central Cafe",
imageUrl: "https://via.placeholder.com/300",
latitude: 40.7128,
longitude: -74.0060,
),
DayItem(
id: -2,
itineraryDayXid: -1,
timeSlot: "11:00 AM",
title: "Visit Historic Landmark",
description: "Explore the city's most famous landmark",
locationName: "City Monument",
imageUrl: "https://via.placeholder.com/300",
latitude: 40.7589,
longitude: -73.9851,
),
],
),
ItineraryDay(
id: -2,
itineraryXid: -1,
dayNumber: 2,
title: "Day 2: Museum & Parks",
summary: "Discover art and nature",
items: [
DayItem(
id: -3,
itineraryDayXid: -2,
timeSlot: "10:00 AM",
title: "Art Museum Visit",
description: "Immerse yourself in contemporary art",
locationName: "Modern Art Museum",
imageUrl: "https://via.placeholder.com/300",
latitude: 40.7614,
longitude: -73.9776,
),
],
),
],
);
}
}

View File

@@ -31,7 +31,7 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFFFF5F5),
backgroundColor: Colors.white,
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
@@ -41,7 +41,7 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: false,
showDivider: true,
),
SizedBox(height: 24.h),
// BLoC Builder for all states

View File

@@ -22,14 +22,6 @@ class LocalDatabase {
path,
version: 1,
onCreate: (db, version) async {
/// CITY TABLE
await db.execute('''
CREATE TABLE selected_city (
id INTEGER PRIMARY KEY,
city_id INTEGER
)
''');
/// ONBOARDING TABLE
await db.execute('''
CREATE TABLE onboarding_state (
@@ -90,7 +82,8 @@ class LocalDatabase {
description TEXT
)
''');
/// CITY TABLE
/// CITY TABLE (with city_logo field)
await db.execute('''
CREATE TABLE selected_city (
id INTEGER PRIMARY KEY,

View File

@@ -1,6 +1,8 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_bloc.dart';
import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_bloc.dart';
import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart';
import 'package:citycards_customer/postcard/blocs/myPostCards/my_postcard_bloc.dart';
import 'package:citycards_customer/profile/bloc/profile/profile_bloc.dart';
import 'package:citycards_customer/profile/bloc/profile/profile_event.dart';
@@ -48,6 +50,7 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
// context.read<MyPostCardBloc>().add(FetchOrderPostCards());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
// User exists - navigate to home/dashboard
// Navigator.of(context).pushReplacementNamed(RouteConstants.home);
ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -1,4 +1,5 @@
import 'package:citycards_customer/cart/blocs/postcard_bloc.dart';
import 'package:citycards_customer/cart/repository/my_pass_cart_repository.dart';
import 'package:citycards_customer/core/route_constants.dart';
import 'package:citycards_customer/search_offers/bloc/offers_bloc.dart';
import 'package:citycards_customer/trail.dart';
@@ -8,6 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_stripe/flutter_stripe.dart'; // ADD THIS
import 'cart/blocs/myPassCart/my_pass_cart_bloc.dart';
import 'core/app_router.dart';
import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart';
import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart';
@@ -18,7 +20,9 @@ import 'itinerary_creation/bloc/get_itinerary_bloc.dart';
import 'itinerary_creation/views/magic_itinerary_view.dart';
import 'login/bloc/login/login_bloc.dart';
import 'login/repository/login_repository.dart';
import 'my_pass/blocs/myPasses/my_passes_bloc.dart';
import 'my_pass/blocs/my_pass_bloc.dart';
import 'my_pass/repository/my_passes_repository.dart';
import 'postcard/blocs/myPostCards/my_postcard_bloc.dart';
import 'postcard/repository/my_postcard_repository.dart';
import 'profile/bloc/profile/profile_bloc.dart';
@@ -58,6 +62,12 @@ class MyApp extends StatelessWidget {
BlocProvider<MyPassBloc>(
create: (_) => MyPassBloc()..add(LoadMyPasses()),
),
BlocProvider<MyPassesBloc>(
create: (_) => MyPassesBloc(MyPassesRepository()),
),
BlocProvider<MyPassCartBloc>(
create: (_) => MyPassCartBloc(repository: MyPassCartRepository()),
),
BlocProvider<FirstTimeUserHomeBloc>(
create: (context) => FirstTimeUserHomeBloc(
FirstTimeUserHomeRepository(),

View File

@@ -0,0 +1,85 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../localPreference/local_preference.dart';
import '../../repository/my_passes_repository.dart';
import 'my_passes_event.dart';
import 'my_passes_state.dart';
class MyPassesBloc extends Bloc<MyPassesEvent, MyPassesState> {
final MyPassesRepository repository;
MyPassesBloc(this.repository) : super(MyPassesInitial()) {
on<CheckLoginAndFetchPasses>(_onCheckLoginAndFetchPasses);
on<FetchMyPasses>(_onFetchMyPasses);
on<RefreshMyPasses>(_onRefreshMyPasses);
}
Future<void> _onCheckLoginAndFetchPasses(
CheckLoginAndFetchPasses event,
Emitter<MyPassesState> emit,
) async {
try {
emit(MyPassesLoading());
// Check if user is logged in
final isLoggedIn = await LocalPreference.getLogin();
if (!isLoggedIn) {
emit(MyPassesNotLoggedIn());
return;
}
// User is logged in, fetch passes
final data = await repository.fetchMyPasses(
cardMode: event.cardMode,
sort: event.sort,
);
emit(MyPassesLoaded(data));
} catch (e) {
emit(MyPassesError(
e.toString().contains('Exception')
? e.toString().replaceAll('Exception: ', '')
: "Failed to load passes. Please try again."));
}
}
Future<void> _onFetchMyPasses(
FetchMyPasses event,
Emitter<MyPassesState> emit,
) async {
emit(MyPassesLoading());
try {
final data = await repository.fetchMyPasses(
cardMode: event.cardMode,
sort: event.sort,
);
emit(MyPassesLoaded(data));
} catch (e) {
emit(MyPassesError(
e.toString().contains('Exception')
? e.toString().replaceAll('Exception: ', '')
: "Failed to load passes. Please try again."));
}
}
Future<void> _onRefreshMyPasses(
RefreshMyPasses event,
Emitter<MyPassesState> emit,
) async {
try {
final data = await repository.fetchMyPasses(
cardMode: event.cardMode,
sort: event.sort,
);
emit(MyPassesLoaded(data));
} catch (e) {
emit(MyPassesError(
e.toString().contains('Exception')
? e.toString().replaceAll('Exception: ', '')
: "Failed to load passes. Please try again."));
}
}
}

View File

@@ -0,0 +1,50 @@
import 'package:equatable/equatable.dart';
abstract class MyPassesEvent extends Equatable {
const MyPassesEvent();
@override
List<Object?> get props => [];
}
/// Check Login and Fetch Passes Event
class CheckLoginAndFetchPasses extends MyPassesEvent {
final String cardMode;
final String sort;
const CheckLoginAndFetchPasses({
this.cardMode = "",
this.sort = "",
});
@override
List<Object?> get props => [cardMode, sort];
}
/// Initial / Normal Fetch
class FetchMyPasses extends MyPassesEvent {
final String cardMode;
final String sort;
const FetchMyPasses({
this.cardMode = "",
this.sort = "",
});
@override
List<Object?> get props => [cardMode, sort];
}
/// Refresh Event
class RefreshMyPasses extends MyPassesEvent {
final String cardMode;
final String sort;
const RefreshMyPasses({
this.cardMode = "",
this.sort = "",
});
@override
List<Object?> get props => [cardMode, sort];
}

View File

@@ -0,0 +1,39 @@
import 'package:equatable/equatable.dart';
import '../../models/my_passes_model.dart';
abstract class MyPassesState extends Equatable {
const MyPassesState();
@override
List<Object?> get props => [];
}
/// Initial State
class MyPassesInitial extends MyPassesState {}
/// Loading State
class MyPassesLoading extends MyPassesState {}
/// Not Logged In State
class MyPassesNotLoggedIn extends MyPassesState {}
/// Loaded State
class MyPassesLoaded extends MyPassesState {
final MyPassesModel passes;
const MyPassesLoaded(this.passes);
@override
List<Object?> get props => [passes];
}
/// Error State
class MyPassesError extends MyPassesState {
final String message;
const MyPassesError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,72 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../attractions/models/attraction_model.dart';
import '../../repository/my_passes_attractions_repository.dart';
import 'my_passes_attractions_event.dart';
import 'my_passes_attractions_state.dart';
class MyPassesAttractionsBloc
extends Bloc<MyPassesAttractionsEvent, MyPassesAttractionsState> {
final MyPassesAttractionsRepository repository;
MyPassesAttractionsBloc({required this.repository})
: super(MyPassesAttractionsInitial()) {
on<FetchMyPassesAttractionsByCategory>(_onFetchMyPassesAttractionsByCategory);
on<SearchMyPassesAttractions>(_onSearchMyPassesAttractions);
}
Future<void> _onFetchMyPassesAttractionsByCategory(
FetchMyPassesAttractionsByCategory event,
Emitter<MyPassesAttractionsState> emit,
) async {
emit(MyPassesAttractionsLoading());
try {
final AttractionsResponse response =
await repository.fetchMyPassesAttractions(
cityXid: event.cityXid,
categoryXid: event.categoryXid, // Can be null
);
final attractions = response.attractions ?? [];
emit(
MyPassesAttractionsLoaded(
attractions: attractions,
filteredAttractions: attractions, // Initially show all
categories: response.categories ?? [],
selectedCategoryId: event.categoryXid, // Can be null
searchQuery: '', // Reset search query on category change
),
);
} catch (e) {
emit(
MyPassesAttractionsError(
e.toString(),
),
);
}
}
void _onSearchMyPassesAttractions(
SearchMyPassesAttractions event,
Emitter<MyPassesAttractionsState> emit,
) {
final currentState = state;
if (currentState is MyPassesAttractionsLoaded) {
final query = event.query.toLowerCase();
final filtered = currentState.attractions.where((attraction) {
if (query.isEmpty) return true;
return attraction.title?.toLowerCase().contains(query) ?? false;
}).toList();
emit(
currentState.copyWith(
filteredAttractions: filtered,
searchQuery: event.query,
),
);
}
}
}

View File

@@ -0,0 +1,30 @@
import 'package:equatable/equatable.dart';
abstract class MyPassesAttractionsEvent extends Equatable {
const MyPassesAttractionsEvent();
@override
List<Object?> get props => [];
}
class FetchMyPassesAttractionsByCategory extends MyPassesAttractionsEvent {
final int cityXid;
final int? categoryXid;
const FetchMyPassesAttractionsByCategory({
required this.cityXid,
this.categoryXid,
});
@override
List<Object?> get props => [cityXid, categoryXid];
}
class SearchMyPassesAttractions extends MyPassesAttractionsEvent {
final String query;
const SearchMyPassesAttractions(this.query);
@override
List<Object?> get props => [query];
}

View File

@@ -0,0 +1,64 @@
import 'package:equatable/equatable.dart';
import '../../../attractions/models/attraction_model.dart';
abstract class MyPassesAttractionsState extends Equatable {
const MyPassesAttractionsState();
@override
List<Object?> get props => [];
}
class MyPassesAttractionsInitial extends MyPassesAttractionsState {}
class MyPassesAttractionsLoading extends MyPassesAttractionsState {}
class MyPassesAttractionsLoaded extends MyPassesAttractionsState {
final List<Attraction> attractions;
final List<Attraction> filteredAttractions;
final List<Category> categories;
final int? selectedCategoryId;
final String searchQuery;
const MyPassesAttractionsLoaded({
required this.attractions,
required this.filteredAttractions,
required this.categories,
this.selectedCategoryId,
this.searchQuery = '',
});
MyPassesAttractionsLoaded copyWith({
List<Attraction>? attractions,
List<Attraction>? filteredAttractions,
List<Category>? categories,
int? selectedCategoryId,
String? searchQuery,
}) {
return MyPassesAttractionsLoaded(
attractions: attractions ?? this.attractions,
filteredAttractions: filteredAttractions ?? this.filteredAttractions,
categories: categories ?? this.categories,
selectedCategoryId: selectedCategoryId ?? this.selectedCategoryId,
searchQuery: searchQuery ?? this.searchQuery,
);
}
@override
List<Object?> get props => [
attractions,
filteredAttractions,
categories,
selectedCategoryId,
searchQuery,
];
}
class MyPassesAttractionsError extends MyPassesAttractionsState {
final String message;
const MyPassesAttractionsError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/my_passes_details_repository.dart';
import 'my_passes_details_event.dart';
import 'my_passes_details_state.dart';
class MyPassesDetailsBloc
extends Bloc<MyPassesDetailsEvent, MyPassesDetailsState> {
final MyPassesDetailsRepository repository;
MyPassesDetailsBloc({required this.repository})
: super(MyPassesDetailsInitial()) {
on<FetchMyPassDetails>(_fetchPassDetails);
}
Future<void> _fetchPassDetails(
FetchMyPassDetails event,
Emitter<MyPassesDetailsState> emit,
) async {
emit(MyPassesDetailsLoading());
try {
final response =
await repository.fetchPassDetails(passId: event.passId);
emit(MyPassesDetailsLoaded(data: response));
} catch (e) {
emit(MyPassesDetailsError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,17 @@
import 'package:equatable/equatable.dart';
abstract class MyPassesDetailsEvent extends Equatable {
const MyPassesDetailsEvent();
@override
List<Object?> get props => [];
}
class FetchMyPassDetails extends MyPassesDetailsEvent {
final int passId;
const FetchMyPassDetails({required this.passId});
@override
List<Object?> get props => [passId];
}

View File

@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import '../../models/my_passes_details_model.dart';
abstract class MyPassesDetailsState extends Equatable {
const MyPassesDetailsState();
@override
List<Object?> get props => [];
}
class MyPassesDetailsInitial extends MyPassesDetailsState {}
class MyPassesDetailsLoading extends MyPassesDetailsState {}
class MyPassesDetailsLoaded extends MyPassesDetailsState {
final MyPassesDetailsModel data;
const MyPassesDetailsLoaded({required this.data});
@override
List<Object?> get props => [data];
}
class MyPassesDetailsError extends MyPassesDetailsState {
final String message;
const MyPassesDetailsError({required this.message});
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../search_offers/model/offers_model.dart';
import '../../repository/my_passes_offers_repository.dart';
import 'my_passes_offers_event.dart';
import 'my_passes_offers_state.dart';
class MyPassesOffersBloc extends Bloc<MyPassesOffersEvent, MyPassesOffersState> {
final MyPassesOffersRepository repository;
List<Offer> _allOffers = [];
MyPassesOffersBloc(this.repository) : super(MyPassesOffersInitial()) {
on<LoadMyPassesOffers>(_onLoadMyPassesOffers);
on<SearchMyPassesOffers>(_onSearchMyPassesOffers);
}
Future<void> _onLoadMyPassesOffers(
LoadMyPassesOffers event,
Emitter<MyPassesOffersState> emit,
) async {
emit(MyPassesOffersLoading());
try {
final response = await repository.fetchMyPassesOffers(
cityXid: event.cityXid,
categoryXid: event.categoryXid,
);
_allOffers = response.offers;
emit(
MyPassesOffersLoaded(
offers: response.offers,
categories: response.categories,
),
);
} catch (e) {
emit(MyPassesOffersError(e.toString()));
}
}
void _onSearchMyPassesOffers(
SearchMyPassesOffers event,
Emitter<MyPassesOffersState> emit,
) {
final filtered = _allOffers
.where(
(offer) =>
offer.title
.toLowerCase()
.contains(event.query.toLowerCase()) ||
offer.description
.toLowerCase()
.contains(event.query.toLowerCase()),
)
.toList();
if (state is MyPassesOffersLoaded) {
emit(
MyPassesOffersLoaded(
offers: filtered,
categories: (state as MyPassesOffersLoaded).categories,
),
);
}
}
}

View File

@@ -0,0 +1,16 @@
abstract class MyPassesOffersEvent {}
class LoadMyPassesOffers extends MyPassesOffersEvent {
final int cityXid;
final int? categoryXid;
LoadMyPassesOffers({
required this.cityXid,
this.categoryXid,
});
}
class SearchMyPassesOffers extends MyPassesOffersEvent {
final String query;
SearchMyPassesOffers(this.query);
}

View File

@@ -0,0 +1,22 @@
import '../../../search_offers/model/offers_model.dart';
abstract class MyPassesOffersState {}
class MyPassesOffersInitial extends MyPassesOffersState {}
class MyPassesOffersLoading extends MyPassesOffersState {}
class MyPassesOffersLoaded extends MyPassesOffersState {
final List<Offer> offers;
final List<Category> categories;
MyPassesOffersLoaded({
required this.offers,
required this.categories,
});
}
class MyPassesOffersError extends MyPassesOffersState {
final String message;
MyPassesOffersError(this.message);
}

View File

@@ -0,0 +1,167 @@
class MyPassesDetailsModel {
final City? city;
final List<Attraction> attractions;
final List<Offer> offers;
MyPassesDetailsModel({
this.city,
required this.attractions,
required this.offers,
});
factory MyPassesDetailsModel.fromJson(Map<String, dynamic>? json) {
return MyPassesDetailsModel(
city: json?['city'] != null
? City.fromJson(json?['city'])
: null,
attractions: (json?['attractions'] as List<dynamic>?)
?.map((e) => Attraction.fromJson(e))
.toList() ??
[],
offers: (json?['offers'] as List<dynamic>?)
?.map((e) => Offer.fromJson(e))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'city': city?.toJson(),
'attractions': attractions.map((e) => e.toJson()).toList(),
'offers': offers.map((e) => e.toJson()).toList(),
};
}
}
class City {
final num id;
final String name;
final String cardMode;
final String validUpto;
final num totalAdult;
final num totalChild;
final num noOfDays;
final num noOfAttractions;
City({
required this.id,
required this.name,
required this.cardMode,
required this.validUpto,
required this.totalAdult,
required this.totalChild,
required this.noOfDays,
required this.noOfAttractions,
});
factory City.fromJson(Map<String, dynamic>? json) {
return City(
id: json?['id'] ?? 0,
name: json?['name'] ?? '',
cardMode: json?['cardMode'] ?? '',
validUpto: json?['validUpto'] ?? '',
totalAdult: json?['totalAdult'] ?? 0,
totalChild: json?['totalChild'] ?? 0,
noOfDays: json?['noOfDays'] ?? 0,
noOfAttractions: json?['noOfAttractions'] ?? 0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'cardMode': cardMode,
'validUpto': validUpto,
'totalAdult': totalAdult,
'totalChild': totalChild,
'noOfDays': noOfDays,
'noOfAttractions': noOfAttractions,
};
}
}
class Attraction {
final num id;
final String title;
final String description;
final num? ticketPriceAdult;
final num? ticketPriceChild;
final String? bookingEmail;
final String? bookingPhoneNumber;
final String image;
Attraction({
required this.id,
required this.title,
required this.description,
this.ticketPriceAdult,
this.ticketPriceChild,
this.bookingEmail,
this.bookingPhoneNumber,
required this.image,
});
factory Attraction.fromJson(Map<String, dynamic>? json) {
return Attraction(
id: json?['id'] ?? 0,
title: json?['title'] ?? '',
description: json?['description'] ?? '',
ticketPriceAdult: json?['ticketPriceAdult'],
ticketPriceChild: json?['ticketPriceChild'],
bookingEmail: json?['bookingEmail'],
bookingPhoneNumber: json?['bookingPhoneNumber'],
image: json?['image'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'ticketPriceAdult': ticketPriceAdult,
'ticketPriceChild': ticketPriceChild,
'bookingEmail': bookingEmail,
'bookingPhoneNumber': bookingPhoneNumber,
'image': image,
};
}
}
class Offer {
final num id;
final String title;
final String description;
final String mobileBannerImage;
final String websiteBannerImage;
Offer({
required this.id,
required this.title,
required this.description,
required this.mobileBannerImage,
required this.websiteBannerImage,
});
factory Offer.fromJson(Map<String, dynamic>? json) {
return Offer(
id: json?['id'] ?? 0,
title: json?['title'] ?? '',
description: json?['description'] ?? '',
mobileBannerImage: json?['mobileBannerImage'] ?? '',
websiteBannerImage: json?['websiteBannerImage'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'mobileBannerImage': mobileBannerImage,
'websiteBannerImage': websiteBannerImage,
};
}
}

View File

@@ -0,0 +1,119 @@
class MyPassesModel {
final List<MyPassData>? data;
MyPassesModel({
this.data,
});
factory MyPassesModel.fromJson(List<dynamic>? json) {
return MyPassesModel(
data: json != null
? json.map((e) => MyPassData.fromJson(e)).toList()
: [],
);
}
List<dynamic> toJson() {
return data != null
? data!.map((e) => e.toJson()).toList()
: [];
}
}
class MyPassData {
final num? id;
final String? bookingNumber;
final String? cardMode;
final String? validUpto;
final num? totalAdult;
final num? totalChild;
final num? totalAmount;
final String? bookingStatus;
final num? noOfAttractions;
final num? noOfDays;
final String? paymentStatus;
final String? updatedAt;
final City? city;
MyPassData({
this.id,
this.bookingNumber,
this.cardMode,
this.validUpto,
this.totalAdult,
this.totalChild,
this.totalAmount,
this.bookingStatus,
this.noOfAttractions,
this.noOfDays,
this.paymentStatus,
this.updatedAt,
this.city,
});
factory MyPassData.fromJson(Map<String, dynamic>? json) {
return MyPassData(
id: json?['id'] ?? 0,
bookingNumber: json?['bookingNumber'] ?? '',
cardMode: json?['cardMode'] ?? '',
validUpto: json?['validUpto'] ?? '',
totalAdult: json?['totalAdult'] ?? 0,
totalChild: json?['totalChild'] ?? 0,
totalAmount: json?['totalAmount'] ?? 0,
bookingStatus: json?['bookingStatus'] ?? '',
noOfAttractions: json?['noOfAttractions'] ?? 0,
noOfDays: json?['noOfDays'] ?? 0,
paymentStatus: json?['paymentStatus'] ?? '',
updatedAt: json?['updatedAt'] ?? '',
city: json?['city'] != null
? City.fromJson(json?['city'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id ?? 0,
'bookingNumber': bookingNumber ?? '',
'cardMode': cardMode ?? '',
'validUpto': validUpto ?? '',
'totalAdult': totalAdult ?? 0,
'totalChild': totalChild ?? 0,
'totalAmount': totalAmount ?? 0,
'bookingStatus': bookingStatus ?? '',
'noOfAttractions': noOfAttractions ?? 0,
'noOfDays': noOfDays ?? 0,
'paymentStatus': paymentStatus ?? '',
'updatedAt': updatedAt ?? '',
'city': city?.toJson(),
};
}
}
class City {
final num? id;
final String? name;
final String? bannerImage;
City({
this.id,
this.name,
this.bannerImage,
});
factory City.fromJson(Map<String, dynamic>? json) {
return City(
id: json?['id'] ?? 0,
name: json?['name'] ?? '',
bannerImage: json?['bannerImage'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id ?? 0,
'name': name ?? '',
'bannerImage': bannerImage ?? '',
};
}
}

View File

@@ -0,0 +1,29 @@
import 'package:citycards_customer/networkApiServices/api_urls.dart';
import '../../attractions/models/attraction_model.dart';
import '../../networkApiServices/network_api_services.dart';
class MyPassesAttractionsRepository {
final NetworkApiService _apiServices = NetworkApiService();
/// Fetch my passes attractions by cityXid and optional categoryXid
Future<AttractionsResponse> fetchMyPassesAttractions({
required int cityXid,
int? categoryXid,
}) async {
try {
// Base URL
String url = '${ApiUrls.passAttractionsList}?cityXid=$cityXid';
// Add categoryXid if provided
if (categoryXid != null) {
url = '$url&categoryXid=$categoryXid';
}
final response = await _apiServices.getApi(url: url);
return AttractionsResponse.fromJson(response.data);
} catch (e) {
throw Exception('Failed to fetch my passes attractions: $e');
}
}
}

View File

@@ -0,0 +1,18 @@
import '../models/my_passes_details_model.dart';
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
class MyPassesDetailsRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch pass details by passId
Future<MyPassesDetailsModel> fetchPassDetails({
required int passId,
}) async {
final response = await _apiService.getApi(
url: '${ApiUrls.passDetails}/$passId/details',
);
return MyPassesDetailsModel.fromJson(response.data);
}
}

View File

@@ -0,0 +1,25 @@
import '../../networkApiServices/api_urls.dart';
import '../../search_offers/model/offers_model.dart';
import '../../networkApiServices/network_api_services.dart';
class MyPassesOffersRepository {
final NetworkApiService _apiService = NetworkApiService();
/// Fetch my passes offers by cityXid and optionally by categoryXid
Future<OffersResponse> fetchMyPassesOffers({
required int cityXid,
int? categoryXid,
}) async {
String url = '${ApiUrls.passOffers}?cityXid=$cityXid';
if (categoryXid != null) {
url += '&categoryXid=$categoryXid';
}
final response = await _apiService.getApi(
url: url,
);
return OffersResponse.fromJson(response.data);
}
}

View File

@@ -0,0 +1,32 @@
import '../models/my_passes_model.dart';
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
class MyPassesRepository {
final NetworkApiService _apiService = NetworkApiService();
Future<MyPassesModel> fetchMyPasses({
String cardMode = "",
String sort = "",
}) async {
String url = ApiUrls.myPasses;
List<String> queryParams = [];
if (cardMode.isNotEmpty) {
queryParams.add("cardMode=$cardMode");
}
if (sort.isNotEmpty) {
queryParams.add("sort=$sort");
}
if (queryParams.isNotEmpty) {
url += "?${queryParams.join("&")}";
}
final response = await _apiService.getApi(url: url);
return MyPassesModel.fromJson(response.data);
}
}

View File

@@ -3,78 +3,337 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../common_bloc/bottom_navigation_bloc.dart';
import '../../common_packages/custom_filled_button.dart';
import '../../core/route_constants.dart';
import '../blocs/my_pass_bloc.dart';
import '../../login/view/login_email_bottomsheet.dart';
import '../blocs/myPasses/my_passes_bloc.dart';
import '../blocs/myPasses/my_passes_event.dart';
import '../blocs/myPasses/my_passes_state.dart';
import '../widgets/pass_widget.dart';
class MyPassesView extends StatelessWidget {
class MyPassesView extends StatefulWidget {
const MyPassesView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPassBloc, MyPassState>(
builder: (context, state) {
if (state is MyPassLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is MyPassEmpty) {
return _noPassView(context);
} else if (state is MyPassLoaded) {
return _passListView(state.passes);
}
return const SizedBox.shrink();
},
State<MyPassesView> createState() => _MyPassesViewState();
}
class _MyPassesViewState extends State<MyPassesView> {
String selectedCardMode = "";
String selectedSort = "";
@override
void initState() {
super.initState();
// Changed from FetchMyPasses to CheckLoginAndFetchPasses
context.read<MyPassesBloc>().add(const CheckLoginAndFetchPasses());
}
void _showCardModeBottomSheet() {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
builder: (context) {
return Container(
padding: EdgeInsets.all(16.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(
"All",
style: GoogleFonts.poppins(fontSize: 14.sp),
),
onTap: () {
setState(() {
selectedCardMode = "";
});
Navigator.pop(context);
context.read<MyPassesBloc>().add(FetchMyPasses(
cardMode: "",
sort: selectedSort,
));
},
),
ListTile(
title: Text(
"flexi",
style: GoogleFonts.poppins(fontSize: 14.sp),
),
onTap: () {
setState(() {
selectedCardMode = "flexi";
});
Navigator.pop(context);
context.read<MyPassesBloc>().add(FetchMyPasses(
cardMode: "flexi",
sort: selectedSort,
));
},
),
ListTile(
title: Text(
"unlimited",
style: GoogleFonts.poppins(fontSize: 14.sp),
),
onTap: () {
setState(() {
selectedCardMode = "unlimited";
});
Navigator.pop(context);
context.read<MyPassesBloc>().add(FetchMyPasses(
cardMode: "unlimited",
sort: selectedSort,
));
},
),
],
),
);
},
);
}
Widget _noPassView(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 30.h),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/no_pass.png', // your woman sitting image
height: 180.h,
),
SizedBox(height: 20.h),
Text(
"You Dont have a Pass Yet! 😕",
style: GoogleFonts.poppins(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
textAlign: TextAlign.center,
),
SizedBox(height: 8.h),
Text(
"Get a pass and get offers and discounts and\nmore on your trip to your favourite city",
style: GoogleFonts.poppins(fontSize: 12.sp, color: Colors.black54),
textAlign: TextAlign.center,
),
SizedBox(height: 24.h),
GestureDetector(
onTap: () {
// Navigate to Buy a Pass
Navigator.pushNamed(context, '/buyPass');
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 14.h),
decoration: BoxDecoration(
color: const Color(0xffFF5A5F),
borderRadius: BorderRadius.circular(30.r),
void _showSortBottomSheet() {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
builder: (context) {
return Container(
padding: EdgeInsets.all(16.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(
"All",
style: GoogleFonts.poppins(fontSize: 14.sp),
),
onTap: () {
setState(() {
selectedSort = "";
});
Navigator.pop(context);
context.read<MyPassesBloc>().add(FetchMyPasses(
cardMode: selectedCardMode,
sort: "",
));
},
),
child: Center(
ListTile(
title: Text(
"latest",
style: GoogleFonts.poppins(fontSize: 14.sp),
),
onTap: () {
setState(() {
selectedSort = "latest";
});
Navigator.pop(context);
context.read<MyPassesBloc>().add(FetchMyPasses(
cardMode: selectedCardMode,
sort: "latest",
));
},
),
ListTile(
title: Text(
"oldest",
style: GoogleFonts.poppins(fontSize: 14.sp),
),
onTap: () {
setState(() {
selectedSort = "oldest";
});
Navigator.pop(context);
context.read<MyPassesBloc>().add(FetchMyPasses(
cardMode: selectedCardMode,
sort: "oldest",
));
},
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: BlocBuilder<MyPassesBloc, MyPassesState>(
builder: (context, state) {
if (state is MyPassesLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is MyPassesNotLoggedIn) {
// New state handling for not logged in users
return _notLoggedInView(context);
} else if (state is MyPassesLoaded) {
return SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
SizedBox(height: 10.h),
Row(
children: [
GestureDetector(
onTap: _showSortBottomSheet,
child: Container(
width: 130.w,
height: 36.h,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: const Color(0xffFDCDCE)),
),
child: Row(
children: [
Text(
selectedSort.isEmpty ? "Sort by Date" : selectedSort,
style: GoogleFonts.poppins(fontSize: 12.sp),
),
const Spacer(),
const Icon(Icons.sort, size: 16),
],
),
),
),
SizedBox(width: 10.w),
GestureDetector(
onTap: _showCardModeBottomSheet,
child: Container(
height: 36.h,
width: 130.w,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: const Color(0xffFDCDCE)),
),
child: Row(
children: [
Text(
selectedCardMode.isEmpty ? "All" : selectedCardMode,
style: GoogleFonts.poppins(fontSize: 12.sp),
),
const Spacer(),
const Icon(Icons.keyboard_arrow_down_rounded, size: 18),
],
),
),
),
],
),
SizedBox(height: 20.h),
if (state.passes.data == null || state.passes.data!.isEmpty)
_noPassView(context)
else
_passListView(state.passes.data!),
],
),
);
} else if (state is MyPassesError) {
return Center(
child: Text(
"Buy a Pass",
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
state.message,
style: GoogleFonts.poppins(fontSize: 14.sp, color: Colors.red),
),
);
}
return const SizedBox.shrink();
},
),
),
);
}
/// New widget for not logged in state
Widget _notLoggedInView(BuildContext context) {
return SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
SizedBox(height: 40.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: Column(
children: [
/// Illustration Image
Center(
child: Image.asset(
"assets/images/no_itinerary.png", // You can use a different image if available
height: 260.h,
fit: BoxFit.contain,
),
),
),
SizedBox(height: 32.h),
/// Title
Text(
"Please Log In to View Your Passes",
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
textAlign: TextAlign.center,
),
SizedBox(height: 12.h),
/// Subtitle
Text(
"Log in to access your passes, exclusive offers, and discounts on your trip to your favourite city.",
style: GoogleFonts.poppins(
fontSize: 14.sp,
color: const Color(0xFF656565),
),
textAlign: TextAlign.center,
),
SizedBox(height: 32.h),
/// Login Button
CustomFilledButton(
onTap: () {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
builder: (_) => const LoginEmailBottomsheet(),
);
},
label: "Log In",
showArrow: true,
),
SizedBox(height: 40.h),
],
),
),
],
@@ -82,87 +341,84 @@ class MyPassesView extends StatelessWidget {
);
}
Widget _passListView(List passes) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
SizedBox(height: 10.h),
Row(
children: [
Container(
width: 130.w,
height: 36.h,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: const Color(0xffFDCDCE)),
),
child: Row(
children: [
Text(
"Sort by Date",
style: GoogleFonts.poppins(fontSize: 12.sp),
),
const Spacer(),
const Icon(Icons.sort, size: 16),
],
),
),
SizedBox(width: 10.w),
Container(
height: 36.h,
width: 130.w,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: const Color(0xffFEE7E7),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: const Color(0xffFDCDCE)),
),
child: Row(
children: [
Text(
"All",
style: GoogleFonts.poppins(fontSize: 12.sp),
),
const Spacer(),
const Icon(Icons.keyboard_arrow_down_rounded, size: 18),
],
),
),
],
),
SizedBox(height: 20.h),
ListView.builder(
itemCount: passes.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final pass = passes[index];
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: InkWell(
onTap: (){
context.read<MyPassBloc>().add(SelectPass(pass));
Navigator.of(
context,
).pushNamed(RouteConstants.qrPage);
},
child: PassTicketCard(pass: pass),
),
);
},
),
],
Widget _noPassView(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
SizedBox(height: 60.h),
/// Illustration Image
Center(
child: Image.asset(
"assets/images/no_itinerary.png",
height: 260.h,
fit: BoxFit.contain,
),
),
),
SizedBox(height: 32.h),
/// Title
Text(
"You Don't have a Pass Yet! 😕",
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
textAlign: TextAlign.center,
),
SizedBox(height: 12.h),
/// Subtitle
Text(
"Get a pass and unlock exclusive offers, discounts, and more on your trip to your favourite city.",
style: GoogleFonts.poppins(
fontSize: 14.sp,
color: const Color(0xFF656565),
),
textAlign: TextAlign.center,
),
SizedBox(height: 32.h),
/// Custom Filled Button
CustomFilledButton(
onTap: () {
context.read<NavigationBloc>().add(NavigationTabChanged(0));
},
label: "Buy a Pass",
showArrow: true,
),
SizedBox(height: 40.h),
],
),
);
}
}
Widget _passListView(List passes) {
return ListView.builder(
itemCount: passes.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final pass = passes[index];
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: InkWell(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.qrPage,
arguments: pass.id, // Pass your booking ID here
);
},
child: PassTicketCard(pass: pass),
),
);
},
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.
import 'package:citycards_customer/common_packages/app_bar.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -177,10 +178,142 @@ class PassAttractionDetailsView extends StatelessWidget {
],
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h),
child: Container(
width: double.infinity,
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: Color(0xFFFDCDCE),
width: 1.5,
),
),
child: Column(
children: [
Text(
"Scan this at the site of the attraction",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Color(0xFFF95F62),
),
textAlign: TextAlign.center,
),
SizedBox(height: 20.h),
// QR Code Image
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Image.asset(
'assets/images/qr_image.png',
height: 200.h,
width: 200.w,
fit: BoxFit.contain,
),
),
SizedBox(height: 16.h),
// QR Code Text
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"IYFHHVN254ADSD",
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
letterSpacing: 1.2,
),
),
SizedBox(width: 8.w),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: "IYFHHVN254ADSD"));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Code copied to clipboard'),
duration: Duration(seconds: 2),
backgroundColor: Color(0xFFF95F62),
),
);
},
child: Icon(
Icons.copy,
size: 18.sp,
color: Colors.black54,
),
),
],
),
SizedBox(height: 20.h),
// Check in Button
SizedBox(
width: double.infinity,
height: 50.h,
child: ElevatedButton(
onPressed: () {
// Add your check-in logic here
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFFF95F62),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.r),
),
elevation: 0,
),
child: Text(
"Check in",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
SizedBox(height: 12.h),
// Help Text
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Having problems redeeming the pass? ",
style: TextStyle(
fontSize: 12.sp,
color: Colors.black54,
),
),
GestureDetector(
onTap: () {
// Add your help/support navigation here
},
child: Text(
"Click Here",
style: TextStyle(
fontSize: 12.sp,
color: Color(0xFFF95F62),
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
),
],
),
],
),
),
),
// About Section
Padding(
padding:
EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h),
EdgeInsets.only(left: 16.w, right: 16.w,),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -5,35 +5,43 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../attractions/blocs/attractions_bloc.dart';
import '../../attractions/blocs/attractions_event.dart';
import '../../attractions/blocs/attractions_state.dart';
import '../../attractions/repository/attractions_repository.dart';
import '../../attractions/widget/attraction_card.dart';
import '../../attractions/widget/filter_chip.dart';
import '../../common_packages/custom_search_field.dart';
import '../blocs/myPassesAttrctions/my_passes_attractions_bloc.dart';
import '../blocs/myPassesAttrctions/my_passes_attractions_event.dart';
import '../blocs/myPassesAttrctions/my_passes_attractions_state.dart';
import '../repository/my_passes_attractions_repository.dart';
class PassAttractionsPage extends StatelessWidget {
final int cityXid;
final String source;
const PassAttractionsPage({super.key, required this.source});
const PassAttractionsPage({
super.key,
required this.cityXid,
required this.source,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
final bloc = AttractionsBloc(
repository: AttractionsRepository(),
final bloc = MyPassesAttractionsBloc(
repository: MyPassesAttractionsRepository(),
);
// Fetch attractions with cityXid
bloc.add(
const FetchAttractionsByCategory(), // No categoryXid parameter
FetchMyPassesAttractionsByCategory(
cityXid: cityXid,
),
);
return bloc;
},
child: BlocBuilder<AttractionsBloc, AttractionsState>(
child: BlocBuilder<MyPassesAttractionsBloc, MyPassesAttractionsState>(
builder: (context, state) {
final bloc = context.read<AttractionsBloc>();
final bloc = context.read<MyPassesAttractionsBloc>();
return Scaffold(
backgroundColor: Colors.white,
@@ -49,23 +57,22 @@ class PassAttractionsPage extends StatelessWidget {
isProfilePage: false,
showDivider: true,
),
backWidget(context, "Your Attraction", Colors.black),
backWidget(context, "Pass Attractions", Colors.black),
const SizedBox(height: 20),
// 🔍 Search field (UI kept, logic disabled)
// 🔍 Search field with BLoC logic
CommonSearchField(
hint: "Search attractions...",
hintColor: Colors.grey.shade500,
onChanged: (value) {
// ❌ Search logic intentionally disabled
// UI only, no API call
bloc.add(SearchMyPassesAttractions(value));
},
),
const SizedBox(height: 16),
// 🏖 Category chips row - DYNAMIC
if (state is AttractionsLoaded)
// 🖼 Category chips row - DYNAMIC
if (state is MyPassesAttractionsLoaded)
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
@@ -73,10 +80,12 @@ class PassAttractionsPage extends StatelessWidget {
.map(
(category) => buildCategoryChip(
category.categoryName ?? '',
isSelected: state.selectedCategoryId == category.id,
isSelected:
state.selectedCategoryId == category.id,
onTap: () {
bloc.add(
FetchAttractionsByCategory(
FetchMyPassesAttractionsByCategory(
cityXid: cityXid,
categoryXid: category.id,
),
);
@@ -86,54 +95,20 @@ class PassAttractionsPage extends StatelessWidget {
.toList(),
),
),
// else
// // Show placeholder chips while loading
// SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Row(
// children: [
// buildCategoryChip("Beach", isSelected: true, onTap: () {}),
// buildCategoryChip("Hike", isSelected: false, onTap: () {}),
// buildCategoryChip("Adventure", isSelected: false, onTap: () {}),
// buildCategoryChip("Best in Summer", isSelected: false, onTap: () {}),
// ],
// ),
// ),
const SizedBox(height: 10),
// 🙏 Attraction list
if (state is AttractionsLoading)
// 🏙 Attraction list with search filter
if (state is MyPassesAttractionsLoading)
const Center(
child: Padding(
padding: EdgeInsets.only(top: 60),
child: CircularProgressIndicator(),
),
)
else if (state is AttractionsLoaded)
state.attractions.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
"No attractions found",
style: TextStyle(
color: Colors.grey,
fontSize: 14.sp,
),
),
),
)
: Column(
children: state.attractions
.map(
(attraction) => PassAttractionCard(
attraction: attraction,
),
)
.toList(),
)
else if (state is AttractionsError)
else if (state is MyPassesAttractionsLoaded)
_buildAttractionsList(state)
else if (state is MyPassesAttractionsError)
Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
@@ -157,4 +132,34 @@ class PassAttractionsPage extends StatelessWidget {
),
);
}
// Helper method to build attractions list
Widget _buildAttractionsList(MyPassesAttractionsLoaded state) {
if (state.filteredAttractions.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Text(
state.searchQuery.isEmpty
? "No attractions found"
: "No attractions match your search",
style: TextStyle(
color: Colors.grey,
fontSize: 14.sp,
),
),
),
);
}
return Column(
children: state.filteredAttractions
.map(
(attraction) => PassAttractionCard(
attraction: attraction,
),
)
.toList(),
);
}
}

View File

@@ -1,4 +1,3 @@
import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@@ -8,16 +7,57 @@ import '../../common_packages/app_bar.dart';
import '../../common_packages/back_widget.dart';
import '../../common_packages/custom_dash_border_painter.dart';
import '../../core/route_constants.dart';
import '../../networkApiServices/api_urls.dart';
import '../blocs/myPassesDetails/my_passes_details_bloc.dart';
import '../blocs/myPassesDetails/my_passes_details_event.dart';
import '../blocs/myPassesDetails/my_passes_details_state.dart';
class PassDetailsView extends StatelessWidget {
const PassDetailsView({super.key});
class PassDetailsView extends StatefulWidget {
final int bookingId;
const PassDetailsView({super.key, required this.bookingId});
@override
State<PassDetailsView> createState() => _PassDetailsViewState();
}
class _PassDetailsViewState extends State<PassDetailsView> {
@override
void initState() {
super.initState();
context.read<MyPassesDetailsBloc>().add(
FetchMyPassDetails(passId: widget.bookingId),
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPassBloc, MyPassState>(
return BlocBuilder<MyPassesDetailsBloc, MyPassesDetailsState>(
builder: (context, state) {
if (state is MyPassLoaded) {
final pass = state.selectedPass!;
if (state is MyPassesDetailsLoading) {
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator()),
);
}
if (state is MyPassesDetailsError) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Text(
'Error: ${state.message}',
style: GoogleFonts.poppins(color: Colors.red),
),
),
);
}
if (state is MyPassesDetailsLoaded) {
final data = state.data;
final city = data.city;
final attractions = data.attractions ?? [];
final offers = data.offers ?? [];
return SafeArea(
child: Scaffold(
@@ -27,7 +67,6 @@ class PassDetailsView extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// App Bar
SizedBox(height: 10.h),
const CommonAppBar(
@@ -44,144 +83,176 @@ class PassDetailsView extends StatelessWidget {
/// -------------------------------
/// UNLIMITED CARD CONTAINER
/// -------------------------------
CustomPaint(
painter: DashedBorderPainter(
color: const Color(0xffF95F62),
radius: 20.r,
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 18.h),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withOpacity(0.08),
borderRadius: BorderRadius.circular(20.r),
CustomPaint(
painter: DashedBorderPainter(
color: const Color(0xffF95F62),
radius: 20.r,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Title
Text(
pass.title,
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
),
SizedBox(height: 18.h),
/// MAIN CONTENT ROW
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// IMAGE
ClipRRect(
borderRadius: BorderRadius.circular(14.r),
child: Image.asset(
"assets/images/unlimited_card_details.png",
height: 100.w,
width: 100.w,
fit: BoxFit.contain,
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 18.w, vertical: 18.h),
decoration: BoxDecoration(
color: const Color(0xffF95F62).withOpacity(0.08),
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Title
Text(
'${(city?.cardMode ?? '').isNotEmpty
? city!.cardMode![0].toUpperCase() + city.cardMode!.substring(1)
: ''} Card',
style: GoogleFonts.poppins(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
),
SizedBox(height: 18.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// IMAGE
ClipRRect(
borderRadius: BorderRadius.circular(14.r),
child: Image.asset(
"assets/images/unlimited_card_details.png",
height: 100.w,
width: 100.w,
fit: BoxFit.contain,
),
),
SizedBox(width: 14.w),
SizedBox(width: 14.w),
/// RIGHT CONTENT
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Adults + Kids (WRAP prevents overflow)
Wrap(
spacing: 10.w,
runSpacing: 10.h,
children: [
_infoChip(
icon: Icons.person_outline,
text: "Adults-${pass.adults ?? 0}",
),
_infoChip(
icon: Icons.person_outline,
text: "Kids-${pass.kids ?? 0}",
),
],
),
SizedBox(height: 12.h),
/// Days Container (NOT full width)
_infoChip(
icon: Icons.access_time,
text: "${pass.duration} Days",
),
SizedBox(height: 14.h),
/// Valid Till
Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
/// RIGHT CONTENT
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
/// Adults + Kids (WRAP prevents overflow)
Wrap(
spacing: 10.w,
runSpacing: 10.h,
children: [
Icon(
Icons.calendar_today_outlined,
size: 15.sp,
color: const Color(0xffF95F62),
_infoChip(
imagePath: "assets/icons/person.png",
text: "Adults-${city?.totalAdult ?? 0}",
),
SizedBox(width: 6.w),
/// "Valid till:" → Black
Text(
"Valid till: ",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
/// Date → Red
Text(
pass.validity ?? "",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
_infoChip(
imagePath: "assets/icons/person.png",
text: "Kids-${city?.totalChild ?? 0}",
),
],
),
),
],
SizedBox(height: 12.h),
/// Days Container (Full width)
_infoChip(
imagePath: "assets/icons/time.png",
text: "${city?.noOfDays ?? 0} Days",
isExpanded: true,
),
SizedBox(height: 14.h),
/// Valid Till
Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
"assets/icons/calendar.png",
height: 15.h,
width: 15.w,
color: const Color(0xffF95F62),
),
SizedBox(width: 6.w),
/// "Valid till:" → Black
Text(
"Valid till: ",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
/// Date → Red
Text(
city?.validUpto ?? "",
style: GoogleFonts.poppins(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: const Color(0xffF95F62),
),
),
],
),
),
],
),
),
),
],
),
],
],
),
],
),
),
),
),
SizedBox(height: 24.h),
SizedBox(height: 24.h),
_sectionTitle("Suggested Attractions"),
SizedBox(height: 12.h),
_attractionCard(),
SizedBox(height: 12.h),
_attractionCard(),
/// 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,
),
),
)),
] else ...[
_attractionCard(
title: 'No attractions available',
description: '',
image: '',
ticketPriceAdult: null,
ticketPriceChild: null,
bookingEmail: null,
bookingPhoneNumber: null,
),
],
SizedBox(height: 16.h),
_outlineButton(
"View all Attractions",
() {
Navigator.pushNamed(
context,
RouteConstants.passAttractionsPage,
arguments: "qrPass",
arguments: {
'cityId': city?.id,
'source': 'my_passes',
},
);
},
),
@@ -194,13 +265,64 @@ class PassDetailsView extends StatelessWidget {
_sectionTitle("Recommended Offers"),
SizedBox(height: 12.h),
Row(
children: [
Expanded(child: _offerCard()),
SizedBox(width: 12.w),
Expanded(child: _offerCard()),
],
),
/// Display offers from API
if (offers.isNotEmpty) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.offerPassDetail,
arguments: offers[0].id,
);
},
child: _offerCard(
title: offers[0].title ?? '',
description: offers[0].description ?? '',
image: offers[0].mobileBannerImage != null
? "${ApiUrls.baseUrl}/${offers[0].mobileBannerImage!}"
: '',
),
),
),
if (offers.length > 1) ...[
SizedBox(width: 12.w),
Expanded(
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.offerPassDetail,
arguments: offers[1].id,
);
},
child: _offerCard(
title: offers[1].title ?? '',
description: offers[1].description ?? '',
image: offers[1].mobileBannerImage != null
? "${ApiUrls.baseUrl}/${offers[1].mobileBannerImage!}"
: '',
),
),
),
],
],
),
] else ...[
Row(
children: [
Expanded(
child: _offerCard(
title: 'No offers available',
description: '',
image: '',
),
),
],
),
],
SizedBox(height: 16.h),
@@ -210,6 +332,7 @@ class PassDetailsView extends StatelessWidget {
Navigator.pushNamed(
context,
RouteConstants.searchPassOffer,
arguments: city?.id ??"",
);
},
),
@@ -219,7 +342,7 @@ class PassDetailsView extends StatelessWidget {
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(
RouteConstants.privacyPolicy, // ✅ pass offerId
RouteConstants.privacyPolicy,
);
},
child: Center(
@@ -227,6 +350,7 @@ class PassDetailsView extends StatelessWidget {
"Learn about policies",
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
),
),
@@ -241,7 +365,10 @@ class PassDetailsView extends StatelessWidget {
);
}
return const Center(child: CircularProgressIndicator());
return const Scaffold(
backgroundColor: Colors.white,
body: Center(child: CircularProgressIndicator()),
);
},
);
}
@@ -279,22 +406,53 @@ class PassDetailsView extends StatelessWidget {
);
}
Widget _attractionCard() {
Widget _attractionCard({
required String title,
required String description,
required String image,
num? ticketPriceAdult,
num? ticketPriceChild,
String? bookingEmail,
String? bookingPhoneNumber,
}) {
// Check if booking is required (both email and phone are empty/null)
final bool isBookingRequired = (bookingEmail == null || bookingEmail.isEmpty) &&
(bookingPhoneNumber == null || bookingPhoneNumber.isEmpty);
// Format the price display
String priceText = ticketPriceAdult != null
? "from \$${ticketPriceAdult}/person"
: "Price not available";
return Container(
padding: EdgeInsets.all(12.w),
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
border: Border.all(color: const Color(0xffF2D6D6)),
),
child: Row(
children: [
/// 🔥 Attraction Image (Real Image Style Box)
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Image.asset(
"assets/images/aa4.png", // <-- your attraction image
height: 90.w,
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,
);
},
)
: Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
),
@@ -308,7 +466,7 @@ class PassDetailsView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Koh Rong Samloem",
title,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 14.sp,
@@ -318,17 +476,19 @@ class PassDetailsView extends StatelessWidget {
SizedBox(height: 2.h),
Text(
"Krong Siem Reap",
description,
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.grey.shade600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Text(
"from \$25/person",
priceText,
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
@@ -337,23 +497,25 @@ class PassDetailsView extends StatelessWidget {
SizedBox(height: 6.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
"Booking Required",
style: GoogleFonts.poppins(
fontSize: 10.sp,
color: Colors.blue.shade700,
// Show "Booking Required" tag only if both email and phone are null/empty
if (isBookingRequired)
Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.h,
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
"Booking Required",
style: GoogleFonts.poppins(
fontSize: 10.sp,
color: Colors.blue.shade700,
),
),
),
)
],
),
),
@@ -381,20 +543,31 @@ class PassDetailsView extends StatelessWidget {
);
}
Widget _infoChip({
required IconData icon,
required String imagePath, // 👈 image asset path
required String text,
bool isExpanded = false,
}) {
return Container(
width: isExpanded ? double.infinity : null,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xffF95F62)),
borderRadius: BorderRadius.circular(14.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize:
isExpanded ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment:
isExpanded ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Icon(icon, size: 14.sp, color: const Color(0xffF95F62)),
Image.asset(
imagePath,
height: 14.h,
width: 14.w,
color: const Color(0xffF95F62), // remove if your icon has its own color
),
SizedBox(width: 6.w),
Text(
text,
@@ -409,7 +582,11 @@ class PassDetailsView extends StatelessWidget {
);
}
Widget _offerCard() {
Widget _offerCard({
required String title,
required String description,
required String image,
}) {
return Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
@@ -419,13 +596,27 @@ class PassDetailsView extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// 🔥 Top Offer Image
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Image.asset(
"assets/images/aa4.png", // <-- your offer image
height: 120.h, // 🔥 closer to design ratio
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,
),
@@ -435,26 +626,30 @@ class PassDetailsView extends StatelessWidget {
/// 🔥 Title
Text(
"Astor Hotels Ultra Deluxe",
title,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 16.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 6.h),
/// 🔥 Description
Text(
"15% Discount on all treatments for first-time clients",
description,
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.grey.shade700,
height: 1.4,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
}

View File

@@ -2,19 +2,25 @@ 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';
import 'package:citycards_customer/core/route_constants.dart';
import 'package:citycards_customer/search_offers/bloc/offers_bloc.dart';
import 'package:citycards_customer/search_offers/bloc/offers_event.dart';
import 'package:citycards_customer/search_offers/bloc/offers_state.dart';
import 'package:citycards_customer/search_offers/repository/offers_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/common_app_texts.dart';
import '../../networkApiServices/api_urls.dart';
import '../blocs/myPassesOffers/my_passes_offers_bloc.dart';
import '../blocs/myPassesOffers/my_passes_offers_event.dart';
import '../blocs/myPassesOffers/my_passes_offers_state.dart';
import '../repository/my_passes_offers_repository.dart';
class PassOffersScreen extends StatefulWidget {
const PassOffersScreen({super.key});
final int cityId;
const PassOffersScreen({
super.key,
required this.cityId,
});
@override
State<PassOffersScreen> createState() => _PassOffersScreenState();
@@ -26,7 +32,8 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => OffersBloc(OffersRepository())..add(LoadOffers()),
create: (_) => MyPassesOffersBloc(MyPassesOffersRepository())
..add(LoadMyPassesOffers(cityXid: widget.cityId)),
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
@@ -62,82 +69,88 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
hintColor: const Color(0xFFF95F62).withOpacity(.6),
showSuffix: true,
onChanged: (value) {
context.read<OffersBloc>().add(SearchOffers(value));
context.read<MyPassesOffersBloc>().add(SearchMyPassesOffers(value));
},
),
),
SizedBox(height: 20.h),
/// Dynamic Categories
BlocBuilder<OffersBloc, OffersState>(
BlocBuilder<MyPassesOffersBloc, MyPassesOffersState>(
builder: (context, state) {
if (state is OffersLoaded) {
if (state is MyPassesOffersLoaded) {
final categories = state.categories;
if (categories.isEmpty) {
return SizedBox.shrink();
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...List.generate(categories.length, (index) {
final category = categories[index];
final isSelected =
selectedCategoryId == category.id;
return Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
...List.generate(categories.length, (index) {
final category = categories[index];
final isSelected =
selectedCategoryId == category.id;
return Padding(
padding: EdgeInsets.only(right: 8.0.w),
child: GestureDetector(
onTap: () {
setState(() {
if (selectedCategoryId == category.id) {
// Deselect if already selected
selectedCategoryId = null;
context
.read<OffersBloc>()
.add(LoadOffers());
} else {
// Select new category
selectedCategoryId = category.id;
context.read<OffersBloc>().add(
LoadOffers(
categoryXid: category.id),
);
}
});
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.h,
horizontal: 12.w,
),
decoration: BoxDecoration(
color: isSelected
? Color(0xFFF95F62)
: Color(0xFFFEE7E7),
borderRadius:
BorderRadius.circular(100.sp),
border: Border.all(
return Padding(
padding: EdgeInsets.only(right: 8.0.w),
child: GestureDetector(
onTap: () {
setState(() {
if (selectedCategoryId == category.id) {
// Deselect if already selected
selectedCategoryId = null;
context
.read<MyPassesOffersBloc>()
.add(LoadMyPassesOffers(cityXid: widget.cityId));
} else {
// Select new category
selectedCategoryId = category.id;
context.read<MyPassesOffersBloc>().add(
LoadMyPassesOffers(
cityXid: widget.cityId,
categoryXid: category.id,
),
);
}
});
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.h,
horizontal: 12.w,
),
decoration: BoxDecoration(
color: isSelected
? Color(0xFFF95F62)
: Color(0xFFFDCDCE),
: Color(0xFFFEE7E7),
borderRadius:
BorderRadius.circular(100.sp),
border: Border.all(
color: isSelected
? Color(0xFFF95F62)
: Color(0xFFFDCDCE),
),
),
),
child: Center(
child: CustomText(
text: category.categoryName,
color: isSelected
? Colors.white
: Color(0xFFF95F62),
child: Center(
child: CustomText(
text: category.categoryName,
color: isSelected
? Colors.white
: Color(0xFFF95F62),
),
),
),
),
),
);
}),
],
);
}),
],
),
),
);
}
@@ -149,9 +162,9 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
/// Offer list
Expanded(
child: BlocBuilder<OffersBloc, OffersState>(
child: BlocBuilder<MyPassesOffersBloc, MyPassesOffersState>(
builder: (context, state) {
if (state is OffersLoading) {
if (state is MyPassesOffersLoading) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xFFF95F62),
@@ -159,7 +172,7 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
);
}
if (state is OffersError) {
if (state is MyPassesOffersError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -171,7 +184,7 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
),
SizedBox(height: 16.h),
Text(
"Error: ${state.message}",
state.message,
style: TextStyle(
color: Colors.red,
fontSize: 14.sp,
@@ -183,7 +196,7 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
);
}
if (state is OffersLoaded) {
if (state is MyPassesOffersLoaded) {
final offers = state.offers;
if (offers.isEmpty) {
@@ -240,6 +253,7 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
borderRadius: BorderRadius.circular(12.sp),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius:
@@ -310,17 +324,64 @@ class _PassOffersScreenState extends State<PassOffersScreen> {
CustomText(
text: offer.title,
size: 18.sp,
maxLines: 2,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h),
CustomText(
text: offer.description,
color: Colors.black.withOpacity(.6),
size: 12.sp,
maxLines: 3,
overflow: TextOverflow.ellipsis,
Expanded(
child: CustomText(
text: offer.description,
color: Colors.black.withOpacity(.6),
size: 12.sp,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
if (offer.offerCode != null && offer.offerCode!.isNotEmpty) ...[
SizedBox(height: 8.h),
GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: offer.offerCode!),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Code copied: ${offer.offerCode!}"),
duration: Duration(seconds: 1),
backgroundColor: Color(0xFFF95F62),
),
);
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 6.h,
),
decoration: BoxDecoration(
color: Color(0xFFFEE7E7),
borderRadius: BorderRadius.circular(6.sp),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: CustomText(
text: offer.offerCode!,
size: 12.sp,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Icon(
Icons.copy,
size: 16.sp,
color: Color(0xFFF95F62),
),
],
),
),
),
],
],
),
),

View File

@@ -20,6 +20,16 @@ class PassAttractionCard extends StatelessWidget {
/// GALLERY IMAGE (handled safely in model)
final String imageUrl = attraction.coverImageUrl;
/// 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);
/// Format the price display
String priceText = attraction.ticketPriceAdult != null
? "from \$${attraction.ticketPriceAdult}/person"
: "Price not available";
return InkWell(
onTap: () {
Navigator.of(context).pushNamed(
@@ -29,85 +39,94 @@ class PassAttractionCard extends StatelessWidget {
},
child: Container(
margin: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.w),
padding: EdgeInsets.all(12.w),
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(15.r),
color: const Color(0xffFFF5F5),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(color: const Color(0xffF2D6D6)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// IMAGE (network with fallback)
/// 🔥 Attraction Image (Real Image Style Box)
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
borderRadius: BorderRadius.circular(12.r),
child: imageUrl.isNotEmpty
? Image.network(
imageUrl,
height: 94.h,
width: 94.w,
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _imageFallback(),
errorBuilder: (context, error, stackTrace) {
return _imageFallback();
},
)
: _imageFallback(),
),
SizedBox(width: 10.w),
SizedBox(width: 12.w),
/// CONTENT
/// 🔥 Text Section
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attraction.title,
style: TextStyle(
fontSize: 16.sp,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 14.sp,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2.h),
Text(
attraction.description,
style: GoogleFonts.poppins(
fontSize: 12.sp,
color: Colors.grey.shade600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Text(
priceText,
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 6.h),
Text(
attraction.address,
style: GoogleFonts.poppins(
fontSize: 12.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff464646),
/// TAGS (CARD TITLES) OR BOOKING REQUIRED
showBookingRequired
? Container(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.h,
),
),
SizedBox(height: 6.h),
Text.rich(
TextSpan(
children: [
TextSpan(
text: "from \$${attraction.ticketPriceAdult}",
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
TextSpan(
text: "/person",
style: TextStyle(
fontSize: 10.sp,
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
],
decoration: BoxDecoration(
color: const Color(0xffC1D2F8),
border: Border.all(
color: const Color(0xff2563EB),
),
borderRadius: BorderRadius.circular(20.r),
),
),
SizedBox(height: 6.h),
/// TAGS (CARD TITLES)
attraction.isBookingRequired == false
? Wrap(
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
@@ -130,8 +149,7 @@ class PassAttractionCard extends StatelessWidget {
? const Color(0xffF95FAF)
: const Color(0xffF95F62),
),
borderRadius:
BorderRadius.circular(20.r),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
tag,
@@ -144,48 +162,42 @@ class PassAttractionCard extends StatelessWidget {
),
)
.toList(),
)
: 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,
),
),
),
],
),
),
SizedBox(width: 8.w),
/// 🔥 QR Code Circle (Proper UI like Design)
Container(
height: 44.w,
width: 44.w,
decoration: const BoxDecoration(
color: Color(0xffF8EDED), // light pink circle bg
shape: BoxShape.circle,
),
child: Padding(
padding: EdgeInsets.all(10.w),
child: Image.asset(
"assets/images/qr_image.png",
fit: BoxFit.contain,
),
),
),
],
),
),
);
}
/// SAME PLACEHOLDER AS BEFORE
/// Image Fallback Widget
Widget _imageFallback() {
return Container(
height: 94.h,
width: 94.w,
color: Colors.grey.shade200,
child: Icon(
Icons.image_not_supported_outlined,
size: 28.sp,
color: Colors.grey,
),
return Image.asset(
"assets/images/aa4.png",
height: 100.w,
width: 90.w,
fit: BoxFit.cover,
);
}
}
}

View File

@@ -1,24 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import '../models/my_passes_model.dart';
class PassTicketCard extends StatelessWidget {
final dynamic pass;
final MyPassData pass;
const PassTicketCard({super.key, required this.pass});
@override
Widget build(BuildContext context) {
// Dimensions tuned to your screenshot
final double cardWidth = MediaQuery.of(context).size.width - 32.w;
final double topSectionHeight = 105.h; // where dotted line sits
final double topSectionHeight = 105.h;
final double bottomSectionHeight = 50.h;
final double cardHeight = topSectionHeight + bottomSectionHeight;
return SizedBox(
width: cardWidth,
child: CustomPaint(
// paints white background, border, corner radius, side cuts, shadow, and divider dots
painter: _TicketBackgroundPainter(
cornerRadius: 16.r,
notchRadius: 9.r,
@@ -27,7 +26,6 @@ class PassTicketCard extends StatelessWidget {
shadowColor: Colors.black.withOpacity(0.08),
),
child: ClipPath(
// actual clipping so child content never bleeds outside the shape
clipper: _TicketClipper(
cornerRadius: 16.r,
notchRadius: 9.r,
@@ -37,32 +35,36 @@ class PassTicketCard extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
child: Column(
children: [
// ---------- TOP SECTION ----------
SizedBox(
height: topSectionHeight - 12.h, // keep space for the dots line
height: topSectionHeight - 12.h,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(10.r),
child: Image.asset(
pass.imageUrl,
child: Image.network(
pass.city?.bannerImage ?? '',
height: 80.h,
width: 80.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 80.h,
width: 80.w,
color: Colors.grey[300],
child: Icon(Icons.image, size: 40),
);
},
),
),
SizedBox(width: 10.w),
// details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (pass.isActive)
if (pass.bookingStatus == "active")
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 3.h),
@@ -81,7 +83,7 @@ class PassTicketCard extends StatelessWidget {
),
SizedBox(width: 8.w),
Text(
pass.duration, // "2 Days"
"${pass.noOfDays ?? 0} Days",
style: GoogleFonts.poppins(
color: Colors.black87,
fontSize: 12.sp,
@@ -91,7 +93,9 @@ class PassTicketCard extends StatelessWidget {
),
SizedBox(height: 10.h),
Text(
pass.title,
"${(pass.cardMode?.isNotEmpty ?? false)
? pass.cardMode![0].toUpperCase() + pass.cardMode!.substring(1)
: ''} Card",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 18.sp,
@@ -100,7 +104,7 @@ class PassTicketCard extends StatelessWidget {
),
SizedBox(height: 4.h),
Text(
"Adults-${pass.adults} • Kids-${pass.kids}",
"Adults-${pass.totalAdult ?? 0} • Kids-${pass.totalChild ?? 0}",
style: GoogleFonts.poppins(
color: Colors.black54,
fontSize: 11.sp,
@@ -109,8 +113,6 @@ class PassTicketCard extends StatelessWidget {
],
),
),
// QR chip
CircleAvatar(
radius: 20.r,
backgroundColor: Color(0xffFEE7E7),
@@ -122,26 +124,21 @@ class PassTicketCard extends StatelessWidget {
],
),
),
// space exactly where the dotted line is painted by the painter
SizedBox(height: 15.h),
// ---------- BOTTOM SECTION ----------
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Valid Till: ${pass.validity}",
"Valid Till: ${pass.validUpto ?? ''}",
style: GoogleFonts.poppins(
fontSize: 11.sp,
color: Colors.black,
fontWeight: FontWeight.w400
),
fontSize: 11.sp,
color: Colors.black,
fontWeight: FontWeight.w400),
),
Text(
pass.city, // "Melbourne"
pass.city?.name ?? '',
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 13.sp,
@@ -159,7 +156,6 @@ class PassTicketCard extends StatelessWidget {
}
}
/// Clips the ticket with rounded corners and 2 side “cuts” centered at dividerY
class _TicketClipper extends CustomClipper<Path> {
final double cornerRadius;
final double notchRadius;
@@ -180,10 +176,11 @@ class _TicketClipper extends CustomClipper<Path> {
));
final cuts = Path()
..addOval(Rect.fromCircle(center: Offset(0, dividerY), radius: notchRadius))
..addOval(Rect.fromCircle(center: Offset(size.width, dividerY), radius: notchRadius));
..addOval(Rect.fromCircle(
center: Offset(0, dividerY), radius: notchRadius))
..addOval(Rect.fromCircle(
center: Offset(size.width, dividerY), radius: notchRadius));
// Rounded-rect MINUS the two circles
return Path.combine(PathOperation.difference, rrectPath, cuts);
}
@@ -194,8 +191,6 @@ class _TicketClipper extends CustomClipper<Path> {
dividerY != old.dividerY;
}
/// Paints fill, border, shadow and the dotted perforation line
class _TicketBackgroundPainter extends CustomPainter {
final double cornerRadius;
final double notchRadius;
@@ -224,35 +219,30 @@ class _TicketBackgroundPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
final path = _ticketPath(size);
// Realistic layered shadow
canvas.save();
canvas.translate(0, 2); // tiny downward offset for depth
canvas.translate(0, 2);
final shadowPaint = Paint()
..color = Colors.black.withOpacity(0.10)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6);
canvas.drawPath(path, shadowPaint);
canvas.restore();
// Subtle ambient shadow (light spread around)
final ambientShadowPaint = Paint()
..color = Colors.black.withOpacity(0.04)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
canvas.drawPath(path, ambientShadowPaint);
// Fill background
final fillPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xffFFFBFB);
canvas.drawPath(path, fillPaint);
// Border stroke
final strokePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 0.8
..color = const Color(0xffE5E5E5);
canvas.drawPath(path, strokePaint);
// 🔹 Dotted perforation line
final dashPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1
@@ -282,4 +272,4 @@ class _TicketBackgroundPainter extends CustomPainter {
borderColor != oldDelegate.borderColor ||
shadowColor != oldDelegate.shadowColor;
}
}
}

View File

@@ -1,7 +1,7 @@
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://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 refreshToken = "$baseUrl/auth/refresh";
@@ -10,15 +10,20 @@ class ApiUrls {
// static const upcomingCityList = "$baseUrl/mobile/upcoming_cities";
static const searchCityList = "$baseUrl/mobile/city-selection";
static const attractionsList = "$baseUrl/mobile/list/all";
static const passAttractionsList = "$baseUrl/mobile/passes/mobile/list";
static const attractionDetails = "$baseUrl/mobile/list";
static const home = "$baseUrl/mobile";
static const faqPrivacyTerms = "$baseUrl/mobile/user/cms-data";
static const userProfile = "$baseUrl/mobile/user";
static const offers = "$baseUrl/mobile/list/offers";
static const passOffers = "$baseUrl/mobile/passes/mobile/list/offers";
static const buyAPass = "$baseUrl/mobile/pass";
static const offersDetails = "$baseUrl/mobile/list/offers";
static const myPostCards = "$baseUrl/mobile/postcards/all";
static const coupons = "$baseUrl/mobile/passes/dropdown/card";
static const myPasses = "$baseUrl/mobile/passes/all";
static const passDetails = "$baseUrl/mobile/passes";
static const myPassesCart = "$baseUrl/mobile/passes/cart/passes";
static const myItineraries = "$baseUrl/mobile/itinerary/all-initineraries";
static const getItineraryCities = "$baseUrl/mobile/itinerary/cities-with-icons";

View File

@@ -24,7 +24,7 @@ class OffersDetailsView extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => OfferDetailsBloc(
repository: OffersDetailsRepository(), // Create directly
repository: OffersDetailsRepository(), // Create directly
)..add(FetchOfferDetailsEvent(offerId: offerId)),
child: const _OffersDetailsContent(),
);
@@ -106,12 +106,16 @@ class _OffersDetailsContent extends StatelessWidget {
),
),
SizedBox(width: 8.w),
Text(
offer.partnerName,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
Expanded(
child: Text(
offer.partnerName,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
@@ -125,6 +129,7 @@ class _OffersDetailsContent extends StatelessWidget {
Positioned(
bottom: 31.h,
left: 12.w,
right: 60.w,
child: Text(
offer.partnerName,
style: TextStyle(
@@ -133,6 +138,8 @@ class _OffersDetailsContent extends StatelessWidget {
fontWeight: FontWeight.w500,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
@@ -299,4 +306,4 @@ class _OffersDetailsContent extends StatelessWidget {
),
);
}
}
}

View File

@@ -0,0 +1,63 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/postcard_add_to_cart_repository.dart';
import 'add_to_cart_postcard_event.dart';
import 'add_to_cart_postcard_state.dart';
class AddToCartPostCardBloc
extends Bloc<AddToCartPostCardEvent, AddToCartPostCardState> {
final AddToCartPostCardRepository repository;
AddToCartPostCardBloc(this.repository)
: super(AddToCartPostCardInitial()) {
on<AddToCartPostCardRequested>(_onAddToCartRequested);
}
Future<void> _onAddToCartRequested(
AddToCartPostCardRequested event,
Emitter<AddToCartPostCardState> emit,
) async {
try {
emit(AddToCartPostCardLoading());
final response = await repository.addToCartPostCard(
countryName: event.countryName,
cityName: event.cityName,
stateName: event.stateName,
zipCode: event.zipCode,
address1: event.address1,
address2: event.address2,
pcTitle: event.pcTitle,
pcContent: event.pcContent,
pcImageFile: event.pcImageFile,
pcNumber: event.pcNumber,
pcDatetime: event.pcDatetime,
fullname: event.fullname,
emailAddress: event.emailAddress,
mobileNumber: event.mobileNumber,
isdCode: event.isdCode,
isForSelf: true, // API default
isDraft: true, // API default
baseAmount: 0,
totalTaxAmount: 0,
totalAmount: 0,
);
final postcard = response['postcard'];
emit(
AddToCartPostCardSuccess(
postcardId: postcard['id'],
pcNumber: postcard['pcNumber'],
baseAmount: (postcard['baseAmount'] as num).toDouble(),
totalTaxAmount: (postcard['totalTaxAmount'] as num).toDouble(),
totalAmount: (postcard['totalAmount'] as num).toDouble(),
pcDatetime: postcard['pcDatetime'],
),
);
} catch (e) {
log('❌ AddToCartPostCardBloc Error', error: e);
emit(AddToCartPostCardFailure(e.toString()));
}
}
}

View File

@@ -0,0 +1,64 @@
import 'dart:io';
import 'package:equatable/equatable.dart';
abstract class AddToCartPostCardEvent extends Equatable {
const AddToCartPostCardEvent();
@override
List<Object?> get props => [];
}
class AddToCartPostCardRequested extends AddToCartPostCardEvent {
final String countryName;
final String cityName;
final String stateName;
final String zipCode;
final String? address1;
final String? address2;
final String pcTitle;
final String pcContent;
final File pcImageFile;
final String pcNumber;
final String pcDatetime;
final String fullname;
final String emailAddress;
final String mobileNumber;
final String isdCode;
AddToCartPostCardRequested({
required this.countryName,
required this.cityName,
required this.stateName,
required this.zipCode,
this.address1,
this.address2,
required this.pcTitle,
required this.pcContent,
required this.pcImageFile,
required this.pcNumber,
required this.pcDatetime,
required this.fullname,
required this.emailAddress,
required this.mobileNumber,
required this.isdCode,
});
@override
List<Object?> get props => [
countryName,
cityName,
stateName,
zipCode,
address1,
address2,
pcTitle,
pcContent,
pcImageFile,
pcNumber,
pcDatetime,
fullname,
emailAddress,
mobileNumber,
isdCode,
];
}

View File

@@ -0,0 +1,48 @@
import 'package:equatable/equatable.dart';
abstract class AddToCartPostCardState extends Equatable {
const AddToCartPostCardState();
@override
List<Object?> get props => [];
}
class AddToCartPostCardInitial extends AddToCartPostCardState {}
class AddToCartPostCardLoading extends AddToCartPostCardState {}
class AddToCartPostCardSuccess extends AddToCartPostCardState {
final int postcardId;
final String pcNumber;
final double baseAmount;
final double totalTaxAmount;
final double totalAmount;
final String pcDatetime;
const AddToCartPostCardSuccess({
required this.postcardId,
required this.pcNumber,
required this.baseAmount,
required this.totalTaxAmount,
required this.totalAmount,
required this.pcDatetime,
});
@override
List<Object?> get props => [
postcardId,
pcNumber,
baseAmount,
totalTaxAmount,
totalAmount,
];
}
class AddToCartPostCardFailure extends AddToCartPostCardState {
final String message;
const AddToCartPostCardFailure(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -60,6 +60,7 @@ class PostcardCheckoutBloc
baseAmount: event.baseAmount,
totalTaxAmount: event.totalTaxAmount,
totalAmount: event.totalAmount,
postcardId: event.postcardId,
));
}
@@ -68,37 +69,19 @@ class PostcardCheckoutBloc
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
try {
// Validate that image file exists before submitting
if (state.pcImageFile == null) {
// Validate pcId exists
if (state.postcardId == null) {
emit(state.copyWith(
isLoading: false,
error: 'Please select a postcard image',
error: 'Postcard ID is missing',
isSuccess: false,
));
return;
}
final response = await repository.createPostCard(
countryName: state.countryName,
cityName: state.cityName,
stateName: state.stateName,
zipCode: state.zipCode,
address1: state.address1.isNotEmpty ? state.address1 : null,
address2: state.address2.isNotEmpty ? state.address2 : null,
pcTitle: state.pcTitle,
pcContent: state.pcContent,
pcImageFile: state.pcImageFile!,
pcNumber: state.pcNumber,
pcDatetime: state.pcDatetime,
fullname: state.fullname,
emailAddress: state.emailAddress,
mobileNumber: state.mobileNumber,
isdCode: state.isdCode,
isForSelf: state.isForSelf,
isDraft: true, // Save as draft
baseAmount: state.baseAmount,
totalTaxAmount: state.totalTaxAmount,
totalAmount: state.totalAmount,
pcId: state.postcardId!,
isDraft: true, // ⭐ Save as draft
);
// Extract order ID from response if available
@@ -126,67 +109,44 @@ class PostcardCheckoutBloc
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
try {
// Validate that image file exists before submitting
if (state.pcImageFile == null) {
// Validate pcId exists
if (state.postcardId == null) {
emit(state.copyWith(
isLoading: false,
error: 'Please select a postcard image',
error: 'Postcard ID is missing',
isSuccess: false,
));
return;
}
final response = await repository.createPostCard(
countryName: state.countryName,
cityName: state.cityName,
stateName: state.stateName,
zipCode: state.zipCode,
address1: state.address1.isNotEmpty ? state.address1 : null,
address2: state.address2.isNotEmpty ? state.address2 : null,
pcTitle: state.pcTitle,
pcContent: state.pcContent,
pcImageFile: state.pcImageFile!,
pcNumber: state.pcNumber,
pcDatetime: state.pcDatetime,
fullname: state.fullname,
emailAddress: state.emailAddress,
mobileNumber: state.mobileNumber,
isdCode: state.isdCode,
isForSelf: state.isForSelf,
isDraft: false, // Final submission (payment)
baseAmount: state.baseAmount,
totalTaxAmount: state.totalTaxAmount,
totalAmount: state.totalAmount,
pcId: state.postcardId!,
isDraft: false, // ⭐ Initiate payment
);
// 🆕 Parse response from backend
// Expected: {"postcardId": 16, "clientSecret": "pi_3Sx0yjRtCkWyT4Em1MKw1FeU_secret_S8M74wnEhTRC9lUz9RqJnuuqg"}
final postcardId = response['postcardId'] as int?;
final clientSecret = response['clientSecret'] as String?;
final status = response['status'] as String?;
// Also try alternative key names in case backend uses different naming
final orderId = response['orderId']?.toString() ??
response['order_id']?.toString() ??
response['id']?.toString();
// Validate clientSecret is present
if (clientSecret == null || clientSecret.isEmpty) {
emit(state.copyWith(
isLoading: false,
error: 'Payment initialization failed - no client secret received from server',
error: 'Payment initialization failed - no client secret received',
isSuccess: false,
));
return;
}
// 🆕 Emit success with clientSecret for payment processing
emit(state.copyWith(
isLoading: false,
isSuccess: true,
isDraft: false,
postcardId: postcardId,
clientSecret: clientSecret, // This will trigger payment flow
postcardId: postcardId ?? state.postcardId,
clientSecret: clientSecret,
orderId: orderId,
));
} catch (e) {

View File

@@ -44,7 +44,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent {
final String? address2;
final String? pcTitle;
final String? pcContent;
final File? pcImageFile; // ⭐ CHANGED: File instead of String
final File? pcImageFile;
final String? pcNumber;
final String? pcDatetime;
final String? fullname;
@@ -55,6 +55,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent {
final double? baseAmount;
final double? totalTaxAmount;
final double? totalAmount;
final int? postcardId; // ⭐ ADD THIS
UpdateCheckoutDataEvent({
this.countryName,
@@ -65,7 +66,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent {
this.address2,
this.pcTitle,
this.pcContent,
this.pcImageFile, // ⭐ CHANGED
this.pcImageFile,
this.pcNumber,
this.pcDatetime,
this.fullname,
@@ -76,6 +77,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent {
this.baseAmount,
this.totalTaxAmount,
this.totalAmount,
this.postcardId, // ⭐ ADD THIS
});
}

View File

@@ -247,6 +247,19 @@ class PostcardCreationBloc
on<TogglePurchaseOption>((event, emit) {
emit(state.copyWith(isGift: event.isGift));
});
on<StoreUserProfileData>((event, emit) {
emit(state.copyWith(
userProfileFullName: event.fullName,
userProfileEmail: event.email,
userProfilePhone: event.phone,
userProfileAddress: event.address,
userProfileCity: event.city,
userProfileState: event.state,
userProfileZipCode: event.zipCode,
userProfileCountry: event.country,
));
});
}
// Add this getter method in PostcardCreationBloc class

View File

@@ -68,4 +68,27 @@ class UpdatePostcardNumber extends PostcardCreationEvent {
final String pcNumber;
UpdatePostcardNumber(this.pcNumber);
}
// Event to store user profile data when "Buy for Myself" is selected
class StoreUserProfileData extends PostcardCreationEvent {
final String? fullName;
final String? email;
final String? phone;
final String? address;
final String? city;
final String? state;
final String? zipCode;
final String? country;
StoreUserProfileData({
this.fullName,
this.email,
this.phone,
this.address,
this.city,
this.state,
this.zipCode,
this.country,
});
}

View File

@@ -20,7 +20,17 @@ class PostcardCreationState {
final String? country;
final String? state;
final String? zipCode;
final String? pcNumber; // 🆕 ADD THIS
final String? pcNumber;
// User's profile data (for "Buy for Myself" option)
final String? userProfileFullName;
final String? userProfileEmail;
final String? userProfilePhone;
final String? userProfileAddress;
final String? userProfileCity;
final String? userProfileState;
final String? userProfileZipCode;
final String? userProfileCountry;
const PostcardCreationState({
required this.currentStep,
@@ -41,7 +51,16 @@ class PostcardCreationState {
this.state,
this.zipCode,
this.pcNumber,
required this.address, // 🆕 ADD THIS
required this.address,
// User profile data
this.userProfileFullName,
this.userProfileEmail,
this.userProfilePhone,
this.userProfileAddress,
this.userProfileCity,
this.userProfileState,
this.userProfileZipCode,
this.userProfileCountry,
});
PostcardCreationState copyWith({
@@ -63,7 +82,16 @@ class PostcardCreationState {
String? country,
String? state,
String? zipCode,
String? pcNumber, // 🆕 ADD THIS
String? pcNumber,
// User profile fields
String? userProfileFullName,
String? userProfileEmail,
String? userProfilePhone,
String? userProfileAddress,
String? userProfileCity,
String? userProfileState,
String? userProfileZipCode,
String? userProfileCountry,
}) {
return PostcardCreationState(
currentStep: currentStep ?? this.currentStep,
@@ -84,7 +112,16 @@ class PostcardCreationState {
country: country ?? this.country,
state: state ?? this.state,
zipCode: zipCode ?? this.zipCode,
pcNumber: pcNumber ?? this.pcNumber, // 🆕 ADD THIS
pcNumber: pcNumber ?? this.pcNumber,
// User profile data
userProfileFullName: userProfileFullName ?? this.userProfileFullName,
userProfileEmail: userProfileEmail ?? this.userProfileEmail,
userProfilePhone: userProfilePhone ?? this.userProfilePhone,
userProfileAddress: userProfileAddress ?? this.userProfileAddress,
userProfileCity: userProfileCity ?? this.userProfileCity,
userProfileState: userProfileState ?? this.userProfileState,
userProfileZipCode: userProfileZipCode ?? this.userProfileZipCode,
userProfileCountry: userProfileCountry ?? this.userProfileCountry,
);
}
}

View File

@@ -0,0 +1,203 @@
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../../networkApiServices/api_urls.dart';
import '../../networkApiServices/network_api_services.dart';
class AddToCartPostCardRepository {
final NetworkApiService _apiServices = NetworkApiService();
/// Create / Save Postcard (Draft or Final)
/// ⭐ UPDATED: Now uses multipart/form-data for file upload
Future<Map<String, dynamic>> addToCartPostCard({
required String countryName,
required String cityName,
required String stateName,
required String zipCode,
String? address1, // NOT required
String? address2, // NOT required
required String pcTitle,
required String pcContent,
required File pcImageFile, // ⭐ CHANGED: File instead of String
required String pcNumber,
required String pcDatetime,
required String fullname,
required String emailAddress,
required String mobileNumber,
required String isdCode,
required bool isForSelf,
required bool isDraft,
required double baseAmount,
required double totalTaxAmount,
required double totalAmount,
}) async {
try {
log('🟡 createPostCard() called');
if (kDebugMode) {
print('📤 [CREATE POSTCARD] Country: $countryName');
print('📤 [CREATE POSTCARD] City: $cityName');
print('📤 [CREATE POSTCARD] State: $stateName');
print('📤 [CREATE POSTCARD] Zip: $zipCode');
print('📤 [CREATE POSTCARD] Title: $pcTitle');
print('📤 [CREATE POSTCARD] Number: $pcNumber');
print('📤 [CREATE POSTCARD] Image File: ${pcImageFile.path}');
print('📤 [CREATE POSTCARD] Is Draft: $isDraft');
}
// ⭐ Create FormData for multipart/form-data upload
final formData = FormData();
// Add text fields
formData.fields.addAll([
MapEntry('countryName', countryName),
MapEntry('cityName', cityName),
MapEntry('stateName', stateName),
MapEntry('zipCode', zipCode),
MapEntry('pcTitle', pcTitle),
MapEntry('pcContent', pcContent),
MapEntry('pcNumber', pcNumber),
MapEntry('pcDatetime', pcDatetime),
MapEntry('fullname', fullname),
MapEntry('emailAddress', emailAddress),
MapEntry('mobileNumber', mobileNumber),
MapEntry('isdCode', isdCode),
MapEntry('isForSelf', isForSelf.toString()),
MapEntry('isDraft', 'true'),
MapEntry('isAddedToCart', 'true'),
]);
// Add optional address fields only if they are not null
if (address1 != null && address1.isNotEmpty) {
formData.fields.add(MapEntry('address1', address1));
}
if (address2 != null && address2.isNotEmpty) {
formData.fields.add(MapEntry('address2', address2));
}
// ⭐ Add postcard image file
final fileName = pcImageFile.path.split('/').last;
formData.files.add(
MapEntry(
'pcImage',
await MultipartFile.fromFile(
pcImageFile.path,
filename: fileName,
),
),
);
if (kDebugMode) {
print('📤 [CREATE POSTCARD] ✅ Postcard Image File Added');
print('📤 [CREATE POSTCARD] File Name: $fileName');
print('📤 [CREATE POSTCARD] File Path: ${pcImageFile.path}');
final fileSize = await pcImageFile.length();
print('📤 [CREATE POSTCARD] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB');
}
// ⭐ Log complete payload details
log('📦 Request Payload Summary:');
log('📦 Total Fields: ${formData.fields.length}');
log('📦 Total Files: ${formData.files.length}');
log('📦 Field Details:');
for (var field in formData.fields) {
log(' - ${field.key}: ${field.value}');
}
log('📦 File Details:');
for (var file in formData.files) {
log(' - ${file.key}: ${file.value.filename} (${file.value.length} bytes)');
}
log('🌐 API URL: ${ApiUrls.createPostCard}');
// ⭐ Send as multipart/form-data
final response = await _apiServices.postApi(
url: ApiUrls.createPostCard,
data: formData,
);
log('✅ API Response Status: ${response.statusCode}');
log('📥 API Response Data: ${response.data}');
if (kDebugMode) {
print('📤 [CREATE POSTCARD] ✅ Response Status: Success');
print('📤 [CREATE POSTCARD] Full Response: ${response.data}');
}
return response.data as Map<String, dynamic>;
} catch (e, stackTrace) {
log(
'❌ createPostCard FAILED',
error: e,
stackTrace: stackTrace,
);
throw Exception('Failed to create postcard: $e');
}
}
/// 🆕 Confirm Payment after successful Stripe payment
/// POST https://devapi.citycards.betadelivery.com/mobile/postcards/{postcardId}/confirm-payment
Future<Map<String, dynamic>> confirmPayment({
required int postcardId,
required String stripeStatus,
required String paymentStatus,
}) async {
try {
log('🟢 confirmPayment() called');
log('📤 [CONFIRM PAYMENT] Postcard ID: $postcardId');
log('📤 [CONFIRM PAYMENT] Stripe Status: $stripeStatus');
log('📤 [CONFIRM PAYMENT] Payment Status: $paymentStatus');
// Construct URL with postcardId
final url = '${ApiUrls.baseUrl}/mobile/postcards/$postcardId/confirm-payment';
// Note: Update ApiUrls class if you want to use a constant instead
// final url = ApiUrls.confirmPayment(postcardId);
if (kDebugMode) {
print('📤 [CONFIRM PAYMENT] API URL: $url');
}
// Request body
final requestBody = {
'stripeStatus': stripeStatus,
'paymentStatus': paymentStatus,
};
log('📦 Request Body: $requestBody');
// Send POST request
final response = await _apiServices.postApi(
url: url,
data: requestBody,
);
log('✅ [CONFIRM PAYMENT] Response Status: ${response.statusCode}');
log('📥 [CONFIRM PAYMENT] Response Data: ${response.data}');
if (kDebugMode) {
print('📤 [CONFIRM PAYMENT] ✅ Payment confirmation successful');
print('📤 [CONFIRM PAYMENT] Full Response: ${response.data}');
}
return response.data as Map<String, dynamic>;
} catch (e, stackTrace) {
log(
'❌ confirmPayment FAILED',
error: e,
stackTrace: stackTrace,
);
throw Exception('Failed to confirm payment: $e');
}
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
@@ -9,132 +10,44 @@ import '../../networkApiServices/network_api_services.dart';
class CreatePostCardRepository {
final NetworkApiService _apiServices = NetworkApiService();
/// Create / Save Postcard (Draft or Final)
/// ⭐ UPDATED: Now uses multipart/form-data for file upload
/// ============================================================
/// Create / Update Postcard (Draft or Final)
/// Uses multipart/form-data
/// URL requires pcId
/// ============================================================
/// ============================================================
/// Create / Update Postcard (Draft or Pay)
/// POST /mobile/postcards/{pcId}/draft-or-pay
/// Payload: { "isDraft": true/false }
/// ============================================================
Future<Map<String, dynamic>> createPostCard({
required String countryName,
required String cityName,
required String stateName,
required String zipCode,
String? address1, // NOT required
String? address2, // NOT required
required String pcTitle,
required String pcContent,
required File pcImageFile, // ⭐ CHANGED: File instead of String
required String pcNumber,
required String pcDatetime,
required String fullname,
required String emailAddress,
required String mobileNumber,
required String isdCode,
required bool isForSelf,
required int pcId,
required bool isDraft,
required double baseAmount,
required double totalTaxAmount,
required double totalAmount,
}) async {
try {
log('🟡 createPostCard() called');
log('🆔 Postcard ID: $pcId');
log('📝 isDraft: $isDraft');
if (kDebugMode) {
print('📤 [CREATE POSTCARD] Country: $countryName');
print('📤 [CREATE POSTCARD] City: $cityName');
print('📤 [CREATE POSTCARD] State: $stateName');
print('📤 [CREATE POSTCARD] Zip: $zipCode');
print('📤 [CREATE POSTCARD] Title: $pcTitle');
print('📤 [CREATE POSTCARD] Number: $pcNumber');
print('📤 [CREATE POSTCARD] Image File: ${pcImageFile.path}');
print('📤 [CREATE POSTCARD] Is Draft: $isDraft');
}
// ============================
// API Call
// ============================
final url = '${ApiUrls.baseUrl}/mobile/postcards/$pcId/draft-or-pay';
// ⭐ Create FormData for multipart/form-data upload
final formData = FormData();
final requestBody = {
'isDraft': isDraft,
};
// Add text fields
formData.fields.addAll([
MapEntry('countryName', countryName),
MapEntry('cityName', cityName),
MapEntry('stateName', stateName),
MapEntry('zipCode', zipCode),
MapEntry('pcTitle', pcTitle),
MapEntry('pcContent', pcContent),
MapEntry('pcNumber', pcNumber),
MapEntry('pcDatetime', pcDatetime),
MapEntry('fullname', fullname),
MapEntry('emailAddress', emailAddress),
MapEntry('mobileNumber', mobileNumber),
MapEntry('isdCode', isdCode),
MapEntry('isForSelf', isForSelf.toString()),
MapEntry('isDraft', isDraft.toString()),
MapEntry('baseAmount', baseAmount.toString()),
MapEntry('totalTaxAmount', totalTaxAmount.toString()),
MapEntry('totalAmount', totalAmount.toString()),
]);
log('🌐 API URL: $url');
log('📦 Request Body: $requestBody');
// Add optional address fields only if they are not null
if (address1 != null && address1.isNotEmpty) {
formData.fields.add(MapEntry('address1', address1));
}
if (address2 != null && address2.isNotEmpty) {
formData.fields.add(MapEntry('address2', address2));
}
// ⭐ Add postcard image file
final fileName = pcImageFile.path.split('/').last;
formData.files.add(
MapEntry(
'pcImage',
await MultipartFile.fromFile(
pcImageFile.path,
filename: fileName,
),
),
final response = await _apiServices.putApi(
url: url,
data: requestBody,
);
if (kDebugMode) {
print('📤 [CREATE POSTCARD] ✅ Postcard Image File Added');
print('📤 [CREATE POSTCARD] File Name: $fileName');
print('📤 [CREATE POSTCARD] File Path: ${pcImageFile.path}');
final fileSize = await pcImageFile.length();
print('📤 [CREATE POSTCARD] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB');
}
// ⭐ Log complete payload details
log('📦 Request Payload Summary:');
log('📦 Total Fields: ${formData.fields.length}');
log('📦 Total Files: ${formData.files.length}');
log('📦 Field Details:');
for (var field in formData.fields) {
log(' - ${field.key}: ${field.value}');
}
log('📦 File Details:');
for (var file in formData.files) {
log(' - ${file.key}: ${file.value.filename} (${file.value.length} bytes)');
}
log('🌐 API URL: ${ApiUrls.createPostCard}');
// ⭐ Send as multipart/form-data
final response = await _apiServices.postApi(
url: ApiUrls.createPostCard,
data: formData,
);
log('✅ API Response Status: ${response.statusCode}');
log('📥 API Response Data: ${response.data}');
if (kDebugMode) {
print('📤 [CREATE POSTCARD] ✅ Response Status: Success');
print('📤 [CREATE POSTCARD] Full Response: ${response.data}');
}
log('✅ API Status: ${response.statusCode}');
log('📥 API Response: ${response.data}');
return response.data as Map<String, dynamic>;
} catch (e, stackTrace) {
@@ -147,8 +60,10 @@ class CreatePostCardRepository {
}
}
/// 🆕 Confirm Payment after successful Stripe payment
/// POST https://devapi.citycards.betadelivery.com/mobile/postcards/{postcardId}/confirm-payment
/// ============================================================
/// Confirm Stripe Payment
/// POST /mobile/postcards/{postcardId}/confirm-payment
/// ============================================================
Future<Map<String, dynamic>> confirmPayment({
required int postcardId,
required String stripeStatus,
@@ -156,41 +71,26 @@ class CreatePostCardRepository {
}) async {
try {
log('🟢 confirmPayment() called');
log('📤 [CONFIRM PAYMENT] Postcard ID: $postcardId');
log('📤 [CONFIRM PAYMENT] Stripe Status: $stripeStatus');
log('📤 [CONFIRM PAYMENT] Payment Status: $paymentStatus');
log('🆔 Postcard ID: $postcardId');
// Construct URL with postcardId
final url = '${ApiUrls.baseUrl}/mobile/postcards/$postcardId/confirm-payment';
final url =
'${ApiUrls.baseUrl}/mobile/postcards/$postcardId/confirm-payment';
// Note: Update ApiUrls class if you want to use a constant instead
// final url = ApiUrls.confirmPayment(postcardId);
if (kDebugMode) {
print('📤 [CONFIRM PAYMENT] API URL: $url');
}
// Request body
final requestBody = {
'stripeStatus': stripeStatus,
'paymentStatus': paymentStatus,
};
log('🌐 API URL: $url');
log('📦 Request Body: $requestBody');
// Send POST request
final response = await _apiServices.postApi(
url: url,
data: requestBody,
);
log('[CONFIRM PAYMENT] Response Status: ${response.statusCode}');
log('📥 [CONFIRM PAYMENT] Response Data: ${response.data}');
if (kDebugMode) {
print('📤 [CONFIRM PAYMENT] ✅ Payment confirmation successful');
print('📤 [CONFIRM PAYMENT] Full Response: ${response.data}');
}
log('Payment Confirmed: ${response.statusCode}');
log('📥 Response: ${response.data}');
return response.data as Map<String, dynamic>;
} catch (e, stackTrace) {
@@ -202,4 +102,4 @@ class CreatePostCardRepository {
throw Exception('Failed to confirm payment: $e');
}
}
}
}

View File

@@ -31,7 +31,27 @@ class AddFilterStepPageView extends StatelessWidget {
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true),
StepProgressBar(totalSteps: 4, currentStep: 2),
const SizedBox(height: 24),
GestureDetector(
onTap: () {
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Text(
"Add a Filter",
style: TextStyle(

View File

@@ -42,59 +42,67 @@ class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
SizedBox(height: 29.h),
// Postcard Number with Action Icons
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"#${widget.postcard.pcNumber}",
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
children: [
/// PC Number (takes only available space)
Expanded(
child: Text(
widget.postcard.pcNumber,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: Colors.black,
fontSize: 18.sp,
fontWeight: FontWeight.w400,
),
),
Row(
children: [
GestureDetector(
onTap: () {
// Delete functionality
},
child: Image.asset(
'assets/icons/delete_icon.png',
width: 24,
height: 24,
),
),
SizedBox(width: 12.w),
/// Action Icons
Row(
children: [
GestureDetector(
onTap: () {
// Delete functionality
},
child: Image.asset(
'assets/icons/delete_icon.png',
width: 24,
height: 24,
),
SizedBox(width: 16.w),
GestureDetector(
onTap: () {
// Edit functionality
},
child: Image.asset(
'assets/icons/edit_icon.png',
width: 24,
height: 24,
),
),
SizedBox(width: 16.w),
GestureDetector(
onTap: () {
// Edit functionality
},
child: Image.asset(
'assets/icons/edit_icon.png',
width: 24,
height: 24,
),
SizedBox(width: 16.w),
GestureDetector(
onTap: () {
// Send functionality
},
child: Image.asset(
'assets/icons/send_icon.png',
width: 24,
height: 24,
),
),
SizedBox(width: 16.w),
GestureDetector(
onTap: () {
// Send functionality
},
child: Image.asset(
'assets/icons/send_icon.png',
width: 24,
height: 24,
),
],
),
],
),
),
],
),
],
),
SizedBox(height: 20.h),
),
SizedBox(height: 20.h),
// Flip buttons
Padding(
@@ -112,14 +120,14 @@ class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
children: [
Icon(
Icons.arrow_back,
color: !showBack ? const Color(0xffF95F62) : Colors.grey[400],
color: !showBack ? Colors.grey[400] : const Color(0xffF95F62),
size: 20,
),
SizedBox(width: 6.w),
Text(
'Flip',
style: GoogleFonts.poppins(
color: !showBack ? const Color(0xffF95F62) : Colors.grey[400],
color: !showBack ? Colors.grey[400] : const Color(0xffF95F62),
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
@@ -138,7 +146,7 @@ class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
Text(
'Flip',
style: GoogleFonts.poppins(
color: showBack ? const Color(0xffF95F62) : Colors.grey[400],
color: showBack ? Colors.grey[400] : const Color(0xffF95F62),
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
@@ -146,7 +154,7 @@ class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
SizedBox(width: 6.w),
Icon(
Icons.arrow_forward,
color: showBack ? const Color(0xffF95F62) : Colors.grey[400],
color: showBack ? Colors.grey[400] : const Color(0xffF95F62),
size: 20,
),
],

View File

@@ -58,7 +58,7 @@ class OrderSuccessPageView extends StatelessWidget {
text: "Your order has been placed. Your order\nid is ",
),
TextSpan(
text: "#${state.pcNumber ?? 'N/A'}", // 🆕 USE DYNAMIC VALUE
text: state.pcNumber ?? 'N/A', // 🆕 USE DYNAMIC VALUE
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xff585858),
@@ -86,9 +86,13 @@ class OrderSuccessPageView extends StatelessWidget {
angle: 0.20,
child: BackCardWidget(
message: state.message ?? "",
state: "State",
country: "country",
city: "City",
state: state.state??"",
country: state.country??"",
city: state.city??"",
selectedFont: state.selectedFont,
pincode: state.zipCode??"",
name: state.fullName??"",
address: state.address,
key: const ValueKey('back'),
// selectedFont: state.selectedFont,
),

View File

@@ -41,6 +41,7 @@ class PostcardCheckoutPageView extends StatefulWidget {
final double baseAmount;
final double totalTaxAmount;
final double totalAmount;
final int? postcardId;
const PostcardCheckoutPageView({
super.key,
@@ -61,6 +62,7 @@ class PostcardCheckoutPageView extends StatefulWidget {
required this.baseAmount,
required this.totalTaxAmount,
required this.totalAmount,
required this.postcardId,
});
@override
@@ -102,6 +104,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
baseAmount: widget.baseAmount,
totalTaxAmount: widget.totalTaxAmount,
totalAmount: widget.totalAmount,
postcardId: widget.postcardId,
),
);
});
@@ -302,13 +305,13 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
),
);
final bloc = context.read<PostcardCheckoutBloc>();
bloc.add(
ConfirmPaymentEvent(
stripeStatus: 'requires_payment_method',
paymentStatus: 'failed',
),
);
// final bloc = context.read<PostcardCheckoutBloc>();
// bloc.add(
// ConfirmPaymentEvent(
// stripeStatus: 'requires_payment_method',
// paymentStatus: 'failed',
// ),
// );
}
}
@@ -382,7 +385,27 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
isProfilePage: false,
showDivider: true,
),
// Header
GestureDetector(
onTap: () {
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -426,6 +449,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
address: creationState.address,
name: widget.fullname,
pincode: widget.zipCode,
selectedFont: creationState.selectedFont,
key: const ValueKey('back'),
// selectedFont: creationState.selectedFont,
),

View File

@@ -7,10 +7,12 @@ import 'package:citycards_customer/postcard/views/upload_photo_step_page_view.da
import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart';
import '../blocs/addToCartPostcard/add_to_cart_postcard_state.dart';
import '../blocs/postcardCheckout/postcard_checkout_bloc.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_state.dart';
import '../repository/postcard_add_to_cart_repository.dart';
import '../repository/postcard_checkout_repository.dart';
import 'my_postcards_view.dart';
import 'order_success_page_view.dart';
@@ -20,8 +22,17 @@ class PostcardCreationPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => PostcardCreationBloc(),
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => PostcardCreationBloc(),
),
BlocProvider(
create: (_) => AddToCartPostCardBloc(
AddToCartPostCardRepository(),
),
),
],
child: BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
Widget stepWidget;
@@ -39,9 +50,40 @@ class PostcardCreationPage extends StatelessWidget {
stepWidget = const PreviewPostcardStepPageView();
break;
case PostcardStep.purchase:
stepWidget = const PostcardPurchaseFormPageView();
// If buying for myself (isGift = false), use user profile data
// Otherwise, leave fields empty for gift recipient
stepWidget = PostcardPurchaseFormPageView(
initialFullName: !state.isGift ? state.userProfileFullName : null,
initialEmail: !state.isGift ? state.userProfileEmail : null,
initialPhone: !state.isGift ? state.userProfilePhone : null,
initialAddress: !state.isGift ? state.userProfileAddress : null,
initialCity: !state.isGift ? state.userProfileCity : null,
initialState: !state.isGift ? state.userProfileState : null,
initialZipCode: !state.isGift ? state.userProfileZipCode : null,
initialCountry: !state.isGift ? state.userProfileCountry : null,
);
break;
case PostcardStep.checkout:
// Get the cart state to access response data
final cartState = context.read<AddToCartPostCardBloc>().state;
// Extract values from the cart response or use defaults
String pcNumber = '12';
String pcDatetime = '';
double baseAmount = 50;
double totalTaxAmount = 20;
double totalAmount = 30;
int? postcardId;
if (cartState is AddToCartPostCardSuccess) {
pcNumber = cartState.pcNumber;
pcDatetime = cartState.pcDatetime;
baseAmount = cartState.baseAmount;
totalTaxAmount = cartState.totalTaxAmount;
totalAmount = cartState.totalAmount;
postcardId = cartState.postcardId;
}
stepWidget = BlocProvider(
create: (_) => PostcardCheckoutBloc(
repository: CreatePostCardRepository(),
@@ -51,17 +93,20 @@ class PostcardCreationPage extends StatelessWidget {
cityName: state.city ?? 'N/A',
stateName: state.state ?? 'N/A',
zipCode: state.zipCode ?? 'N/A',
address1: state.address, // ✅ Add this
address2: '', // ✅ Add this (or pass actual value if you have it)
pcTitle: state.pcTitle ?? 'N/A',
pcNumber: '12',
pcDatetime: '2008-11-20',
pcNumber: pcNumber,
pcDatetime: pcDatetime,
fullname: state.fullName ?? 'N/A',
emailAddress: state.emailId ?? 'N/A',
mobileNumber: state.phoneNumber ?? 'N/A',
isdCode: '+91',
isForSelf: !state.isGift,
totalTaxAmount: 20,
baseAmount: 50,
totalAmount: 30,
totalTaxAmount: totalTaxAmount,
baseAmount: baseAmount,
totalAmount: totalAmount,
postcardId: postcardId,
),
);
break;
@@ -74,7 +119,7 @@ class PostcardCreationPage extends StatelessWidget {
break;
case PostcardStep.myOrderPostcardPreview:
stepWidget = const OrderPostcardPreviewPageView();
}
}
return Scaffold(
backgroundColor: Colors.white,
@@ -84,4 +129,4 @@ class PostcardCreationPage extends StatelessWidget {
),
);
}
}
}

View File

@@ -3,13 +3,36 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import '../../common_packages/app_bar.dart';
import '../blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart';
import '../blocs/addToCartPostcard/add_to_cart_postcard_event.dart';
import '../blocs/addToCartPostcard/add_to_cart_postcard_state.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_events.dart';
import '../blocs/postcard_creation_state.dart';
class PostcardPurchaseFormPageView extends StatefulWidget {
const PostcardPurchaseFormPageView({super.key});
final String? initialFullName;
final String? initialEmail;
final String? initialPhone;
final String? initialAddress;
final String? initialCity;
final String? initialState;
final String? initialZipCode;
final String? initialCountry;
const PostcardPurchaseFormPageView({
super.key,
this.initialFullName,
this.initialEmail,
this.initialPhone,
this.initialAddress,
this.initialCity,
this.initialState,
this.initialZipCode,
this.initialCountry,
});
@override
State<PostcardPurchaseFormPageView> createState() => _PostcardPurchaseFormPageViewState();
@@ -30,6 +53,20 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
String? _selectedCountry;
String? _selectedState;
@override
void initState() {
super.initState();
// Initialize controllers with prefill values
_fullNameController.text = widget.initialFullName ?? '';
_emailController.text = widget.initialEmail ?? '';
_phoneController.text = widget.initialPhone ?? '';
_addressController.text = widget.initialAddress ?? '';
_cityController.text = widget.initialCity ?? '';
_zipCodeController.text = widget.initialZipCode ?? '';
_selectedState = widget.initialState;
_selectedCountry = widget.initialCountry;
}
@override
void dispose() {
_titleController.dispose();
@@ -46,179 +83,256 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
Widget build(BuildContext context) {
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
final creationBloc = context.read<PostcardCreationBloc>();
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
const SizedBox(height: 20),
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: state.imagePath != null
? Image.file(
File(state.imagePath!),
height: 70,
width: 70,
fit: BoxFit.cover,
)
: Container(
height: 70,
width: 70,
color: const Color(0xffFEE7E7),
child: const Icon(Icons.image_outlined,
color: Color(0xffFDCDCE)),
return BlocListener<AddToCartPostCardBloc, AddToCartPostCardState>(
listener: (context, cartState) {
if (cartState is AddToCartPostCardSuccess) {
// Update the postcard number in creation bloc
creationBloc.add(UpdatePostcardNumber(cartState.pcNumber));
// Navigate to next step (checkout)
creationBloc.add(GoToNextStep());
} else if (cartState is AddToCartPostCardFailure) {
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(cartState.message),
backgroundColor: Colors.red,
),
);
}
},
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showDivider: true,
),
GestureDetector(
onTap: () {
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _titleController,
decoration: InputDecoration(
hintText: "Add title",
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999), fontSize: 14.sp),
enabledBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: Color(0xffFDCDCE), width: 1),
),
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: state.imagePath != null
? Image.file(
File(state.imagePath!),
height: 70,
width: 70,
fit: BoxFit.cover,
)
: Container(
height: 70,
width: 70,
color: const Color(0xffFEE7E7),
child: const Icon(Icons.image_outlined,
color: Color(0xffFDCDCE)),
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _titleController,
decoration: InputDecoration(
hintText: "Add title",
hintStyle: GoogleFonts.poppins(
color: const Color(0xff999999), fontSize: 14.sp),
enabledBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: Color(0xffFDCDCE), width: 1),
),
focusedBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: Color(0xffFDCDCE), width: 1),
),
),
focusedBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: Color(0xffFDCDCE), width: 1),
style: GoogleFonts.poppins(fontSize: 14.sp),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
),
),
],
),
const SizedBox(height: 28),
// Personal details section
Text(
"Recipient Details",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 16),
_buildInputField(
label: "Recipient",
hint: "Enter the recipient's name",
controller: _fullNameController,
),
_buildInputField(
label: "Email",
hint: "eg: Jay@gmail.com",
controller: _emailController,
keyboardType: TextInputType.emailAddress,
),
_buildInputField(
label: "Phone number",
hint: "eg: +91 9999 999 999",
controller: _phoneController,
keyboardType: TextInputType.phone,
),
_buildInputField(
label: "Address",
hint: "Enter the recipient's Address",
controller: _addressController,
),
_buildInputField(
label: "City",
hint: "Enter the name of your city",
controller: _cityController,
),
_buildDropdownField(
label: "State",
hint: "Select your state",
value: _selectedState,
onChanged: (val) {
setState(() {
_selectedState = val;
});
},
),
_buildInputField(
label: "Zip Code",
hint: "Enter the Zip Code you reside in",
controller: _zipCodeController,
keyboardType: TextInputType.number,
),
_buildDropdownField(
label: "Country",
hint: "Select your country",
value: _selectedCountry,
onChanged: (val) {
setState(() {
_selectedCountry = val;
});
},
),
const SizedBox(height: 24),
// Next button
BlocBuilder<AddToCartPostCardBloc, AddToCartPostCardState>(
builder: (context, cartState) {
final isLoading = cartState is AddToCartPostCardLoading;
final addToCartBloc = context.read<AddToCartPostCardBloc>();
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading
? null
: () {
creationBloc.add(
UpdatePurchaseFormData(
pcTitle: _titleController.text,
fullName: _fullNameController.text,
emailId: _emailController.text,
phoneNumber: _phoneController.text,
address: _addressController.text,
city: _cityController.text,
state: _selectedState,
zipCode: _zipCodeController.text,
country: _selectedCountry,
),
);
if (_formKey.currentState!.validate()) {
final currentDate = DateFormat('yyyy-MM-dd').format(DateTime.now());
addToCartBloc.add(
AddToCartPostCardRequested(
countryName: _selectedCountry ?? '',
cityName: _cityController.text,
stateName: _selectedState ?? '',
zipCode: _zipCodeController.text,
address1: _addressController.text,
address2: null,
pcTitle: _titleController.text,
pcContent: creationBloc.getFormattedMessage(),
pcImageFile: File(state.imagePath!),
pcNumber: '12',
pcDatetime: currentDate,
fullname: _fullNameController.text,
emailAddress: _emailController.text,
mobileNumber: _phoneController.text,
isdCode: '+91',
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
"Next",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
style: GoogleFonts.poppins(fontSize: 14.sp),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
),
),
],
),
const SizedBox(height: 28),
// Personal details section
Text(
"Recipient Details",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: const Color(0xff1A1A1A),
),
),
const SizedBox(height: 16),
_buildInputField(
label: "Recipient",
hint: "Enter the recipient's name",
controller: _fullNameController,
),
_buildInputField(
label: "Email",
hint: "eg: Jay@gmail.com",
controller: _emailController,
keyboardType: TextInputType.emailAddress,
),
_buildInputField(
label: "Phone number",
hint: "eg: +91 9999 999 999",
controller: _phoneController,
keyboardType: TextInputType.phone,
),
_buildInputField(
label: "Address",
hint: "Enter the recipient's Address",
controller: _addressController,
),
_buildInputField(
label: "City",
hint: "Enter the name of your city",
controller: _cityController,
),
_buildDropdownField(
label: "State",
hint: "Select your state",
value: _selectedState,
onChanged: (val) {
setState(() {
_selectedState = val;
});
},
),
_buildInputField(
label: "Zip Code",
hint: "Enter the Zip Code you reside in",
controller: _zipCodeController,
keyboardType: TextInputType.number,
),
_buildDropdownField(
label: "Country",
hint: "Select your country",
value: _selectedCountry,
onChanged: (val) {
setState(() {
_selectedCountry = val;
});
},
),
const SizedBox(height: 30),
// Next Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Update the bloc with form data
bloc.add(UpdatePurchaseFormData(
pcTitle: _titleController.text,
fullName: _fullNameController.text,
emailId: _emailController.text,
phoneNumber: _phoneController.text,
address: _addressController.text,
city: _cityController.text,
country: _selectedCountry ?? '',
state: _selectedState ?? '',
zipCode: _zipCodeController.text,
));
// Navigate to next step
bloc.add(GoToNextStep());
}
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: Text(
"Next",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
],
],
),
),
),
),
@@ -347,9 +461,23 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
fontSize: 14.sp,
),
),
items: const [
items: label == "Country"
? const [
DropdownMenuItem(value: "Australia", child: Text("Australia")),
]
: label == "State"
? const [
DropdownMenuItem(value: "New South Wales", child: Text("New South Wales")),
DropdownMenuItem(value: "Victoria", child: Text("Victoria")),
DropdownMenuItem(value: "Queensland", child: Text("Queensland")),
DropdownMenuItem(value: "South Australia", child: Text("South Australia")),
DropdownMenuItem(value: "Western Australia", child: Text("Western Australia")),
DropdownMenuItem(value: "Tasmania", child: Text("Tasmania")),
DropdownMenuItem(value: "Northern Territory", child: Text("Northern Territory")),
DropdownMenuItem(value: "Australian Capital Territory", child: Text("Australian Capital Territory")),
]
: const [
DropdownMenuItem(value: "Lorem Ipsum", child: Text("Lorem Ipsum")),
// Add more items as needed
],
onChanged: onChanged,
validator: (value) {

View File

@@ -7,6 +7,7 @@ import 'package:google_fonts/google_fonts.dart';
import '../../common_packages/app_bar.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_events.dart';
import '../blocs/postcard_creation_state.dart';
import '../widgets/back_card_widget.dart';
import '../widgets/front_card_widget.dart';
@@ -38,7 +39,27 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
StepProgressBar(totalSteps: 4, currentStep: 4),
const SizedBox(height: 24),
GestureDetector(
onTap: () {
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Text(
"Preview your Postcard",
@@ -103,6 +124,7 @@ class _PreviewPostcardStepPageViewState extends State<PreviewPostcardStepPageVie
city: state.city??"",
country: state.country??"",
state:state.state??"",
selectedFont: state.selectedFont,
// selectedFont: state.selectedFont,
):
FrontCardWidget(

View File

@@ -62,7 +62,27 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
children: [
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
StepProgressBar(totalSteps: 4, currentStep: 3),
const SizedBox(height: 24),
GestureDetector(
onTap: () {
context.read<PostcardCreationBloc>().add(GoToPreviousStep());
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Icon(Icons.arrow_back, size: 20),
const SizedBox(width: 8),
Text(
"Back",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Text("Write a message",
style:
TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),

View File

@@ -5,6 +5,7 @@ import 'package:html/parser.dart' as html_parser;
class BackCardWidget extends StatelessWidget {
final String message;
final String? selectedFont;
final String name;
final String address;
final String city;
@@ -17,6 +18,7 @@ class BackCardWidget extends StatelessWidget {
const BackCardWidget({
super.key,
this.message = '',
this.selectedFont,
this.name = '',
this.address = '',
this.city = '',
@@ -98,6 +100,14 @@ class BackCardWidget extends StatelessWidget {
final messageText = parsedMessage['text'] ?? '';
final fontFamily = parsedMessage['fontFamily'] ?? '';
// Determine which font to use: selectedFont takes priority, then parsed fontFamily, then default
String finalFontFamily = '';
if (selectedFont != null && selectedFont!.isNotEmpty) {
finalFontFamily = selectedFont!;
} else if (fontFamily.isNotEmpty) {
finalFontFamily = fontFamily;
}
return Transform.scale(
scale: scale,
child: Container(
@@ -129,7 +139,7 @@ class BackCardWidget extends StatelessWidget {
child: SingleChildScrollView(
child: Text(
messageText,
style: _getFontStyle(fontFamily, 16.sp, 1.7),
style: _getFontStyle(finalFontFamily, 16.sp, 1.7),
),
),
),
@@ -192,28 +202,28 @@ class BackCardWidget extends StatelessWidget {
SizedBox(height: 5.h),
if (name.isNotEmpty) ...[
_addressLine(name),
_divider(),
],
_divider(),
if (address.isNotEmpty) ...[
_addressLine(address),
_divider(),
],
_divider(),
if (city.isNotEmpty) ...[
_addressLine(city),
_divider(),
],
if (state.isNotEmpty) ...[
_addressLine(state),
_divider(),
],
_divider(),
if (country.isNotEmpty) ...[
_addressLine(country),
_divider(),
],
_divider(),
if (pincode.isNotEmpty) ...[
_addressLine(pincode),
_divider(),
],
_divider(),
],
),
),

View File

@@ -219,6 +219,21 @@ class PurchaseDetailsBottomSheet {
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// If buying for myself, store the profile data
if (!postcardState.isGift && purchaseState.profile != null) {
final profile = purchaseState.profile!;
postcardBloc.add(StoreUserProfileData(
fullName: "${profile.firstName ?? ''} ${profile.lastName ?? ''}".trim(),
email: profile.emailAddress,
phone: profile.mobileNumber,
address: "${profile.address1 ?? ''} ${profile.address2 ?? ''}".trim(),
city: profile.cityName,
state: profile.stateName,
zipCode: profile.zipCode,
country: profile.country,
));
}
PurchaseDetailsBottomSheet.close(context);
postcardBloc.add(GoToNextStep());
},

View File

@@ -30,11 +30,13 @@ class _EditProfilePageState extends State<EditProfilePage> {
final TextEditingController phoneController = TextEditingController();
final TextEditingController address1Controller = TextEditingController();
final TextEditingController address2Controller = TextEditingController();
final TextEditingController stateController = TextEditingController();
final TextEditingController countryController = TextEditingController();
final TextEditingController cityController = TextEditingController();
final TextEditingController zipCodeController = TextEditingController();
// Dropdown values
String? selectedState;
String? selectedCountry;
final _formKey = GlobalKey<FormState>();
final ImagePicker _picker = ImagePicker();
@@ -68,11 +70,15 @@ class _EditProfilePageState extends State<EditProfilePage> {
phoneController.text = profile.mobileNumber;
address1Controller.text = profile.address1 ?? '';
address2Controller.text = profile.address2 ?? '';
stateController.text = profile.stateName ?? '';
countryController.text = profile.country ?? '';
cityController.text = profile.cityName ?? '';
zipCodeController.text = profile.zipCode ?? '';
// Set dropdown values from fetched data
setState(() {
selectedState = profile.stateName;
selectedCountry = profile.country;
});
// ⭐ REMOVED setState - image is now managed by BLoC state
if (kDebugMode && profile.profileImage != null && profile.profileImage!.isNotEmpty) {
print('🔵 [EDIT PROFILE] ✅ Current profile image URL: ${profile.profileImage}');
@@ -329,16 +335,12 @@ class _EditProfilePageState extends State<EditProfilePage> {
address2: address2Controller.text.trim().isEmpty
? null
: address2Controller.text.trim(),
// ⭐ ADD THESE NEW FIELDS
// ⭐ UPDATED: Use dropdown values instead of controllers
city: cityController.text.trim().isEmpty
? null
: cityController.text.trim(),
state: stateController.text.trim().isEmpty
? null
: stateController.text.trim(),
country: countryController.text.trim().isEmpty
? null
: countryController.text.trim(),
state: selectedState,
country: selectedCountry,
postalCode: zipCodeController.text.trim().isEmpty
? null
: zipCodeController.text.trim(),
@@ -354,8 +356,6 @@ class _EditProfilePageState extends State<EditProfilePage> {
phoneController.dispose();
address1Controller.dispose();
address2Controller.dispose();
stateController.dispose();
countryController.dispose();
cityController.dispose();
zipCodeController.dispose();
super.dispose();
@@ -538,22 +538,127 @@ class _EditProfilePageState extends State<EditProfilePage> {
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
child: CustomTextField(
label: "State",
hint: "Select your State",
controller: stateController,
enabled: !isLoading,
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "State", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedState,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select state",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: isLoading ? null : (value) {
setState(() {
selectedState = value;
});
},
items: [
"New South Wales",
"Victoria",
"Queensland",
"South Australia",
"Western Australia",
"Tasmania",
"Northern Territory",
"Australian Capital Territory"
].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
child: CustomTextField(
label: "Country",
hint: "Select your Country",
controller: countryController,
enabled: !isLoading,
padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomText(text: "Country", size: 14.sp),
SizedBox(height: 6.h),
Container(
height: 42.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: BoxDecoration(
color: const Color(0xFFFFF5F5),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: const Color(0xBBC83B61).withOpacity(0.4),
width: 0.4.w,
),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCountry,
isExpanded: true,
icon: const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFF8E8E8E),
),
hint: Text(
"Select country",
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF8E8E8E),
),
),
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF2D3134),
),
onChanged: isLoading ? null : (value) {
setState(() {
selectedCountry = value;
});
},
items: ["Australia"].map((value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
);
}).toList(),
),
),
),
],
),
),

View File

@@ -73,7 +73,7 @@ class Offer {
factory Offer.fromJson(Map<String, dynamic> json) {
return Offer(
id: json['id'],
id: json['id'] ?? 0,
title: json['title'] ?? '',
description: json['description'] ?? '',
offerCode: json['offerCode'] ?? '',
@@ -133,7 +133,7 @@ class City {
factory City.fromJson(Map<String, dynamic> json) {
return City(
id: json['id'],
id: json['id'] ?? 0,
name: json['name'] ?? '',
);
}
@@ -151,8 +151,8 @@ class City {
class CardInfo {
final int id;
final String title;
final int adultPrice;
final int childPrice;
final num adultPrice;
final num childPrice;
CardInfo({
required this.id,
@@ -163,7 +163,7 @@ class CardInfo {
factory CardInfo.fromJson(Map<String, dynamic> json) {
return CardInfo(
id: json['id'],
id: json['id'] ?? 0,
title: json['title'] ?? '',
adultPrice: json['adultPrice'] ?? 0,
childPrice: json['childPrice'] ?? 0,
@@ -193,7 +193,7 @@ class CardType {
factory CardType.fromJson(Map<String, dynamic> json) {
return CardType(
id: json['id'],
id: json['id'] ?? 0,
displayName: json['displayName'] ?? '',
);
}
@@ -219,7 +219,7 @@ class Category {
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
id: json['id'],
id: json['id'] ?? 0,
categoryName: json['categoryName'] ?? '',
);
}
@@ -230,4 +230,4 @@ class Category {
'categoryName': categoryName,
};
}
}
}

View File

@@ -129,6 +129,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.6"
csc_picker_plus:
dependency: "direct main"
description:
name: csc_picker_plus
sha256: "105e1989dd7462a504d60af024880918bb2936dbb9c97f46c4bd4923fe011411"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
csslib:
dependency: transitive
description:

View File

@@ -60,6 +60,7 @@ dependencies:
geocoding: ^4.0.0
cached_network_image: ^3.4.1
bloc: ^9.2.0
csc_picker_plus: ^0.0.3
dev_dependencies:
flutter_test: