my Post Cards added with get api and more changes

This commit is contained in:
mystery012728
2026-02-05 12:07:33 +05:30
parent 082bb9b74a
commit c2ffc9d9a7
69 changed files with 5658 additions and 2168 deletions

View File

@@ -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.
<h1>Figma Link</h1>

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -14,6 +14,7 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
}) : _stripeService = stripeService ?? StripeService(),
super(const StripePaymentInitial()) {
on<InitiatePayment>(_onInitiatePayment);
on<InitiatePaymentWithClientSecret>(_onInitiatePaymentWithClientSecret);
on<ResetPaymentState>(_onResetPaymentState);
}
@@ -66,6 +67,46 @@ class StripePaymentBloc extends Bloc<StripePaymentEvent, StripePaymentState> {
}
}
/// 🆕 NEW: Handle payment with clientSecret directly from backend
Future<void> _onInitiatePaymentWithClientSecret(
InitiatePaymentWithClientSecret event,
Emitter<StripePaymentState> 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<StripePaymentState> emit,

View File

@@ -20,6 +20,18 @@ class InitiatePayment extends StripePaymentEvent {
List<Object?> 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<Object?> get props => [clientSecret];
}
class ResetPaymentState extends StripePaymentEvent {
const ResetPaymentState();
}

View File

@@ -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<String> createPaymentIntent({
required int amount,

View File

@@ -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),

View File

@@ -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(

View File

@@ -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<MyPassCartEvent, MyPassCartState> {
final MyPassCartRepository repository;
MyPassCartBloc({required this.repository}) : super(const MyPassCartInitial()) {
on<FetchPassCartEvent>(_onFetchPassCart);
on<ClearPassCartEvent>(_onClearPassCart);
}
/// Handle fetching pass cart data
Future<void> _onFetchPassCart(
FetchPassCartEvent event,
Emitter<MyPassCartState> 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<void> _onClearPassCart(
ClearPassCartEvent event,
Emitter<MyPassCartState> 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()));
}
}
}

View File

@@ -0,0 +1,18 @@
import 'package:equatable/equatable.dart';
abstract class MyPassCartEvent extends Equatable {
const MyPassCartEvent();
@override
List<Object?> 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();
}

View File

@@ -0,0 +1,43 @@
import 'package:equatable/equatable.dart';
abstract class MyPassCartState extends Equatable {
const MyPassCartState();
@override
List<Object?> 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<String, dynamic> cartData;
const MyPassCartLoaded({required this.cartData});
@override
List<Object?> 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<Object?> get props => [message];
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/foundation.dart';
import '../../localPreference/local_preference.dart';
class MyPassCartRepository {
/// Fetch pass cart data from local database
Future<Map<String, dynamic>?> 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;
}
}
}

View File

@@ -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<MyCartPage> {
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,

View File

@@ -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<MyPassesPage> createState() => _MyPassesPageState();
}
class _MyPassesPageState extends State<MyPassesPage> {
// For coupon/discount management
String? appliedCouponCode;
double discountPercentage = 0.0;
@override
void initState() {
super.initState();
// Fetch cart data when page loads
context.read<MyPassCartBloc>().add(const FetchPassCartEvent());
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPassCartBloc, MyPassCartState>(
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<bool>(
future: LocalPreference.getLogin(),
builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false;
return CustomFilledButton(
onTap: () {
if (!isLoggedIn) {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
} else {
// Handle checkout logic for logged in user
// You can navigate to checkout or payment screen
print("✅ User is logged in, proceed to checkout");
}
},
width: double.infinity,
label: isLoggedIn ? "Checkout" : "Login to Checkout",
);
},
),
SizedBox(height: 25.h),
],
);
} else if (state is MyPassCartEmpty) {
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();
},
);
}
}

View File

@@ -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<PassBloc, PassState>(
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,
),
],
),
);
},
);
}
}

View File

@@ -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<CheckoutEvent, CheckoutState> {
CheckoutBloc() : super(CheckoutState.initial()) {
// Handle apply coupon event
on<ApplyCouponEvent>(_onApplyCoupon);
// Handle remove coupon event
on<RemoveCouponEvent>(_onRemoveCoupon);
// Handle confirm purchase details event
on<ConfirmPurchaseDetailsEvent>(_onConfirmPurchaseDetails);
// Handle reset purchase details event
on<ResetPurchaseDetailsEvent>(_onResetPurchaseDetails);
}
/// Handle applying a coupon
void _onApplyCoupon(ApplyCouponEvent event, Emitter<CheckoutState> emit) {
emit(state.copyWith(
appliedCouponCode: event.couponCode,
discountPercentage: event.discountPercentage,
));
}
/// Handle removing a coupon
void _onRemoveCoupon(RemoveCouponEvent event, Emitter<CheckoutState> emit) {
emit(state.copyWith(
clearCoupon: true,
discountPercentage: 0.0,
));
}
/// Handle confirming purchase details
void _onConfirmPurchaseDetails(
ConfirmPurchaseDetailsEvent event,
Emitter<CheckoutState> emit,
) {
emit(state.copyWith(isPurchaseDetailsConfirmed: true));
}
/// Handle resetting purchase details confirmation
void _onResetPurchaseDetails(
ResetPurchaseDetailsEvent event,
Emitter<CheckoutState> emit,
) {
emit(state.copyWith(isPurchaseDetailsConfirmed: false));
}
}

View File

@@ -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 {}

View File

@@ -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;
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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,
};
}
}

View File

@@ -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 {

View File

@@ -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<ProfileBloc, ProfileState>(
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,
);
},
),
),
],

View File

@@ -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),
),
),
),
],
),
);
}
}

View File

@@ -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';

View File

@@ -45,6 +45,7 @@ class CreateAccountBloc extends Bloc<CreateAccountEvent, CreateAccountState> {
role: userModel.user.role,
roleId: userModel.user.roleId,
);
await LocalPreference.setProfileImage(userModel.user.profileImage);
emit(CreateAccountSuccess(
message: response['message'] ?? 'Account created successfully',
userData: response['data'] ?? {},

View File

@@ -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,
};

View File

@@ -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<CreateAccountBloc, CreateAccountState>(
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<ProfileBloc>().add(FetchProfileEvent(userId: userId!));
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
Navigator.pop(context);
Navigator.pop(context);
} else if (state is CreateAccountFailure) {

View File

@@ -201,13 +201,23 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
// 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+',
),
);
},
),

View File

@@ -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<HomePage> {
buildOffstageNavigator(
3,
currentIndex,
const PostcardPage(),
const MyPostCardsView(),
_navigatorKeys[3],
),
],

View File

@@ -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<RegisteredUserHomePage> {
@override
void initState() {
super.initState();
// _loadMyPostCards();
_checkAndShowCitySelection();
_loadProfileIfLoggedIn();
}
Future<void> _loadProfileIfLoggedIn() async {
final userId = await LocalPreference.getUserId();
if (userId != null && mounted) {
context.read<ProfileBloc>().add(
FetchProfileEvent(userId: userId),
);
}
}
Future<void> _loadMyPostCards() async {
final userId = await LocalPreference.getUserId();
if (userId != null && mounted) {
context.read<MyPostCardBloc>().add(FetchDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
context.read<MyPostCardBloc>().add(FetchOrderPostCards());
}
}
Future<void> _checkAndShowCitySelection() async {

View File

@@ -79,7 +79,12 @@ class _IntroScreensViewState extends State<IntroScreensView> {
right: 20,
child: GestureDetector(
onTap: (){
Navigator.pushReplacementNamed(context,RouteConstants.home);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => const FirstTimeUserHomePage(),
),
);
},
child: Container(
height: 48.h,

View File

@@ -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
)
''');
},
);

View File

@@ -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<void> 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<void> 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<void> 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<String?> 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<void> 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<Map<String, dynamic>?> 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<void> 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<void> 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<void> 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<void> resetAppData() async {
await clearLogin();
await clearTokens();
await clearUserDetails();
await clearPassCart();// optional
await clearProfileImage();// optional
}
static Future<void> 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;
}
}
}

View File

@@ -41,6 +41,7 @@ class VerifyOtpBloc extends Bloc<VerifyOtpEvent, VerifyOtpState> {
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()));

View File

@@ -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<VerifyOtpBottomsheet> {
final userId = await LocalPreference.getUserId();
context.read<ProfileBloc>().add(FetchProfileEvent(userId: userId!));
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
context.read<MyPostCardBloc>().add(CheckLoginStatus());
context.read<MyPostCardBloc>().add(FetchDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
context.read<MyPostCardBloc>().add(FetchOrderPostCards());
// User exists - navigate to home/dashboard
// Navigator.of(context).pushReplacementNamed(RouteConstants.home);
ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -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<MyPostCardBloc>(
create: (context) => MyPostCardBloc(
repository: MyPostCardsRepository(),
),
),
],
child: MaterialApp(
onGenerateRoute: _appRouter.onGenerateRoute,

View File

@@ -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";
}

View File

@@ -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<MyPostCardEvent, MyPostCardState> {
final MyPostCardsRepository repository;
MyPostCardBloc({required this.repository}) : super(const MyPostCardInitial()) {
on<CheckLoginStatus>(_onCheckLoginStatus);
on<FetchDraftPostCards>(_onFetchDraftPostCards);
on<FetchOrderPostCards>(_onFetchOrderPostCards);
on<RefreshDraftPostCards>(_onRefreshDraftPostCards);
on<RefreshOrderPostCards>(_onRefreshOrderPostCards);
}
/// Handle checking login status
Future<void> _onCheckLoginStatus(
CheckLoginStatus event,
Emitter<MyPostCardState> 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<void> _onFetchDraftPostCards(
FetchDraftPostCards event,
Emitter<MyPostCardState> 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<void> _onFetchOrderPostCards(
FetchOrderPostCards event,
Emitter<MyPostCardState> 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<void> _onRefreshDraftPostCards(
RefreshDraftPostCards event,
Emitter<MyPostCardState> 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<void> _onRefreshOrderPostCards(
RefreshOrderPostCards event,
Emitter<MyPostCardState> 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',
));
}
}
}

View File

@@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
abstract class MyPostCardEvent extends Equatable {
const MyPostCardEvent();
@override
List<Object?> 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();
}

View File

@@ -0,0 +1,76 @@
import 'package:equatable/equatable.dart';
import '../../models/my_postcard_model.dart';
abstract class MyPostCardState extends Equatable {
const MyPostCardState();
@override
List<Object?> 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<MyPostCard> draftPostCards;
final List<MyPostCard> orderPostCards;
final bool isDraftLoading;
final bool isOrderLoading;
const MyPostCardLoaded({
required this.draftPostCards,
required this.orderPostCards,
this.isDraftLoading = false,
this.isOrderLoading = false,
});
@override
List<Object?> get props => [
draftPostCards,
orderPostCards,
isDraftLoading,
isOrderLoading,
];
/// Helper method to create a copy with updated values
MyPostCardLoaded copyWith({
List<MyPostCard>? draftPostCards,
List<MyPostCard>? 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<Object?> get props => [errorMessage, errorType];
}

View File

@@ -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<PostcardCheckoutEvent, PostcardCheckoutState> {
final CreatePostCardRepository repository;
PostcardCheckoutBloc({required this.repository})
: super(const PostcardCheckoutState()) {
on<UpdateAddressEvent>(_onUpdateAddress);
on<UpdatePostcardContentEvent>(_onUpdateContent);
on<UpdateCheckoutDataEvent>(_onUpdateCheckoutData);
on<SaveAsDraftEvent>(_onSaveAsDraft);
on<SubmitPostcardEvent>(_onSubmitPostcard);
on<ConfirmPaymentEvent>(_onConfirmPayment); // 🆕 NEW
}
void _onUpdateAddress(
UpdateAddressEvent event, Emitter<PostcardCheckoutState> 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<PostcardCheckoutState> emit) {
emit(state.copyWith(
pcTitle: event.pcTitle,
pcContent: event.pcContent,
pcImageFile: event.pcImageFile,
));
}
void _onUpdateCheckoutData(
UpdateCheckoutDataEvent event, Emitter<PostcardCheckoutState> 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<void> _onSaveAsDraft(
SaveAsDraftEvent event, Emitter<PostcardCheckoutState> 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<void> _onSubmitPostcard(
SubmitPostcardEvent event, Emitter<PostcardCheckoutState> 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<void> _onConfirmPayment(
ConfirmPaymentEvent event, Emitter<PostcardCheckoutState> 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(),
));
}
}
}

View File

@@ -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,
});
}

View File

@@ -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
);
}
}

View File

@@ -72,6 +72,19 @@ class PostcardCreationBloc
}
});
on<UpdatePurchaseFormData>((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<SelectFilter>((event, emit) async {
// 1⃣ No image? Exit early.

View File

@@ -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,
});
}

View File

@@ -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,
);
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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(),
};
}
}

View File

@@ -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<List<MyPostCard>> 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();
}
}

View File

@@ -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<Map<String, dynamic>> createPostCard({
required String countryName,
required String cityName,
required String stateName,
required String zipCode,
String? address1, // NOT required
String? address2, // NOT required
required String pcTitle,
required String pcContent,
required File pcImageFile, // ⭐ CHANGED: File instead of String
required String pcNumber,
required String pcDatetime,
required String fullname,
required String emailAddress,
required String mobileNumber,
required String isdCode,
required bool isForSelf,
required 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<String, dynamic>;
} catch (e, stackTrace) {
log(
'❌ createPostCard FAILED',
error: e,
stackTrace: stackTrace,
);
throw Exception('Failed to create postcard: $e');
}
}
/// 🆕 Confirm Payment after successful Stripe payment
/// POST https://devapi.citycards.betadelivery.com/mobile/postcards/{postcardId}/confirm-payment
Future<Map<String, dynamic>> confirmPayment({
required int postcardId,
required String stripeStatus,
required String paymentStatus,
}) async {
try {
log('🟢 confirmPayment() called');
log('📤 [CONFIRM PAYMENT] Postcard ID: $postcardId');
log('📤 [CONFIRM PAYMENT] Stripe Status: $stripeStatus');
log('📤 [CONFIRM PAYMENT] Payment Status: $paymentStatus');
// Construct URL with postcardId
final url = '${ApiUrls.baseUrl}/mobile/postcards/$postcardId/confirm-payment';
// Note: Update ApiUrls class if you want to use a constant instead
// final url = ApiUrls.confirmPayment(postcardId);
if (kDebugMode) {
print('📤 [CONFIRM PAYMENT] API URL: $url');
}
// Request body
final requestBody = {
'stripeStatus': stripeStatus,
'paymentStatus': paymentStatus,
};
log('📦 Request Body: $requestBody');
// Send POST request
final response = await _apiServices.postApi(
url: url,
data: requestBody,
);
log('✅ [CONFIRM PAYMENT] Response Status: ${response.statusCode}');
log('📥 [CONFIRM PAYMENT] Response Data: ${response.data}');
if (kDebugMode) {
print('📤 [CONFIRM PAYMENT] ✅ Payment confirmation successful');
print('📤 [CONFIRM PAYMENT] Full Response: ${response.data}');
}
return response.data as Map<String, dynamic>;
} catch (e, stackTrace) {
log(
'❌ confirmPayment FAILED',
error: e,
stackTrace: stackTrace,
);
throw Exception('Failed to confirm payment: $e');
}
}
}

View File

@@ -1,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<MyOrdersPageView> createState() => _MyOrdersPageViewState();
}
class _MyOrdersPageViewState extends State<MyOrdersPageView> {
bool showDrafts = true;
@override
Widget build(BuildContext context) {
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
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<MyOrdersPageView> createState() => _MyOrdersPageViewState();
// }
//
// class _MyOrdersPageViewState extends State<MyOrdersPageView> {
// 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,
// ),
// ),
// ),
// ),
// ],
// ),
// ),
// );
// }
// }

View File

@@ -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<MyPostCardBloc, MyPostCardState>(
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<MyPostCardBloc>().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<MyPostCardBloc>().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,
),
),
],
),
),
],
),
),
],
),
);
}
}

View File

@@ -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<MyPostCardBloc, MyPostCardState>(
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<MyPostCardBloc>().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<MyPostCardBloc>().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;
}
}
}

View File

@@ -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<MyPostcardPreviewView> createState() => _MyPostcardPreviewViewState();
}
class _MyPostcardPreviewViewState extends State<MyPostcardPreviewView> {
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<double> 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(),
],
),
),
),
],
),
),
);
}
}

View File

@@ -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<MyPostCardsView> createState() => _MyPostCardsViewState();
}
class _MyPostCardsViewState extends State<MyPostCardsView> {
bool showDrafts = true;
@override
void initState() {
super.initState();
developer.log('🚀 MyPostCardsView initialized', name: 'MyPostCardsView');
context.read<MyPostCardBloc>().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<MyPostCardBloc, MyPostCardState>(
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<MyPostCardBloc>().add(const CheckLoginStatus());
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
),
child: const Text("Retry"),
),
],
),
),
),
],
);
}
}

View File

@@ -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<PostcardCheckoutPageView> createState() => _PostcardCheckoutPageViewState();
}
class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
@override
void initState() {
super.initState();
// Initialize checkout bloc with data from widget
WidgetsBinding.instance.addPostFrameCallback((_) {
final creationState = context.read<PostcardCreationBloc>().state;
// ⭐ Convert image path to File object
File? imageFile;
if (creationState.imagePath != null && creationState.imagePath!.isNotEmpty) {
imageFile = File(creationState.imagePath!);
}
context.read<PostcardCheckoutBloc>().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<void> _handlePaymentFlow(BuildContext context, String clientSecret) async {
// Show payment bottom sheet with BLoC
final paymentSuccess = await showModalBottomSheet<bool>(
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<StripePaymentBloc, StripePaymentState>(
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>(
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<PostcardCreationBloc>().add(GoToNextStep());
final bloc = context.read<PostcardCheckoutBloc>();
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<PostcardCheckoutBloc>();
bloc.add(
ConfirmPaymentEvent(
stripeStatus: 'requires_payment_method',
paymentStatus: 'failed',
),
);
}
}
@override
Widget build(BuildContext context) {
return BlocBuilder<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
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<PostcardCheckoutBloc, PostcardCheckoutState>(
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<PostcardCreationBloc, PostcardCreationState>(
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<PostcardCheckoutBloc>()
.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<PostcardCheckoutBloc>()
.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<Color>(
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>(
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 {
],
);
}
}
}

View File

@@ -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();

View File

@@ -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<PostcardPurchaseFormPageView> createState() => _PostcardPurchaseFormPageViewState();
}
class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageView> {
final _formKey = GlobalKey<FormState>();
// 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<PostcardCreationBloc, PostcardCreationState>(
@@ -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<String>(
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;
},
),
],
),
);
}
}
}

View File

@@ -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<PostcardCreationBloc>(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<PostcardCreationBloc, PostcardCreationState>(
builder: (context, state) {
final bloc = context.read<PostcardCreationBloc>();
builder: (context, postcardState) {
final postcardBloc = context.read<PostcardCreationBloc>();
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<PurchaseDetailsBloc, PurchaseDetailsState>(
builder: (context, purchaseState) {
final purchaseBloc = context.read<PurchaseDetailsBloc>();
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<bool>(
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<bool>(
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<bool>(
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<bool>(
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();
}
}
}

View File

@@ -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<ContactUsEvent, ContactUsState> {
final ContactUsRepository repository;
ContactUsBloc({required this.repository})
: super(ContactUsInitial()) {
on<SubmitContactUsEvent>(_onSubmitContactUs);
}
Future<void> _onSubmitContactUs(
SubmitContactUsEvent event,
Emitter<ContactUsState> 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(),
),
);
}
}
}

View File

@@ -0,0 +1,34 @@
import 'package:equatable/equatable.dart';
abstract class ContactUsEvent extends Equatable {
const ContactUsEvent();
@override
List<Object?> 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<Object?> get props => [
firstName,
lastName,
emailAddress,
mobileNumber,
description,
];
}

View File

@@ -0,0 +1,34 @@
import 'package:equatable/equatable.dart';
abstract class ContactUsState extends Equatable {
const ContactUsState();
@override
List<Object?> 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<Object?> get props => [message];
}
/// Error state
class ContactUsFailure extends ContactUsState {
final String error;
const ContactUsFailure({required this.error});
@override
List<Object?> get props => [error];
}

View File

@@ -195,8 +195,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
print('📄 [BLOC] LogoutEvent received');
}
// Clear local preferences (uncomment when ready)
// await LocalPreference.clearPreference();
await LocalPreference.resetAppData();
emit(const ProfileLoggedOut());
emit(const ProfileInitial());

View File

@@ -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<Map<String, dynamic>> 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<String, dynamic>;
} catch (e) {
throw Exception('Failed to submit ticket: $e');
}
}
}

View File

@@ -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<ContactUsBloc, ContactUsState>(
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<ContactUsBloc, ContactUsState>(
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<ContactUsBloc>().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),
),
),
],
),
),
],
),
);
}
}

View File

@@ -46,6 +46,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
}
final userId = await LocalPreference.getUserId();
if (!mounted) return;
if (userId != null) {
context.read<ProfileBloc>().add(FetchProfileEvent(userId: userId));
}
@@ -174,6 +176,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
print('🔵 [EDIT PROFILE] File size: ${(fileSize / 1024).toStringAsFixed(2)} KB');
}
if (!mounted) return;
// ⭐ REPLACED setState with BLoC event
context.read<ProfileBloc>().add(
ProfileImageSelectedEvent(imageFile: imageFile),
@@ -184,6 +188,8 @@ class _EditProfilePageState extends State<EditProfilePage> {
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<EditProfilePage> {
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<EditProfilePage> {
return;
}
if (!mounted) return;
// ⭐ Get selectedImageFile from current BLoC state
File? imageFileToSend;
final currentState = context.read<ProfileBloc>().state;
@@ -333,8 +342,18 @@ class _EditProfilePageState extends State<EditProfilePage> {
backgroundColor: Colors.white,
body: SafeArea(
child: BlocConsumer<ProfileBloc, ProfileState>(
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),

View File

@@ -35,7 +35,20 @@ class _ProfilePageState extends State<ProfilePage> {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: BlocBuilder<ProfileBloc, ProfileState>(
child: BlocConsumer<ProfileBloc, ProfileState>(
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<ProfileBloc>().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<ProfilePage> {
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<ProfileBloc>().add(
FetchProfileEvent(userId: userId),
@@ -213,8 +230,11 @@ class _ProfilePageState extends State<ProfilePage> {
padding: EdgeInsets.symmetric(vertical: 6.h),
),
onPressed: () {
// ⭐ REPLACED setState with BLoC event
context.read<ProfileBloc>().add(const LogoutEvent());
Navigator.pushReplacementNamed(
context,
RouteConstants.home,
);
},
child: Text(
'Log out',
@@ -451,20 +471,13 @@ class _ProfilePageState extends State<ProfilePage> {
_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<ProfileBloc>().add(
FetchProfileEvent(userId: userId),
);
}
}
},
),