From f5782f6da1cc7f3695f90c1d5b23d7d567d0fa33 Mon Sep 17 00:00:00 2001 From: mystery012728 Date: Fri, 23 Jan 2026 19:00:55 +0530 Subject: [PATCH] api integrtaion send Otp ,verify otp and faq , Terms ,Policy --- lib/cart/views/my_pass_page_view.dart | 2 +- lib/cart/views/my_postcard_page_view.dart | 2 +- lib/checkout/view/checkout_view.dart | 2 +- .../widget/login_email_bottomsheet.dart | 115 ------ .../widget/verify_otp_bottomsheet.dart | 122 ------ lib/common_packages/custom_textfield.dart | 28 +- lib/core/app_router.dart | 17 +- lib/core/inside_bottom_navigator.dart | 8 +- .../bloc/create_account_bloc.dart | 48 +++ .../bloc/create_account_event.dart | 40 ++ .../bloc/create_account_state.dart | 38 ++ lib/create_account/create_account_view.dart | 122 ------ .../models/create_account_model.dart | 86 ++++ .../repository/create_account_repository.dart | 33 ++ .../view/create_account_view.dart | 203 ++++++++++ lib/faq/faq_view.dart | 155 -------- lib/home/bloc/app_start_bloc.dart | 26 -- .../repository/search_city_repository.dart | 2 +- lib/home/views/first_time_user_home_page.dart | 21 +- lib/home/views/home_page_view.dart | 97 ++--- lib/home/views/registered_user_home_page.dart | 33 +- lib/home/widgets/search_city_bottomsheet.dart | 375 ++++++++++-------- .../views/intro_screen_view.dart | 41 +- lib/localPreference/local_database.dart | 8 + lib/localPreference/local_preference.dart | 51 ++- lib/login/bloc/login/login_bloc.dart | 29 ++ lib/login/bloc/login/login_event.dart | 7 + lib/login/bloc/login/login_state.dart | 17 + lib/login/bloc/verify/verify_bloc.dart | 49 +++ lib/login/bloc/verify/verify_event.dart | 17 + lib/login/bloc/verify/verify_state.dart | 27 ++ lib/login/repository/login_repository.dart | 44 ++ lib/login/view/login_email_bottomsheet.dart | 172 ++++++++ lib/login/view/verify_otp_bottomsheet.dart | 225 +++++++++++ lib/main.dart | 7 + lib/networkApiServices/api_urls.dart | 7 + lib/privacy/privacy_view.dart | 38 -- .../faq_n_privacy_n_terms_bloc.dart | 25 ++ .../faq_n_privacy_n_terms_event.dart | 3 + .../faq_n_privacy_n_terms_state.dart | 19 + .../models/faq_n_privacy_n_terms_model.dart | 118 ++++++ .../faq_n_privacy_n_terms_repository.dart | 17 + lib/profile/view/faq/faq_view.dart | 152 +++++++ lib/profile/view/privacy/privacy_view.dart | 81 ++++ lib/profile/{ => view}/profile_page_view.dart | 0 .../terms_and_condition_view.dart | 81 ++++ lib/splash_screen/bloc/app_start_bloc.dart | 40 ++ lib/splash_screen/bloc/app_start_event.dart | 3 + lib/splash_screen/bloc/app_start_state.dart | 11 + lib/splash_screen/views/splash_screen.dart | 81 ++-- .../terms_and_condition_view.dart | 41 -- 51 files changed, 2088 insertions(+), 898 deletions(-) delete mode 100644 lib/checkout/widget/login_email_bottomsheet.dart delete mode 100644 lib/checkout/widget/verify_otp_bottomsheet.dart create mode 100644 lib/create_account/bloc/create_account_bloc.dart create mode 100644 lib/create_account/bloc/create_account_event.dart create mode 100644 lib/create_account/bloc/create_account_state.dart delete mode 100644 lib/create_account/create_account_view.dart create mode 100644 lib/create_account/models/create_account_model.dart create mode 100644 lib/create_account/repository/create_account_repository.dart create mode 100644 lib/create_account/view/create_account_view.dart delete mode 100644 lib/faq/faq_view.dart create mode 100644 lib/login/bloc/login/login_bloc.dart create mode 100644 lib/login/bloc/login/login_event.dart create mode 100644 lib/login/bloc/login/login_state.dart create mode 100644 lib/login/bloc/verify/verify_bloc.dart create mode 100644 lib/login/bloc/verify/verify_event.dart create mode 100644 lib/login/bloc/verify/verify_state.dart create mode 100644 lib/login/repository/login_repository.dart create mode 100644 lib/login/view/login_email_bottomsheet.dart create mode 100644 lib/login/view/verify_otp_bottomsheet.dart delete mode 100644 lib/privacy/privacy_view.dart create mode 100644 lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart create mode 100644 lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart create mode 100644 lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart create mode 100644 lib/profile/models/faq_n_privacy_n_terms_model.dart create mode 100644 lib/profile/repository/faq_n_privacy_n_terms_repository.dart create mode 100644 lib/profile/view/faq/faq_view.dart create mode 100644 lib/profile/view/privacy/privacy_view.dart rename lib/profile/{ => view}/profile_page_view.dart (100%) create mode 100644 lib/profile/view/terms_and_condition/terms_and_condition_view.dart create mode 100644 lib/splash_screen/bloc/app_start_bloc.dart create mode 100644 lib/splash_screen/bloc/app_start_event.dart create mode 100644 lib/splash_screen/bloc/app_start_state.dart delete mode 100644 lib/terms_and_condition/terms_and_condition_view.dart diff --git a/lib/cart/views/my_pass_page_view.dart b/lib/cart/views/my_pass_page_view.dart index 53e4d9a..30daef7 100644 --- a/lib/cart/views/my_pass_page_view.dart +++ b/lib/cart/views/my_pass_page_view.dart @@ -6,7 +6,7 @@ import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../checkout/widget/login_email_bottomsheet.dart'; +import '../../login/view/login_email_bottomsheet.dart'; import '../../common_packages/common_app_texts.dart'; import '../blocs/pass_bloc.dart'; diff --git a/lib/cart/views/my_postcard_page_view.dart b/lib/cart/views/my_postcard_page_view.dart index 3379a4b..dfb1a22 100644 --- a/lib/cart/views/my_postcard_page_view.dart +++ b/lib/cart/views/my_postcard_page_view.dart @@ -5,7 +5,7 @@ import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../checkout/widget/login_email_bottomsheet.dart'; +import '../../login/view/login_email_bottomsheet.dart'; import '../blocs/postcard_bloc.dart'; class MyPostCardsPage extends StatelessWidget { diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index d01f4ef..e621c8a 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -1,5 +1,5 @@ import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart'; -import 'package:citycards_customer/checkout/widget/login_email_bottomsheet.dart'; +import 'package:citycards_customer/login/view/login_email_bottomsheet.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; diff --git a/lib/checkout/widget/login_email_bottomsheet.dart b/lib/checkout/widget/login_email_bottomsheet.dart deleted file mode 100644 index f11160f..0000000 --- a/lib/checkout/widget/login_email_bottomsheet.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:citycards_customer/checkout/widget/verify_otp_bottomsheet.dart'; -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'; - -class LoginEmailBottomsheet extends StatelessWidget { - const LoginEmailBottomsheet({super.key}); - - @override - Widget build(BuildContext context) { - return AnimatedPadding( - duration: const Duration(milliseconds: 250), - curve: Curves.easeOut, - padding: EdgeInsets.only( - top: 24.h, - left: 20.h, - right: 20.h, - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: SafeArea( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, // shrink to fit content - children: [ - Image.asset("assets/logo/logo_city_cards_orange.png", scale: 4), - SizedBox(height: 8.h), - CustomText(text: "Get Started", size: 18.sp, weight: FontWeight.w500), - SizedBox(height: 42.h), - CustomText( - text: "Enter your email to begin your CityCards journey", - size: 14.sp, - color: const Color(0xFF000000).withOpacity(.6), - ), - SizedBox(height: 12.h), - - TextField( - decoration: InputDecoration( - filled: true, - contentPadding: EdgeInsets.symmetric(vertical: 6.h), - fillColor: const Color(0xFFFFF5F5), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w), - borderRadius: BorderRadius.circular(8.sp), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w), - borderRadius: BorderRadius.circular(8.sp), - ), - prefixIcon: const Icon(Icons.email_outlined, color: Color(0xFFF95F62)), - hintText: "john.doe@gmail.com", - hintStyle: TextStyle( - color: const Color(0xFF000000).withOpacity(0.6), - fontSize: 12.sp, - ), - ), - ), - - SizedBox(height: 38.h), - CustomFilledButton( - onTap: () { - Navigator.pop(context); - showModalBottomSheet( - context: context, - backgroundColor: Colors.white, - isScrollControlled: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - builder: (_) => VerifyOtpBottomsheet(), - ); - }, - label: "Continue", - width: double.infinity, - ), - - SizedBox(height: 20.h), - InkWell( - onTap: (){ - Navigator.of(context).pushNamed(RouteConstants.createAcct); - }, - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Already have an account?", - style: TextStyle( - color: Colors.black.withOpacity(0.6), - fontSize: 12.sp, - fontWeight: FontWeight.w400, - ), - ), - TextSpan( - text: " Sign in", - style: TextStyle( - color: const Color(0xFFF95F62), - fontSize: 12.sp, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - SizedBox(height: 15.h), - ], - ), - ), - ), - ); - } -} diff --git a/lib/checkout/widget/verify_otp_bottomsheet.dart b/lib/checkout/widget/verify_otp_bottomsheet.dart deleted file mode 100644 index 40501b2..0000000 --- a/lib/checkout/widget/verify_otp_bottomsheet.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:citycards_customer/common_packages/custom_filled_button.dart'; -import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -import '../../core/route_constants.dart'; - -class VerifyOtpBottomsheet extends StatelessWidget { - VerifyOtpBottomsheet({super.key}); - - @override - Widget build(BuildContext context) { - return AnimatedPadding( - duration: const Duration(milliseconds: 250), - curve: Curves.easeOut, - padding: EdgeInsets.only( - top: 24.h, - left: 20.h, - right: 20.h, - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, // shrink to fit content - children: [ - Image.asset("assets/logo/logo_city_cards_orange.png", scale: 4), - SizedBox(height: 8.h), - CustomText( - text: "Verify your phone", - size: 18.sp, - weight: FontWeight.w500, - ), - SizedBox(height: 42.h), - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Enter the verification code sent to your email id", - style: TextStyle( - fontSize: 14.sp, - color: Colors.black.withOpacity(0.6), - ), - ), - TextSpan( - text: " frank7824@mail.com", - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - ], - ), - ), - SizedBox(height: 15.h), - - OtpTextField( - numberOfFields: 6, - borderWidth: 0.4.w, - fieldWidth: 48.w, - fieldHeight: 60.h, - borderRadius: BorderRadius.circular(8.r), - filled: true, - fillColor: const Color(0xFFFFF5F5), - borderColor: const Color(0xFFBB474A), - cursorColor: const Color(0xFFF95F62), - showFieldAsBox: true, - textStyle: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w500, - ), - onCodeChanged: (code) {}, - onSubmit: (code) { - debugPrint("OTP entered: $code"); - }, - ), - - SizedBox(height: 42.h), - CustomFilledButton( - onTap: () { - Navigator.pop(context); - }, - label: "Continue", - width: double.infinity, - ), - - SizedBox(height: 20.h), - InkWell( - onTap: () { - Navigator.of(context).pushNamed(RouteConstants.createAcct); - }, - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Already have an account?", - style: TextStyle( - color: Colors.black.withOpacity(0.6), - fontSize: 12.sp, - fontWeight: FontWeight.w400, - ), - ), - TextSpan( - text: " Sign in", - style: TextStyle( - color: const Color(0xFFF95F62), - fontSize: 12.sp, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - SizedBox(height: 15.h), - ], - ), - ), - ); - } -} diff --git a/lib/common_packages/custom_textfield.dart b/lib/common_packages/custom_textfield.dart index 7cf0843..8d4dd79 100644 --- a/lib/common_packages/custom_textfield.dart +++ b/lib/common_packages/custom_textfield.dart @@ -7,6 +7,7 @@ class CustomTextField extends StatelessWidget { final String hint; final TextEditingController controller; final int? maxLines; + final bool enabled; // ✅ NEW PARAMETER const CustomTextField({ super.key, @@ -14,6 +15,7 @@ class CustomTextField extends StatelessWidget { required this.hint, required this.controller, this.maxLines = 1, + this.enabled = true, // ✅ default enabled }); @override @@ -23,33 +25,49 @@ class CustomTextField extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText(text: label, size: 14.sp), + CustomText( + text: label, + size: 14.sp, + ), SizedBox(height: 6.h), SizedBox( height: maxLines == 1 ? 42.h : null, child: TextField( controller: controller, maxLines: maxLines, + enabled: enabled, // ✅ applied here decoration: InputDecoration( hintText: hint, - hintStyle: TextStyle(fontSize: 12.sp, color: Color(0xFF8E8E8E)), + hintStyle: TextStyle( + fontSize: 12.sp, + color: const Color(0xFF8E8E8E), + ), filled: true, - fillColor: const Color(0xFFFFF5F5), + fillColor: enabled + ? const Color(0xFFFFF5F5) + : Colors.grey.shade200, // subtle disabled look contentPadding: EdgeInsets.symmetric(horizontal: 24.w), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), borderSide: BorderSide( - color: Color(0xBBC83B61).withOpacity(0.4), + color: const Color(0xBBC83B61).withOpacity(0.4), width: .4.w, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), borderSide: BorderSide( - color: Color(0xFFF95F62), + color: const Color(0xFFF95F62), width: 1.w, ), ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: Colors.grey.shade400, + width: .4.w, + ), + ), ), ), ), diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index fcacdda..1def1b5 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,4 +1,4 @@ -import 'package:citycards_customer/Profile/profile_page_view.dart'; + import 'package:citycards_customer/add_details/add_details_view.dart'; import 'package:citycards_customer/attraction_details/views/attraction_details_view.dart'; import 'package:citycards_customer/attractions/models/attraction_model.dart'; @@ -6,10 +6,9 @@ import 'package:citycards_customer/buy_a_pass/view/buy_pass_view.dart'; import 'package:citycards_customer/checkout/view/checkout_view.dart'; import 'package:citycards_customer/common_bloc/language_selection_bloc.dart'; import 'package:citycards_customer/contact_us/contact_us_view.dart'; -import 'package:citycards_customer/create_account/create_account_view.dart'; +import 'package:citycards_customer/create_account/view/create_account_view.dart'; import 'package:citycards_customer/edit_profile/edit_profile_view.dart'; import 'package:citycards_customer/esim_offer/esim_offer_view.dart'; -import 'package:citycards_customer/faq/faq_view.dart'; import 'package:citycards_customer/hotel_offer/hotel_offer_view.dart'; import 'package:citycards_customer/intro_screens/views/intro_screen_view.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc.dart'; @@ -19,11 +18,9 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_v import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_empty_view.dart'; import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_filled_view.dart'; import 'package:citycards_customer/offer_pass_detail/offer_pass_detail_view.dart'; -import 'package:citycards_customer/privacy/privacy_view.dart'; import 'package:citycards_customer/search_offers/bloc/search_offers_listing_bloc.dart'; import 'package:citycards_customer/search_offers/view/search_offers_with_listing.dart'; import 'package:citycards_customer/splash_screen/views/splash_screen.dart'; -import 'package:citycards_customer/terms_and_condition/terms_and_condition_view.dart'; import 'package:citycards_customer/trail.dart'; import 'package:citycards_customer/your_itinerary/view/your_itinerary_view.dart'; import 'package:flutter/material.dart'; @@ -33,6 +30,10 @@ import '../cart/views/my_cart_view_page.dart'; import '../common_bloc/bottom_navigation_bloc.dart'; import '../home/views/home_page_view.dart'; import '../home/views/registered_user_home_page.dart'; +import '../profile/view/faq/faq_view.dart'; +import '../profile/view/privacy/privacy_view.dart'; +import '../profile/view/profile_page_view.dart'; +import '../profile/view/terms_and_condition/terms_and_condition_view.dart'; import 'route_constants.dart'; class AppRouter { @@ -193,9 +194,13 @@ class AppRouter { ); case RouteConstants.createAcct: + final email = settings.arguments as String; + return MaterialPageRoute( builder: (_) { - return CreateAccountView(); + return CreateAccountView( + email: email, // ✅ required param + ); }, ); diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart index 48c3365..73664f6 100644 --- a/lib/core/inside_bottom_navigator.dart +++ b/lib/core/inside_bottom_navigator.dart @@ -10,7 +10,7 @@ import '../attraction_details/views/attraction_details_view.dart'; import '../attractions/views/attractions_page_view.dart'; import '../buy_a_pass/view/buy_pass_view.dart'; import '../checkout/view/checkout_view.dart'; -import '../create_account/create_account_view.dart'; +import '../create_account/view/create_account_view.dart'; import '../intro_screens/views/intro_screen_view.dart'; import '../itinerary_creation/bloc/itinerary_detail_bloc.dart'; import '../itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; @@ -173,9 +173,13 @@ Widget buildOffstageNavigator( ); case RouteConstants.createAcct: + final email = settings.arguments as String; + return MaterialPageRoute( builder: (_) { - return CreateAccountView(); + return CreateAccountView( + email: email, // ✅ required param + ); }, ); diff --git a/lib/create_account/bloc/create_account_bloc.dart b/lib/create_account/bloc/create_account_bloc.dart new file mode 100644 index 0000000..c0512a9 --- /dev/null +++ b/lib/create_account/bloc/create_account_bloc.dart @@ -0,0 +1,48 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../repository/create_account_repository.dart'; +import 'create_account_event.dart'; +import 'create_account_state.dart'; + +class CreateAccountBloc extends Bloc { + final CreateAccountRepository repository; + + CreateAccountBloc({required this.repository}) + : super(const CreateAccountInitial()) { + on(_onCreateAccountSubmitted); + on(_onCreateAccountReset); + } + + Future _onCreateAccountSubmitted( + CreateAccountSubmitted event, + Emitter emit, + ) async { + emit(const CreateAccountLoading()); + + try { + final response = await repository.registerUser( + firstName: event.firstName, + lastName: event.lastName, + emailAddress: event.emailAddress, + mobileNumber: event.mobileNumber, + address1: event.address1, + address2: event.address2, + ); + + emit(CreateAccountSuccess( + message: response['message'] ?? 'Account created successfully', + userData: response['data'] ?? {}, + )); + } catch (e) { + emit(CreateAccountFailure( + errorMessage: e.toString().replaceAll('Exception: ', ''), + )); + } + } + + void _onCreateAccountReset( + CreateAccountReset event, + Emitter emit, + ) { + emit(const CreateAccountInitial()); + } +} \ No newline at end of file diff --git a/lib/create_account/bloc/create_account_event.dart b/lib/create_account/bloc/create_account_event.dart new file mode 100644 index 0000000..5bd6fd7 --- /dev/null +++ b/lib/create_account/bloc/create_account_event.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; + +abstract class CreateAccountEvent extends Equatable { + const CreateAccountEvent(); + + @override + List get props => []; +} + +class CreateAccountSubmitted extends CreateAccountEvent { + final String firstName; + final String lastName; + final String emailAddress; + final String mobileNumber; + final String address1; + final String address2; + + const CreateAccountSubmitted({ + required this.firstName, + required this.lastName, + required this.emailAddress, + required this.mobileNumber, + required this.address1, + required this.address2, + }); + + @override + List get props => [ + firstName, + lastName, + emailAddress, + mobileNumber, + address1, + address2, + ]; +} + +class CreateAccountReset extends CreateAccountEvent { + const CreateAccountReset(); +} \ No newline at end of file diff --git a/lib/create_account/bloc/create_account_state.dart b/lib/create_account/bloc/create_account_state.dart new file mode 100644 index 0000000..3a12a89 --- /dev/null +++ b/lib/create_account/bloc/create_account_state.dart @@ -0,0 +1,38 @@ +import 'package:equatable/equatable.dart'; + +abstract class CreateAccountState extends Equatable { + const CreateAccountState(); + + @override + List get props => []; +} + +class CreateAccountInitial extends CreateAccountState { + const CreateAccountInitial(); +} + +class CreateAccountLoading extends CreateAccountState { + const CreateAccountLoading(); +} + +class CreateAccountSuccess extends CreateAccountState { + final String message; + final Map userData; + + const CreateAccountSuccess({ + required this.message, + required this.userData, + }); + + @override + List get props => [message, userData]; +} + +class CreateAccountFailure extends CreateAccountState { + final String errorMessage; + + const CreateAccountFailure({required this.errorMessage}); + + @override + List get props => [errorMessage]; +} \ No newline at end of file diff --git a/lib/create_account/create_account_view.dart b/lib/create_account/create_account_view.dart deleted file mode 100644 index aef7073..0000000 --- a/lib/create_account/create_account_view.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:citycards_customer/common_packages/app_bar.dart'; -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_textfield.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class CreateAccountView extends StatelessWidget { - CreateAccountView({super.key}); - - final TextEditingController firstNameController = TextEditingController(); - final TextEditingController lastNameController = TextEditingController(); - final TextEditingController emailController = TextEditingController(); - final TextEditingController phoneController = TextEditingController(); - final TextEditingController addressController = TextEditingController(); - - @override - Widget build(BuildContext context) { - - 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), - ), - SizedBox(width: 8.w), - CustomText(text: "Create your account", size: 12.sp), - ], - ), - SizedBox(height: 26.h,), - - Align( - alignment: Alignment.centerLeft, - child: CustomText( - text: "Personal Information", - size: 18.sp, - weight: FontWeight.w500, - ), - ), - 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, - ), - ), - - SizedBox(height: 2.h), - - // Location Details - Align( - alignment: Alignment.centerLeft, - child: CustomText( - text: "Location Details", - size: 18.sp, - weight: FontWeight.w500, - ), - ), - SizedBox(height: 16.h), - - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0.w), - child: CustomTextField( - label: "Address 1", - hint: "Enter address manually or tap to search", - controller: addressController, - ), - ), - - SizedBox(height: 16.h), - CustomFilledButton( - width: double.infinity, - onTap: (){}, label: "Create Account") - ], - ), - ), - ), - ); - } -} diff --git a/lib/create_account/models/create_account_model.dart b/lib/create_account/models/create_account_model.dart new file mode 100644 index 0000000..506d46e --- /dev/null +++ b/lib/create_account/models/create_account_model.dart @@ -0,0 +1,86 @@ +class UserRegisteredModel { + final bool verified; + final bool userExists; + final String accessToken; + final String refreshToken; + final int refreshTokenMaxAge; + final User user; + + UserRegisteredModel({ + required this.verified, + required this.userExists, + required this.accessToken, + required this.refreshToken, + required this.refreshTokenMaxAge, + required this.user, + }); + + factory UserRegisteredModel.fromJson(Map json) { + return UserRegisteredModel( + verified: json['verified'] ?? false, + userExists: json['userExists'] ?? false, + accessToken: json['accessToken'] ?? '', + refreshToken: json['refreshToken'] ?? '', + refreshTokenMaxAge: json['refreshTokenMaxAge'] ?? 0, + user: User.fromJson(json['user'] ?? {}), + ); + } + + Map toJson() { + return { + 'verified': verified, + 'userExists': userExists, + 'accessToken': accessToken, + 'refreshToken': refreshToken, + 'refreshTokenMaxAge': refreshTokenMaxAge, + 'user': user.toJson(), + }; + } +} + +/// ------------------------------------------------------------ +/// User Model (Nested) +/// ------------------------------------------------------------ +class User { + final int id; + final String firstName; + final String lastName; + final String fullName; + final String emailAddress; + final String role; + final int roleId; + + User({ + required this.id, + required this.firstName, + required this.lastName, + required this.fullName, + required this.emailAddress, + required this.role, + required this.roleId, + }); + + factory User.fromJson(Map json) { + return User( + id: json['id'] ?? 0, + firstName: json['firstName'] ?? '', + lastName: json['lastName'] ?? '', + fullName: json['fullName'] ?? '', + emailAddress: json['emailAddress'] ?? '', + role: json['role'] ?? '', + roleId: json['roleId'] ?? 0, + ); + } + + Map toJson() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'fullName': fullName, + 'emailAddress': emailAddress, + 'role': role, + 'roleId': roleId, + }; + } +} diff --git a/lib/create_account/repository/create_account_repository.dart b/lib/create_account/repository/create_account_repository.dart new file mode 100644 index 0000000..738f7d4 --- /dev/null +++ b/lib/create_account/repository/create_account_repository.dart @@ -0,0 +1,33 @@ +import 'package:citycards_customer/networkApiServices/api_urls.dart'; +import 'package:citycards_customer/networkApiServices/network_api_services.dart'; + +class CreateAccountRepository { + final NetworkApiService _apiServices = NetworkApiService(); + + Future> registerUser({ + required String firstName, + required String lastName, + required String emailAddress, + required String mobileNumber, + required String address1, + required String address2, + }) async { + try { + final response = await _apiServices.postApi( + url: ApiUrls.createAccount, + data: { + 'firstName': firstName, + 'lastName': lastName, + 'emailAddress': emailAddress, + 'mobileNumber': mobileNumber, + 'address1': address1, + 'address2': address2, + }, + ); + + return response.data as Map; + } catch (e) { + throw Exception('Failed to create account: $e'); + } + } +} \ No newline at end of file diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart new file mode 100644 index 0000000..3fa9f15 --- /dev/null +++ b/lib/create_account/view/create_account_view.dart @@ -0,0 +1,203 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +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_textfield.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../bloc/create_account_bloc.dart'; +import '../bloc/create_account_event.dart'; +import '../bloc/create_account_state.dart'; +import '../repository/create_account_repository.dart'; + +class CreateAccountView extends StatelessWidget { + final String email; + CreateAccountView({super.key,required this.email}); + + final TextEditingController firstNameController = TextEditingController(); + final TextEditingController lastNameController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController phoneController = TextEditingController(); + final TextEditingController addressController = TextEditingController(); + + void _submitForm(BuildContext context) { + if (firstNameController.text.trim().isEmpty || + lastNameController.text.trim().isEmpty || + emailController.text.trim().isEmpty || + phoneController.text.trim().isEmpty || + addressController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please fill all fields')), + ); + return; + } + + context.read().add( + CreateAccountSubmitted( + firstName: firstNameController.text.trim(), + lastName: lastNameController.text.trim(), + emailAddress: emailController.text.trim(), + mobileNumber: phoneController.text.trim(), + address1: addressController.text.trim(), + address2: '', + ), + ); + } + + @override + Widget build(BuildContext context) { + emailController.text = email; + return BlocProvider( + create: (context) => CreateAccountBloc( + repository: CreateAccountRepository(), + ), + child: BlocListener( + listener: (context, state) { + if (state is CreateAccountSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.message)), + ); + Navigator.pop(context); + Navigator.pop(context); + } else if (state is CreateAccountFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + backgroundColor: Colors.red, + ), + ); + } + }, + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showCart: false, + showDivider: true, + ), + ), + + /// 🔹 Scrollable content starts here + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const Icon(Icons.arrow_back), + ), + SizedBox(width: 8.w), + CustomText( + text: "Create your account", + size: 12.sp, + ), + ], + ), + + SizedBox(height: 26.h), + + CustomText( + text: "Personal Information", + size: 18.sp, + weight: FontWeight.w500, + ), + + 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, + enabled: false, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Phone Number", + hint: "Enter your phone number", + controller: phoneController, + ), + ), + + SizedBox(height: 12.h), + + CustomText( + text: "Location Details", + size: 18.sp, + weight: FontWeight.w500, + ), + + SizedBox(height: 16.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Address 1", + hint: "Enter address manually or tap to search", + controller: addressController, + ), + ), + + SizedBox(height: 20.h), + + BlocBuilder( + builder: (context, state) { + if (state is CreateAccountLoading) { + return CustomFilledButton( + width: double.infinity, + onTap: () {}, + label: "Creating...", + ); + } + return CustomFilledButton( + width: double.infinity, + onTap: () => _submitForm(context), + label: "Create Account", + ); + }, + ), + + SizedBox(height: 20.h), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/faq/faq_view.dart b/lib/faq/faq_view.dart deleted file mode 100644 index cb1f211..0000000 --- a/lib/faq/faq_view.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:citycards_customer/common_packages/back_widget.dart'; -import 'package:citycards_customer/common_packages/custom_expansion_tile.dart'; -import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class FaqPage extends StatelessWidget { - const FaqPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: true, showDivider: true,), - backWidget(context,"FAQ", Colors.black), - SizedBox(height: 34.h), - - FAQSection(title: "🧭 General FAQs", faqs: generalFAQs), - SizedBox(height: 20.h), - FAQSection(title: "✈️ Booking & Planning", faqs: bookingFaq), - SizedBox(height: 20.h), - FAQSection(title: "🌍 Discover & Explore", faqs: discoverFAQs), - ], - ), - ), - ), - ), - ); - } -} - -// Model for FAQ -class FAQItem { - final String question; - final String answer; - - FAQItem({required this.question, required this.answer}); -} - -// Sample FAQ data -final List generalFAQs = [ - FAQItem( - question: "What is CityCards?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), - FAQItem( - question: "Is the app free to use?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), - FAQItem( - question: "Do I need an account to use the app?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), -]; - -final List discoverFAQs = [ - FAQItem( - question: "How does the app recommend destinations?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), - FAQItem( - question: "Can I create a custom itinerary?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), - FAQItem( - question: "Does the app work offline?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), -]; - -final List bookingFaq = [ - FAQItem( - question: "Can I modify or cancel my bookings?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), - FAQItem( - question: "Can I plan multi-city trips?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), - FAQItem( - question: "Can I book hotels through the app?", - answer: - "You can browse without an account, but signing up allows you to save trips, sync data, and get personalized recommendations.", - ), -]; - -// Widget for FAQ section -Widget FAQSection({required String title, required List faqs}) { - return Container( - padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w), - decoration: BoxDecoration( - border: Border.all(color: Color(0xFFF95F62)), - borderRadius: BorderRadius.circular(10.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Section heading - CustomText( - text: title, - size: 16.sp, - weight: FontWeight.w500, - color: Color(0xFF212121), - ), - SizedBox(height: 12.h), - - // Dynamic list of questions - Column( - children: faqs.map((faq) { - int index = faqs.indexOf(faq); - return Column( - children: [ - CustomExpansionTile( - minTileHeight: 42.h, - borderRadius: BorderRadius.circular(5.r), - backgroundColor: Color(0xFFFEE7E7), - collapsedBackgroundColor: Color(0xFFFEE7E7), - tilePadding: EdgeInsets.symmetric( - horizontal: 14.w, - vertical: 0, - ), - childrenPadding: EdgeInsets.only(left: 12.w,right: 12.w, bottom: 12.h), - title: Text(faq.question, style: TextStyle(fontSize: 14.sp)), - children: [ - Text( - faq.answer, - style: TextStyle(color: Color(0xFF5C5C5C), fontSize: 14.sp), - ), - ], - ), - if (index != faqs.length - 1) SizedBox(height: 8.h), // spacing - ], - ); - }).toList(), - ), - ], - ), - ); -} diff --git a/lib/home/bloc/app_start_bloc.dart b/lib/home/bloc/app_start_bloc.dart index 90badb1..e69de29 100644 --- a/lib/home/bloc/app_start_bloc.dart +++ b/lib/home/bloc/app_start_bloc.dart @@ -1,26 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; - -/// --- Events --- -abstract class AppStartEvent {} - -class StartApp extends AppStartEvent {} -class MarkUserAsRegistered extends AppStartEvent {} - -/// --- States --- -abstract class AppStartState {} - -class AppStartFirstTime extends AppStartState {} -class AppStartRegistered extends AppStartState {} - -/// --- Bloc --- -class AppStartBloc extends Bloc { - AppStartBloc() : super(AppStartFirstTime()) { - on((event, emit) { - emit(AppStartFirstTime()); // always first-time - }); - - on((event, emit) { - emit(AppStartRegistered()); - }); - } -} diff --git a/lib/home/repository/search_city_repository.dart b/lib/home/repository/search_city_repository.dart index 9ca42bc..a5c4f22 100644 --- a/lib/home/repository/search_city_repository.dart +++ b/lib/home/repository/search_city_repository.dart @@ -13,7 +13,7 @@ class SearchCityRepository { url: ApiUrls.searchCityList, ); return CitySelectionResponse.fromJson(response.data); - } catch (e) { + } catch (e) { throw Exception('Failed to fetch cities: $e'); } } diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index b13bae5..af9859b 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:citycards_customer/core/route_constants.dart'; import '../../common_packages/app_bar.dart'; +import '../../localPreference/local_preference.dart'; import '../../networkApiServices/api_urls.dart'; import '../widgets/explore_cities_card.dart'; import '../bloc/FirstTimeUserHome/first_time_user_home_bloc.dart'; @@ -9,8 +11,7 @@ import '../bloc/FirstTimeUserHome/first_time_user_home_event.dart'; import '../bloc/FirstTimeUserHome/first_time_user_home_state.dart'; class FirstTimeUserHomePage extends StatefulWidget { - final VoidCallback onContinue; - const FirstTimeUserHomePage({super.key, required this.onContinue}); + const FirstTimeUserHomePage({super.key}); @override State createState() => _FirstTimeUserHomePageState(); @@ -46,6 +47,20 @@ class _FirstTimeUserHomePageState extends State { super.dispose(); } + Future _handleGetCityCard() async { + // Update onboarding page from 1 to 2 + await LocalPreference.updateOnboardingPage(2); + print('✅ Onboarding page updated from 1 to 2'); + + if (mounted) { + // Navigate to regular home screen + Navigator.pushReplacementNamed( + context, + RouteConstants.home, + ); + } + } + @override Widget build(BuildContext context) { return SafeArea( @@ -88,7 +103,7 @@ class _FirstTimeUserHomePageState extends State { borderRadius: BorderRadius.circular(25.r), ), ), - onPressed: widget.onContinue, + onPressed: _handleGetCityCard, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index b8348b8..048a249 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -6,8 +6,6 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_s import 'package:citycards_customer/my_pass/views/my_pass_page_view.dart'; import 'package:citycards_customer/postcard/views/postcard_initial_page_view.dart'; import '../../common_bloc/bottom_navigation_bloc.dart'; -import '../bloc/app_start_bloc.dart'; -import 'first_time_user_home_page.dart'; import 'registered_user_home_page.dart'; class HomePage extends StatefulWidget { @@ -22,64 +20,45 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => AppStartBloc()..add(StartApp()), - child: BlocBuilder( - builder: (context, state) { - // 🚀 Always first time initially - if (state is AppStartFirstTime) { - return FirstTimeUserHomePage( - onContinue: () { - context - .read() - .add(MarkUserAsRegistered()); - }, - ); - } + return BlocBuilder( + builder: (context, navState) { + final currentIndex = navState.selectedIndex; - // ✅ Registered user flow - return BlocBuilder( - builder: (context, navState) { - final currentIndex = navState.selectedIndex; - - return SafeArea( - top: false, - child: Scaffold( - body: Stack( - children: [ - buildOffstageNavigator( - 0, - currentIndex, - const RegisteredUserHomePage(), - _navigatorKeys[0], - ), - buildOffstageNavigator( - 1, - currentIndex, - const ItineraryCreationStartPage(), - _navigatorKeys[1], - ), - buildOffstageNavigator( - 2, - currentIndex, - const MyPassesView(), - _navigatorKeys[2], - ), - buildOffstageNavigator( - 3, - currentIndex, - const PostcardPage(), - _navigatorKeys[3], - ), - ], - ), - bottomNavigationBar: const CustomBottomNavBar(), + return SafeArea( + top: false, + child: Scaffold( + body: Stack( + children: [ + buildOffstageNavigator( + 0, + currentIndex, + const RegisteredUserHomePage(), + _navigatorKeys[0], ), - ); - }, - ); - }, - ), + buildOffstageNavigator( + 1, + currentIndex, + const ItineraryCreationStartPage(), + _navigatorKeys[1], + ), + buildOffstageNavigator( + 2, + currentIndex, + const MyPassesView(), + _navigatorKeys[2], + ), + buildOffstageNavigator( + 3, + currentIndex, + const PostcardPage(), + _navigatorKeys[3], + ), + ], + ), + bottomNavigationBar: const CustomBottomNavBar(), + ), + ); + }, ); } -} +} \ No newline at end of file diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index 8a30e73..3521586 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -7,6 +7,7 @@ import 'package:google_fonts/google_fonts.dart'; import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../common_packages/app_bar.dart'; import '../../core/route_constants.dart'; +import '../../localPreference/local_preference.dart'; import '../../networkApiServices/api_urls.dart'; import '../bloc/registeredHome/home_bloc.dart'; import '../bloc/registeredHome/home_event.dart'; @@ -16,6 +17,7 @@ import '../widgets/get_your_pass_card.dart'; import '../widgets/gradient_container_bg.dart'; import '../widgets/itineary_animation.dart'; import '../widgets/pass_card_list.dart'; +import '../widgets/search_city_bottomsheet.dart'; class RegisteredUserHomePage extends StatefulWidget { const RegisteredUserHomePage({super.key}); @@ -25,10 +27,39 @@ class RegisteredUserHomePage extends StatefulWidget { } class _RegisteredUserHomePageState extends State { + @override @override void initState() { super.initState(); - context.read().add(FetchHomeData()); + _checkAndShowCitySelection(); + } + + Future _checkAndShowCitySelection() async { + final int cityId = await LocalPreference.getSelectedCityId(); + + // If cityId is 1 (default) or invalid, show city selection + if (cityId == 0) { + // Use addPostFrameCallback to show bottom sheet after build is complete + WidgetsBinding.instance.addPostFrameCallback((_) { + _showCitySelectionBottomSheet(); + }); + } else { + // Load home data only if city is already selected + if (mounted) { + context.read().add(FetchHomeData()); + } + } + } + + void _showCitySelectionBottomSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + isDismissible: false, // Prevent dismissing without selecting a city + enableDrag: false, // Prevent dragging to close + builder: (_) => const CitySelectionBottomSheet(), + ); } @override diff --git a/lib/home/widgets/search_city_bottomsheet.dart b/lib/home/widgets/search_city_bottomsheet.dart index ceb30ee..d51cb09 100644 --- a/lib/home/widgets/search_city_bottomsheet.dart +++ b/lib/home/widgets/search_city_bottomsheet.dart @@ -28,198 +28,228 @@ class _CitySelectionView extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - height: 620.h, - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(12.r), - topRight: Radius.circular(12.r), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - InkWell( - onTap: () => Navigator.pop(context), - child: const Icon(Icons.arrow_back, size: 18), - ), - SizedBox(width: 4.w), - CustomText(text: "Back", size: 12.sp), - ], - ), - Text( - "Select a City", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), - ), - SizedBox(width: 25.w), - ], + return PopScope( + canPop: false, // Disable default back behavior + onPopInvokedWithResult: (didPop, result) async { + if (didPop) return; + + // Check if a city is already selected + final cityId = await LocalPreference.getSelectedCityId(); + // Only allow back if city is selected (not default) + if (cityId != 0) { + Navigator.pop(context); + } + }, + child: Container( + height: 620.h, + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.r), + topRight: Radius.circular(12.r), ), - - SizedBox(height: 19.h), - - // Search Field - SizedBox( - height: 45.h, - child: TextField( - controller: _controller, - onChanged: (value) { - if (value.isEmpty) { - context.read().add(LoadAllCity()); - } else { - context.read().add(SearchCity(value)); - } - }, - decoration: InputDecoration( - hintText: "Search Cities", - hintStyle: TextStyle( - fontSize: 14.sp, - color: const Color(0xFF2B2B2B), - fontWeight: FontWeight.w300, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FutureBuilder( + future: LocalPreference.getSelectedCityId(), + builder: (context, snapshot) { + final cityId = snapshot.data ?? 1; + // Only show back button if city is already selected + if (cityId == 0) { + return SizedBox(width: 60.w); // Empty space to maintain layout + } + return Row( + children: [ + InkWell( + onTap: () => Navigator.pop(context), + child: const Icon(Icons.arrow_back, size: 18), + ), + SizedBox(width: 4.w), + CustomText(text: "Back", size: 12.sp), + ], + ); + }, ), - filled: true, - fillColor: const Color(0xFFFFFFFF).withOpacity(.24), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: const Color(0xFFF95F62).withOpacity(.40), - width: 1, + Text( + "Select a City", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, ), ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: const Color(0xFFF95F62).withOpacity(.40), - width: 1, + SizedBox(width: 25.w), + ], + ), + + SizedBox(height: 19.h), + + // Search Field + SizedBox( + height: 45.h, + child: TextField( + controller: _controller, + onChanged: (value) { + if (value.isEmpty) { + context.read().add(LoadAllCity()); + } else { + context.read().add(SearchCity(value)); + } + }, + decoration: InputDecoration( + hintText: "Search Cities", + hintStyle: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF2B2B2B), + fontWeight: FontWeight.w300, ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: const Color(0xFFF95F62).withOpacity(.40), - width: 1.2, + filled: true, + fillColor: const Color(0xFFFFFFFF).withOpacity(.24), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: const Color(0xFFF95F62).withOpacity(.40), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: const Color(0xFFF95F62).withOpacity(.40), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: const Color(0xFFF95F62).withOpacity(.40), + width: 1.2, + ), ), ), ), ), - ), - SizedBox(height: 19.h), + SizedBox(height: 19.h), - // City Grid - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state is CityLoading) { - return const Center( - child: CircularProgressIndicator( - color: Color(0xFFF95F62), - ), - ); - } + // City Grid + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is CityLoading) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xFFF95F62), + ), + ); + } - if (state is CityError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, size: 48, color: Colors.red), - SizedBox(height: 16.h), - Text( - 'Error loading cities', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 8.h), - Text( - state.message, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12.sp, - color: Colors.grey[600], - ), - ), - SizedBox(height: 16.h), - ElevatedButton( - onPressed: () { - context.read().add(LoadAllCity()); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF95F62), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - child: const Text( - 'Retry', - style: TextStyle(color: Colors.white), - ), - ), - ], - ), - ); - } - - if (state is CityLoaded) { - if (state.cities.isEmpty) { + if (state is CityError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.location_city_outlined, - size: 48, - color: Colors.grey[400]), + const Icon(Icons.error_outline, size: 48, color: Colors.red), SizedBox(height: 16.h), Text( - "No cities found", + 'Error loading cities', style: TextStyle( fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + Text( + state.message, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.sp, color: Colors.grey[600], ), ), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () { + context.read().add(LoadAllCity()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + child: const Text( + 'Retry', + style: TextStyle(color: Colors.white), + ), + ), ], ), ); } - return GridView.builder( - itemCount: state.cities.length, - physics: const BouncingScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 12.h, - crossAxisSpacing: 12.w, - childAspectRatio: 1.2, - ), - itemBuilder: (context, index) { - final city = state.cities[index]; - return _cityCard( - context, - city.id, // 👈 important - city.getImageUrl(), - city.cityName, - city.isNetworkImage(), + if (state is CityLoaded) { + if (state.cities.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.location_city_outlined, + size: 48, + color: Colors.grey[400]), + SizedBox(height: 16.h), + Text( + "No cities found", + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey[600], + ), + ), + ], + ), ); - }, - ); - } - return const SizedBox.shrink(); - }, + } + + return GridView.builder( + itemCount: state.cities.length, + physics: const BouncingScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12.h, + crossAxisSpacing: 12.w, + childAspectRatio: 1.2, + ), + itemBuilder: (context, index) { + final city = state.cities[index]; + return FutureBuilder( + future: LocalPreference.getSelectedCityId(), + builder: (context, snapshot) { + final selectedCityId = snapshot.data ?? 1; + return _cityCard( + context, + city.id, + city.getImageUrl(), + city.cityName, + city.isNetworkImage(), + selectedCityId, + ); + }, + ); + }, + ); + } + return const SizedBox.shrink(); + }, + ), ), - ), - ], + ], + ), ), ); } @@ -230,7 +260,9 @@ class _CitySelectionView extends StatelessWidget { String imageUrl, String name, bool isNetwork, + int selectedCityId, // Add this parameter ) { + final bool isSelected = cityId == selectedCityId; // Check if selected return InkWell( onTap: () async { await LocalPreference.setSelectedCityId(cityId); @@ -282,6 +314,25 @@ class _CitySelectionView extends StatelessWidget { ), ), ), + + // Selected icon (top right) + if (isSelected) + Positioned( + top: 8.h, + right: 8.w, + child: Container( + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: const Color(0xFFF95F62), + shape: BoxShape.circle, + ), + child: Icon( + Icons.check, + color: Colors.white, + size: 16.sp, + ), + ), + ), ], ), ), diff --git a/lib/intro_screens/views/intro_screen_view.dart b/lib/intro_screens/views/intro_screen_view.dart index 747b3b2..c6c8158 100644 --- a/lib/intro_screens/views/intro_screen_view.dart +++ b/lib/intro_screens/views/intro_screen_view.dart @@ -4,14 +4,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_glass_morphism/flutter_glass_morphism.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../home/views/first_time_user_home_page.dart'; +import '../../localPreference/local_preference.dart'; import '../blocs/intro_screens_bloc.dart'; -class IntroScreensView extends StatelessWidget { - final PageController _pageController = PageController(); - final IntroScreensViewModel _viewModel = IntroScreensViewModel(); +class IntroScreensView extends StatefulWidget { IntroScreensView({super.key}); + @override + State createState() => _IntroScreensViewState(); +} + +class _IntroScreensViewState extends State { + final PageController _pageController = PageController(); + + final IntroScreensViewModel _viewModel = IntroScreensViewModel(); + + @override + void initState() { + super.initState(); + _updateOnboardingProgress(); + } + + Future _updateOnboardingProgress() async { + final currentPage = await LocalPreference.getOnboardingPage(); + if (currentPage == 0) { + await LocalPreference.updateOnboardingPage(1); + } + } + @override Widget build(BuildContext context) { final pages = _viewModel.pages; @@ -49,7 +71,7 @@ class IntroScreensView extends StatelessWidget { ); }, ), - + // Skip Button (Only first 2 pages) if (state.currentPage < pages.length - 1) Positioned( @@ -79,7 +101,7 @@ class IntroScreensView extends StatelessWidget { ), ), ), - + // Bottom Content Align( alignment: Alignment.bottomCenter, @@ -112,7 +134,7 @@ class IntroScreensView extends StatelessWidget { textAlign: TextAlign.center, ), SizedBox(height: 24.h), - + // Dots Indicator Row( mainAxisAlignment: MainAxisAlignment.center, @@ -144,7 +166,12 @@ class IntroScreensView extends StatelessWidget { ), onPressed: () { if (state.currentPage == pages.length - 1) { - Navigator.pushReplacementNamed(context, '/home'); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const FirstTimeUserHomePage(), + ), + ); } else { _pageController.nextPage( duration: const Duration(milliseconds: 400), diff --git a/lib/localPreference/local_database.dart b/lib/localPreference/local_database.dart index 4bfd02a..2daf24a 100644 --- a/lib/localPreference/local_database.dart +++ b/lib/localPreference/local_database.dart @@ -38,6 +38,14 @@ class LocalDatabase { page INTEGER NOT NULL ) '''); + + /// LOGIN TABLE + await db.execute(''' + CREATE TABLE login_state ( + id INTEGER PRIMARY KEY, + is_login INTEGER NOT NULL + ) + '''); }, ); } diff --git a/lib/localPreference/local_preference.dart b/lib/localPreference/local_preference.dart index a1ffa18..938b385 100644 --- a/lib/localPreference/local_preference.dart +++ b/lib/localPreference/local_preference.dart @@ -28,7 +28,7 @@ class LocalPreference { if (result.isNotEmpty) { return result.first['city_id'] as int; } - return 1; + return 0; } /// Insert default onboarding row (call once in splash) @@ -90,4 +90,53 @@ class LocalPreference { static Future resetOnboarding() async { await updateOnboardingPage(0); } + + static Future initLoginState() async { + final db = await LocalDatabase().database; + + final result = await db.query('login_state'); + + if (result.isEmpty) { + await db.insert( + 'login_state', + { + 'id': 1, + 'is_login': 0, // false by default + }, + ); + } + } + + static Future setIsLogin(bool value) async { + final db = await LocalDatabase().database; + + await db.insert( + 'login_state', + { + 'id': 1, + 'is_login': value ? 1 : 0, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + static Future isLogin() async { + final db = await LocalDatabase().database; + + final result = await db.query( + 'login_state', + where: 'id = ?', + whereArgs: [1], + ); + + if (result.isNotEmpty) { + return (result.first['is_login'] as int) == 1; + } + return false; + } + + static Future logout() async { + await setIsLogin(false); + } + } diff --git a/lib/login/bloc/login/login_bloc.dart b/lib/login/bloc/login/login_bloc.dart new file mode 100644 index 0000000..f00aeb3 --- /dev/null +++ b/lib/login/bloc/login/login_bloc.dart @@ -0,0 +1,29 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:citycards_customer/login/repository/login_repository.dart'; +import 'login_event.dart'; +import 'login_state.dart'; + +class LoginBloc extends Bloc { + final LoginRepository _loginRepository; + + LoginBloc({required LoginRepository loginRepository}) + : _loginRepository = loginRepository, + super(LoginInitial()) { + on(_onSendEmailOtp); + } + + Future _onSendEmailOtp( + SendEmailOtpEvent event, + Emitter emit, + ) async { + emit(LoginLoading()); + try { + final response = await _loginRepository.sendEmailOtp( + emailAddress: event.emailAddress, + ); + emit(SendOtpSuccess(response: response)); + } catch (e) { + emit(LoginError(errorMessage: e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/login/bloc/login/login_event.dart b/lib/login/bloc/login/login_event.dart new file mode 100644 index 0000000..04685c1 --- /dev/null +++ b/lib/login/bloc/login/login_event.dart @@ -0,0 +1,7 @@ +abstract class LoginEvent {} + +class SendEmailOtpEvent extends LoginEvent { + final String emailAddress; + + SendEmailOtpEvent({required this.emailAddress}); +} \ No newline at end of file diff --git a/lib/login/bloc/login/login_state.dart b/lib/login/bloc/login/login_state.dart new file mode 100644 index 0000000..d4d58ca --- /dev/null +++ b/lib/login/bloc/login/login_state.dart @@ -0,0 +1,17 @@ +abstract class LoginState {} + +class LoginInitial extends LoginState {} + +class LoginLoading extends LoginState {} + +class SendOtpSuccess extends LoginState { + final Map response; + + SendOtpSuccess({required this.response}); +} + +class LoginError extends LoginState { + final String errorMessage; + + LoginError({required this.errorMessage}); +} \ No newline at end of file diff --git a/lib/login/bloc/verify/verify_bloc.dart b/lib/login/bloc/verify/verify_bloc.dart new file mode 100644 index 0000000..7385e78 --- /dev/null +++ b/lib/login/bloc/verify/verify_bloc.dart @@ -0,0 +1,49 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:citycards_customer/login/repository/login_repository.dart'; +import '../../../create_account/models/create_account_model.dart'; +import 'verify_event.dart'; +import 'verify_state.dart'; + +class VerifyOtpBloc extends Bloc { + final LoginRepository _loginRepository; + + VerifyOtpBloc({required LoginRepository loginRepository}) + : _loginRepository = loginRepository, + super(VerifyOtpInitial()) { + on(_onVerifyEmailOtp); + on(_onResendOtp); + } + + Future _onVerifyEmailOtp( + VerifyEmailOtpEvent event, + Emitter emit, + ) async { + emit(VerifyOtpLoading()); + try { + final response = await _loginRepository.verifyEmailOtp( + emailAddress: event.emailAddress, + otp: event.otp, + ); + + final userModel = UserRegisteredModel.fromJson(response); + emit(VerifyOtpSuccess(response: userModel)); + } catch (e) { + emit(VerifyOtpError(errorMessage: e.toString())); + } + } + + Future _onResendOtp( + ResendOtpEvent event, + Emitter emit, + ) async { + emit(ResendOtpLoading()); + try { + final response = await _loginRepository.sendEmailOtp( + emailAddress: event.emailAddress, + ); + emit(ResendOtpSuccess(response: response)); + } catch (e) { + emit(VerifyOtpError(errorMessage: e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/login/bloc/verify/verify_event.dart b/lib/login/bloc/verify/verify_event.dart new file mode 100644 index 0000000..bc333f5 --- /dev/null +++ b/lib/login/bloc/verify/verify_event.dart @@ -0,0 +1,17 @@ +abstract class VerifyOtpEvent {} + +class VerifyEmailOtpEvent extends VerifyOtpEvent { + final String emailAddress; + final String otp; + + VerifyEmailOtpEvent({ + required this.emailAddress, + required this.otp, + }); +} + +class ResendOtpEvent extends VerifyOtpEvent { + final String emailAddress; + + ResendOtpEvent({required this.emailAddress}); +} \ No newline at end of file diff --git a/lib/login/bloc/verify/verify_state.dart b/lib/login/bloc/verify/verify_state.dart new file mode 100644 index 0000000..e3a9ecf --- /dev/null +++ b/lib/login/bloc/verify/verify_state.dart @@ -0,0 +1,27 @@ +import '../../../create_account/models/create_account_model.dart'; + +abstract class VerifyOtpState {} + +class VerifyOtpInitial extends VerifyOtpState {} + +class VerifyOtpLoading extends VerifyOtpState {} + +class VerifyOtpSuccess extends VerifyOtpState { + final UserRegisteredModel response; + + VerifyOtpSuccess({required this.response}); +} + +class ResendOtpLoading extends VerifyOtpState {} + +class ResendOtpSuccess extends VerifyOtpState { + final Map response; + + ResendOtpSuccess({required this.response}); +} + +class VerifyOtpError extends VerifyOtpState { + final String errorMessage; + + VerifyOtpError({required this.errorMessage}); +} \ No newline at end of file diff --git a/lib/login/repository/login_repository.dart b/lib/login/repository/login_repository.dart new file mode 100644 index 0000000..3f949aa --- /dev/null +++ b/lib/login/repository/login_repository.dart @@ -0,0 +1,44 @@ +import 'package:citycards_customer/networkApiServices/api_urls.dart'; +import 'package:citycards_customer/networkApiServices/network_api_services.dart'; + +class LoginRepository { + final NetworkApiService _apiServices = NetworkApiService(); + + /// Send OTP to email address + Future> sendEmailOtp({ + required String emailAddress, + }) async { + try { + final response = await _apiServices.postApi( + url: ApiUrls.sendOtp, // existing API key + data: { + 'emailAddress': emailAddress, + }, + ); + + return response.data as Map; + } catch (e) { + throw Exception('Failed to send OTP: $e'); + } + } + + /// Verify OTP + Future> verifyEmailOtp({ + required String emailAddress, + required String otp, + }) async { + try { + final response = await _apiServices.postApi( + url: ApiUrls.verifyOtp, // add this in ApiUrls + data: { + 'emailAddress': emailAddress, + 'otp': otp, + }, + ); + + return response.data as Map; + } catch (e) { + throw Exception('Failed to verify OTP: $e'); + } + } +} diff --git a/lib/login/view/login_email_bottomsheet.dart b/lib/login/view/login_email_bottomsheet.dart new file mode 100644 index 0000000..6015c8b --- /dev/null +++ b/lib/login/view/login_email_bottomsheet.dart @@ -0,0 +1,172 @@ +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:citycards_customer/login/view/verify_otp_bottomsheet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../bloc/login/login_bloc.dart'; +import '../bloc/login/login_state.dart'; +import '../bloc/login/login_event.dart'; +import '../bloc/verify/verify_bloc.dart'; +import '../repository/login_repository.dart'; + +class LoginEmailBottomsheet extends StatefulWidget { + const LoginEmailBottomsheet({super.key}); + + @override + State createState() => _LoginEmailBottomsheetState(); +} + +class _LoginEmailBottomsheetState extends State { + final TextEditingController _emailController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is SendOtpSuccess) { + Navigator.pop(context); + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (context) => BlocProvider( + create: (context) => VerifyOtpBloc( + loginRepository: LoginRepository(), + ), + child: VerifyOtpBottomsheet( + emailAddress: _emailController.text.trim(), + ), + ), + ); + } else if (state is LoginError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + backgroundColor: Colors.red, + ), + ); + } + }, + child: AnimatedPadding( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + padding: EdgeInsets.only( + top: 24.h, + left: 20.h, + right: 20.h, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SafeArea( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset("assets/logo/logo_city_cards_orange.png", scale: 4), + SizedBox(height: 8.h), + CustomText(text: "Get Started", size: 18.sp, weight: FontWeight.w500), + SizedBox(height: 42.h), + CustomText( + text: "Enter your email to begin your CityCards journey", + size: 14.sp, + color: const Color(0xFF000000).withOpacity(.6), + ), + SizedBox(height: 12.h), + TextField( + controller: _emailController, + decoration: InputDecoration( + filled: true, + contentPadding: EdgeInsets.symmetric(vertical: 6.h), + fillColor: const Color(0xFFFFF5F5), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w), + borderRadius: BorderRadius.circular(8.sp), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: const Color(0xFFBB474A), width: 0.4.w), + borderRadius: BorderRadius.circular(8.sp), + ), + prefixIcon: const Icon(Icons.email_outlined, color: Color(0xFFF95F62)), + hintText: "john.doe@gmail.com", + hintStyle: TextStyle( + color: const Color(0xFF000000).withOpacity(0.6), + fontSize: 12.sp, + ), + ), + ), + SizedBox(height: 38.h), + BlocBuilder( + builder: (context, state) { + final isLoading = state is LoginLoading; + return CustomFilledButton( + onTap: () { + if (isLoading) return; + + final email = _emailController.text.trim(); + if (email.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please enter your email'), + backgroundColor: Colors.red, + ), + ); + return; + } + context.read().add( + SendEmailOtpEvent(emailAddress: email), + ); + }, + label: isLoading ? "Sending..." : "Continue", + width: double.infinity, + ); + }, + ), + SizedBox(height: 20.h), + InkWell( + onTap: () { + Navigator.of(context).pushNamed(RouteConstants.createAcct); + }, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Already have an account?", + style: TextStyle( + color: Colors.black.withOpacity(0.6), + fontSize: 12.sp, + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: " Sign in", + style: TextStyle( + color: const Color(0xFFF95F62), + fontSize: 12.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + SizedBox(height: 15.h), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart new file mode 100644 index 0000000..c93f6e4 --- /dev/null +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -0,0 +1,225 @@ +import 'package:citycards_customer/common_packages/custom_filled_button.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../core/route_constants.dart'; +import '../bloc/verify/verify_bloc.dart'; +import '../bloc/verify/verify_event.dart'; +import '../bloc/verify/verify_state.dart'; + +class VerifyOtpBottomsheet extends StatefulWidget { + final String emailAddress; + + const VerifyOtpBottomsheet({ + super.key, + required this.emailAddress, + }); + + @override + State createState() => _VerifyOtpBottomsheetState(); +} + +class _VerifyOtpBottomsheetState extends State { + String _otpCode = ''; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is VerifyOtpSuccess) { + Navigator.pop(context); // Close the bottom sheet + + if (state.response.userExists) { + // User exists - navigate to home/dashboard + // Navigator.of(context).pushReplacementNamed(RouteConstants.home); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('OTP verified successfully!'), + backgroundColor: Colors.green, + ), + ); + } else { + // User doesn't exist - navigate to create account + Navigator.of(context).pushReplacementNamed(RouteConstants.createAcct,arguments: widget.emailAddress); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please complete your profile'), + backgroundColor: Colors.orange, + ), + ); + } + } else if (state is ResendOtpSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('OTP resent successfully!'), + backgroundColor: Colors.green, + ), + ); + } else if (state is VerifyOtpError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + backgroundColor: Colors.red, + ), + ); + } + }, + child: AnimatedPadding( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + padding: EdgeInsets.only( + top: 24.h, + left: 20.h, + right: 20.h, + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset("assets/logo/logo_city_cards_orange.png", scale: 4), + SizedBox(height: 8.h), + CustomText( + text: "Verify your phone", + size: 18.sp, + weight: FontWeight.w500, + ), + SizedBox(height: 42.h), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Enter the verification code sent to your email id", + style: TextStyle( + fontSize: 14.sp, + color: Colors.black.withOpacity(0.6), + ), + ), + TextSpan( + text: " ${widget.emailAddress}", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ], + ), + ), + SizedBox(height: 15.h), + OtpTextField( + numberOfFields: 6, + borderWidth: 0.4.w, + fieldWidth: 48.w, + fieldHeight: 60.h, + borderRadius: BorderRadius.circular(8.r), + filled: true, + fillColor: const Color(0xFFFFF5F5), + borderColor: const Color(0xFFBB474A), + cursorColor: const Color(0xFFF95F62), + showFieldAsBox: true, + textStyle: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + onCodeChanged: (code) { + _otpCode = code; + }, + onSubmit: (code) { + _otpCode = code; + debugPrint("OTP entered: $code"); + }, + ), + SizedBox(height: 20.h), + BlocBuilder( + builder: (context, state) { + final isResending = state is ResendOtpLoading; + return InkWell( + onTap: isResending + ? null + : () { + context.read().add( + ResendOtpEvent(emailAddress: widget.emailAddress), + ); + }, + child: Text( + isResending ? "Resending..." : "Resend OTP", + style: TextStyle( + color: isResending + ? Colors.grey + : const Color(0xFFF95F62), + fontSize: 12.sp, + fontWeight: FontWeight.w600, + ), + ), + ); + }, + ), + SizedBox(height: 22.h), + BlocBuilder( + builder: (context, state) { + final isLoading = state is VerifyOtpLoading; + return CustomFilledButton( + onTap: () { + if (isLoading) return; + + if (_otpCode.isEmpty || _otpCode.length < 6) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please enter complete OTP'), + backgroundColor: Colors.red, + ), + ); + return; + } + + context.read().add( + VerifyEmailOtpEvent( + emailAddress: widget.emailAddress, + otp: _otpCode, + ), + ); + }, + label: isLoading ? "Verifying..." : "Continue", + width: double.infinity, + ); + }, + ), + SizedBox(height: 20.h), + InkWell( + onTap: () { + Navigator.of(context).pushNamed(RouteConstants.createAcct); + }, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Already have an account?", + style: TextStyle( + color: Colors.black.withOpacity(0.6), + fontSize: 12.sp, + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: " Sign in", + style: TextStyle( + color: const Color(0xFFF95F62), + fontSize: 12.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + SizedBox(height: 15.h), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 0e7e6e1..3464ac9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,8 @@ import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart'; import 'home/bloc/registeredHome/home_bloc.dart'; import 'home/repository/first_time_user_home_repository.dart'; import 'home/repository/home_repository.dart'; +import 'login/bloc/login/login_bloc.dart'; +import 'login/repository/login_repository.dart'; import 'my_pass/blocs/my_pass_bloc.dart'; void main() { @@ -53,6 +55,11 @@ class MyApp extends StatelessWidget { homeRepository: HomeRepository(), ), ), + BlocProvider( + create: (context) => LoginBloc( + loginRepository: LoginRepository(), + ), + ), ], child: MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index c204cd6..c6339e4 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -8,4 +8,11 @@ class ApiUrls { static const attractionsList = "$baseUrl/mobile/list/all"; static const attractionDetails = "$baseUrl/mobile/list"; static const home = "$baseUrl/mobile"; + static const faqPrivacyTerms = "$baseUrl/mobile/user/cms-data"; + + + //Post Apis + static const createAccount = "$baseUrl/mobile/user/register"; + static const sendOtp = "$baseUrl/mobile/send-otp"; + static const verifyOtp = "$baseUrl/mobile/user/verify-otp"; } \ No newline at end of file diff --git a/lib/privacy/privacy_view.dart b/lib/privacy/privacy_view.dart deleted file mode 100644 index ce82715..0000000 --- a/lib/privacy/privacy_view.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:citycards_customer/common_packages/back_widget.dart'; -import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class PrivacyPolicyPage extends StatelessWidget { - const PrivacyPolicyPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: Column( - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: true, showDivider: true,), - backWidget(context,"Privacy Policy", Colors.black), - SizedBox(height: 32.h), - CustomText( - text: - "Your use of our website is governed by the following terms and conditions (“Terms of Use”), as well as the CARDONE CAPITAL Privacy Policy and other operating rules, minimum qualifications and cautions posted throughout the website or presented to you individually during the course of your use of the website (collectively, the “Terms”). \n\n" - "The Terms govern your use of the website and CARDONE CAPITAL reserves the right to update or replace the Terms any time without notice. You are advised to review the Terms for any changes when you visit the website even if you have not received a notification of changes as you are bound by them even if you have not reviewed them. \n\n" - "Your viewing and use of the website after such change constitutes your acceptance of the Terms and any changes to such terms. If at any time you do not want to be bound by the Terms you should logout, exit and cease using the website immediately.", - size: 14.sp, - weight: FontWeight.w400, - color: Color(0xFF000000).withOpacity(.6), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart b/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart new file mode 100644 index 0000000..6c5f876 --- /dev/null +++ b/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart @@ -0,0 +1,25 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/faq_n_privacy_n_terms_repository.dart'; +import 'faq_n_privacy_n_terms_event.dart'; +import 'faq_n_privacy_n_terms_state.dart'; + +class FAQnPrivacynTermsBloc extends Bloc { + final FAQnPrivacynTermsRepository repository; + + FAQnPrivacynTermsBloc(this.repository) : super(FAQnPrivacynTermsInitial()) { + on(_onFetchFAQnPrivacynTerms); + } + + Future _onFetchFAQnPrivacynTerms( + FetchFAQnPrivacynTermsEvent event, + Emitter emit, + ) async { + emit(FAQnPrivacynTermsLoading()); + try { + final data = await repository.fetchFAQnPrivacynTerms(); + emit(FAQnPrivacynTermsLoaded(data)); + } catch (e) { + emit(FAQnPrivacynTermsError(e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart b/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart new file mode 100644 index 0000000..fe03efe --- /dev/null +++ b/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart @@ -0,0 +1,3 @@ +abstract class FAQnPrivacynTermsEvent {} + +class FetchFAQnPrivacynTermsEvent extends FAQnPrivacynTermsEvent {} \ No newline at end of file diff --git a/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart b/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart new file mode 100644 index 0000000..07f228f --- /dev/null +++ b/lib/profile/bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart @@ -0,0 +1,19 @@ +import '../../models/faq_n_privacy_n_terms_model.dart'; + +abstract class FAQnPrivacynTermsState {} + +class FAQnPrivacynTermsInitial extends FAQnPrivacynTermsState {} + +class FAQnPrivacynTermsLoading extends FAQnPrivacynTermsState {} + +class FAQnPrivacynTermsLoaded extends FAQnPrivacynTermsState { + final FAQnPrivacynTerms data; + + FAQnPrivacynTermsLoaded(this.data); +} + +class FAQnPrivacynTermsError extends FAQnPrivacynTermsState { + final String message; + + FAQnPrivacynTermsError(this.message); +} \ No newline at end of file diff --git a/lib/profile/models/faq_n_privacy_n_terms_model.dart b/lib/profile/models/faq_n_privacy_n_terms_model.dart new file mode 100644 index 0000000..4ff7495 --- /dev/null +++ b/lib/profile/models/faq_n_privacy_n_terms_model.dart @@ -0,0 +1,118 @@ +class FAQnPrivacynTerms { + final StaticContent? aboutUs; + final StaticContent? privacyPolicy; + final StaticContent? terms; + final List? faqs; + + FAQnPrivacynTerms({ + this.aboutUs, + this.privacyPolicy, + this.terms, + this.faqs, + }); + + factory FAQnPrivacynTerms.fromJson(Map json) { + return FAQnPrivacynTerms( + aboutUs: json['aboutUs'] != null + ? StaticContent.fromJson(json['aboutUs']) + : null, + privacyPolicy: json['privacyPolicy'] != null + ? StaticContent.fromJson(json['privacyPolicy']) + : null, + terms: json['terms'] != null + ? StaticContent.fromJson(json['terms']) + : null, + faqs: json['faqs'] != null + ? (json['faqs'] as List) + .map((e) => FaqCategory.fromJson(e)) + .toList() + : [], + ); + } + + Map toJson() { + return { + 'aboutUs': aboutUs?.toJson(), + 'privacyPolicy': privacyPolicy?.toJson(), + 'terms': terms?.toJson(), + 'faqs': faqs?.map((e) => e.toJson()).toList(), + }; + } +} + +/// ---------------- STATIC CONTENT MODEL ---------------- +class StaticContent { + final String? content; + + StaticContent({this.content}); + + factory StaticContent.fromJson(Map json) { + return StaticContent( + content: json['content'], + ); + } + + Map toJson() { + return { + 'content': content, + }; + } +} + +/// ---------------- FAQ CATEGORY MODEL ---------------- +class FaqCategory { + final int? categoryId; + final String? categoryName; + final List? faqs; + + FaqCategory({ + this.categoryId, + this.categoryName, + this.faqs, + }); + + factory FaqCategory.fromJson(Map json) { + return FaqCategory( + categoryId: json['categoryId'], + categoryName: json['categoryName'], + faqs: json['faqs'] != null + ? (json['faqs'] as List) + .map((e) => FaqItem.fromJson(e)) + .toList() + : [], + ); + } + + Map toJson() { + return { + 'categoryId': categoryId, + 'categoryName': categoryName, + 'faqs': faqs?.map((e) => e.toJson()).toList(), + }; + } +} + +/// ---------------- FAQ ITEM MODEL ---------------- +class FaqItem { + final String? question; + final String? answer; + + FaqItem({ + this.question, + this.answer, + }); + + factory FaqItem.fromJson(Map json) { + return FaqItem( + question: json['question'], + answer: json['answer'], + ); + } + + Map toJson() { + return { + 'question': question, + 'answer': answer, + }; + } +} diff --git a/lib/profile/repository/faq_n_privacy_n_terms_repository.dart b/lib/profile/repository/faq_n_privacy_n_terms_repository.dart new file mode 100644 index 0000000..726559d --- /dev/null +++ b/lib/profile/repository/faq_n_privacy_n_terms_repository.dart @@ -0,0 +1,17 @@ + +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../models/faq_n_privacy_n_terms_model.dart'; + +class FAQnPrivacynTermsRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Fetch FAQ, Privacy Policy & Terms data + Future fetchFAQnPrivacynTerms() async { + final response = await _apiService.getApi( + url: ApiUrls.faqPrivacyTerms, + ); + + return FAQnPrivacynTerms.fromJson(response.data); + } +} diff --git a/lib/profile/view/faq/faq_view.dart b/lib/profile/view/faq/faq_view.dart new file mode 100644 index 0000000..f8ac792 --- /dev/null +++ b/lib/profile/view/faq/faq_view.dart @@ -0,0 +1,152 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/back_widget.dart'; +import 'package:citycards_customer/common_packages/custom_expansion_tile.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart'; +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart'; +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart'; +import '../../models/faq_n_privacy_n_terms_model.dart'; +import '../../repository/faq_n_privacy_n_terms_repository.dart'; + +class FaqPage extends StatelessWidget { + const FaqPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FAQnPrivacynTermsBloc(FAQnPrivacynTermsRepository()) + ..add(FetchFAQnPrivacynTermsEvent()), + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state is FAQnPrivacynTermsLoading) { + return Center(child: CircularProgressIndicator()); + } + + if (state is FAQnPrivacynTermsError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: ${state.message}'), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () { + context.read().add(FetchFAQnPrivacynTermsEvent()); + }, + child: Text('Retry'), + ), + ], + ), + ); + } + + if (state is FAQnPrivacynTermsLoaded) { + final faqs = state.data.faqs ?? []; + + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: true, + showDivider: true, + ), + backWidget(context, "FAQ", Colors.black), + SizedBox(height: 34.h), + + // Dynamic FAQ sections from API + ...faqs.asMap().entries.map((entry) { + final index = entry.key; + final category = entry.value; + + return Column( + children: [ + FAQSection( + title: category.categoryName ?? '', + faqs: category.faqs ?? [], + ), + if (index < faqs.length - 1) SizedBox(height: 20.h), + ], + ); + }).toList(), + ], + ), + ), + ); + } + + return Center(child: Text('No data available')); + }, + ), + ), + ), + ); + } +} + +// Widget for FAQ section +Widget FAQSection({required String title, required List faqs}) { + return Container( + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFFF95F62)), + borderRadius: BorderRadius.circular(10.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Section heading + CustomText( + text: title, + size: 16.sp, + weight: FontWeight.w500, + color: Color(0xFF212121), + ), + SizedBox(height: 12.h), + + // Dynamic list of questions + Column( + children: faqs.map((faq) { + int index = faqs.indexOf(faq); + return Column( + children: [ + CustomExpansionTile( + minTileHeight: 42.h, + borderRadius: BorderRadius.circular(5.r), + backgroundColor: Color(0xFFFEE7E7), + collapsedBackgroundColor: Color(0xFFFEE7E7), + tilePadding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 0, + ), + childrenPadding: EdgeInsets.only(left: 12.w, right: 12.w, bottom: 12.h), + title: Text( + faq.question ?? '', + style: TextStyle(fontSize: 14.sp), + ), + children: [ + Text( + faq.answer ?? '', + style: TextStyle(color: Color(0xFF5C5C5C), fontSize: 14.sp), + ), + ], + ), + if (index != faqs.length - 1) SizedBox(height: 8.h), + ], + ); + }).toList(), + ), + ], + ), + ); +} \ No newline at end of file diff --git a/lib/profile/view/privacy/privacy_view.dart b/lib/profile/view/privacy/privacy_view.dart new file mode 100644 index 0000000..bd7869b --- /dev/null +++ b/lib/profile/view/privacy/privacy_view.dart @@ -0,0 +1,81 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/back_widget.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart'; +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart'; +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart'; +import '../../repository/faq_n_privacy_n_terms_repository.dart'; + +class PrivacyPolicyPage extends StatelessWidget { + const PrivacyPolicyPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FAQnPrivacynTermsBloc(FAQnPrivacynTermsRepository()) + ..add(FetchFAQnPrivacynTermsEvent()), + child: Scaffold( + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state is FAQnPrivacynTermsLoading) { + return Center(child: CircularProgressIndicator()); + } + + if (state is FAQnPrivacynTermsError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: ${state.message}'), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () { + context.read().add(FetchFAQnPrivacynTermsEvent()); + }, + child: Text('Retry'), + ), + ], + ), + ); + } + + String privacyContent = ''; + if (state is FAQnPrivacynTermsLoaded) { + privacyContent = state.data.privacyPolicy?.content ?? + 'No privacy policy content available.'; + } + + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: true, + showDivider: true, + ), + backWidget(context, "Privacy Policy", Colors.black), + SizedBox(height: 32.h), + CustomText( + text: privacyContent, + size: 14.sp, + weight: FontWeight.w400, + color: Color(0xFF000000).withOpacity(.6), + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/profile/profile_page_view.dart b/lib/profile/view/profile_page_view.dart similarity index 100% rename from lib/profile/profile_page_view.dart rename to lib/profile/view/profile_page_view.dart diff --git a/lib/profile/view/terms_and_condition/terms_and_condition_view.dart b/lib/profile/view/terms_and_condition/terms_and_condition_view.dart new file mode 100644 index 0000000..3259902 --- /dev/null +++ b/lib/profile/view/terms_and_condition/terms_and_condition_view.dart @@ -0,0 +1,81 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/back_widget.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_bloc.dart'; +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_event.dart'; +import '../../bloc/fnqPrivacyTerms/faq_n_privacy_n_terms_state.dart'; +import '../../repository/faq_n_privacy_n_terms_repository.dart'; + +class TermsAndCondition extends StatelessWidget { + const TermsAndCondition({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FAQnPrivacynTermsBloc(FAQnPrivacynTermsRepository()) + ..add(FetchFAQnPrivacynTermsEvent()), + child: Scaffold( + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state is FAQnPrivacynTermsLoading) { + return Center(child: CircularProgressIndicator()); + } + + if (state is FAQnPrivacynTermsError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: ${state.message}'), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () { + context.read().add(FetchFAQnPrivacynTermsEvent()); + }, + child: Text('Retry'), + ), + ], + ), + ); + } + + String termsContent = ''; + if (state is FAQnPrivacynTermsLoaded) { + termsContent = state.data.terms?.content ?? + 'No terms and conditions content available.'; + } + + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: true, + showDivider: true, + ), + backWidget(context, "Terms and Conditions", Colors.black), + SizedBox(height: 32.h), + CustomText( + text: termsContent, + size: 14.sp, + weight: FontWeight.w400, + color: Color(0xFF000000).withOpacity(.6), + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/splash_screen/bloc/app_start_bloc.dart b/lib/splash_screen/bloc/app_start_bloc.dart new file mode 100644 index 0000000..3f7f80c --- /dev/null +++ b/lib/splash_screen/bloc/app_start_bloc.dart @@ -0,0 +1,40 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../localPreference/local_preference.dart'; +import 'app_start_event.dart'; +import 'app_start_state.dart'; + +class AppStartBloc extends Bloc { + AppStartBloc() : super(AppStartInitial()) { + on(_onCheckAppStartStatus); + } + + Future _onCheckAppStartStatus( + CheckAppStartStatus event, + Emitter emit, + ) async { + emit(AppStartLoading()); + + // Initialize onboarding table if needed + await LocalPreference.initOnboarding(); + + // Get current onboarding page + final currentPage = await LocalPreference.getOnboardingPage(); + + print('🔍 Current onboarding page: $currentPage'); + + // Navigation logic based on page value + if (currentPage == 0) { + // First time user, not seen intro yet + print('➡️ Navigating to Intro Screen'); + emit(NavigateToIntro()); + } else if (currentPage == 1) { + // Seen intro, but first time on home + print('➡️ Navigating to First Time Home'); + emit(NavigateToFirstTimeHome()); + } else { + // Regular user (page >= 2) + print('➡️ Navigating to Regular Home'); + emit(NavigateToHome()); + } + } +} \ No newline at end of file diff --git a/lib/splash_screen/bloc/app_start_event.dart b/lib/splash_screen/bloc/app_start_event.dart new file mode 100644 index 0000000..69100a9 --- /dev/null +++ b/lib/splash_screen/bloc/app_start_event.dart @@ -0,0 +1,3 @@ +abstract class AppStartEvent {} + +class CheckAppStartStatus extends AppStartEvent {} \ No newline at end of file diff --git a/lib/splash_screen/bloc/app_start_state.dart b/lib/splash_screen/bloc/app_start_state.dart new file mode 100644 index 0000000..17cd3bf --- /dev/null +++ b/lib/splash_screen/bloc/app_start_state.dart @@ -0,0 +1,11 @@ +abstract class AppStartState {} + +class AppStartInitial extends AppStartState {} + +class AppStartLoading extends AppStartState {} + +class NavigateToIntro extends AppStartState {} + +class NavigateToFirstTimeHome extends AppStartState {} + +class NavigateToHome extends AppStartState {} \ No newline at end of file diff --git a/lib/splash_screen/views/splash_screen.dart b/lib/splash_screen/views/splash_screen.dart index 3cd09c9..262dd4c 100644 --- a/lib/splash_screen/views/splash_screen.dart +++ b/lib/splash_screen/views/splash_screen.dart @@ -1,41 +1,64 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:citycards_customer/intro_screens/views/intro_screen_view.dart'; +import 'package:citycards_customer/core/route_constants.dart'; +import '../../home/views/first_time_user_home_page.dart'; +import '../bloc/app_start_bloc.dart'; +import '../bloc/app_start_event.dart'; +import '../bloc/app_start_state.dart'; -class SplashScreen extends StatefulWidget { +class SplashScreen extends StatelessWidget { const SplashScreen({super.key}); - @override - State createState() => _SplashScreenState(); -} - -class _SplashScreenState extends State { - @override - void initState() { - super.initState(); - - Future.delayed(const Duration(seconds: 4), () { - if (mounted) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (_) => IntroScreensView()), - ); - } - }); - } - @override Widget build(BuildContext context) { - print('🎯 SplashScreen build called'); - return Scaffold( - backgroundColor: const Color(0xFFF95F62), // Coral red background - body: Center( - child: Lottie.asset( - 'assets/intro/animation.json', // Your Lottie file - fit: BoxFit.cover, - repeat: true, + return BlocProvider( + create: (_) => AppStartBloc()..add(CheckAppStartStatus()), + child: BlocListener( + listener: (context, state) { + if (state is NavigateToIntro) { + Future.delayed(const Duration(seconds: 4), () { + if (context.mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => IntroScreensView()), + ); + } + }); + } else if (state is NavigateToFirstTimeHome) { + Future.delayed(const Duration(seconds: 4), () { + if (context.mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const FirstTimeUserHomePage(), + ), + ); + } + }); + } else if (state is NavigateToHome) { + Future.delayed(const Duration(seconds: 4), () { + if (context.mounted) { + Navigator.pushReplacementNamed( + context, + RouteConstants.home, + ); + } + }); + } + }, + child: Scaffold( + backgroundColor: const Color(0xFFF95F62), + body: Center( + child: Lottie.asset( + 'assets/intro/animation.json', + fit: BoxFit.cover, + repeat: true, + ), + ), ), ), ); } -} +} \ No newline at end of file diff --git a/lib/terms_and_condition/terms_and_condition_view.dart b/lib/terms_and_condition/terms_and_condition_view.dart deleted file mode 100644 index 767698a..0000000 --- a/lib/terms_and_condition/terms_and_condition_view.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:citycards_customer/common_packages/back_widget.dart'; -import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class TermsAndCondition extends StatelessWidget { - const TermsAndCondition({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: Column( - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: true, showDivider: true,), - - - // Back + Title - backWidget(context,"Terms and Conditions", Colors.black), - SizedBox(height: 32.h), - CustomText( - text: - "Your use of our website is governed by the following terms and conditions (“Terms of Use”), as well as the CARDONE CAPITAL Privacy Policy and other operating rules, minimum qualifications and cautions posted throughout the website or presented to you individually during the course of your use of the website (collectively, the “Terms”). \n\n" - "The Terms govern your use of the website and CARDONE CAPITAL reserves the right to update or replace the Terms any time without notice. You are advised to review the Terms for any changes when you visit the website even if you have not received a notification of changes as you are bound by them even if you have not reviewed them. \n\n" - "Your viewing and use of the website after such change constitutes your acceptance of the Terms and any changes to such terms. If at any time you do not want to be bound by the Terms you should logout, exit and cease using the website immediately.", - size: 14.sp, - weight: FontWeight.w400, - color: Color(0xFF000000).withOpacity(.6), - ), - ], - ), - ), - ), - ), - ); - } -}