diff --git a/android/app/src/main/kotlin/com/citycards_customer/citycards_customer/MainActivity.kt b/android/app/src/main/kotlin/com/citycards_customer/citycards_customer/MainActivity.kt index 9b90374..17c95b7 100644 --- a/android/app/src/main/kotlin/com/citycards_customer/citycards_customer/MainActivity.kt +++ b/android/app/src/main/kotlin/com/citycards_customer/citycards_customer/MainActivity.kt @@ -1,5 +1,5 @@ package com.citycards_customer.citycards_customer -import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.android.FlutterFragmentActivity -class MainActivity : FlutterActivity() +class MainActivity : FlutterFragmentActivity() \ No newline at end of file diff --git a/lib/StripePayment/bloc/stripe_payment_bloc.dart b/lib/StripePayment/bloc/stripe_payment_bloc.dart new file mode 100644 index 0000000..39e336e --- /dev/null +++ b/lib/StripePayment/bloc/stripe_payment_bloc.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; + +import '../repository/stripe_service.dart'; +import 'stripe_payment_event.dart'; +import 'stripe_payment_state.dart'; + +class StripePaymentBloc extends Bloc { + final StripeService _stripeService; + + StripePaymentBloc({ + StripeService? stripeService, + }) : _stripeService = stripeService ?? StripeService(), + super(const StripePaymentInitial()) { + on(_onInitiatePayment); + on(_onResetPaymentState); + } + + Future _onInitiatePayment( + InitiatePayment event, + Emitter emit, + ) async { + try { + emit(const StripePaymentLoading()); + + /// Stripe expects smallest currency unit + /// USD → cents, INR → paise + final int stripeAmount = (event.amount * 100).toInt(); + + // 1️⃣ Create PaymentIntent from backend + final clientSecret = await _stripeService.createPaymentIntent( + amount: stripeAmount, + currency: event.currency, + ); + + // 2️⃣ Init Payment Sheet + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: clientSecret, + merchantDisplayName: "CityCards", + style: ThemeMode.light, + ), + ); + + // 3️⃣ Show Payment Sheet + await Stripe.instance.presentPaymentSheet(); + + // ✅ SUCCESS + emit(const StripePaymentSuccess()); + } on StripeException catch (e) { + // Handle Stripe-specific errors + if (e.error.code == FailureCode.Canceled) { + emit(StripePaymentCancelled( + message: e.error.localizedMessage ?? 'Payment Cancelled', + )); + } else { + emit(StripePaymentFailure( + error: e.error.localizedMessage ?? 'Payment failed', + )); + } + } catch (e) { + emit(StripePaymentFailure( + error: e.toString(), + )); + } + } + + void _onResetPaymentState( + ResetPaymentState event, + Emitter emit, + ) { + emit(const StripePaymentInitial()); + } +} \ No newline at end of file diff --git a/lib/StripePayment/bloc/stripe_payment_event.dart b/lib/StripePayment/bloc/stripe_payment_event.dart new file mode 100644 index 0000000..f356b54 --- /dev/null +++ b/lib/StripePayment/bloc/stripe_payment_event.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +abstract class StripePaymentEvent extends Equatable { + const StripePaymentEvent(); + + @override + List get props => []; +} + +class InitiatePayment extends StripePaymentEvent { + final double amount; + final String currency; + + const InitiatePayment({ + required this.amount, + required this.currency, + }); + + @override + List get props => [amount, currency]; +} + +class ResetPaymentState extends StripePaymentEvent { + const ResetPaymentState(); +} \ No newline at end of file diff --git a/lib/StripePayment/bloc/stripe_payment_state.dart b/lib/StripePayment/bloc/stripe_payment_state.dart new file mode 100644 index 0000000..1d6383c --- /dev/null +++ b/lib/StripePayment/bloc/stripe_payment_state.dart @@ -0,0 +1,49 @@ +import 'package:equatable/equatable.dart'; + +abstract class StripePaymentState extends Equatable { + const StripePaymentState(); + + @override + List get props => []; +} + +class StripePaymentInitial extends StripePaymentState { + const StripePaymentInitial(); +} + +class StripePaymentLoading extends StripePaymentState { + const StripePaymentLoading(); +} + +class StripePaymentSuccess extends StripePaymentState { + final String message; + + const StripePaymentSuccess({ + this.message = 'Payment Successful', + }); + + @override + List get props => [message]; +} + +class StripePaymentFailure extends StripePaymentState { + final String error; + + const StripePaymentFailure({ + required this.error, + }); + + @override + List get props => [error]; +} + +class StripePaymentCancelled extends StripePaymentState { + final String message; + + const StripePaymentCancelled({ + this.message = 'Payment Cancelled', + }); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/StripePayment/repository/stripe_service.dart b/lib/StripePayment/repository/stripe_service.dart new file mode 100644 index 0000000..dddd3d1 --- /dev/null +++ b/lib/StripePayment/repository/stripe_service.dart @@ -0,0 +1,97 @@ +import 'package:dio/dio.dart'; + +class StripeService { + final Dio _dio = Dio( + BaseOptions( + headers: { + "Content-Type": "application/json", + }, + ), + ); + + // ⚠️ TEMPORARY FALLBACK - Use secret key directly + // TODO: Remove this and use backend when ready! + final String _stripeSecretKey = 'sk_test_51SrwZ7RtCkWyT4EmgS97odPlrKNj2TUxIkyu5L2i6qQyEpCivhYtEO6cW660UjBMoUsN1rUldvVhGx7RpGMarANp00Ntyi2Bp4'; // ← ADD YOUR SECRET KEY + + Future createPaymentIntent({ + required int amount, + required String currency, + }) async { + try { + // 🔥 DIRECT STRIPE API CALL (Temporary fallback) + final response = await _dio.post( + 'https://api.stripe.com/v1/payment_intents', + data: { + 'amount': amount.toString(), + 'currency': currency, + 'automatic_payment_methods[enabled]': 'true', + }, + options: Options( + headers: { + 'Authorization': 'Bearer $_stripeSecretKey', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + contentType: Headers.formUrlEncodedContentType, + ), + ); + + if (response.data == null || response.data['client_secret'] == null) { + throw Exception('Invalid response from Stripe'); + } + + return response.data['client_secret']; + } on DioException catch (e) { + if (e.response != null) { + print('Stripe API Error: ${e.response?.data}'); + throw Exception('Stripe error: ${e.response?.data['error']?['message'] ?? e.message}'); + } + throw Exception('Network error: ${e.message}'); + } catch (e) { + print('Payment Intent Error: $e'); + throw Exception('Failed to create payment intent: $e'); + } + } +} + +/* +🔒 PRODUCTION VERSION (Use this when backend is ready): + +import 'package:citycards_customer/networkApiServices/api_urls.dart'; +import 'package:dio/dio.dart'; + +class StripeService { + final Dio _dio = Dio( + BaseOptions( + baseUrl: ApiUrls.baseUrl, + headers: { + "Content-Type": "application/json", + }, + ), + ); + + Future createPaymentIntent({ + required int amount, + required String currency, + }) async { + try { + final response = await _dio.post( + "/create-payment-intent", + data: { + "amount": amount, + "currency": currency, + }, + ); + + if (response.data == null || response.data['clientSecret'] == null) { + throw Exception('Invalid response from server'); + } + + return response.data['clientSecret']; + } on DioException catch (e) { + throw Exception('Network error: ${e.message}'); + } catch (e) { + throw Exception('Failed to create payment intent: $e'); + } + } +} +*/ \ No newline at end of file diff --git a/lib/StripePayment/view/stripe_payment.dart b/lib/StripePayment/view/stripe_payment.dart new file mode 100644 index 0000000..8bd1d47 --- /dev/null +++ b/lib/StripePayment/view/stripe_payment.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../bloc/stripe_payment_bloc.dart'; +import '../bloc/stripe_payment_event.dart'; +import '../bloc/stripe_payment_state.dart'; +import '../repository/stripe_service.dart'; + +class StripePaymentView extends StatelessWidget { + const StripePaymentView({super.key}); + + @override + Widget build(BuildContext context) { + final args = + ModalRoute.of(context)!.settings.arguments as Map; + + final double amount = args['amount']; + final String currency = args['currency']; + + return BlocProvider( + create: (context) => StripePaymentBloc( + stripeService: StripeService(), + ), + child: StripePaymentViewContent( + amount: amount, + currency: currency, + ), + ); + } +} + +class StripePaymentViewContent extends StatefulWidget { + final double amount; + final String currency; + + const StripePaymentViewContent({ + super.key, + required this.amount, + required this.currency, + }); + + @override + State createState() => + _StripePaymentViewContentState(); +} + +class _StripePaymentViewContentState extends State { + @override + void initState() { + super.initState(); + // Automatically initiate payment when screen loads + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add( + InitiatePayment( + amount: widget.amount, + currency: widget.currency, + ), + ); + }); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is StripePaymentSuccess) { + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.green, + duration: const Duration(seconds: 2), + ), + ); + // Return success to previous screen + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + Navigator.pop(context, true); + } + }); + } else if (state is StripePaymentFailure) { + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + // Go back to checkout on error + Future.delayed(const Duration(seconds: 1), () { + if (mounted) { + Navigator.pop(context, false); + } + }); + } else if (state is StripePaymentCancelled) { + // Show cancellation message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 2), + ), + ); + // Go back to checkout on cancellation + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + Navigator.pop(context, false); + } + }); + } + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: const Text("Processing Payment"), + backgroundColor: Colors.white, + elevation: 0, + automaticallyImplyLeading: false, // Remove back button during processing + centerTitle: true, + ), + body: BlocBuilder( + builder: (context, state) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Loading Indicator + if (state is StripePaymentLoading) ...[ + const CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Color(0xFFF95F62), + ), + ), + const SizedBox(height: 24), + const Text( + "Preparing secure payment...", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 12), + Text( + "Please wait", + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + + // Amount Display + const SizedBox(height: 32), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE0E0E0), + ), + ), + child: Column( + children: [ + Text( + "Payment Amount", + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + "\$${widget.amount.toStringAsFixed(2)}", + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 4), + Text( + widget.currency.toUpperCase(), + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + + const SizedBox(height: 32), + + // Security Badge + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.lock_outline, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 6), + Text( + "Secured by Stripe", + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/add_details/add_details_view.dart b/lib/add_details/add_details_view.dart index 6928a8a..034359a 100644 --- a/lib/add_details/add_details_view.dart +++ b/lib/add_details/add_details_view.dart @@ -20,163 +20,161 @@ class AddDetailsView extends StatelessWidget { return Scaffold( backgroundColor: Colors.white, body: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Column( - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showCart: false, - showDivider: true, - ), - Row( - children: [ - GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Icon(Icons.arrow_back, size: 24.sp), - ), - SizedBox(width: 8.w), - Text( - "Add details", - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - SizedBox(height: 42.h), - Align( - alignment: Alignment.centerLeft, - child: CustomText( - text: "Tell us about yourself", - size: 18.sp, - weight: FontWeight.w500, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showCart: false, + showDivider: true, ), - ), - SizedBox(height: 12.h), - - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "First Name", - hint: "Enter your first name", - controller: firstNameController, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "Last Name", - hint: "Enter your last name", - controller: lastNameController, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "Email", - hint: "Enter your email address", - controller: emailController, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "Phone Number", - hint: "Enter your phone number", - controller: phoneController, - ), - ), - - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "City", - hint: "Enter the name of your city", - controller: phoneController, - ), - ), - - Padding( - padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( children: [ - CustomText(text: "Country", size: 14.sp), - SizedBox(height: 6.h), - Container( - height: 42.h, - padding: EdgeInsets.symmetric(horizontal: 24.w), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: 0.4.w, - ), - ), - child: DropdownButtonHideUnderline( - child: StatefulBuilder( - builder: (context, setState) { - String? selectedCountry; - return DropdownButton( - value: selectedCountry, - isExpanded: true, - icon: const Icon( - Icons.keyboard_arrow_down, - color: Color(0xFF8E8E8E), - ), - hint: Text( - "Select your country", - style: TextStyle( - fontSize: 12.sp, - color: Color(0xFF8E8E8E), - ), - ), - style: TextStyle( - fontSize: 14.sp, - color: const Color(0xFF2D3134), - ), - onChanged: (value) { - setState(() { - selectedCountry = value; - }); - }, - items: ["India", "USA", "UK", "Canada"].map(( - value, - ) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle(fontSize: 14.sp), - ), - ); - }).toList(), - ); - }, - ), + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Icon(Icons.arrow_back, size: 24.sp), + ), + SizedBox(width: 8.w), + Text( + "Add details", + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, ), ), ], ), - ), + SizedBox(height: 42.h), + Align( + alignment: Alignment.centerLeft, + child: CustomText( + text: "Tell us about yourself", + size: 18.sp, + weight: FontWeight.w500, + ), + ), + SizedBox(height: 12.h), - const Spacer(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "First Name", + hint: "Enter your first name", + controller: firstNameController, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Last Name", + hint: "Enter your last name", + controller: lastNameController, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Email", + hint: "Enter your email address", + controller: emailController, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Phone Number", + hint: "Enter your phone number", + controller: phoneController, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "City", + hint: "Enter the name of your city", + controller: phoneController, + ), + ), - CustomFilledButton( - onTap: () { + Padding( + padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText(text: "Country", size: 14.sp), + SizedBox(height: 6.h), + Container( + height: 42.h, + padding: EdgeInsets.symmetric(horizontal: 24.w), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: const Color(0xBBC83B61).withOpacity(0.4), + width: 0.4.w, + ), + ), + child: DropdownButtonHideUnderline( + child: StatefulBuilder( + builder: (context, setState) { + String? selectedCountry; + return DropdownButton( + value: selectedCountry, + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFF8E8E8E), + ), + hint: Text( + "Select your country", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFF8E8E8E), + ), + ), + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF2D3134), + ), + onChanged: (value) { + setState(() { + selectedCountry = value; + }); + }, + items: ["India", "USA", "UK", "Canada"] + .map((value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle(fontSize: 14.sp), + ), + ); + }).toList(), + ); + }, + ), + ), + ), + ], + ), + ), - }, - label: "Continue", - width: double.infinity, - ), - SizedBox(height: 50.h), - ], + SizedBox(height: 24.h), + + CustomFilledButton( + onTap: () {}, + label: "Continue", + width: double.infinity, + ), + SizedBox(height: 50.h), + ], + ), ), ), ), diff --git a/lib/buy_a_pass/models/checkout_model.dart b/lib/buy_a_pass/models/checkout_model.dart new file mode 100644 index 0000000..056bfcc --- /dev/null +++ b/lib/buy_a_pass/models/checkout_model.dart @@ -0,0 +1,42 @@ +/// Model to pass checkout data from Buy Pass screen to Checkout screen +import 'package:flutter/material.dart'; +class CheckoutData { + final String cityName; + final String heroImage; + final String cardTypeName; // "unlimited_card" or "selective_pass" + final String cardDisplayName; // "Unlimited" or "Selective" + final Color themeColor; + final int adultCount; + final int childCount; + final double adultPrice; + final double childPrice; + final int validityDuration; // Days or attractions count + final double totalPrice; + final String? description; + + CheckoutData({ + required this.cityName, + required this.heroImage, + required this.cardTypeName, + required this.cardDisplayName, + required this.themeColor, + required this.adultCount, + required this.childCount, + required this.adultPrice, + required this.childPrice, + required this.validityDuration, + required this.totalPrice, + this.description, + }); + + // Calculate quantity (total adults + children) + int get totalQuantity => adultCount + childCount; + + // Check if it's unlimited card + bool get isUnlimitedCard => cardTypeName == "unlimited_card"; + + // Get validity label + String get validityLabel => isUnlimitedCard + ? "$validityDuration Days" + : "$validityDuration Attractions"; +} \ No newline at end of file diff --git a/lib/buy_a_pass/view/buy_pass_view.dart b/lib/buy_a_pass/view/buy_pass_view.dart index 7de6070..3ffd6e0 100644 --- a/lib/buy_a_pass/view/buy_pass_view.dart +++ b/lib/buy_a_pass/view/buy_pass_view.dart @@ -132,6 +132,7 @@ class BuyPassContent extends StatelessWidget { ? Color(0xFFF97316) : Color(0xFF1E8AF6), city: data.city.name, + heroImage: data.city.heroBanner.image, adultPrice: card.adultPrice, childPrice: card.childPrice, cardType: card.cardType.displayName, @@ -149,13 +150,18 @@ class BuyPassContent extends StatelessWidget { SizedBox(height: 30.h), // Payment Card - Center( - // Updated PaymentCard usage with BLoC + // ✅ UPDATED PAYMENT CARD SECTION IN buy_pass_view.dart +// Replace the existing PaymentCard widget (around line 154) with this: + Center( child: PaymentCard( city: data.city.name, + heroImage: data.city.heroBanner.image, // ✅ Pass hero image cardType: selectedCard.cardType.name, cardDisplayName: selectedCard.cardType.displayName, + themeColor: state.selectedCardIndex == 0 + ? Color(0xFFF97316) // Orange for first card + : Color(0xFF1E8AF6), // ✅ Pass theme color adultPrice: selectedCard.adultPrice.toDouble(), childPrice: selectedCard.childPrice.toDouble(), adults: state.adultCount, @@ -163,7 +169,8 @@ class BuyPassContent extends StatelessWidget { totalPrice: state.totalPrice, minNumber: selectedCard.minNumber, maxNumber: selectedCard.maxNumber, - selectedValue: state.validityDuration, // Use from BLoC state + selectedValue: state.validityDuration, + description: selectedCard.description, // ✅ Pass description onAdultChanged: (count) { context.read().add( UpdateAdultCount(count), diff --git a/lib/buy_a_pass/widget/feature_table.dart b/lib/buy_a_pass/widget/feature_table.dart index 426a274..f3a71d0 100644 --- a/lib/buy_a_pass/widget/feature_table.dart +++ b/lib/buy_a_pass/widget/feature_table.dart @@ -8,7 +8,6 @@ class FeatureTable extends StatelessWidget { @override Widget build(BuildContext context) { - // Static data using a simple model final features = [ FeatureModel('Access to attractions', true, true), FeatureModel('Entry to attractions', true, true), @@ -16,109 +15,147 @@ class FeatureTable extends StatelessWidget { FeatureModel('Entry to sites', false, true), FeatureModel('Access to venues', true, true), FeatureModel('Entry to events', true, true), - FeatureModel('Access to experiences', true, true), + FeatureModel('Access to experiences', false, true), + FeatureModel('Access to Itinerary creation', false, true), + FeatureModel('Access to postcard creation', false, true), ]; return Center( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), - decoration: BoxDecoration( - color: Color(0xFFF3F3F3), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black12, - blurRadius: 1, - offset: const Offset(0, 2), - ), - ], - ), - child: Table( - columnWidths: const { - 0: FlexColumnWidth(2.5), - 1: FlexColumnWidth(1.2), - 2: FlexColumnWidth(1.2), - }, - - children: [ - _buildHeaderRow(), - ...features.map((f) => _buildFeatureRow(f)).toList(), - ], - ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h), + decoration: BoxDecoration( + color: const Color(0xFFF3F3F3), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 2, + offset: Offset(0, 2), + ), + ], + ), + child: Table( + columnWidths: const { + 0: FlexColumnWidth(2.7), + 1: FlexColumnWidth(1.15), + 2: FlexColumnWidth(1.15), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + _buildHeaderRow(), + ...features.map(_buildFeatureRow).toList(), + ], ), ), - ); - + ), + ); } - // Header Row + // HEADER ROW TableRow _buildHeaderRow() { return TableRow( children: [ Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), + padding: EdgeInsets.only(bottom: 12.h), child: Text( 'Features', - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16.sp), - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), - child: Center( - child: Text( - '${CommonAppText.selectiveCard}', - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16.sp), - ), - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), - child: Center( - child: Text( - 'Unlimited', - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16.sp), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 15.sp, ), ), ), + _buildHeaderText(CommonAppText.selectiveCard), + _buildHeaderText('Unlimited'), ], ); } - // Each Feature Row + Widget _buildHeaderText(String text) { + return Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: Center( + child: Text( + text, + maxLines: 1, + softWrap: false, + overflow: TextOverflow.visible, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14.sp, + ), + ), + ), + ); + } + + // FEATURE ROW TableRow _buildFeatureRow(FeatureModel feature) { return TableRow( children: [ - _buildCell(feature.name), + _buildFeatureCell(feature.name), _buildIconCell(feature.flexi), _buildIconCell(feature.unlimited), ], ); } - // Text cell - Widget _buildCell(String text) { + // FEATURE TEXT WITH BULLET + Widget _buildFeatureCell(String text) { return Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), - child: Text(text, style: TextStyle(fontSize: 12.sp, color: Colors.black.withOpacity(.8)),), + padding: EdgeInsets.symmetric(vertical: 7.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 2.h, right: 6.w), + child: Text( + '•', + style: TextStyle(fontSize: 18.sp, height: 1), + ), + ), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 12.5.sp, + color: Colors.black.withOpacity(0.85), + height: 1.35, + ), + ), + ), + ], + ), ); } - // Icon cell + // ICON CELL Widget _buildIconCell(bool isAvailable) { return Padding( - padding: EdgeInsets.symmetric(vertical: 6.h), + padding: EdgeInsets.symmetric(vertical: 7.h), child: Center( child: isAvailable - ? Icon(Icons.check_circle, color: Colors.redAccent,size: 16.sp,) - : const Text('–', style: TextStyle(color: Colors.black54)), + ? Icon( + Icons.check_circle, + color: Colors.redAccent, + size: 16.sp, + ) + : Text( + '–', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black45, + ), + ), ), ); } } -// Model for feature row +// MODEL class FeatureModel { final String name; final bool flexi; diff --git a/lib/buy_a_pass/widget/pass_card_view.dart b/lib/buy_a_pass/widget/pass_card_view.dart index 7c33ca4..d33013d 100644 --- a/lib/buy_a_pass/widget/pass_card_view.dart +++ b/lib/buy_a_pass/widget/pass_card_view.dart @@ -5,6 +5,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; class PassCardView extends StatelessWidget { final Color? themeColor; final String? city; + final String? heroImage; // ✅ heroBanner.image from API final int? adultPrice; final int? childPrice; final String? cardType; @@ -15,6 +16,7 @@ class PassCardView extends StatelessWidget { super.key, this.themeColor, this.city, + this.heroImage, this.adultPrice, this.childPrice, this.cardType, @@ -28,26 +30,17 @@ class PassCardView extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, border: Border.all( - color: (themeColor ?? Color(0xFFF95FAF)).withOpacity(0.24), + color: (themeColor ?? const Color(0xFFF95FAF)).withOpacity(0.24), width: isSelected ? 2 : 1, ), borderRadius: BorderRadius.circular(8.r), - // boxShadow: isSelected - // ? [ - // BoxShadow( - // color: (themeColor ?? Color(0xFFF95FAF)).withOpacity(0.3), - // blurRadius: 8, - // spreadRadius: 1, - // ) - // ] - // : [], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - // Banner Image Placeholder + /// -------- HERO BANNER IMAGE -------- ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(8.r), @@ -57,16 +50,33 @@ class PassCardView extends StatelessWidget { width: 103.w, height: 140.h, color: Colors.grey[200], - child: Icon( - Icons.card_travel, - size: 40.sp, - color: Colors.grey[400], - ), + child: heroImage != null && heroImage!.isNotEmpty + ? Image.network( + heroImage!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _fallbackIcon(); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: SizedBox( + width: 24.w, + height: 24.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ); + }, + ) + : _fallbackIcon(), ), ), + SizedBox(width: 6.66.w), - // Card Details + /// -------- CARD DETAILS -------- Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, @@ -77,7 +87,7 @@ class PassCardView extends StatelessWidget { size: 16.sp, ), - // Adult Price + /// Adult Price Row( children: [ Text( @@ -107,7 +117,7 @@ class PassCardView extends StatelessWidget { ], ), - // Child Price + /// Child Price Row( children: [ Text( @@ -137,13 +147,13 @@ class PassCardView extends StatelessWidget { ], ), - // Description + /// Description SizedBox( width: 193.w, child: CustomText( text: description ?? "Dive into an extensive selection of thrilling destinations!", - color: Color(0xFF000000).withOpacity(0.6), + color: const Color(0xFF000000).withOpacity(0.6), size: 11.sp, maxLines: 2, overflow: TextOverflow.ellipsis, @@ -154,7 +164,7 @@ class PassCardView extends StatelessWidget { ], ), - // Card Type Label (Vertical) + /// -------- CARD TYPE LABEL -------- Container( width: 35.w, height: 140.h, @@ -175,7 +185,6 @@ class PassCardView extends StatelessWidget { fontSize: 14.sp, fontWeight: FontWeight.w500, ), - textAlign: TextAlign.center, ), ), ), @@ -184,4 +193,13 @@ class PassCardView extends StatelessWidget { ), ); } -} \ No newline at end of file + + /// -------- FALLBACK ICON -------- + Widget _fallbackIcon() { + return Icon( + Icons.card_travel, + size: 40.sp, + color: Colors.grey[400], + ); + } +} diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart index 58b02e8..f88b7f2 100644 --- a/lib/buy_a_pass/widget/payment_card_view.dart +++ b/lib/buy_a_pass/widget/payment_card_view.dart @@ -1,13 +1,17 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:citycards_customer/core/route_constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../models/checkout_model.dart'; +import '../../checkout/view/checkout_view.dart'; // ✅ Import CheckoutView directly + class PaymentCard extends StatelessWidget { final String city; - final String cardType; // "unlimited_card" or "selective_pass" + final String heroImage; + final String cardType; final String cardDisplayName; + final Color themeColor; final double adultPrice; final double childPrice; final int adults; @@ -15,7 +19,8 @@ class PaymentCard extends StatelessWidget { final double totalPrice; final int minNumber; final int maxNumber; - final int selectedValue; // Current selected value for dropdown + final int selectedValue; + final String? description; final Function(int) onAdultChanged; final Function(int) onChildChanged; final Function(int) onValidityChanged; @@ -23,8 +28,10 @@ class PaymentCard extends StatelessWidget { const PaymentCard({ super.key, required this.city, + required this.heroImage, required this.cardType, required this.cardDisplayName, + required this.themeColor, required this.adultPrice, required this.childPrice, required this.adults, @@ -33,6 +40,7 @@ class PaymentCard extends StatelessWidget { required this.minNumber, required this.maxNumber, required this.selectedValue, + this.description, required this.onAdultChanged, required this.onChildChanged, required this.onValidityChanged, @@ -40,7 +48,6 @@ class PaymentCard extends StatelessWidget { @override Widget build(BuildContext context) { - // Determine if it's unlimited card or selective pass final bool isUnlimitedCard = cardType == "unlimited_card"; final bool isSelectivePass = cardType == "selective_pass"; @@ -65,16 +72,12 @@ class PaymentCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Title CustomText( text: city, size: 20.sp, weight: FontWeight.bold, ), - SizedBox(height: 6.h), - - // Tag Container( padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h), decoration: BoxDecoration( @@ -88,28 +91,11 @@ class PaymentCard extends StatelessWidget { weight: FontWeight.w500, ), ), - SizedBox(height: 16.h), - - // Adult Counter - _buildCounterRow( - "No. of Adults", - adults, - onAdultChanged, - ), - + _buildCounterRow("No. of Adults", adults, onAdultChanged), SizedBox(height: 10.h), - - // Children Counter - _buildCounterRow( - "No. of Children", - children, - onChildChanged, - ), - + _buildCounterRow("No. of Children", children, onChildChanged), SizedBox(height: 10.h), - - // Show days dropdown for unlimited_card OR attractions dropdown for selective_pass if (isUnlimitedCard) _buildDropdownRow( label: "No. of Days", @@ -122,10 +108,7 @@ class PaymentCard extends StatelessWidget { value: selectedValue, onChanged: onValidityChanged, ), - Divider(height: 30.h, thickness: 1), - - // Price section Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -134,26 +117,42 @@ class PaymentCard extends StatelessWidget { size: 16.sp, weight: FontWeight.w500, ), - Row( - children: [ - // Calculate original price (without any discount logic for now) - CustomText( - text: "\$${totalPrice.toStringAsFixed(0)}", - size: 18.sp, - color: Color(0xFFF95F62), - weight: FontWeight.bold, - ), - ], + CustomText( + text: "\$${totalPrice.toStringAsFixed(0)}", + size: 18.sp, + color: Color(0xFFF95F62), + weight: FontWeight.bold, ), ], ), - SizedBox(height: 20.h), - - // Proceed Button CustomFilledButton( onTap: () { - Navigator.of(context).pushNamed(RouteConstants.checkout); + // ✅ Create checkout data + final checkoutData = CheckoutData( + cityName: city, + heroImage: heroImage, + cardTypeName: cardType, + cardDisplayName: cardDisplayName, + themeColor: themeColor, + adultCount: adults, + childCount: children, + adultPrice: adultPrice, + childPrice: childPrice, + validityDuration: selectedValue, + totalPrice: totalPrice, + description: description, + ); + + // ✅ DIRECT NAVIGATION - This fixes the route issue! + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const CheckoutView(), + settings: RouteSettings( + arguments: checkoutData, + ), + ), + ); }, label: "Proceed to Pay", ), @@ -163,7 +162,6 @@ class PaymentCard extends StatelessWidget { ); } - /// Dropdown row for days or attractions count Widget _buildDropdownRow({ required String label, required int value, @@ -181,10 +179,9 @@ class PaymentCard extends StatelessWidget { text: label, size: 15.sp, ), - Container( height: 36.h, - width: 88.w, // 👈 fixed width for proper spacing + width: 88.w, padding: EdgeInsets.symmetric(horizontal: 14.w), decoration: BoxDecoration( color: Color(0xFFF95F62).withValues(alpha: 0.13), @@ -207,7 +204,7 @@ class PaymentCard extends StatelessWidget { return DropdownMenuItem( value: number, child: Align( - alignment: Alignment.centerLeft, // 🔢 number fully left + alignment: Alignment.centerLeft, child: CustomText( text: "$number", size: 16.sp, @@ -228,7 +225,6 @@ class PaymentCard extends StatelessWidget { ); } - /// Counter row for adults/children Widget _buildCounterRow( String label, int value, @@ -260,7 +256,6 @@ class PaymentCard extends StatelessWidget { ); } - /// Circle button for increment/decrement Widget _circleButton(IconData icon, VoidCallback onTap) { return InkWell( onTap: onTap, diff --git a/lib/cart/views/my_pass_page_view.dart b/lib/cart/views/my_pass_page_view.dart index 30daef7..ac3bf04 100644 --- a/lib/cart/views/my_pass_page_view.dart +++ b/lib/cart/views/my_pass_page_view.dart @@ -349,7 +349,7 @@ class MyPassesPage extends StatelessWidget { ); }, width: double.infinity, - label: "Proceed to Checkout", + label: "Login to Checkout", ), SizedBox(height: 25.h), ], @@ -376,7 +376,4 @@ class MyPassesPage extends StatelessWidget { }, ); } - - - } diff --git a/lib/checkout/bloc/checkout/checkout_bloc.dart b/lib/checkout/bloc/checkout/checkout_bloc.dart new file mode 100644 index 0000000..4cae026 --- /dev/null +++ b/lib/checkout/bloc/checkout/checkout_bloc.dart @@ -0,0 +1,53 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'checkout_event.dart'; +part 'checkout_state.dart'; + +/// BLoC for managing checkout screen state +class CheckoutBloc extends Bloc { + CheckoutBloc() : super(CheckoutState.initial()) { + // Handle apply coupon event + on(_onApplyCoupon); + + // Handle remove coupon event + on(_onRemoveCoupon); + + // Handle confirm purchase details event + on(_onConfirmPurchaseDetails); + + // Handle reset purchase details event + on(_onResetPurchaseDetails); + } + + /// Handle applying a coupon + void _onApplyCoupon(ApplyCouponEvent event, Emitter emit) { + emit(state.copyWith( + appliedCouponCode: event.couponCode, + discountPercentage: event.discountPercentage, + )); + } + + /// Handle removing a coupon + void _onRemoveCoupon(RemoveCouponEvent event, Emitter emit) { + emit(state.copyWith( + clearCoupon: true, + discountPercentage: 0.0, + )); + } + + /// Handle confirming purchase details + void _onConfirmPurchaseDetails( + ConfirmPurchaseDetailsEvent event, + Emitter emit, + ) { + emit(state.copyWith(isPurchaseDetailsConfirmed: true)); + } + + /// Handle resetting purchase details confirmation + void _onResetPurchaseDetails( + ResetPurchaseDetailsEvent event, + Emitter emit, + ) { + emit(state.copyWith(isPurchaseDetailsConfirmed: false)); + } +} \ 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..7ed9c86 --- /dev/null +++ b/lib/checkout/bloc/checkout/checkout_event.dart @@ -0,0 +1,24 @@ +part of 'checkout_bloc.dart'; + +/// Base class for all checkout events +abstract class CheckoutEvent {} + +/// Event to apply a coupon code +class ApplyCouponEvent extends CheckoutEvent { + final String couponCode; + final double discountPercentage; + + ApplyCouponEvent({ + required this.couponCode, + required this.discountPercentage, + }); +} + +/// Event to remove the applied coupon +class RemoveCouponEvent extends CheckoutEvent {} + +/// Event to confirm purchase details +class ConfirmPurchaseDetailsEvent extends CheckoutEvent {} + +/// Event to reset purchase details confirmation +class ResetPurchaseDetailsEvent extends CheckoutEvent {} \ 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..00db8c2 --- /dev/null +++ b/lib/checkout/bloc/checkout/checkout_state.dart @@ -0,0 +1,52 @@ +part of 'checkout_bloc.dart'; + +/// State class for checkout screen +class CheckoutState { + final String? appliedCouponCode; + final double discountPercentage; + final bool isPurchaseDetailsConfirmed; + + const CheckoutState({ + this.appliedCouponCode, + this.discountPercentage = 0.0, + this.isPurchaseDetailsConfirmed = false, + }); + + /// Initial state + factory CheckoutState.initial() { + return const CheckoutState(); + } + + /// Copy with method for immutable state updates + CheckoutState copyWith({ + String? appliedCouponCode, + double? discountPercentage, + bool? isPurchaseDetailsConfirmed, + bool clearCoupon = false, + }) { + return CheckoutState( + appliedCouponCode: clearCoupon ? null : appliedCouponCode ?? this.appliedCouponCode, + discountPercentage: discountPercentage ?? this.discountPercentage, + isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed ?? this.isPurchaseDetailsConfirmed, + ); + } + + /// Calculate discount amount based on subtotal + double calculateDiscountAmount(double subtotal) { + return subtotal * (discountPercentage / 100); + } + + /// Calculate tax amount + double calculateTaxAmount(double subtotal, {double taxRate = 0.05}) { + final totalBeforeTax = subtotal - calculateDiscountAmount(subtotal); + return totalBeforeTax * taxRate; + } + + /// Calculate final total + double calculateFinalTotal(double subtotal, {double taxRate = 0.05}) { + final discountAmount = calculateDiscountAmount(subtotal); + final totalBeforeTax = subtotal - discountAmount; + final taxAmount = totalBeforeTax * taxRate; + return totalBeforeTax + taxAmount; + } +} \ No newline at end of file diff --git a/lib/checkout/bloc/pass_purchase_details_bloc.dart b/lib/checkout/bloc/pass_purchase_details_bloc.dart new file mode 100644 index 0000000..146cd34 --- /dev/null +++ b/lib/checkout/bloc/pass_purchase_details_bloc.dart @@ -0,0 +1,61 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../profile/repository/profile_repository.dart'; +import 'pass_purchase_details_event.dart'; +import 'pass_purchase_details_state.dart'; + +class PurchaseDetailsBloc + extends Bloc { + final ProfileRepository _profileRepository; + + PurchaseDetailsBloc({ProfileRepository? profileRepository}) + : _profileRepository = profileRepository ?? ProfileRepository(), + super(PurchaseDetailsInitial()) { + on(_onLoadProfile); + on(_onSetPurchaseDetails); + on(_onToggleGiftMode); + } + + Future _onLoadProfile( + LoadProfileEvent event, + Emitter emit, + ) async { + emit(PurchaseDetailsProfileLoading(isGift: state.isGift)); + + try { + final profile = await _profileRepository.fetchUserProfile(); + emit(PurchaseDetailsLoaded( + isGift: state.isGift, + profile: profile, + )); + } catch (e) { + // Handle error - emit loaded state with null profile + emit(PurchaseDetailsLoaded( + isGift: state.isGift, + profile: null, + )); + } + } + + void _onSetPurchaseDetails( + SetPurchaseDetailsEvent event, + Emitter emit, + ) { + final isGift = event.buyPassValue == "gift"; + emit(PurchaseDetailsUpdated( + buyPassState: event.buyPassValue, + isGift: isGift, + profile: state.profile, + )); + } + + void _onToggleGiftMode( + ToggleGiftModeEvent event, + Emitter emit, + ) { + emit(PurchaseDetailsLoaded( + isGift: event.isGift, + profile: state.profile, + )); + } +} \ No newline at end of file diff --git a/lib/checkout/bloc/pass_purchase_details_event.dart b/lib/checkout/bloc/pass_purchase_details_event.dart new file mode 100644 index 0000000..deb389a --- /dev/null +++ b/lib/checkout/bloc/pass_purchase_details_event.dart @@ -0,0 +1,15 @@ +abstract class PassPurchaseDetailsEvent {} + +class SetPurchaseDetailsEvent extends PassPurchaseDetailsEvent { + final String buyPassValue; // "self" or "gift" + + SetPurchaseDetailsEvent(this.buyPassValue); +} + +class LoadProfileEvent extends PassPurchaseDetailsEvent {} + +class ToggleGiftModeEvent extends PassPurchaseDetailsEvent { + final bool isGift; + + ToggleGiftModeEvent(this.isGift); +} \ No newline at end of file diff --git a/lib/checkout/bloc/pass_purchase_details_state.dart b/lib/checkout/bloc/pass_purchase_details_state.dart new file mode 100644 index 0000000..e61345f --- /dev/null +++ b/lib/checkout/bloc/pass_purchase_details_state.dart @@ -0,0 +1,51 @@ +import '../../profile/models/profile_model.dart'; + +abstract class PurchaseDetailsState { + final bool isGift; + final ProfileModel? profile; + final bool isLoadingProfile; + + PurchaseDetailsState({ + this.isGift = false, + this.profile, + this.isLoadingProfile = false, + }); +} + +class PurchaseDetailsInitial extends PurchaseDetailsState { + PurchaseDetailsInitial() : super(isLoadingProfile: true); +} + +class PurchaseDetailsLoaded extends PurchaseDetailsState { + PurchaseDetailsLoaded({ + required bool isGift, + ProfileModel? profile, + }) : super( + isGift: isGift, + profile: profile, + isLoadingProfile: false, + ); +} + +class PurchaseDetailsUpdated extends PurchaseDetailsState { + final String buyPassState; // "self" or "gift" + + PurchaseDetailsUpdated({ + required this.buyPassState, + required bool isGift, + ProfileModel? profile, + }) : super( + isGift: isGift, + profile: profile, + isLoadingProfile: false, + ); +} + +class PurchaseDetailsProfileLoading extends PurchaseDetailsState { + PurchaseDetailsProfileLoading({ + required bool isGift, + }) : super( + isGift: isGift, + isLoadingProfile: true, + ); +} \ No newline at end of file diff --git a/lib/checkout/bloc/purchase_details_bloc.dart b/lib/checkout/bloc/purchase_details_bloc.dart deleted file mode 100644 index c22cc45..0000000 --- a/lib/checkout/bloc/purchase_details_bloc.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; - -abstract class PurchaseDetails {} - -class SetPurchaseDetailsEvent extends PurchaseDetails { - final String buyPassValue; - - SetPurchaseDetailsEvent(this.buyPassValue); -} - -class PurchaseDetailsState { - final String buyPassState; - - PurchaseDetailsState(this.buyPassState); -} - -class PurchaseDetailsBloc - extends Bloc { - PurchaseDetailsBloc() : super(PurchaseDetailsState("")) { - on((event, emit){ - emit(PurchaseDetailsState(event.buyPassValue)); - }); - } -} diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index b64717a..8813992 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -5,18 +5,90 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../StripePayment/view/stripe_payment.dart'; +import '../../buy_a_pass/models/checkout_model.dart'; import '../../common_packages/common_app_texts.dart'; import '../../localPreference/local_preference.dart'; import '../../postcard/widgets/purchase_details_bottom_sheet.dart'; +import '../bloc/pass_purchase_details_bloc.dart'; import '../widget/pass_purchase_details_bottomsheet.dart'; -class CheckoutView extends StatelessWidget { +class CheckoutView extends StatefulWidget { const CheckoutView({super.key}); + @override + State createState() => _CheckoutViewState(); +} + +class _CheckoutViewState extends State { + // For coupon/discount management + String? appliedCouponCode; + double discountPercentage = 0.0; + bool isPurchaseDetailsConfirmed = false; + @override Widget build(BuildContext context) { + // ✅ Receive checkout data from navigation arguments + final arguments = ModalRoute.of(context)?.settings.arguments; + + CheckoutData? checkoutData; + + if (arguments is CheckoutData) { + checkoutData = arguments; + print("✅ CHECKOUT DATA RECEIVED!"); + print(" City: ${checkoutData.cityName}"); + print(" Adults: ${checkoutData.adultCount}"); + print(" Children: ${checkoutData.childCount}"); + print(" Total: \$${checkoutData.totalPrice}"); + } else { + print("❌ NO CHECKOUT DATA - showing error screen"); + } + + // ✅ If no data passed, show error or default values + if (checkoutData == null) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 60.sp, color: Colors.red), + SizedBox(height: 16.h), + CustomText( + text: "No checkout data available", + size: 16.sp, + color: Colors.red, + ), + SizedBox(height: 8.h), + CustomText( + text: "Arguments type: ${arguments.runtimeType}", + size: 12.sp, + color: Colors.grey, + ), + SizedBox(height: 20.h), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text("Go Back"), + ), + ], + ), + ), + ), + ); + } + + // ✅ 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 Scaffold( resizeToAvoidBottomInset: true, backgroundColor: Colors.white, @@ -25,19 +97,20 @@ class CheckoutView extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 20.w), child: Column( children: [ + // ✅ App Bar CommonAppBar( isWhiteLogo: false, isProfilePage: false, showCart: false, showDivider: true, ), + + // ✅ Back Button & Title Row( children: [ GestureDetector( - onTap: () { - Navigator.pop(context); - }, - child: Icon(Icons.arrow_back), + onTap: () => Navigator.pop(context), + child: const Icon(Icons.arrow_back), ), SizedBox(width: 8.w), CustomText(text: "Checkout", size: 12.sp), @@ -45,54 +118,92 @@ class CheckoutView extends StatelessWidget { ), SizedBox(height: 22.h), - Expanded( - child: Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Color(0xFFF95FAF).withOpacity(0.2)), - borderRadius: BorderRadius.circular(8.r), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.r), - bottomLeft: Radius.circular(8.r), - ), - child: Image.asset( - "assets/images/card_banner.png", - scale: 4, - width: 105.w, - height: 123.h, - fit: BoxFit.cover, - ), - ), - SizedBox(width: 6.66.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText( - text: "Melbourne", - weight: FontWeight.w500, - size: 16.sp, - ), - SizedBox(height: 5.h), - CustomText( - text: "2 Days", - color: Color(0xFF8E8E8E), - size: 12.sp, - ), - SizedBox(height: 5.h), - SizedBox( - width: MediaQuery.of(context).size.width * .5, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ + // ✅ PASS CARD SECTION (showing pass details) + 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: [ + 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, + 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, + ), + ), + ), + ); + }, + ) + : _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( @@ -101,122 +212,125 @@ class CheckoutView extends StatelessWidget { ), SizedBox(width: 4.w), CustomText( - text: "3 adults", - color: Color(0xFF8E8E8E), + text: + "${checkoutData.adultCount} adult${checkoutData.adultCount > 1 ? 's' : ''}", + color: const Color(0xFF8E8E8E), size: 12.sp, ), ], ), - Row( - children: [ - Image.asset( - 'assets/icons/qty.png', - scale: 4, - ), - SizedBox(width: 4.w), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Qty:", - style: TextStyle( - color: Color(0xFF8E8E8E), - fontSize: 12.sp, - ), + // Total Quantity + Row( + children: [ + Image.asset( + 'assets/icons/qty.png', + scale: 4, + ), + SizedBox(width: 4.w), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Qty:", + style: TextStyle( + color: const Color(0xFF8E8E8E), + fontSize: 12.sp, ), - TextSpan( - text: " 2", - style: TextStyle( - color: Color(0xFF000000), - fontSize: 12.sp, - fontWeight: FontWeight.w500, - ), + ), + TextSpan( + text: + " ${checkoutData.totalQuantity}", + style: TextStyle( + color: const Color(0xFF000000), + fontSize: 12.sp, + fontWeight: FontWeight.w500, ), - ], - ), + ), + ], ), - ], - ), - ], - ), - ), - - SizedBox(height: 5.h), - Row( - children: [ - Image.asset("assets/icons/kid.png", scale: 4), - SizedBox(width: 4.w), - CustomText( - text: "3 Kids", - color: Color(0xFF8E8E8E), - size: 12.sp, - ), - - SizedBox(width: 53.w), - - CustomText( - text: "\$49.50", - size: 24.sp, - weight: FontWeight.w500, - color: Color(0xFFF95F62), - ), - ], - ), - ], - ), - ], - ), - - Container( - width: 35.w, - height: 123.h, - decoration: BoxDecoration( - color: Color(0xFFF97316), - borderRadius: BorderRadius.only( - bottomRight: Radius.circular(8.r), - topRight: Radius.circular(8.r), - ), - ), - child: RotatedBox( - quarterTurns: -1, - child: Center( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: "${CommonAppText.selectiveCard} ", - style: TextStyle( - color: Colors.white, - fontSize: 16.sp, - ), - ), - TextSpan( - text: "Card", - style: TextStyle( - color: Colors.white, - fontSize: 12.sp, - ), + ), + ], ), ], ), ), + + SizedBox(height: 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, + ), + ], + ), + ], + ), + ], + ), + + // ✅ 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: 10.h), + + // ✅ COUPON SECTION Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( - color: Color(0xFFFFF5F5), + color: const Color(0xFFFFF5F5), borderRadius: BorderRadius.circular(8.r), border: Border.all( - color: Color(0xFFBB474A).withOpacity(0.4), + color: const Color(0xFFBB474A).withOpacity(0.4), width: 0.8, ), ), @@ -227,13 +341,14 @@ class CheckoutView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomText( - text: "Get 10% off on your first trip", - color: Color(0xFF262626), + 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( - mainAxisAlignment: MainAxisAlignment.start, children: [ GestureDetector( onTap: () { @@ -246,36 +361,52 @@ class CheckoutView extends StatelessWidget { top: Radius.circular(12.r), ), ), - builder: (_) => AllCouponsBottomsheet(), + builder: (_) => AllCouponsBottomsheet(), ); }, child: CustomText( text: "View all coupons", - color: Color(0xFFF95F62), + color: const Color(0xFFF95F62), size: 12, ), ), SizedBox(width: 3.w), - Icon(Icons.arrow_right, color: Color(0xFFF95F62)), + const Icon( + Icons.arrow_right, + color: Color(0xFFF95F62), + ), ], ), ], ), - const Spacer(), - Container( - padding: EdgeInsets.symmetric( - horizontal: 20.w, - vertical: 10.h, - ), - decoration: BoxDecoration( - border: Border.all(color: Color(0xFFF95F62)), - borderRadius: BorderRadius.circular(8.r), - ), - child: CustomText( - text: "Apply", - color: Color(0xFFF95F62), - size: 14.sp, + 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, + ), ), ), ], @@ -284,45 +415,56 @@ class CheckoutView extends StatelessWidget { SizedBox(height: 15.h), + // ✅ PRICING BREAKDOWN DashedDivider( - color: Color(0xFFACACAC), - thickness: 1.h, - dashLength: 4, - dashSpace: 4, - ), - SizedBox(height: 10.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomText(text: "Subtotal", size: 14.sp), - CustomText( - text: "\$49.50", - size: 14.sp, - weight: FontWeight.w500, - ), - ], - ), - SizedBox(height: 14.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomText(text: "Discount", size: 14.sp), - CustomText( - text: "-7.20%", - size: 14.sp, - weight: FontWeight.w500, - ), - ], - ), - SizedBox(height: 10.h), - DashedDivider( - color: Color(0xFFACACAC), + 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: [ @@ -333,7 +475,8 @@ class CheckoutView extends StatelessWidget { CustomText(text: 'Total', size: 14.sp), SizedBox(height: 4.h), CustomText( - text: "Including \$2.24 in taxes", + text: + "Including \$${taxAmount.toStringAsFixed(2)} in taxes", size: 12.sp, color: Colors.black.withOpacity(0.6), ), @@ -341,23 +484,56 @@ class CheckoutView extends StatelessWidget { ), ), CustomText( - text: "\$42.60", + text: "\$${finalTotal.toStringAsFixed(2)}", size: 24.sp, weight: FontWeight.w500, ), ], ), + const Spacer(), + + // ✅ CHECKOUT BUTTON FutureBuilder( future: LocalPreference.getLogin(), builder: (context, snapshot) { final isLoggedIn = snapshot.data ?? false; return CustomFilledButton( - onTap: () { + onTap: () async { if (isLoggedIn) { - // Show purchase details bottom sheet if logged in - PassPurchaseBottomSheet.show(context); + 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 + }, + ), + ), + ); + + // 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); + + // If user selected 'self', show purchase confirmation + if (result == 'self') { + setState(() { + isPurchaseDetailsConfirmed = true; + }); + } + } } else { // Show login bottom sheet if not logged in showModalBottomSheet( @@ -374,7 +550,11 @@ class CheckoutView extends StatelessWidget { } }, width: double.infinity, - label: isLoggedIn ? "Checkout" : "Login to Checkout", + label: isLoggedIn + ? (isPurchaseDetailsConfirmed + ? "Pay \$${finalTotal.toStringAsFixed(2)}" + : "Checkout") + : "Login to Checkout", ); }, ), @@ -385,4 +565,18 @@ class CheckoutView extends StatelessWidget { ), ); } -} + + // ✅ Fallback image widget + Widget _fallbackImage() { + return Container( + width: 105.w, + height: 140.h, + color: Colors.grey[200], + child: Icon( + Icons.card_travel, + size: 40.sp, + color: Colors.grey[400], + ), + ); + } +} \ No newline at end of file diff --git a/lib/checkout/widget/pass_purchase_details_bottomsheet.dart b/lib/checkout/widget/pass_purchase_details_bottomsheet.dart index 8bf37fd..43f0718 100644 --- a/lib/checkout/widget/pass_purchase_details_bottomsheet.dart +++ b/lib/checkout/widget/pass_purchase_details_bottomsheet.dart @@ -1,17 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../add_details/add_details_view.dart'; +import '../../profile/repository/profile_repository.dart'; +import '../../profile/view/edit_profile/edit_profile_view.dart'; +import '../bloc/pass_purchase_details_bloc.dart'; +import '../bloc/pass_purchase_details_event.dart'; +import '../bloc/pass_purchase_details_state.dart'; class PassPurchaseBottomSheet { - static void show(BuildContext context) { - showModalBottomSheet( + static Future show(BuildContext context) async { + return await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (BuildContext modalContext) { - return _PassPurchaseContent(); + builder: (_) { + return BlocProvider( + create: (_) => PurchaseDetailsBloc()..add(LoadProfileEvent()), + child: _PassPurchaseContent(), + ); }, ); } @@ -21,218 +32,218 @@ class PassPurchaseBottomSheet { } } -class _PassPurchaseContent extends StatefulWidget { - @override - State<_PassPurchaseContent> createState() => _PassPurchaseContentState(); -} - -class _PassPurchaseContentState extends State<_PassPurchaseContent> { - bool isGift = false; - +class _PassPurchaseContent extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - top: 16, - left: 16, - right: 16, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Handle bar - Container( - width: 45, - height: 5, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(10), - ), + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + top: 16, + left: 16, + right: 16, ), - const SizedBox(height: 12), - - // Title - Text( - "Purchase Details", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 24), - - // Option 1: Buy Pass for Myself - GestureDetector( - onTap: () => setState(() => isGift = false), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: !isGift - ? Border.all( - color: const Color(0xffF95F62), - width: 1.5, - ) - : null, - ), - child: Row( - children: [ - Radio( - value: false, - groupValue: isGift, - onChanged: (value) => setState(() => isGift = false), - activeColor: const Color(0xffF95F62), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Buy Pass for Myself", - style: TextStyle( - fontWeight: FontWeight.w600, - color: !isGift - ? const Color(0xffF95F62) - : const Color(0xff9E9E9E), - ), - ), - if (!isGift) ...[ - const SizedBox(height: 8), - const Text( - "Frank Adam", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Color(0xff1A1A1A), - ), - ), - const Text( - "132 My Street, Kingston, NY\n12401", - style: TextStyle( - fontSize: 13, - color: Color(0xff5E5E5E), - ), - ), - ], - ], - ), - ), - if (!isGift) - ElevatedButton( - onPressed: () { - // Handle edit details - PassPurchaseBottomSheet.close(context); - // Navigate to edit details screen - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: const EdgeInsets.symmetric( - horizontal: 14, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text( - "Edit Details", - style: TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ), - ), - - const SizedBox(height: 20), - - // Option 2: Gift the Pass - GestureDetector( - onTap: () => setState(() => isGift = true), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: isGift - ? Border.all( - color: const Color(0xffF95F62), - width: 1.5, - ) - : null, - ), - child: Row( - children: [ - Radio( - value: true, - groupValue: isGift, - onChanged: (value) => setState(() => isGift = true), - activeColor: const Color(0xffF95F62), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Gift the pass", - style: TextStyle( - fontWeight: FontWeight.w600, - color: isGift - ? const Color(0xffF95F62) - : const Color(0xff9E9E9E), - ), - ), - if (isGift) - const SizedBox(height: 4), - if (isGift) - const Text( - "Gift the pass for someone else", - style: TextStyle( - fontSize: 13, - color: Color(0xff9E9E9E), - ), - ), - ], - ), - ), - ], - ), - ), - ), - - const SizedBox(height: 15), - - // Proceed Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - PassPurchaseBottomSheet.close(context); - // Handle proceed action - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 45, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(10), ), ), - child: Text( - "Proceed", - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, + const SizedBox(height: 12), + + Text( + "Purchase Details", + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 24), + + /// BUY FOR MYSELF + GestureDetector( + onTap: () { + context.read().add(ToggleGiftModeEvent(false)); + context.read().add(SetPurchaseDetailsEvent("self")); + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: !state.isGift + ? Border.all(color: const Color(0xffF95F62), width: 1.5) + : null, + ), + child: Row( + children: [ + Radio( + value: false, + groupValue: state.isGift, + onChanged: (_) {}, + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Buy Pass for Myself", + style: TextStyle( + fontWeight: FontWeight.w600, + color: !state.isGift + ? const Color(0xffF95F62) + : const Color(0xff9E9E9E), + ), + ), + if (!state.isGift && state.profile != null) ...[ + const SizedBox(height: 8), + Text( + "${state.profile!.firstName} ${state.profile!.lastName}", + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + Text( + "${state.profile!.address1 ?? ""}\n${state.profile!.address2 ?? ""}", + style: const TextStyle( + fontSize: 13, + color: Color(0xff5E5E5E), + ), + ), + ], + if (!state.isGift && state.isLoadingProfile) ...[ + const SizedBox(height: 8), + const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xffF95F62), + ), + ), + ], + ], + ), + ), + if (!state.isGift) + ElevatedButton( + onPressed: () async { + PassPurchaseBottomSheet.close(context); + await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const EditProfilePage(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + "Edit Details", + style: TextStyle(fontSize: 12, color: Colors.white), + ), + ), + ], + ), ), ), - ), + + const SizedBox(height: 20), + + /// GIFT PASS + GestureDetector( + onTap: () { + context.read().add(ToggleGiftModeEvent(true)); + context.read().add(SetPurchaseDetailsEvent("gift")); + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: state.isGift + ? Border.all(color: const Color(0xffF95F62), width: 1.5) + : null, + ), + child: Row( + children: [ + Radio( + value: true, + groupValue: state.isGift, + onChanged: (_) {}, + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + "Gift the pass", + style: TextStyle(fontWeight: FontWeight.w600), + ), + SizedBox(height: 4), + Text( + "Gift the pass for someone else", + style: TextStyle( + fontSize: 13, color: Color(0xff9E9E9E)), + ), + ], + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 15), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + // Close bottom sheet and return the selection + Navigator.of(context).pop(state.isGift ? 'gift' : 'self'); + + if (state.isGift) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => AddDetailsView(), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Proceed", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(height: 15), + ], ), - const SizedBox(height: 15), - ], - ), + ); + }, ); } } \ No newline at end of file diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 4c42ce8..6180a3c 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -12,7 +12,7 @@ class CommonAppBar extends StatelessWidget { this.showCart = true, required this.showDivider, this.imageUrl, - this.isSelectCity = false, // ✅ NEW PARAMETER (default false) + this.isSelectCity = false, }); final bool isWhiteLogo; @@ -20,10 +20,13 @@ class CommonAppBar extends StatelessWidget { final bool? showCart; final bool showDivider; final String? imageUrl; - final bool isSelectCity; // ✅ NEW + final bool isSelectCity; @override Widget build(BuildContext context) { + final bool isPathIcon = + imageUrl != null && imageUrl!.isNotEmpty; + return Column( children: [ Row( @@ -32,28 +35,31 @@ class CommonAppBar extends StatelessWidget { /// LEFT SIDE Row( children: [ - /// ✅ Logo handling - imageUrl != null && imageUrl!.isNotEmpty - ? Image.network( - imageUrl!, - scale: 4, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - isWhiteLogo - ? "assets/logo/logo_city_cards_white.png" - : "assets/logo/logo_city_cards.png", - scale: 4, - ); - }, - ) - : Image.asset( - isWhiteLogo - ? "assets/logo/logo_city_cards_white.png" - : "assets/logo/logo_city_cards.png", - scale: 4, + /// ✅ LOGO / PATH ICON (SIZE CONTROLLED) + SizedBox( + height: isPathIcon ? 40.h : 32.h, // 🔥 ONLY path icon bigger + child: isPathIcon + ? Image.network( + imageUrl!, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + isWhiteLogo + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", + fit: BoxFit.contain, + ); + }, + ) + : Image.asset( + isWhiteLogo + ? "assets/logo/logo_city_cards_white.png" + : "assets/logo/logo_city_cards.png", + fit: BoxFit.contain, + ), ), - /// ✅ Show dropdown ONLY if isSelectCity == true + /// ✅ CITY DROPDOWN if (isSelectCity) IconButton( onPressed: () { @@ -120,7 +126,6 @@ class CommonAppBar extends StatelessWidget { ), ], ), - /// DIVIDER if (showDivider) Column( diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart index 385896e..595b01d 100644 --- a/lib/login/view/verify_otp_bottomsheet.dart +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -1,5 +1,7 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/profile/bloc/profile/profile_bloc.dart'; +import 'package:citycards_customer/profile/bloc/profile/profile_event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; @@ -34,6 +36,9 @@ class _VerifyOtpBottomsheetState extends State { if (state.response.userExists) { await LocalPreference.setLogin(true); + final userId = await LocalPreference.getUserId(); + context.read().add(FetchProfileEvent(userId: userId!)); + context.read().add(CheckLoginStatusEvent()); // User exists - navigate to home/dashboard // Navigator.of(context).pushReplacementNamed(RouteConstants.home); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/main.dart b/lib/main.dart index 445ee18..529fe33 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter_stripe/flutter_stripe.dart'; // ADD THIS import 'core/app_router.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart'; @@ -20,9 +21,13 @@ import 'profile/bloc/profile/profile_bloc.dart'; import 'search_offers/repository/offers_repository.dart'; import 'search_offers/view/search_offers_with_listing.dart'; -void main() { +void main() async { // CHANGE: Add async WidgetsFlutterBinding.ensureInitialized(); + Stripe.publishableKey = 'pk_test_51SrwZ7RtCkWyT4EmmP5ozlEVEscMvWPDPVshQbNdQe1S27iGkwZ7YD4BSEm7TUvvlgPRvznuQq6daVyA9p1UWNnz00WVsaw2Yj'; // Replace with your key + // Stripe.merchantIdentifier = 'merchant.com.citycards'; // Optional + // Stripe.urlScheme = 'flutterstripe'; // Optional + SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.white, @@ -64,12 +69,11 @@ class MyApp extends StatelessWidget { loginRepository: LoginRepository(), ), ), - BlocProvider( - create: (_) => OffersBloc(OffersRepository()), - child: const OffersScreen(), - ), - BlocProvider(create: (context) => ProfileBloc()), - + BlocProvider( + create: (_) => OffersBloc(OffersRepository()), + child: const OffersScreen(), + ), + BlocProvider(create: (context) => ProfileBloc()), ], child: MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, @@ -86,4 +90,4 @@ class MyApp extends StatelessWidget { }, ); } -} +} \ No newline at end of file diff --git a/lib/postcard/views/my_orders_page_view.dart b/lib/postcard/views/my_orders_page_view.dart index d78ca05..f21470f 100644 --- a/lib/postcard/views/my_orders_page_view.dart +++ b/lib/postcard/views/my_orders_page_view.dart @@ -401,3 +401,417 @@ class _MyOrdersPageViewState extends State { ); } } + +// import 'package:citycards_customer/common_packages/app_bar.dart'; +// import 'package:citycards_customer/core/route_constants.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_screenutil/flutter_screenutil.dart'; +// import 'package:google_fonts/google_fonts.dart'; +// +// class MyOrdersPageView extends StatefulWidget { +// const MyOrdersPageView({super.key}); +// +// @override +// State createState() => _MyOrdersPageViewState(); +// } +// +// class _MyOrdersPageViewState extends State { +// bool showDrafts = true; +// +// @override +// Widget build(BuildContext context) { +// return SafeArea( +// child: Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // 🏙️ Header +// CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), +// +// Row( +// children: [ +// Expanded( +// child: GestureDetector( +// onTap: () => setState(() => showDrafts = true), +// child: Container( +// padding: const EdgeInsets.symmetric(vertical: 12), +// decoration: BoxDecoration( +// color: showDrafts +// ? const Color(0xffF95F62).withOpacity(0.24) +// : Colors.transparent, +// borderRadius: BorderRadius.circular(12), +// border: Border.all( +// color: showDrafts +// ? const Color(0xffF95F62).withOpacity(0.4) +// : const Color(0xffE0E0E0), +// ), +// ), +// child: Center( +// child: Text( +// "My drafts", +// style: TextStyle( +// fontWeight: FontWeight.w400, +// fontSize: 14.sp, +// color: showDrafts +// ? Colors.black +// : Colors.black.withOpacity(0.56), +// ), +// ), +// ), +// ), +// ), +// ), +// const SizedBox(width: 12), +// Expanded( +// child: GestureDetector( +// onTap: () => setState(() => showDrafts = false), +// child: Container( +// padding: const EdgeInsets.symmetric(vertical: 12), +// decoration: BoxDecoration( +// color: !showDrafts +// ? const Color(0xffF95F62).withOpacity(0.24) +// : Colors.transparent, +// borderRadius: BorderRadius.circular(12), +// border: Border.all( +// color: !showDrafts +// ? const Color(0xffF95F62).withOpacity(0.4) +// : const Color(0xffE0E0E0), +// ), +// ), +// child: Center( +// child: Text( +// "My orders", +// style: TextStyle( +// fontWeight: FontWeight.w400, +// fontSize: 14.sp, +// color: !showDrafts +// ? Colors.black +// : Colors.black.withOpacity(0.56), +// ), +// ), +// ), +// ), +// ), +// ), +// ], +// ), +// const SizedBox(height: 24), +// +// // 📬 Postcard List +// showDrafts +// ? Expanded( +// child: ListView.builder( +// itemCount: 5, +// itemBuilder: (context, index) { +// return Container( +// margin: const EdgeInsets.only(bottom: 16), +// padding: const EdgeInsets.fromLTRB( +// 10, +// 10, +// 10, +// 10, +// ), +// decoration: BoxDecoration( +// color: const Color(0xFFFFF5F5), +// borderRadius: BorderRadius.circular(12), +// border: Border.all( +// color: const Color(0xffF1F5F7), +// ), +// ), +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// ClipRRect( +// borderRadius: BorderRadius.circular(8), +// child: Container( +// height: 90.h, +// width: 90.w, +// color: const Color(0xffFEE7E7), +// child: const Icon( +// Icons.image_outlined, +// color: Color(0xffFDCDCE), +// size: 40, +// ), +// ), +// ), +// const SizedBox(width: 20), +// +// Expanded( +// child: SizedBox( +// height: 90.h, +// child: Stack( +// children: [ +// /// Centered texts +// Align( +// alignment: Alignment.centerLeft, +// child: Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: +// CrossAxisAlignment.start, +// children: [ +// Text( +// "#688574", +// style: GoogleFonts.poppins( +// fontSize: 16.sp, +// fontWeight: FontWeight.w400, +// color: Colors.black, +// ), +// ), +// const SizedBox(height: 4), +// Text( +// "Created 24 Jan 2025", +// style: GoogleFonts.poppins( +// fontSize: 12.sp, +// fontWeight: FontWeight.w400, +// color: +// const Color(0xff999999), +// ), +// ), +// ], +// ), +// ), +// +// /// Top-right buttons +// Positioned( +// top: 0, +// right: 0, +// child: Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// InkWell( +// onTap: () {}, +// child: Image.asset( +// "assets/icons/delete_icon.png", +// scale: 3.5, +// ), +// ), +// const SizedBox(width: 20), +// InkWell( +// onTap: () {}, +// child: Image.asset( +// "assets/icons/edit_icon.png", +// scale: 3.5, +// ), +// ), +// ], +// ), +// ), +// +// /// Bottom-right "Preview" link +// Positioned( +// bottom: 0, +// right: 0, +// child: InkWell( +// onTap: () { +// // Navigate to preview +// // You can use Navigator or your routing solution +// }, +// child: Row( +// children: [ +// const Icon( +// Icons.remove_red_eye_outlined, +// size: 15, +// color: Color(0xffF95F62), +// ), +// SizedBox(width: 5.w), +// Text( +// "Preview", +// style: TextStyle( +// fontWeight: FontWeight.w400, +// color: +// const Color(0xffF95F62), +// ), +// ), +// ], +// ), +// ), +// ), +// ], +// ), +// ), +// ), +// ], +// ), +// ); +// }, +// ), +// ) +// : Expanded( +// child: ListView.builder( +// itemCount: 3, +// itemBuilder: (context, index) { +// return Container( +// margin: const EdgeInsets.only(bottom: 16), +// padding: const EdgeInsets.fromLTRB( +// 16, +// 16, +// 16, +// 16, +// ), +// decoration: BoxDecoration( +// color: const Color(0xFFFFF5F5), +// borderRadius: BorderRadius.circular(12), +// border: Border.all( +// color: const Color(0xffF1F5F7), +// ), +// ), +// child: Row( +// crossAxisAlignment: +// CrossAxisAlignment.center, +// children: [ +// ClipRRect( +// borderRadius: BorderRadius.circular(8), +// child: Container( +// height: 70.h, +// width: 70.w, +// color: const Color(0xffFEE7E7), +// child: const Icon( +// Icons.image_outlined, +// color: Color(0xffFDCDCE), +// size: 30, +// ), +// ), +// ), +// const SizedBox(width: 20), +// +// Expanded( +// child: SizedBox( +// height: 60.h, +// child: Row( +// mainAxisAlignment: +// MainAxisAlignment.spaceBetween, +// children: [ +// Column( +// crossAxisAlignment: +// CrossAxisAlignment.start, +// mainAxisAlignment: +// MainAxisAlignment.start, +// children: [ +// Text( +// "My PostCard", +// style: GoogleFonts.poppins( +// color: Colors.black, +// fontWeight: +// FontWeight.w400, +// fontSize: 16.sp, +// ), +// ), +// const SizedBox(height: 6), +// Text( +// "5 Post cards", +// style: GoogleFonts.poppins( +// color: Colors.black, +// fontWeight: +// FontWeight.w400, +// fontSize: 14.sp, +// ), +// ), +// ], +// ), +// Column( +// mainAxisAlignment: +// MainAxisAlignment +// .spaceBetween, +// crossAxisAlignment: +// CrossAxisAlignment.end, +// children: [ +// Container( +// padding: EdgeInsets.fromLTRB(13, 7, 13, 7), +// decoration: BoxDecoration( +// color: Color( +// 0xff00FFA6, +// ).withOpacity(0.16), +// border: Border.all( +// color: Color( +// 0xff439F6E, +// ), +// ), +// borderRadius: +// BorderRadius.circular( +// 16, +// ), +// ), +// child: Text( +// "In Progress", +// style: TextStyle( +// color: Colors.black, +// fontWeight: +// FontWeight.w400, +// fontSize: 8.54.sp, +// ), +// ), +// ), +// InkWell( +// onTap: () { +// // Navigate to preview +// // You can use Navigator or your routing solution +// }, +// child: Row( +// children: [ +// Icon( +// Icons +// .remove_red_eye_outlined, +// size: 15, +// color: Color( +// 0xffF95F62, +// ), +// ), +// SizedBox(width: 5.w), +// Text( +// "Preview", +// style: TextStyle( +// fontWeight: +// FontWeight.w400, +// color: Color( +// 0xffF95F62, +// ), +// ), +// ), +// ], +// ), +// ), +// ], +// ), +// ], +// ), +// ), +// ), +// ], +// ), +// ); +// }, +// ), +// ), +// +// // ➕ Create postcard button +// SizedBox( +// width: double.infinity, +// child: ElevatedButton( +// onPressed: () { +// // Navigate to postcard creation flow (starts at upload photo step) +// Navigator.of(context).pushNamed(RouteConstants.uploadPhotoPage); +// }, +// style: ElevatedButton.styleFrom( +// backgroundColor: const Color(0xffF95F62), +// padding: EdgeInsets.symmetric(vertical: 16.h), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(40), +// ), +// ), +// child: Text( +// "Create post card", +// style: GoogleFonts.poppins( +// color: Colors.white, +// fontSize: 14.sp, +// fontWeight: FontWeight.w600, +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/profile/bloc/profile/profile_bloc.dart b/lib/profile/bloc/profile/profile_bloc.dart index 078b9b2..97fa946 100644 --- a/lib/profile/bloc/profile/profile_bloc.dart +++ b/lib/profile/bloc/profile/profile_bloc.dart @@ -1,5 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/foundation.dart'; +import '../../../localPreference/local_preference.dart'; import '../../repository/profile_repository.dart'; import 'profile_event.dart'; import 'profile_state.dart'; @@ -13,6 +14,10 @@ class ProfileBloc extends Bloc { on(_onFetchProfile); on(_onUpdateProfile); on(_onResetProfile); + on(_onCheckLoginStatus); + on(_onProfileImageSelected); + on(_onClearProfileImage); + on(_onLogout); } /// Handle fetching user profile @@ -21,68 +26,53 @@ class ProfileBloc extends Bloc { Emitter emit, ) async { try { - if (kDebugMode) { - print('🔄 [BLOC] FetchProfileEvent received for userId: ${event.userId}'); - } emit(const ProfileLoading()); final profile = await _profileRepository.fetchUserProfile(); - if (kDebugMode) { - print('✅ [BLOC] Profile fetched successfully: ${profile.firstName} ${profile.lastName}'); - print('✅ [BLOC] Profile Image URL: ${profile.profileImage}'); - } - emit(ProfileLoaded(profile: profile)); } catch (e) { final errorMessage = e.toString(); - if (kDebugMode) { - print('❌ [BLOC] Error fetching profile: $errorMessage'); - print('❌ [BLOC] Error type: ${e.runtimeType}'); - } - emit(ProfileError(message: errorMessage)); } } /// Handle updating user profile - /// ⭐ UPDATED: Now passes File to repository Future _onUpdateProfile( UpdateProfileEvent event, Emitter emit, ) async { try { if (kDebugMode) { - print('🔄 [BLOC] UpdateProfileEvent received'); - print('🔄 [BLOC] User ID: ${event.userId}'); - print('🔄 [BLOC] First Name: ${event.firstName}'); - print('🔄 [BLOC] Last Name: ${event.lastName}'); - print('🔄 [BLOC] Mobile: ${event.mobileNumber}'); - print('🔄 [BLOC] Address1: ${event.address1}'); - print('🔄 [BLOC] Address2: ${event.address2}'); + print('📄 [BLOC] UpdateProfileEvent received'); + print('📄 [BLOC] User ID: ${event.userId}'); + print('📄 [BLOC] First Name: ${event.firstName}'); + print('📄 [BLOC] Last Name: ${event.lastName}'); + print('📄 [BLOC] Mobile: ${event.mobileNumber}'); + print('📄 [BLOC] Address1: ${event.address1}'); + print('📄 [BLOC] Address2: ${event.address2}'); if (event.profileImageFile != null) { - print('🔄 [BLOC] ✅ Profile Image File Present in Event'); - print('🔄 [BLOC] File Path: ${event.profileImageFile!.path}'); + print('📄 [BLOC] ✅ Profile Image File Present in Event'); + print('📄 [BLOC] File Path: ${event.profileImageFile!.path}'); final fileSize = await event.profileImageFile!.length(); - print('🔄 [BLOC] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); + print('📄 [BLOC] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); } else { - print('🔄 [BLOC] ⚠️ Profile Image File is NULL in Event'); + print('📄 [BLOC] ⚠️ Profile Image File is NULL in Event'); } - print('🔄 [BLOC] Calling toJson()...'); + print('📄 [BLOC] Calling toJson()...'); final jsonData = event.toJson(); - print('🔄 [BLOC] JSON Data: $jsonData'); + print('📄 [BLOC] JSON Data: $jsonData'); } emit(const ProfileUpdating()); - // ⭐ Pass both data and file to repository final updatedProfile = await _profileRepository.updateUserProfile( data: event.toJson(), - profileImageFile: event.profileImageFile, // ⭐ NEW: Pass File + profileImageFile: event.profileImageFile, ); if (kDebugMode) { @@ -110,8 +100,111 @@ class ProfileBloc extends Bloc { Emitter emit, ) { if (kDebugMode) { - print('🔄 [BLOC] ResetProfileEvent received'); + print('📄 [BLOC] ResetProfileEvent received'); } emit(const ProfileInitial()); } + + /// ⭐ NEW: Handle checking login status + Future _onCheckLoginStatus( + CheckLoginStatusEvent event, + Emitter emit, + ) async { + try { + if (kDebugMode) { + print('📄 [BLOC] CheckLoginStatusEvent received'); + } + + emit(const LoginStatusChecking()); + + final loginStatus = await LocalPreference.getLogin(); + final userId = await LocalPreference.getUserId(); + + if (kDebugMode) { + print('📄 [BLOC] Login Status: $loginStatus'); + print('📄 [BLOC] User ID: $userId'); + } + + emit(LoginStatusChecked(isLoggedIn: loginStatus)); + + // Automatically fetch profile if logged in + if (loginStatus && userId != null) { + if (kDebugMode) { + print('📄 [BLOC] User is logged in, fetching profile...'); + } + add(FetchProfileEvent(userId: userId)); + } + } catch (e) { + if (kDebugMode) { + print('❌ [BLOC] Error checking login status: $e'); + } + emit(LoginStatusChecked(isLoggedIn: false)); + } + } + + /// ⭐ NEW: Handle profile image selection + void _onProfileImageSelected( + ProfileImageSelectedEvent event, + Emitter emit, + ) { + if (kDebugMode) { + print('📄 [BLOC] ProfileImageSelectedEvent received'); + print('📄 [BLOC] Image file path: ${event.imageFile.path}'); + } + + // Maintain the current profile data with the new image + final currentState = state; + if (currentState is ProfileLoaded) { + emit(ProfileLoaded( + profile: currentState.profile, + selectedImageFile: event.imageFile, + )); + } else if (currentState is ProfileUpdated) { + emit(ProfileLoaded( + profile: currentState.profile, + selectedImageFile: event.imageFile, + )); + } + } + + /// ⭐ NEW: Handle clearing selected image + void _onClearProfileImage( + ClearProfileImageEvent event, + Emitter emit, + ) { + if (kDebugMode) { + print('📄 [BLOC] ClearProfileImageEvent received'); + } + + final currentState = state; + if (currentState is ProfileLoaded) { + emit(ProfileLoaded( + profile: currentState.profile, + selectedImageFile: null, + )); + } + } + + /// ⭐ NEW: Handle logout + Future _onLogout( + LogoutEvent event, + Emitter emit, + ) async { + try { + if (kDebugMode) { + print('📄 [BLOC] LogoutEvent received'); + } + + // Clear local preferences (uncomment when ready) + // await LocalPreference.clearPreference(); + + emit(const ProfileLoggedOut()); + emit(const ProfileInitial()); + } catch (e) { + if (kDebugMode) { + print('❌ [BLOC] Error during logout: $e'); + } + emit(ProfileError(message: 'Failed to logout: $e')); + } + } } \ No newline at end of file diff --git a/lib/profile/bloc/profile/profile_event.dart b/lib/profile/bloc/profile/profile_event.dart index 6e0c04b..3ec20c4 100644 --- a/lib/profile/bloc/profile/profile_event.dart +++ b/lib/profile/bloc/profile/profile_event.dart @@ -19,7 +19,6 @@ class FetchProfileEvent extends ProfileEvent { } /// Event to update user profile -/// ⭐ UPDATED: Now accepts File instead of base64 string class UpdateProfileEvent extends ProfileEvent { final int userId; final String firstName; @@ -27,7 +26,7 @@ class UpdateProfileEvent extends ProfileEvent { final String mobileNumber; final String? address1; final String? address2; - final File? profileImageFile; // ⭐ CHANGED: File instead of String + final File? profileImageFile; const UpdateProfileEvent({ required this.userId, @@ -36,7 +35,7 @@ class UpdateProfileEvent extends ProfileEvent { required this.mobileNumber, this.address1, this.address2, - this.profileImageFile, // ⭐ CHANGED + this.profileImageFile, }); @override @@ -47,7 +46,7 @@ class UpdateProfileEvent extends ProfileEvent { mobileNumber, address1, address2, - profileImageFile, // ⭐ CHANGED + profileImageFile, ]; Map toJson() { @@ -57,7 +56,6 @@ class UpdateProfileEvent extends ProfileEvent { 'mobileNumber': mobileNumber, if (address1 != null && address1!.isNotEmpty) 'address1': address1, if (address2 != null && address2!.isNotEmpty) 'address2': address2, - // ⭐ Note: profileImageFile is handled separately in repository }; } } @@ -65,4 +63,29 @@ class UpdateProfileEvent extends ProfileEvent { /// Event to reset profile state class ResetProfileEvent extends ProfileEvent { const ResetProfileEvent(); +} + + +class CheckLoginStatusEvent extends ProfileEvent { + const CheckLoginStatusEvent(); +} + +/// ⭐ NEW: Event to handle image selection (temporary state management) +class ProfileImageSelectedEvent extends ProfileEvent { + final File imageFile; + + const ProfileImageSelectedEvent({required this.imageFile}); + + @override + List get props => [imageFile]; +} + +/// ⭐ NEW: Event to clear selected image +class ClearProfileImageEvent extends ProfileEvent { + const ClearProfileImageEvent(); +} + +/// ⭐ NEW: Event to handle logout +class LogoutEvent extends ProfileEvent { + const LogoutEvent(); } \ No newline at end of file diff --git a/lib/profile/bloc/profile/profile_state.dart b/lib/profile/bloc/profile/profile_state.dart index e67bfec..691e4ab 100644 --- a/lib/profile/bloc/profile/profile_state.dart +++ b/lib/profile/bloc/profile/profile_state.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:equatable/equatable.dart'; import '../../models/profile_model.dart'; @@ -13,6 +14,21 @@ class ProfileInitial extends ProfileState { const ProfileInitial(); } +/// ⭐ NEW: Login status checking state +class LoginStatusChecking extends ProfileState { + const LoginStatusChecking(); +} + +/// ⭐ NEW: Login status checked state +class LoginStatusChecked extends ProfileState { + final bool isLoggedIn; + + const LoginStatusChecked({required this.isLoggedIn}); + + @override + List get props => [isLoggedIn]; +} + /// Loading state for fetching profile class ProfileLoading extends ProfileState { const ProfileLoading(); @@ -21,11 +37,27 @@ class ProfileLoading extends ProfileState { /// Success state when profile is fetched class ProfileLoaded extends ProfileState { final ProfileModel profile; + final File? selectedImageFile; // ⭐ NEW: Track selected image - const ProfileLoaded({required this.profile}); + const ProfileLoaded({ + required this.profile, + this.selectedImageFile, + }); @override - List get props => [profile]; + List get props => [profile, selectedImageFile]; + + /// ⭐ NEW: Helper method to create a copy with updated image + ProfileLoaded copyWith({ + ProfileModel? profile, + File? selectedImageFile, + bool clearImage = false, + }) { + return ProfileLoaded( + profile: profile ?? this.profile, + selectedImageFile: clearImage ? null : (selectedImageFile ?? this.selectedImageFile), + ); + } } /// Loading state for updating profile @@ -47,6 +79,20 @@ class ProfileUpdated extends ProfileState { List get props => [profile, message]; } +/// ⭐ NEW: Image selected state (for edit screen) +class ProfileImageSelected extends ProfileState { + final ProfileModel profile; + final File selectedImageFile; + + const ProfileImageSelected({ + required this.profile, + required this.selectedImageFile, + }); + + @override + List get props => [profile, selectedImageFile]; +} + /// Error state class ProfileError extends ProfileState { final String message; @@ -55,4 +101,9 @@ class ProfileError extends ProfileState { @override List get props => [message]; +} + +/// ⭐ NEW: Logged out state +class ProfileLoggedOut extends ProfileState { + const ProfileLoggedOut(); } \ No newline at end of file diff --git a/lib/profile/repository/profile_repository.dart b/lib/profile/repository/profile_repository.dart index eb143d6..cbd035e 100644 --- a/lib/profile/repository/profile_repository.dart +++ b/lib/profile/repository/profile_repository.dart @@ -13,20 +13,10 @@ class ProfileRepository { Future fetchUserProfile() async { final int? userId = await LocalPreference.getUserId(); - if (kDebugMode) { - print('📥 [FETCH PROFILE] User ID: $userId'); - print('📥 [FETCH PROFILE] URL: ${ApiUrls.userProfile}/$userId'); - } - final response = await _apiService.getApi( url: '${ApiUrls.userProfile}/$userId', ); - if (kDebugMode) { - print('📥 [FETCH PROFILE] Response: ${response.data}'); - print('📥 [FETCH PROFILE] Profile Image: ${response.data['profileImage']}'); - } - return ProfileModel.fromJson(response.data); } diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart index 8630ba9..12b8b56 100644 --- a/lib/profile/view/edit_profile/edit_profile_view.dart +++ b/lib/profile/view/edit_profile/edit_profile_view.dart @@ -34,10 +34,6 @@ class _EditProfilePageState extends State { final _formKey = GlobalKey(); final ImagePicker _picker = ImagePicker(); - // Profile image variables - File? _selectedImageFile; // ⭐ Only need File now, no base64 - String? _currentProfileImageUrl; - @override void initState() { super.initState(); @@ -67,15 +63,9 @@ class _EditProfilePageState extends State { address1Controller.text = profile.address1 ?? ''; address2Controller.text = profile.address2 ?? ''; - // Store current profile image URL if exists - if (profile.profileImage != null && profile.profileImage!.isNotEmpty) { - setState(() { - _currentProfileImageUrl = profile.profileImage; - }); - - if (kDebugMode) { - print('🔵 [EDIT PROFILE] ✅ Current profile image URL set: $_currentProfileImageUrl'); - } + // ⭐ REMOVED setState - image is now managed by BLoC state + if (kDebugMode && profile.profileImage != null && profile.profileImage!.isNotEmpty) { + print('🔵 [EDIT PROFILE] ✅ Current profile image URL: ${profile.profileImage}'); } } @@ -161,6 +151,7 @@ class _EditProfilePageState extends State { ); } + // ⭐ UPDATED: Use BLoC event instead of setState Future _pickImage(ImageSource source) async { try { if (kDebugMode) { @@ -183,9 +174,10 @@ class _EditProfilePageState extends State { print('🔵 [EDIT PROFILE] File size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); } - setState(() { - _selectedImageFile = imageFile; // ⭐ Just store the File - }); + // ⭐ REPLACED setState with BLoC event + context.read().add( + ProfileImageSelectedEvent(imageFile: imageFile), + ); } } catch (e) { if (kDebugMode) { @@ -201,7 +193,8 @@ class _EditProfilePageState extends State { } } - Widget _buildProfileImageWidget() { + // ⭐ UPDATED: Accept selectedImageFile from BLoC state + Widget _buildProfileImageWidget(String? currentProfileImageUrl, File? selectedImageFile) { return Center( child: Stack( children: [ @@ -210,35 +203,28 @@ class _EditProfilePageState extends State { height: 76.w, decoration: BoxDecoration( shape: BoxShape.circle, - color: Color(0xFFFCE4E5), + color: const Color(0xFFFCE4E5), ), child: ClipOval( - child: _selectedImageFile != null + child: selectedImageFile != null ? Image.file( - _selectedImageFile!, + selectedImageFile, fit: BoxFit.cover, ) - : _currentProfileImageUrl != null && _currentProfileImageUrl!.isNotEmpty + : (currentProfileImageUrl != null && currentProfileImageUrl.isNotEmpty) ? Image.network( - _getFullImageUrl(_currentProfileImageUrl), + _getFullImageUrl(currentProfileImageUrl), fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Center( + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return const Center( child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - color: Color(0xFFF95F62), strokeWidth: 2, + color: Color(0xFFF95F62), ), ); }, - errorBuilder: (context, error, stackTrace) { - if (kDebugMode) { - print('❌ [EDIT PROFILE] Error loading image: $error'); - } + errorBuilder: (_, __, ___) { return Padding( padding: EdgeInsets.all(16.w), child: Image.asset( @@ -257,42 +243,78 @@ class _EditProfilePageState extends State { ), ), ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: _showImageSourceDialog, + child: Container( + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: const Color(0xFFF95F62), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + child: Icon( + Icons.camera_alt, + color: Colors.white, + size: 12.sp, + ), + ), + ), + ), ], ), ); } - void _saveProfile() async { - if (_formKey.currentState?.validate() ?? false) { - final userId = await LocalPreference.getUserId(); - if (userId != null) { - if (kDebugMode) { - print('🔵 [EDIT PROFILE] Saving profile...'); - if (_selectedImageFile != null) { - print('🔵 [EDIT PROFILE] ✅ New image will be uploaded'); - print('🔵 [EDIT PROFILE] File path: ${_selectedImageFile!.path}'); - } else { - print('🔵 [EDIT PROFILE] ⚠️ No new image selected'); - } - } - - context.read().add( - UpdateProfileEvent( - userId: userId, - firstName: firstNameController.text.trim(), - lastName: lastNameController.text.trim(), - mobileNumber: phoneController.text.trim(), - address1: address1Controller.text.trim().isEmpty - ? null - : address1Controller.text.trim(), - address2: address2Controller.text.trim().isEmpty - ? null - : address2Controller.text.trim(), - profileImageFile: _selectedImageFile, // ⭐ Pass File directly - ), - ); - } + Future _saveProfile() async { + if (!_formKey.currentState!.validate()) { + return; } + + final userId = await LocalPreference.getUserId(); + if (userId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('User ID not found'), + backgroundColor: Colors.red, + ), + ); + return; + } + + // ⭐ Get selectedImageFile from current BLoC state + File? imageFileToSend; + final currentState = context.read().state; + if (currentState is ProfileLoaded) { + imageFileToSend = currentState.selectedImageFile; + } + + if (kDebugMode) { + print('🔵 [EDIT PROFILE] Saving profile...'); + print('🔵 [EDIT PROFILE] Image file to send: ${imageFileToSend?.path ?? "null"}'); + } + + context.read().add( + UpdateProfileEvent( + userId: userId, + firstName: firstNameController.text.trim(), + lastName: lastNameController.text.trim(), + mobileNumber: phoneController.text.trim(), + address1: address1Controller.text.trim().isEmpty + ? null + : address1Controller.text.trim(), + address2: address2Controller.text.trim().isEmpty + ? null + : address2Controller.text.trim(), + profileImageFile: imageFileToSend, + ), + ); } @override @@ -307,70 +329,64 @@ class _EditProfilePageState extends State { @override Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state is ProfileLoaded) { - _populateFields(state.profile); - } else if (state is ProfileUpdated) { - if (kDebugMode) { - print('✅ [EDIT PROFILE] Profile updated successfully!'); - } + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: BlocConsumer( + listener: (context, state) { + if (state is ProfileUpdated) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context, true); + } else if (state is ProfileError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.red, + ), + ); + } else if (state is ProfileLoaded) { + _populateFields(state.profile); + } + }, + builder: (context, state) { + final isLoading = state is ProfileLoading || state is ProfileUpdating; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.green, - duration: const Duration(seconds: 2), - ), - ); - Navigator.pop(context, true); - } else if (state is ProfileError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.red, - duration: const Duration(seconds: 3), - ), - ); - } - }, - builder: (context, state) { - final isLoading = state is ProfileLoading || state is ProfileUpdating; - final isInitialLoading = state is ProfileLoading; + // ⭐ Extract profile data and selectedImageFile from state + ProfileModel? profile; + File? selectedImageFile; - if (isInitialLoading) { - return Scaffold( - backgroundColor: Colors.white, - body: Center( - child: CircularProgressIndicator( - color: Color(0xFFF95F62), - ), - ), - ); - } + if (state is ProfileLoaded) { + profile = state.profile; + selectedImageFile = state.selectedImageFile; + } else if (state is ProfileUpdated) { + profile = state.profile; + } - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Stack( + final currentProfileImageUrl = profile?.profileImage; + + return Stack( children: [ SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), child: Form( key: _formKey, child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ CommonAppBar( isWhiteLogo: false, - isProfilePage: true, + isProfilePage: false, showDivider: true, ), backWidget(context, "Edit Profile", Colors.black), SizedBox(height: 33.h), // Profile Picture - _buildProfileImageWidget(), + _buildProfileImageWidget(currentProfileImageUrl, selectedImageFile), SizedBox(height: 12.h), GestureDetector( onTap: isLoading ? null : _showImageSourceDialog, @@ -569,10 +585,10 @@ class _EditProfilePageState extends State { ), ), ], - ), - ), - ); - }, + ); + }, + ), + ), ); } } \ No newline at end of file diff --git a/lib/profile/view/profile_page_view.dart b/lib/profile/view/profile_page_view.dart index 66e28c7..0fafa63 100644 --- a/lib/profile/view/profile_page_view.dart +++ b/lib/profile/view/profile_page_view.dart @@ -23,208 +23,212 @@ class ProfilePage extends StatefulWidget { } class _ProfilePageState extends State { - bool isLogin = false; - bool isLoading = true; - @override void initState() { super.initState(); - _checkLoginStatus(); - } - - Future _checkLoginStatus() async { - final loginStatus = await LocalPreference.getLogin(); - final userId = await LocalPreference.getUserId(); - - setState(() { - isLogin = loginStatus; - isLoading = false; - }); - - // Fetch profile data if user is logged in - if (loginStatus && userId != null) { - context.read().add(FetchProfileEvent(userId: userId)); - } + // ⭐ REPLACED _checkLoginStatus() with BLoC event + context.read().add(const CheckLoginStatusEvent()); } @override Widget build(BuildContext context) { - if (isLoading) { - return Scaffold( - backgroundColor: Colors.white, - body: Center( - child: CircularProgressIndicator(), - ), - ); - } - return Scaffold( backgroundColor: Colors.white, body: SafeArea( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: true, - showDivider: true, - ), - backWidget(context, "My Profile", Colors.black), + child: BlocBuilder( + builder: (context, state) { + // ⭐ Show loading during initial checks and profile loading + if (state is ProfileInitial || + state is LoginStatusChecking || + state is ProfileLoading) { + return Center( + child: CircularProgressIndicator( + color: Color(0xFFF95F62), + ), + ); + } - SizedBox(height: 29.h), + // ⭐ Determine login status from state + bool isLogin = false; - // Show different UI based on login status - if (!isLogin) ...[ - // Guest User UI - _buildGuestUI(context), - ] else ...[ - // Logged In User UI with BLoC - BlocBuilder( - builder: (context, state) { - if (state is ProfileLoading) { - return Center( - child: CircularProgressIndicator(), - ); - } else if (state is ProfileLoaded) { - return _buildLoggedInUI(context, state.profile); - } else if (state is ProfileUpdated) { - return _buildLoggedInUI(context, state.profile); - } else if (state is ProfileError) { - return Column( - children: [ - Icon( - Icons.error_outline, - color: Colors.red, - size: 48.sp, - ), - SizedBox(height: 16.h), - Text( - 'Failed to load profile', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 8.h), - Text( - state.message, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14.sp, - color: Color(0xFF8E8E8E), - ), - ), - SizedBox(height: 16.h), - ElevatedButton( - onPressed: () async { - final userId = await LocalPreference.getUserId(); - if (userId != null) { - context.read().add( - FetchProfileEvent(userId: userId), - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFFF95F62), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(38.r), + if (state is ProfileLoaded || state is ProfileUpdated) { + isLogin = true; + } else if (state is LoginStatusChecked) { + isLogin = state.isLoggedIn; + // ⭐ If logged in but profile not loaded yet, show loading + if (isLogin && state is! ProfileLoaded) { + return Center( + child: CircularProgressIndicator( + color: Color(0xFFF95F62), + ), + ); + } + } else if (state is ProfileLoggedOut) { + isLogin = false; + } + + return SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: true, + showDivider: true, + ), + backWidget(context, "My Profile", Colors.black), + + SizedBox(height: 29.h), + + // Show different UI based on login status + if (!isLogin) ...[ + // Guest User UI + _buildGuestUI(context), + ] else ...[ + // Logged In User UI with BLoC + BlocBuilder( + builder: (context, state) { + if (state is ProfileLoading) { + return Center( + child: CircularProgressIndicator(), + ); + } else if (state is ProfileLoaded) { + return _buildLoggedInUI(context, state.profile); + } else if (state is ProfileUpdated) { + return _buildLoggedInUI(context, state.profile); + } else if (state is ProfileError) { + return Column( + children: [ + Icon( + Icons.error_outline, + color: Colors.red, + size: 48.sp, ), - ), - child: Text( - 'Retry', - style: TextStyle(color: Colors.white), - ), - ), - ], - ); - } - // Default fallback - return _buildLoggedInUI(context, null); - }, - ), - ], - - SizedBox(height: 30.h), - - // Support & Legal Section (Always visible) - Align( - alignment: Alignment.centerLeft, - child: CustomText( - text: "Support & Legal", - weight: FontWeight.w500, - size: 18.sp, - ), - ), - SizedBox(height: 10.h), - - _buildListTile( - icon: "assets/icons/contact_us.png", - title: 'Contact Us', - onTap: () { - Navigator.pushNamed(context, RouteConstants.contactUs); - }, - ), - _buildListTile( - icon: "assets/icons/terms_and_condition.png", - title: 'Terms and Conditions', - onTap: () { - Navigator.pushNamed( - context, - RouteConstants.termsAndCondition, - ); - }, - ), - _buildListTile( - icon: "assets/icons/faq.png", - title: 'FAQ', - onTap: () { - Navigator.pushNamed(context, RouteConstants.faq); - }, - ), - _buildListTile( - icon: "assets/icons/privacy.png", - title: 'Privacy Policy', - onTap: () { - Navigator.pushNamed(context, RouteConstants.privacyPolicy); - }, - ), - - SizedBox(height: 22.h), - - // Logout Button (Only for logged in users) - if (isLogin) - SizedBox( - width: double.infinity, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: Color(0xFFF95F62), - side: const BorderSide(color: Color(0xFFF95F62)), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(38.r), - ), - padding: EdgeInsets.symmetric(vertical: 6.h), + SizedBox(height: 16.h), + Text( + 'Failed to load profile', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + Text( + state.message, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + color: Color(0xFF8E8E8E), + ), + ), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () async { + final userId = await LocalPreference.getUserId(); + if (userId != null) { + context.read().add( + FetchProfileEvent(userId: userId), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFFF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(38.r), + ), + ), + child: Text( + 'Retry', + style: TextStyle(color: Colors.white), + ), + ), + ], + ); + } + // Default fallback + return _buildLoggedInUI(context, null); + }, ), - onPressed: () async { - // Handle logout - // await LocalPreference.clearPreference(); - context.read().add(ResetProfileEvent()); - setState(() { - isLogin = false; - }); - }, - child: Text( - 'Log out', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), + ], + + SizedBox(height: 30.h), + + // Support & Legal Section (Always visible) + Align( + alignment: Alignment.centerLeft, + child: CustomText( + text: "Support & Legal", + weight: FontWeight.w500, + size: 18.sp, ), ), - ), - ], - ), + SizedBox(height: 10.h), + + _buildListTile( + icon: "assets/icons/contact_us.png", + title: 'Contact Us', + onTap: () { + Navigator.pushNamed(context, RouteConstants.contactUs); + }, + ), + _buildListTile( + icon: "assets/icons/terms_and_condition.png", + title: 'Terms and Conditions', + onTap: () { + Navigator.pushNamed( + context, + RouteConstants.termsAndCondition, + ); + }, + ), + _buildListTile( + icon: "assets/icons/faq.png", + title: 'FAQ', + onTap: () { + Navigator.pushNamed(context, RouteConstants.faq); + }, + ), + _buildListTile( + icon: "assets/icons/privacy.png", + title: 'Privacy Policy', + onTap: () { + Navigator.pushNamed(context, RouteConstants.privacyPolicy); + }, + ), + + SizedBox(height: 22.h), + + // Logout Button (Only for logged in users) + if (isLogin) + SizedBox( + width: double.infinity, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Color(0xFFF95F62), + side: const BorderSide(color: Color(0xFFF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(38.r), + ), + padding: EdgeInsets.symmetric(vertical: 6.h), + ), + onPressed: () { + // ⭐ REPLACED setState with BLoC event + context.read().add(const LogoutEvent()); + }, + child: Text( + 'Log out', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ); + }, ), ), ); @@ -345,7 +349,7 @@ class _ProfilePageState extends State { /// ================= Profile Row ================= Row( children: [ - /// -------- Profile Image (NO ZOOM PLACEHOLDER) -------- + /// -------- Profile Image -------- SizedBox( width: 76.w, height: 76.w, @@ -379,7 +383,7 @@ class _ProfilePageState extends State { padding: EdgeInsets.all(16.w), child: Image.asset( 'assets/images/profile_default_img.png', - fit: BoxFit.contain, // ✅ NO ZOOM + fit: BoxFit.contain, ), ), ), diff --git a/pubspec.lock b/pubspec.lock index ef09470..ff9f06d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -331,6 +331,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.9.3" + flutter_stripe: + dependency: "direct main" + description: + name: flutter_stripe + sha256: "588f7b179784c0203fe99a388ca1f453b61fdc5e35bf32e508685ed00cffbb2e" + url: "https://pub.dev" + source: hosted + version: "12.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -341,6 +349,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" geoclue: dependency: transitive description: @@ -978,6 +994,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + stripe_android: + dependency: transitive + description: + name: stripe_android + sha256: "2bd3ad54a024701a3ef1d623c7ab81e6e8800c0586f91e4263530c47cdd19d82" + url: "https://pub.dev" + source: hosted + version: "12.2.0" + stripe_ios: + dependency: transitive + description: + name: stripe_ios + sha256: "948e2480182931c600ca07e2a132973503e9ba0f2caa8c7a94517ef861786f60" + url: "https://pub.dev" + source: hosted + version: "12.2.0" + stripe_platform_interface: + dependency: transitive + description: + name: stripe_platform_interface + sha256: "34b39cbc987b46f4d47cc0b3df2dd83994b6247c076a35ea7b1ec016959af449" + url: "https://pub.dev" + source: hosted + version: "12.2.0" syncfusion_flutter_calendar: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a13e607..655b564 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: dio: ^5.9.0 sqflite: ^2.4.2 flutter_map: ^8.2.2 + flutter_stripe: ^12.2.0 dev_dependencies: flutter_test: