diff --git a/android/.kotlin/sessions/kotlin-compiler-14452277761891113509.salive b/android/.kotlin/sessions/kotlin-compiler-14452277761891113509.salive new file mode 100644 index 0000000..e69de29 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6d76d97..243f6fd 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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 = "../.." -} +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..d5944e4 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -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.** \ No newline at end of file diff --git a/lib/checkout/bloc/allCoupons/all_coupons_bloc.dart b/lib/checkout/bloc/allCoupons/all_coupons_bloc.dart index e69de29..ca809cc 100644 --- a/lib/checkout/bloc/allCoupons/all_coupons_bloc.dart +++ b/lib/checkout/bloc/allCoupons/all_coupons_bloc.dart @@ -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 { + final AllCouponsRepository repository; + + AllCouponsBloc({required this.repository}) : super(AllCouponsInitialState()) { + on(_onFetchAllCoupons); + } + + Future _onFetchAllCoupons( + FetchAllCouponsEvent event, + Emitter emit, + ) async { + emit(CouponsLoadingState()); + try { + final coupons = await repository.fetchAllCoupons(); + emit(CouponsLoadedState(coupons: coupons)); + } catch (e) { + emit(CouponsErrorState(error: e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/checkout/bloc/allCoupons/all_coupons_event.dart b/lib/checkout/bloc/allCoupons/all_coupons_event.dart index e69de29..eee842f 100644 --- a/lib/checkout/bloc/allCoupons/all_coupons_event.dart +++ b/lib/checkout/bloc/allCoupons/all_coupons_event.dart @@ -0,0 +1,3 @@ +abstract class AllCouponsEvent {} + +class FetchAllCouponsEvent extends AllCouponsEvent {} \ No newline at end of file diff --git a/lib/checkout/bloc/allCoupons/all_coupons_state.dart b/lib/checkout/bloc/allCoupons/all_coupons_state.dart index e69de29..f1c4807 100644 --- a/lib/checkout/bloc/allCoupons/all_coupons_state.dart +++ b/lib/checkout/bloc/allCoupons/all_coupons_state.dart @@ -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 coupons; + + CouponsLoadedState({required this.coupons}); +} + +class CouponsErrorState extends AllCouponsState { + final String error; + + CouponsErrorState({required this.error}); +} \ No newline at end of file diff --git a/lib/checkout/bloc/checkOut/checkout_bloc.dart b/lib/checkout/bloc/checkOut/checkout_bloc.dart new file mode 100644 index 0000000..b726984 --- /dev/null +++ b/lib/checkout/bloc/checkOut/checkout_bloc.dart @@ -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 { + final AllCouponsRepository repository; + + CheckoutBloc({required this.repository}) : super(CheckoutInitialState()) { + on(_onFetchCheckoutCoupons); + on(_onApplyCoupon); + on(_onRemoveCoupon); + } + + Future _onFetchCheckoutCoupons( + FetchCheckoutCouponsEvent event, + Emitter 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 emit, + ) { + if (state is CheckoutCouponsLoadedState) { + final currentState = state as CheckoutCouponsLoadedState; + emit(currentState.copyWith(appliedCoupon: event.coupon)); + } + } + + void _onRemoveCoupon( + RemoveCouponEvent event, + Emitter emit, + ) { + if (state is CheckoutCouponsLoadedState) { + final currentState = state as CheckoutCouponsLoadedState; + emit(currentState.copyWith(clearAppliedCoupon: true)); + } + } +} \ No newline at end of file diff --git a/lib/checkout/bloc/checkOut/checkout_event.dart b/lib/checkout/bloc/checkOut/checkout_event.dart new file mode 100644 index 0000000..df9cc8a --- /dev/null +++ b/lib/checkout/bloc/checkOut/checkout_event.dart @@ -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 {} \ No newline at end of file diff --git a/lib/checkout/bloc/checkOut/checkout_state.dart b/lib/checkout/bloc/checkOut/checkout_state.dart new file mode 100644 index 0000000..e1f9af8 --- /dev/null +++ b/lib/checkout/bloc/checkOut/checkout_state.dart @@ -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 coupons; + final AllCouponsModel? appliedCoupon; + + CheckoutCouponsLoadedState({ + required this.coupons, + this.appliedCoupon, + }); + + CheckoutCouponsLoadedState copyWith({ + List? 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}); +} \ No newline at end of file diff --git a/lib/checkout/repository/all_coupons_repository.dart b/lib/checkout/repository/all_coupons_repository.dart index e69de29..208c0c0 100644 --- a/lib/checkout/repository/all_coupons_repository.dart +++ b/lib/checkout/repository/all_coupons_repository.dart @@ -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> fetchAllCoupons() async { + final int cityXid = await LocalPreference.getSelectedCityId(); + final response = await _apiService.getApi( + url: '${ApiUrls.coupons}?cityXid=$cityXid', + ); + final List data = response.data as List; + return data.map((json) => AllCouponsModel.fromJson(json)).toList(); + } +} diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index 83e18be..d651d6f 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -1,3 +1,6 @@ +import 'package:citycards_customer/checkout/bloc/checkOut/checkout_bloc.dart'; +import 'package:citycards_customer/checkout/bloc/checkOut/checkout_event.dart'; +import 'package:citycards_customer/checkout/bloc/checkOut/checkout_state.dart'; import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart'; import 'package:citycards_customer/login/view/login_email_bottomsheet.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; @@ -6,10 +9,13 @@ 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_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../../StripePayment/view/stripe_payment.dart'; import '../../buy_a_pass/models/checkout_model.dart'; import '../../localPreference/local_preference.dart'; import '../widget/pass_purchase_details_bottomsheet.dart'; +import '../repository/all_coupons_repository.dart'; +import '../models/all_coupons_model.dart'; class CheckoutView extends StatefulWidget { const CheckoutView({super.key}); @@ -19,9 +25,6 @@ class CheckoutView extends StatefulWidget { } class _CheckoutViewState extends State { - // For coupon/discount management - String? appliedCouponCode; - double discountPercentage = 0.0; bool isPurchaseDetailsConfirmed = false; @override @@ -76,488 +79,544 @@ class _CheckoutViewState extends State { ); } - // ✅ Calculate pricing - final double subtotal = checkoutData.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; + return BlocProvider( + create: (context) => CheckoutBloc(repository: AllCouponsRepository()) + ..add(FetchCheckoutCouponsEvent()), + child: _CheckoutContent( + checkoutData: checkoutData, + isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed, + onPurchaseDetailsChanged: (value) { + setState(() { + isPurchaseDetailsConfirmed = value; + }); + }, + ), + ); + } +} - return Scaffold( - resizeToAvoidBottomInset: true, - backgroundColor: Colors.white, - body: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Column( - children: [ - // ✅ App Bar - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showCart: false, - showDivider: true, - ), +class _CheckoutContent extends StatelessWidget { + final CheckoutData checkoutData; + final bool isPurchaseDetailsConfirmed; + final Function(bool) onPurchaseDetailsChanged; - // ✅ Back Button & Title - Row( + const _CheckoutContent({ + required this.checkoutData, + required this.isPurchaseDetailsConfirmed, + required this.onPurchaseDetailsChanged, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + // ✅ Calculate pricing + double discountPercentage = 0.0; + AllCouponsModel? appliedCoupon; + + if (state is CheckoutCouponsLoadedState && state.appliedCoupon != null) { + appliedCoupon = state.appliedCoupon; + discountPercentage = appliedCoupon!.discountPercent.toDouble(); + } + + final double subtotal = checkoutData.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; + + return Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Column( children: [ - GestureDetector( - onTap: () => Navigator.pop(context), - child: const Icon(Icons.arrow_back), + // ✅ App Bar + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showCart: false, + showDivider: true, ), - SizedBox(width: 8.w), - CustomText(text: "Checkout", size: 12.sp), - ], - ), - SizedBox(height: 22.h), - - // ✅ PASS CARD SECTION (showing pass details) - Container( - height: 140.h, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: checkoutData.themeColor.withOpacity(0.2), + // ✅ Back Button & Title + Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon(Icons.arrow_back), + ), + SizedBox(width: 8.w), + CustomText(text: "Checkout", size: 12.sp), + ], ), - borderRadius: BorderRadius.circular(8.r), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( + + SizedBox(height: 22.h), + + // ✅ PASS CARD SECTION + Container( + height: 140.h, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: checkoutData.themeColor.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // ✅ Hero Image - ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.r), - bottomLeft: Radius.circular(8.r), - ), - child: checkoutData.heroImage.isNotEmpty - ? Image.network( - checkoutData.heroImage, - width: 105.w, - height: 140.h, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _fallbackImage(); - }, - loadingBuilder: - (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( + Row( + children: [ + // ✅ Hero Image + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.r), + bottomLeft: Radius.circular(8.r), + ), + child: checkoutData.heroImage.isNotEmpty + ? Image.network( + checkoutData.heroImage, width: 105.w, height: 140.h, - color: Colors.grey[200], - child: Center( - child: SizedBox( - width: 24.w, - height: 24.w, - child: CircularProgressIndicator( - strokeWidth: 2, - color: checkoutData?.themeColor, - ), - ), - ), - ); - }, - ) - : _fallbackImage(), - ), - - SizedBox(width: 6.66.w), - - // ✅ Pass Details - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // City Name - CustomText( - text: checkoutData.cityName, - weight: FontWeight.w500, - size: 16.sp, - ), - SizedBox(height: 5.h), - - // Validity (Days or Attractions) - CustomText( - text: checkoutData.validityLabel, - color: const Color(0xFF8E8E8E), - size: 12.sp, - ), - SizedBox(height: 5.h), - - // Adults and Quantity - SizedBox( - width: MediaQuery.of(context).size.width * .5, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - // Adults - if (checkoutData.adultCount > 0) - Row( - children: [ - Image.asset( - 'assets/icons/adult.png', - scale: 4, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _fallbackImage(); + }, + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + width: 105.w, + height: 140.h, + color: Colors.grey[200], + child: Center( + child: SizedBox( + width: 24.w, + height: 24.w, + child: CircularProgressIndicator( + strokeWidth: 2, + color: checkoutData.themeColor, ), - SizedBox(width: 4.w), - CustomText( - text: - "${checkoutData.adultCount} adult${checkoutData.adultCount > 1 ? 's' : ''}", - color: const Color(0xFF8E8E8E), - size: 12.sp, - ), - ], - ), - - // Total Quantity - Row( - children: [ - Image.asset( - 'assets/icons/qty.png', - scale: 4, ), - SizedBox(width: 4.w), - Text.rich( - TextSpan( + ), + ); + }, + ) + : _fallbackImage(), + ), + + SizedBox(width: 6.66.w), + + // ✅ Pass Details + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // City Name + CustomText( + text: checkoutData.cityName, + weight: FontWeight.w500, + size: 16.sp, + ), + SizedBox(height: 5.h), + + // Validity (Days or Attractions) + CustomText( + text: checkoutData.validityLabel, + color: const Color(0xFF8E8E8E), + size: 12.sp, + ), + SizedBox(height: 5.h), + + // Adults + SizedBox( + width: MediaQuery.of(context).size.width * .5, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + // Adults + if (checkoutData.adultCount > 0) + Row( children: [ - TextSpan( - text: "Qty:", - style: TextStyle( - color: const Color(0xFF8E8E8E), - fontSize: 12.sp, - ), + Image.asset( + 'assets/icons/adult.png', + scale: 4, ), - TextSpan( + SizedBox(width: 4.w), + CustomText( text: - " ${checkoutData.totalQuantity}", - style: TextStyle( - color: const Color(0xFF000000), - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), + "${checkoutData.adultCount} adult${checkoutData.adultCount > 1 ? 's' : ''}", + color: const Color(0xFF8E8E8E), + size: 12.sp, ), ], ), - ), ], ), - ], - ), - ), + ), + SizedBox(height: 5.h), + Row( + children: [ + // Children + if (checkoutData.childCount > 0) ...[ + Image.asset( + "assets/icons/kid.png", + scale: 4, + ), + SizedBox(width: 4.w), + CustomText( + text: + "${checkoutData.childCount} Kid${checkoutData.childCount > 1 ? 's' : ''}", + color: const Color(0xFF8E8E8E), + size: 12.sp, + ), + SizedBox(width: 53.w), + ] else + SizedBox(width: 120.w), - SizedBox(height: 5.h), - - // Children and Price - Row( - children: [ - // Children - if (checkoutData.childCount > 0) ...[ - Image.asset( - "assets/icons/kid.png", - scale: 4, - ), - SizedBox(width: 4.w), - CustomText( - text: - "${checkoutData.childCount} Kid${checkoutData.childCount > 1 ? 's' : ''}", - color: const Color(0xFF8E8E8E), - size: 12.sp, - ), - SizedBox(width: 53.w), - ] else - SizedBox(width: 120.w), - - // Total Price - CustomText( - text: "\$${subtotal.toStringAsFixed(2)}", - size: 24.sp, - weight: FontWeight.w500, - color: checkoutData.themeColor, + // Total Price + CustomText( + text: "\$${subtotal.toStringAsFixed(2)}", + size: 24.sp, + weight: FontWeight.w500, + color: checkoutData.themeColor, + ), + ], ), ], ), ], ), - ], - ), - // ✅ Card Type Label (Vertical) - Container( - width: 35.w, - height: 140.h, - decoration: BoxDecoration( - color: checkoutData.themeColor, - borderRadius: BorderRadius.only( - bottomRight: Radius.circular(8.r), - topRight: Radius.circular(8.r), - ), - ), - child: RotatedBox( - quarterTurns: -1, - child: Center( - child: Text( - checkoutData.cardDisplayName, - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w500, + // ✅ Card Type Label (Vertical) + Container( + width: 35.w, + height: 140.h, + decoration: BoxDecoration( + color: checkoutData.themeColor, + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(8.r), + topRight: Radius.circular(8.r), + ), + ), + child: RotatedBox( + quarterTurns: -1, + child: Center( + child: Text( + checkoutData.cardDisplayName, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), ), ), ), + ], + ), + ), + + SizedBox(height: 20.h), + + // ✅ COUPON SECTION + Container( + width: double.infinity, + padding: + EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + decoration: BoxDecoration( + color: const Color(0xFFF95F62).withOpacity(0.06), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: const Color(0xFFF95F62), + width: 0.3, ), ), - ], - ), - ), - - SizedBox(height: 10.h), - - // ✅ COUPON SECTION - Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: const Color(0xFFBB474A).withOpacity(0.4), - width: 0.8, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( + child: state is CheckoutCouponsLoadingState + ? Row( + children: [ + SizedBox( + width: 16.w, + height: 16.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFF95F62), + ), + ), + SizedBox(width: 8.w), + CustomText( + text: "Loading coupons...", + size: 12.sp, + color: Colors.grey, + ), + ], + ) + : state is CheckoutCouponsErrorState + ? CustomText( + text: "Error loading coupons", + size: 12.sp, + color: Colors.red, + ) + : state is CheckoutCouponsLoadedState + ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText( - text: appliedCouponCode != null - ? "Coupon Applied: $appliedCouponCode" - : "Get 10% off on your first trip", - color: const Color(0xFF262626), - size: 14.sp, - ), - SizedBox(height: 7.h), - Row( - 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: const Color(0xFFF95F62), - size: 12, + /// LEFT CONTENT + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: appliedCoupon != null + ? "Coupon Applied: ${appliedCoupon.couponCode}" + : state.coupons.isNotEmpty + ? "${state.coupons[0].discountPercent}% discount on ${state.coupons[0].title}" + : "No coupons available", + color: const Color(0xFF262626), + size: 14.sp, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), + SizedBox(height: 7.h), + GestureDetector( + onTap: () { + // ✅ Updated: Pass callback to bottomsheet + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => AllCouponsBottomsheet( + onCouponSelected: (selectedCoupon) { + // Apply the selected coupon + context.read().add( + ApplyCouponEvent( + coupon: selectedCoupon), + ); + }, + ), + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CustomText( + text: "View all coupons", + color: const Color(0xFFF95F62), + size: 12.sp, + ), + SizedBox(width: 3.w), + const Icon( + Icons.arrow_right, + size: 18, + color: Color(0xFFF95F62), + ), + ], + ), + ), + ], + ), + ), + + SizedBox(width: 12.w), + + /// APPLY / REMOVE BUTTON + GestureDetector( + onTap: () { + if (appliedCoupon != null) { + context + .read() + .add(RemoveCouponEvent()); + } else if (state.coupons.isNotEmpty) { + context.read().add( + ApplyCouponEvent( + coupon: state.coupons[0]), + ); + } + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 18.w, + vertical: 10.h, ), - SizedBox(width: 3.w), - const Icon( - Icons.arrow_right, - color: Color(0xFFF95F62), + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xFFF95F62), + ), + borderRadius: BorderRadius.circular(8.r), + ), + child: CustomText( + text: + appliedCoupon != null ? "Remove" : "Apply", + color: const Color(0xFFF95F62), + size: 14.sp, + ), + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + + SizedBox(height: 15.h), + + // ✅ PRICING BREAKDOWN + DashedDivider( + color: const Color(0xFFACACAC), + thickness: 1.h, + dashLength: 4, + dashSpace: 4, + ), + SizedBox(height: 10.h), + + // Subtotal + 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), + + // Discount + 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: const Color(0xFFACACAC), + thickness: 1.h, + dashLength: 4, + dashSpace: 4, + ), + SizedBox(height: 10.h), + + // Total + 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), ), ], ), - ], - ), - const Spacer(), - GestureDetector( - onTap: () { - // ✅ Apply coupon logic (for demo, applying 10% discount) - 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: const Color(0xFFF95F62)), - borderRadius: BorderRadius.circular(8.r), - ), - child: CustomText( - text: appliedCouponCode != null ? "Remove" : "Apply", - color: const Color(0xFFF95F62), - size: 14.sp, - ), ), - ), - ], - ), - ), - - SizedBox(height: 15.h), - - // ✅ PRICING BREAKDOWN - DashedDivider( - color: const Color(0xFFACACAC), - thickness: 1.h, - dashLength: 4, - dashSpace: 4, - ), - SizedBox(height: 10.h), - - // Subtotal - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomText(text: "Subtotal", size: 14.sp), - CustomText( - text: "\$${subtotal.toStringAsFixed(2)}", - size: 14.sp, - weight: FontWeight.w500, + CustomText( + text: "\$${finalTotal.toStringAsFixed(2)}", + size: 24.sp, + weight: FontWeight.w500, + ), + ], ), - ], - ), - SizedBox(height: 14.h), - // Discount - 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), - ], + const Spacer(), - DashedDivider( - color: const Color(0xFFACACAC), - thickness: 1.h, - dashLength: 4, - dashSpace: 4, - ), - SizedBox(height: 10.h), + // ✅ CHECKOUT BUTTON + FutureBuilder( + future: LocalPreference.getLogin(), + builder: (context, snapshot) { + final isLoggedIn = snapshot.data ?? false; - // Total - 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, - ), - ], - ), + return CustomFilledButton( + onTap: () async { + if (isLoggedIn) { + if (isPurchaseDetailsConfirmed) { + // Navigate to Stripe payment directly + final paymentResult = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const StripePaymentView(), + settings: RouteSettings( + arguments: { + 'amount': finalTotal, + 'currency': 'usd', // or your currency + }, + ), + ), + ); - const Spacer(), + // Handle payment result + if (paymentResult == true) { + // Payment successful + print("Payment successful!"); + // Handle success - clear cart, show confirmation, etc. + } + } else { + // Show purchase details bottom sheet and wait for result + final result = await PassPurchaseBottomSheet.show(context); - // ✅ CHECKOUT BUTTON - FutureBuilder( - future: LocalPreference.getLogin(), - builder: (context, snapshot) { - final isLoggedIn = snapshot.data ?? false; - - return CustomFilledButton( - onTap: () async { - if (isLoggedIn) { - if (isPurchaseDetailsConfirmed) { - // Navigate to Stripe payment directly - final paymentResult = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const StripePaymentView(), - settings: RouteSettings( - arguments: { - 'amount': finalTotal, - 'currency': 'usd', // or your currency - }, + // If user selected 'self', show purchase confirmation + if (result == 'self') { + onPurchaseDetailsChanged(true); + } + } + } else { + // Show login bottom sheet if not logged in + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), ), - ), - ); - - // Handle payment result - if (paymentResult == true) { - // Payment successful - print("Payment successful!"); - // Handle success - clear cart, show confirmation, etc. + builder: (_) => const LoginEmailBottomsheet(), + ); } - } else { - // Show purchase details bottom sheet and wait for result - final result = await PassPurchaseBottomSheet.show(context); - - // If user selected 'self', show purchase confirmation - if (result == 'self') { - setState(() { - isPurchaseDetailsConfirmed = true; - }); - } - } - } else { - // Show login bottom sheet if not logged in - 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: isLoggedIn + ? (isPurchaseDetailsConfirmed + ? "Pay \$${finalTotal.toStringAsFixed(2)}" + : "Checkout") + : "Login to Checkout", + ); }, - width: double.infinity, - label: isLoggedIn - ? (isPurchaseDetailsConfirmed - ? "Pay \$${finalTotal.toStringAsFixed(2)}" - : "Checkout") - : "Login to Checkout", - ); - }, + ), + SizedBox(height: 25.h), + ], ), - SizedBox(height: 25.h), - ], + ), ), - ), - ), + ); + }, ); } diff --git a/lib/checkout/widget/all_coupons_bottomsheet.dart b/lib/checkout/widget/all_coupons_bottomsheet.dart index e5e1c46..da89f0b 100644 --- a/lib/checkout/widget/all_coupons_bottomsheet.dart +++ b/lib/checkout/widget/all_coupons_bottomsheet.dart @@ -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> 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( + 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(); + }, + ), ), - ), - ], + ], + ), ), ); } -} +} \ No newline at end of file diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 3e10e99..94fa2ca 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -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 diff --git a/lib/networkApiServices/network_api_services.dart b/lib/networkApiServices/network_api_services.dart index 78f626e..41d9baf 100644 --- a/lib/networkApiServices/network_api_services.dart +++ b/lib/networkApiServices/network_api_services.dart @@ -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) { + 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: diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index ac1d4d8..34eb3e6 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -12,48 +12,91 @@ class PostcardCreationBloc extends Bloc { 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((event, emit) { - final next = - PostcardStep.values[(state.currentStep.index + 1).clamp( - 0, - PostcardStep.values.length - 1, - )]; - emit(state.copyWith(currentStep: next)); + on((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((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((event, emit) { + on((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((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((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((event, emit) { + emit(state.copyWith(errorMessage: null)); + }); + on((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((event, emit) { + emit(state.copyWith(pcNumber: event.pcNumber)); + }); + on((event, emit) { emit(state.copyWith(isGift: event.isGift)); }); } -} +} \ No newline at end of file diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart index 0439cab..8fd7f20 100644 --- a/lib/postcard/blocs/postcard_creation_events.dart +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -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); } \ No newline at end of file diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart index 44191e1..d9c3064 100644 --- a/lib/postcard/blocs/postcard_creation_state.dart +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -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 ); } } \ No newline at end of file diff --git a/lib/postcard/views/order_success_page_view.dart b/lib/postcard/views/order_success_page_view.dart index 61f4c78..41874e8 100644 --- a/lib/postcard/views/order_success_page_view.dart +++ b/lib/postcard/views/order_success_page_view.dart @@ -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), ), diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart index 04242fc..9617aff 100644 --- a/lib/postcard/views/postcard_checkout_page_view.dart +++ b/lib/postcard/views/postcard_checkout_page_view.dart @@ -315,6 +315,11 @@ class _PostcardCheckoutPageViewState extends State { return BlocConsumer( listener: (context, checkoutState) { if (checkoutState.isSuccess && !checkoutState.isDraft) { + if (checkoutState.pcNumber != null) { + context.read().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 { "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), diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart index cfcb88c..b241ada 100644 --- a/lib/postcard/views/postcard_creation_page_view.dart +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -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; diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart index 88e67d0..461c79d 100644 --- a/lib/postcard/views/postcard_purchase_form_page_view.dart +++ b/lib/postcard/views/postcard_purchase_form_page_view.dart @@ -369,8 +369,7 @@ class _PostcardPurchaseFormPageViewState extends State( - builder: (context, state) { - final bloc = context.read(); - - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - - 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(); - 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( + // 🆕 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().add(ClearError()); + }, + ), ), - ), - ); + ); + + // Clear error after showing snackbar + Future.delayed(const Duration(seconds: 4), () { + context.read().add(ClearError()); + }); + } }, + child: BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), + + 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, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ), ); } -} +} \ No newline at end of file