Compare commits
4 Commits
53264619a8
...
48fd7037ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48fd7037ea | ||
|
|
40f0ed3a52 | ||
|
|
b08e2699e9 | ||
|
|
5d08e07de3 |
BIN
assets/icons/calendar.png
Normal file
BIN
assets/icons/calendar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 863 B |
BIN
assets/icons/person.png
Normal file
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
BIN
assets/icons/time.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 749 KiB After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/unlimited_card_details.png
Normal file
BIN
assets/images/unlimited_card_details.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -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,16 +135,22 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
|
||||
CancelPaymentEvent event,
|
||||
Emitter<StripePaymentState> emit,
|
||||
) {
|
||||
// Only emit cancelled if not already completed
|
||||
if (!_paymentCompleted) {
|
||||
emit(const StripePaymentCancelled(
|
||||
message: 'Payment cancelled by user',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle payment retry
|
||||
Future<void> _onRetryPayment(
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,7 +93,10 @@ class ShareBottomSheet extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// page indicator
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
@@ -88,13 +106,16 @@ class ShareBottomSheet extends StatelessWidget {
|
||||
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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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 {
|
||||
@@ -301,4 +297,3 @@ class Category {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class AttractionCard extends StatelessWidget {
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.attractionDetails,
|
||||
arguments: attraction,
|
||||
arguments: attraction.id,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -401,10 +401,10 @@ class BuyPassContent extends StatelessWidget {
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// Navigator.of(context).pushNamed(
|
||||
// RouteConstants.attractionDetails,
|
||||
// arguments: attraction,
|
||||
// );
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.attractionDetails,
|
||||
arguments: attraction.id,
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
207
lib/cart/model/my_passes_cart_mode.dart
Normal file
207
lib/cart/model/my_passes_cart_mode.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
class CommonAppText {
|
||||
static const String selectiveCard = "Selective";
|
||||
static const String selectiveCard = "Flexi";
|
||||
}
|
||||
49
lib/common_packages/custom_dash_border_painter.dart
Normal file
49
lib/common_packages/custom_dash_border_painter.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DashedBorderPainter extends CustomPainter {
|
||||
final Color color;
|
||||
final double strokeWidth;
|
||||
final double gap;
|
||||
final double dashWidth;
|
||||
final double radius;
|
||||
|
||||
DashedBorderPainter({
|
||||
required this.color,
|
||||
this.strokeWidth = 1.5,
|
||||
this.gap = 6,
|
||||
this.dashWidth = 6,
|
||||
this.radius = 16,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final rRect = RRect.fromRectAndRadius(
|
||||
Offset.zero & size,
|
||||
Radius.circular(radius),
|
||||
);
|
||||
|
||||
final path = Path()..addRRect(rRect);
|
||||
|
||||
final dashPath = Path();
|
||||
for (final metric in path.computeMetrics()) {
|
||||
double distance = 0;
|
||||
while (distance < metric.length) {
|
||||
dashPath.addPath(
|
||||
metric.extractPath(distance, distance + dashWidth),
|
||||
Offset.zero,
|
||||
);
|
||||
distance += dashWidth + gap;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawPath(dashPath, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import 'package:citycards_customer/add_details/add_details_view.dart';
|
||||
import 'package:citycards_customer/attraction_details/views/attraction_details_view.dart';
|
||||
import 'package:citycards_customer/attractions/models/attraction_model.dart';
|
||||
@@ -14,6 +15,8 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_s
|
||||
import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_empty_view.dart';
|
||||
import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart';
|
||||
import 'package:citycards_customer/my_pass/views/pass_attractions_page_view.dart';
|
||||
import 'package:citycards_customer/my_pass/views/search_pass_offers_with_listing.dart';
|
||||
import 'package:citycards_customer/offer_pass_detail/offer_pass_detail_view.dart';
|
||||
import 'package:citycards_customer/search_offers/bloc/search_offers_listing_bloc.dart';
|
||||
import 'package:citycards_customer/search_offers/view/search_offers_with_listing.dart';
|
||||
@@ -27,6 +30,11 @@ 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';
|
||||
import '../profile/view/faq/faq_view.dart';
|
||||
@@ -69,6 +77,24 @@ class AppRouter {
|
||||
case RouteConstants.attractionsPage:
|
||||
final args = settings.arguments as String;
|
||||
return MaterialPageRoute(builder: (_) => AttractionsPage(source: args));
|
||||
case RouteConstants.passAttractionsPage:
|
||||
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: (_) {
|
||||
@@ -149,10 +175,18 @@ class AppRouter {
|
||||
);
|
||||
|
||||
case RouteConstants.attractionDetails:
|
||||
final attractionId = settings.arguments as Attraction;
|
||||
final attractionId = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return AttractionDetailsView(attractionId: attractionId.id);
|
||||
return AttractionDetailsView(attractionId: attractionId);
|
||||
},
|
||||
);
|
||||
|
||||
case RouteConstants.passAttractionDetails:
|
||||
final attractionID = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return AttractionDetailsView(attractionId: attractionID);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -170,6 +204,7 @@ class AppRouter {
|
||||
builder: (_) => CheckoutView(bookingId: bookingId),
|
||||
);
|
||||
|
||||
|
||||
case RouteConstants.cartPage:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
@@ -186,6 +221,16 @@ class AppRouter {
|
||||
);
|
||||
},
|
||||
);
|
||||
case RouteConstants.searchPassOffer:
|
||||
final int cityId = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return BlocProvider(
|
||||
create: (_) => MyPassesOffersBloc(MyPassesOffersRepository()),
|
||||
child: PassOffersScreen(cityId: cityId),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
case RouteConstants.addDetails:
|
||||
final bookingId = settings.arguments as int;
|
||||
@@ -196,6 +241,7 @@ class AppRouter {
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
case RouteConstants.createAcct:
|
||||
final email = settings.arguments as String;
|
||||
|
||||
@@ -232,9 +278,12 @@ class AppRouter {
|
||||
final offerId = settings.arguments as int;
|
||||
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => OffersDetailsView(offerId: offerId),
|
||||
builder: (_) => OffersDetailsView(
|
||||
offerId: offerId,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
case RouteConstants.registeredUserHome:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
|
||||
6
lib/core/global_keys.dart
Normal file
6
lib/core/global_keys.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GlobalKeys {
|
||||
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||
GlobalKey<ScaffoldMessengerState>();
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import 'package:citycards_customer/attractions/models/attraction_model.dart';
|
||||
import 'package:citycards_customer/core/route_constants.dart';
|
||||
import 'package:citycards_customer/home/views/registered_user_home_page.dart';
|
||||
import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart';
|
||||
import 'package:citycards_customer/my_pass/views/pass_attraction_details_view.dart';
|
||||
import 'package:citycards_customer/my_pass/views/pass_attractions_page_view.dart';
|
||||
import 'package:citycards_customer/my_pass/views/search_pass_offers_with_listing.dart';
|
||||
import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -16,12 +19,19 @@ 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/qr_pass_page_view.dart';
|
||||
import '../my_pass/views/pass_details_page_view.dart';
|
||||
import '../offer_pass_detail/offer_pass_detail_view.dart';
|
||||
import '../postcard/blocs/postcard_creation_bloc.dart';
|
||||
import '../postcard/views/postcard_creation_page_view.dart';
|
||||
import '../profile/view/privacy/privacy_view.dart';
|
||||
import '../search_offers/bloc/offers_bloc.dart';
|
||||
import '../search_offers/bloc/search_offers_listing_bloc.dart';
|
||||
import '../search_offers/repository/offers_repository.dart';
|
||||
@@ -54,12 +64,38 @@ Widget buildOffstageNavigator(
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => AttractionsPage(source: args),
|
||||
);
|
||||
case RouteConstants.passAttractionsPage:
|
||||
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;
|
||||
|
||||
case RouteConstants.attractionDetails:
|
||||
final attraction = settings.arguments as Attraction;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return AttractionDetailsView(attractionId: attraction.id);
|
||||
return BlocProvider(
|
||||
create: (_) => MyPassesAttractionsBloc(
|
||||
repository: MyPassesAttractionsRepository(),
|
||||
),
|
||||
child: PassAttractionsPage(
|
||||
cityXid: cityId,
|
||||
source: source,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
case RouteConstants.attractionDetails:
|
||||
final attractionID = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return AttractionDetailsView(attractionId: attractionID);
|
||||
},
|
||||
);
|
||||
|
||||
case RouteConstants.passAttractionDetails:
|
||||
final attractionID = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return PassAttractionDetailsView(attractionId: attractionID);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -99,6 +135,23 @@ Widget buildOffstageNavigator(
|
||||
);
|
||||
},
|
||||
);
|
||||
case RouteConstants.searchPassOffer:
|
||||
final int cityId = settings.arguments as int;
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return BlocProvider(
|
||||
create: (_) => MyPassesOffersBloc(MyPassesOffersRepository()),
|
||||
child: PassOffersScreen(cityId: cityId),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
case RouteConstants.privacyPolicy:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) {
|
||||
return const PrivacyPolicyPage();
|
||||
},
|
||||
);
|
||||
|
||||
// 🔹 Upload Photo Page (start of postcard creation flow)
|
||||
case RouteConstants.uploadPhotoPage:
|
||||
@@ -124,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 QrPassView(),
|
||||
return BlocProvider(
|
||||
create: (context) => MyPassesDetailsBloc(
|
||||
repository: MyPassesDetailsRepository(),
|
||||
),
|
||||
child: PassDetailsView(bookingId: bookingId),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class RouteConstants {
|
||||
|
||||
|
||||
static const String intro = '/intro';
|
||||
static const String splash = '/splash';
|
||||
|
||||
@@ -7,6 +9,7 @@ class RouteConstants {
|
||||
static const String home = '/home';
|
||||
static const String registeredUserHome = '/registeredUserHome';
|
||||
static const String attractionsPage = "/attractions";
|
||||
static const String passAttractionsPage = "/passAttractionsPage";
|
||||
static const String postCardPage = "/postcards";
|
||||
static const String uploadPhotoPage = "/uploadPhoto";
|
||||
static const String addFilterPage = "/addFilter";
|
||||
@@ -33,23 +36,27 @@ class RouteConstants {
|
||||
static const String esimOffer = '/esim_offer';
|
||||
static const String hotelOffer = '/hotelOffer';
|
||||
|
||||
/**************************** Attraction Page *****************************************/
|
||||
/**************************** Attraction Page *****************************************/
|
||||
|
||||
static const String attractionDetails = '/attractionDetails';
|
||||
static const String attractionDetails ='/attractionDetails';
|
||||
static const String passAttractionDetails ='/passAttractionDetails';
|
||||
|
||||
/**************************** By Pass Page Page *****************************************/
|
||||
|
||||
static const String buyPass = '/buyPass';
|
||||
static const String checkout = '/checkout';
|
||||
static const String searchOffer = '/searchOffer';
|
||||
static const String searchPassOffer = '/searchPassOffer';
|
||||
static const String createAcct = '/createAcct';
|
||||
static const String addDetails = '/addDetails';
|
||||
static const String offerPassDetail = "/offerPassDetail";
|
||||
|
||||
|
||||
/************************** My card page ***************************************/
|
||||
static const String cartPage = '/cartPage';
|
||||
static const String yourItinerary = '/yourItinerary';
|
||||
|
||||
|
||||
static const String qrPage = '/qrPage';
|
||||
static const String makeBooking = '/makeBooking';
|
||||
static const String bookingSuccessful = '/bookingSuccessful';
|
||||
|
||||
@@ -28,14 +28,21 @@ class CreateAccountBloc extends Bloc<CreateAccountEvent, CreateAccountState> {
|
||||
mobileNumber: event.mobileNumber,
|
||||
address1: event.address1,
|
||||
address2: event.address2,
|
||||
city: event.city,
|
||||
state: event.state,
|
||||
country: event.country,
|
||||
postalCode: event.postalCode,
|
||||
);
|
||||
await LocalPreference.setLogin(true);
|
||||
// ✅ FIX: Parse directly from response, just like verify OTP
|
||||
final userModel = UserRegisteredModel.fromJson(response);
|
||||
|
||||
final userModel = UserRegisteredModel.fromJson(response['data'] ?? {});
|
||||
await LocalPreference.setTokens(
|
||||
accessToken: userModel.accessToken,
|
||||
refreshToken: userModel.refreshToken,
|
||||
refreshTokenMaxAge: userModel.refreshTokenMaxAge,
|
||||
);
|
||||
|
||||
await LocalPreference.setUserDetails(
|
||||
userId: userModel.user.id,
|
||||
firstName: userModel.user.firstName,
|
||||
@@ -45,10 +52,12 @@ class CreateAccountBloc extends Bloc<CreateAccountEvent, CreateAccountState> {
|
||||
role: userModel.user.role,
|
||||
roleId: userModel.user.roleId,
|
||||
);
|
||||
|
||||
await LocalPreference.setProfileImage(userModel.user.profileImage);
|
||||
|
||||
emit(CreateAccountSuccess(
|
||||
message: response['message'] ?? 'Account created successfully',
|
||||
userData: response['data'] ?? {},
|
||||
message: 'Account created successfully',
|
||||
userData: response,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(CreateAccountFailure(
|
||||
|
||||
@@ -14,6 +14,10 @@ class CreateAccountSubmitted extends CreateAccountEvent {
|
||||
final String mobileNumber;
|
||||
final String address1;
|
||||
final String address2;
|
||||
final String city;
|
||||
final String state;
|
||||
final String country;
|
||||
final String postalCode;
|
||||
|
||||
const CreateAccountSubmitted({
|
||||
required this.firstName,
|
||||
@@ -22,6 +26,10 @@ class CreateAccountSubmitted extends CreateAccountEvent {
|
||||
required this.mobileNumber,
|
||||
required this.address1,
|
||||
required this.address2,
|
||||
required this.city,
|
||||
required this.state,
|
||||
required this.country,
|
||||
required this.postalCode,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -32,6 +40,10 @@ class CreateAccountSubmitted extends CreateAccountEvent {
|
||||
mobileNumber,
|
||||
address1,
|
||||
address2,
|
||||
city,
|
||||
state,
|
||||
country,
|
||||
postalCode,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,25 @@ class CreateAccountRepository {
|
||||
required String mobileNumber,
|
||||
required String address1,
|
||||
required String address2,
|
||||
required String city,
|
||||
required String state,
|
||||
required String country,
|
||||
required String postalCode,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiServices.postApi(
|
||||
url: ApiUrls.createAccount,
|
||||
data: {
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'emailAddress': emailAddress,
|
||||
'mobileNumber': mobileNumber,
|
||||
'address1': address1,
|
||||
'address2': address2,
|
||||
"firstName": firstName,
|
||||
"lastName": lastName,
|
||||
"emailAddress": emailAddress,
|
||||
"mobileNumber": mobileNumber,
|
||||
"address1": address1,
|
||||
"address2": address2,
|
||||
"city": city,
|
||||
"state": state,
|
||||
"country": country,
|
||||
"postalCode": postalCode,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,13 @@ import 'package:citycards_customer/common_packages/custom_textfield.dart';
|
||||
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';
|
||||
import '../../profile/bloc/profile/profile_event.dart';
|
||||
import '../bloc/create_account_bloc.dart';
|
||||
@@ -13,25 +19,39 @@ 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 postalController = TextEditingController();
|
||||
|
||||
String? selectedState;
|
||||
String? selectedCountry;
|
||||
|
||||
void _submitForm(BuildContext context) {
|
||||
if (firstNameController.text.trim().isEmpty ||
|
||||
lastNameController.text.trim().isEmpty ||
|
||||
emailController.text.trim().isEmpty ||
|
||||
phoneController.text.trim().isEmpty ||
|
||||
addressController.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Please fill all fields')));
|
||||
addressController.text.trim().isEmpty ||
|
||||
cityController.text.trim().isEmpty ||
|
||||
selectedState == null ||
|
||||
selectedCountry == null ||
|
||||
postalController.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please fill all fields')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,27 +63,49 @@ class CreateAccountView extends StatelessWidget {
|
||||
mobileNumber: phoneController.text.trim(),
|
||||
address1: addressController.text.trim(),
|
||||
address2: '',
|
||||
city: cityController.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()),
|
||||
child: BlocListener<CreateAccountBloc, CreateAccountState>(
|
||||
listener: (ctx, state) async {
|
||||
if (state is CreateAccountSuccess) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(state.message)));
|
||||
await LocalPreference.setLogin(true);
|
||||
final userId = await LocalPreference.getUserId();
|
||||
context.read<ProfileBloc>().add(FetchProfileEvent(userId: userId!));
|
||||
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
|
||||
context.read<MyPostCardBloc>().add(CheckLoginStatus());
|
||||
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
|
||||
// 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,
|
||||
).showSnackBar(SnackBar(content: Text(state.message)));
|
||||
} else if (state is CreateAccountFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -168,14 +210,157 @@ class CreateAccountView extends StatelessWidget {
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "Address 1",
|
||||
label: "Address",
|
||||
hint: "Enter address manually or tap to search",
|
||||
controller: addressController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: CustomTextField(
|
||||
label: "City",
|
||||
hint: "Enter your city",
|
||||
controller: cityController,
|
||||
),
|
||||
),
|
||||
|
||||
// State Dropdown
|
||||
Padding(
|
||||
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.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(
|
||||
label: "Postal Code",
|
||||
hint: "Enter postal / zip code",
|
||||
controller: postalController,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
BlocBuilder<CreateAccountBloc, CreateAccountState>(
|
||||
builder: (context, state) {
|
||||
if (state is CreateAccountLoading) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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';
|
||||
@@ -23,19 +97,28 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||
try {
|
||||
emit(GetItineraryLoading());
|
||||
|
||||
// Check login status
|
||||
final isLoggedIn = await LocalPreference.getLogin();
|
||||
// Uncomment above and remove below line when ready for production
|
||||
// final isLoggedIn = true; // For testing
|
||||
|
||||
if (!isLoggedIn) {
|
||||
emit(GetItineraryNotLoggedIn());
|
||||
return;
|
||||
}
|
||||
|
||||
// If logged in, fetch itineraries
|
||||
final response = await _repository.fetchMyItineraries();
|
||||
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||
|
||||
// Add static itinerary to the list
|
||||
final itinerariesWithStatic = [
|
||||
_createStaticItinerary(),
|
||||
...response.itineraries,
|
||||
];
|
||||
|
||||
// Check if user has unlimited pass
|
||||
if (!response.isUnlimitedPass) {
|
||||
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
||||
} catch (e) {
|
||||
emit(GetItineraryFailed(
|
||||
error: e.toString().contains('Exception')
|
||||
@@ -53,7 +136,19 @@ class GetItineraryBloc extends Bloc<GetItineraryEvent, GetItineraryState> {
|
||||
|
||||
final response = await _repository.fetchMyItineraries();
|
||||
|
||||
emit(GetItinerarySuccessfully(itineraries: response.itineraries));
|
||||
// Add static itinerary to the list
|
||||
final itinerariesWithStatic = [
|
||||
_createStaticItinerary(),
|
||||
...response.itineraries,
|
||||
];
|
||||
|
||||
// Check if user has unlimited pass
|
||||
if (!response.isUnlimitedPass) {
|
||||
emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic));
|
||||
} catch (e) {
|
||||
emit(GetItineraryFailed(
|
||||
error: e.toString().contains('Exception')
|
||||
@@ -61,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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,15 @@ class GetItinerarySuccessfully extends GetItineraryState {
|
||||
List<Object> get props => [itineraries];
|
||||
}
|
||||
|
||||
class GetItineraryRequiresPass extends GetItineraryState {
|
||||
final List<MyItinerary> itineraries;
|
||||
|
||||
const GetItineraryRequiresPass({required this.itineraries});
|
||||
|
||||
@override
|
||||
List<Object> get props => [itineraries];
|
||||
}
|
||||
|
||||
class GetItineraryFailed extends GetItineraryState {
|
||||
final String error;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import '../../common_bloc/bottom_navigation_bloc.dart';
|
||||
import '../../login/view/login_email_bottomsheet.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@@ -30,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),
|
||||
@@ -40,10 +41,9 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: false,
|
||||
showDivider: true,
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// BLoC Builder for all states
|
||||
BlocBuilder<GetItineraryBloc, GetItineraryState>(
|
||||
builder: (context, state) {
|
||||
@@ -56,6 +56,8 @@ class _MagicItineraryViewState extends State<MagicItineraryView> {
|
||||
);
|
||||
} else if (state is GetItineraryNotLoggedIn) {
|
||||
return NotLoggedInItineraryView();
|
||||
} else if (state is GetItineraryRequiresPass) {
|
||||
return RequiresUnlimitedPassView();
|
||||
} else if (state is GetItinerarySuccessfully) {
|
||||
if (state.itineraries.isEmpty) {
|
||||
return NoItineraryView();
|
||||
@@ -192,8 +194,8 @@ class NotLoggedInItineraryView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class NoItineraryView extends StatelessWidget {
|
||||
const NoItineraryView({super.key});
|
||||
class RequiresUnlimitedPassView extends StatelessWidget {
|
||||
const RequiresUnlimitedPassView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -201,17 +203,17 @@ class NoItineraryView extends StatelessWidget {
|
||||
children: [
|
||||
SizedBox(height: 40.h),
|
||||
|
||||
// Illustration for no itineraries
|
||||
Icon(
|
||||
Icons.travel_explore,
|
||||
size: 120.sp,
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
// Illustration image
|
||||
Image.asset(
|
||||
"assets/images/no_itinerary.png", // Update with your actual asset path
|
||||
height: 300.h,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
CustomText(
|
||||
text: "No Itineraries Yet",
|
||||
text: "You do not possess an Unlimited Pass! 😔",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
@@ -222,8 +224,7 @@ class NoItineraryView extends StatelessWidget {
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
||||
child: CustomText(
|
||||
text:
|
||||
"You haven't created any itineraries yet. Start planning your next adventure!",
|
||||
text: "Get your Unlimited Pass and create a custom itinerary!",
|
||||
size: 14.sp,
|
||||
color: Color(0xFF656565),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -232,6 +233,62 @@ class NoItineraryView extends StatelessWidget {
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
context.read<NavigationBloc>().add(NavigationTabChanged(0));
|
||||
},
|
||||
label: "Buy Unlimited CityCard",
|
||||
showArrow: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoItineraryView extends StatelessWidget {
|
||||
const NoItineraryView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(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
|
||||
CustomText(
|
||||
text: "You Don’t have an Itinerary Yet! 😟",
|
||||
size: 18.sp,
|
||||
weight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
/// Subtitle
|
||||
CustomText(
|
||||
text:
|
||||
"Create your own personalized magic itinerary that suites your travel needs",
|
||||
size: 14.sp,
|
||||
color: const Color(0xFF656565),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 32.h),
|
||||
|
||||
/// Button
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@@ -244,7 +301,10 @@ class NoItineraryView extends StatelessWidget {
|
||||
label: "Create My Itinerary",
|
||||
showArrow: true,
|
||||
),
|
||||
|
||||
SizedBox(height: 40.h),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -52,6 +52,7 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
|
||||
),
|
||||
);
|
||||
} else if (state is LoginError) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage),
|
||||
|
||||
@@ -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(
|
||||
@@ -58,7 +61,7 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
|
||||
);
|
||||
} else {
|
||||
// User doesn't exist - navigate to create account
|
||||
Navigator.of(context).pushReplacementNamed(RouteConstants.createAcct,arguments: widget.emailAddress);
|
||||
Navigator.of(context).pushNamed(RouteConstants.createAcct,arguments: widget.emailAddress);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Please complete your profile'),
|
||||
@@ -74,6 +77,7 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
|
||||
),
|
||||
);
|
||||
} else if (state is VerifyOtpError) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage),
|
||||
|
||||
@@ -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,7 +9,9 @@ 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 'core/global_keys.dart';
|
||||
import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart';
|
||||
import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart';
|
||||
import 'home/bloc/registeredHome/home_bloc.dart';
|
||||
@@ -18,7 +21,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 +63,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(),
|
||||
@@ -89,6 +100,7 @@ class MyApp extends StatelessWidget {
|
||||
)
|
||||
],
|
||||
child: MaterialApp(
|
||||
scaffoldMessengerKey: GlobalKeys.scaffoldMessengerKey,
|
||||
onGenerateRoute: _appRouter.onGenerateRoute,
|
||||
initialRoute: RouteConstants.splash,
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
85
lib/my_pass/blocs/myPasses/my_passes_bloc.dart
Normal file
85
lib/my_pass/blocs/myPasses/my_passes_bloc.dart
Normal 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."));
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/my_pass/blocs/myPasses/my_passes_event.dart
Normal file
50
lib/my_pass/blocs/myPasses/my_passes_event.dart
Normal 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];
|
||||
}
|
||||
39
lib/my_pass/blocs/myPasses/my_passes_state.dart
Normal file
39
lib/my_pass/blocs/myPasses/my_passes_state.dart
Normal 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];
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
67
lib/my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart
Normal file
67
lib/my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
lib/my_pass/blocs/myPassesOffers/my_passes_offers_event.dart
Normal file
16
lib/my_pass/blocs/myPassesOffers/my_passes_offers_event.dart
Normal 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);
|
||||
}
|
||||
22
lib/my_pass/blocs/myPassesOffers/my_passes_offers_state.dart
Normal file
22
lib/my_pass/blocs/myPassesOffers/my_passes_offers_state.dart
Normal 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);
|
||||
}
|
||||
167
lib/my_pass/models/my_passes_details_model.dart
Normal file
167
lib/my_pass/models/my_passes_details_model.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
119
lib/my_pass/models/my_passes_model.dart
Normal file
119
lib/my_pass/models/my_passes_model.dart
Normal 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 ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
29
lib/my_pass/repository/my_passes_attractions_repository.dart
Normal file
29
lib/my_pass/repository/my_passes_attractions_repository.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
18
lib/my_pass/repository/my_passes_details_repository.dart
Normal file
18
lib/my_pass/repository/my_passes_details_repository.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
25
lib/my_pass/repository/my_passes_offers_repository.dart
Normal file
25
lib/my_pass/repository/my_passes_offers_repository.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
32
lib/my_pass/repository/my_passes_repository.dart
Normal file
32
lib/my_pass/repository/my_passes_repository.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -3,99 +3,196 @@ 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());
|
||||
}
|
||||
|
||||
Widget _noPassView(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 30.h),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/no_pass.png', // your woman sitting image
|
||||
height: 180.h,
|
||||
ListTile(
|
||||
title: Text(
|
||||
"All",
|
||||
style: GoogleFonts.poppins(fontSize: 14.sp),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
Text(
|
||||
"You Don’t 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');
|
||||
setState(() {
|
||||
selectedCardMode = "";
|
||||
});
|
||||
Navigator.pop(context);
|
||||
context.read<MyPassesBloc>().add(FetchMyPasses(
|
||||
cardMode: "",
|
||||
sort: selectedSort,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFF5A5F),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Buy a Pass",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
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 _passListView(List passes) {
|
||||
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: "",
|
||||
));
|
||||
},
|
||||
),
|
||||
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: SingleChildScrollView(
|
||||
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,),
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
GestureDetector(
|
||||
onTap: _showSortBottomSheet,
|
||||
child: Container(
|
||||
width: 130.w,
|
||||
height: 36.h,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
@@ -107,7 +204,7 @@ class MyPassesView extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Sort by Date",
|
||||
selectedSort.isEmpty ? "Sort by Date" : selectedSort,
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp),
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -115,8 +212,11 @@ class MyPassesView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Container(
|
||||
GestureDetector(
|
||||
onTap: _showCardModeBottomSheet,
|
||||
child: Container(
|
||||
height: 36.h,
|
||||
width: 130.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
@@ -128,7 +228,7 @@ class MyPassesView extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"All",
|
||||
selectedCardMode.isEmpty ? "All" : selectedCardMode,
|
||||
style: GoogleFonts.poppins(fontSize: 12.sp),
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -136,10 +236,171 @@ class MyPassesView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
ListView.builder(
|
||||
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(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
@@ -148,21 +409,16 @@ class MyPassesView extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 16.h),
|
||||
child: InkWell(
|
||||
onTap: (){
|
||||
context.read<MyPassBloc>().add(SelectPass(pass));
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamed(RouteConstants.qrPage);
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.qrPage,
|
||||
arguments: pass.id, // Pass your booking ID here
|
||||
);
|
||||
},
|
||||
child: PassTicketCard(pass: pass),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
727
lib/my_pass/views/pass_attraction_details_view.dart
Normal file
727
lib/my_pass/views/pass_attraction_details_view.dart
Normal file
@@ -0,0 +1,727 @@
|
||||
import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart';
|
||||
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';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../../attraction_details/bloc/attraction_details_bloc.dart';
|
||||
import '../../attraction_details/bloc/attraction_details_event.dart';
|
||||
import '../../attraction_details/bloc/attraction_details_state.dart';
|
||||
import '../../attraction_details/repository/attraction_details_repository.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
|
||||
class PassAttractionDetailsView extends StatelessWidget {
|
||||
final int? attractionId;
|
||||
|
||||
const PassAttractionDetailsView({
|
||||
super.key,
|
||||
required this.attractionId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => AttractionDetailsBloc(
|
||||
repository: AttractionDetailsRepository(),
|
||||
)..add(FetchAttractionDetails(attractionId: attractionId??0)),
|
||||
child: BlocBuilder<AttractionDetailsBloc, AttractionDetailsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AttractionDetailsLoading) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is AttractionDetailsError) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Text(
|
||||
state.message,
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is AttractionDetailsLoaded) {
|
||||
final attraction = state.attractionDetails;
|
||||
final coverImage = attraction.attractionGalleries
|
||||
.firstWhere(
|
||||
(gallery) => gallery.isCoverImage,
|
||||
orElse: () => attraction.attractionGalleries.first,
|
||||
)
|
||||
.filePathUrl;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Image.network(
|
||||
coverImage,
|
||||
height: 377.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(
|
||||
'assets/images/koh_rong_samloem_banner.png',
|
||||
height: 377.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20.w, vertical: 10.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: true,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
size: 24.sp,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
attraction.title,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 31.h,
|
||||
left: 12.w,
|
||||
right: 60.w, // Add this - leaves space for share button
|
||||
child: Text(
|
||||
attraction.title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 44.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 31.h,
|
||||
right: 17.w,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) =>
|
||||
const ShareBottomSheet(),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 36.h,
|
||||
width: 36.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.share_sharp,
|
||||
color: Colors.black,
|
||||
size: 18.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
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,),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"About",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12.32.h),
|
||||
Text(
|
||||
attraction.description,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF262626),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14.sp,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 41.h),
|
||||
|
||||
// Booking Section
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"How to make a booking?",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: Color(0xFFF95F62)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.call,
|
||||
color: Color(0xFFF95F62),
|
||||
size: 32.w,
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Contact Number",
|
||||
color: Colors.black.withOpacity(.6),
|
||||
size: 12.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
CustomText(
|
||||
text: attraction.bookingPhoneNumber??"N/A",
|
||||
color: Colors.black,
|
||||
size: 14.sp,
|
||||
weight: FontWeight.w600,
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
CustomText(
|
||||
text: "Tap to call",
|
||||
color: Colors.black.withOpacity(.4),
|
||||
size: 12.sp,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 12.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: Color(0xFFF95F62)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.email_sharp,
|
||||
color: Color(0xFFF95F62),
|
||||
size: 32.w,
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Email",
|
||||
color: Colors.black.withOpacity(.6),
|
||||
size: 12.sp,
|
||||
weight: FontWeight.w500,
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
CustomText(
|
||||
text: attraction.bookingEmail??"N/A",
|
||||
color: Colors.black,
|
||||
size: 14.sp,
|
||||
weight: FontWeight.w600,
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
CustomText(
|
||||
text: "Tap to email",
|
||||
color: Colors.black.withOpacity(.4),
|
||||
size: 12.sp,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.pushNamed(RouteConstants.makeBooking);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical: 18.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF95F62),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: "Via CityCards",
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
CustomText(
|
||||
text: "Create a booking via app",
|
||||
size: 11.sp,
|
||||
weight: FontWeight.w400,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30.h),
|
||||
Divider(color: Colors.black.withOpacity(0.2)),
|
||||
SizedBox(height: 30.h),
|
||||
Text(
|
||||
"What is included",
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
|
||||
// Dynamic Inclusions from API
|
||||
Wrap(
|
||||
runSpacing: 16.h,
|
||||
spacing: 16.w,
|
||||
children: attraction.attractionInclusions
|
||||
.where((inclusion) => inclusion.isInclusion)
|
||||
.map(
|
||||
(inclusion) => includedBox(
|
||||
"assets/icons/bus.png",
|
||||
inclusion.title,
|
||||
inclusion.description,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
SizedBox(height: 30.h),
|
||||
// Divider(color: Colors.black.withOpacity(0.2)),
|
||||
SizedBox(height: 30.h),
|
||||
Text(
|
||||
"Exact Location",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
CustomText(
|
||||
text: "View the location on map",
|
||||
size: 12.sp,
|
||||
color: Colors.black.withOpacity(.6),
|
||||
),
|
||||
SizedBox(height: 17.h),
|
||||
Container(
|
||||
height: 178.7.h,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(13.54.r),
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(13.54.r),
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
initialCenter: LatLng(
|
||||
attraction.latitudeCoordinate,
|
||||
attraction.longitudeCoordinate,
|
||||
),
|
||||
initialZoom: 15.0,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.example.citycards_customer',
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
point: LatLng(
|
||||
attraction.latitudeCoordinate,
|
||||
attraction.longitudeCoordinate,
|
||||
),
|
||||
width: 40.w,
|
||||
height: 40.h,
|
||||
child: Icon(
|
||||
Icons.location_on,
|
||||
color: Color(0xFFF95F62),
|
||||
size: 40.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 17.h),
|
||||
CustomText(
|
||||
text: attraction.address,
|
||||
size: 12.sp,
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
),
|
||||
SizedBox(height: 30.h),
|
||||
Divider(color: Colors.black.withOpacity(0.2)),
|
||||
SizedBox(height: 30.h),
|
||||
Text(
|
||||
"People frequently ask",
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 15.h),
|
||||
Column(
|
||||
children: attraction.attractionFaqs.map((faq) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 15.h),
|
||||
child: faqBox(
|
||||
title: faq.faqQuestion,
|
||||
desc: faq.faqAnswer,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Text("Something went wrong"),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget includedBox(String icon, String title, String disc) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFFFF5F5),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
border: Border.all(color: Color(0xFFFDCDCE)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(icon, scale: 4),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomText(
|
||||
text: title,
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
color: Color(0xFF212121),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
CustomText(
|
||||
text: disc,
|
||||
size: 11.sp,
|
||||
weight: FontWeight.w400,
|
||||
color: Color(0xFF666666),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget faqBox({
|
||||
required String title,
|
||||
required String desc,
|
||||
}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF5F5),
|
||||
border: Border.all(color: const Color(0xFFFDCDCE)),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomText(
|
||||
text: title,
|
||||
size: 16.sp,
|
||||
weight: FontWeight.w500,
|
||||
color: const Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20.w),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
size: 18.sp,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 9.h),
|
||||
CustomText(
|
||||
text: desc,
|
||||
size: 11.sp,
|
||||
color: const Color(0xFF7D7D7D),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
lib/my_pass/views/pass_attractions_page_view.dart
Normal file
165
lib/my_pass/views/pass_attractions_page_view.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'package:citycards_customer/common_packages/app_bar.dart';
|
||||
import 'package:citycards_customer/common_packages/back_widget.dart';
|
||||
import 'package:citycards_customer/my_pass/widgets/pass_attraction_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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.cityXid,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) {
|
||||
final bloc = MyPassesAttractionsBloc(
|
||||
repository: MyPassesAttractionsRepository(),
|
||||
);
|
||||
|
||||
// Fetch attractions with cityXid
|
||||
bloc.add(
|
||||
FetchMyPassesAttractionsByCategory(
|
||||
cityXid: cityXid,
|
||||
),
|
||||
);
|
||||
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<MyPassesAttractionsBloc, MyPassesAttractionsState>(
|
||||
builder: (context, state) {
|
||||
final bloc = context.read<MyPassesAttractionsBloc>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// App bar
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
backWidget(context, "Pass Attractions", Colors.black),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 🔍 Search field with BLoC logic
|
||||
CommonSearchField(
|
||||
hint: "Search attractions...",
|
||||
hintColor: Colors.grey.shade500,
|
||||
onChanged: (value) {
|
||||
bloc.add(SearchMyPassesAttractions(value));
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 🖼️ Category chips row - DYNAMIC
|
||||
if (state is MyPassesAttractionsLoaded)
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: state.categories
|
||||
.map(
|
||||
(category) => buildCategoryChip(
|
||||
category.categoryName ?? '',
|
||||
isSelected:
|
||||
state.selectedCategoryId == category.id,
|
||||
onTap: () {
|
||||
bloc.add(
|
||||
FetchMyPassesAttractionsByCategory(
|
||||
cityXid: cityXid,
|
||||
categoryXid: category.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 🏙️ Attraction list with search filter
|
||||
if (state is MyPassesAttractionsLoading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 60),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (state is MyPassesAttractionsLoaded)
|
||||
_buildAttractionsList(state)
|
||||
else if (state is MyPassesAttractionsError)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 60),
|
||||
child: Text(
|
||||
state.message,
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
655
lib/my_pass/views/pass_details_page_view.dart
Normal file
655
lib/my_pass/views/pass_details_page_view.dart
Normal file
@@ -0,0 +1,655 @@
|
||||
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_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 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<MyPassesDetailsBloc, MyPassesDetailsState>(
|
||||
builder: (context, state) {
|
||||
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(
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// App Bar
|
||||
SizedBox(height: 10.h),
|
||||
const CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
|
||||
SizedBox(height: 10.h),
|
||||
backWidget(context, "Back", Colors.black),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// -------------------------------
|
||||
/// 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),
|
||||
),
|
||||
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),
|
||||
|
||||
/// RIGHT CONTENT
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// Adults + Kids (WRAP prevents overflow)
|
||||
Wrap(
|
||||
spacing: 10.w,
|
||||
runSpacing: 10.h,
|
||||
children: [
|
||||
_infoChip(
|
||||
imagePath: "assets/icons/person.png",
|
||||
text: "Adults-${city?.totalAdult ?? 0}",
|
||||
),
|
||||
_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),
|
||||
_sectionTitle("Suggested Attractions"),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
/// 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: {
|
||||
'cityId': city?.id,
|
||||
'source': 'my_passes',
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
/// -------------------------------
|
||||
/// RECOMMENDED OFFERS
|
||||
/// -------------------------------
|
||||
_sectionTitle("Recommended Offers"),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
/// 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),
|
||||
|
||||
_outlineButton(
|
||||
"View all Offers",
|
||||
() {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
RouteConstants.searchPassOffer,
|
||||
arguments: city?.id ??"",
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.privacyPolicy,
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Learn about policies",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 30.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _outlineButton(String title, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
border: Border.all(color: const Color(0xffF95F62)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
color: const Color(0xffF95F62),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(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.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,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
/// 🔥 Text Section
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 2.h),
|
||||
|
||||
Text(
|
||||
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),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 8.w),
|
||||
|
||||
/// 🔥 QR Code Circle (Proper UI like Design)
|
||||
Container(
|
||||
height: 44.w,
|
||||
width: 44.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _infoChip({
|
||||
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:
|
||||
isExpanded ? MainAxisSize.max : MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
isExpanded ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xffF95F62),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _offerCard({
|
||||
required String title,
|
||||
required String description,
|
||||
required String image,
|
||||
}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(6.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
border: Border.all(color: const Color(0xffF2D6D6)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// 🔥 Top Offer Image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: image.isNotEmpty
|
||||
? Image.network(
|
||||
image,
|
||||
height: 120.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 120.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 120.h,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
/// 🔥 Title
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
SizedBox(height: 6.h),
|
||||
|
||||
/// 🔥 Description
|
||||
Text(
|
||||
description,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.grey.shade700,
|
||||
height: 1.4,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
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';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../common_packages/app_bar.dart';
|
||||
import '../../common_packages/back_widget.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
import '../widgets/action_button_widget.dart';
|
||||
import '../widgets/qr_container_widget.dart';
|
||||
|
||||
class QrPassView extends StatelessWidget {
|
||||
const QrPassView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<MyPassBloc, MyPassState>(
|
||||
builder: (context, state) {
|
||||
if (state is MyPassLoaded) {
|
||||
final pass = state.selectedPass!;
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
|
||||
SizedBox(height: 10.h),
|
||||
backWidget(context, "Back", Colors.black),
|
||||
SizedBox(height: 20.h),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
"Scan this at the site of\nattraction",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// ♻️ Reusable QR Container Component
|
||||
QrContainerWidget(
|
||||
qrImagePath: "assets/images/qr_image.png",
|
||||
cityCardTitle: "Melbourne CityCards",
|
||||
qrCode: "IYFHHVN254ADSD",
|
||||
cardType: pass.title,
|
||||
),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
/// 🎟 Card details section
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 40,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: pass.title.toLowerCase() == "unlimited card"
|
||||
? const Color(0xffF95F62).withOpacity(0.1)
|
||||
: const Color(0xffF95FAF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(25.r),
|
||||
border: Border.all(
|
||||
color: pass.title.toLowerCase() == "unlimited card"
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffF95FAF),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
pass.title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16.sp,
|
||||
color: const Color(0xffFF5A5F),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
Text(
|
||||
"Adults-${pass.adults} • Kids-${pass.kids} • ${pass.duration}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xff212121),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Valid Till: ${pass.validity}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12.sp,
|
||||
color: Color(0xff212121),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 28.h),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Learn about policies",
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.black,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
/// 🔘 Buttons
|
||||
Column(
|
||||
children: [
|
||||
actionButton(
|
||||
label: "View All Attractions",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(RouteConstants.attractionsPage, arguments: "qrPass");
|
||||
},
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
actionButton(
|
||||
label: "View All Available Offers",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(RouteConstants.searchOffer);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
409
lib/my_pass/views/search_pass_offers_with_listing.dart
Normal file
409
lib/my_pass/views/search_pass_offers_with_listing.dart
Normal file
@@ -0,0 +1,409 @@
|
||||
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: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 {
|
||||
final int cityId;
|
||||
|
||||
const PassOffersScreen({
|
||||
super.key,
|
||||
required this.cityId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PassOffersScreen> createState() => _PassOffersScreenState();
|
||||
}
|
||||
|
||||
class _PassOffersScreenState extends State<PassOffersScreen> {
|
||||
int? selectedCategoryId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => MyPassesOffersBloc(MyPassesOffersRepository())
|
||||
..add(LoadMyPassesOffers(cityXid: widget.cityId)),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showCart: false,
|
||||
showDivider: true,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Icon(Icons.arrow_back),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
CustomText(
|
||||
text: "Offers with ${CommonAppText.selectiveCard} Card",
|
||||
size: 12.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 33.h),
|
||||
Builder(
|
||||
builder: (context) => CommonSearchField(
|
||||
hint: "Search offers",
|
||||
hintColor: const Color(0xFFF95F62).withOpacity(.6),
|
||||
showSuffix: true,
|
||||
onChanged: (value) {
|
||||
context.read<MyPassesOffersBloc>().add(SearchMyPassesOffers(value));
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// Dynamic Categories
|
||||
BlocBuilder<MyPassesOffersBloc, MyPassesOffersState>(
|
||||
builder: (context, state) {
|
||||
if (state is MyPassesOffersLoaded) {
|
||||
final categories = state.categories;
|
||||
|
||||
if (categories.isEmpty) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
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<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(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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// Offer list
|
||||
Expanded(
|
||||
child: BlocBuilder<MyPassesOffersBloc, MyPassesOffersState>(
|
||||
builder: (context, state) {
|
||||
if (state is MyPassesOffersLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is MyPassesOffersError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48.sp,
|
||||
color: Colors.red,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
state.message,
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is MyPassesOffersLoaded) {
|
||||
final offers = state.offers;
|
||||
|
||||
if (offers.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_offer_outlined,
|
||||
size: 48.sp,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
"No offers found",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16.w,
|
||||
mainAxisSpacing: 22.h,
|
||||
childAspectRatio: 0.65,
|
||||
),
|
||||
itemCount: offers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final offer = offers[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
RouteConstants.offerPassDetail,
|
||||
arguments: offer.id, // ✅ pass offerId
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 6.w,
|
||||
vertical: 6.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color:
|
||||
Color(0xFFF95F62).withOpacity(.24),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.sp),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8.sp),
|
||||
child: offer.mobileBannerImage != null &&
|
||||
offer.mobileBannerImage!
|
||||
.isNotEmpty
|
||||
? Image.network(
|
||||
'${ApiUrls.baseUrl}/${offer.mobileBannerImage}',
|
||||
width: double.infinity,
|
||||
height: 120.5.h,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 120.5.h,
|
||||
color: Color(0xFFFEE7E7),
|
||||
child: Icon(
|
||||
Icons.local_offer,
|
||||
size: 40.sp,
|
||||
color: Color(0xFFF95F62)
|
||||
.withOpacity(.6),
|
||||
),
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child,
|
||||
loadingProgress) {
|
||||
if (loadingProgress == null) {
|
||||
return child;
|
||||
}
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 120.5.h,
|
||||
color: Color(0xFFFEE7E7),
|
||||
child: Center(
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
value: loadingProgress
|
||||
.expectedTotalBytes !=
|
||||
null
|
||||
? loadingProgress
|
||||
.cumulativeBytesLoaded /
|
||||
loadingProgress
|
||||
.expectedTotalBytes!
|
||||
: null,
|
||||
strokeWidth: 2,
|
||||
color:
|
||||
Color(0xFFF95F62),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: double.infinity,
|
||||
height: 120.5.h,
|
||||
color: Color(0xFFFEE7E7),
|
||||
child: Icon(
|
||||
Icons.local_offer,
|
||||
size: 40.sp,
|
||||
color: Color(0xFFF95F62)
|
||||
.withOpacity(.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
CustomText(
|
||||
text: offer.title,
|
||||
size: 18.sp,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 8.h),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text(
|
||||
"No data available",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
203
lib/my_pass/widgets/pass_attraction_card.dart
Normal file
203
lib/my_pass/widgets/pass_attraction_card.dart
Normal file
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../attractions/models/attraction_model.dart';
|
||||
import '../../common_packages/common_app_texts.dart';
|
||||
import '../../core/route_constants.dart';
|
||||
|
||||
class PassAttractionCard extends StatelessWidget {
|
||||
final Attraction attraction;
|
||||
const PassAttractionCard({super.key, required this.attraction});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
/// CARD TITLES (instead of categories)
|
||||
final List<String> tags = attraction.cards
|
||||
.map((e) => e.title)
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
/// 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(
|
||||
RouteConstants.passAttractionDetails,
|
||||
arguments: attraction.id,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.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: imageUrl.isNotEmpty
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _imageFallback();
|
||||
},
|
||||
)
|
||||
: _imageFallback(),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
/// 🔥 Text Section
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
attraction.title,
|
||||
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),
|
||||
|
||||
/// TAGS (CARD TITLES) OR BOOKING REQUIRED
|
||||
showBookingRequired
|
||||
? Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10.w,
|
||||
vertical: 4.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffC1D2F8),
|
||||
border: Border.all(
|
||||
color: const Color(0xff2563EB),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Text(
|
||||
"Booking Required",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Wrap(
|
||||
spacing: 6.w,
|
||||
runSpacing: 6.h,
|
||||
children: tags
|
||||
.map(
|
||||
(tag) => Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10.w,
|
||||
vertical: 4.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: tag ==
|
||||
"${CommonAppText.selectiveCard} Card"
|
||||
? const Color(0xffF95FAF)
|
||||
.withOpacity(0.1)
|
||||
: const Color(0xffF95F62)
|
||||
.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: tag ==
|
||||
"${CommonAppText.selectiveCard} Card"
|
||||
? const Color(0xffF95FAF)
|
||||
: const Color(0xffF95F62),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Image Fallback Widget
|
||||
Widget _imageFallback() {
|
||||
return Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class ApiUrls {
|
||||
static const baseUrl =
|
||||
"https://devapi.citycards.betadelivery.com"; //Normal 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 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 editPostcard = "$baseUrl/mobile/postcards";
|
||||
|
||||
|
||||
@@ -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,13 +106,17 @@ class _OffersDetailsContent extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../repository/postcard_checkout_repository.dart';
|
||||
import 'postcard_checkout_event.dart';
|
||||
@@ -16,7 +15,7 @@ class PostcardCheckoutBloc
|
||||
on<UpdateCheckoutDataEvent>(_onUpdateCheckoutData);
|
||||
on<SaveAsDraftEvent>(_onSaveAsDraft);
|
||||
on<SubmitPostcardEvent>(_onSubmitPostcard);
|
||||
on<ConfirmPaymentEvent>(_onConfirmPayment); // 🆕 NEW
|
||||
on<ConfirmPaymentEvent>(_onConfirmPayment);
|
||||
}
|
||||
|
||||
void _onUpdateAddress(
|
||||
@@ -73,6 +72,7 @@ class PostcardCheckoutBloc
|
||||
baseAmount: event.baseAmount,
|
||||
totalTaxAmount: event.totalTaxAmount,
|
||||
totalAmount: event.totalAmount,
|
||||
postcardId: event.postcardId,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -84,6 +84,16 @@ class PostcardCheckoutBloc
|
||||
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
|
||||
|
||||
try {
|
||||
// Validate pcId exists
|
||||
if (state.postcardId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Postcard ID is missing',
|
||||
isSuccess: false,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that image file exists before submitting
|
||||
if (state.pcImageFile == null) {
|
||||
emit(
|
||||
@@ -97,31 +107,12 @@ class PostcardCheckoutBloc
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// Extract order ID from response if available
|
||||
final orderId =
|
||||
response['orderId']?.toString() ??
|
||||
final orderId = response['orderId']?.toString() ??
|
||||
response['order_id']?.toString() ??
|
||||
response['id']?.toString();
|
||||
|
||||
@@ -135,7 +126,11 @@ class PostcardCheckoutBloc
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(isLoading: false, error: e.toString(), isSuccess: false),
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
isSuccess: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -147,6 +142,16 @@ class PostcardCheckoutBloc
|
||||
emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
|
||||
|
||||
try {
|
||||
// Validate pcId exists
|
||||
if (state.postcardId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Postcard ID is missing',
|
||||
isSuccess: false,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that image file exists before submitting
|
||||
if (state.pcImageFile == null) {
|
||||
emit(
|
||||
@@ -160,37 +165,16 @@ class PostcardCheckoutBloc
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// 🆕 Parse response from backend
|
||||
// Expected: {"postcardId": 16, "clientSecret": "pi_3Sx0yjRtCkWyT4Em1MKw1FeU_secret_S8M74wnEhTRC9lUz9RqJnuuqg"}
|
||||
|
||||
// Parse response from backend
|
||||
final postcardId = response['postcardId'] as int?;
|
||||
final clientSecret = response['clientSecret'] as String?;
|
||||
|
||||
// Also try alternative key names in case backend uses different naming
|
||||
final orderId =
|
||||
response['orderId']?.toString() ??
|
||||
// Extract order ID from response
|
||||
final orderId = response['orderId']?.toString() ??
|
||||
response['order_id']?.toString() ??
|
||||
response['id']?.toString();
|
||||
|
||||
@@ -199,36 +183,38 @@ class PostcardCheckoutBloc
|
||||
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 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, stack) {
|
||||
log("Payment Error ${e.toString()}");
|
||||
log("Payment Error ${stack.toString()}");
|
||||
log("Payment Error: ${e.toString()}");
|
||||
log("Payment Stack: ${stack.toString()}");
|
||||
emit(
|
||||
state.copyWith(isLoading: false, error: e.toString(), isSuccess: false),
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
isSuccess: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🆕 Confirm payment after Stripe payment completes
|
||||
/// This should be called after Stripe payment succeeds or fails
|
||||
/// Confirm payment after Stripe payment completes
|
||||
Future<void> _onConfirmPayment(
|
||||
ConfirmPaymentEvent event,
|
||||
Emitter<PostcardCheckoutState> emit,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,3 +69,26 @@ class UpdatePostcardNumber extends PostcardCreationEvent {
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
203
lib/postcard/repository/postcard_add_to_cart_repository.dart
Normal file
203
lib/postcard/repository/postcard_add_to_cart_repository.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -351,6 +351,7 @@ class _EditPostcardViewState extends State<EditPostcardView> {
|
||||
style: TextStyle(
|
||||
color: Color(0xffF95F62),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: icon != null ? 8 : 0),
|
||||
|
||||
@@ -45,16 +45,24 @@ class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"#${widget.postcard.pcNumber}",
|
||||
/// 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
|
||||
/// Action Icons
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
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;
|
||||
|
||||
@@ -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,9 +83,27 @@ 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(
|
||||
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(
|
||||
@@ -61,7 +116,27 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
@@ -176,29 +251,56 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Next Button
|
||||
SizedBox(
|
||||
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: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Update the bloc with form data
|
||||
bloc.add(UpdatePurchaseFormData(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
creationBloc.add(
|
||||
UpdatePurchaseFormData(
|
||||
pcTitle: _titleController.text,
|
||||
fullName: _fullNameController.text,
|
||||
emailId: _emailController.text,
|
||||
phoneNumber: _phoneController.text,
|
||||
address: _addressController.text,
|
||||
city: _cityController.text,
|
||||
country: _selectedCountry ?? '',
|
||||
state: _selectedState ?? '',
|
||||
state: _selectedState,
|
||||
zipCode: _zipCodeController.text,
|
||||
));
|
||||
country: _selectedCountry,
|
||||
),
|
||||
);
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final currentDate = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
||||
|
||||
// Navigate to next step
|
||||
bloc.add(GoToNextStep());
|
||||
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(
|
||||
@@ -208,7 +310,16 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Next",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
@@ -217,11 +328,14 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -45,30 +45,13 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
final bloc = context.read<PostcardCreationBloc>();
|
||||
_controller.text = state.message ?? "";
|
||||
_controller.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _controller.text.length),
|
||||
);
|
||||
TextPosition(offset: _controller.text.length));
|
||||
|
||||
final fonts = [
|
||||
{
|
||||
"name": "Default",
|
||||
"font": GoogleFonts.poppins(),
|
||||
"cleanName": "Poppins",
|
||||
},
|
||||
{
|
||||
"name": "Patrick Hand",
|
||||
"font": GoogleFonts.patrickHand(),
|
||||
"cleanName": "Patrick Hand",
|
||||
},
|
||||
{
|
||||
"name": "Indie Flower",
|
||||
"font": GoogleFonts.indieFlower(),
|
||||
"cleanName": "Indie Flower",
|
||||
},
|
||||
{
|
||||
"name": "Gloria Hallelujah",
|
||||
"font": GoogleFonts.gloriaHallelujah(),
|
||||
"cleanName": "Gloria Hallelujah",
|
||||
},
|
||||
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
|
||||
{"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"},
|
||||
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
|
||||
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
@@ -77,17 +60,32 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonAppBar(
|
||||
isWhiteLogo: false,
|
||||
isProfilePage: false,
|
||||
showDivider: true,
|
||||
),
|
||||
CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
|
||||
StepProgressBar(totalSteps: 4, currentStep: 3),
|
||||
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(
|
||||
"Write a message",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||
"Back",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Text("Write a message",
|
||||
style:
|
||||
TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"Design your own unique postcards to cherish your unforgettable moments.",
|
||||
@@ -155,8 +153,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
final String fontName = font["name"] as String;
|
||||
final String cleanName = font["cleanName"] as String;
|
||||
|
||||
final isSelected =
|
||||
state.selectedFont == cleanName ||
|
||||
final isSelected = state.selectedFont == cleanName ||
|
||||
(state.selectedFont == null && fontName == "Default");
|
||||
|
||||
return GestureDetector(
|
||||
@@ -183,24 +180,20 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Aa",
|
||||
Text("Aa",
|
||||
style: fontStyle.copyWith(
|
||||
fontSize: 24.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
fontName,
|
||||
Text(fontName,
|
||||
textAlign: TextAlign.center,
|
||||
style: fontStyle.copyWith(
|
||||
fontSize: 11.sp,
|
||||
color: isSelected
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xff2D3134),
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -245,10 +238,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
}
|
||||
|
||||
// Helper method to get the correct font style for the text field
|
||||
TextStyle _getTextFieldStyle(
|
||||
String? selectedFont,
|
||||
List<Map<String, dynamic>> fonts,
|
||||
) {
|
||||
TextStyle _getTextFieldStyle(String? selectedFont, List<Map<String, dynamic>> fonts) {
|
||||
if (selectedFont == null || selectedFont.isEmpty) {
|
||||
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
|
||||
}
|
||||
@@ -290,12 +280,10 @@ class DottedBorderPainter extends CustomPainter {
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path()
|
||||
..addRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
..addRRect(RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Radius.circular(borderRadius),
|
||||
),
|
||||
);
|
||||
));
|
||||
|
||||
// Create dashed path
|
||||
final dashPath = _createDashedPath(path, dashWidth, dashSpace);
|
||||
@@ -351,7 +339,11 @@ class LinedPaperPainter extends CustomPainter {
|
||||
|
||||
const lineSpacing = 30.0;
|
||||
for (double i = lineSpacing; i < size.height; i += lineSpacing) {
|
||||
canvas.drawLine(Offset(0, i), Offset(size.width, i), paint);
|
||||
canvas.drawLine(
|
||||
Offset(0, i),
|
||||
Offset(size.width, i),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,6 +33,21 @@ class _EditYourdetailsState extends State<EditYourdetails> {
|
||||
String? _selectedState;
|
||||
String? _selectedCountry;
|
||||
|
||||
final List<String> countries = [
|
||||
'Australia',
|
||||
];
|
||||
|
||||
final List<String> states = [
|
||||
'New South Wales',
|
||||
'Victoria',
|
||||
'Queensland',
|
||||
'South Australia',
|
||||
'Western Australia',
|
||||
'Tasmania',
|
||||
'Northern Territory',
|
||||
'Australian Capital Territory',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
setState(() {
|
||||
@@ -46,6 +61,7 @@ class _EditYourdetailsState extends State<EditYourdetails> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Recipient Details",
|
||||
@@ -81,15 +97,28 @@ class _EditYourdetailsState extends State<EditYourdetails> {
|
||||
hint: "Enter the name of your city",
|
||||
controller: widget.cityController,
|
||||
),
|
||||
_buildDropdownField(
|
||||
label: "Country",
|
||||
hint: "Select your country",
|
||||
value: _selectedCountry,
|
||||
items: countries,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedCountry = val;
|
||||
});
|
||||
widget.selectCountry(val!);
|
||||
},
|
||||
),
|
||||
_buildDropdownField(
|
||||
label: "State",
|
||||
hint: "Select your state",
|
||||
value: _selectedState,
|
||||
items: states,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedState = val;
|
||||
});
|
||||
widget.selectState;
|
||||
widget.selectState(val!);
|
||||
},
|
||||
),
|
||||
_buildInputField(
|
||||
@@ -98,17 +127,6 @@ class _EditYourdetailsState extends State<EditYourdetails> {
|
||||
controller: widget.zipCodeController,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
_buildDropdownField(
|
||||
label: "Country",
|
||||
hint: "Select your country",
|
||||
value: _selectedCountry,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedCountry = val;
|
||||
});
|
||||
widget.selectCountry;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -182,11 +200,11 @@ class _EditYourdetailsState extends State<EditYourdetails> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔹 Dropdown input
|
||||
Widget _buildDropdownField({
|
||||
required String label,
|
||||
required String hint,
|
||||
required String? value,
|
||||
required List<String> items,
|
||||
required Function(String?) onChanged,
|
||||
}) {
|
||||
return Padding(
|
||||
@@ -238,13 +256,12 @@ class _EditYourdetailsState extends State<EditYourdetails> {
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: "Lorem Ipsum",
|
||||
child: Text("Lorem Ipsum"),
|
||||
),
|
||||
// Add more items as needed
|
||||
],
|
||||
items: items.map((String item) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
|
||||
Emitter<ProfileState> emit,
|
||||
) async {
|
||||
try {
|
||||
|
||||
emit(const ProfileLoading());
|
||||
|
||||
final profile = await _profileRepository.fetchUserProfile();
|
||||
@@ -54,6 +53,12 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
|
||||
print('📄 [BLOC] Address1: ${event.address1}');
|
||||
print('📄 [BLOC] Address2: ${event.address2}');
|
||||
|
||||
// ⭐ NEW DEBUG LOGS
|
||||
print('📄 [BLOC] City: ${event.city}');
|
||||
print('📄 [BLOC] State: ${event.state}');
|
||||
print('📄 [BLOC] Country: ${event.country}');
|
||||
print('📄 [BLOC] Postal Code: ${event.postalCode}');
|
||||
|
||||
if (event.profileImageFile != null) {
|
||||
print('📄 [BLOC] ✅ Profile Image File Present in Event');
|
||||
print('📄 [BLOC] File Path: ${event.profileImageFile!.path}');
|
||||
|
||||
@@ -18,6 +18,7 @@ class FetchProfileEvent extends ProfileEvent {
|
||||
List<Object?> get props => [userId];
|
||||
}
|
||||
|
||||
/// Event to update user profile
|
||||
/// Event to update user profile
|
||||
class UpdateProfileEvent extends ProfileEvent {
|
||||
final int userId;
|
||||
@@ -26,6 +27,10 @@ class UpdateProfileEvent extends ProfileEvent {
|
||||
final String mobileNumber;
|
||||
final String? address1;
|
||||
final String? address2;
|
||||
final String? city; // ⭐ NEW
|
||||
final String? state; // ⭐ NEW
|
||||
final String? country; // ⭐ NEW
|
||||
final String? postalCode; // ⭐ NEW
|
||||
final File? profileImageFile;
|
||||
|
||||
const UpdateProfileEvent({
|
||||
@@ -35,6 +40,10 @@ class UpdateProfileEvent extends ProfileEvent {
|
||||
required this.mobileNumber,
|
||||
this.address1,
|
||||
this.address2,
|
||||
this.city, // ⭐ NEW
|
||||
this.state, // ⭐ NEW
|
||||
this.country, // ⭐ NEW
|
||||
this.postalCode, // ⭐ NEW
|
||||
this.profileImageFile,
|
||||
});
|
||||
|
||||
@@ -46,6 +55,10 @@ class UpdateProfileEvent extends ProfileEvent {
|
||||
mobileNumber,
|
||||
address1,
|
||||
address2,
|
||||
city, // ⭐ NEW
|
||||
state, // ⭐ NEW
|
||||
country, // ⭐ NEW
|
||||
postalCode, // ⭐ NEW
|
||||
profileImageFile,
|
||||
];
|
||||
|
||||
@@ -56,6 +69,10 @@ class UpdateProfileEvent extends ProfileEvent {
|
||||
'mobileNumber': mobileNumber,
|
||||
if (address1 != null && address1!.isNotEmpty) 'address1': address1,
|
||||
if (address2 != null && address2!.isNotEmpty) 'address2': address2,
|
||||
if (city != null && city!.isNotEmpty) 'city': city, // ⭐ NEW
|
||||
if (state != null && state!.isNotEmpty) 'state': state, // ⭐ NEW
|
||||
if (country != null && country!.isNotEmpty) 'country': country, // ⭐ NEW
|
||||
if (postalCode != null && postalCode!.isNotEmpty) 'postalCode': postalCode, // ⭐ NEW
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/profile_model.dart';
|
||||
import '../../networkApiServices/network_api_services.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
@@ -9,7 +10,7 @@ import '../../localPreference/local_preference.dart';
|
||||
class ProfileRepository {
|
||||
final NetworkApiService _apiService = NetworkApiService();
|
||||
|
||||
/// Fetch user profile (userId from local storage)
|
||||
/// ✅ Fetch user profile (userId from local storage)
|
||||
Future<ProfileModel> fetchUserProfile() async {
|
||||
final int? userId = await LocalPreference.getUserId();
|
||||
|
||||
@@ -20,11 +21,10 @@ class ProfileRepository {
|
||||
return ProfileModel.fromJson(response.data);
|
||||
}
|
||||
|
||||
/// Update user profile (userId from local storage)
|
||||
/// ⭐ FIXED: Now uses multipart/form-data for file upload
|
||||
/// ✅ Update user profile (Multipart with Image + New Address Fields)
|
||||
Future<ProfileModel> updateUserProfile({
|
||||
required Map<String, dynamic> data,
|
||||
File? profileImageFile, // ⭐ NEW: Accept File instead of base64
|
||||
File? profileImageFile,
|
||||
}) async {
|
||||
final int? userId = await LocalPreference.getUserId();
|
||||
|
||||
@@ -32,31 +32,56 @@ class ProfileRepository {
|
||||
print('📤 [UPDATE PROFILE] User ID: $userId');
|
||||
print('📤 [UPDATE PROFILE] URL: ${ApiUrls.userProfile}/$userId');
|
||||
print('📤 [UPDATE PROFILE] Data Keys: ${data.keys.toList()}');
|
||||
print('📤 [UPDATE PROFILE] First Name: ${data['firstName']}');
|
||||
print('📤 [UPDATE PROFILE] Last Name: ${data['lastName']}');
|
||||
print('📤 [UPDATE PROFILE] Mobile: ${data['mobileNumber']}');
|
||||
print('📤 [UPDATE PROFILE] Address1: ${data['address1']}');
|
||||
print('📤 [UPDATE PROFILE] Address2: ${data['address2']}');
|
||||
print('📤 [UPDATE PROFILE] Profile Image File: ${profileImageFile?.path}');
|
||||
|
||||
print('📤 First Name: ${data['firstName']}');
|
||||
print('📤 Last Name: ${data['lastName']}');
|
||||
print('📤 Mobile: ${data['mobileNumber']}');
|
||||
print('📤 Address1: ${data['address1']}');
|
||||
print('📤 Address2: ${data['address2']}');
|
||||
|
||||
// ⭐ NEW DEBUG LOGS
|
||||
print('📤 City: ${data['city']}');
|
||||
print('📤 State: ${data['state']}');
|
||||
print('📤 Country: ${data['country']}');
|
||||
print('📤 Postal Code: ${data['postalCode']}');
|
||||
|
||||
print('📤 Profile Image File: ${profileImageFile?.path}');
|
||||
}
|
||||
|
||||
// ⭐ Create FormData for multipart/form-data upload
|
||||
/// ✅ Create FormData (Multipart)
|
||||
final formData = FormData();
|
||||
|
||||
// Add text fields
|
||||
/// ✅ Add Text Fields
|
||||
formData.fields.addAll([
|
||||
MapEntry('firstName', data['firstName']),
|
||||
MapEntry('lastName', data['lastName']),
|
||||
MapEntry('mobileNumber', data['mobileNumber']),
|
||||
|
||||
if (data['address1'] != null && data['address1'].toString().isNotEmpty)
|
||||
MapEntry('address1', data['address1']),
|
||||
|
||||
if (data['address2'] != null && data['address2'].toString().isNotEmpty)
|
||||
MapEntry('address2', data['address2']),
|
||||
|
||||
/// ⭐ NEW FIELDS
|
||||
if (data['city'] != null && data['city'].toString().isNotEmpty)
|
||||
MapEntry('city', data['city']),
|
||||
|
||||
if (data['state'] != null && data['state'].toString().isNotEmpty)
|
||||
MapEntry('state', data['state']),
|
||||
|
||||
if (data['country'] != null && data['country'].toString().isNotEmpty)
|
||||
MapEntry('country', data['country']),
|
||||
|
||||
if (data['postalCode'] != null &&
|
||||
data['postalCode'].toString().isNotEmpty)
|
||||
MapEntry('postalCode', data['postalCode']),
|
||||
]);
|
||||
|
||||
// ⭐ Add profile image file if provided
|
||||
/// ✅ Add Profile Image File
|
||||
if (profileImageFile != null) {
|
||||
final fileName = profileImageFile.path.split('/').last;
|
||||
|
||||
formData.files.add(
|
||||
MapEntry(
|
||||
'profileImage',
|
||||
@@ -68,46 +93,36 @@ class ProfileRepository {
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📤 [UPDATE PROFILE] ✅ Profile Image File Added');
|
||||
print('📤 [UPDATE PROFILE] File Name: $fileName');
|
||||
print('📤 [UPDATE PROFILE] File Path: ${profileImageFile.path}');
|
||||
final fileSize = await profileImageFile.length();
|
||||
print('📤 [UPDATE PROFILE] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB');
|
||||
print('📤 ✅ Profile Image Added');
|
||||
print('📤 File Name: $fileName');
|
||||
print(
|
||||
'📤 File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB');
|
||||
}
|
||||
} else {
|
||||
if (kDebugMode) {
|
||||
print('📤 [UPDATE PROFILE] ⚠️ No profile image file provided');
|
||||
print('📤 ⚠️ No profile image provided');
|
||||
}
|
||||
}
|
||||
|
||||
// ⭐ Send as multipart/form-data
|
||||
/// ✅ API Call (Multipart PUT)
|
||||
final response = await _apiService.putApi(
|
||||
url: '${ApiUrls.userProfile}/$userId',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📤 [UPDATE PROFILE] ✅ Response Status: Success');
|
||||
print('📤 [UPDATE PROFILE] Full Response: ${response.data}');
|
||||
|
||||
// Check if response has nested 'user' object
|
||||
if (response.data.containsKey('user')) {
|
||||
print('📤 [UPDATE PROFILE] ✅ Response has nested "user" object');
|
||||
print('📤 [UPDATE PROFILE] User Data: ${response.data['user']}');
|
||||
print('📤 [UPDATE PROFILE] Updated Profile Image: ${response.data['user']['profileImage']}');
|
||||
} else {
|
||||
print('📤 [UPDATE PROFILE] Response structure: ${response.data.keys.toList()}');
|
||||
print('📤 [UPDATE PROFILE] Updated Profile Image: ${response.data['profileImage']}');
|
||||
}
|
||||
print('📤 ✅ Response Success');
|
||||
print('📤 Full Response: ${response.data}');
|
||||
}
|
||||
|
||||
// Extract user data from nested response
|
||||
/// ✅ Handle Nested Response (user object)
|
||||
final userData = response.data.containsKey('user')
|
||||
? response.data['user']
|
||||
: response.data;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📤 [UPDATE PROFILE] Parsing ProfileModel from: $userData');
|
||||
print('📤 Parsing ProfileModel from: $userData');
|
||||
}
|
||||
|
||||
return ProfileModel.fromJson(userData);
|
||||
|
||||
@@ -30,6 +30,12 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final TextEditingController address1Controller = TextEditingController();
|
||||
final TextEditingController address2Controller = TextEditingController();
|
||||
final TextEditingController cityController = TextEditingController();
|
||||
final TextEditingController zipCodeController = TextEditingController();
|
||||
|
||||
// Dropdown values
|
||||
String? selectedState;
|
||||
String? selectedCountry;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
@@ -64,6 +70,14 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
phoneController.text = profile.mobileNumber;
|
||||
address1Controller.text = profile.address1 ?? '';
|
||||
address2Controller.text = profile.address2 ?? '';
|
||||
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) {
|
||||
@@ -321,6 +335,15 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
address2: address2Controller.text.trim().isEmpty
|
||||
? null
|
||||
: address2Controller.text.trim(),
|
||||
// ⭐ UPDATED: Use dropdown values instead of controllers
|
||||
city: cityController.text.trim().isEmpty
|
||||
? null
|
||||
: cityController.text.trim(),
|
||||
state: selectedState,
|
||||
country: selectedCountry,
|
||||
postalCode: zipCodeController.text.trim().isEmpty
|
||||
? null
|
||||
: zipCodeController.text.trim(),
|
||||
profileImageFile: imageFileToSend,
|
||||
),
|
||||
);
|
||||
@@ -333,6 +356,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
phoneController.dispose();
|
||||
address1Controller.dispose();
|
||||
address2Controller.dispose();
|
||||
cityController.dispose();
|
||||
zipCodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -495,7 +520,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
|
||||
child: CustomTextField(
|
||||
label: "Address 1",
|
||||
label: "Address",
|
||||
hint: "Enter address manually or tap to search",
|
||||
controller: address1Controller,
|
||||
enabled: !isLoading,
|
||||
@@ -512,6 +537,151 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
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.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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
|
||||
child: CustomTextField(
|
||||
label: "City",
|
||||
hint: "Enter the name of your city",
|
||||
controller: cityController,
|
||||
enabled: !isLoading,
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0.w),
|
||||
child: CustomTextField(
|
||||
label: "ZIP Code",
|
||||
hint: "Enter the ZIP code you reside in",
|
||||
controller: zipCodeController,
|
||||
enabled: !isLoading,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 26.h),
|
||||
|
||||
// Buttons
|
||||
|
||||
@@ -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'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user