added apply coupon with api intigration and more fixes

This commit is contained in:
mystery012728
2026-02-05 19:35:01 +05:30
parent c2ffc9d9a7
commit a7548ccebd
22 changed files with 1203 additions and 812 deletions

View File

@@ -35,10 +35,16 @@ android {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
flutter {
source = "../.."
}
}

15
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,15 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# Keep Stripe Push Provisioning classes
-keep class com.stripe.android.pushProvisioning.** { *; }
-dontwarn com.stripe.android.pushProvisioning.**
# Keep Stripe SDK
-keep class com.stripe.android.** { *; }
-dontwarn com.stripe.android.**
# Keep React Native Stripe SDK
-keep class com.reactnativestripesdk.** { *; }
-dontwarn com.reactnativestripesdk.**

View File

@@ -0,0 +1,25 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/all_coupons_repository.dart';
import 'all_coupons_event.dart';
import 'all_coupons_state.dart';
class AllCouponsBloc extends Bloc<AllCouponsEvent, AllCouponsState> {
final AllCouponsRepository repository;
AllCouponsBloc({required this.repository}) : super(AllCouponsInitialState()) {
on<FetchAllCouponsEvent>(_onFetchAllCoupons);
}
Future<void> _onFetchAllCoupons(
FetchAllCouponsEvent event,
Emitter<AllCouponsState> emit,
) async {
emit(CouponsLoadingState());
try {
final coupons = await repository.fetchAllCoupons();
emit(CouponsLoadedState(coupons: coupons));
} catch (e) {
emit(CouponsErrorState(error: e.toString()));
}
}
}

View File

@@ -0,0 +1,3 @@
abstract class AllCouponsEvent {}
class FetchAllCouponsEvent extends AllCouponsEvent {}

View File

@@ -0,0 +1,19 @@
import '../../models/all_coupons_model.dart';
abstract class AllCouponsState {}
class AllCouponsInitialState extends AllCouponsState {}
class CouponsLoadingState extends AllCouponsState {}
class CouponsLoadedState extends AllCouponsState {
final List<AllCouponsModel> coupons;
CouponsLoadedState({required this.coupons});
}
class CouponsErrorState extends AllCouponsState {
final String error;
CouponsErrorState({required this.error});
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/all_coupons_repository.dart';
import 'checkout_event.dart';
import 'checkout_state.dart';
class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
final AllCouponsRepository repository;
CheckoutBloc({required this.repository}) : super(CheckoutInitialState()) {
on<FetchCheckoutCouponsEvent>(_onFetchCheckoutCoupons);
on<ApplyCouponEvent>(_onApplyCoupon);
on<RemoveCouponEvent>(_onRemoveCoupon);
}
Future<void> _onFetchCheckoutCoupons(
FetchCheckoutCouponsEvent event,
Emitter<CheckoutState> emit,
) async {
emit(CheckoutCouponsLoadingState());
try {
final coupons = await repository.fetchAllCoupons();
emit(CheckoutCouponsLoadedState(coupons: coupons));
} catch (e) {
emit(CheckoutCouponsErrorState(error: e.toString()));
}
}
void _onApplyCoupon(
ApplyCouponEvent event,
Emitter<CheckoutState> emit,
) {
if (state is CheckoutCouponsLoadedState) {
final currentState = state as CheckoutCouponsLoadedState;
emit(currentState.copyWith(appliedCoupon: event.coupon));
}
}
void _onRemoveCoupon(
RemoveCouponEvent event,
Emitter<CheckoutState> emit,
) {
if (state is CheckoutCouponsLoadedState) {
final currentState = state as CheckoutCouponsLoadedState;
emit(currentState.copyWith(clearAppliedCoupon: true));
}
}
}

View File

@@ -0,0 +1,12 @@
import '../../models/all_coupons_model.dart';
abstract class CheckoutEvent {}
class FetchCheckoutCouponsEvent extends CheckoutEvent {}
class ApplyCouponEvent extends CheckoutEvent {
final AllCouponsModel coupon;
ApplyCouponEvent({required this.coupon});
}
class RemoveCouponEvent extends CheckoutEvent {}

View File

@@ -0,0 +1,33 @@
import '../../models/all_coupons_model.dart';
abstract class CheckoutState {}
class CheckoutInitialState extends CheckoutState {}
class CheckoutCouponsLoadingState extends CheckoutState {}
class CheckoutCouponsLoadedState extends CheckoutState {
final List<AllCouponsModel> coupons;
final AllCouponsModel? appliedCoupon;
CheckoutCouponsLoadedState({
required this.coupons,
this.appliedCoupon,
});
CheckoutCouponsLoadedState copyWith({
List<AllCouponsModel>? coupons,
AllCouponsModel? appliedCoupon,
bool clearAppliedCoupon = false,
}) {
return CheckoutCouponsLoadedState(
coupons: coupons ?? this.coupons,
appliedCoupon: clearAppliedCoupon ? null : (appliedCoupon ?? this.appliedCoupon),
);
}
}
class CheckoutCouponsErrorState extends CheckoutState {
final String error;
CheckoutCouponsErrorState({required this.error});
}

View File

@@ -0,0 +1,16 @@
import 'package:citycards_customer/localPreference/local_preference.dart';
import '../models/all_coupons_model.dart';
import '../../networkApiServices/network_api_services.dart';
import '../../networkApiServices/api_urls.dart';
class AllCouponsRepository {
final NetworkApiService _apiService = NetworkApiService();
Future<List<AllCouponsModel>> fetchAllCoupons() async {
final int cityXid = await LocalPreference.getSelectedCityId();
final response = await _apiService.getApi(
url: '${ApiUrls.coupons}?cityXid=$cityXid',
);
final List<dynamic> data = response.data as List;
return data.map((json) => AllCouponsModel.fromJson(json)).toList();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,142 +1,174 @@
import 'package:citycards_customer/postcard/widgets/purchase_details_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:citycards_customer/common_packages/custom_text.dart';
import '../bloc/allCoupons/all_coupons_bloc.dart';
import '../bloc/allCoupons/all_coupons_event.dart';
import '../bloc/allCoupons/all_coupons_state.dart';
import '../repository/all_coupons_repository.dart';
class AllCouponsBottomsheet extends StatelessWidget {
AllCouponsBottomsheet({super.key});
final Function(dynamic coupon)? onCouponSelected;
final List<Map<String, String>> coupons = [
{
"text": "Flat 3% cashback using Amazon Pay Balance",
"coupon_code": "AMZNPAY3",
},
{
"text": "Flat 3% cashback using Amazon Pay Balance",
"coupon_code": "AMZNPAY3",
},
{
"text": "Flat 3% cashback using Amazon Pay Balance",
"coupon_code": "AMZNPAY3",
},
{
"text": "Flat 3% cashback using Amazon Pay Balance",
"coupon_code": "AMZNPAY3",
},
];
const AllCouponsBottomsheet({
super.key,
this.onCouponSelected,
});
@override
Widget build(BuildContext context) {
return AnimatedPadding(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
padding: EdgeInsets.only(
top: 24.h,
left: 20.w,
right: 20.w,
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// --- Header ---
Container(
height: 4.h,
width: 40.w,
decoration: BoxDecoration(
color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
return BlocProvider(
create: (context) => AllCouponsBloc(repository: AllCouponsRepository())
..add(FetchAllCouponsEvent()),
child: AnimatedPadding(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
padding: EdgeInsets.only(
top: 24.h,
left: 20.w,
right: 20.w,
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
/// --- Header ---
Container(
height: 4.h,
width: 40.w,
decoration: BoxDecoration(
color: Color(0xFF2D3134),
borderRadius: BorderRadius.circular(4.r),
),
),
),
SizedBox(height: 12.h),
CustomText(text: "All Coupons", size: 18.sp, weight: FontWeight.w500),
SizedBox(height: 22.h),
SizedBox(height: 12.h),
CustomText(
text: "All Coupons", size: 18.sp, weight: FontWeight.w500),
SizedBox(height: 22.h),
/// --- Coupon list ---
Flexible(
child: ListView.separated(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemCount: coupons.length,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final coupon = coupons[index];
return Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: const Color(0xFFF95F62).withOpacity(0.12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 220.w,
child: CustomText(
text: coupon['text'] ?? "",
size: 12.sp,
weight: FontWeight.w400,
/// --- Coupon list ---
Flexible(
child: BlocBuilder<AllCouponsBloc, AllCouponsState>(
builder: (context, state) {
if (state is CouponsLoadingState) {
return Center(
child: CircularProgressIndicator(
color: Color(0xFFF95F62),
),
);
} else if (state is CouponsErrorState) {
return Center(
child: CustomText(
text: "Error: ${state.error}",
size: 14.sp,
color: Colors.red,
),
);
} else if (state is CouponsLoadedState) {
if (state.coupons.isEmpty) {
return Center(
child: CustomText(
text: "No coupons available",
size: 14.sp,
),
);
}
return ListView.separated(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemCount: state.coupons.length,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
final coupon = state.coupons[index];
return Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: const Color(0xFFF95F62).withOpacity(0.12),
),
),
GestureDetector(
onTap: () {
Navigator.pop(context);
PurchaseDetailsBottomSheet.show(context);
},
child: Container(
width: 110.w,
height: 44.h,
decoration: BoxDecoration(
color: Color(0xFFF95F62),
borderRadius: BorderRadius.circular(12.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 220.w,
child: CustomText(
text: "${coupon.discountPercent}% discount on ${coupon.title}",
size: 12.sp,
weight: FontWeight.w400,
),
),
GestureDetector(
onTap: () {
// Pass the selected coupon back to checkout view
if (onCouponSelected != null) {
onCouponSelected!(coupon);
}
Navigator.pop(context);
},
child: Container(
width: 110.w,
height: 44.h,
decoration: BoxDecoration(
color: Color(0xFFF95F62),
borderRadius:
BorderRadius.circular(12.r),
),
child: Center(
child: CustomText(
text: "Apply Coupon",
size: 12.sp,
color: Colors.white,
),
),
),
),
],
),
child: Center(
child: CustomText(
text: "Apply Coupon",
size: 12.sp,
color: Colors.white,
SizedBox(height: 8.h),
Container(
height: 32.h,
width: 83.w,
decoration: BoxDecoration(
color:
Color(0xFFF95F62).withOpacity(0.12),
border: Border.all(color: Color(0xFFF95F62)),
borderRadius: BorderRadius.circular(6.r),
),
child: Center(
child: CustomText(
text: coupon.couponCode,
size: 12.sp,
weight: FontWeight.w400,
color: Color(0xFFF95F62),
),
),
),
),
],
),
],
),
SizedBox(height: 8.h),
Container(
height: 32.h,
width: 83.w,
decoration: BoxDecoration(
color: Color(0xFFF95F62).withOpacity(0.12),
border: Border.all(color: Color(0xFFF95F62)),
);
},
);
}
borderRadius: BorderRadius.circular(6.r),
),
child: Center(
child: CustomText(
text: coupon['coupon_code'] ?? "",
size: 12.sp,
weight: FontWeight.w400,
color: Color(0xFFF95F62),
),
),
),
],
),
);
},
return SizedBox.shrink();
},
),
),
),
],
],
),
),
);
}
}
}

View File

@@ -16,6 +16,7 @@ class ApiUrls {
static const buyAPass = "$baseUrl/mobile/pass";
static const offersDetails = "$baseUrl/mobile/list/offers";
static const myPostCards = "$baseUrl/mobile/postcards/all";
static const coupons = "$baseUrl/mobile/passes/dropdown/card";
//Post Apis

View File

@@ -208,6 +208,7 @@ class NetworkApiService {
// TODO: navigate to login screen
}
// ================= ERROR HANDLER =================
// ================= ERROR HANDLER =================
String _handleError(DioException error) {
switch (error.type) {
@@ -220,8 +221,29 @@ class NetworkApiService {
case DioExceptionType.badCertificate:
return "Bad certificate.";
case DioExceptionType.badResponse:
return error.response?.data['message'] ??
"Invalid status code: ${error.response?.statusCode}";
// 🔥 FIXED: Safely handle different response data types
try {
final responseData = error.response?.data;
// If it's a Map, try to get the message
if (responseData is Map<String, dynamic>) {
return responseData['message'] ??
responseData['error'] ??
"Invalid status code: ${error.response?.statusCode}";
}
// If it's a String, return it directly
if (responseData is String) {
return responseData.isNotEmpty
? responseData
: "Invalid status code: ${error.response?.statusCode}";
}
// For any other type, return generic error
return "Invalid status code: ${error.response?.statusCode}";
} catch (e) {
return "Invalid status code: ${error.response?.statusCode}";
}
case DioExceptionType.cancel:
return "Request was cancelled.";
case DioExceptionType.connectionError:

View File

@@ -12,48 +12,91 @@ class PostcardCreationBloc
extends Bloc<PostcardCreationEvent, PostcardCreationState> {
final ImagePicker _picker = ImagePicker();
// 🆕 Image size limit: 10 MB in bytes
static const int maxImageSizeInBytes = 10 * 1024 * 1024; // 10 MB
PostcardCreationBloc()
: super(
const PostcardCreationState(currentStep: PostcardStep.uploadPhoto),
) {
: super(
const PostcardCreationState(currentStep: PostcardStep.uploadPhoto),
) {
/* Navigation steps */
on<GoToNextStep>((event, emit) {
final next =
PostcardStep.values[(state.currentStep.index + 1).clamp(
0,
PostcardStep.values.length - 1,
)];
emit(state.copyWith(currentStep: next));
on<GoToNextStep>((event, emit) async {
// 🆕 Validate image size before going to next step
if (state.currentStep == PostcardStep.uploadPhoto && state.imagePath != null) {
final file = File(state.imagePath!);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB.",
));
return; // Don't proceed to next step
}
}
// Clear any previous errors and proceed
final next = PostcardStep.values[(state.currentStep.index + 1).clamp(
0,
PostcardStep.values.length - 1,
)];
emit(state.copyWith(currentStep: next, errorMessage: null));
});
/* Go to previous step */
on<GoToPreviousStep>((event, emit) {
final prev =
PostcardStep.values[(state.currentStep.index - 1).clamp(
0,
PostcardStep.values.length - 1,
)];
emit(state.copyWith(currentStep: prev));
final prev = PostcardStep.values[(state.currentStep.index - 1).clamp(
0,
PostcardStep.values.length - 1,
)];
emit(state.copyWith(currentStep: prev, errorMessage: null));
});
/* Upload image */
on<UploadImage>((event, emit) {
on<UploadImage>((event, emit) async {
// 🆕 Validate image size
final file = File(event.imagePath);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB.",
));
return;
}
emit(
state.copyWith(
imagePath: event.imagePath,
originalImagePath: event.imagePath,
errorMessage: null, // Clear any previous errors
),
);
});
/* Pick image from galley */
/* Pick image from gallery */
on<PickImageFromGallery>((event, emit) async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
// 🆕 Validate image size
final file = File(pickedFile.path);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB. Please select a smaller image.",
));
return;
}
emit(
state.copyWith(
imagePath: pickedFile.path,
originalImagePath: pickedFile.path,
errorMessage: null, // Clear any previous errors
),
);
}
@@ -63,15 +106,33 @@ class PostcardCreationBloc
on<PickImageFromCamera>((event, emit) async {
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
if (pickedFile != null) {
// 🆕 Validate image size
final file = File(pickedFile.path);
final fileSize = await file.length();
if (fileSize > maxImageSizeInBytes) {
final sizeInMB = (fileSize / (1024 * 1024)).toStringAsFixed(2);
emit(state.copyWith(
errorMessage: "Image size is too large ($sizeInMB MB). Maximum allowed size is 10 MB. Please try taking a photo with lower quality.",
));
return;
}
emit(
state.copyWith(
imagePath: pickedFile.path,
originalImagePath: pickedFile.path,
errorMessage: null, // Clear any previous errors
),
);
}
});
// 🆕 NEW: Clear error handler
on<ClearError>((event, emit) {
emit(state.copyWith(errorMessage: null));
});
on<UpdatePurchaseFormData>((event, emit) {
emit(state.copyWith(
pcTitle: event.pcTitle,
@@ -95,7 +156,6 @@ class PostcardCreationBloc
emit(
state.copyWith(
imagePath: state.originalImagePath,
// revert to the untouched original
filter: "none",
isProcessing: false,
),
@@ -107,7 +167,6 @@ class PostcardCreationBloc
emit(state.copyWith(isProcessing: true));
try {
// Always base filters on the ORIGINAL image, not the last filtered one
final originalFile = File(state.originalImagePath!);
final bytes = await originalFile.readAsBytes();
img.Image? image = img.decodeImage(bytes);
@@ -152,7 +211,7 @@ class PostcardCreationBloc
return;
}
// 5⃣ Save filtered image to a new temporary file
// 5⃣ Save filtered image
final filteredFile = File(
"${originalFile.parent.path}/filtered_${event.filterName}.jpg",
)..writeAsBytesSync(img.encodeJpg(image, quality: 95));
@@ -179,8 +238,13 @@ class PostcardCreationBloc
emit(state.copyWith(selectedFont: event.fontName));
});
// Add this handler in the constructor after other handlers
on<UpdatePostcardNumber>((event, emit) {
emit(state.copyWith(pcNumber: event.pcNumber));
});
on<TogglePurchaseOption>((event, emit) {
emit(state.copyWith(isGift: event.isGift));
});
}
}
}

View File

@@ -31,12 +31,12 @@ class ChangeFontStyle extends PostcardCreationEvent {
ChangeFontStyle(this.fontName);
}
class TogglePurchaseOption extends PostcardCreationEvent {
final bool isGift;
TogglePurchaseOption(this.isGift);
}
class UpdatePurchaseFormData extends PostcardCreationEvent {
final String? pcTitle;
final String? fullName;
@@ -57,4 +57,13 @@ class UpdatePurchaseFormData extends PostcardCreationEvent {
this.state,
this.zipCode,
});
}
// 🆕 NEW: Clear error message
class ClearError extends PostcardCreationEvent {}
// 🆕 ADD THIS EVENT
class UpdatePostcardNumber extends PostcardCreationEvent {
final String pcNumber;
UpdatePostcardNumber(this.pcNumber);
}

View File

@@ -9,8 +9,8 @@ class PostcardCreationState {
final bool isGift;
final bool isProcessing;
final String? selectedFont;
final String? errorMessage;
// Add these new fields
final String? pcTitle;
final String? fullName;
final String? emailId;
@@ -19,6 +19,7 @@ class PostcardCreationState {
final String? country;
final String? state;
final String? zipCode;
final String? pcNumber; // 🆕 ADD THIS
const PostcardCreationState({
required this.currentStep,
@@ -29,6 +30,7 @@ class PostcardCreationState {
this.isGift = false,
this.isProcessing = false,
this.selectedFont,
this.errorMessage,
this.pcTitle,
this.fullName,
this.emailId,
@@ -37,6 +39,7 @@ class PostcardCreationState {
this.country,
this.state,
this.zipCode,
this.pcNumber, // 🆕 ADD THIS
});
PostcardCreationState copyWith({
@@ -48,6 +51,7 @@ class PostcardCreationState {
bool? isGift,
bool? isProcessing,
String? selectedFont,
String? errorMessage,
String? pcTitle,
String? fullName,
String? emailId,
@@ -56,6 +60,7 @@ class PostcardCreationState {
String? country,
String? state,
String? zipCode,
String? pcNumber, // 🆕 ADD THIS
}) {
return PostcardCreationState(
currentStep: currentStep ?? this.currentStep,
@@ -66,6 +71,7 @@ class PostcardCreationState {
isGift: isGift ?? this.isGift,
isProcessing: isProcessing ?? this.isProcessing,
selectedFont: selectedFont ?? this.selectedFont,
errorMessage: errorMessage,
pcTitle: pcTitle ?? this.pcTitle,
fullName: fullName ?? this.fullName,
emailId: emailId ?? this.emailId,
@@ -74,6 +80,7 @@ class PostcardCreationState {
country: country ?? this.country,
state: state ?? this.state,
zipCode: zipCode ?? this.zipCode,
pcNumber: pcNumber ?? this.pcNumber, // 🆕 ADD THIS
);
}
}

View File

@@ -51,13 +51,13 @@ class OrderSuccessPageView extends StatelessWidget {
fontWeight: FontWeight.w400,
color: const Color(0xff585858),
),
children: const [
TextSpan(
children: [
const TextSpan(
text: "Your order has been placed. Your order\nid is ",
),
TextSpan(
text: "#AG74563",
style: TextStyle(
text: "#${state.pcNumber ?? 'N/A'}", // 🆕 USE DYNAMIC VALUE
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xff585858),
),

View File

@@ -315,6 +315,11 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
return BlocConsumer<PostcardCheckoutBloc, PostcardCheckoutState>(
listener: (context, checkoutState) {
if (checkoutState.isSuccess && !checkoutState.isDraft) {
if (checkoutState.pcNumber != null) {
context.read<PostcardCreationBloc>().add(
UpdatePostcardNumber(checkoutState.pcNumber!),
);
}
// 🆕 Payment flow: Check if we have clientSecret
if (checkoutState.clientSecret != null && checkoutState.clientSecret!.isNotEmpty) {
// Initiate Stripe payment with clientSecret
@@ -440,7 +445,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
"Subtotal", "\$ ${widget.baseAmount.toStringAsFixed(2)}"),
const SizedBox(height: 20),
_buildPaymentRow(
"Tax", "\$ ${widget.totalTaxAmount.toStringAsFixed(2)}",
"Discount", "\$ ${widget.totalTaxAmount.toStringAsFixed(2)}",
highlight: true),
const SizedBox(height: 8),
Divider(color: Colors.black),

View File

@@ -59,9 +59,9 @@ class PostcardCreationPage extends StatelessWidget {
mobileNumber: state.phoneNumber ?? 'N/A',
isdCode: '+91',
isForSelf: !state.isGift,
totalTaxAmount: 0.5,
baseAmount: 10,
totalAmount: 10.5,
totalTaxAmount: 20,
baseAmount: 50,
totalAmount: 30,
),
);
break;

View File

@@ -369,8 +369,7 @@ class _PostcardPurchaseFormPageViewState extends State<PostcardPurchaseFormPageV
),
),
items: const [
DropdownMenuItem(value: "India", child: Text("India")),
DropdownMenuItem(value: "USA", child: Text("USA")),
DropdownMenuItem(value: "Lorem Ipsum", child: Text("Lorem Ipsum")),
// Add more items as needed
],
onChanged: onChanged,

View File

@@ -15,228 +15,245 @@ class UploadPhotoStepPageView extends StatelessWidget {
@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,),
StepProgressBar(totalSteps: 4, currentStep: 1),
const SizedBox(height: 24),
Text(
"Upload a photo",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
Text(
"Design your own unique postcards to cherish your unforgettable moments.",
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff2D3134),
),
),
const SizedBox(height: 30),
if (state.imagePath != null)
Container(
height: 300.h,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: const Color(0xFFFFF5F5),
image: DecorationImage(
image: FileImage(File(state.imagePath!)),
fit: BoxFit.cover,
),
),
)
else
GestureDetector(
onTap: () => bloc.add(PickImageFromGallery()),
child: const DottedBorderContainer(),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
width: MediaQuery.of(context).size.width / 2 - 40,
height: 1.5,
color: Color(0xffD9D9D9),
),
Text(
"OR",
style: TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
Container(
width: MediaQuery.of(context).size.width / 2 - 40,
height: 1.5,
color: Color(0xffD9D9D9),
),
],
),
const SizedBox(height: 12),
if(state.imagePath == null)
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => bloc.add(PickImageFromCamera()),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Take a photo",
style: TextStyle(
color: Color(0xffF95F62),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 8),
Icon(
Icons.camera_alt_outlined,
color: Color(0xffF95F62),
),
],
),
),
),
],
),
if(state.imagePath != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: OutlinedButton(
onPressed: () => bloc.add(PickImageFromCamera()),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Take a photo",
style: TextStyle(
color: Color(0xffF95F62),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 8),
Icon(
Icons.camera_alt_outlined,
color: Color(0xffF95F62),
),
],
),
),
),
const SizedBox(width: 16), // spacing between buttons
// 🖼️ Upload Photo button
Expanded(
child: OutlinedButton(
onPressed: () => bloc.add(PickImageFromGallery()),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Upload again",
style: TextStyle(
color: Color(0xffF95F62),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 8),
Icon(Icons.refresh, color: Color(0xffF95F62)),
],
),
),
),
],
),
SizedBox(height: 30.h),
if(state.imagePath != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
onPressed: () {
final bloc = context.read<PostcardCreationBloc>();
if (bloc.state.imagePath != null) {
bloc.add(GoToNextStep());
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Please upload an image first")),
);
}
// Navigator.of(context).pushNamed(RouteConstants.addFilterPage);
// Navigator.of(context).pushNamed(RouteConstants.);
},
child: Text(
"Next",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
],
return BlocListener<PostcardCreationBloc, PostcardCreationState>(
// 🆕 Listen for error messages
listener: (context, state) {
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage!),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {
context.read<PostcardCreationBloc>().add(ClearError());
},
),
),
),
);
);
// Clear error after showing snackbar
Future.delayed(const Duration(seconds: 4), () {
context.read<PostcardCreationBloc>().add(ClearError());
});
}
},
child: 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,),
StepProgressBar(totalSteps: 4, currentStep: 1),
const SizedBox(height: 24),
Text(
"Upload a photo",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
Text(
"Design your own unique postcards to cherish your unforgettable moments.",
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: const Color(0xff2D3134),
),
),
const SizedBox(height: 30),
if (state.imagePath != null)
Container(
height: 300.h,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: const Color(0xFFFFF5F5),
image: DecorationImage(
image: FileImage(File(state.imagePath!)),
fit: BoxFit.cover,
),
),
)
else
GestureDetector(
onTap: () => bloc.add(PickImageFromGallery()),
child: const DottedBorderContainer(),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
width: MediaQuery.of(context).size.width / 2 - 40,
height: 1.5,
color: Color(0xffD9D9D9),
),
Text(
"OR",
style: TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
Container(
width: MediaQuery.of(context).size.width / 2 - 40,
height: 1.5,
color: Color(0xffD9D9D9),
),
],
),
const SizedBox(height: 12),
if(state.imagePath == null)
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => bloc.add(PickImageFromCamera()),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Take a photo",
style: TextStyle(
color: Color(0xffF95F62),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 8),
Icon(
Icons.camera_alt_outlined,
color: Color(0xffF95F62),
),
],
),
),
),
],
),
if(state.imagePath != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: OutlinedButton(
onPressed: () => bloc.add(PickImageFromCamera()),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Take a photo",
style: TextStyle(
color: Color(0xffF95F62),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 8),
Icon(
Icons.camera_alt_outlined,
color: Color(0xffF95F62),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: () => bloc.add(PickImageFromGallery()),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
side: const BorderSide(color: Color(0xffF95F62)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
"Upload again",
style: TextStyle(
color: Color(0xffF95F62),
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 8),
Icon(Icons.refresh, color: Color(0xffF95F62)),
],
),
),
),
],
),
SizedBox(height: 30.h),
if(state.imagePath != null)
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
onPressed: () {
// 🆕 Just trigger GoToNextStep - validation happens in bloc
bloc.add(GoToNextStep());
},
child: Text(
"Next",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
},
),
);
}
}
}