feat: Add FAQ screen and navigation, implement authentication-gated access for home screen features, and dynamically render header menu options based on authentication status.
This commit is contained in:
4
assets/icons/home_faq_icon.svg
Normal file
4
assets/icons/home_faq_icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9.5L11 3L19 9.5V18C19 18.5304 18.7893 19.0391 18.4142 19.4142C18.0391 19.7893 17.5304 20 17 20H5C4.46957 20 3.96086 19.7893 3.58579 19.4142C3.21071 19.0391 3 18.5304 3 18V9.5Z" stroke="#E58625" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 20V12H14V20" stroke="#E58625" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 491 B |
@@ -8,6 +8,7 @@ import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dar
|
||||
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
|
||||
import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart';
|
||||
import 'package:real_estate_mobile/features/agents/presentation/widgets/agent_filter_sheet.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart';
|
||||
|
||||
class AgentSearchScreen extends ConsumerStatefulWidget {
|
||||
@@ -390,14 +391,26 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
||||
svgPath: 'assets/icons/nav_messages_icon.svg',
|
||||
fallbackIcon: Icons.chat_bubble,
|
||||
isSelected: false,
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
final authState = ref.read(authProvider);
|
||||
if (authState.status != AuthStatus.authenticated) {
|
||||
context.push('/login');
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 3,
|
||||
svgPath: 'assets/icons/nav_profile_icon.svg',
|
||||
fallbackIcon: Icons.person_outline,
|
||||
isSelected: false,
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
final authState = ref.read(authProvider);
|
||||
if (authState.status != AuthStatus.authenticated) {
|
||||
context.push('/login');
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
545
lib/features/faq/presentation/screens/faq_screen.dart
Normal file
545
lib/features/faq/presentation/screens/faq_screen.dart
Normal file
@@ -0,0 +1,545 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
||||
import 'package:real_estate_mobile/core/network/api_client.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart';
|
||||
|
||||
// ── Data models ──
|
||||
|
||||
class FaqItem {
|
||||
final int id;
|
||||
final String icon;
|
||||
final String question;
|
||||
final String answer;
|
||||
final String category;
|
||||
|
||||
const FaqItem({
|
||||
required this.id,
|
||||
required this.icon,
|
||||
required this.question,
|
||||
required this.answer,
|
||||
required this.category,
|
||||
});
|
||||
|
||||
factory FaqItem.fromJson(Map<String, dynamic> json) {
|
||||
return FaqItem(
|
||||
id: json['id'] as int? ?? 0,
|
||||
icon: json['icon'] as String? ?? '',
|
||||
question: json['question'] as String? ?? '',
|
||||
answer: json['answer'] as String? ?? '',
|
||||
category: json['category'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Default data (same as web) ──
|
||||
|
||||
const _defaultFaqs = <FaqItem>[
|
||||
FaqItem(
|
||||
id: 1,
|
||||
icon: 'assets/icons/home_faq_icon.svg',
|
||||
question: 'How do I update my listing status?',
|
||||
answer:
|
||||
'To update your listing status, log in to your Agent Dashboard. Locate the property in your "Active Listings" tab, click the three-dot menu on the right, and select "Update Status." You can change it to Pending, Sold, or Temporarily Off Market. Changes are reflected immediately.',
|
||||
category: 'agent',
|
||||
),
|
||||
FaqItem(
|
||||
id: 2,
|
||||
icon: 'assets/icons/home_faq_icon.svg',
|
||||
question: 'What is the pre-approval process?',
|
||||
answer:
|
||||
'The pre-approval process involves submitting your financial documents to a lender who will review your credit history, income, assets, and debts. Once approved, you\'ll receive a pre-approval letter stating how much you can borrow, which strengthens your position when making offers on properties.',
|
||||
category: 'buying',
|
||||
),
|
||||
FaqItem(
|
||||
id: 3,
|
||||
icon: 'assets/icons/home_faq_icon.svg',
|
||||
question: 'How do I schedule an open house?',
|
||||
answer:
|
||||
'To schedule an open house, navigate to your Agent Dashboard and select the property you want to showcase. Click on "Schedule Open House" and choose your preferred date and time.',
|
||||
category: 'agent',
|
||||
),
|
||||
FaqItem(
|
||||
id: 4,
|
||||
icon: 'assets/icons/home_faq_icon.svg',
|
||||
question: 'How do I reset my password?',
|
||||
answer:
|
||||
'To reset your password, click on the "Login" button and then select "Forgot Password." Enter your registered email address and we\'ll send you a password reset link. Follow the link to create a new password.',
|
||||
category: 'platform',
|
||||
),
|
||||
FaqItem(
|
||||
id: 5,
|
||||
icon: 'assets/icons/home_faq_icon.svg',
|
||||
question: 'How do I contact a listing agent directly?',
|
||||
answer:
|
||||
'You can contact a listing agent by visiting the property page and clicking the "Contact Agent" button. You\'ll be able to send a message directly to the agent through our secure messaging system.',
|
||||
category: 'buying',
|
||||
),
|
||||
FaqItem(
|
||||
id: 6,
|
||||
icon: 'assets/icons/home_faq_icon.svg',
|
||||
question: 'What if I find inaccurate data on a listing?',
|
||||
answer:
|
||||
'If you find inaccurate information on a listing, please use the "Report Issue" button on the property page. Describe the inaccuracy and our team will review and correct it within 24-48 hours.',
|
||||
category: 'platform',
|
||||
),
|
||||
];
|
||||
|
||||
// ── Provider ──
|
||||
|
||||
final faqProvider =
|
||||
StateNotifierProvider.autoDispose<FaqNotifier, FaqState>((ref) {
|
||||
return FaqNotifier();
|
||||
});
|
||||
|
||||
class FaqState {
|
||||
final List<FaqItem> faqs;
|
||||
final bool isLoading;
|
||||
final int? expandedId;
|
||||
|
||||
const FaqState({
|
||||
this.faqs = const [],
|
||||
this.isLoading = true,
|
||||
this.expandedId,
|
||||
});
|
||||
|
||||
FaqState copyWith({
|
||||
List<FaqItem>? faqs,
|
||||
bool? isLoading,
|
||||
int? expandedId,
|
||||
bool clearExpanded = false,
|
||||
}) {
|
||||
return FaqState(
|
||||
faqs: faqs ?? this.faqs,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
expandedId: clearExpanded ? null : (expandedId ?? this.expandedId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FaqNotifier extends StateNotifier<FaqState> {
|
||||
FaqNotifier() : super(const FaqState()) {
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final response = await ApiClient.instance.dio.get('/cms/page/faq');
|
||||
final data = response.data['data'] as List<dynamic>?;
|
||||
if (data != null) {
|
||||
for (final record in data) {
|
||||
final map = record as Map<String, dynamic>;
|
||||
if (map['sectionKey'] == 'faqContent') {
|
||||
final content = map['content'] as Map<String, dynamic>?;
|
||||
if (content != null) {
|
||||
final faqsList = content['faqs'] as List<dynamic>?;
|
||||
if (faqsList != null && faqsList.isNotEmpty) {
|
||||
final faqs = faqsList
|
||||
.map((e) => FaqItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = state.copyWith(
|
||||
faqs: faqs,
|
||||
isLoading: false,
|
||||
expandedId: faqs.first.id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall through to defaults
|
||||
}
|
||||
|
||||
// Use defaults
|
||||
state = state.copyWith(
|
||||
faqs: _defaultFaqs,
|
||||
isLoading: false,
|
||||
expandedId: _defaultFaqs.first.id,
|
||||
);
|
||||
}
|
||||
|
||||
void toggleFaq(int id) {
|
||||
if (state.expandedId == id) {
|
||||
state = state.copyWith(clearExpanded: true);
|
||||
} else {
|
||||
state = state.copyWith(expandedId: id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Screen ──
|
||||
|
||||
class FaqScreen extends ConsumerWidget {
|
||||
const FaqScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(faqProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Column(
|
||||
children: [
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: const HomeHeader(),
|
||||
),
|
||||
Expanded(
|
||||
child: state.isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.accentOrange),
|
||||
)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(0, 20, 0, 30),
|
||||
children: [
|
||||
// Title
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
'Frequently Asked Questions ?',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// FAQ items
|
||||
...state.faqs.map((faq) => _FaqAccordion(
|
||||
faq: faq,
|
||||
isExpanded: state.expandedId == faq.id,
|
||||
onTap: () =>
|
||||
ref.read(faqProvider.notifier).toggleFaq(faq.id),
|
||||
)),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Still Need Help section
|
||||
const _StillNeedHelpSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNavBar(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNavBar(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
top: BorderSide(color: Color(0xFFE8E8E8), width: 1),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
svgPath: 'assets/icons/nav_home_icon.svg',
|
||||
fallbackIcon: Icons.home,
|
||||
isSelected: false,
|
||||
onTap: () => context.go('/home'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
svgPath: 'assets/icons/nav_list_icon.svg',
|
||||
fallbackIcon: Icons.list,
|
||||
isSelected: false,
|
||||
onTap: () => context.push('/agents/search'),
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
svgPath: 'assets/icons/nav_messages_icon.svg',
|
||||
fallbackIcon: Icons.chat_bubble,
|
||||
isSelected: false,
|
||||
onTap: () {
|
||||
final authState = ref.read(authProvider);
|
||||
if (authState.status != AuthStatus.authenticated) {
|
||||
context.push('/login');
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildNavItem(
|
||||
context: context,
|
||||
svgPath: 'assets/icons/nav_profile_icon.svg',
|
||||
fallbackIcon: Icons.person_outline,
|
||||
isSelected: false,
|
||||
onTap: () {
|
||||
final authState = ref.read(authProvider);
|
||||
if (authState.status != AuthStatus.authenticated) {
|
||||
context.push('/login');
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem({
|
||||
required BuildContext context,
|
||||
required String svgPath,
|
||||
required IconData fallbackIcon,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: SvgPicture.asset(
|
||||
svgPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: isSelected
|
||||
? const ColorFilter.mode(AppColors.primaryDark, BlendMode.srcIn)
|
||||
: const ColorFilter.mode(
|
||||
AppColors.accentOrange, BlendMode.srcIn),
|
||||
placeholderBuilder: (_) => Icon(
|
||||
fallbackIcon,
|
||||
size: 24,
|
||||
color: isSelected ? AppColors.primaryDark : AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Accordion item ──
|
||||
|
||||
class _FaqAccordion extends StatelessWidget {
|
||||
final FaqItem faq;
|
||||
final bool isExpanded;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _FaqAccordion({
|
||||
required this.faq,
|
||||
required this.isExpanded,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Question row
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon
|
||||
SvgPicture.asset(
|
||||
'assets/icons/home_faq_icon.svg',
|
||||
width: 22,
|
||||
height: 22,
|
||||
placeholderBuilder: (_) => const Icon(
|
||||
Icons.home_outlined,
|
||||
color: AppColors.accentOrange,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// Question text
|
||||
Expanded(
|
||||
child: Text(
|
||||
faq.question,
|
||||
style: TextStyle(
|
||||
fontFamily: isExpanded ? 'Fractul' : 'SourceSerif4',
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
isExpanded ? FontWeight.w500 : FontWeight.w600,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Chevron
|
||||
Icon(
|
||||
isExpanded
|
||||
? Icons.keyboard_arrow_down
|
||||
: Icons.keyboard_arrow_right,
|
||||
color: AppColors.primaryDark,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Answer (expanded)
|
||||
if (isExpanded) ...[
|
||||
// Divider
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(56, 14, 20, 18),
|
||||
child: Text(
|
||||
faq.answer,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Still Need Help section ──
|
||||
|
||||
class _StillNeedHelpSection extends StatelessWidget {
|
||||
const _StillNeedHelpSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 20, 24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Left side: text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Still Need Help ?',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Our Support Team Is Available\nMon-Fri, 9am-6pm IST',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Right side: buttons
|
||||
Column(
|
||||
children: [
|
||||
// Start Live Chat
|
||||
_buildSupportButton(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
label: 'Start Live Chat',
|
||||
onTap: () {},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Email Support
|
||||
_buildSupportButton(
|
||||
icon: Icons.email_outlined,
|
||||
label: 'Email Support',
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSupportButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 162,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: AppColors.primaryDark),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/featured_professionals_section.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/features_section.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/hero_section.dart';
|
||||
@@ -10,14 +12,14 @@ import 'package:real_estate_mobile/features/home/presentation/widgets/testimonia
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/top_professionals_section.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/widgets/trust_stats_section.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
class HomeScreen extends ConsumerStatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
@override
|
||||
@@ -148,6 +150,13 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
context.push('/agents/search');
|
||||
return;
|
||||
}
|
||||
if (index == 2 || index == 3) {
|
||||
final authState = ref.read(authProvider);
|
||||
if (authState.status != AuthStatus.authenticated) {
|
||||
context.push('/login');
|
||||
return;
|
||||
}
|
||||
}
|
||||
setState(() => _selectedIndex = index);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
|
||||
@@ -154,7 +154,10 @@ class _MenuDrawer extends ConsumerWidget {
|
||||
context,
|
||||
icon: Icons.help_outline,
|
||||
label: "FAQ's",
|
||||
onTap: () => Navigator.pop(context),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push('/faq');
|
||||
},
|
||||
),
|
||||
_buildMenuItem(
|
||||
context,
|
||||
@@ -165,6 +168,8 @@ class _MenuDrawer extends ConsumerWidget {
|
||||
const SizedBox(height: 16),
|
||||
const Divider(color: AppColors.divider, height: 1),
|
||||
const SizedBox(height: 16),
|
||||
if (ref.watch(authProvider).status ==
|
||||
AuthStatus.authenticated) ...[
|
||||
_buildMenuItem(
|
||||
context,
|
||||
icon: Icons.person_outline,
|
||||
@@ -203,7 +208,8 @@ class _MenuDrawer extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 16),
|
||||
side: const BorderSide(
|
||||
color: AppColors.accentOrange,
|
||||
width: 1.5,
|
||||
@@ -214,6 +220,76 @@ class _MenuDrawer extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Login button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.push('/login');
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.login,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
'Log In',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Sign Up button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.push('/signup');
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.person_add_outlined,
|
||||
color: AppColors.accentOrange,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 16),
|
||||
side: const BorderSide(
|
||||
color: AppColors.accentOrange,
|
||||
width: 1.5,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:real_estate_mobile/features/auth/presentation/providers/auth_pro
|
||||
import 'package:real_estate_mobile/features/auth/presentation/screens/login_screen.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/screens/signup_screen.dart';
|
||||
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_search_screen.dart';
|
||||
import 'package:real_estate_mobile/features/faq/presentation/screens/faq_screen.dart';
|
||||
import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart';
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
@@ -18,25 +19,35 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
authRefreshNotifier.dispose();
|
||||
});
|
||||
|
||||
// Public routes accessible without authentication (matching web middleware)
|
||||
const publicRoutes = ['/home', '/agents/search', '/faq'];
|
||||
const authRoutes = ['/login', '/signup'];
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: '/login',
|
||||
initialLocation: '/home',
|
||||
refreshListenable: authRefreshNotifier,
|
||||
redirect: (context, state) {
|
||||
final authState = ref.read(authProvider);
|
||||
final isAuthenticated = authState.status == AuthStatus.authenticated;
|
||||
final isLoading = authState.status == AuthStatus.loading;
|
||||
|
||||
final isAuthRoute = state.matchedLocation == '/login' ||
|
||||
state.matchedLocation == '/signup';
|
||||
final location = state.matchedLocation;
|
||||
final isAuthRoute = authRoutes.contains(location);
|
||||
final isPublicRoute = publicRoutes.any(
|
||||
(route) => location == route || location.startsWith('$route/'),
|
||||
);
|
||||
|
||||
// While checking auth status, don't redirect
|
||||
if (isLoading) return null;
|
||||
|
||||
// If authenticated and on auth page, go to home
|
||||
// If authenticated and on auth page (login/signup), go to home
|
||||
if (isAuthenticated && isAuthRoute) return '/home';
|
||||
|
||||
// If not authenticated and on protected page, go to login
|
||||
if (!isAuthenticated && !isAuthRoute) return '/login';
|
||||
// Public routes are always accessible
|
||||
if (isPublicRoute || isAuthRoute) return null;
|
||||
|
||||
// Protected routes require authentication — redirect to login
|
||||
if (!isAuthenticated) return '/login';
|
||||
|
||||
return null;
|
||||
},
|
||||
@@ -60,6 +71,10 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return AgentSearchScreen(initialQuery: query);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/faq',
|
||||
builder: (context, state) => const FaqScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user