api integrtaion send Otp ,verify otp and faq , Terms ,Policy
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
48
lib/create_account/bloc/create_account_bloc.dart
Normal file
48
lib/create_account/bloc/create_account_bloc.dart
Normal file
@@ -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<CreateAccountEvent, CreateAccountState> {
|
||||
final CreateAccountRepository repository;
|
||||
|
||||
CreateAccountBloc({required this.repository})
|
||||
: super(const CreateAccountInitial()) {
|
||||
on<CreateAccountSubmitted>(_onCreateAccountSubmitted);
|
||||
on<CreateAccountReset>(_onCreateAccountReset);
|
||||
}
|
||||
|
||||
Future<void> _onCreateAccountSubmitted(
|
||||
CreateAccountSubmitted event,
|
||||
Emitter<CreateAccountState> 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<CreateAccountState> emit,
|
||||
) {
|
||||
emit(const CreateAccountInitial());
|
||||
}
|
||||
}
|
||||
40
lib/create_account/bloc/create_account_event.dart
Normal file
40
lib/create_account/bloc/create_account_event.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class CreateAccountEvent extends Equatable {
|
||||
const CreateAccountEvent();
|
||||
|
||||
@override
|
||||
List<Object?> 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<Object?> get props => [
|
||||
firstName,
|
||||
lastName,
|
||||
emailAddress,
|
||||
mobileNumber,
|
||||
address1,
|
||||
address2,
|
||||
];
|
||||
}
|
||||
|
||||
class CreateAccountReset extends CreateAccountEvent {
|
||||
const CreateAccountReset();
|
||||
}
|
||||
38
lib/create_account/bloc/create_account_state.dart
Normal file
38
lib/create_account/bloc/create_account_state.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class CreateAccountState extends Equatable {
|
||||
const CreateAccountState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class CreateAccountInitial extends CreateAccountState {
|
||||
const CreateAccountInitial();
|
||||
}
|
||||
|
||||
class CreateAccountLoading extends CreateAccountState {
|
||||
const CreateAccountLoading();
|
||||
}
|
||||
|
||||
class CreateAccountSuccess extends CreateAccountState {
|
||||
final String message;
|
||||
final Map<String, dynamic> userData;
|
||||
|
||||
const CreateAccountSuccess({
|
||||
required this.message,
|
||||
required this.userData,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, userData];
|
||||
}
|
||||
|
||||
class CreateAccountFailure extends CreateAccountState {
|
||||
final String errorMessage;
|
||||
|
||||
const CreateAccountFailure({required this.errorMessage});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [errorMessage];
|
||||
}
|
||||
@@ -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")
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
86
lib/create_account/models/create_account_model.dart
Normal file
86
lib/create_account/models/create_account_model.dart
Normal file
@@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'fullName': fullName,
|
||||
'emailAddress': emailAddress,
|
||||
'role': role,
|
||||
'roleId': roleId,
|
||||
};
|
||||
}
|
||||
}
|
||||
33
lib/create_account/repository/create_account_repository.dart
Normal file
33
lib/create_account/repository/create_account_repository.dart
Normal file
@@ -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<Map<String, dynamic>> 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<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to create account: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
203
lib/create_account/view/create_account_view.dart
Normal file
203
lib/create_account/view/create_account_view.dart
Normal file
@@ -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<CreateAccountBloc>().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<CreateAccountBloc, CreateAccountState>(
|
||||
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<CreateAccountBloc, CreateAccountState>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<FAQItem> 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<FAQItem> 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<FAQItem> 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<FAQItem> 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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<AppStartEvent, AppStartState> {
|
||||
AppStartBloc() : super(AppStartFirstTime()) {
|
||||
on<StartApp>((event, emit) {
|
||||
emit(AppStartFirstTime()); // always first-time
|
||||
});
|
||||
|
||||
on<MarkUserAsRegistered>((event, emit) {
|
||||
emit(AppStartRegistered());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FirstTimeUserHomePage> createState() => _FirstTimeUserHomePageState();
|
||||
@@ -46,6 +47,20 @@ class _FirstTimeUserHomePageState extends State<FirstTimeUserHomePage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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<FirstTimeUserHomePage> {
|
||||
borderRadius: BorderRadius.circular(25.r),
|
||||
),
|
||||
),
|
||||
onPressed: widget.onContinue,
|
||||
onPressed: _handleGetCityCard,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
@@ -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<HomePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => AppStartBloc()..add(StartApp()),
|
||||
child: BlocBuilder<AppStartBloc, AppStartState>(
|
||||
builder: (context, state) {
|
||||
// 🚀 Always first time initially
|
||||
if (state is AppStartFirstTime) {
|
||||
return FirstTimeUserHomePage(
|
||||
onContinue: () {
|
||||
context
|
||||
.read<AppStartBloc>()
|
||||
.add(MarkUserAsRegistered());
|
||||
},
|
||||
);
|
||||
}
|
||||
return BlocBuilder<NavigationBloc, NavigationState>(
|
||||
builder: (context, navState) {
|
||||
final currentIndex = navState.selectedIndex;
|
||||
|
||||
// ✅ Registered user flow
|
||||
return BlocBuilder<NavigationBloc, NavigationState>(
|
||||
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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<RegisteredUserHomePage> {
|
||||
@override
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<HomeBloc>().add(FetchHomeData());
|
||||
_checkAndShowCitySelection();
|
||||
}
|
||||
|
||||
Future<void> _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<HomeBloc>().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
|
||||
|
||||
@@ -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<SearchCityBloc>().add(LoadAllCity());
|
||||
} else {
|
||||
context.read<SearchCityBloc>().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<int>(
|
||||
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<SearchCityBloc>().add(LoadAllCity());
|
||||
} else {
|
||||
context.read<SearchCityBloc>().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<SearchCityBloc, CityState>(
|
||||
builder: (context, state) {
|
||||
if (state is CityLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFFF95F62),
|
||||
),
|
||||
);
|
||||
}
|
||||
// City Grid
|
||||
Expanded(
|
||||
child: BlocBuilder<SearchCityBloc, CityState>(
|
||||
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<SearchCityBloc>().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<SearchCityBloc>().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<int>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<IntroScreensView> createState() => _IntroScreensViewState();
|
||||
}
|
||||
|
||||
class _IntroScreensViewState extends State<IntroScreensView> {
|
||||
final PageController _pageController = PageController();
|
||||
|
||||
final IntroScreensViewModel _viewModel = IntroScreensViewModel();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateOnboardingProgress();
|
||||
}
|
||||
|
||||
Future<void> _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),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
''');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<void> resetOnboarding() async {
|
||||
await updateOnboardingPage(0);
|
||||
}
|
||||
|
||||
static Future<void> 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<void> 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<bool> 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<void> logout() async {
|
||||
await setIsLogin(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
29
lib/login/bloc/login/login_bloc.dart
Normal file
29
lib/login/bloc/login/login_bloc.dart
Normal file
@@ -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<LoginEvent, LoginState> {
|
||||
final LoginRepository _loginRepository;
|
||||
|
||||
LoginBloc({required LoginRepository loginRepository})
|
||||
: _loginRepository = loginRepository,
|
||||
super(LoginInitial()) {
|
||||
on<SendEmailOtpEvent>(_onSendEmailOtp);
|
||||
}
|
||||
|
||||
Future<void> _onSendEmailOtp(
|
||||
SendEmailOtpEvent event,
|
||||
Emitter<LoginState> emit,
|
||||
) async {
|
||||
emit(LoginLoading());
|
||||
try {
|
||||
final response = await _loginRepository.sendEmailOtp(
|
||||
emailAddress: event.emailAddress,
|
||||
);
|
||||
emit(SendOtpSuccess(response: response));
|
||||
} catch (e) {
|
||||
emit(LoginError(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
7
lib/login/bloc/login/login_event.dart
Normal file
7
lib/login/bloc/login/login_event.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
abstract class LoginEvent {}
|
||||
|
||||
class SendEmailOtpEvent extends LoginEvent {
|
||||
final String emailAddress;
|
||||
|
||||
SendEmailOtpEvent({required this.emailAddress});
|
||||
}
|
||||
17
lib/login/bloc/login/login_state.dart
Normal file
17
lib/login/bloc/login/login_state.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
abstract class LoginState {}
|
||||
|
||||
class LoginInitial extends LoginState {}
|
||||
|
||||
class LoginLoading extends LoginState {}
|
||||
|
||||
class SendOtpSuccess extends LoginState {
|
||||
final Map<String, dynamic> response;
|
||||
|
||||
SendOtpSuccess({required this.response});
|
||||
}
|
||||
|
||||
class LoginError extends LoginState {
|
||||
final String errorMessage;
|
||||
|
||||
LoginError({required this.errorMessage});
|
||||
}
|
||||
49
lib/login/bloc/verify/verify_bloc.dart
Normal file
49
lib/login/bloc/verify/verify_bloc.dart
Normal file
@@ -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<VerifyOtpEvent, VerifyOtpState> {
|
||||
final LoginRepository _loginRepository;
|
||||
|
||||
VerifyOtpBloc({required LoginRepository loginRepository})
|
||||
: _loginRepository = loginRepository,
|
||||
super(VerifyOtpInitial()) {
|
||||
on<VerifyEmailOtpEvent>(_onVerifyEmailOtp);
|
||||
on<ResendOtpEvent>(_onResendOtp);
|
||||
}
|
||||
|
||||
Future<void> _onVerifyEmailOtp(
|
||||
VerifyEmailOtpEvent event,
|
||||
Emitter<VerifyOtpState> 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<void> _onResendOtp(
|
||||
ResendOtpEvent event,
|
||||
Emitter<VerifyOtpState> emit,
|
||||
) async {
|
||||
emit(ResendOtpLoading());
|
||||
try {
|
||||
final response = await _loginRepository.sendEmailOtp(
|
||||
emailAddress: event.emailAddress,
|
||||
);
|
||||
emit(ResendOtpSuccess(response: response));
|
||||
} catch (e) {
|
||||
emit(VerifyOtpError(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
17
lib/login/bloc/verify/verify_event.dart
Normal file
17
lib/login/bloc/verify/verify_event.dart
Normal file
@@ -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});
|
||||
}
|
||||
27
lib/login/bloc/verify/verify_state.dart
Normal file
27
lib/login/bloc/verify/verify_state.dart
Normal file
@@ -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<String, dynamic> response;
|
||||
|
||||
ResendOtpSuccess({required this.response});
|
||||
}
|
||||
|
||||
class VerifyOtpError extends VerifyOtpState {
|
||||
final String errorMessage;
|
||||
|
||||
VerifyOtpError({required this.errorMessage});
|
||||
}
|
||||
44
lib/login/repository/login_repository.dart
Normal file
44
lib/login/repository/login_repository.dart
Normal file
@@ -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<Map<String, dynamic>> 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<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to send OTP: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify OTP
|
||||
Future<Map<String, dynamic>> 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<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to verify OTP: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
172
lib/login/view/login_email_bottomsheet.dart
Normal file
172
lib/login/view/login_email_bottomsheet.dart
Normal file
@@ -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<LoginEmailBottomsheet> createState() => _LoginEmailBottomsheetState();
|
||||
}
|
||||
|
||||
class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<LoginBloc, LoginState>(
|
||||
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<LoginBloc, LoginState>(
|
||||
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<LoginBloc>().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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
225
lib/login/view/verify_otp_bottomsheet.dart
Normal file
225
lib/login/view/verify_otp_bottomsheet.dart
Normal file
@@ -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<VerifyOtpBottomsheet> createState() => _VerifyOtpBottomsheetState();
|
||||
}
|
||||
|
||||
class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
|
||||
String _otpCode = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<VerifyOtpBloc, VerifyOtpState>(
|
||||
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<VerifyOtpBloc, VerifyOtpState>(
|
||||
builder: (context, state) {
|
||||
final isResending = state is ResendOtpLoading;
|
||||
return InkWell(
|
||||
onTap: isResending
|
||||
? null
|
||||
: () {
|
||||
context.read<VerifyOtpBloc>().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<VerifyOtpBloc, VerifyOtpState>(
|
||||
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<VerifyOtpBloc>().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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<FAQnPrivacynTermsEvent, FAQnPrivacynTermsState> {
|
||||
final FAQnPrivacynTermsRepository repository;
|
||||
|
||||
FAQnPrivacynTermsBloc(this.repository) : super(FAQnPrivacynTermsInitial()) {
|
||||
on<FetchFAQnPrivacynTermsEvent>(_onFetchFAQnPrivacynTerms);
|
||||
}
|
||||
|
||||
Future<void> _onFetchFAQnPrivacynTerms(
|
||||
FetchFAQnPrivacynTermsEvent event,
|
||||
Emitter<FAQnPrivacynTermsState> emit,
|
||||
) async {
|
||||
emit(FAQnPrivacynTermsLoading());
|
||||
try {
|
||||
final data = await repository.fetchFAQnPrivacynTerms();
|
||||
emit(FAQnPrivacynTermsLoaded(data));
|
||||
} catch (e) {
|
||||
emit(FAQnPrivacynTermsError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
abstract class FAQnPrivacynTermsEvent {}
|
||||
|
||||
class FetchFAQnPrivacynTermsEvent extends FAQnPrivacynTermsEvent {}
|
||||
@@ -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);
|
||||
}
|
||||
118
lib/profile/models/faq_n_privacy_n_terms_model.dart
Normal file
118
lib/profile/models/faq_n_privacy_n_terms_model.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
class FAQnPrivacynTerms {
|
||||
final StaticContent? aboutUs;
|
||||
final StaticContent? privacyPolicy;
|
||||
final StaticContent? terms;
|
||||
final List<FaqCategory>? faqs;
|
||||
|
||||
FAQnPrivacynTerms({
|
||||
this.aboutUs,
|
||||
this.privacyPolicy,
|
||||
this.terms,
|
||||
this.faqs,
|
||||
});
|
||||
|
||||
factory FAQnPrivacynTerms.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
|
||||
return StaticContent(
|
||||
content: json['content'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'content': content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// ---------------- FAQ CATEGORY MODEL ----------------
|
||||
class FaqCategory {
|
||||
final int? categoryId;
|
||||
final String? categoryName;
|
||||
final List<FaqItem>? faqs;
|
||||
|
||||
FaqCategory({
|
||||
this.categoryId,
|
||||
this.categoryName,
|
||||
this.faqs,
|
||||
});
|
||||
|
||||
factory FaqCategory.fromJson(Map<String, dynamic> json) {
|
||||
return FaqCategory(
|
||||
categoryId: json['categoryId'],
|
||||
categoryName: json['categoryName'],
|
||||
faqs: json['faqs'] != null
|
||||
? (json['faqs'] as List)
|
||||
.map((e) => FaqItem.fromJson(e))
|
||||
.toList()
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> json) {
|
||||
return FaqItem(
|
||||
question: json['question'],
|
||||
answer: json['answer'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'question': question,
|
||||
'answer': answer,
|
||||
};
|
||||
}
|
||||
}
|
||||
17
lib/profile/repository/faq_n_privacy_n_terms_repository.dart
Normal file
17
lib/profile/repository/faq_n_privacy_n_terms_repository.dart
Normal file
@@ -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<FAQnPrivacynTerms> fetchFAQnPrivacynTerms() async {
|
||||
final response = await _apiService.getApi(
|
||||
url: ApiUrls.faqPrivacyTerms,
|
||||
);
|
||||
|
||||
return FAQnPrivacynTerms.fromJson(response.data);
|
||||
}
|
||||
}
|
||||
152
lib/profile/view/faq/faq_view.dart
Normal file
152
lib/profile/view/faq/faq_view.dart
Normal file
@@ -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<FAQnPrivacynTermsBloc, FAQnPrivacynTermsState>(
|
||||
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<FAQnPrivacynTermsBloc>().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<FaqItem> 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
81
lib/profile/view/privacy/privacy_view.dart
Normal file
81
lib/profile/view/privacy/privacy_view.dart
Normal file
@@ -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<FAQnPrivacynTermsBloc, FAQnPrivacynTermsState>(
|
||||
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<FAQnPrivacynTermsBloc>().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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<FAQnPrivacynTermsBloc, FAQnPrivacynTermsState>(
|
||||
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<FAQnPrivacynTermsBloc>().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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
40
lib/splash_screen/bloc/app_start_bloc.dart
Normal file
40
lib/splash_screen/bloc/app_start_bloc.dart
Normal file
@@ -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<AppStartEvent, AppStartState> {
|
||||
AppStartBloc() : super(AppStartInitial()) {
|
||||
on<CheckAppStartStatus>(_onCheckAppStartStatus);
|
||||
}
|
||||
|
||||
Future<void> _onCheckAppStartStatus(
|
||||
CheckAppStartStatus event,
|
||||
Emitter<AppStartState> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
3
lib/splash_screen/bloc/app_start_event.dart
Normal file
3
lib/splash_screen/bloc/app_start_event.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
abstract class AppStartEvent {}
|
||||
|
||||
class CheckAppStartStatus extends AppStartEvent {}
|
||||
11
lib/splash_screen/bloc/app_start_state.dart
Normal file
11
lib/splash_screen/bloc/app_start_state.dart
Normal file
@@ -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 {}
|
||||
@@ -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<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
@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<AppStartBloc, AppStartState>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user