feat: redirect agents to profile on login, update UI layouts, and add analytics dependencies.
This commit is contained in:
@@ -1336,8 +1336,10 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
||||
color: const Color(0xFFE6E6E6),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text('Specialization',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 20,
|
||||
@@ -1345,6 +1347,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
||||
color: AppColors.primaryDark)),
|
||||
const SizedBox(height: 4),
|
||||
const Text('Area Of Expertise and Focus',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
@@ -1365,8 +1368,7 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
||||
final hasMore = card.values.length > 3;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 66, vertical: 8),
|
||||
width: 298,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@@ -1375,11 +1377,13 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> {
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.category_outlined,
|
||||
size: 28, color: AppColors.accentOrange),
|
||||
const SizedBox(height: 10),
|
||||
Text(displayName,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
|
||||
@@ -170,7 +170,7 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search Agents',
|
||||
hintText: 'Search Professionals',
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
|
||||
@@ -65,7 +65,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
||||
// Navigate on auth state changes
|
||||
ref.listen<AuthState>(authProvider, (previous, next) {
|
||||
if (next.status == AuthStatus.authenticated) {
|
||||
context.go('/home');
|
||||
// Agents land on /profile (defaults to "Edit Profile" tab —
|
||||
// their dashboard/home view). Users land on /home.
|
||||
final isAgent = next.user?.role == 'AGENT';
|
||||
context.go(isAgent ? '/profile' : '/home');
|
||||
}
|
||||
// Navigate to 2FA verification screen
|
||||
if (next.requiresTwoFactor && next.tempToken != null) {
|
||||
|
||||
@@ -95,10 +95,12 @@ class _Verify2faScreenState extends ConsumerState<Verify2faScreen> {
|
||||
final authState = ref.watch(authProvider);
|
||||
final isLoading = authState.status == AuthStatus.loading;
|
||||
|
||||
// Navigate to home when authentication succeeds, reset submit lock on error
|
||||
// Navigate when authentication succeeds, reset submit lock on error.
|
||||
// Agents land on /profile (Edit Profile tab — their home/dashboard view).
|
||||
ref.listen<AuthState>(authProvider, (previous, next) {
|
||||
if (next.status == AuthStatus.authenticated) {
|
||||
context.go('/home');
|
||||
final isAgent = next.user?.role == 'AGENT';
|
||||
context.go(isAgent ? '/profile' : '/home');
|
||||
}
|
||||
if (next.status != AuthStatus.loading) {
|
||||
_isSubmitting = false;
|
||||
|
||||
@@ -479,25 +479,33 @@ class _FeaturesSectionState extends ConsumerState<FeaturesSection> {
|
||||
const SizedBox(height: 8),
|
||||
// Location
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/location_filled_icon.svg',
|
||||
width: 15,
|
||||
height: 15,
|
||||
placeholderBuilder: (_) => const Icon(
|
||||
Icons.location_on,
|
||||
color: AppColors.accentOrange,
|
||||
size: 15,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/location_filled_icon.svg',
|
||||
width: 15,
|
||||
height: 15,
|
||||
placeholderBuilder: (_) => const Icon(
|
||||
Icons.location_on,
|
||||
color: AppColors.accentOrange,
|
||||
size: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
agent.location,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryDark,
|
||||
Expanded(
|
||||
child: Text(
|
||||
agent.location,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -21,18 +21,18 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
|
||||
iconAsset: 'assets/images/onboarding_1_icon.svg',
|
||||
iconWidth: 110,
|
||||
iconHeight: 100,
|
||||
title: 'Connect with Top\nProfessionals',
|
||||
title: 'Connect with Top Professionals',
|
||||
subtitle:
|
||||
'Professionals can list properties and find qualified\nbuyers faster than ever.',
|
||||
'Professionals can list properties and find qualified buyers faster than ever.',
|
||||
),
|
||||
_OnboardingData(
|
||||
bgAsset: 'assets/images/onboarding_2_bg.svg',
|
||||
iconAsset: 'assets/images/onboarding_2_icon.svg',
|
||||
iconWidth: 83,
|
||||
iconHeight: 75,
|
||||
title: 'Real help. Real professionals.\nInstantly.',
|
||||
title: 'Real help. Real professionals. Instantly.',
|
||||
subtitle:
|
||||
'Chat with professionals and get answers about\nproperties instantly.',
|
||||
'Chat with professionals and get answers about properties instantly.',
|
||||
),
|
||||
];
|
||||
|
||||
@@ -208,150 +208,151 @@ class _OnboardingPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Illustration section - takes upper portion
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 0, top: 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width - 24,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background oval/pill shape with orange gradient
|
||||
SvgPicture.asset(
|
||||
// Scale the illustration to ~38% of screen height so it shrinks on small
|
||||
// devices, leaving room for the title + subtitle without overflow.
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final illustrationHeight = (screenHeight * 0.38).clamp(220.0, 346.0);
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
// Illustration section
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width - 24,
|
||||
height: illustrationHeight,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Background oval/pill shape with orange gradient
|
||||
Positioned.fill(
|
||||
child: SvgPicture.asset(
|
||||
data.bgAsset,
|
||||
width: 382,
|
||||
height: 346,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
// Blurred decoration - bottom right
|
||||
Positioned(
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
child: Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.accentOrange.withValues(alpha: 0.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.accentOrange
|
||||
.withValues(alpha: 0.15),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Blurred decoration - bottom right
|
||||
Positioned(
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.accentOrange.withValues(alpha: 0.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.accentOrange
|
||||
.withValues(alpha: 0.15),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Blurred decoration - top left
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.accentOrange.withValues(alpha: 0.1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.accentOrange
|
||||
.withValues(alpha: 0.08),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Blurred decoration - top left
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.accentOrange.withValues(alpha: 0.1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.accentOrange
|
||||
.withValues(alpha: 0.08),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Main icon + small icons
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
data.iconAsset,
|
||||
width: data.iconWidth,
|
||||
height: data.iconHeight,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Small property icons row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/images/onboarding_small_icon_1.svg',
|
||||
width: 27,
|
||||
height: 27,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SvgPicture.asset(
|
||||
'assets/images/onboarding_small_icon_2.svg',
|
||||
width: 31.5,
|
||||
height: 31.5,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SvgPicture.asset(
|
||||
'assets/images/onboarding_small_icon_3.svg',
|
||||
width: 24,
|
||||
height: 27,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Main icon + small icons
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
data.iconAsset,
|
||||
width: data.iconWidth,
|
||||
height: data.iconHeight,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Small property icons row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/images/onboarding_small_icon_1.svg',
|
||||
width: 27,
|
||||
height: 27,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SvgPicture.asset(
|
||||
'assets/images/onboarding_small_icon_2.svg',
|
||||
width: 31.5,
|
||||
height: 31.5,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SvgPicture.asset(
|
||||
'assets/images/onboarding_small_icon_3.svg',
|
||||
width: 24,
|
||||
height: 27,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Text content section
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
// Text content section
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
// Title
|
||||
Text(
|
||||
data.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 32,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF0F172A),
|
||||
height: 1.25,
|
||||
letterSpacing: -0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
// Subtitle
|
||||
Text(
|
||||
data.subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 16,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF475569),
|
||||
height: 1.625,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,47 +467,54 @@ class _ProfileSettingsTabState extends ConsumerState<ProfileSettingsTab> {
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Change Email Address'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'A verification link will be sent to your new email. Your email changes only after you click the link.',
|
||||
style: TextStyle(fontSize: 13),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'A verification link will be sent to your new email. Your email changes only after you click the link.',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: newEmailCtl,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
textCapitalization: TextCapitalization.none,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'New Email',
|
||||
hintText: 'new@example.com',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: newEmailCtl,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
textCapitalization: TextCapitalization.none,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'New Email',
|
||||
hintText: 'new@example.com',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: passwordCtl,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
// Empty autofillHints prevents iOS from showing the
|
||||
// "Strong Password" suggestion bar, which was adding
|
||||
// extra bottom inset and shifting the dialog upward.
|
||||
autofillHints: const <String>[],
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Current Password',
|
||||
),
|
||||
),
|
||||
if (localError != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: passwordCtl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Current Password',
|
||||
Text(
|
||||
localError!,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
if (localError != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
localError!,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
class SplashScreen extends ConsumerStatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
ConsumerState<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
class _SplashScreenState extends ConsumerState<SplashScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _fadeAnimation;
|
||||
@@ -43,11 +45,15 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
final onboardingCompleted = prefs.getBool('onboarding_completed') ?? false;
|
||||
|
||||
if (!mounted) return;
|
||||
if (onboardingCompleted) {
|
||||
context.go('/home');
|
||||
} else {
|
||||
if (!onboardingCompleted) {
|
||||
context.go('/onboarding');
|
||||
return;
|
||||
}
|
||||
// Agents land on /profile (Edit Profile tab); everyone else on /home.
|
||||
final authState = ref.read(authProvider);
|
||||
final isAgent = authState.status == AuthStatus.authenticated &&
|
||||
authState.user?.role == 'AGENT';
|
||||
context.go(isAgent ? '/profile' : '/home');
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:real_estate_mobile/features/agents/presentation/screens/agent_ed
|
||||
import 'package:real_estate_mobile/features/profile/presentation/screens/payment_success_screen.dart';
|
||||
import 'package:real_estate_mobile/features/splash/presentation/screens/splash_screen.dart';
|
||||
import 'package:real_estate_mobile/features/onboarding/presentation/screens/onboarding_screen.dart';
|
||||
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_home_screen.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/screens/verify_2fa_screen.dart';
|
||||
import 'package:real_estate_mobile/features/support_chat/presentation/screens/support_chat_screen.dart';
|
||||
import 'package:real_estate_mobile/features/coming_soon/presentation/screens/coming_soon_screen.dart';
|
||||
@@ -288,8 +289,8 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
);
|
||||
});
|
||||
|
||||
/// Always shows the user HomeScreen for /home. Agents reach their own
|
||||
/// dashboard via the Edit Profile tab in the Profile section.
|
||||
/// Reactively switches between HomeScreen and AgentHomeScreen based on auth.
|
||||
/// Agents see their dashboard (profile view); users see the discovery home.
|
||||
class _HomeRouteWrapper extends ConsumerWidget {
|
||||
const _HomeRouteWrapper();
|
||||
|
||||
@@ -306,8 +307,10 @@ class _HomeRouteWrapper extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Home icon always shows the User Home Page regardless of role.
|
||||
// Agents access their own dashboard via the Edit Profile tab in /profile.
|
||||
if (authState.status == AuthStatus.authenticated &&
|
||||
authState.user?.role == 'AGENT') {
|
||||
return const AgentHomeScreen();
|
||||
}
|
||||
return const HomeScreen();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user