diff --git a/README.md b/README.md
index 3f2cf54..fcb7f41 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ A few resources to get you started if this is your first Flutter project:
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
-[online documentation](https://docs.flutter.dev/), which offers tutorials,
+[online documentation](https://docs.flutter.dev/),which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Figma Link
diff --git a/assets/images/empty_postcard_drafts.png b/assets/images/empty_postcard_drafts.png
new file mode 100644
index 0000000..5f71aae
Binary files /dev/null and b/assets/images/empty_postcard_drafts.png differ
diff --git a/assets/images/empty_postcard_orders.png b/assets/images/empty_postcard_orders.png
new file mode 100644
index 0000000..76f34b9
Binary files /dev/null and b/assets/images/empty_postcard_orders.png differ
diff --git a/lib/StripePayment/bloc/stripe_payment_bloc.dart b/lib/StripePayment/bloc/stripe_payment_bloc.dart
index 39e336e..4b63321 100644
--- a/lib/StripePayment/bloc/stripe_payment_bloc.dart
+++ b/lib/StripePayment/bloc/stripe_payment_bloc.dart
@@ -14,6 +14,7 @@ class StripePaymentBloc extends Bloc {
}) : _stripeService = stripeService ?? StripeService(),
super(const StripePaymentInitial()) {
on(_onInitiatePayment);
+ on(_onInitiatePaymentWithClientSecret);
on(_onResetPaymentState);
}
@@ -66,6 +67,46 @@ class StripePaymentBloc extends Bloc {
}
}
+ /// đ NEW: Handle payment with clientSecret directly from backend
+ Future _onInitiatePaymentWithClientSecret(
+ InitiatePaymentWithClientSecret event,
+ Emitter emit,
+ ) async {
+ try {
+ emit(const StripePaymentLoading());
+
+ // 1ī¸âŖ Init Payment Sheet with clientSecret from backend
+ await Stripe.instance.initPaymentSheet(
+ paymentSheetParameters: SetupPaymentSheetParameters(
+ paymentIntentClientSecret: event.clientSecret,
+ merchantDisplayName: "CityCards",
+ style: ThemeMode.light,
+ ),
+ );
+
+ // 2ī¸âŖ Show Payment Sheet
+ await Stripe.instance.presentPaymentSheet();
+
+ // â
SUCCESS
+ emit(const StripePaymentSuccess());
+ } on StripeException catch (e) {
+ // Handle Stripe-specific errors
+ if (e.error.code == FailureCode.Canceled) {
+ emit(StripePaymentCancelled(
+ message: e.error.localizedMessage ?? 'Payment Cancelled',
+ ));
+ } else {
+ emit(StripePaymentFailure(
+ error: e.error.localizedMessage ?? 'Payment failed',
+ ));
+ }
+ } catch (e) {
+ emit(StripePaymentFailure(
+ error: e.toString(),
+ ));
+ }
+ }
+
void _onResetPaymentState(
ResetPaymentState event,
Emitter emit,
diff --git a/lib/StripePayment/bloc/stripe_payment_event.dart b/lib/StripePayment/bloc/stripe_payment_event.dart
index f356b54..470e359 100644
--- a/lib/StripePayment/bloc/stripe_payment_event.dart
+++ b/lib/StripePayment/bloc/stripe_payment_event.dart
@@ -20,6 +20,18 @@ class InitiatePayment extends StripePaymentEvent {
List get props => [amount, currency];
}
+/// đ NEW: Event to initiate payment with clientSecret from backend
+class InitiatePaymentWithClientSecret extends StripePaymentEvent {
+ final String clientSecret;
+
+ const InitiatePaymentWithClientSecret({
+ required this.clientSecret,
+ });
+
+ @override
+ List get props => [clientSecret];
+}
+
class ResetPaymentState extends StripePaymentEvent {
const ResetPaymentState();
}
\ No newline at end of file
diff --git a/lib/StripePayment/repository/stripe_service.dart b/lib/StripePayment/repository/stripe_service.dart
index dddd3d1..312daee 100644
--- a/lib/StripePayment/repository/stripe_service.dart
+++ b/lib/StripePayment/repository/stripe_service.dart
@@ -11,7 +11,7 @@ class StripeService {
// â ī¸ TEMPORARY FALLBACK - Use secret key directly
// TODO: Remove this and use backend when ready!
- final String _stripeSecretKey = 'sk_test_51SrwZ7RtCkWyT4EmgS97odPlrKNj2TUxIkyu5L2i6qQyEpCivhYtEO6cW660UjBMoUsN1rUldvVhGx7RpGMarANp00Ntyi2Bp4'; // â ADD YOUR SECRET KEY
+ final String _stripeSecretKey = ''; // â ADD YOUR SECRET KEY
Future createPaymentIntent({
required int amount,
diff --git a/lib/buy_a_pass/view/buy_pass_view.dart b/lib/buy_a_pass/view/buy_pass_view.dart
index 3ffd6e0..c01845a 100644
--- a/lib/buy_a_pass/view/buy_pass_view.dart
+++ b/lib/buy_a_pass/view/buy_pass_view.dart
@@ -240,102 +240,110 @@ class BuyPassContent extends StatelessWidget {
itemBuilder: (context, index) {
final offer = selectedCard.offers[index];
- return Container(
- padding: EdgeInsets.symmetric(
- horizontal: 6.w,
- vertical: 6.h,
- ),
- decoration: BoxDecoration(
- border: Border.all(
- color: const Color(0xFFF95F62).withOpacity(.24),
+ return GestureDetector(
+ onTap: () {
+ Navigator.of(context).pushNamed(
+ RouteConstants.offerPassDetail,
+ arguments: offer.id, // â
pass offerId
+ );
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: 6.w,
+ vertical: 6.h,
),
- borderRadius: BorderRadius.circular(12.sp),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- /// Image
- 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: const Color(0xFFFEE7E7),
- child: Icon(
- Icons.local_offer,
- size: 40.sp,
- color:
- const Color(0xFFF95F62).withOpacity(.6),
- ),
- );
- },
- loadingBuilder:
- (context, child, loadingProgress) {
- if (loadingProgress == null) return child;
-
- return Container(
- width: double.infinity,
- height: 120.5.h,
- color: const Color(0xFFFEE7E7),
- child: Center(
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: const Color(0xFFF95F62),
- value: loadingProgress
- .expectedTotalBytes !=
- null
- ? loadingProgress
- .cumulativeBytesLoaded /
- loadingProgress
- .expectedTotalBytes!
- : null,
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: const Color(0xFFF95F62).withOpacity(.24),
+ ),
+ borderRadius: BorderRadius.circular(12.sp),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ /// Image
+ 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: const Color(0xFFFEE7E7),
+ child: Icon(
+ Icons.local_offer,
+ size: 40.sp,
+ color:
+ const Color(0xFFF95F62).withOpacity(.6),
),
- ),
- );
- },
- )
- : Container(
- width: double.infinity,
- height: 120.5.h,
- color: const Color(0xFFFEE7E7),
- child: Icon(
- Icons.local_offer,
- size: 40.sp,
- color:
- const Color(0xFFF95F62).withOpacity(.6),
+ );
+ },
+ loadingBuilder:
+ (context, child, loadingProgress) {
+ if (loadingProgress == null) return child;
+
+ return Container(
+ width: double.infinity,
+ height: 120.5.h,
+ color: const Color(0xFFFEE7E7),
+ child: Center(
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: const Color(0xFFF95F62),
+ value: loadingProgress
+ .expectedTotalBytes !=
+ null
+ ? loadingProgress
+ .cumulativeBytesLoaded /
+ loadingProgress
+ .expectedTotalBytes!
+ : null,
+ ),
+ ),
+ );
+ },
+ )
+ : Container(
+ width: double.infinity,
+ height: 120.5.h,
+ color: const Color(0xFFFEE7E7),
+ child: Icon(
+ Icons.local_offer,
+ size: 40.sp,
+ color:
+ const Color(0xFFF95F62).withOpacity(.6),
+ ),
),
),
- ),
- SizedBox(height: 8.h),
+ SizedBox(height: 8.h),
- /// Title
- CustomText(
- text: offer.title,
- size: 18.sp,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
+ /// Title
+ CustomText(
+ text: offer.title,
+ size: 18.sp,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
- SizedBox(height: 8.h),
+ SizedBox(height: 8.h),
- /// Offer Code
- CustomText(
- text: offer.description??"N/A",
- color: Colors.black.withOpacity(.6),
- size: 12.sp,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
- ],
+ /// Offer Code
+ CustomText(
+ text: offer.description??"N/A",
+ color: Colors.black.withOpacity(.6),
+ size: 12.sp,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
),
);
},
@@ -386,35 +394,43 @@ class BuyPassContent extends StatelessWidget {
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8.r),
),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(8.r),
- child: attraction.thumbnail != null &&
- attraction.thumbnail!.isNotEmpty
- ? Image.network(
- attraction.thumbnail!,
- fit: BoxFit.cover,
- errorBuilder: (context, error, stackTrace) {
- return Icon(
- Icons.location_on,
- size: 40.sp,
- color: Colors.grey[400],
- );
- },
- loadingBuilder: (context, child, loadingProgress) {
- if (loadingProgress == null) return child;
- return Center(
- child: SizedBox(
- width: 20.w,
- height: 20.w,
- child: CircularProgressIndicator(strokeWidth: 2),
- ),
- );
- },
- )
- : Icon(
- Icons.location_on,
- size: 40.sp,
- color: Colors.grey[400],
+ child: GestureDetector(
+ onTap: () {
+ // Navigator.of(context).pushNamed(
+ // RouteConstants.attractionDetails,
+ // arguments: attraction,
+ // );
+ },
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8.r),
+ child: attraction.thumbnail != null &&
+ attraction.thumbnail!.isNotEmpty
+ ? Image.network(
+ attraction.thumbnail!,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Icon(
+ Icons.location_on,
+ size: 40.sp,
+ color: Colors.grey[400],
+ );
+ },
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) return child;
+ return Center(
+ child: SizedBox(
+ width: 20.w,
+ height: 20.w,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ ),
+ );
+ },
+ )
+ : Icon(
+ Icons.location_on,
+ size: 40.sp,
+ color: Colors.grey[400],
+ ),
),
),
),
@@ -447,12 +463,20 @@ class BuyPassContent extends StatelessWidget {
),
SizedBox(height: 20.h),
- Align(
- alignment: Alignment.center,
- child: CustomText(
- text: "View All",
- size: 12.sp,
- color: Color(0xFFF95F62),
+ GestureDetector(
+ onTap: () {
+ Navigator.of(context).pushNamed(
+ RouteConstants.attractionsPage,
+ arguments: "home",
+ );
+ },
+ child: Align(
+ alignment: Alignment.center,
+ child: CustomText(
+ text: "View All",
+ size: 12.sp,
+ color: Color(0xFFF95F62),
+ ),
),
),
SizedBox(height: 41.h),
diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart
index f88b7f2..23d29c3 100644
--- a/lib/buy_a_pass/widget/payment_card_view.dart
+++ b/lib/buy_a_pass/widget/payment_card_view.dart
@@ -3,6 +3,7 @@ import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
+import '../../localPreference/local_preference.dart';
import '../models/checkout_model.dart';
import '../../checkout/view/checkout_view.dart'; // â
Import CheckoutView directly
@@ -127,7 +128,7 @@ class PaymentCard extends StatelessWidget {
),
SizedBox(height: 20.h),
CustomFilledButton(
- onTap: () {
+ onTap: () async {
// â
Create checkout data
final checkoutData = CheckoutData(
cityName: city,
@@ -144,6 +145,21 @@ class PaymentCard extends StatelessWidget {
description: description,
);
+ await LocalPreference.setPassCart(
+ cityName: city,
+ heroImage: heroImage,
+ cardTypeName: cardType,
+ cardDisplayName: cardDisplayName,
+ themeColor: themeColor.value, // Convert Color to int
+ adultCount: adults,
+ childCount: children,
+ adultPrice: adultPrice,
+ childPrice: childPrice,
+ validityDuration: selectedValue,
+ totalPrice: totalPrice,
+ description: description,
+ );
+
// â
DIRECT NAVIGATION - This fixes the route issue!
Navigator.of(context).push(
MaterialPageRoute(
diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart b/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart
new file mode 100644
index 0000000..67607d4
--- /dev/null
+++ b/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import '../../repository/my_pass_cart_repository.dart';
+import 'my_pass_cart_event.dart';
+import 'my_pass_cart_state.dart';
+
+class MyPassCartBloc extends Bloc {
+ final MyPassCartRepository repository;
+
+ MyPassCartBloc({required this.repository}) : super(const MyPassCartInitial()) {
+ on(_onFetchPassCart);
+ on(_onClearPassCart);
+ }
+
+ /// Handle fetching pass cart data
+ Future _onFetchPassCart(
+ FetchPassCartEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ if (kDebugMode) {
+ print('đ [BLOC] Fetching pass cart...');
+ }
+
+ emit(const MyPassCartLoading());
+
+ final cartData = await repository.fetchPassesCartByLocal();
+
+ if (cartData != null) {
+ if (kDebugMode) {
+ print('â
[BLOC] Cart data loaded successfully');
+ }
+ emit(MyPassCartLoaded(cartData: cartData));
+ } else {
+ if (kDebugMode) {
+ print('âšī¸ [BLOC] Cart is empty');
+ }
+ emit(const MyPassCartEmpty());
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [BLOC] Error fetching cart: $e');
+ }
+ emit(MyPassCartError(message: e.toString()));
+ }
+ }
+
+ /// Handle clearing pass cart
+ Future _onClearPassCart(
+ ClearPassCartEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ if (kDebugMode) {
+ print('đ [BLOC] Clearing pass cart...');
+ }
+
+ // You can add clearPassCart method to repository if needed
+ // await repository.clearPassCartFromLocal();
+
+ emit(const MyPassCartEmpty());
+
+ if (kDebugMode) {
+ print('â
[BLOC] Cart cleared successfully');
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [BLOC] Error clearing cart: $e');
+ }
+ emit(MyPassCartError(message: e.toString()));
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_event.dart b/lib/cart/blocs/myPassCart/my_pass_cart_event.dart
new file mode 100644
index 0000000..5bd32ad
--- /dev/null
+++ b/lib/cart/blocs/myPassCart/my_pass_cart_event.dart
@@ -0,0 +1,18 @@
+import 'package:equatable/equatable.dart';
+
+abstract class MyPassCartEvent extends Equatable {
+ const MyPassCartEvent();
+
+ @override
+ List get props => [];
+}
+
+/// Event to fetch pass cart data from local database
+class FetchPassCartEvent extends MyPassCartEvent {
+ const FetchPassCartEvent();
+}
+
+/// Event to clear pass cart
+class ClearPassCartEvent extends MyPassCartEvent {
+ const ClearPassCartEvent();
+}
\ No newline at end of file
diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_state.dart b/lib/cart/blocs/myPassCart/my_pass_cart_state.dart
new file mode 100644
index 0000000..3d6ea24
--- /dev/null
+++ b/lib/cart/blocs/myPassCart/my_pass_cart_state.dart
@@ -0,0 +1,43 @@
+import 'package:equatable/equatable.dart';
+
+abstract class MyPassCartState extends Equatable {
+ const MyPassCartState();
+
+ @override
+ List get props => [];
+}
+
+/// Initial state
+class MyPassCartInitial extends MyPassCartState {
+ const MyPassCartInitial();
+}
+
+/// Loading state when fetching cart data
+class MyPassCartLoading extends MyPassCartState {
+ const MyPassCartLoading();
+}
+
+/// Loaded state with cart data
+class MyPassCartLoaded extends MyPassCartState {
+ final Map cartData;
+
+ const MyPassCartLoaded({required this.cartData});
+
+ @override
+ List get props => [cartData];
+}
+
+/// Empty state when no cart data exists
+class MyPassCartEmpty extends MyPassCartState {
+ const MyPassCartEmpty();
+}
+
+/// Error state
+class MyPassCartError extends MyPassCartState {
+ final String message;
+
+ const MyPassCartError({required this.message});
+
+ @override
+ List get props => [message];
+}
\ No newline at end of file
diff --git a/lib/cart/repository/my_pass_cart_repository.dart b/lib/cart/repository/my_pass_cart_repository.dart
new file mode 100644
index 0000000..c4b9be3
--- /dev/null
+++ b/lib/cart/repository/my_pass_cart_repository.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/foundation.dart';
+
+import '../../localPreference/local_preference.dart';
+
+class MyPassCartRepository {
+
+ /// Fetch pass cart data from local database
+ Future?> fetchPassesCartByLocal() async {
+ try {
+ if (kDebugMode) {
+ print('đ [REPO] Fetching pass cart from local database...');
+ }
+
+ final passCartData = await LocalPreference.getPassCart();
+
+ if (passCartData != null) {
+ if (kDebugMode) {
+ print('â
[REPO] Pass cart retrieved successfully');
+ print('đĻ [REPO] Cart details: ${passCartData['card_display_name']} - ${passCartData['city_name']}');
+ }
+ return passCartData;
+ } else {
+ if (kDebugMode) {
+ print('âšī¸ [REPO] No pass cart data found in local database');
+ }
+ return null;
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [REPO] Error fetching pass cart: $e');
+ }
+ rethrow;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/cart/views/my_cart_view_page.dart b/lib/cart/views/my_cart_view_page.dart
index 4005474..c1cead0 100644
--- a/lib/cart/views/my_cart_view_page.dart
+++ b/lib/cart/views/my_cart_view_page.dart
@@ -3,9 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/back_widget.dart';
-import '../blocs/pass_bloc.dart';
+import '../blocs/myPassCart/my_pass_cart_bloc.dart';
+import '../blocs/myPassCart/my_pass_cart_event.dart';
import '../blocs/postcard_bloc.dart';
-import 'my_pass_page_view.dart';
+import '../repository/my_pass_cart_repository.dart';
+import 'my_pass_cart_page_view.dart';
import 'my_postcard_page_view.dart';
class MyCartPage extends StatefulWidget {
@@ -22,8 +24,14 @@ class _MyCartPageState extends State {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
- BlocProvider(create: (_) => PassBloc()..add(LoadPasses())),
- BlocProvider(create: (_) => PostCardBloc()..add(LoadPostCards())),
+ BlocProvider(
+ create: (_) => PostCardBloc()..add(LoadPostCards()),
+ ),
+ BlocProvider(
+ create: (_) => MyPassCartBloc(
+ repository: MyPassCartRepository(),
+ )..add(const FetchPassCartEvent()),
+ ),
],
child: Scaffold(
backgroundColor: Colors.white,
diff --git a/lib/cart/views/my_pass_cart_page_view.dart b/lib/cart/views/my_pass_cart_page_view.dart
new file mode 100644
index 0000000..6f8d349
--- /dev/null
+++ b/lib/cart/views/my_pass_cart_page_view.dart
@@ -0,0 +1,486 @@
+import 'package:citycards_customer/cart/views/view_pass_page_view.dart';
+import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart';
+import 'package:citycards_customer/common_packages/custom_dashed_line.dart';
+import 'package:citycards_customer/common_packages/custom_filled_button.dart';
+import 'package:citycards_customer/common_packages/custom_text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import '../../login/view/login_email_bottomsheet.dart';
+import '../../common_packages/common_app_texts.dart';
+import '../../localPreference/local_preference.dart';
+import '../blocs/myPassCart/my_pass_cart_bloc.dart';
+import '../blocs/myPassCart/my_pass_cart_event.dart';
+import '../blocs/myPassCart/my_pass_cart_state.dart';
+
+class MyPassesPage extends StatefulWidget {
+ const MyPassesPage({super.key});
+
+ @override
+ State createState() => _MyPassesPageState();
+}
+
+class _MyPassesPageState extends State {
+ // For coupon/discount management
+ String? appliedCouponCode;
+ double discountPercentage = 0.0;
+
+ @override
+ void initState() {
+ super.initState();
+ // Fetch cart data when page loads
+ context.read().add(const FetchPassCartEvent());
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ 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?;
+
+ // Calculate pricing
+ final double subtotal = totalPrice;
+ final double discountAmount = subtotal * (discountPercentage / 100);
+ final double taxRate = 0.05; // 5% tax
+ final double totalBeforeTax = subtotal - discountAmount;
+ final double taxAmount = totalBeforeTax * taxRate;
+ final double finalTotal = totalBeforeTax + taxAmount;
+
+ // 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: [
+ 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,
+ ),
+ ),
+ // TextSpan(
+ // text: "Card",
+ // style: TextStyle(
+ // color: Colors.white,
+ // fontSize: 12.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: "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(),
+ 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),
+ 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 (discountPercentage > 0) ...[
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ CustomText(text: "Discount", size: 14.sp),
+ CustomText(
+ text: "-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.toStringAsFixed(0)}%)",
+ size: 14.sp,
+ weight: FontWeight.w500,
+ color: Colors.green,
+ ),
+ ],
+ ),
+ SizedBox(height: 14.h),
+ ],
+ DashedDivider(
+ color: 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 \$${taxAmount.toStringAsFixed(2)} in taxes",
+ size: 12.sp,
+ color: Colors.black.withOpacity(0.6),
+ ),
+ ],
+ ),
+ ),
+ CustomText(
+ text: "\$${finalTotal.toStringAsFixed(2)}",
+ size: 24.sp,
+ weight: FontWeight.w500,
+ ),
+ ],
+ ),
+ SizedBox(height: 150.h),
+
+ // FutureBuilder for login check
+ FutureBuilder(
+ 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) {
+ return Center(
+ child: Column(
+ children: [
+ Image.asset("assets/gif/empty_cart.gif", width: 250.w),
+ CustomText(
+ text: "You do not have any passes",
+ size: 24.sp,
+ color: Color(0xFFF95F62),
+ ),
+ SizedBox(height: 4.h),
+ Text(
+ "Get a pass and get offers and discounts and more on your trip to your favourite city",
+ style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ );
+ } else if (state is MyPassCartError) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.error_outline, size: 60.sp, color: Colors.red),
+ SizedBox(height: 16.h),
+ CustomText(
+ text: "Error loading cart",
+ size: 16.sp,
+ color: Colors.red,
+ ),
+ SizedBox(height: 8.h),
+ CustomText(
+ text: state.message,
+ size: 12.sp,
+ color: Colors.grey,
+ ),
+ ],
+ ),
+ );
+ }
+
+ return const SizedBox.shrink();
+ },
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/cart/views/my_pass_page_view.dart b/lib/cart/views/my_pass_page_view.dart
deleted file mode 100644
index ac3bf04..0000000
--- a/lib/cart/views/my_pass_page_view.dart
+++ /dev/null
@@ -1,379 +0,0 @@
-import 'package:citycards_customer/cart/views/view_pass_page_view.dart';
-import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart';
-import 'package:citycards_customer/common_packages/custom_dashed_line.dart';
-import 'package:citycards_customer/common_packages/custom_filled_button.dart';
-import 'package:citycards_customer/common_packages/custom_text.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flutter_screenutil/flutter_screenutil.dart';
-import '../../login/view/login_email_bottomsheet.dart';
-import '../../common_packages/common_app_texts.dart';
-import '../blocs/pass_bloc.dart';
-
-class MyPassesPage extends StatelessWidget {
- const MyPassesPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- return BlocBuilder(
- builder: (context, state) {
- if (state is PassLoading) {
- return const Center(child: CircularProgressIndicator());
- } else if (state is PassLoaded) {
- return
- Column(
- children: [
- SizedBox(height: 22.h),
- Container(
- decoration: BoxDecoration(
- color: Colors.white,
- border: Border.all(
- color: Color(0xFFF95FAF).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: 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: "Melbourne",
- weight: FontWeight.w500,
- size: 16.sp,
- ),
- SizedBox(height: 5.h),
- CustomText(
- text: "2 Days",
- 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: [
- Image.asset(
- 'assets/icons/adult.png',
- scale: 4,
- ),
- SizedBox(width: 4.w),
- CustomText(
- text: "3 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: " 2",
- 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: "3 Kids",
- color: Color(0xFF8E8E8E),
- size: 12.sp,
- ),
-
- SizedBox(width: 53.w),
-
- CustomText(
- text: "\$49.50",
- size: 24.sp,
- weight: FontWeight.w500,
- color: Color(0xFFF95F62),
- ),
- ],
- ),
- ],
- ),
- ],
- ),
-
- Container(
- width: 35.w,
- height: 123.h,
- decoration: BoxDecoration(
- color: Color(0xFFF97316),
- 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: "${CommonAppText.selectiveCard} ",
- style: TextStyle(
- color: Colors.white,
- fontSize: 16.sp,
- ),
- ),
- TextSpan(
- text: "Card",
- style: TextStyle(
- color: Colors.white,
- fontSize: 12.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: "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(),
- 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: "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),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- CustomText(text: "Subtotal", size: 14.sp),
- CustomText(
- text: "\$49.50",
- size: 14.sp,
- weight: FontWeight.w500,
- ),
- ],
- ),
- SizedBox(height: 14.h),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- CustomText(text: "Discount", size: 14.sp),
- CustomText(
- text: "-7.20%",
- size: 14.sp,
- weight: FontWeight.w500,
- ),
- ],
- ),
- SizedBox(height: 10.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 \$2.24 in taxes",
- size: 12.sp,
- color: Colors.black.withOpacity(0.6),
- ),
- ],
- ),
- ),
- CustomText(
- text: "\$42.60",
- size: 24.sp,
- weight: FontWeight.w500,
- ),
- ],
- ),
-
- SizedBox(height: 150.h,),
-
- CustomFilledButton(
- onTap: () {
- 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: "Login to Checkout",
- ),
- SizedBox(height: 25.h),
- ],
- );
- }
- return Center(
- child: Column(
- children: [
- Image.asset("assets/gif/empty_cart.gif", width: 250.w),
- CustomText(
- text: "You do not have any passes",
- size: 24.sp,
- color: Color(0xFFF95F62),
- ),
- SizedBox(height: 4.h),
- Text(
- "Get a pass and get offers and discounts and more on your trip to your favourite city",
- style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp),
- textAlign: TextAlign.center,
- ),
- ],
- ),
- );
- },
- );
- }
-}
diff --git a/lib/postcard/models/postcard_model.dart b/lib/checkout/bloc/allCoupons/all_coupons_bloc.dart
similarity index 100%
rename from lib/postcard/models/postcard_model.dart
rename to lib/checkout/bloc/allCoupons/all_coupons_bloc.dart
diff --git a/lib/postcard/repository/postcard_repository.dart b/lib/checkout/bloc/allCoupons/all_coupons_event.dart
similarity index 100%
rename from lib/postcard/repository/postcard_repository.dart
rename to lib/checkout/bloc/allCoupons/all_coupons_event.dart
diff --git a/lib/checkout/bloc/allCoupons/all_coupons_state.dart b/lib/checkout/bloc/allCoupons/all_coupons_state.dart
new file mode 100644
index 0000000..e69de29
diff --git a/lib/checkout/bloc/checkout/checkout_bloc.dart b/lib/checkout/bloc/checkout/checkout_bloc.dart
deleted file mode 100644
index 4cae026..0000000
--- a/lib/checkout/bloc/checkout/checkout_bloc.dart
+++ /dev/null
@@ -1,53 +0,0 @@
-import 'package:flutter_bloc/flutter_bloc.dart';
-
-part 'checkout_event.dart';
-part 'checkout_state.dart';
-
-/// BLoC for managing checkout screen state
-class CheckoutBloc extends Bloc {
- CheckoutBloc() : super(CheckoutState.initial()) {
- // Handle apply coupon event
- on(_onApplyCoupon);
-
- // Handle remove coupon event
- on(_onRemoveCoupon);
-
- // Handle confirm purchase details event
- on(_onConfirmPurchaseDetails);
-
- // Handle reset purchase details event
- on(_onResetPurchaseDetails);
- }
-
- /// Handle applying a coupon
- void _onApplyCoupon(ApplyCouponEvent event, Emitter emit) {
- emit(state.copyWith(
- appliedCouponCode: event.couponCode,
- discountPercentage: event.discountPercentage,
- ));
- }
-
- /// Handle removing a coupon
- void _onRemoveCoupon(RemoveCouponEvent event, Emitter emit) {
- emit(state.copyWith(
- clearCoupon: true,
- discountPercentage: 0.0,
- ));
- }
-
- /// Handle confirming purchase details
- void _onConfirmPurchaseDetails(
- ConfirmPurchaseDetailsEvent event,
- Emitter emit,
- ) {
- emit(state.copyWith(isPurchaseDetailsConfirmed: true));
- }
-
- /// Handle resetting purchase details confirmation
- void _onResetPurchaseDetails(
- ResetPurchaseDetailsEvent event,
- Emitter emit,
- ) {
- emit(state.copyWith(isPurchaseDetailsConfirmed: false));
- }
-}
\ No newline at end of file
diff --git a/lib/checkout/bloc/checkout/checkout_event.dart b/lib/checkout/bloc/checkout/checkout_event.dart
deleted file mode 100644
index 7ed9c86..0000000
--- a/lib/checkout/bloc/checkout/checkout_event.dart
+++ /dev/null
@@ -1,24 +0,0 @@
-part of 'checkout_bloc.dart';
-
-/// Base class for all checkout events
-abstract class CheckoutEvent {}
-
-/// Event to apply a coupon code
-class ApplyCouponEvent extends CheckoutEvent {
- final String couponCode;
- final double discountPercentage;
-
- ApplyCouponEvent({
- required this.couponCode,
- required this.discountPercentage,
- });
-}
-
-/// Event to remove the applied coupon
-class RemoveCouponEvent extends CheckoutEvent {}
-
-/// Event to confirm purchase details
-class ConfirmPurchaseDetailsEvent extends CheckoutEvent {}
-
-/// Event to reset purchase details confirmation
-class ResetPurchaseDetailsEvent extends CheckoutEvent {}
\ No newline at end of file
diff --git a/lib/checkout/bloc/checkout/checkout_state.dart b/lib/checkout/bloc/checkout/checkout_state.dart
deleted file mode 100644
index 00db8c2..0000000
--- a/lib/checkout/bloc/checkout/checkout_state.dart
+++ /dev/null
@@ -1,52 +0,0 @@
-part of 'checkout_bloc.dart';
-
-/// State class for checkout screen
-class CheckoutState {
- final String? appliedCouponCode;
- final double discountPercentage;
- final bool isPurchaseDetailsConfirmed;
-
- const CheckoutState({
- this.appliedCouponCode,
- this.discountPercentage = 0.0,
- this.isPurchaseDetailsConfirmed = false,
- });
-
- /// Initial state
- factory CheckoutState.initial() {
- return const CheckoutState();
- }
-
- /// Copy with method for immutable state updates
- CheckoutState copyWith({
- String? appliedCouponCode,
- double? discountPercentage,
- bool? isPurchaseDetailsConfirmed,
- bool clearCoupon = false,
- }) {
- return CheckoutState(
- appliedCouponCode: clearCoupon ? null : appliedCouponCode ?? this.appliedCouponCode,
- discountPercentage: discountPercentage ?? this.discountPercentage,
- isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed ?? this.isPurchaseDetailsConfirmed,
- );
- }
-
- /// Calculate discount amount based on subtotal
- double calculateDiscountAmount(double subtotal) {
- return subtotal * (discountPercentage / 100);
- }
-
- /// Calculate tax amount
- double calculateTaxAmount(double subtotal, {double taxRate = 0.05}) {
- final totalBeforeTax = subtotal - calculateDiscountAmount(subtotal);
- return totalBeforeTax * taxRate;
- }
-
- /// Calculate final total
- double calculateFinalTotal(double subtotal, {double taxRate = 0.05}) {
- final discountAmount = calculateDiscountAmount(subtotal);
- final totalBeforeTax = subtotal - discountAmount;
- final taxAmount = totalBeforeTax * taxRate;
- return totalBeforeTax + taxAmount;
- }
-}
\ No newline at end of file
diff --git a/lib/checkout/models/all_coupons_model.dart b/lib/checkout/models/all_coupons_model.dart
new file mode 100644
index 0000000..60698de
--- /dev/null
+++ b/lib/checkout/models/all_coupons_model.dart
@@ -0,0 +1,61 @@
+class AllCouponsModel {
+ final int id;
+ final String title;
+ final String? description;
+ final int cityXid;
+ final int discountPercent;
+ final String couponCode;
+ final DateTime startDateTime;
+ final DateTime endDateTime;
+ final bool showAtCheckout;
+ final String couponStatus;
+ final bool isActive;
+
+ AllCouponsModel({
+ required this.id,
+ required this.title,
+ this.description,
+ required this.cityXid,
+ required this.discountPercent,
+ required this.couponCode,
+ required this.startDateTime,
+ required this.endDateTime,
+ required this.showAtCheckout,
+ required this.couponStatus,
+ required this.isActive,
+ });
+
+ /// From JSON
+ factory AllCouponsModel.fromJson(Map json) {
+ return AllCouponsModel(
+ id: json['id'] as int,
+ title: json['title'] as String,
+ description: json['description'],
+ cityXid: json['cityXid'] as int,
+ discountPercent: json['discountPercent'] as int,
+ couponCode: json['couponCode'] as String,
+ startDateTime: DateTime.parse(json['startDateTime']),
+ endDateTime: DateTime.parse(json['endDateTime']),
+ showAtCheckout: json['showAtCheckout'] as bool,
+ couponStatus: json['couponStatus'] as String,
+ isActive: json['isActive'] as bool,
+ );
+ }
+
+ /// To JSON
+ Map toJson() {
+ return {
+ 'id': id,
+ 'title': title,
+ 'description': description,
+ 'cityXid': cityXid,
+ 'discountPercent': discountPercent,
+ 'couponCode': couponCode,
+ 'startDateTime': startDateTime.toIso8601String(),
+ 'endDateTime': endDateTime.toIso8601String(),
+ 'showAtCheckout': showAtCheckout,
+ 'couponStatus': couponStatus,
+ 'isActive': isActive,
+ };
+ }
+}
diff --git a/lib/checkout/repository/all_coupons_repository.dart b/lib/checkout/repository/all_coupons_repository.dart
new file mode 100644
index 0000000..e69de29
diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart
index 8813992..83e18be 100644
--- a/lib/checkout/view/checkout_view.dart
+++ b/lib/checkout/view/checkout_view.dart
@@ -5,15 +5,10 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import 'package:citycards_customer/common_packages/custom_dashed_line.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
-
import '../../StripePayment/view/stripe_payment.dart';
import '../../buy_a_pass/models/checkout_model.dart';
-import '../../common_packages/common_app_texts.dart';
import '../../localPreference/local_preference.dart';
-import '../../postcard/widgets/purchase_details_bottom_sheet.dart';
-import '../bloc/pass_purchase_details_bloc.dart';
import '../widget/pass_purchase_details_bottomsheet.dart';
class CheckoutView extends StatefulWidget {
diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart
index 6180a3c..b53bbcc 100644
--- a/lib/common_packages/app_bar.dart
+++ b/lib/common_packages/app_bar.dart
@@ -1,8 +1,13 @@
+import 'package:citycards_customer/networkApiServices/api_urls.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../core/route_constants.dart';
import '../home/widgets/search_city_bottomsheet.dart';
+import '../localPreference/local_preference.dart';
+import '../profile/bloc/profile/profile_bloc.dart';
+import '../profile/bloc/profile/profile_state.dart';
class CommonAppBar extends StatelessWidget {
const CommonAppBar({
@@ -115,11 +120,39 @@ class CommonAppBar extends StatelessWidget {
rootNavigator: true,
).pushNamed(RouteConstants.profile);
},
- child: CircleAvatar(
- backgroundColor: const Color(0xffFFDFDF),
- child: Image.asset(
- "assets/images/profile_default_img.png",
- ),
+ child: BlocBuilder(
+ builder: (context, state) {
+ String? imagePath;
+
+ // â
Get image from profile state
+ if (state is ProfileLoaded) {
+ imagePath = state.profile.profileImage;
+ }
+
+ // â
Build full image URL
+ final String? imageUrl =
+ (imagePath != null && imagePath.isNotEmpty)
+ ? "${ApiUrls.baseUrl}$imagePath"
+ : null;
+
+ return CircleAvatar(
+ radius: 20.r,
+ backgroundColor: const Color(0xffFFDFDF),
+
+ // â
Network image only if exists
+ backgroundImage:
+ (imageUrl != null && imageUrl.isNotEmpty)
+ ? NetworkImage(imageUrl)
+ : null,
+
+ // â
Default fallback (unchanged)
+ child: (imageUrl == null || imageUrl.isEmpty)
+ ? Image.asset(
+ "assets/images/profile_default_img.png",
+ )
+ : null,
+ );
+ },
),
),
],
diff --git a/lib/contact_us/contact_us_view.dart b/lib/contact_us/contact_us_view.dart
deleted file mode 100644
index ad56f7c..0000000
--- a/lib/contact_us/contact_us_view.dart
+++ /dev/null
@@ -1,237 +0,0 @@
-import 'package:citycards_customer/common_packages/app_bar.dart';
-import 'package:citycards_customer/common_packages/back_widget.dart';
-import 'package:flutter/material.dart';
-import 'package:citycards_customer/common_packages/custom_text.dart';
-import 'package:citycards_customer/common_packages/custom_textfield.dart';
-import 'package:flutter_screenutil/flutter_screenutil.dart';
-
-class ContactUsPage extends StatelessWidget {
- const ContactUsPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- final TextEditingController firstNameController = TextEditingController();
- final TextEditingController lastNameController = TextEditingController();
- final TextEditingController emailController = TextEditingController();
- final TextEditingController phoneController = TextEditingController();
- final TextEditingController messageController = TextEditingController();
-
- return Scaffold(
- backgroundColor: Colors.white,
- body: SafeArea(
- child: SingleChildScrollView(
- padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Header bar
- CommonAppBar(isWhiteLogo: false, isProfilePage: true, showDivider: true,),
-
- backWidget(context,"Contact Us", Colors.black),
- SizedBox(height: 22.h),
-
- CustomText(
- text:
- "You can get in touch with us through the below platforms. Our team will contact you shortly",
- size: 14.sp,
- color: Colors.black.withOpacity(.6),
- ),
- SizedBox(height: 20.h),
-
- // Customer Support Section
- Container(
- padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 16.h),
- decoration: BoxDecoration(
- color: Color(0x00000005).withOpacity(.02),
- borderRadius: BorderRadius.circular(12.r),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- CustomText(
- text: "Customer Support",
- size: 18.sp,
- weight: FontWeight.w500,
- ),
- SizedBox(height: 16.h),
- _supportBox(
- icon: Icons.phone,
- title: "Contact Number",
- subtitle: "+1012 3456 789",
- action: "Tap to call",
- ),
- SizedBox(height: 12.h),
- _supportBox(
- icon: Icons.email_rounded,
- title: "Email",
- subtitle: "citycards24@gmail.com",
- action: "Tap to email",
- ),
- SizedBox(height: 12.h),
- _supportBox(
- icon: Icons.location_on,
- title: "Location",
- subtitle:
- "132 Dartmouth Street Boston, Massachusetts 02156 United States",
- action: "View on map",
- ),
- ],
- ),
- ),
- SizedBox(height: 24.h),
-
- // Text fields
- CustomTextField(
- label: "First Name",
- hint: "Enter your first name",
- controller: firstNameController,
- ),
- CustomTextField(
- label: "Last Name",
- hint: "Enter your last name",
- controller: lastNameController,
- ),
- CustomTextField(
- label: "Email",
- hint: "Enter your email address",
- controller: emailController,
- ),
- CustomTextField(
- label: "Phone Number",
- hint: "Enter your phone number",
- controller: phoneController,
- ),
-
- CustomTextField(
- label: "Description",
- hint: "Write your message here",
- maxLines: 4,
- controller: messageController,
- ),
-
- // _descriptionField(messageController),
- SizedBox(height: 24.h),
-
- // Submit Button
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xFFF95F62),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(38.r),
- ),
- padding: EdgeInsets.symmetric(vertical: 6.h),
- ),
- onPressed: () {},
- child: CustomText(
- text: "Submit Ticket",
- size: 16.sp,
- weight: FontWeight.w500,
- color: Colors.white,
- ),
- ),
- ),
- SizedBox(height: 20.h),
- ],
- ),
- ),
- ),
- );
- }
-
- // --- Support Info Box ---
- Widget _supportBox({
- required IconData icon,
- required String title,
- required String subtitle,
- required String action,
- }) {
- return Container(
- width: double.infinity,
- padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(8.r),
- border: Border.all(color: const Color(0xFFF95F62), width: 0.8),
- color: Colors.white,
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Icon(icon, color: const Color(0xFFF95F62), size: 32.sp),
- SizedBox(width: 12.w),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- CustomText(
- text: title,
- size: 11.sp,
- weight: FontWeight.w600,
- color: Color(0x00000000).withOpacity(.6),
- ),
- SizedBox(height: 6.h),
- Text(
- subtitle,
- style: TextStyle(
- fontSize: 14.sp,
- fontWeight: FontWeight.w400,
- color: Colors.black,
- ),
- ),
- SizedBox(height: 2.h),
- Text(
- action,
- style: TextStyle(
- fontSize: 11.sp,
- color: Color(0xFF000000).withOpacity(.4),
- fontWeight: FontWeight.w400,
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- );
- }
-
- // --- Description Field ---
- Widget _descriptionField(TextEditingController controller) {
- return Padding(
- padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- CustomText(text: "Description", size: 14.sp),
- SizedBox(height: 6.h),
- TextField(
- controller: controller,
- maxLines: 4,
- decoration: InputDecoration(
- hintText: "Write your message here",
- hintStyle: TextStyle(fontSize: 12.sp, color: Color(0xFF8E8E8E)),
- filled: true,
- fillColor: const Color(0xFFFFF5F5),
- contentPadding: EdgeInsets.symmetric(
- horizontal: 24.w,
- vertical: 12.h,
- ),
- enabledBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.r),
- borderSide: BorderSide(
- color: const Color(0xBBC83B61).withOpacity(0.4),
- width: .4.w,
- ),
- ),
- focusedBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.r),
- borderSide: BorderSide(color: Color(0xFFF95F62), width: 1.w),
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart
index f733105..47812b7 100644
--- a/lib/core/app_router.dart
+++ b/lib/core/app_router.dart
@@ -5,7 +5,6 @@ import 'package:citycards_customer/attractions/models/attraction_model.dart';
import 'package:citycards_customer/buy_a_pass/view/buy_pass_view.dart';
import 'package:citycards_customer/checkout/view/checkout_view.dart';
import 'package:citycards_customer/common_bloc/language_selection_bloc.dart';
-import 'package:citycards_customer/contact_us/contact_us_view.dart';
import 'package:citycards_customer/create_account/view/create_account_view.dart';
import 'package:citycards_customer/esim_offer/esim_offer_view.dart';
import 'package:citycards_customer/hotel_offer/hotel_offer_view.dart';
@@ -29,6 +28,7 @@ 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 '../profile/view/contact_us/contact_us_view.dart';
import '../profile/view/edit_profile/edit_profile_view.dart';
import '../profile/view/faq/faq_view.dart';
import '../profile/view/privacy/privacy_view.dart';
diff --git a/lib/create_account/bloc/create_account_bloc.dart b/lib/create_account/bloc/create_account_bloc.dart
index 6c2abcd..cd62f97 100644
--- a/lib/create_account/bloc/create_account_bloc.dart
+++ b/lib/create_account/bloc/create_account_bloc.dart
@@ -45,6 +45,7 @@ class CreateAccountBloc extends Bloc {
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'] ?? {},
diff --git a/lib/create_account/models/create_account_model.dart b/lib/create_account/models/create_account_model.dart
index 506d46e..7f28757 100644
--- a/lib/create_account/models/create_account_model.dart
+++ b/lib/create_account/models/create_account_model.dart
@@ -47,6 +47,7 @@ class User {
final String lastName;
final String fullName;
final String emailAddress;
+ final String profileImage; // â
newly added
final String role;
final int roleId;
@@ -56,6 +57,7 @@ class User {
required this.lastName,
required this.fullName,
required this.emailAddress,
+ required this.profileImage,
required this.role,
required this.roleId,
});
@@ -67,6 +69,7 @@ class User {
lastName: json['lastName'] ?? '',
fullName: json['fullName'] ?? '',
emailAddress: json['emailAddress'] ?? '',
+ profileImage: json['profileImage'] ?? '',
role: json['role'] ?? '',
roleId: json['roleId'] ?? 0,
);
@@ -79,6 +82,7 @@ class User {
'lastName': lastName,
'fullName': fullName,
'emailAddress': emailAddress,
+ 'profileImage': profileImage,
'role': role,
'roleId': roleId,
};
diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart
index 3fa9f15..a0d3c5e 100644
--- a/lib/create_account/view/create_account_view.dart
+++ b/lib/create_account/view/create_account_view.dart
@@ -5,6 +5,9 @@ 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 '../../localPreference/local_preference.dart';
+import '../../profile/bloc/profile/profile_bloc.dart';
+import '../../profile/bloc/profile/profile_event.dart';
import '../bloc/create_account_bloc.dart';
import '../bloc/create_account_event.dart';
import '../bloc/create_account_state.dart';
@@ -52,11 +55,15 @@ class CreateAccountView extends StatelessWidget {
repository: CreateAccountRepository(),
),
child: BlocListener(
- listener: (context, state) {
+ listener: (context, 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().add(FetchProfileEvent(userId: userId!));
+ context.read().add(CheckLoginStatusEvent());
Navigator.pop(context);
Navigator.pop(context);
} else if (state is CreateAccountFailure) {
diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart
index af9859b..024b4a6 100644
--- a/lib/home/views/first_time_user_home_page.dart
+++ b/lib/home/views/first_time_user_home_page.dart
@@ -201,13 +201,23 @@ class _FirstTimeUserHomePageState extends State {
// Determine if it's a network image or asset
final isNetworkImage = imageUrl.startsWith('http');
- return ExploreCitiesCard(
- name: city.cityName ?? 'N/A',
- description: city.tagLine ?? 'N/A',
- imageUrl: imageUrl,
- individualPrice: '\$${city.indivisualTicketAmt ?? 0}+',
- cityCardPrice: '\$${city.cityCardTicketAmt ?? 0}',
- savingsText: city.saveLabel ?? 'Save \$0+',
+ return GestureDetector(
+ onTap: () async {
+ await LocalPreference.updateOnboardingPage(2);
+ await LocalPreference.setSelectedCityId(city.id!);
+ Navigator.pushReplacementNamed(
+ context,
+ RouteConstants.home,
+ );
+ },
+ child: ExploreCitiesCard(
+ name: city.cityName ?? 'N/A',
+ description: city.tagLine ?? 'N/A',
+ imageUrl: imageUrl,
+ individualPrice: '\$${city.indivisualTicketAmt ?? 0}+',
+ cityCardPrice: '\$${city.cityCardTicketAmt ?? 0}',
+ savingsText: city.saveLabel ?? 'Save \$0+',
+ ),
);
},
),
diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart
index 07659a1..a7e4201 100644
--- a/lib/home/views/home_page_view.dart
+++ b/lib/home/views/home_page_view.dart
@@ -1,4 +1,5 @@
import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart';
+import 'package:citycards_customer/postcard/views/my_postcards_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:citycards_customer/common_packages/custom_bottom_navbar.dart';
@@ -52,7 +53,7 @@ class _HomePageState extends State {
buildOffstageNavigator(
3,
currentIndex,
- const PostcardPage(),
+ const MyPostCardsView(),
_navigatorKeys[3],
),
],
diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart
index 3521586..890fc2a 100644
--- a/lib/home/views/registered_user_home_page.dart
+++ b/lib/home/views/registered_user_home_page.dart
@@ -9,6 +9,10 @@ import '../../common_packages/app_bar.dart';
import '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart';
import '../../networkApiServices/api_urls.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/registeredHome/home_bloc.dart';
import '../bloc/registeredHome/home_event.dart';
import '../bloc/registeredHome/home_state.dart';
@@ -31,7 +35,29 @@ class _RegisteredUserHomePageState extends State {
@override
void initState() {
super.initState();
+ // _loadMyPostCards();
_checkAndShowCitySelection();
+ _loadProfileIfLoggedIn();
+ }
+ Future _loadProfileIfLoggedIn() async {
+ final userId = await LocalPreference.getUserId();
+
+ if (userId != null && mounted) {
+ context.read().add(
+ FetchProfileEvent(userId: userId),
+ );
+ }
+ }
+
+ Future _loadMyPostCards() async {
+ final userId = await LocalPreference.getUserId();
+
+ if (userId != null && mounted) {
+ context.read().add(FetchDraftPostCards());
+ context.read().add(RefreshDraftPostCards());
+ context.read().add(RefreshOrderPostCards());
+ context.read().add(FetchOrderPostCards());
+ }
}
Future _checkAndShowCitySelection() async {
diff --git a/lib/intro_screens/views/intro_screen_view.dart b/lib/intro_screens/views/intro_screen_view.dart
index c6c8158..84c8cc2 100644
--- a/lib/intro_screens/views/intro_screen_view.dart
+++ b/lib/intro_screens/views/intro_screen_view.dart
@@ -79,7 +79,12 @@ class _IntroScreensViewState extends State {
right: 20,
child: GestureDetector(
onTap: (){
- Navigator.pushReplacementNamed(context,RouteConstants.home);
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (_) => const FirstTimeUserHomePage(),
+ ),
+ );
},
child: Container(
height: 48.h,
diff --git a/lib/localPreference/local_database.dart b/lib/localPreference/local_database.dart
index 764a184..4380d0f 100644
--- a/lib/localPreference/local_database.dart
+++ b/lib/localPreference/local_database.dart
@@ -67,10 +67,29 @@ class LocalDatabase {
full_name TEXT NOT NULL,
email_address TEXT NOT NULL,
role TEXT NOT NULL,
- role_id INTEGER NOT NULL
+ role_id INTEGER NOT NULL,
+ profile_image TEXT
)
''');
+ /// PASS CART TABLE
+ await db.execute('''
+ CREATE TABLE pass_cart (
+ id INTEGER PRIMARY KEY,
+ city_name TEXT NOT NULL,
+ hero_image TEXT NOT NULL,
+ card_type_name TEXT NOT NULL,
+ card_display_name TEXT NOT NULL,
+ theme_color INTEGER NOT NULL,
+ adult_count INTEGER NOT NULL,
+ child_count INTEGER NOT NULL,
+ adult_price REAL NOT NULL,
+ child_price REAL NOT NULL,
+ validity_duration INTEGER NOT NULL,
+ total_price REAL NOT NULL,
+ description TEXT
+ )
+''');
},
);
diff --git a/lib/localPreference/local_preference.dart b/lib/localPreference/local_preference.dart
index 0414915..2e20eba 100644
--- a/lib/localPreference/local_preference.dart
+++ b/lib/localPreference/local_preference.dart
@@ -1,4 +1,5 @@
import 'package:sqflite/sqflite.dart';
+import 'package:flutter/foundation.dart';
import 'local_database.dart';
class LocalPreference {
@@ -121,6 +122,18 @@ class LocalPreference {
return false;
}
+ static Future clearLogin() async {
+ final db = await LocalDatabase().database;
+
+ await db.update(
+ 'login_state',
+ {'is_logged_in': 0},
+ where: 'id = ?',
+ whereArgs: [1],
+ );
+ }
+
+
/// Set user tokens
static Future setTokens({
required String accessToken,
@@ -205,6 +218,7 @@ class LocalPreference {
required String emailAddress,
required String role,
required int roleId,
+ String? profileImage, // Added optional profileImage parameter
}) async {
final db = await LocalDatabase().database;
@@ -219,6 +233,7 @@ class LocalPreference {
'email_address': emailAddress,
'role': role,
'role_id': roleId,
+ 'profile_image': profileImage, // Include profile image
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
@@ -240,5 +255,218 @@ class LocalPreference {
return null;
}
+ /// Set profile image with error handling
+ static Future setProfileImage(String imageUrl) async {
+ try {
+ final db = await LocalDatabase().database;
+
+ final result = await db.update(
+ 'user_details',
+ {'profile_image': imageUrl},
+ where: 'id = ?',
+ whereArgs: [1],
+ );
+
+ if (kDebugMode) {
+ print('â
[LOCAL_PREF] Profile image saved: $imageUrl');
+ print('đ [LOCAL_PREF] Rows affected: $result');
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [LOCAL_PREF] Error saving profile image: $e');
+ }
+ rethrow;
+ }
+ }
+
+ /// Get profile image
+ static Future getProfileImage() async {
+ try {
+ final db = await LocalDatabase().database;
+
+ final result = await db.query(
+ 'user_details',
+ columns: ['profile_image'],
+ where: 'id = ?',
+ whereArgs: [1],
+ );
+
+ if (result.isNotEmpty) {
+ final imageUrl = result.first['profile_image'] as String?;
+ if (kDebugMode && imageUrl != null) {
+ print('â
[LOCAL_PREF] Retrieved profile image: $imageUrl');
+ }
+ return imageUrl;
+ }
+ return null;
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [LOCAL_PREF] Error getting profile image: $e');
+ }
+ return null;
+ }
+ }
+
+ /// Set pass cart data
+ static Future setPassCart({
+ required String cityName,
+ required String heroImage,
+ required String cardTypeName,
+ required String cardDisplayName,
+ required int themeColor,
+ required int adultCount,
+ required int childCount,
+ required double adultPrice,
+ required double childPrice,
+ required int validityDuration,
+ required double totalPrice,
+ String? description,
+ }) async {
+ final db = await LocalDatabase().database;
+
+ await db.insert(
+ 'pass_cart',
+ {
+ 'id': 1,
+ 'city_name': cityName,
+ 'hero_image': heroImage,
+ 'card_type_name': cardTypeName,
+ 'card_display_name': cardDisplayName,
+ 'theme_color': themeColor,
+ 'adult_count': adultCount,
+ 'child_count': childCount,
+ 'adult_price': adultPrice,
+ 'child_price': childPrice,
+ 'validity_duration': validityDuration,
+ 'total_price': totalPrice,
+ 'description': description,
+ },
+ conflictAlgorithm: ConflictAlgorithm.replace,
+ );
+
+ if (kDebugMode) {
+ print('â
[LOCAL_PREF] Pass cart saved: $cardDisplayName for $cityName');
+ }
+ }
+
+ /// Get pass cart data
+ static Future?> getPassCart() async {
+ try {
+ final db = await LocalDatabase().database;
+
+ final result = await db.query(
+ 'pass_cart',
+ where: 'id = ?',
+ whereArgs: [1],
+ );
+
+ if (result.isNotEmpty) {
+ if (kDebugMode) {
+ print('â
[LOCAL_PREF] Retrieved pass cart data');
+ }
+ return result.first;
+ }
+ return null;
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [LOCAL_PREF] Error getting pass cart: $e');
+ }
+ return null;
+ }
+ }
+
+ static Future clearPassCart() async {
+ try {
+ final db = await LocalDatabase().database;
+
+ await db.delete(
+ 'pass_cart',
+ where: 'id = ?',
+ whereArgs: [1],
+ );
+
+ if (kDebugMode) {
+ print('â
[LOCAL_PREF] Pass cart cleared');
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [LOCAL_PREF] Error clearing pass cart: $e');
+ }
+ }
+ }
+
+ static Future clearUserDetails() async {
+ final db = await LocalDatabase().database;
+
+ await db.update(
+ 'user_details',
+ {
+ 'user_id': null,
+ 'first_name': '',
+ 'last_name': '',
+ 'full_name': '',
+ 'email_address': '',
+ 'role': '',
+ 'role_id': 0,
+ 'profile_image': null,
+ },
+ where: 'id = ?',
+ whereArgs: [1],
+ );
+ }
+
+
+ static Future clearProfileImage() async {
+ try {
+ final db = await LocalDatabase().database;
+
+ final result = await db.update(
+ 'user_details',
+ {'profile_image': null},
+ where: 'id = ?',
+ whereArgs: [1],
+ );
+
+ if (kDebugMode) {
+ print('đ§š [LOCAL_PREF] Profile image cleared');
+ print('đ [LOCAL_PREF] Rows affected: $result');
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [LOCAL_PREF] Error clearing profile image: $e');
+ }
+ rethrow;
+ }
+ }
+
+ static Future resetAppData() async {
+ await clearLogin();
+ await clearTokens();
+ await clearUserDetails();
+ await clearPassCart();// optional
+ await clearProfileImage();// optional
+ }
+
+ static Future clearAllData() async {
+ try {
+ final db = await LocalDatabase().database;
+
+ // Clear all tables
+ await db.delete('selected_city');
+ await db.delete('login_state');
+ await db.delete('user_tokens');
+ await db.delete('user_details');
+ await db.delete('pass_cart');
+
+ if (kDebugMode) {
+ print('đ§š [LOCAL_PREF] All local data cleared successfully');
+ }
+ } catch (e) {
+ if (kDebugMode) {
+ print('â [LOCAL_PREF] Error clearing all local data: $e');
+ }
+ rethrow;
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/login/bloc/verify/verify_bloc.dart b/lib/login/bloc/verify/verify_bloc.dart
index 07b3378..0142249 100644
--- a/lib/login/bloc/verify/verify_bloc.dart
+++ b/lib/login/bloc/verify/verify_bloc.dart
@@ -41,6 +41,7 @@ class VerifyOtpBloc extends Bloc {
role: userModel.user.role,
roleId: userModel.user.roleId,
);
+ await LocalPreference.setProfileImage(userModel.user.profileImage);
emit(VerifyOtpSuccess(response: userModel));
} catch (e) {
emit(VerifyOtpError(errorMessage: e.toString()));
diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart
index 595b01d..3c8146d 100644
--- a/lib/login/view/verify_otp_bottomsheet.dart
+++ b/lib/login/view/verify_otp_bottomsheet.dart
@@ -1,5 +1,6 @@
import 'package:citycards_customer/common_packages/custom_filled_button.dart';
import 'package:citycards_customer/common_packages/custom_text.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';
import 'package:flutter/material.dart';
@@ -8,6 +9,7 @@ import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart';
+import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../bloc/verify/verify_bloc.dart';
import '../bloc/verify/verify_event.dart';
import '../bloc/verify/verify_state.dart';
@@ -39,6 +41,11 @@ class _VerifyOtpBottomsheetState extends State {
final userId = await LocalPreference.getUserId();
context.read().add(FetchProfileEvent(userId: userId!));
context.read().add(CheckLoginStatusEvent());
+ context.read().add(CheckLoginStatus());
+ context.read().add(FetchDraftPostCards());
+ context.read().add(RefreshDraftPostCards());
+ context.read().add(RefreshOrderPostCards());
+ context.read().add(FetchOrderPostCards());
// User exists - navigate to home/dashboard
// Navigator.of(context).pushReplacementNamed(RouteConstants.home);
ScaffoldMessenger.of(context).showSnackBar(
diff --git a/lib/main.dart b/lib/main.dart
index 529fe33..0422f2e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -17,6 +17,8 @@ import 'home/repository/home_repository.dart';
import 'login/bloc/login/login_bloc.dart';
import 'login/repository/login_repository.dart';
import 'my_pass/blocs/my_pass_bloc.dart';
+import 'postcard/blocs/myPostCards/my_postcard_bloc.dart';
+import 'postcard/repository/my_postcard_repository.dart';
import 'profile/bloc/profile/profile_bloc.dart';
import 'search_offers/repository/offers_repository.dart';
import 'search_offers/view/search_offers_with_listing.dart';
@@ -74,6 +76,11 @@ class MyApp extends StatelessWidget {
child: const OffersScreen(),
),
BlocProvider(create: (context) => ProfileBloc()),
+ BlocProvider(
+ create: (context) => MyPostCardBloc(
+ repository: MyPostCardsRepository(),
+ ),
+ ),
],
child: MaterialApp(
onGenerateRoute: _appRouter.onGenerateRoute,
diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart
index 07c1262..3e10e99 100644
--- a/lib/networkApiServices/api_urls.dart
+++ b/lib/networkApiServices/api_urls.dart
@@ -15,10 +15,13 @@ class ApiUrls {
static const offers = "$baseUrl/mobile/list/offers";
static const buyAPass = "$baseUrl/mobile/pass";
static const offersDetails = "$baseUrl/mobile/list/offers";
+ static const myPostCards = "$baseUrl/mobile/postcards/all";
//Post Apis
static const createAccount = "$baseUrl/mobile/user/register";
static const sendOtp = "$baseUrl/mobile/send-otp";
static const verifyOtp = "$baseUrl/mobile/user/verify-otp";
+ static const submitTicket = "$baseUrl/mobile/user/support";
+ static const createPostCard = "$baseUrl/mobile/postcards";
}
\ No newline at end of file
diff --git a/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart b/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart
new file mode 100644
index 0000000..fa9b4d5
--- /dev/null
+++ b/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart
@@ -0,0 +1,201 @@
+import 'package:citycards_customer/localPreference/local_preference.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'dart:developer' as developer;
+import '../../repository/my_postcard_repository.dart';
+import 'my_postcard_event.dart';
+import 'my_postcard_state.dart';
+
+class MyPostCardBloc extends Bloc {
+ final MyPostCardsRepository repository;
+
+ MyPostCardBloc({required this.repository}) : super(const MyPostCardInitial()) {
+ on(_onCheckLoginStatus);
+ on(_onFetchDraftPostCards);
+ on(_onFetchOrderPostCards);
+ on(_onRefreshDraftPostCards);
+ on(_onRefreshOrderPostCards);
+ }
+
+ /// Handle checking login status
+ Future _onCheckLoginStatus(
+ CheckLoginStatus event,
+ Emitter emit,
+ ) async {
+ developer.log('đ Checking login status...', name: 'MyPostCardBloc');
+ emit(const MyPostCardCheckingLogin());
+
+ try {
+ final isLogin = await LocalPreference.getLogin();
+ developer.log('đ Login status: $isLogin', name: 'MyPostCardBloc');
+
+ if (isLogin) {
+ developer.log('â
User is logged in - initializing state', name: 'MyPostCardBloc');
+ // User is logged in, initialize with empty lists and loading states
+ emit(const MyPostCardLoaded(
+ draftPostCards: [],
+ orderPostCards: [],
+ isDraftLoading: true,
+ isOrderLoading: true,
+ ));
+
+ // Fetch both drafts and orders
+ add(const FetchDraftPostCards());
+ add(const FetchOrderPostCards());
+ } else {
+ developer.log('â User is NOT logged in - emitting MyPostCardNotLoggedIn', name: 'MyPostCardBloc');
+ // User is not logged in
+ emit(const MyPostCardNotLoggedIn());
+ }
+ } catch (error) {
+ developer.log('â ī¸ Error checking login: $error', name: 'MyPostCardBloc');
+ // If there's an error checking login, treat as not logged in
+ emit(const MyPostCardNotLoggedIn());
+ }
+ }
+
+ /// Handle fetching draft postcards
+ Future _onFetchDraftPostCards(
+ FetchDraftPostCards event,
+ Emitter emit,
+ ) async {
+ developer.log('đĨ Fetching draft postcards...', name: 'MyPostCardBloc');
+ // Get current state
+ final currentState = state;
+
+ if (currentState is MyPostCardLoaded) {
+ // Set draft loading to true
+ emit(currentState.copyWith(isDraftLoading: true));
+ }
+
+ try {
+ final draftPostCards = await repository.fetchMyPostCards(type: 'draft');
+ developer.log('â
Draft postcards fetched: ${draftPostCards.length} items', name: 'MyPostCardBloc');
+
+ if (state is MyPostCardLoaded) {
+ // Update with fetched drafts
+ emit((state as MyPostCardLoaded).copyWith(
+ draftPostCards: draftPostCards,
+ isDraftLoading: false,
+ ));
+ } else {
+ developer.log('â ī¸ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc');
+ // Fallback: create new loaded state (shouldn't normally happen)
+ emit(MyPostCardLoaded(
+ draftPostCards: draftPostCards,
+ orderPostCards: const [],
+ isDraftLoading: false,
+ isOrderLoading: false,
+ ));
+ }
+ } catch (error) {
+ developer.log('â Error fetching drafts: $error', name: 'MyPostCardBloc');
+ // Keep current lists but stop loading
+ if (state is MyPostCardLoaded) {
+ emit((state as MyPostCardLoaded).copyWith(isDraftLoading: false));
+ }
+
+ // Emit error state
+ emit(MyPostCardError(
+ errorMessage: error.toString(),
+ errorType: 'draft',
+ ));
+ }
+ }
+
+ /// Handle fetching order postcards
+ Future _onFetchOrderPostCards(
+ FetchOrderPostCards event,
+ Emitter emit,
+ ) async {
+ developer.log('đĨ Fetching order postcards...', name: 'MyPostCardBloc');
+ // Get current state
+ final currentState = state;
+
+ if (currentState is MyPostCardLoaded) {
+ // Set order loading to true
+ emit(currentState.copyWith(isOrderLoading: true));
+ }
+
+ try {
+ final orderPostCards = await repository.fetchMyPostCards(type: 'orders');
+ developer.log('â
Order postcards fetched: ${orderPostCards.length} items', name: 'MyPostCardBloc');
+
+ if (state is MyPostCardLoaded) {
+ // Update with fetched orders
+ emit((state as MyPostCardLoaded).copyWith(
+ orderPostCards: orderPostCards,
+ isOrderLoading: false,
+ ));
+ } else {
+ developer.log('â ī¸ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc');
+ // Fallback: create new loaded state (shouldn't normally happen)
+ emit(MyPostCardLoaded(
+ draftPostCards: const [],
+ orderPostCards: orderPostCards,
+ isDraftLoading: false,
+ isOrderLoading: false,
+ ));
+ }
+ } catch (error) {
+ developer.log('â Error fetching orders: $error', name: 'MyPostCardBloc');
+ // Keep current lists but stop loading
+ if (state is MyPostCardLoaded) {
+ emit((state as MyPostCardLoaded).copyWith(isOrderLoading: false));
+ }
+
+ // Emit error state
+ emit(MyPostCardError(
+ errorMessage: error.toString(),
+ errorType: 'order',
+ ));
+ }
+ }
+
+ /// Handle refreshing draft postcards
+ Future _onRefreshDraftPostCards(
+ RefreshDraftPostCards event,
+ Emitter emit,
+ ) async {
+ developer.log('đ Refreshing draft postcards...', name: 'MyPostCardBloc');
+ try {
+ final draftPostCards = await repository.fetchMyPostCards(type: 'draft');
+ developer.log('â
Draft postcards refreshed: ${draftPostCards.length} items', name: 'MyPostCardBloc');
+
+ if (state is MyPostCardLoaded) {
+ emit((state as MyPostCardLoaded).copyWith(
+ draftPostCards: draftPostCards,
+ ));
+ }
+ } catch (error) {
+ developer.log('â Error refreshing drafts: $error', name: 'MyPostCardBloc');
+ emit(MyPostCardError(
+ errorMessage: error.toString(),
+ errorType: 'draft',
+ ));
+ }
+ }
+
+ /// Handle refreshing order postcards
+ Future _onRefreshOrderPostCards(
+ RefreshOrderPostCards event,
+ Emitter emit,
+ ) async {
+ developer.log('đ Refreshing order postcards...', name: 'MyPostCardBloc');
+ try {
+ final orderPostCards = await repository.fetchMyPostCards(type: 'orders');
+ developer.log('â
Order postcards refreshed: ${orderPostCards.length} items', name: 'MyPostCardBloc');
+
+ if (state is MyPostCardLoaded) {
+ emit((state as MyPostCardLoaded).copyWith(
+ orderPostCards: orderPostCards,
+ ));
+ }
+ } catch (error) {
+ developer.log('â Error refreshing orders: $error', name: 'MyPostCardBloc');
+ emit(MyPostCardError(
+ errorMessage: error.toString(),
+ errorType: 'order',
+ ));
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/blocs/myPostCards/my_postcard_event.dart b/lib/postcard/blocs/myPostCards/my_postcard_event.dart
new file mode 100644
index 0000000..7c25209
--- /dev/null
+++ b/lib/postcard/blocs/myPostCards/my_postcard_event.dart
@@ -0,0 +1,33 @@
+import 'package:equatable/equatable.dart';
+
+abstract class MyPostCardEvent extends Equatable {
+ const MyPostCardEvent();
+
+ @override
+ List get props => [];
+}
+
+/// Event to check login status
+class CheckLoginStatus extends MyPostCardEvent {
+ const CheckLoginStatus();
+}
+
+/// Event to fetch draft postcards
+class FetchDraftPostCards extends MyPostCardEvent {
+ const FetchDraftPostCards();
+}
+
+/// Event to fetch order postcards
+class FetchOrderPostCards extends MyPostCardEvent {
+ const FetchOrderPostCards();
+}
+
+/// Event to refresh draft postcards
+class RefreshDraftPostCards extends MyPostCardEvent {
+ const RefreshDraftPostCards();
+}
+
+/// Event to refresh order postcards
+class RefreshOrderPostCards extends MyPostCardEvent {
+ const RefreshOrderPostCards();
+}
\ No newline at end of file
diff --git a/lib/postcard/blocs/myPostCards/my_postcard_state.dart b/lib/postcard/blocs/myPostCards/my_postcard_state.dart
new file mode 100644
index 0000000..cef9dd7
--- /dev/null
+++ b/lib/postcard/blocs/myPostCards/my_postcard_state.dart
@@ -0,0 +1,76 @@
+import 'package:equatable/equatable.dart';
+import '../../models/my_postcard_model.dart';
+
+abstract class MyPostCardState extends Equatable {
+ const MyPostCardState();
+
+ @override
+ List get props => [];
+}
+
+/// Initial state
+class MyPostCardInitial extends MyPostCardState {
+ const MyPostCardInitial();
+}
+
+/// State to check login status
+class MyPostCardCheckingLogin extends MyPostCardState {
+ const MyPostCardCheckingLogin();
+}
+
+/// State when user is not logged in
+class MyPostCardNotLoggedIn extends MyPostCardState {
+ const MyPostCardNotLoggedIn();
+}
+
+/// Combined state that holds both drafts and orders
+class MyPostCardLoaded extends MyPostCardState {
+ final List draftPostCards;
+ final List orderPostCards;
+ final bool isDraftLoading;
+ final bool isOrderLoading;
+
+ const MyPostCardLoaded({
+ required this.draftPostCards,
+ required this.orderPostCards,
+ this.isDraftLoading = false,
+ this.isOrderLoading = false,
+ });
+
+ @override
+ List get props => [
+ draftPostCards,
+ orderPostCards,
+ isDraftLoading,
+ isOrderLoading,
+ ];
+
+ /// Helper method to create a copy with updated values
+ MyPostCardLoaded copyWith({
+ List? draftPostCards,
+ List? orderPostCards,
+ bool? isDraftLoading,
+ bool? isOrderLoading,
+ }) {
+ return MyPostCardLoaded(
+ draftPostCards: draftPostCards ?? this.draftPostCards,
+ orderPostCards: orderPostCards ?? this.orderPostCards,
+ isDraftLoading: isDraftLoading ?? this.isDraftLoading,
+ isOrderLoading: isOrderLoading ?? this.isOrderLoading,
+ );
+ }
+}
+
+/// Error state
+class MyPostCardError extends MyPostCardState {
+ final String errorMessage;
+ final String errorType; // 'draft' or 'order'
+
+ const MyPostCardError({
+ required this.errorMessage,
+ required this.errorType,
+ });
+
+ @override
+ List get props => [errorMessage, errorType];
+}
\ No newline at end of file
diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart
new file mode 100644
index 0000000..016b511
--- /dev/null
+++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart
@@ -0,0 +1,243 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import '../../repository/postcard_checkout_repository.dart';
+import 'postcard_checkout_event.dart';
+import 'postcard_checkout_state.dart';
+
+class PostcardCheckoutBloc
+ extends Bloc {
+ final CreatePostCardRepository repository;
+
+ PostcardCheckoutBloc({required this.repository})
+ : super(const PostcardCheckoutState()) {
+ on(_onUpdateAddress);
+ on(_onUpdateContent);
+ on(_onUpdateCheckoutData);
+ on(_onSaveAsDraft);
+ on(_onSubmitPostcard);
+ on(_onConfirmPayment); // đ NEW
+ }
+
+ void _onUpdateAddress(
+ UpdateAddressEvent event, Emitter emit) {
+ emit(state.copyWith(
+ countryName: event.countryName,
+ cityName: event.cityName,
+ stateName: event.stateName,
+ zipCode: event.zipCode,
+ address1: event.address1,
+ address2: event.address2,
+ ));
+ }
+
+ void _onUpdateContent(
+ UpdatePostcardContentEvent event, Emitter emit) {
+ emit(state.copyWith(
+ pcTitle: event.pcTitle,
+ pcContent: event.pcContent,
+ pcImageFile: event.pcImageFile,
+ ));
+ }
+
+ void _onUpdateCheckoutData(
+ UpdateCheckoutDataEvent event, Emitter emit) {
+ emit(state.copyWith(
+ 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: event.isForSelf,
+ baseAmount: event.baseAmount,
+ totalTaxAmount: event.totalTaxAmount,
+ totalAmount: event.totalAmount,
+ ));
+ }
+
+ Future _onSaveAsDraft(
+ SaveAsDraftEvent event, Emitter emit) async {
+ emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
+
+ try {
+ // Validate that image file exists before submitting
+ if (state.pcImageFile == null) {
+ emit(state.copyWith(
+ isLoading: false,
+ error: 'Please select a postcard image',
+ isSuccess: false,
+ ));
+ return;
+ }
+
+ final response = await repository.createPostCard(
+ countryName: state.countryName,
+ cityName: state.cityName,
+ stateName: state.stateName,
+ zipCode: state.zipCode,
+ address1: state.address1.isNotEmpty ? state.address1 : null,
+ address2: state.address2.isNotEmpty ? state.address2 : null,
+ pcTitle: state.pcTitle,
+ pcContent: state.pcContent,
+ pcImageFile: state.pcImageFile!,
+ pcNumber: state.pcNumber,
+ pcDatetime: state.pcDatetime,
+ fullname: state.fullname,
+ emailAddress: state.emailAddress,
+ mobileNumber: state.mobileNumber,
+ isdCode: state.isdCode,
+ isForSelf: state.isForSelf,
+ isDraft: true, // Save as draft
+ baseAmount: state.baseAmount,
+ totalTaxAmount: state.totalTaxAmount,
+ totalAmount: state.totalAmount,
+ );
+
+ // Extract order ID from response if available
+ final orderId = response['orderId']?.toString() ??
+ response['order_id']?.toString() ??
+ response['id']?.toString();
+
+ emit(state.copyWith(
+ isLoading: false,
+ isSuccess: true,
+ isDraft: true,
+ orderId: orderId,
+ ));
+ } catch (e) {
+ emit(state.copyWith(
+ isLoading: false,
+ error: e.toString(),
+ isSuccess: false,
+ ));
+ }
+ }
+
+ Future _onSubmitPostcard(
+ SubmitPostcardEvent event, Emitter emit) async {
+ emit(state.copyWith(isLoading: true, error: null, isSuccess: false));
+
+ try {
+ // Validate that image file exists before submitting
+ if (state.pcImageFile == null) {
+ emit(state.copyWith(
+ isLoading: false,
+ error: 'Please select a postcard image',
+ isSuccess: false,
+ ));
+ return;
+ }
+
+ final response = await repository.createPostCard(
+ countryName: state.countryName,
+ cityName: state.cityName,
+ stateName: state.stateName,
+ zipCode: state.zipCode,
+ address1: state.address1.isNotEmpty ? state.address1 : null,
+ address2: state.address2.isNotEmpty ? state.address2 : null,
+ pcTitle: state.pcTitle,
+ pcContent: state.pcContent,
+ pcImageFile: state.pcImageFile!,
+ pcNumber: state.pcNumber,
+ pcDatetime: state.pcDatetime,
+ fullname: state.fullname,
+ emailAddress: state.emailAddress,
+ mobileNumber: state.mobileNumber,
+ isdCode: state.isdCode,
+ isForSelf: state.isForSelf,
+ isDraft: false, // Final submission (payment)
+ baseAmount: state.baseAmount,
+ totalTaxAmount: state.totalTaxAmount,
+ totalAmount: state.totalAmount,
+ );
+
+ // đ Parse response from backend
+ // Expected: {"postcardId": 16, "clientSecret": "pi_3Sx0yjRtCkWyT4Em1MKw1FeU_secret_S8M74wnEhTRC9lUz9RqJnuuqg"}
+
+ 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() ??
+ response['order_id']?.toString() ??
+ response['id']?.toString();
+
+ // Validate clientSecret is present
+ if (clientSecret == null || clientSecret.isEmpty) {
+ emit(state.copyWith(
+ isLoading: false,
+ error: 'Payment initialization failed - no client secret received from server',
+ isSuccess: false,
+ ));
+ return;
+ }
+
+ // đ Emit success with clientSecret for payment processing
+ emit(state.copyWith(
+ isLoading: false,
+ isSuccess: true,
+ isDraft: false,
+ postcardId: postcardId,
+ clientSecret: clientSecret, // This will trigger payment flow
+ orderId: orderId,
+ ));
+ } catch (e) {
+ emit(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
+ Future _onConfirmPayment(
+ ConfirmPaymentEvent event, Emitter emit) async {
+
+ // Validate postcardId exists
+ if (state.postcardId == null) {
+ emit(state.copyWith(
+ confirmationError: 'Cannot confirm payment - postcard ID is missing',
+ isConfirmingPayment: false,
+ isPaymentConfirmed: false,
+ ));
+ return;
+ }
+
+ emit(state.copyWith(
+ isConfirmingPayment: true,
+ confirmationError: null,
+ isPaymentConfirmed: false,
+ ));
+
+ try {
+ final response = await repository.confirmPayment(
+ postcardId: state.postcardId!,
+ stripeStatus: event.stripeStatus,
+ paymentStatus: event.paymentStatus,
+ );
+
+ // Payment confirmation successful
+ emit(state.copyWith(
+ isConfirmingPayment: false,
+ isPaymentConfirmed: true,
+ confirmationError: null,
+ ));
+ } catch (e) {
+ emit(state.copyWith(
+ isConfirmingPayment: false,
+ isPaymentConfirmed: false,
+ confirmationError: e.toString(),
+ ));
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart
new file mode 100644
index 0000000..765e6a7
--- /dev/null
+++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart
@@ -0,0 +1,97 @@
+import 'dart:io';
+
+abstract class PostcardCheckoutEvent {}
+
+/// Page 1 â Address
+class UpdateAddressEvent extends PostcardCheckoutEvent {
+ final String countryName;
+ final String cityName;
+ final String stateName;
+ final String zipCode;
+ final String address1;
+ final String address2;
+
+ UpdateAddressEvent({
+ required this.countryName,
+ required this.cityName,
+ required this.stateName,
+ required this.zipCode,
+ required this.address1,
+ required this.address2,
+ });
+}
+
+/// Page 2 â Postcard content
+class UpdatePostcardContentEvent extends PostcardCheckoutEvent {
+ final String pcTitle;
+ final String pcContent;
+ final File? pcImageFile; // â CHANGED: File instead of String
+
+ UpdatePostcardContentEvent({
+ required this.pcTitle,
+ required this.pcContent,
+ this.pcImageFile, // â CHANGED: nullable File
+ });
+}
+
+/// Update all checkout data at once
+class UpdateCheckoutDataEvent extends PostcardCheckoutEvent {
+ 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; // â CHANGED: File instead of String
+ final String? pcNumber;
+ final String? pcDatetime;
+ final String? fullname;
+ final String? emailAddress;
+ final String? mobileNumber;
+ final String? isdCode;
+ final bool? isForSelf;
+ final double? baseAmount;
+ final double? totalTaxAmount;
+ final double? totalAmount;
+
+ UpdateCheckoutDataEvent({
+ this.countryName,
+ this.cityName,
+ this.stateName,
+ this.zipCode,
+ this.address1,
+ this.address2,
+ this.pcTitle,
+ this.pcContent,
+ this.pcImageFile, // â CHANGED
+ this.pcNumber,
+ this.pcDatetime,
+ this.fullname,
+ this.emailAddress,
+ this.mobileNumber,
+ this.isdCode,
+ this.isForSelf,
+ this.baseAmount,
+ this.totalTaxAmount,
+ this.totalAmount,
+ });
+}
+
+/// Save as draft
+class SaveAsDraftEvent extends PostcardCheckoutEvent {}
+
+/// Page 3 â Checkout & submit (Pay button)
+class SubmitPostcardEvent extends PostcardCheckoutEvent {}
+
+/// đ Confirm payment after successful Stripe payment
+class ConfirmPaymentEvent extends PostcardCheckoutEvent {
+ final String stripeStatus; // e.g., "succeeded", "requires_payment_method"
+ final String paymentStatus; // e.g., "success", "failed"
+
+ ConfirmPaymentEvent({
+ required this.stripeStatus,
+ required this.paymentStatus,
+ });
+}
\ No newline at end of file
diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart
new file mode 100644
index 0000000..ff165bb
--- /dev/null
+++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart
@@ -0,0 +1,136 @@
+import 'dart:io';
+
+class PostcardCheckoutState {
+ 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;
+
+ final bool isForSelf;
+ final bool isDraft;
+
+ final double baseAmount;
+ final double totalTaxAmount;
+ final double totalAmount;
+
+ final bool isLoading;
+ final String? error;
+ final bool isSuccess;
+ final String? orderId;
+ final String? clientSecret; // đ NEW: For Stripe payment
+ final int? postcardId; // đ NEW: Postcard ID from API
+
+ // đ Payment confirmation tracking
+ final bool isConfirmingPayment; // Loading state for payment confirmation
+ final bool isPaymentConfirmed; // Whether payment was confirmed successfully
+ final String? confirmationError; // Error during payment confirmation
+
+ const PostcardCheckoutState({
+ this.countryName = '',
+ this.cityName = '',
+ this.stateName = '',
+ this.zipCode = '',
+ this.address1 = '',
+ this.address2 = '',
+ this.pcTitle = '',
+ this.pcContent = '',
+ this.pcImageFile,
+ this.pcNumber = '',
+ this.pcDatetime = '',
+ this.fullname = '',
+ this.emailAddress = '',
+ this.mobileNumber = '',
+ this.isdCode = '',
+ this.isForSelf = true,
+ this.isDraft = true,
+ this.baseAmount = 0,
+ this.totalTaxAmount = 0,
+ this.totalAmount = 0,
+ this.isLoading = false,
+ this.error,
+ this.isSuccess = false,
+ this.orderId,
+ this.clientSecret, // đ NEW
+ this.postcardId, // đ NEW
+ this.isConfirmingPayment = false, // đ NEW
+ this.isPaymentConfirmed = false, // đ NEW
+ this.confirmationError, // đ NEW
+ });
+
+ PostcardCheckoutState copyWith({
+ String? countryName,
+ String? cityName,
+ String? stateName,
+ String? zipCode,
+ String? address1,
+ String? address2,
+ String? pcTitle,
+ String? pcContent,
+ File? pcImageFile,
+ String? pcNumber,
+ String? pcDatetime,
+ String? fullname,
+ String? emailAddress,
+ String? mobileNumber,
+ String? isdCode,
+ bool? isForSelf,
+ bool? isDraft,
+ double? baseAmount,
+ double? totalTaxAmount,
+ double? totalAmount,
+ bool? isLoading,
+ String? error,
+ bool? isSuccess,
+ String? orderId,
+ String? clientSecret, // đ NEW
+ int? postcardId, // đ NEW
+ bool? isConfirmingPayment, // đ NEW
+ bool? isPaymentConfirmed, // đ NEW
+ String? confirmationError, // đ NEW
+ }) {
+ return PostcardCheckoutState(
+ countryName: countryName ?? this.countryName,
+ cityName: cityName ?? this.cityName,
+ stateName: stateName ?? this.stateName,
+ zipCode: zipCode ?? this.zipCode,
+ address1: address1 ?? this.address1,
+ address2: address2 ?? this.address2,
+ pcTitle: pcTitle ?? this.pcTitle,
+ pcContent: pcContent ?? this.pcContent,
+ pcImageFile: pcImageFile ?? this.pcImageFile,
+ pcNumber: pcNumber ?? this.pcNumber,
+ pcDatetime: pcDatetime ?? this.pcDatetime,
+ fullname: fullname ?? this.fullname,
+ emailAddress: emailAddress ?? this.emailAddress,
+ mobileNumber: mobileNumber ?? this.mobileNumber,
+ isdCode: isdCode ?? this.isdCode,
+ isForSelf: isForSelf ?? this.isForSelf,
+ isDraft: isDraft ?? this.isDraft,
+ baseAmount: baseAmount ?? this.baseAmount,
+ totalTaxAmount: totalTaxAmount ?? this.totalTaxAmount,
+ totalAmount: totalAmount ?? this.totalAmount,
+ isLoading: isLoading ?? this.isLoading,
+ error: error,
+ isSuccess: isSuccess ?? this.isSuccess,
+ orderId: orderId ?? this.orderId,
+ clientSecret: clientSecret ?? this.clientSecret, // đ NEW
+ postcardId: postcardId ?? this.postcardId, // đ NEW
+ isConfirmingPayment: isConfirmingPayment ?? this.isConfirmingPayment, // đ NEW
+ isPaymentConfirmed: isPaymentConfirmed ?? this.isPaymentConfirmed, // đ NEW
+ confirmationError: confirmationError, // đ NEW
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart
index 749dcaf..ac1d4d8 100644
--- a/lib/postcard/blocs/postcard_creation_bloc.dart
+++ b/lib/postcard/blocs/postcard_creation_bloc.dart
@@ -72,6 +72,19 @@ class PostcardCreationBloc
}
});
+ on((event, emit) {
+ emit(state.copyWith(
+ pcTitle: event.pcTitle,
+ fullName: event.fullName,
+ emailId: event.emailId,
+ phoneNumber: event.phoneNumber,
+ city: event.city,
+ country: event.country,
+ state: event.state,
+ zipCode: event.zipCode,
+ ));
+ });
+
/* Select filter */
on((event, emit) async {
// 1ī¸âŖ No image? Exit early.
diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart
index 737885f..0439cab 100644
--- a/lib/postcard/blocs/postcard_creation_events.dart
+++ b/lib/postcard/blocs/postcard_creation_events.dart
@@ -36,4 +36,25 @@ class TogglePurchaseOption extends PostcardCreationEvent {
final bool isGift;
TogglePurchaseOption(this.isGift);
+}
+class UpdatePurchaseFormData extends PostcardCreationEvent {
+ final String? pcTitle;
+ final String? fullName;
+ final String? emailId;
+ final String? phoneNumber;
+ final String? city;
+ final String? country;
+ final String? state;
+ final String? zipCode;
+
+ UpdatePurchaseFormData({
+ this.pcTitle,
+ this.fullName,
+ this.emailId,
+ this.phoneNumber,
+ this.city,
+ this.country,
+ this.state,
+ this.zipCode,
+ });
}
\ No newline at end of file
diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart
index d8fec4a..44191e1 100644
--- a/lib/postcard/blocs/postcard_creation_state.dart
+++ b/lib/postcard/blocs/postcard_creation_state.dart
@@ -10,6 +10,16 @@ class PostcardCreationState {
final bool isProcessing;
final String? selectedFont;
+ // Add these new fields
+ final String? pcTitle;
+ final String? fullName;
+ final String? emailId;
+ final String? phoneNumber;
+ final String? city;
+ final String? country;
+ final String? state;
+ final String? zipCode;
+
const PostcardCreationState({
required this.currentStep,
this.imagePath,
@@ -18,7 +28,15 @@ class PostcardCreationState {
this.message,
this.isGift = false,
this.isProcessing = false,
- this.selectedFont
+ this.selectedFont,
+ this.pcTitle,
+ this.fullName,
+ this.emailId,
+ this.phoneNumber,
+ this.city,
+ this.country,
+ this.state,
+ this.zipCode,
});
PostcardCreationState copyWith({
@@ -30,6 +48,14 @@ class PostcardCreationState {
bool? isGift,
bool? isProcessing,
String? selectedFont,
+ String? pcTitle,
+ String? fullName,
+ String? emailId,
+ String? phoneNumber,
+ String? city,
+ String? country,
+ String? state,
+ String? zipCode,
}) {
return PostcardCreationState(
currentStep: currentStep ?? this.currentStep,
@@ -39,7 +65,15 @@ class PostcardCreationState {
message: message ?? this.message,
isGift: isGift ?? this.isGift,
isProcessing: isProcessing ?? this.isProcessing,
- selectedFont: selectedFont ?? this.selectedFont
+ selectedFont: selectedFont ?? this.selectedFont,
+ pcTitle: pcTitle ?? this.pcTitle,
+ fullName: fullName ?? this.fullName,
+ emailId: emailId ?? this.emailId,
+ phoneNumber: phoneNumber ?? this.phoneNumber,
+ city: city ?? this.city,
+ country: country ?? this.country,
+ state: state ?? this.state,
+ zipCode: zipCode ?? this.zipCode,
);
}
}
\ No newline at end of file
diff --git a/lib/postcard/models/my_postcard_model.dart b/lib/postcard/models/my_postcard_model.dart
new file mode 100644
index 0000000..1ff827e
--- /dev/null
+++ b/lib/postcard/models/my_postcard_model.dart
@@ -0,0 +1,173 @@
+class MyPostCard {
+ final int id;
+ final int userXid;
+ final String pcTitle;
+ final String pcNumber;
+ final String cityName;
+ final DateTime pcDatetime;
+ final String pcContent;
+ final String pcImagePath;
+ final bool isForSelf;
+ final String fullname;
+ final String emailAddress;
+ final String isdCode;
+ final String mobileNumber;
+ final String address1;
+ final String address2;
+ final String zipCode;
+ final String stateName;
+ final String countryName;
+ final String orderStatus;
+ final double baseAmount;
+ final int? couponXid;
+ final double? couponDiscountPercent;
+ final double? couponDiscountAmount;
+ final double totalTaxAmount;
+ final double totalAmount;
+ final bool isPaid;
+ final String paymentMode;
+ final String? paymentId;
+ final String paymentStatus;
+ final String? paymentIntentId;
+ final bool isDraft;
+ final DateTime? deliveredOn;
+ final bool isActive;
+ final DateTime createdAt;
+ final DateTime updatedAt;
+
+ MyPostCard({
+ required this.id,
+ required this.userXid,
+ required this.pcTitle,
+ required this.pcNumber,
+ required this.cityName,
+ required this.pcDatetime,
+ required this.pcContent,
+ required this.pcImagePath,
+ required this.isForSelf,
+ required this.fullname,
+ required this.emailAddress,
+ required this.isdCode,
+ required this.mobileNumber,
+ required this.address1,
+ required this.address2,
+ required this.zipCode,
+ required this.stateName,
+ required this.countryName,
+ required this.orderStatus,
+ required this.baseAmount,
+ this.couponXid,
+ this.couponDiscountPercent,
+ this.couponDiscountAmount,
+ required this.totalTaxAmount,
+ required this.totalAmount,
+ required this.isPaid,
+ required this.paymentMode,
+ this.paymentId,
+ required this.paymentStatus,
+ this.paymentIntentId,
+ required this.isDraft,
+ this.deliveredOn,
+ required this.isActive,
+ required this.createdAt,
+ required this.updatedAt,
+ });
+
+ factory MyPostCard.fromJson(Map json) {
+ return MyPostCard(
+ id: json['id'] ?? 0,
+ userXid: json['userXid'] ?? 0,
+ pcTitle: json['pcTitle'] ?? 'N/A',
+ pcNumber: json['pcNumber'] ?? 'N/A',
+ cityName: json['cityName'] ?? 'N/A',
+ pcDatetime: json['pcDatetime'] != null
+ ? DateTime.parse(json['pcDatetime'])
+ : DateTime.now(),
+ pcContent: json['pcContent'] ?? 'N/A',
+ pcImagePath: json['pcImagePath'] ?? '',
+ isForSelf: json['isForSelf'] ?? false,
+ fullname: json['fullname'] ?? 'N/A',
+ emailAddress: json['emailAddress'] ?? 'N/A',
+ isdCode: json['isdCode'] ?? '',
+ mobileNumber: json['mobileNumber'] ?? '',
+ address1: json['address1'] ?? 'N/A',
+ address2: json['address2'] ?? '',
+ zipCode: json['zipCode'] ?? '',
+ stateName: json['stateName'] ?? 'N/A',
+ countryName: json['countryName'] ?? 'N/A',
+ orderStatus: json['orderStatus'] ?? 'N/A',
+ baseAmount: json['baseAmount'] != null
+ ? (json['baseAmount'] as num).toDouble()
+ : 0.0,
+ couponXid: json['couponXid'],
+ couponDiscountPercent: json['couponDiscountPercent'] != null
+ ? (json['couponDiscountPercent'] as num).toDouble()
+ : null,
+ couponDiscountAmount: json['couponDiscountAmount'] != null
+ ? (json['couponDiscountAmount'] as num).toDouble()
+ : null,
+ totalTaxAmount: json['totalTaxAmount'] != null
+ ? (json['totalTaxAmount'] as num).toDouble()
+ : 0.0,
+ totalAmount: json['totalAmount'] != null
+ ? (json['totalAmount'] as num).toDouble()
+ : 0.0,
+ isPaid: json['isPaid'] ?? false,
+ paymentMode: json['paymentMode'] ?? 'N/A',
+ paymentId: json['paymentId'],
+ paymentStatus: json['paymentStatus'] ?? 'N/A',
+ paymentIntentId: json['paymentIntentId'],
+ isDraft: json['isDraft'] ?? false,
+ deliveredOn: json['deliveredOn'] != null
+ ? DateTime.parse(json['deliveredOn'])
+ : null,
+ isActive: json['isActive'] ?? false,
+ createdAt: json['createdAt'] != null
+ ? DateTime.parse(json['createdAt'])
+ : DateTime.now(),
+ updatedAt: json['updatedAt'] != null
+ ? DateTime.parse(json['updatedAt'])
+ : DateTime.now(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'userXid': userXid,
+ 'pcTitle': pcTitle,
+ 'pcNumber': pcNumber,
+ 'cityName': cityName,
+ 'pcDatetime': pcDatetime.toIso8601String(),
+ 'pcContent': pcContent,
+ 'pcImagePath': pcImagePath,
+ 'isForSelf': isForSelf,
+ 'fullname': fullname,
+ 'emailAddress': emailAddress,
+ 'isdCode': isdCode,
+ 'mobileNumber': mobileNumber,
+ 'address1': address1,
+ 'address2': address2,
+ 'zipCode': zipCode,
+ 'stateName': stateName,
+ 'countryName': countryName,
+ 'orderStatus': orderStatus,
+ 'baseAmount': baseAmount,
+ 'couponXid': couponXid,
+ 'couponDiscountPercent': couponDiscountPercent,
+ 'couponDiscountAmount': couponDiscountAmount,
+ 'totalTaxAmount': totalTaxAmount,
+ 'totalAmount': totalAmount,
+ 'isPaid': isPaid,
+ 'paymentMode': paymentMode,
+ 'paymentId': paymentId,
+ 'paymentStatus': paymentStatus,
+ 'paymentIntentId': paymentIntentId,
+ 'isDraft': isDraft,
+ 'deliveredOn': deliveredOn?.toIso8601String(),
+ 'isActive': isActive,
+ 'createdAt': createdAt.toIso8601String(),
+ 'updatedAt': updatedAt.toIso8601String(),
+ };
+ }
+}
diff --git a/lib/postcard/repository/my_postcard_repository.dart b/lib/postcard/repository/my_postcard_repository.dart
new file mode 100644
index 0000000..2a5932b
--- /dev/null
+++ b/lib/postcard/repository/my_postcard_repository.dart
@@ -0,0 +1,20 @@
+import '../../networkApiServices/network_api_services.dart';
+import '../../networkApiServices/api_urls.dart';
+import '../models/my_postcard_model.dart';
+
+class MyPostCardsRepository {
+ final NetworkApiService _apiService = NetworkApiService();
+
+ /// Fetch My Postcards (draft / orders)
+ Future> fetchMyPostCards({
+ required String type, // "draft" or "orders"
+ }) async {
+ final response = await _apiService.getApi(
+ url: '${ApiUrls.myPostCards}?type=$type',
+ );
+
+ return (response.data as List)
+ .map((e) => MyPostCard.fromJson(e))
+ .toList();
+ }
+}
diff --git a/lib/postcard/repository/postcard_checkout_repository.dart b/lib/postcard/repository/postcard_checkout_repository.dart
new file mode 100644
index 0000000..3d82050
--- /dev/null
+++ b/lib/postcard/repository/postcard_checkout_repository.dart
@@ -0,0 +1,205 @@
+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 CreatePostCardRepository {
+ final NetworkApiService _apiServices = NetworkApiService();
+
+ /// Create / Save Postcard (Draft or Final)
+ /// â UPDATED: Now uses multipart/form-data for file upload
+ Future> 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 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', isDraft.toString()),
+ MapEntry('baseAmount', baseAmount.toString()),
+ MapEntry('totalTaxAmount', totalTaxAmount.toString()),
+ MapEntry('totalAmount', totalAmount.toString()),
+ ]);
+
+ // 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;
+ } 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> 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;
+ } catch (e, stackTrace) {
+ log(
+ 'â confirmPayment FAILED',
+ error: e,
+ stackTrace: stackTrace,
+ );
+ throw Exception('Failed to confirm payment: $e');
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/views/my_orders_page_view.dart b/lib/postcard/views/my_orders_page_view.dart
deleted file mode 100644
index f21470f..0000000
--- a/lib/postcard/views/my_orders_page_view.dart
+++ /dev/null
@@ -1,817 +0,0 @@
-import 'dart:io';
-
-import 'package:citycards_customer/common_packages/app_bar.dart';
-import 'package:citycards_customer/postcard/blocs/postcard_creation_events.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 '../blocs/postcard_creation_bloc.dart';
-import '../blocs/postcard_creation_state.dart';
-
-class MyOrdersPageView extends StatefulWidget {
- const MyOrdersPageView({super.key});
-
- @override
- State createState() => _MyOrdersPageViewState();
-}
-
-class _MyOrdersPageViewState extends State {
- bool showDrafts = true;
-
- @override
- Widget build(BuildContext context) {
- return BlocBuilder(
- builder: (context, state) {
- final bloc = context.read();
- return SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // đī¸ Header
- CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
-
- Row(
- children: [
- Expanded(
- child: GestureDetector(
- onTap: () => setState(() => showDrafts = true),
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 12),
- decoration: BoxDecoration(
- color: showDrafts
- ? const Color(0xffF95F62).withOpacity(0.24)
- : Colors.transparent,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: showDrafts
- ? const Color(0xffF95F62).withOpacity(0.4)
- : const Color(0xffE0E0E0),
- ),
- ),
- child: Center(
- child: Text(
- "My drafts",
- style: TextStyle(
- fontWeight: FontWeight.w400,
- fontSize: 14.sp,
- color: showDrafts
- ? Colors.black
- : Colors.black.withOpacity(0.56),
- ),
- ),
- ),
- ),
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: GestureDetector(
- onTap: () => setState(() => showDrafts = false),
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 12),
- decoration: BoxDecoration(
- color: !showDrafts
- ? const Color(0xffF95F62).withOpacity(0.24)
- : Colors.transparent,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: !showDrafts
- ? const Color(0xffF95F62).withOpacity(0.4)
- : const Color(0xffE0E0E0),
- ),
- ),
- child: Center(
- child: Text(
- "My orders",
- style: TextStyle(
- fontWeight: FontWeight.w400,
- fontSize: 14.sp,
- color: !showDrafts
- ? Colors.black
- : Colors.black.withOpacity(0.56),
- ),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 24),
-
- // đŦ Postcard List
- showDrafts
- ? Expanded(
- child: ListView.builder(
- itemCount: 5,
- itemBuilder: (context, index) {
- return Container(
- margin: const EdgeInsets.only(bottom: 16),
- padding: const EdgeInsets.fromLTRB(
- 10,
- 10,
- 10,
- 10,
- ),
- decoration: BoxDecoration(
- color: const Color(0xFFFFF5F5),
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: const Color(0xffF1F5F7),
- ),
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: Image.file(
- File(state.imagePath ?? ""),
- height: 90.h,
- width: 90.w,
- fit: BoxFit.cover,
- ),
- ),
- const SizedBox(width: 20),
-
- Expanded(
- child: SizedBox(
- height: 90.h,
- child: Stack(
- children: [
- /// Centered texts
- Align(
- alignment: Alignment.centerLeft,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- Text(
- "#688574",
- style: GoogleFonts.poppins(
- fontSize: 14.sp,
- fontWeight: FontWeight.w400,
- color: Colors.black,
- ),
- ),
- const SizedBox(height: 3),
- Text(
- "My postcard",
- style: GoogleFonts.poppins(
- fontSize: 16.sp,
- fontWeight: FontWeight.w400,
- color: Colors.black,
- ),
- ),
- ],
- ),
- ),
-
- /// đ§ Bottom-right icons
- Align(
- alignment: Alignment.bottomRight,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- InkWell(
- onTap: () {},
- child: Image.asset(
- "assets/icons/delete_icon.png",
- scale: 4,
- ),
- ),
- const SizedBox(width: 20),
- InkWell(
- onTap: () {},
- child: Image.asset(
- "assets/icons/edit_icon.png",
- scale: 4,
- ),
- ),
- const SizedBox(width: 20),
- InkWell(
- onTap: () {},
- child: Image.asset(
- "assets/icons/send_icon.png",
- scale: 4,
- ),
- ),
- const SizedBox(width: 10),
- ],
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- );
- },
- ),
- )
- : Expanded(
- child: ListView.builder(
- itemCount: 2,
- itemBuilder: (context, index) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "#688574",
- style: GoogleFonts.poppins(
- fontSize: 14.sp,
- fontWeight: FontWeight.w400,
- color: Colors.black,
- ),
- ),
- const SizedBox(height: 3),
- Container(
- margin: const EdgeInsets.only(bottom: 16),
- padding: const EdgeInsets.fromLTRB(
- 10,
- 10,
- 10,
- 10,
- ),
- decoration: BoxDecoration(
- color: const Color(0xFFFFF5F5),
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: const Color(0xffF1F5F7),
- ),
- ),
- child: Row(
- crossAxisAlignment:
- CrossAxisAlignment.center,
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: Image.file(
- File(state.imagePath ?? ""),
- height: 70.h,
- width: 70.w,
- fit: BoxFit.cover,
- ),
- ),
- const SizedBox(width: 20),
-
- Expanded(
- child: SizedBox(
- height: 60.h,
- child: Row(
- mainAxisAlignment:
- MainAxisAlignment.spaceBetween,
- children: [
- Column(
- crossAxisAlignment:
- CrossAxisAlignment.start,
- mainAxisAlignment:
- MainAxisAlignment.start,
- children: [
- Text(
- "My PostCard",
- style: GoogleFonts.poppins(
- color: Colors.black,
- fontWeight:
- FontWeight.w400,
- fontSize: 16.sp,
- ),
- ),
- const SizedBox(height: 6),
- Text(
- "5 Post cards",
- style: GoogleFonts.poppins(
- color: Colors.black,
- fontWeight:
- FontWeight.w400,
- fontSize: 14.sp,
- ),
- ),
- ],
- ),
- Column(
- mainAxisAlignment:
- MainAxisAlignment
- .spaceBetween,
- crossAxisAlignment:
- CrossAxisAlignment.end,
- children: [
- Container(
- padding: EdgeInsets.fromLTRB(13, 7, 13, 7),
- decoration: BoxDecoration(
- color: Color(
- 0xff00FFA6,
- ).withOpacity(0.16),
- border: Border.all(
- color: Color(
- 0xff439F6E,
- ),
- ),
- borderRadius:
- BorderRadius.circular(
- 16,
- ),
- ),
- child: Text(
- "In Progress",
- style: TextStyle(
- color: Colors.black,
- fontWeight:
- FontWeight.w400,
- fontSize: 8.54.sp,
- ),
- ),
- ),
- InkWell(
- onTap: () {
- bloc.add(GoToNextStep());
- },
- child: Row(
- children: [
- Icon(
- Icons
- .remove_red_eye_outlined,
- size: 15,
- color: Color(
- 0xffF95F62,
- ),
- ),
- SizedBox(width: 5.w),
- Text(
- "Preview",
- style: TextStyle(
- fontWeight:
- FontWeight.w400,
- color: Color(
- 0xffF95F62,
- ),
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- ],
- );
- },
- ),
- ),
-
- // â Create postcard button
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: () {},
- style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xffF95F62),
- padding: EdgeInsets.symmetric(vertical: 16.h),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(40),
- ),
- ),
- child: Text(
- "Create post card",
- style: GoogleFonts.poppins(
- color: Colors.white,
- fontSize: 14.sp,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- );
- },
- );
- }
-}
-
-// import 'package:citycards_customer/common_packages/app_bar.dart';
-// import 'package:citycards_customer/core/route_constants.dart';
-// import 'package:flutter/material.dart';
-// import 'package:flutter_screenutil/flutter_screenutil.dart';
-// import 'package:google_fonts/google_fonts.dart';
-//
-// class MyOrdersPageView extends StatefulWidget {
-// const MyOrdersPageView({super.key});
-//
-// @override
-// State createState() => _MyOrdersPageViewState();
-// }
-//
-// class _MyOrdersPageViewState extends State {
-// bool showDrafts = true;
-//
-// @override
-// Widget build(BuildContext context) {
-// return SafeArea(
-// child: Padding(
-// padding: const EdgeInsets.all(16),
-// child: Column(
-// crossAxisAlignment: CrossAxisAlignment.start,
-// children: [
-// // đī¸ Header
-// CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
-//
-// Row(
-// children: [
-// Expanded(
-// child: GestureDetector(
-// onTap: () => setState(() => showDrafts = true),
-// child: Container(
-// padding: const EdgeInsets.symmetric(vertical: 12),
-// decoration: BoxDecoration(
-// color: showDrafts
-// ? const Color(0xffF95F62).withOpacity(0.24)
-// : Colors.transparent,
-// borderRadius: BorderRadius.circular(12),
-// border: Border.all(
-// color: showDrafts
-// ? const Color(0xffF95F62).withOpacity(0.4)
-// : const Color(0xffE0E0E0),
-// ),
-// ),
-// child: Center(
-// child: Text(
-// "My drafts",
-// style: TextStyle(
-// fontWeight: FontWeight.w400,
-// fontSize: 14.sp,
-// color: showDrafts
-// ? Colors.black
-// : Colors.black.withOpacity(0.56),
-// ),
-// ),
-// ),
-// ),
-// ),
-// ),
-// const SizedBox(width: 12),
-// Expanded(
-// child: GestureDetector(
-// onTap: () => setState(() => showDrafts = false),
-// child: Container(
-// padding: const EdgeInsets.symmetric(vertical: 12),
-// decoration: BoxDecoration(
-// color: !showDrafts
-// ? const Color(0xffF95F62).withOpacity(0.24)
-// : Colors.transparent,
-// borderRadius: BorderRadius.circular(12),
-// border: Border.all(
-// color: !showDrafts
-// ? const Color(0xffF95F62).withOpacity(0.4)
-// : const Color(0xffE0E0E0),
-// ),
-// ),
-// child: Center(
-// child: Text(
-// "My orders",
-// style: TextStyle(
-// fontWeight: FontWeight.w400,
-// fontSize: 14.sp,
-// color: !showDrafts
-// ? Colors.black
-// : Colors.black.withOpacity(0.56),
-// ),
-// ),
-// ),
-// ),
-// ),
-// ),
-// ],
-// ),
-// const SizedBox(height: 24),
-//
-// // đŦ Postcard List
-// showDrafts
-// ? Expanded(
-// child: ListView.builder(
-// itemCount: 5,
-// itemBuilder: (context, index) {
-// return Container(
-// margin: const EdgeInsets.only(bottom: 16),
-// padding: const EdgeInsets.fromLTRB(
-// 10,
-// 10,
-// 10,
-// 10,
-// ),
-// decoration: BoxDecoration(
-// color: const Color(0xFFFFF5F5),
-// borderRadius: BorderRadius.circular(12),
-// border: Border.all(
-// color: const Color(0xffF1F5F7),
-// ),
-// ),
-// child: Row(
-// crossAxisAlignment: CrossAxisAlignment.center,
-// children: [
-// ClipRRect(
-// borderRadius: BorderRadius.circular(8),
-// child: Container(
-// height: 90.h,
-// width: 90.w,
-// color: const Color(0xffFEE7E7),
-// child: const Icon(
-// Icons.image_outlined,
-// color: Color(0xffFDCDCE),
-// size: 40,
-// ),
-// ),
-// ),
-// const SizedBox(width: 20),
-//
-// Expanded(
-// child: SizedBox(
-// height: 90.h,
-// child: Stack(
-// children: [
-// /// Centered texts
-// Align(
-// alignment: Alignment.centerLeft,
-// child: Column(
-// mainAxisSize: MainAxisSize.min,
-// crossAxisAlignment:
-// CrossAxisAlignment.start,
-// children: [
-// Text(
-// "#688574",
-// style: GoogleFonts.poppins(
-// fontSize: 16.sp,
-// fontWeight: FontWeight.w400,
-// color: Colors.black,
-// ),
-// ),
-// const SizedBox(height: 4),
-// Text(
-// "Created 24 Jan 2025",
-// style: GoogleFonts.poppins(
-// fontSize: 12.sp,
-// fontWeight: FontWeight.w400,
-// color:
-// const Color(0xff999999),
-// ),
-// ),
-// ],
-// ),
-// ),
-//
-// /// Top-right buttons
-// Positioned(
-// top: 0,
-// right: 0,
-// child: Row(
-// mainAxisSize: MainAxisSize.min,
-// children: [
-// InkWell(
-// onTap: () {},
-// child: Image.asset(
-// "assets/icons/delete_icon.png",
-// scale: 3.5,
-// ),
-// ),
-// const SizedBox(width: 20),
-// InkWell(
-// onTap: () {},
-// child: Image.asset(
-// "assets/icons/edit_icon.png",
-// scale: 3.5,
-// ),
-// ),
-// ],
-// ),
-// ),
-//
-// /// Bottom-right "Preview" link
-// Positioned(
-// bottom: 0,
-// right: 0,
-// child: InkWell(
-// onTap: () {
-// // Navigate to preview
-// // You can use Navigator or your routing solution
-// },
-// child: Row(
-// children: [
-// const Icon(
-// Icons.remove_red_eye_outlined,
-// size: 15,
-// color: Color(0xffF95F62),
-// ),
-// SizedBox(width: 5.w),
-// Text(
-// "Preview",
-// style: TextStyle(
-// fontWeight: FontWeight.w400,
-// color:
-// const Color(0xffF95F62),
-// ),
-// ),
-// ],
-// ),
-// ),
-// ),
-// ],
-// ),
-// ),
-// ),
-// ],
-// ),
-// );
-// },
-// ),
-// )
-// : Expanded(
-// child: ListView.builder(
-// itemCount: 3,
-// itemBuilder: (context, index) {
-// return Container(
-// margin: const EdgeInsets.only(bottom: 16),
-// padding: const EdgeInsets.fromLTRB(
-// 16,
-// 16,
-// 16,
-// 16,
-// ),
-// decoration: BoxDecoration(
-// color: const Color(0xFFFFF5F5),
-// borderRadius: BorderRadius.circular(12),
-// border: Border.all(
-// color: const Color(0xffF1F5F7),
-// ),
-// ),
-// child: Row(
-// crossAxisAlignment:
-// CrossAxisAlignment.center,
-// children: [
-// ClipRRect(
-// borderRadius: BorderRadius.circular(8),
-// child: Container(
-// height: 70.h,
-// width: 70.w,
-// color: const Color(0xffFEE7E7),
-// child: const Icon(
-// Icons.image_outlined,
-// color: Color(0xffFDCDCE),
-// size: 30,
-// ),
-// ),
-// ),
-// const SizedBox(width: 20),
-//
-// Expanded(
-// child: SizedBox(
-// height: 60.h,
-// child: Row(
-// mainAxisAlignment:
-// MainAxisAlignment.spaceBetween,
-// children: [
-// Column(
-// crossAxisAlignment:
-// CrossAxisAlignment.start,
-// mainAxisAlignment:
-// MainAxisAlignment.start,
-// children: [
-// Text(
-// "My PostCard",
-// style: GoogleFonts.poppins(
-// color: Colors.black,
-// fontWeight:
-// FontWeight.w400,
-// fontSize: 16.sp,
-// ),
-// ),
-// const SizedBox(height: 6),
-// Text(
-// "5 Post cards",
-// style: GoogleFonts.poppins(
-// color: Colors.black,
-// fontWeight:
-// FontWeight.w400,
-// fontSize: 14.sp,
-// ),
-// ),
-// ],
-// ),
-// Column(
-// mainAxisAlignment:
-// MainAxisAlignment
-// .spaceBetween,
-// crossAxisAlignment:
-// CrossAxisAlignment.end,
-// children: [
-// Container(
-// padding: EdgeInsets.fromLTRB(13, 7, 13, 7),
-// decoration: BoxDecoration(
-// color: Color(
-// 0xff00FFA6,
-// ).withOpacity(0.16),
-// border: Border.all(
-// color: Color(
-// 0xff439F6E,
-// ),
-// ),
-// borderRadius:
-// BorderRadius.circular(
-// 16,
-// ),
-// ),
-// child: Text(
-// "In Progress",
-// style: TextStyle(
-// color: Colors.black,
-// fontWeight:
-// FontWeight.w400,
-// fontSize: 8.54.sp,
-// ),
-// ),
-// ),
-// InkWell(
-// onTap: () {
-// // Navigate to preview
-// // You can use Navigator or your routing solution
-// },
-// child: Row(
-// children: [
-// Icon(
-// Icons
-// .remove_red_eye_outlined,
-// size: 15,
-// color: Color(
-// 0xffF95F62,
-// ),
-// ),
-// SizedBox(width: 5.w),
-// Text(
-// "Preview",
-// style: TextStyle(
-// fontWeight:
-// FontWeight.w400,
-// color: Color(
-// 0xffF95F62,
-// ),
-// ),
-// ),
-// ],
-// ),
-// ),
-// ],
-// ),
-// ],
-// ),
-// ),
-// ),
-// ],
-// ),
-// );
-// },
-// ),
-// ),
-//
-// // â Create postcard button
-// SizedBox(
-// width: double.infinity,
-// child: ElevatedButton(
-// onPressed: () {
-// // Navigate to postcard creation flow (starts at upload photo step)
-// Navigator.of(context).pushNamed(RouteConstants.uploadPhotoPage);
-// },
-// style: ElevatedButton.styleFrom(
-// backgroundColor: const Color(0xffF95F62),
-// padding: EdgeInsets.symmetric(vertical: 16.h),
-// shape: RoundedRectangleBorder(
-// borderRadius: BorderRadius.circular(40),
-// ),
-// ),
-// child: Text(
-// "Create post card",
-// style: GoogleFonts.poppins(
-// color: Colors.white,
-// fontSize: 14.sp,
-// fontWeight: FontWeight.w600,
-// ),
-// ),
-// ),
-// ),
-// ],
-// ),
-// ),
-// );
-// }
-// }
diff --git a/lib/postcard/views/my_postcard_drafts_view.dart b/lib/postcard/views/my_postcard_drafts_view.dart
new file mode 100644
index 0000000..a9a5502
--- /dev/null
+++ b/lib/postcard/views/my_postcard_drafts_view.dart
@@ -0,0 +1,290 @@
+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 '../../core/route_constants.dart';
+import '../../networkApiServices/api_urls.dart';
+import '../blocs/myPostCards/my_postcard_bloc.dart';
+import '../blocs/myPostCards/my_postcard_event.dart';
+import '../blocs/myPostCards/my_postcard_state.dart';
+import '../models/my_postcard_model.dart';
+
+class MyPostCardDraftView extends StatelessWidget {
+ const MyPostCardDraftView({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (context, state) {
+ // Handle the new combined MyPostCardLoaded state
+ if (state is MyPostCardLoaded) {
+ // Show loading indicator if drafts are loading
+ if (state.isDraftLoading && state.draftPostCards.isEmpty) {
+ return const Center(
+ child: CircularProgressIndicator(
+ color: Color(0xffF95F62),
+ ),
+ );
+ }
+
+ // Show empty state if no drafts
+ if (state.draftPostCards.isEmpty) {
+ return Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // Empty state image
+ Image.asset(
+ "assets/images/empty_postcard_drafts.png",
+ width: 260.w,
+ fit: BoxFit.contain,
+ ),
+
+ SizedBox(height: 32.h),
+
+ // Title
+ Text(
+ "Looks like you haven't created\nany postcards yet!",
+ textAlign: TextAlign.center,
+ style: GoogleFonts.poppins(
+ fontSize: 18.sp,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xffF95F62),
+ height: 1.4,
+ ),
+ ),
+
+ SizedBox(height: 12.h),
+
+ // Subtitle
+ Text(
+ "Why not whip up a postcard and send it to someone special who's far away?",
+ textAlign: TextAlign.center,
+ style: GoogleFonts.poppins(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w400,
+ color: Colors.black54,
+ height: 1.5,
+ ),
+ ),
+
+ SizedBox(height: 32.h),
+ ],
+ ),
+ ),
+ );
+ }
+
+ // Show the list of drafts
+ return RefreshIndicator(
+ onRefresh: () async {
+ context.read().add(const RefreshDraftPostCards());
+ },
+ color: const Color(0xffF95F62),
+ child: ListView.builder(
+ physics: const AlwaysScrollableScrollPhysics(),
+ itemCount: state.draftPostCards.length,
+ itemBuilder: (context, index) {
+ final postcard = state.draftPostCards[index];
+ return _buildDraftCard(context, postcard);
+ },
+ ),
+ );
+ }
+
+ // Handle error state
+ if (state is MyPostCardError && state.errorType == 'draft') {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.error_outline,
+ size: 60,
+ color: Colors.red.withOpacity(0.6),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Error loading drafts',
+ style: GoogleFonts.poppins(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w500,
+ color: Colors.black87,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: Text(
+ state.errorMessage,
+ textAlign: TextAlign.center,
+ style: GoogleFonts.poppins(
+ fontSize: 14.sp,
+ color: Colors.black54,
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+ ElevatedButton(
+ onPressed: () {
+ context.read().add(const FetchDraftPostCards());
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ ),
+ child: Text(
+ 'Retry',
+ style: GoogleFonts.poppins(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return const SizedBox.shrink();
+ },
+ );
+ }
+
+ Widget _buildDraftCard(BuildContext context, MyPostCard postcard) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 16),
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: const Color(0xffF95F62).withValues(alpha: 0.08),
+ borderRadius: BorderRadius.circular(14),
+ border: Border.all(color: const Color(0xffF1F5F7)),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ /// LEFT IMAGE
+ ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ child: Image.network(
+ '${ApiUrls.baseUrl}${postcard.pcImagePath}',
+ height: 72,
+ width: 72,
+ fit: BoxFit.cover,
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) return child;
+ return Container(
+ height: 72,
+ width: 72,
+ color: Colors.grey[300],
+ child: const Center(
+ child: CircularProgressIndicator(
+ color: Color(0xffF95F62),
+ strokeWidth: 2,
+ ),
+ ),
+ );
+ },
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ height: 72,
+ width: 72,
+ color: Colors.grey[300],
+ child: const Icon(
+ Icons.image_not_supported,
+ color: Colors.grey,
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(width: 14),
+
+ /// RIGHT CONTENT
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ /// NUMBER
+ Text(
+ "#${postcard.pcNumber}",
+ style: GoogleFonts.poppins(
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ color: Colors.black,
+ ),
+ ),
+
+ const SizedBox(height: 4),
+
+ /// TITLE
+ Text(
+ postcard.pcTitle,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: GoogleFonts.poppins(
+ fontSize: 15,
+ fontWeight: FontWeight.w400,
+ color: Colors.black87,
+ ),
+ ),
+
+ const SizedBox(height: 10),
+
+ /// ICONS â BOTTOM RIGHT (UNDER TITLE)
+ Align(
+ alignment: Alignment.centerRight,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ GestureDetector(
+ onTap: () {
+ // delete
+ },
+ child: Image.asset(
+ 'assets/icons/delete_icon.png',
+ width: 20,
+ height: 20,
+ ),
+ ),
+ const SizedBox(width: 16),
+ GestureDetector(
+ onTap: () {
+ // edit
+ },
+ child: Image.asset(
+ 'assets/icons/edit_icon.png',
+ width: 20,
+ height: 20,
+ ),
+ ),
+ const SizedBox(width: 16),
+ GestureDetector(
+ onTap: () {
+ // send
+ },
+ child: Image.asset(
+ 'assets/icons/send_icon.png',
+ width: 20,
+ height: 20,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/views/my_postcard_orders_view.dart b/lib/postcard/views/my_postcard_orders_view.dart
new file mode 100644
index 0000000..74811ad
--- /dev/null
+++ b/lib/postcard/views/my_postcard_orders_view.dart
@@ -0,0 +1,385 @@
+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 '../../core/route_constants.dart';
+import '../blocs/myPostCards/my_postcard_bloc.dart';
+import '../blocs/myPostCards/my_postcard_event.dart';
+import '../blocs/myPostCards/my_postcard_state.dart';
+import '../models/my_postcard_model.dart';
+import '../../networkApiServices/api_urls.dart';
+import 'my_postcard_preview_view.dart';
+
+class MyPostCardOrdersView extends StatelessWidget {
+ const MyPostCardOrdersView({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (context, state) {
+ // Handle the new combined MyPostCardLoaded state
+ if (state is MyPostCardLoaded) {
+ // Show loading indicator if orders are loading
+ if (state.isOrderLoading && state.orderPostCards.isEmpty) {
+ return const Center(
+ child: CircularProgressIndicator(
+ color: Color(0xffF95F62),
+ ),
+ );
+ }
+
+ // Show empty state if no orders
+ if (state.orderPostCards.isEmpty) {
+ return Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ // Empty state image
+ Image.asset(
+ "assets/images/empty_postcard_orders.png",
+ width: 260.w,
+ fit: BoxFit.contain,
+ ),
+
+ SizedBox(height: 32.h),
+
+ // Title
+ Text(
+ "It looks like you haven't ordered\na postcards yet!",
+ textAlign: TextAlign.center,
+ style: GoogleFonts.poppins(
+ fontSize: 18.sp,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xffF95F62),
+ height: 1.4,
+ ),
+ ),
+
+ SizedBox(height: 12.h),
+
+ // Subtitle
+ Text(
+ "How about we whip up a fun postcard to send to your loved ones? Lets get started on that!",
+ textAlign: TextAlign.center,
+ style: GoogleFonts.poppins(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w400,
+ color: Colors.black54,
+ height: 1.5,
+ ),
+ ),
+
+ SizedBox(height: 32.h),
+ ],
+ ),
+ ),
+ );
+ }
+
+ // Show the list of orders
+ return RefreshIndicator(
+ onRefresh: () async {
+ context.read().add(const RefreshOrderPostCards());
+ },
+ color: const Color(0xffF95F62),
+ child: ListView.builder(
+ physics: const AlwaysScrollableScrollPhysics(),
+ itemCount: state.orderPostCards.length,
+ itemBuilder: (context, index) {
+ final postcard = state.orderPostCards[index];
+ return _buildOrderCard(context, postcard);
+ },
+ ),
+ );
+ }
+
+ // Handle error state
+ if (state is MyPostCardError && state.errorType == 'order') {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.error_outline,
+ size: 60,
+ color: Colors.red.withOpacity(0.6),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Error loading orders',
+ style: GoogleFonts.poppins(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w500,
+ color: Colors.black87,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: Text(
+ state.errorMessage,
+ textAlign: TextAlign.center,
+ style: GoogleFonts.poppins(
+ fontSize: 14.sp,
+ color: Colors.black54,
+ ),
+ ),
+ ),
+ const SizedBox(height: 24),
+ ElevatedButton(
+ onPressed: () {
+ context.read().add(const FetchOrderPostCards());
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ ),
+ child: Text(
+ 'Retry',
+ style: GoogleFonts.poppins(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return const SizedBox.shrink();
+ },
+ );
+ }
+
+ Widget _buildOrderCard(BuildContext context, MyPostCard postcard) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Postcard Number above the card
+ Padding(
+ padding: const EdgeInsets.only(left: 4, bottom: 8),
+ child: Text(
+ "#${postcard.pcNumber}",
+ style: GoogleFonts.poppins(
+ color: Colors.black,
+ fontWeight: FontWeight.w500,
+ fontSize: 15.sp,
+ ),
+ ),
+ ),
+
+ // Order Card
+ Container(
+ margin: const EdgeInsets.only(bottom: 16),
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: const Color(0xffF95F62).withValues(alpha:0.08),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: const Color(0xffF1F5F7),
+ ),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ // Postcard Image
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image(
+ image: NetworkImage('${ApiUrls.baseUrl}${postcard.pcImagePath}'),
+ height: 70.h,
+ width: 70.w,
+ fit: BoxFit.cover,
+
+ // Loading indicator
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) return child;
+ return Container(
+ height: 70.h,
+ width: 70.w,
+ color: Colors.grey[300],
+ child: const Center(
+ child: CircularProgressIndicator(
+ color: Color(0xffF95F62),
+ strokeWidth: 2,
+ ),
+ ),
+ );
+ },
+
+ // Error UI
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ height: 70.h,
+ width: 70.w,
+ color: Colors.grey[300],
+ child: const Icon(
+ Icons.image_not_supported,
+ color: Colors.grey,
+ ),
+ );
+ },
+ ),
+ ),
+ const SizedBox(width: 20),
+
+ // Postcard Details
+ Expanded(
+ child: SizedBox(
+ height: 60.h,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ Text(
+ postcard.pcTitle,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: GoogleFonts.poppins(
+ color: Colors.black,
+ fontWeight: FontWeight.w400,
+ fontSize: 16.sp,
+ ),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ "5 Post cards",
+ style: GoogleFonts.poppins(
+ color: Colors.black,
+ fontWeight: FontWeight.w400,
+ fontSize: 14.sp,
+ ),
+ ),
+ ],
+ ),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Container(
+ padding: const EdgeInsets.fromLTRB(13, 7, 13, 7),
+ decoration: BoxDecoration(
+ color: _getStatusColor(postcard.orderStatus).withOpacity(0.16),
+ border: Border.all(
+ color: _getStatusBorderColor(postcard.orderStatus),
+ ),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Text(
+ _getStatusText(postcard.orderStatus),
+ style: TextStyle(
+ color: Colors.black,
+ fontWeight: FontWeight.w400,
+ fontSize: 8.54.sp,
+ ),
+ ),
+ ),
+ InkWell(
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => MyPostcardPreviewView(
+ postcard: postcard,
+ ),
+ ),
+ );
+ },
+ child: Row(
+ children: [
+ Icon(
+ Icons.remove_red_eye_outlined,
+ size: 15,
+ color: const Color(0xffF95F62),
+ ),
+ SizedBox(width: 5.w),
+ Text(
+ "Preview",
+ style: TextStyle(
+ fontWeight: FontWeight.w400,
+ color: const Color(0xffF95F62),
+ fontSize: 13.sp,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Color _getStatusColor(String status) {
+ switch (status.toLowerCase()) {
+ case 'pending':
+ return const Color(0xffFFA500);
+ case 'processing':
+ case 'in progress':
+ return const Color(0xff00FFA6);
+ case 'shipped':
+ return const Color(0xff0096FF);
+ case 'delivered':
+ return const Color(0xff00C851);
+ case 'cancelled':
+ return const Color(0xffFF4444);
+ default:
+ return const Color(0xff00FFA6);
+ }
+ }
+
+ Color _getStatusBorderColor(String status) {
+ switch (status.toLowerCase()) {
+ case 'pending':
+ return const Color(0xffCC8400);
+ case 'processing':
+ case 'in progress':
+ return const Color(0xff439F6E);
+ case 'shipped':
+ return const Color(0xff0078CC);
+ case 'delivered':
+ return const Color(0xff00A041);
+ case 'cancelled':
+ return const Color(0xffCC0000);
+ default:
+ return const Color(0xff439F6E);
+ }
+ }
+
+ String _getStatusText(String status) {
+ switch (status.toLowerCase()) {
+ case 'pending':
+ return 'Pending';
+ case 'processing':
+ return 'Processing';
+ case 'in progress':
+ return 'In Progress';
+ case 'shipped':
+ return 'Shipped';
+ case 'delivered':
+ return 'Delivered';
+ case 'cancelled':
+ return 'Cancelled';
+ default:
+ return status;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/views/my_postcard_preview_view.dart b/lib/postcard/views/my_postcard_preview_view.dart
new file mode 100644
index 0000000..eedc6ca
--- /dev/null
+++ b/lib/postcard/views/my_postcard_preview_view.dart
@@ -0,0 +1,438 @@
+import 'package:flutter/material.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 '../models/my_postcard_model.dart';
+import '../../networkApiServices/api_urls.dart';
+
+class MyPostcardPreviewView extends StatefulWidget {
+ final MyPostCard postcard;
+
+ const MyPostcardPreviewView({
+ super.key,
+ required this.postcard,
+ });
+
+ @override
+ State createState() => _MyPostcardPreviewViewState();
+}
+
+class _MyPostcardPreviewViewState extends State {
+ bool showBack = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
+ ),
+ backWidget(context, "Preview", Colors.black),
+
+ SizedBox(height: 29.h),
+ // Postcard Number with Action Icons
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 20.w),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "#${widget.postcard.pcNumber}",
+ style: GoogleFonts.poppins(
+ color: Colors.black,
+ fontSize: 18.sp,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ Row(
+ children: [
+ GestureDetector(
+ onTap: () {
+ // Delete functionality
+ },
+ child: Image.asset(
+ 'assets/icons/delete_icon.png',
+ width: 24,
+ height: 24,
+ ),
+ ),
+ SizedBox(width: 16.w),
+ GestureDetector(
+ onTap: () {
+ // Edit functionality
+ },
+ child: Image.asset(
+ 'assets/icons/edit_icon.png',
+ width: 24,
+ height: 24,
+ ),
+ ),
+ SizedBox(width: 16.w),
+ GestureDetector(
+ onTap: () {
+ // Send functionality
+ },
+ child: Image.asset(
+ 'assets/icons/send_icon.png',
+ width: 24,
+ height: 24,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ SizedBox(height: 20.h),
+
+ // Flip buttons
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 20.w),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ GestureDetector(
+ onTap: () {
+ setState(() {
+ showBack = false;
+ });
+ },
+ child: Row(
+ children: [
+ Icon(
+ Icons.arrow_back,
+ color: !showBack ? const Color(0xffF95F62) : Colors.grey[400],
+ size: 20,
+ ),
+ SizedBox(width: 6.w),
+ Text(
+ 'Flip',
+ style: GoogleFonts.poppins(
+ color: !showBack ? const Color(0xffF95F62) : Colors.grey[400],
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ GestureDetector(
+ onTap: () {
+ setState(() {
+ showBack = true;
+ });
+ },
+ child: Row(
+ children: [
+ Text(
+ 'Flip',
+ style: GoogleFonts.poppins(
+ color: showBack ? const Color(0xffF95F62) : Colors.grey[400],
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ SizedBox(width: 6.w),
+ Icon(
+ Icons.arrow_forward,
+ color: showBack ? const Color(0xffF95F62) : Colors.grey[400],
+ size: 20,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ // Postcard Display
+ Expanded(
+ child: Center(
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 400),
+ transitionBuilder: (Widget child, Animation animation) {
+ return FadeTransition(
+ opacity: animation,
+ child: child,
+ );
+ },
+ child: showBack ? _buildBackSide() : _buildFrontSide(),
+ ),
+ ),
+ ),
+ SizedBox(height: 40.h),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFrontSide() {
+ return Container(
+ key: const ValueKey('front'),
+ margin: EdgeInsets.symmetric(horizontal: 20.w),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: AspectRatio(
+ aspectRatio: 1.5, // Standard postcard ratio
+ child: Image.network(
+ '${ApiUrls.baseUrl}${widget.postcard.pcImagePath}',
+ fit: BoxFit.cover,
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) return child;
+ return Container(
+ color: Colors.grey[300],
+ child: Center(
+ child: CircularProgressIndicator(
+ color: const Color(0xffF95F62),
+ value: loadingProgress.expectedTotalBytes != null
+ ? loadingProgress.cumulativeBytesLoaded /
+ loadingProgress.expectedTotalBytes!
+ : null,
+ ),
+ ),
+ );
+ },
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ color: Colors.grey[300],
+ child: const Center(
+ child: Icon(
+ Icons.image_not_supported,
+ size: 60,
+ color: Colors.grey,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildBackSide() {
+ return Container(
+ key: const ValueKey('back'),
+ margin: EdgeInsets.symmetric(horizontal: 20.w),
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [
+ Color(0xffE2D6C2),
+ Color(0xffFFF5E6),
+ Color(0xffFFF5E6),
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(
+ color: const Color(0xff000000).withOpacity(0.12),
+ width: 1,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: AspectRatio(
+ aspectRatio: 1.5,
+ child: Row(
+ children: [
+ // ================= LEFT SIDE =================
+ Expanded(
+ flex: 55,
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Logo
+ Image.asset(
+ 'assets/logo/logo_city_cards.png',
+ height: 24.h, // adjust as needed
+ fit: BoxFit.contain,
+ ),
+ SizedBox(height: 2.h),
+ Text(
+ 'POSTCARD',
+ style: TextStyle(
+ color: Colors.black45,
+ fontSize: 6.sp,
+ letterSpacing: 1.4,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+
+ SizedBox(height: 14.h),
+
+ // Message label
+ Text(
+ 'MESSAGE PREVIEW',
+ style: TextStyle(
+ color: Colors.black,
+ fontSize: 6.sp,
+ letterSpacing: 1.4,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ SizedBox(height: 8.h),
+
+ // Message text
+ Expanded(
+ child: SingleChildScrollView(
+ child: Text(
+ widget.postcard.pcContent,
+ style: TextStyle(
+ color: Colors.black87,
+ fontSize: 13.sp,
+ height: 1.45,
+ ),
+ ),
+ ),
+ ),
+
+ SizedBox(height: 10.h),
+
+ // Footer
+ Text(
+ 'CityCards.co',
+ style: TextStyle(
+ color: const Color(0xffF95F62),
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // ================= DIVIDER =================
+ Container(
+ width: 4,
+ margin: EdgeInsets.symmetric(vertical: 14.h),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Colors.black.withOpacity(0.05),
+ Colors.black.withOpacity(0.30),
+ Colors.black.withOpacity(0.05),
+ ],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ ),
+ ),
+ ),
+
+ // ================= RIGHT SIDE =================
+ Expanded(
+ flex: 45,
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h),
+ child: Column(
+ children: [
+ const Spacer(),
+
+ // Address with BORDER
+ Container(
+ width: double.infinity,
+ padding: EdgeInsets.all(4.w),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(
+ color: Colors.black.withOpacity(0.15),
+ width: 1,
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ // ADDRESS label
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ 'ADDRESS',
+ style: TextStyle(
+ color: Colors.black45,
+ fontSize: 7.5.sp,
+ letterSpacing: 1.6,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+
+ SizedBox(height: 6.h),
+
+ // Address line 1
+ Text(
+ '${widget.postcard.cityName},',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Colors.black87,
+ fontSize: 13.sp,
+ height: 1.5,
+ ),
+ ),
+
+ SizedBox(height: 6.h),
+
+ // State
+ Text(
+ '${widget.postcard.stateName},',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Colors.black87,
+ fontSize: 13.sp,
+ height: 1.5,
+ ),
+ ),
+ SizedBox(height: 6.h),
+ // Country
+ Text(
+ widget.postcard.countryName,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Colors.black87,
+ fontSize: 13.sp,
+ height: 1.5,
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const Spacer(),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/views/my_postcards_view.dart b/lib/postcard/views/my_postcards_view.dart
new file mode 100644
index 0000000..059ca67
--- /dev/null
+++ b/lib/postcard/views/my_postcards_view.dart
@@ -0,0 +1,491 @@
+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:citycards_customer/common_packages/app_bar.dart';
+import 'dart:developer' as developer;
+import '../../core/route_constants.dart';
+import '../../login/view/login_email_bottomsheet.dart';
+import '../blocs/myPostCards/my_postcard_bloc.dart';
+import '../blocs/myPostCards/my_postcard_event.dart';
+import '../blocs/myPostCards/my_postcard_state.dart';
+import 'my_postcard_drafts_view.dart';
+import 'my_postcard_orders_view.dart';
+
+class MyPostCardsView extends StatefulWidget {
+ const MyPostCardsView({super.key});
+
+ @override
+ State createState() => _MyPostCardsViewState();
+}
+
+class _MyPostCardsViewState extends State {
+ bool showDrafts = true;
+
+ @override
+ void initState() {
+ super.initState();
+ developer.log('đ MyPostCardsView initialized', name: 'MyPostCardsView');
+ context.read().add(const CheckLoginStatus());
+ }
+
+ void _switchTab(bool isDrafts) {
+ setState(() {
+ showDrafts = isDrafts;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: BlocBuilder(
+ builder: (context, state) {
+ developer.log('đ Current state: ${state.runtimeType}', name: 'MyPostCardsView');
+
+ // Handle not logged in state
+ if (state is MyPostCardNotLoggedIn) {
+ developer.log('â Showing login page - user not logged in', name: 'MyPostCardsView');
+ return _buildPleaseLoginPageUI();
+ }
+
+ // Handle checking login state
+ if (state is MyPostCardCheckingLogin) {
+ developer.log('đ Checking login...', name: 'MyPostCardsView');
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ // Handle loaded state
+ if (state is MyPostCardLoaded) {
+ final isDraftsEmpty = state.draftPostCards.isEmpty;
+ final isOrdersEmpty = state.orderPostCards.isEmpty;
+
+ developer.log('đ Loaded - Drafts: ${state.draftPostCards.length}, Orders: ${state.orderPostCards.length}', name: 'MyPostCardsView');
+ developer.log('đ Loading - Drafts: ${state.isDraftLoading}, Orders: ${state.isOrderLoading}', name: 'MyPostCardsView');
+
+ // Show initial UI only when both are empty AND not loading
+ final shouldShowInitialUI = isDraftsEmpty &&
+ isOrdersEmpty &&
+ !state.isDraftLoading &&
+ !state.isOrderLoading;
+
+ if (shouldShowInitialUI) {
+ developer.log('đ¨ Showing initial UI - both lists empty', name: 'MyPostCardsView');
+ return _buildInitialPageUI();
+ }
+
+ // Show loading state while initial data is being fetched
+ if (state.isDraftLoading && state.isOrderLoading &&
+ isDraftsEmpty && isOrdersEmpty) {
+ developer.log('âŗ Showing loading - fetching initial data', name: 'MyPostCardsView');
+ return Column(
+ children: [
+ const CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
+ ),
+ const Expanded(
+ child: Center(child: CircularProgressIndicator()),
+ ),
+ ],
+ );
+ }
+
+ developer.log('đą Showing main content UI', name: 'MyPostCardsView');
+ return _buildMainContentUI(state);
+ }
+
+ // Handle error state
+ if (state is MyPostCardError) {
+ developer.log('â Error state: ${state.errorMessage}', name: 'MyPostCardsView');
+ return _buildErrorUI(state.errorMessage);
+ }
+
+ // Default fallback
+ developer.log('â ī¸ Unknown state - showing loading', name: 'MyPostCardsView');
+ return const Center(child: CircularProgressIndicator());
+ },
+ ),
+ ),
+ );
+ }
+
+ Widget _buildMainContentUI(MyPostCardLoaded state) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
+ ),
+
+ // Tab Buttons
+ Row(
+ children: [
+ Expanded(
+ child: GestureDetector(
+ onTap: () => _switchTab(true),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ decoration: BoxDecoration(
+ color: showDrafts
+ ? const Color(0xffF95F62).withOpacity(0.24)
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: showDrafts
+ ? const Color(0xffF95F62).withOpacity(0.4)
+ : const Color(0xffE0E0E0),
+ ),
+ ),
+ child: Center(
+ child: Text(
+ "My drafts",
+ style: TextStyle(
+ fontWeight: FontWeight.w400,
+ fontSize: 14.sp,
+ color: showDrafts
+ ? Colors.black
+ : Colors.black.withOpacity(0.56),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: GestureDetector(
+ onTap: () => _switchTab(false),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ decoration: BoxDecoration(
+ color: !showDrafts
+ ? const Color(0xffF95F62).withOpacity(0.24)
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: !showDrafts
+ ? const Color(0xffF95F62).withOpacity(0.4)
+ : const Color(0xffE0E0E0),
+ ),
+ ),
+ child: Center(
+ child: Text(
+ "My orders",
+ style: TextStyle(
+ fontWeight: FontWeight.w400,
+ fontSize: 14.sp,
+ color: !showDrafts
+ ? Colors.black
+ : Colors.black.withOpacity(0.56),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 24),
+
+ // Content based on selected tab
+ Expanded(
+ child: showDrafts
+ ? (state.isDraftLoading && state.draftPostCards.isEmpty
+ ? const Center(child: CircularProgressIndicator())
+ : const MyPostCardDraftView())
+ : (state.isOrderLoading && state.orderPostCards.isEmpty
+ ? const Center(child: CircularProgressIndicator())
+ : const MyPostCardOrdersView()),
+ ),
+
+ // Create postcard button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () {
+ Navigator.of(context)
+ .pushNamed(RouteConstants.uploadPhotoPage);
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(vertical: 16.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ ),
+ child: Text(
+ "Create post card",
+ style: GoogleFonts.poppins(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ // Initial page UI when both lists are empty
+ Widget _buildInitialPageUI() {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
+ ),
+
+ Expanded(
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(height: 30.h),
+
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.asset(
+ "assets/images/post_card_intro.png",
+ width: double.infinity,
+ fit: BoxFit.cover,
+ ),
+ ),
+
+ SizedBox(height: 50.h),
+
+ const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text("đ´", style: TextStyle(fontSize: 16)),
+ SizedBox(width: 4),
+ Text("đŽ", style: TextStyle(fontSize: 16)),
+ SizedBox(width: 4),
+ Text("đ", style: TextStyle(fontSize: 16)),
+ ],
+ ),
+
+ SizedBox(height: 24.h),
+
+ Text(
+ "Make the most of your trip",
+ style: TextStyle(
+ fontSize: 20.sp,
+ fontWeight: FontWeight.w800,
+ color: const Color(0xffF95F62),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ SizedBox(height: 8.h),
+ Text(
+ "Design your own unique postcards to\ncherish your unforgettable moments.",
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w400,
+ color: const Color(0xff707070),
+ height: 1.5,
+ ),
+ textAlign: TextAlign.center,
+ ),
+
+ SizedBox(height: 36.h),
+ ],
+ ),
+ ),
+ ),
+
+ // Create postcard button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () {
+ Navigator.of(context).pushNamed(RouteConstants.uploadPhotoPage);
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(vertical: 16.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ ),
+ child: Text(
+ "Lets Create",
+ style: GoogleFonts.poppins(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ // Please login page UI when user is not logged in
+ Widget _buildPleaseLoginPageUI() {
+ developer.log('đ Building login page UI', name: 'MyPostCardsView');
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header
+ const CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
+ ),
+
+ Expanded(
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(height: 50.h),
+
+ // Postcard Image with opacity
+ Opacity(
+ opacity: 0.3,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.asset(
+ "assets/images/post_card_intro.png",
+ width: double.infinity,
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+
+ SizedBox(height: 60.h),
+
+ // Error Message
+ Text(
+ "You are not logged in yet!",
+ style: TextStyle(
+ fontSize: 20.sp,
+ fontWeight: FontWeight.w800,
+ color: const Color(0xffF95F62),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ SizedBox(height: 12.h),
+ Text(
+ "To design your own unique postcards, log\nin and purchase an unlimited pass",
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w400,
+ color: const Color(0xff707070),
+ height: 1.5,
+ ),
+ textAlign: TextAlign.center,
+ ),
+
+ SizedBox(height: 36.h),
+ ],
+ ),
+ ),
+ ),
+
+ // Login button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () {
+ showModalBottomSheet(
+ backgroundColor: Colors.white,
+ context: context,
+ isScrollControlled: true,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(12.r),
+ ),
+ ),
+ builder: (_) => const LoginEmailBottomsheet(),
+ );
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(vertical: 16.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ ),
+ child: Text(
+ "Login",
+ style: GoogleFonts.poppins(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ // Error UI
+ Widget _buildErrorUI(String errorMessage) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
+ ),
+ Expanded(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.error_outline,
+ size: 64,
+ color: Color(0xffF95F62),
+ ),
+ SizedBox(height: 16.h),
+ Text(
+ "Something went wrong",
+ style: TextStyle(
+ fontSize: 18.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ SizedBox(height: 8.h),
+ Text(
+ errorMessage,
+ style: TextStyle(
+ fontSize: 14.sp,
+ color: Colors.grey,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ SizedBox(height: 24.h),
+ ElevatedButton(
+ onPressed: () {
+ context.read().add(const CheckLoginStatus());
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ ),
+ child: const Text("Retry"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart
index c199a3e..04242fc 100644
--- a/lib/postcard/views/postcard_checkout_page_view.dart
+++ b/lib/postcard/views/postcard_checkout_page_view.dart
@@ -1,153 +1,547 @@
+import 'dart:io';
+import 'package:citycards_customer/postcard/views/my_postcards_view.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 '../../StripePayment/bloc/stripe_payment_bloc.dart';
+import '../../StripePayment/bloc/stripe_payment_event.dart';
+import '../../StripePayment/bloc/stripe_payment_state.dart';
+import '../../StripePayment/repository/stripe_service.dart';
import '../../common_packages/app_bar.dart';
+import '../../core/route_constants.dart';
+import '../blocs/myPostCards/my_postcard_bloc.dart';
+import '../blocs/myPostCards/my_postcard_event.dart';
+import '../blocs/postcardCheckout/postcard_checkout_bloc.dart';
+import '../blocs/postcardCheckout/postcard_checkout_event.dart';
+import '../blocs/postcardCheckout/postcard_checkout_state.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_events.dart';
import '../blocs/postcard_creation_state.dart';
import '../widgets/message_card_widget.dart';
import '../widgets/postcard_preview_widget.dart';
-class PostcardCheckoutPageView extends StatelessWidget {
- const PostcardCheckoutPageView({super.key});
+class PostcardCheckoutPageView extends StatefulWidget {
+ final String countryName;
+ final String cityName;
+ final String stateName;
+ final String zipCode;
+ final String address1;
+ final String address2;
+ final String pcTitle;
+ final String pcNumber;
+ final String pcDatetime;
+ final String fullname;
+ final String emailAddress;
+ final String mobileNumber;
+ final String isdCode;
+ final bool isForSelf;
+ final double baseAmount;
+ final double totalTaxAmount;
+ final double totalAmount;
+
+ const PostcardCheckoutPageView({
+ super.key,
+ required this.countryName,
+ required this.cityName,
+ required this.stateName,
+ required this.zipCode,
+ this.address1 = '',
+ this.address2 = '',
+ required this.pcTitle,
+ required this.pcNumber,
+ required this.pcDatetime,
+ required this.fullname,
+ required this.emailAddress,
+ required this.mobileNumber,
+ required this.isdCode,
+ required this.isForSelf,
+ required this.baseAmount,
+ required this.totalTaxAmount,
+ required this.totalAmount,
+ });
+
+ @override
+ State createState() => _PostcardCheckoutPageViewState();
+}
+
+class _PostcardCheckoutPageViewState extends State {
+ @override
+ void initState() {
+ super.initState();
+ // Initialize checkout bloc with data from widget
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ final creationState = context.read().state;
+
+ // â Convert image path to File object
+ File? imageFile;
+ if (creationState.imagePath != null && creationState.imagePath!.isNotEmpty) {
+ imageFile = File(creationState.imagePath!);
+ }
+
+ context.read().add(
+ UpdateCheckoutDataEvent(
+ countryName: widget.countryName,
+ cityName: widget.cityName,
+ stateName: widget.stateName,
+ zipCode: widget.zipCode,
+ address1: widget.address1,
+ address2: widget.address2,
+ pcTitle: widget.pcTitle,
+ pcContent: creationState.message ?? '',
+ pcImageFile: imageFile,
+ pcNumber: widget.pcNumber,
+ pcDatetime: widget.pcDatetime,
+ fullname: widget.fullname,
+ emailAddress: widget.emailAddress,
+ mobileNumber: widget.mobileNumber,
+ isdCode: widget.isdCode,
+ isForSelf: widget.isForSelf,
+ baseAmount: widget.baseAmount,
+ totalTaxAmount: widget.totalTaxAmount,
+ totalAmount: widget.totalAmount,
+ ),
+ );
+ });
+ }
+
+ /// đ Handle payment flow with client secret
+ Future _handlePaymentFlow(BuildContext context, String clientSecret) async {
+ // Show payment bottom sheet with BLoC
+ final paymentSuccess = await showModalBottomSheet(
+ context: context,
+ isDismissible: false,
+ enableDrag: false,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (bottomSheetContext) {
+ return BlocProvider(
+ create: (_) => StripePaymentBloc(stripeService: StripeService())
+ ..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)),
+ child: BlocConsumer(
+ listener: (context, state) {
+ if (state is StripePaymentSuccess) {
+ Navigator.of(bottomSheetContext).pop(true);
+ } else if (state is StripePaymentFailure || state is StripePaymentCancelled) {
+ Navigator.of(bottomSheetContext).pop(false);
+ }
+ },
+ builder: (context, state) {
+ return Container(
+ height: MediaQuery.of(context).size.height * 0.5,
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular(20),
+ topRight: Radius.circular(20),
+ ),
+ ),
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (state is StripePaymentLoading) ...[
+ const CircularProgressIndicator(
+ strokeWidth: 3,
+ valueColor: AlwaysStoppedAnimation(
+ Color(0xFFF95F62),
+ ),
+ ),
+ const SizedBox(height: 24),
+ const Text(
+ "Processing payment...",
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ] else if (state is StripePaymentSuccess) ...[
+ const Icon(
+ Icons.check_circle,
+ color: Colors.green,
+ size: 64,
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ "Payment Successful!",
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ] else if (state is StripePaymentFailure) ...[
+ const Icon(
+ Icons.error,
+ color: Colors.red,
+ size: 64,
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ "Payment Failed",
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF333333),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ state.error,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey[600],
+ ),
+ ),
+ ] else if (state is StripePaymentCancelled) ...[
+ const Icon(
+ Icons.cancel,
+ color: Colors.orange,
+ size: 64,
+ ),
+ const SizedBox(height: 16),
+ const Text(
+ "Payment Cancelled",
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ],
+ const SizedBox(height: 32),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 16,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF5F5F5),
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: const Color(0xFFE0E0E0),
+ ),
+ ),
+ child: Column(
+ children: [
+ Text(
+ "Payment Amount",
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey[600],
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ "\$${widget.totalAmount.toStringAsFixed(2)}",
+ style: const TextStyle(
+ fontSize: 32,
+ fontWeight: FontWeight.bold,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.lock_outline,
+ size: 16,
+ color: Colors.grey[600],
+ ),
+ const SizedBox(width: 6),
+ Text(
+ "Secured by Stripe",
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.grey[600],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ },
+ );
+
+ // Handle payment result
+ if (!mounted) return;
+
+ if (paymentSuccess == true) {
+ // Payment successful - continue to next step
+ context.read().add(GoToNextStep());
+ final bloc = context.read();
+ bloc.add(
+ ConfirmPaymentEvent(
+ stripeStatus: 'succeeded',
+ paymentStatus: 'success',
+ ),
+ );
+ } else {
+
+ // Payment failed or cancelled - go to MyPostCardsView
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const MyPostCardsView(),
+ ),
+ );
+
+ final bloc = context.read();
+ bloc.add(
+ ConfirmPaymentEvent(
+ stripeStatus: 'requires_payment_method',
+ paymentStatus: 'failed',
+ ),
+ );
+ }
+ }
@override
Widget build(BuildContext context) {
- return BlocBuilder(
- builder: (context, state) {
- final bloc = context.read();
- return SafeArea(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
- // Header
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- "Checkout",
- style: GoogleFonts.poppins(
- fontSize: 20.sp,
- fontWeight: FontWeight.w600,
- color: const Color(0xff1A1A1A),
- ),
- ),
- TextButton(
- onPressed: () {
- // TODO: Save as draft
- },
- child: Text(
- "Save as draft",
- style: GoogleFonts.poppins(
- fontSize: 14.sp,
- fontWeight: FontWeight.w500,
- color: const Color(0xffF95F62),
- ),
- ),
- ),
- ],
- ),
-
- const SizedBox(height: 16),
-
- MessageCardWidget(
- message: state.message ?? "",
- selectedFont: state.selectedFont,
- ),
- SizedBox(height: 10.h),
- PostCardPreviewWidget(
- imagePath: state.imagePath ?? "",
- message: state.message ?? "",
- selectedFont: state.selectedFont,
- ),
-
- SizedBox(height: 60.h),
-
- // đ° Payment Summary
- Text(
- "Payment summary",
- style: TextStyle(
- fontSize: 12.sp,
- fontWeight: FontWeight.w400,
- color: const Color(0xff999999),
- ),
- ),
- Divider(color: Color(0xffEDEDED)),
- const SizedBox(height: 5),
-
- _buildPaymentRow("Subtotal", "\$ 50"),
- const SizedBox(height: 20),
- _buildPaymentRow("Discount", "\$ 20", highlight: true),
- const SizedBox(height: 8),
- Divider(color: Colors.black),
- _buildPaymentRow("Grand Total", "\$ 30", size: 20.sp),
- const SizedBox(height: 28),
- Container(color: Color(0xffFAFAFA), height: 10),
- const SizedBox(height: 10),
- Row(
- children: [
- const Icon(
- Icons.home_outlined,
- color: Color(0xffF95F62),
- size: 20,
- ),
- const SizedBox(width: 10),
- Expanded(
- child: Text(
- "Unit 7, Level 3, Dummy Towers 33.......",
- style: GoogleFonts.poppins(
- fontSize: 13.sp,
- color: const Color(0xff2D3134),
- ),
- overflow: TextOverflow.ellipsis,
- ),
- ),
- IconButton(
- onPressed: () {},
- icon: const Icon(
- Icons.edit_outlined,
- color: Color(0xffF95F62),
- size: 18,
- ),
- ),
- ],
- ),
- const SizedBox(height: 10),
- Container(color: Color(0xffFAFAFA), height: 10),
-
- const SizedBox(height: 40),
-
- // đ§ž Pay Button
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: () {
- bloc.add(GoToNextStep());
- },
- style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xffF95F62),
- padding: EdgeInsets.symmetric(vertical: 16.h),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(40),
- ),
- ),
- child: Text(
- "Pay \$30",
- style: TextStyle(
- color: Colors.white,
- fontSize: 14.sp,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ),
- ],
+ return BlocConsumer(
+ listener: (context, checkoutState) {
+ if (checkoutState.isSuccess && !checkoutState.isDraft) {
+ // đ Payment flow: Check if we have clientSecret
+ if (checkoutState.clientSecret != null && checkoutState.clientSecret!.isNotEmpty) {
+ // Initiate Stripe payment with clientSecret
+ _handlePaymentFlow(context, checkoutState.clientSecret!);
+ } else {
+ // No clientSecret - show error
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Error: Payment initialization failed'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ // Navigate to MyPostCardsView on error
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const MyPostCardsView(),
+ ),
+ );
+ }
+ } else if (checkoutState.isSuccess && checkoutState.isDraft) {
+ // Draft saved successfully
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Draft saved successfully!'),
+ backgroundColor: Colors.green,
),
- ),
+ );
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const MyPostCardsView(),
+ ),
+ );
+ } else if (checkoutState.error != null) {
+ // Show error message
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Error: ${checkoutState.error}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ builder: (context, checkoutState) {
+ return BlocBuilder(
+ builder: (context, creationState) {
+ return Stack(
+ children: [
+ SafeArea(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
+ ),
+ // Header
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Checkout",
+ style: GoogleFonts.poppins(
+ fontSize: 20.sp,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xff1A1A1A),
+ ),
+ ),
+ TextButton(
+ onPressed: checkoutState.isLoading
+ ? null
+ : () {
+ context
+ .read()
+ .add(SaveAsDraftEvent());
+ },
+ child: Text(
+ "Save as draft",
+ style: GoogleFonts.poppins(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w500,
+ color: checkoutState.isLoading
+ ? Colors.grey
+ : const Color(0xffF95F62),
+ ),
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ MessageCardWidget(
+ message: creationState.message ?? "",
+ selectedFont: creationState.selectedFont,
+ ),
+ SizedBox(height: 10.h),
+ PostCardPreviewWidget(
+ imagePath: creationState.imagePath ?? "",
+ message: creationState.message ?? "",
+ selectedFont: creationState.selectedFont,
+ ),
+
+ SizedBox(height: 60.h),
+
+ // đ° Payment Summary
+ Text(
+ "Payment summary",
+ style: TextStyle(
+ fontSize: 12.sp,
+ fontWeight: FontWeight.w400,
+ color: const Color(0xff999999),
+ ),
+ ),
+ Divider(color: Color(0xffEDEDED)),
+ const SizedBox(height: 5),
+
+ _buildPaymentRow(
+ "Subtotal", "\$ ${widget.baseAmount.toStringAsFixed(2)}"),
+ const SizedBox(height: 20),
+ _buildPaymentRow(
+ "Tax", "\$ ${widget.totalTaxAmount.toStringAsFixed(2)}",
+ highlight: true),
+ const SizedBox(height: 8),
+ Divider(color: Colors.black),
+ _buildPaymentRow(
+ "Grand Total", "\$ ${widget.totalAmount.toStringAsFixed(2)}",
+ size: 20.sp),
+ const SizedBox(height: 28),
+ Container(color: Color(0xffFAFAFA), height: 10),
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ const Icon(
+ Icons.home_outlined,
+ color: Color(0xffF95F62),
+ size: 20,
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text(
+ "${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}",
+ style: GoogleFonts.poppins(
+ fontSize: 13.sp,
+ color: const Color(0xff2D3134),
+ ),
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ ),
+ ),
+ IconButton(
+ onPressed: () {
+
+ },
+ icon: const Icon(
+ Icons.edit_outlined,
+ color: Color(0xffF95F62),
+ size: 18,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Container(color: Color(0xffFAFAFA), height: 10),
+
+ const SizedBox(height: 40),
+
+ // đ§ž Pay Button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: checkoutState.isLoading
+ ? null
+ : () {
+ context
+ .read()
+ .add(SubmitPostcardEvent());
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(vertical: 16.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ ),
+ child: checkoutState.isLoading
+ ? SizedBox(
+ height: 20.h,
+ width: 20.h,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ Colors.white),
+ ),
+ )
+ : Text(
+ "Pay \$${widget.totalAmount.toStringAsFixed(2)}",
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ // Loading overlay
+ if (checkoutState.isLoading)
+ Container(
+ color: Colors.black.withOpacity(0.3),
+ child: Center(
+ child: CircularProgressIndicator(
+ valueColor: AlwaysStoppedAnimation(
+ Color(0xffF95F62)),
+ ),
+ ),
+ ),
+ ],
+ );
+ },
);
},
);
@@ -155,11 +549,11 @@ class PostcardCheckoutPageView extends StatelessWidget {
/// đĩ Helper for payment summary row
Widget _buildPaymentRow(
- String label,
- String value, {
- bool highlight = false,
- double? size,
- }) {
+ String label,
+ String value, {
+ bool highlight = false,
+ double? size,
+ }) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -174,10 +568,10 @@ class PostcardCheckoutPageView extends StatelessWidget {
Container(
decoration: highlight
? BoxDecoration(
- color: const Color(0xffFDCDCE),
- borderRadius: BorderRadius.circular(6),
- border: Border.all(color: Color(0xffEDEDED)),
- )
+ color: const Color(0xffFDCDCE),
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(color: Color(0xffEDEDED)),
+ )
: null,
padding: EdgeInsets.symmetric(
horizontal: highlight ? 6 : 0,
@@ -195,4 +589,4 @@ class PostcardCheckoutPageView extends StatelessWidget {
],
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart
index 8f3ffc5..cfcb88c 100644
--- a/lib/postcard/views/postcard_creation_page_view.dart
+++ b/lib/postcard/views/postcard_creation_page_view.dart
@@ -8,9 +8,11 @@ import 'package:citycards_customer/postcard/views/write_message_step_page_view.d
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import '../blocs/postcardCheckout/postcard_checkout_bloc.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_state.dart';
-import 'my_orders_page_view.dart';
+import '../repository/postcard_checkout_repository.dart';
+import 'my_postcards_view.dart';
import 'order_success_page_view.dart';
class PostcardCreationPage extends StatelessWidget {
@@ -40,13 +42,35 @@ class PostcardCreationPage extends StatelessWidget {
stepWidget = const PostcardPurchaseFormPageView();
break;
case PostcardStep.checkout:
- stepWidget = const PostcardCheckoutPageView();
+ stepWidget = BlocProvider(
+ create: (_) => PostcardCheckoutBloc(
+ repository: CreatePostCardRepository(),
+ ),
+ child: PostcardCheckoutPageView(
+ countryName: state.country ?? 'N/A',
+ cityName: state.city ?? 'N/A',
+ stateName: state.state ?? 'N/A',
+ zipCode: state.zipCode ?? 'N/A',
+ pcTitle: state.pcTitle ?? 'N/A',
+ pcNumber: '12',
+ pcDatetime: '2008-11-20',
+ fullname: state.fullName ?? 'N/A',
+ emailAddress: state.emailId ?? 'N/A',
+ mobileNumber: state.phoneNumber ?? 'N/A',
+ isdCode: '+91',
+ isForSelf: !state.isGift,
+ totalTaxAmount: 0.5,
+ baseAmount: 10,
+ totalAmount: 10.5,
+ ),
+ );
break;
+
case PostcardStep.orderSuccess:
stepWidget = const OrderSuccessPageView();
break;
case PostcardStep.myOrders:
- stepWidget = const MyOrdersPageView();
+ stepWidget = const MyPostCardsView();
break;
case PostcardStep.myOrderPostcardPreview:
stepWidget = const OrderPostcardPreviewPageView();
diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart
index 92d77e2..88e67d0 100644
--- a/lib/postcard/views/postcard_purchase_form_page_view.dart
+++ b/lib/postcard/views/postcard_purchase_form_page_view.dart
@@ -8,9 +8,38 @@ import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_events.dart';
import '../blocs/postcard_creation_state.dart';
-class PostcardPurchaseFormPageView extends StatelessWidget {
+class PostcardPurchaseFormPageView extends StatefulWidget {
const PostcardPurchaseFormPageView({super.key});
+ @override
+ State createState() => _PostcardPurchaseFormPageViewState();
+}
+
+class _PostcardPurchaseFormPageViewState extends State {
+ final _formKey = GlobalKey();
+
+ // Controllers
+ final _titleController = TextEditingController();
+ final _fullNameController = TextEditingController();
+ final _emailController = TextEditingController();
+ final _phoneController = TextEditingController();
+ final _cityController = TextEditingController();
+ final _zipCodeController = TextEditingController();
+
+ String? _selectedCountry;
+ String? _selectedState;
+
+ @override
+ void dispose() {
+ _titleController.dispose();
+ _fullNameController.dispose();
+ _emailController.dispose();
+ _phoneController.dispose();
+ _cityController.dispose();
+ _zipCodeController.dispose();
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
return BlocBuilder(
@@ -20,140 +49,198 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,),
-
- // Order ID
- Text(
- "#78895436",
- style: TextStyle(
- fontSize: 20.sp,
- fontWeight: FontWeight.w600,
- color: const Color(0xff1A1A1A),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: false,
+ showDivider: true,
),
- ),
- const SizedBox(height: 20),
- // Postcard image + title
- Row(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: state.imagePath != null
- ? Image.file(
- File(state.imagePath!),
- height: 70,
- width: 70,
- fit: BoxFit.cover,
- )
- : Container(
- height: 70,
- width: 70,
- color: const Color(0xffFEE7E7),
- child: const Icon(Icons.image_outlined,
- color: Color(0xffFDCDCE)),
- ),
+ // Order ID
+ Text(
+ "#78895436",
+ style: TextStyle(
+ fontSize: 20.sp,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xff1A1A1A),
),
- const SizedBox(width: 16),
- Expanded(
- child: TextField(
- decoration: InputDecoration(
- hintText: "Add title",
- hintStyle: GoogleFonts.poppins(
- color: const Color(0xff999999), fontSize: 14.sp),
- enabledBorder: const UnderlineInputBorder(
- borderSide:
- BorderSide(color: Color(0xffFDCDCE), width: 1),
- ),
- focusedBorder: const UnderlineInputBorder(
- borderSide:
- BorderSide(color: Color(0xffFDCDCE), width: 1),
- ),
+ ),
+ const SizedBox(height: 20),
+
+ // Postcard image + title
+ Row(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: state.imagePath != null
+ ? Image.file(
+ File(state.imagePath!),
+ height: 70,
+ width: 70,
+ fit: BoxFit.cover,
+ )
+ : Container(
+ height: 70,
+ width: 70,
+ color: const Color(0xffFEE7E7),
+ child: const Icon(Icons.image_outlined,
+ color: Color(0xffFDCDCE)),
),
- style: GoogleFonts.poppins(fontSize: 14.sp),
- onChanged: (val) {
- // You can dispatch event here: bloc.add(UpdateTitle(val));
- },
),
+ const SizedBox(width: 16),
+ Expanded(
+ child: TextFormField(
+ controller: _titleController,
+ decoration: InputDecoration(
+ hintText: "Add title",
+ hintStyle: GoogleFonts.poppins(
+ color: const Color(0xff999999), fontSize: 14.sp),
+ enabledBorder: const UnderlineInputBorder(
+ borderSide:
+ BorderSide(color: Color(0xffFDCDCE), width: 1),
+ ),
+ focusedBorder: const UnderlineInputBorder(
+ borderSide:
+ BorderSide(color: Color(0xffFDCDCE), width: 1),
+ ),
+ ),
+ style: GoogleFonts.poppins(fontSize: 14.sp),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter a title';
+ }
+ return null;
+ },
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 28),
+
+ // Personal details section
+ Text(
+ "Add personal details",
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xff1A1A1A),
),
- ],
- ),
-
- const SizedBox(height: 28),
-
- // Personal details section
- Text(
- "Add personal details",
- style: TextStyle(
- fontSize: 16.sp,
- fontWeight: FontWeight.w600,
- color: const Color(0xff1A1A1A),
),
- ),
- const SizedBox(height: 16),
+ const SizedBox(height: 16),
- _buildInputField(
- label: "Full Name",
- hint: "Lorem Ipsum",
- ),
- _buildInputField(
- label: "Email ID",
- hint: "Lorem@gmail.com",
- icon: Icons.email_outlined,
- ),
- _buildInputField(
- label: "Phone number",
- hint: "+91 9999 999 999",
- icon: Icons.phone_outlined,
- ),
-
- const SizedBox(height: 28),
-
- // Address details section
- Text(
- "Add address details",
- style: TextStyle(
- fontSize: 16.sp,
- fontWeight: FontWeight.w600,
- color: const Color(0xff1A1A1A),
+ _buildInputField(
+ label: "Full Name",
+ hint: "Lorem Ipsum",
+ controller: _fullNameController,
+ ),
+ _buildInputField(
+ label: "Email ID",
+ hint: "Lorem@gmail.com",
+ icon: Icons.email_outlined,
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ ),
+ _buildInputField(
+ label: "Phone number",
+ hint: "+91 9999 999 999",
+ icon: Icons.phone_outlined,
+ controller: _phoneController,
+ keyboardType: TextInputType.phone,
),
- ),
- const SizedBox(height: 16),
- _buildInputField(label: "City", hint: "Lorem Ipsum"),
- _buildDropdownField(label: "Country", hint: "Lorem Ipsum"),
- _buildDropdownField(label: "State", hint: "Lorem Ipsum"),
- _buildInputField(label: "Zip Code", hint: "000000"),
+ const SizedBox(height: 28),
- const SizedBox(height: 30),
+ // Address details section
+ Text(
+ "Add address details",
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ color: const Color(0xff1A1A1A),
+ ),
+ ),
+ const SizedBox(height: 16),
- // Next Button
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: () {
- bloc.add(GoToNextStep());
+ _buildInputField(
+ label: "City",
+ hint: "Lorem Ipsum",
+ controller: _cityController,
+ ),
+ _buildDropdownField(
+ label: "Country",
+ hint: "Lorem Ipsum",
+ value: _selectedCountry,
+ onChanged: (val) {
+ setState(() {
+ _selectedCountry = val;
+ });
},
- style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xffF95F62),
- padding: EdgeInsets.symmetric(vertical: 16.h),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(40),
+ ),
+ _buildDropdownField(
+ label: "State",
+ hint: "Lorem Ipsum",
+ value: _selectedState,
+ onChanged: (val) {
+ setState(() {
+ _selectedState = val;
+ });
+ },
+ ),
+ _buildInputField(
+ label: "Zip Code",
+ hint: "000000",
+ controller: _zipCodeController,
+ keyboardType: TextInputType.number,
+ ),
+
+ const SizedBox(height: 30),
+
+ // Next Button
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () {
+ if (_formKey.currentState!.validate()) {
+ // Update the bloc with form data
+ bloc.add(UpdatePurchaseFormData(
+ pcTitle: _titleController.text,
+ fullName: _fullNameController.text,
+ emailId: _emailController.text,
+ phoneNumber: _phoneController.text,
+ city: _cityController.text,
+ country: _selectedCountry ?? '',
+ state: _selectedState ?? '',
+ zipCode: _zipCodeController.text,
+ ));
+
+ // Navigate to next step
+ bloc.add(GoToNextStep());
+ }
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(vertical: 16.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
),
- ),
- child: Text(
- "Next",
- style: TextStyle(
- color: Colors.white,
- fontSize: 14.sp,
- fontWeight: FontWeight.w600,
+ child: Text(
+ "Next",
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
+ ),
),
),
),
- ),
- ],
+ ],
+ ),
),
),
);
@@ -165,7 +252,9 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
Widget _buildInputField({
required String label,
required String hint,
+ required TextEditingController controller,
IconData? icon,
+ TextInputType? keyboardType,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
@@ -181,7 +270,9 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
),
),
const SizedBox(height: 6),
- TextField(
+ TextFormField(
+ controller: controller,
+ keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
hintStyle: GoogleFonts.poppins(
@@ -201,7 +292,24 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
+ errorBorder: OutlineInputBorder(
+ borderSide: const BorderSide(color: Colors.red),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ focusedErrorBorder: OutlineInputBorder(
+ borderSide: const BorderSide(color: Colors.red),
+ borderRadius: BorderRadius.circular(8),
+ ),
),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter $label';
+ }
+ if (label == "Email ID" && !value.contains('@')) {
+ return 'Please enter a valid email';
+ }
+ return null;
+ },
),
],
),
@@ -212,6 +320,8 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
Widget _buildDropdownField({
required String label,
required String hint,
+ required String? value,
+ required Function(String?) onChanged,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
@@ -228,7 +338,7 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
),
const SizedBox(height: 6),
DropdownButtonFormField(
- value: null,
+ value: value,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
@@ -240,6 +350,14 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
borderSide: const BorderSide(color: Color(0xffFDCDCE)),
borderRadius: BorderRadius.circular(8),
),
+ errorBorder: OutlineInputBorder(
+ borderSide: const BorderSide(color: Colors.red),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ focusedErrorBorder: OutlineInputBorder(
+ borderSide: const BorderSide(color: Colors.red),
+ borderRadius: BorderRadius.circular(8),
+ ),
),
icon: const Icon(Icons.keyboard_arrow_down,
color: Color(0xffFDCDCE)),
@@ -251,12 +369,20 @@ class PostcardPurchaseFormPageView extends StatelessWidget {
),
),
items: const [
- DropdownMenuItem(value: "Lorem Ipsum", child: Text("Lorem Ipsum")),
+ DropdownMenuItem(value: "India", child: Text("India")),
+ DropdownMenuItem(value: "USA", child: Text("USA")),
+ // Add more items as needed
],
- onChanged: (val) {},
+ onChanged: onChanged,
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please select $label';
+ }
+ return null;
+ },
),
],
),
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart
index 39f41da..9f3f696 100644
--- a/lib/postcard/widgets/purchase_details_bottom_sheet.dart
+++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
+import '../../checkout/bloc/pass_purchase_details_bloc.dart';
+import '../../checkout/bloc/pass_purchase_details_event.dart';
+import '../../checkout/bloc/pass_purchase_details_state.dart';
+import '../../profile/view/edit_profile/edit_profile_view.dart';
import '../blocs/postcard_creation_bloc.dart';
import '../blocs/postcard_creation_events.dart';
import '../blocs/postcard_creation_state.dart';
-
class PurchaseDetailsBottomSheet {
static void show(BuildContext context) {
final existingBloc = BlocProvider.of(context);
@@ -17,189 +20,230 @@ class PurchaseDetailsBottomSheet {
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext modalContext) {
- return BlocProvider.value(
- value: existingBloc,
+ return MultiBlocProvider(
+ providers: [
+ BlocProvider.value(value: existingBloc),
+ BlocProvider(
+ create: (_) => PurchaseDetailsBloc()..add(LoadProfileEvent()),
+ ),
+ ],
child: BlocBuilder(
- builder: (context, state) {
- final bloc = context.read();
+ builder: (context, postcardState) {
+ final postcardBloc = context.read();
- return Padding(
- padding: EdgeInsets.only(
- bottom: MediaQuery.of(context).viewInsets.bottom,
- top: 16,
- left: 16,
- right: 16,
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(
- width: 45,
- height: 5,
- decoration: BoxDecoration(
- color: Colors.grey[400],
- borderRadius: BorderRadius.circular(10),
- ),
+ return BlocBuilder(
+ builder: (context, purchaseState) {
+ final purchaseBloc = context.read();
+
+ return Padding(
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).viewInsets.bottom,
+ top: 16,
+ left: 16,
+ right: 16,
),
- const SizedBox(height: 12),
-
- Text(
- "Purchase Details",
- style: TextStyle(
- fontSize: 16.sp,
- fontWeight: FontWeight.w600,
- ),
- ),
- const SizedBox(height: 24),
-
- // đĨ Option 1: Buy Postcard for Myself
- GestureDetector(
- onTap: () => bloc.add(TogglePurchaseOption(false)),
- child: Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: !state.isGift
- ? const Color(0xffF95F62)
- : const Color(0xffE0E0E0),
- width: 1.5,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 45,
+ height: 5,
+ decoration: BoxDecoration(
+ color: Colors.grey[400],
+ borderRadius: BorderRadius.circular(10),
),
),
- child: Row(
- children: [
- Radio(
- value: false,
- groupValue: state.isGift,
- onChanged: (_) =>
- bloc.add(TogglePurchaseOption(false)),
- activeColor: const Color(0xffF95F62),
+ const SizedBox(height: 12),
+
+ Text(
+ "Purchase Details",
+ style: TextStyle(
+ fontSize: 16.sp,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // đĨ Option 1: Buy Postcard for Myself
+ GestureDetector(
+ onTap: () {
+ postcardBloc.add(TogglePurchaseOption(false));
+ purchaseBloc.add(ToggleGiftModeEvent(false));
+ },
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: !postcardState.isGift
+ ? const Color(0xffF95F62)
+ : const Color(0xffE0E0E0),
+ width: 1.5,
+ ),
),
- const SizedBox(width: 8),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: const [
- Text(
- "Buy Postcard for Myself",
+ child: Row(
+ children: [
+ Radio(
+ value: false,
+ groupValue: postcardState.isGift,
+ onChanged: (_) {
+ postcardBloc.add(TogglePurchaseOption(false));
+ purchaseBloc.add(ToggleGiftModeEvent(false));
+ },
+ activeColor: const Color(0xffF95F62),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Buy Postcard for Myself",
+ style: TextStyle(
+ fontWeight: FontWeight.w600,
+ color: !postcardState.isGift
+ ? const Color(0xffF95F62)
+ : const Color(0xff9E9E9E),
+ ),
+ ),
+ if (!postcardState.isGift && purchaseState.profile != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ "${purchaseState.profile!.firstName} ${purchaseState.profile!.lastName}",
+ style: const TextStyle(
+ fontWeight: FontWeight.w500,
+ color: Color(0xff1A1A1A),
+ ),
+ ),
+ Text(
+ "${purchaseState.profile!.address1 ?? ""}\n${purchaseState.profile!.address2 ?? ""}",
+ style: const TextStyle(
+ fontSize: 13,
+ color: Color(0xff5E5E5E),
+ ),
+ ),
+ ],
+ if (!postcardState.isGift && purchaseState.isLoadingProfile) ...[
+ const SizedBox(height: 8),
+ const SizedBox(
+ height: 16,
+ width: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: Color(0xffF95F62),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ if (!postcardState.isGift)
+ ElevatedButton(
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => const EditProfilePage(),
+ ),
+ );
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 14, vertical: 8),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ child: const Text(
+ "Edit Details",
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.white,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 20),
+
+ // đŠļ Option 2: Gift the Postcard
+ GestureDetector(
+ onTap: () {
+ postcardBloc.add(TogglePurchaseOption(true));
+ purchaseBloc.add(ToggleGiftModeEvent(true));
+ },
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: postcardState.isGift
+ ? const Color(0xffF95F62)
+ : const Color(0xffE0E0E0),
+ width: 1.5,
+ ),
+ ),
+ child: Row(
+ children: [
+ Radio(
+ value: true,
+ groupValue: postcardState.isGift,
+ onChanged: (_) {
+ postcardBloc.add(TogglePurchaseOption(true));
+ purchaseBloc.add(ToggleGiftModeEvent(true));
+ },
+ activeColor: const Color(0xffF95F62),
+ ),
+ const SizedBox(width: 8),
+ const Expanded(
+ child: Text(
+ "Gift the Postcard for someone else",
style: TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xffF95F62),
),
),
- SizedBox(height: 8),
- Text(
- "Frank Adam",
- style: TextStyle(
- fontWeight: FontWeight.w500,
- color: Color(0xff1A1A1A),
- ),
- ),
- Text(
- "132 My Street, Kingston, NY\n12401",
- style: TextStyle(
- fontSize: 13,
- color: Color(0xff5E5E5E),
- ),
- ),
- ],
- ),
- ),
- ElevatedButton(
- onPressed: () {
- PurchaseDetailsBottomSheet.close(context);
- bloc.add(GoToNextStep());
- },
- style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xffF95F62),
- padding: const EdgeInsets.symmetric(
- horizontal: 14, vertical: 8),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8),
),
- ),
- child: const Text(
- "Edit Details",
- style: TextStyle(
- fontSize: 12,
- color: Colors.white,
- fontWeight: FontWeight.w500,
- ),
- ),
+ ],
),
- ],
- ),
- ),
- ),
-
- const SizedBox(height: 20),
-
- // đŠļ Option 2: Gift the Postcard
- GestureDetector(
- onTap: () => bloc.add(TogglePurchaseOption(true)),
- child: Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: state.isGift
- ? const Color(0xffF95F62)
- : const Color(0xffE0E0E0),
- width: 1.5,
),
),
- child: Row(
- children: [
- Radio(
- value: true,
- groupValue: state.isGift,
- onChanged: (_) =>
- bloc.add(TogglePurchaseOption(true)),
- activeColor: const Color(0xffF95F62),
- ),
- const SizedBox(width: 8),
- const Expanded(
- child: Text(
- "Gift the Postcard for someone else",
- style: TextStyle(
- fontWeight: FontWeight.w600,
- color: Color(0xffF95F62),
- ),
+
+ const SizedBox(height: 15),
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () {
+ PurchaseDetailsBottomSheet.close(context);
+ postcardBloc.add(GoToNextStep());
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xffF95F62),
+ padding: EdgeInsets.symmetric(vertical: 16.h),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(40),
+ ),
+ ),
+ child: Text(
+ "Proceed",
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w600,
),
),
- ],
- ),
- ),
- ),
-
- const SizedBox(height: 15),
- SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: () {
- PurchaseDetailsBottomSheet.close(context);
- bloc.add(GoToNextStep());
- },
- style: ElevatedButton.styleFrom(
- backgroundColor: const Color(0xffF95F62),
- padding: EdgeInsets.symmetric(vertical: 16.h),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(40),
),
),
- child: Text(
- "Proceed",
- style: TextStyle(
- color: Colors.white,
- fontSize: 14.sp,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
+ const SizedBox(height: 15),
+ ],
),
- const SizedBox(height: 15),
- ],
- ),
+ );
+ },
);
},
),
@@ -211,4 +255,4 @@ class PurchaseDetailsBottomSheet {
static void close(BuildContext context) {
Navigator.of(context).pop();
}
-}
+}
\ No newline at end of file
diff --git a/lib/profile/bloc/contactUs/contact_us_bloc.dart b/lib/profile/bloc/contactUs/contact_us_bloc.dart
new file mode 100644
index 0000000..caca266
--- /dev/null
+++ b/lib/profile/bloc/contactUs/contact_us_bloc.dart
@@ -0,0 +1,42 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import '../../repository/contact_us_repository.dart';
+import 'contact_us_event.dart';
+import 'contact_us_state.dart';
+
+class ContactUsBloc extends Bloc {
+ final ContactUsRepository repository;
+
+ ContactUsBloc({required this.repository})
+ : super(ContactUsInitial()) {
+ on(_onSubmitContactUs);
+ }
+
+ Future _onSubmitContactUs(
+ SubmitContactUsEvent event,
+ Emitter emit,
+ ) async {
+ emit(ContactUsLoading());
+
+ try {
+ final response = await repository.submitTicket(
+ firstName: event.firstName,
+ lastName: event.lastName,
+ emailAddress: event.emailAddress,
+ mobileNumber: event.mobileNumber,
+ description: event.description,
+ );
+
+ emit(
+ ContactUsSuccess(
+ message: response['message'] ?? 'Ticket submitted successfully',
+ ),
+ );
+ } catch (e) {
+ emit(
+ ContactUsFailure(
+ error: e.toString().replaceAll('Exception:', '').trim(),
+ ),
+ );
+ }
+ }
+}
diff --git a/lib/profile/bloc/contactUs/contact_us_event.dart b/lib/profile/bloc/contactUs/contact_us_event.dart
new file mode 100644
index 0000000..4d31a82
--- /dev/null
+++ b/lib/profile/bloc/contactUs/contact_us_event.dart
@@ -0,0 +1,34 @@
+import 'package:equatable/equatable.dart';
+
+abstract class ContactUsEvent extends Equatable {
+ const ContactUsEvent();
+
+ @override
+ List get props => [];
+}
+
+/// Event to submit contact us / support ticket
+class SubmitContactUsEvent extends ContactUsEvent {
+ final String firstName;
+ final String lastName;
+ final String emailAddress;
+ final String mobileNumber;
+ final String description;
+
+ const SubmitContactUsEvent({
+ required this.firstName,
+ required this.lastName,
+ required this.emailAddress,
+ required this.mobileNumber,
+ required this.description,
+ });
+
+ @override
+ List get props => [
+ firstName,
+ lastName,
+ emailAddress,
+ mobileNumber,
+ description,
+ ];
+}
diff --git a/lib/profile/bloc/contactUs/contact_us_state.dart b/lib/profile/bloc/contactUs/contact_us_state.dart
new file mode 100644
index 0000000..ebfadb0
--- /dev/null
+++ b/lib/profile/bloc/contactUs/contact_us_state.dart
@@ -0,0 +1,34 @@
+import 'package:equatable/equatable.dart';
+
+abstract class ContactUsState extends Equatable {
+ const ContactUsState();
+
+ @override
+ List get props => [];
+}
+
+/// Initial state
+class ContactUsInitial extends ContactUsState {}
+
+/// Loading state while submitting ticket
+class ContactUsLoading extends ContactUsState {}
+
+/// Success state
+class ContactUsSuccess extends ContactUsState {
+ final String message;
+
+ const ContactUsSuccess({required this.message});
+
+ @override
+ List get props => [message];
+}
+
+/// Error state
+class ContactUsFailure extends ContactUsState {
+ final String error;
+
+ const ContactUsFailure({required this.error});
+
+ @override
+ List get props => [error];
+}
diff --git a/lib/profile/bloc/profile/profile_bloc.dart b/lib/profile/bloc/profile/profile_bloc.dart
index 97fa946..4a3a6e3 100644
--- a/lib/profile/bloc/profile/profile_bloc.dart
+++ b/lib/profile/bloc/profile/profile_bloc.dart
@@ -195,8 +195,7 @@ class ProfileBloc extends Bloc {
print('đ [BLOC] LogoutEvent received');
}
- // Clear local preferences (uncomment when ready)
- // await LocalPreference.clearPreference();
+ await LocalPreference.resetAppData();
emit(const ProfileLoggedOut());
emit(const ProfileInitial());
diff --git a/lib/profile/repository/contact_us_repository.dart b/lib/profile/repository/contact_us_repository.dart
new file mode 100644
index 0000000..6c5a756
--- /dev/null
+++ b/lib/profile/repository/contact_us_repository.dart
@@ -0,0 +1,32 @@
+import '../../networkApiServices/api_urls.dart';
+import '../../networkApiServices/network_api_services.dart';
+
+class ContactUsRepository {
+ final NetworkApiService _apiServices = NetworkApiService();
+
+ /// Submit support ticket
+ Future> submitTicket({
+ required String firstName,
+ required String lastName,
+ required String emailAddress,
+ required String mobileNumber,
+ required String description,
+ }) async {
+ try {
+ final response = await _apiServices.postApi(
+ url: ApiUrls.submitTicket, // add this key in ApiUrls
+ data: {
+ "firstName": firstName,
+ "lastName": lastName,
+ "emailAddress": emailAddress,
+ "mobileNumber": mobileNumber,
+ "description": description,
+ },
+ );
+
+ return response.data as Map;
+ } catch (e) {
+ throw Exception('Failed to submit ticket: $e');
+ }
+ }
+}
diff --git a/lib/profile/view/contact_us/contact_us_view.dart b/lib/profile/view/contact_us/contact_us_view.dart
new file mode 100644
index 0000000..5aa6e41
--- /dev/null
+++ b/lib/profile/view/contact_us/contact_us_view.dart
@@ -0,0 +1,271 @@
+import 'package:citycards_customer/common_packages/app_bar.dart';
+import 'package:citycards_customer/common_packages/back_widget.dart';
+import 'package:citycards_customer/common_packages/custom_text.dart';
+import 'package:citycards_customer/common_packages/custom_textfield.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../../bloc/contactUs/contact_us_bloc.dart';
+import '../../bloc/contactUs/contact_us_event.dart';
+import '../../bloc/contactUs/contact_us_state.dart';
+import '../../repository/contact_us_repository.dart';
+
+class ContactUsPage extends StatelessWidget {
+ const ContactUsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider(
+ create: (_) => ContactUsBloc(repository: ContactUsRepository()),
+ child: const _ContactUsView(),
+ );
+ }
+}
+
+class _ContactUsView extends StatelessWidget {
+ const _ContactUsView();
+
+ @override
+ Widget build(BuildContext context) {
+ final firstNameController = TextEditingController();
+ final lastNameController = TextEditingController();
+ final emailController = TextEditingController();
+ final phoneController = TextEditingController();
+ final messageController = TextEditingController();
+
+ return Scaffold(
+ backgroundColor: Colors.white,
+ body: SafeArea(
+ child: BlocListener(
+ listener: (context, state) {
+ if (state is ContactUsSuccess) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(state.message),
+ backgroundColor: Colors.green,
+ ),
+ );
+
+ firstNameController.clear();
+ lastNameController.clear();
+ emailController.clear();
+ phoneController.clear();
+ messageController.clear();
+ }
+
+ if (state is ContactUsFailure) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(state.error),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ CommonAppBar(
+ isWhiteLogo: false,
+ isProfilePage: true,
+ showDivider: true,
+ ),
+
+ backWidget(context, "Contact Us", Colors.black),
+ SizedBox(height: 22.h),
+
+ CustomText(
+ text:
+ "You can get in touch with us through the below platforms. Our team will contact you shortly",
+ size: 14.sp,
+ color: Colors.black.withOpacity(.6),
+ ),
+ SizedBox(height: 20.h),
+
+ /// Customer Support Section
+ Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: 10.w,
+ vertical: 16.h,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0x00000005).withOpacity(.02),
+ borderRadius: BorderRadius.circular(12.r),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ CustomText(
+ text: "Customer Support",
+ size: 18.sp,
+ weight: FontWeight.w500,
+ ),
+ SizedBox(height: 16.h),
+ _supportBox(
+ icon: Icons.phone,
+ title: "Contact Number",
+ subtitle: "+1012 3456 789",
+ action: "Tap to call",
+ ),
+ SizedBox(height: 12.h),
+ _supportBox(
+ icon: Icons.email_rounded,
+ title: "Email",
+ subtitle: "citycards24@gmail.com",
+ action: "Tap to email",
+ ),
+ SizedBox(height: 12.h),
+ _supportBox(
+ icon: Icons.location_on,
+ title: "Location",
+ subtitle:
+ "132 Dartmouth Street Boston, Massachusetts 02156 United States",
+ action: "View on map",
+ ),
+ ],
+ ),
+ ),
+ SizedBox(height: 24.h),
+
+ /// Form Fields
+ CustomTextField(
+ label: "First Name",
+ hint: "Enter your first name",
+ controller: firstNameController,
+ ),
+ CustomTextField(
+ label: "Last Name",
+ hint: "Enter your last name",
+ controller: lastNameController,
+ ),
+ CustomTextField(
+ label: "Email",
+ hint: "Enter your email address",
+ controller: emailController,
+ ),
+ CustomTextField(
+ label: "Phone Number",
+ hint: "Enter your phone number",
+ controller: phoneController,
+ ),
+ CustomTextField(
+ label: "Description",
+ hint: "Write your message here",
+ maxLines: 4,
+ controller: messageController,
+ ),
+
+ SizedBox(height: 24.h),
+
+ /// Submit Button with Loading
+ BlocBuilder(
+ builder: (context, state) {
+ final isLoading = state is ContactUsLoading;
+
+ return SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFFF95F62),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(38.r),
+ ),
+ padding: EdgeInsets.symmetric(vertical: 6.h),
+ ),
+ onPressed: isLoading
+ ? null
+ : () {
+ context.read().add(
+ SubmitContactUsEvent(
+ firstName: firstNameController.text.trim(),
+ lastName: lastNameController.text.trim(),
+ emailAddress: emailController.text.trim(),
+ mobileNumber: phoneController.text.trim(),
+ description: messageController.text.trim(),
+ ),
+ );
+ },
+ child: isLoading
+ ? SizedBox(
+ height: 22.h,
+ width: 22.h,
+ child: const CircularProgressIndicator(
+ strokeWidth: 2,
+ color: Colors.white,
+ ),
+ )
+ : CustomText(
+ text: "Submit Ticket",
+ size: 16.sp,
+ weight: FontWeight.w500,
+ color: Colors.white,
+ ),
+ ),
+ );
+ },
+ ),
+ SizedBox(height: 20.h),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ /// Support Box Widget
+ static Widget _supportBox({
+ required IconData icon,
+ required String title,
+ required String subtitle,
+ required String action,
+ }) {
+ return Container(
+ width: double.infinity,
+ padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8.r),
+ border: Border.all(color: const Color(0xFFF95F62), width: 0.8),
+ color: Colors.white,
+ ),
+ child: Row(
+ children: [
+ Icon(icon, color: const Color(0xFFF95F62), size: 32.sp),
+ SizedBox(width: 12.w),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ CustomText(
+ text: title,
+ size: 11.sp,
+ weight: FontWeight.w600,
+ color: Colors.black.withOpacity(.6),
+ ),
+ SizedBox(height: 6.h),
+ Text(
+ subtitle,
+ style: TextStyle(
+ fontSize: 14.sp,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ SizedBox(height: 2.h),
+ Text(
+ action,
+ style: TextStyle(
+ fontSize: 11.sp,
+ color: Colors.black.withOpacity(.4),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart
index 12b8b56..b58ca4c 100644
--- a/lib/profile/view/edit_profile/edit_profile_view.dart
+++ b/lib/profile/view/edit_profile/edit_profile_view.dart
@@ -46,6 +46,8 @@ class _EditProfilePageState extends State {
}
final userId = await LocalPreference.getUserId();
+ if (!mounted) return;
+
if (userId != null) {
context.read().add(FetchProfileEvent(userId: userId));
}
@@ -174,6 +176,8 @@ class _EditProfilePageState extends State {
print('đĩ [EDIT PROFILE] File size: ${(fileSize / 1024).toStringAsFixed(2)} KB');
}
+ if (!mounted) return;
+
// â REPLACED setState with BLoC event
context.read().add(
ProfileImageSelectedEvent(imageFile: imageFile),
@@ -184,6 +188,8 @@ class _EditProfilePageState extends State {
print('â [EDIT PROFILE] Error picking image: $e');
}
+ if (!mounted) return;
+
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to pick image: $e'),
@@ -279,6 +285,7 @@ class _EditProfilePageState extends State {
final userId = await LocalPreference.getUserId();
if (userId == null) {
+ if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('User ID not found'),
@@ -288,6 +295,8 @@ class _EditProfilePageState extends State {
return;
}
+ if (!mounted) return;
+
// â Get selectedImageFile from current BLoC state
File? imageFileToSend;
final currentState = context.read().state;
@@ -333,8 +342,18 @@ class _EditProfilePageState extends State {
backgroundColor: Colors.white,
body: SafeArea(
child: BlocConsumer(
- listener: (context, state) {
+ listener: (context, state) async {
if (state is ProfileUpdated) {
+ if (state.profile.profileImage != null &&
+ state.profile.profileImage!.isNotEmpty) {
+ await LocalPreference.setProfileImage(
+ state.profile.profileImage!,
+ );
+ }
+
+ // Check if widget is still mounted before using context
+ if (!mounted) return;
+
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
diff --git a/lib/profile/view/profile_page_view.dart b/lib/profile/view/profile_page_view.dart
index 0fafa63..fc0efa3 100644
--- a/lib/profile/view/profile_page_view.dart
+++ b/lib/profile/view/profile_page_view.dart
@@ -35,7 +35,20 @@ class _ProfilePageState extends State {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
- child: BlocBuilder(
+ child: BlocConsumer(
+ listener: (context, state) {
+ // â SOLUTION: Auto-refresh when profile is updated (from edit page)
+ // This prevents race conditions with manual navigation callbacks
+ if (state is ProfileUpdated) {
+ // Profile was just updated, fetch fresh data
+ final userId = state.profile.id;
+ if (mounted) {
+ context.read().add(
+ FetchProfileEvent(userId: userId),
+ );
+ }
+ }
+ },
builder: (context, state) {
// â Show loading during initial checks and profile loading
if (state is ProfileInitial ||
@@ -126,6 +139,10 @@ class _ProfilePageState extends State {
ElevatedButton(
onPressed: () async {
final userId = await LocalPreference.getUserId();
+
+ // â Check if widget is still mounted after async call
+ if (!mounted) return;
+
if (userId != null) {
context.read().add(
FetchProfileEvent(userId: userId),
@@ -213,8 +230,11 @@ class _ProfilePageState extends State {
padding: EdgeInsets.symmetric(vertical: 6.h),
),
onPressed: () {
- // â REPLACED setState with BLoC event
context.read().add(const LogoutEvent());
+ Navigator.pushReplacementNamed(
+ context,
+ RouteConstants.home,
+ );
},
child: Text(
'Log out',
@@ -451,20 +471,13 @@ class _ProfilePageState extends State {
_buildListTile(
icon: "assets/icons/user_profile.png",
title: 'Edit profile',
- onTap: () async {
- final result = await Navigator.pushNamed(
+ onTap: () {
+ // â SOLUTION: Just navigate - BlocListener will auto-refresh on ProfileUpdated
+ // This prevents race conditions from manual refresh callbacks
+ Navigator.pushNamed(
context,
RouteConstants.editProfile,
);
-
- if (result == true) {
- final userId = await LocalPreference.getUserId();
- if (userId != null) {
- context.read().add(
- FetchProfileEvent(userId: userId),
- );
- }
- }
},
),