added apply coupon with api intigration and more fixes
This commit is contained in:
@@ -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
15
android/app/proguard-rules.pro
vendored
Normal 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.**
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
abstract class AllCouponsEvent {}
|
||||
|
||||
class FetchAllCouponsEvent extends AllCouponsEvent {}
|
||||
@@ -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});
|
||||
}
|
||||
47
lib/checkout/bloc/checkOut/checkout_bloc.dart
Normal file
47
lib/checkout/bloc/checkOut/checkout_bloc.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
12
lib/checkout/bloc/checkOut/checkout_event.dart
Normal file
12
lib/checkout/bloc/checkOut/checkout_event.dart
Normal 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 {}
|
||||
33
lib/checkout/bloc/checkOut/checkout_state.dart
Normal file
33
lib/checkout/bloc/checkOut/checkout_state.dart
Normal 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});
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user