api integrtaion send Otp ,verify otp and faq , Terms ,Policy

This commit is contained in:
mystery012728
2026-01-23 19:00:55 +05:30
parent bbb96512d1
commit f5782f6da1
51 changed files with 2088 additions and 898 deletions

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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),
],
),
),
),
);
}
}

View File

@@ -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),
],
),
),
);
}
}

View File

@@ -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,
),
),
),
),
),

View File

@@ -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
);
},
);

View File

@@ -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
);
},
);

View 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());
}
}

View 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();
}

View 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];
}

View File

@@ -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")
],
),
),
),
);
}
}

View 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,
};
}
}

View 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');
}
}
}

View 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),
],
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -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(),
),
],
),
);
}

View File

@@ -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());
});
}
}

View File

@@ -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');
}
}

View File

@@ -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: [

View File

@@ -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(),
),
);
},
);
}
}
}

View File

@@ -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

View File

@@ -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,
),
),
),
],
),
),

View File

@@ -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),

View File

@@ -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
)
''');
},
);
}

View File

@@ -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);
}
}

View 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()));
}
}
}

View File

@@ -0,0 +1,7 @@
abstract class LoginEvent {}
class SendEmailOtpEvent extends LoginEvent {
final String emailAddress;
SendEmailOtpEvent({required this.emailAddress});
}

View 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});
}

View 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()));
}
}
}

View 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});
}

View 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});
}

View 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');
}
}
}

View 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),
],
),
),
),
),
);
}
}

View 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),
],
),
),
),
);
}
}

View File

@@ -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,

View File

@@ -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";
}

View File

@@ -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),
),
],
),
),
),
),
);
}
}

View File

@@ -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()));
}
}
}

View File

@@ -0,0 +1,3 @@
abstract class FAQnPrivacynTermsEvent {}
class FetchFAQnPrivacynTermsEvent extends FAQnPrivacynTermsEvent {}

View File

@@ -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);
}

View 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,
};
}
}

View 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);
}
}

View 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(),
),
],
),
);
}

View 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),
),
],
),
),
);
},
),
),
),
);
}
}

View 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 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),
),
],
),
),
);
},
),
),
),
);
}
}

View 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());
}
}
}

View File

@@ -0,0 +1,3 @@
abstract class AppStartEvent {}
class CheckAppStartStatus extends AppStartEvent {}

View 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 {}

View File

@@ -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,
),
),
),
),
);
}
}
}

View File

@@ -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),
),
],
),
),
),
),
);
}
}