my Post Cards added with get api and more changes
This commit is contained in:
@@ -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>
|
||||
|
||||
BIN
assets/images/empty_postcard_drafts.png
Normal file
BIN
assets/images/empty_postcard_drafts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
BIN
assets/images/empty_postcard_orders.png
Normal file
BIN
assets/images/empty_postcard_orders.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
73
lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart
Normal file
73
lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
18
lib/cart/blocs/myPassCart/my_pass_cart_event.dart
Normal file
18
lib/cart/blocs/myPassCart/my_pass_cart_event.dart
Normal 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();
|
||||
}
|
||||
43
lib/cart/blocs/myPassCart/my_pass_cart_state.dart
Normal file
43
lib/cart/blocs/myPassCart/my_pass_cart_state.dart
Normal 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];
|
||||
}
|
||||
35
lib/cart/repository/my_pass_cart_repository.dart
Normal file
35
lib/cart/repository/my_pass_cart_repository.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
486
lib/cart/views/my_pass_cart_page_view.dart
Normal file
486
lib/cart/views/my_pass_cart_page_view.dart
Normal 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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
0
lib/checkout/bloc/allCoupons/all_coupons_state.dart
Normal file
0
lib/checkout/bloc/allCoupons/all_coupons_state.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
61
lib/checkout/models/all_coupons_model.dart
Normal file
61
lib/checkout/models/all_coupons_model.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
0
lib/checkout/repository/all_coupons_repository.dart
Normal file
0
lib/checkout/repository/all_coupons_repository.dart
Normal 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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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'] ?? {},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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+',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
''');
|
||||
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
201
lib/postcard/blocs/myPostCards/my_postcard_bloc.dart
Normal file
201
lib/postcard/blocs/myPostCards/my_postcard_bloc.dart
Normal 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',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/postcard/blocs/myPostCards/my_postcard_event.dart
Normal file
33
lib/postcard/blocs/myPostCards/my_postcard_event.dart
Normal 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();
|
||||
}
|
||||
76
lib/postcard/blocs/myPostCards/my_postcard_state.dart
Normal file
76
lib/postcard/blocs/myPostCards/my_postcard_state.dart
Normal 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];
|
||||
}
|
||||
243
lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart
Normal file
243
lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart
Normal 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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
136
lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart
Normal file
136
lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
173
lib/postcard/models/my_postcard_model.dart
Normal file
173
lib/postcard/models/my_postcard_model.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
20
lib/postcard/repository/my_postcard_repository.dart
Normal file
20
lib/postcard/repository/my_postcard_repository.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
205
lib/postcard/repository/postcard_checkout_repository.dart
Normal file
205
lib/postcard/repository/postcard_checkout_repository.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
290
lib/postcard/views/my_postcard_drafts_view.dart
Normal file
290
lib/postcard/views/my_postcard_drafts_view.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
385
lib/postcard/views/my_postcard_orders_view.dart
Normal file
385
lib/postcard/views/my_postcard_orders_view.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
438
lib/postcard/views/my_postcard_preview_view.dart
Normal file
438
lib/postcard/views/my_postcard_preview_view.dart
Normal 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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
491
lib/postcard/views/my_postcards_view.dart
Normal file
491
lib/postcard/views/my_postcards_view.dart
Normal 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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/profile/bloc/contactUs/contact_us_bloc.dart
Normal file
42
lib/profile/bloc/contactUs/contact_us_bloc.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
lib/profile/bloc/contactUs/contact_us_event.dart
Normal file
34
lib/profile/bloc/contactUs/contact_us_event.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
34
lib/profile/bloc/contactUs/contact_us_state.dart
Normal file
34
lib/profile/bloc/contactUs/contact_us_state.dart
Normal 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];
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
32
lib/profile/repository/contact_us_repository.dart
Normal file
32
lib/profile/repository/contact_us_repository.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
271
lib/profile/view/contact_us/contact_us_view.dart
Normal file
271
lib/profile/view/contact_us/contact_us_view.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user