381 lines
15 KiB
Dart
381 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:real_estate_mobile/core/analytics/firebase_analytics_config.dart';
|
|
import 'package:real_estate_mobile/core/analytics/umami_analytics.dart';
|
|
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
|
import 'package:real_estate_mobile/core/widgets/app_shell.dart';
|
|
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
|
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_detail_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';
|
|
import 'package:real_estate_mobile/features/messaging/presentation/screens/conversations_screen.dart';
|
|
import 'package:real_estate_mobile/features/messaging/presentation/screens/chat_screen.dart';
|
|
import 'package:real_estate_mobile/features/contact/presentation/screens/contact_screen.dart';
|
|
import 'package:real_estate_mobile/features/about/presentation/screens/about_screen.dart';
|
|
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_network_screen.dart';
|
|
import 'package:real_estate_mobile/features/profile/presentation/screens/profile_settings_screen.dart';
|
|
import 'package:real_estate_mobile/features/notifications/presentation/screens/notifications_screen.dart';
|
|
import 'package:real_estate_mobile/features/agents/presentation/screens/agent_edit_profile_screen.dart';
|
|
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/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';
|
|
|
|
final routerProvider = Provider<GoRouter>((ref) {
|
|
final authRefreshNotifier = _AuthRefreshNotifier();
|
|
|
|
ref.listen(authProvider, (previous, next) {
|
|
authRefreshNotifier.notify();
|
|
});
|
|
|
|
ref.onDispose(() {
|
|
authRefreshNotifier.dispose();
|
|
});
|
|
|
|
// Public routes accessible without authentication (matching web middleware)
|
|
const publicRoutes = ['/home', '/agents/search', '/agents/detail', '/faq', '/contact', '/about'];
|
|
const authRoutes = ['/login', '/signup', '/verify-2fa'];
|
|
|
|
return GoRouter(
|
|
initialLocation: '/splash',
|
|
refreshListenable: authRefreshNotifier,
|
|
observers: [
|
|
UmamiAnalytics.navigationObserver,
|
|
FirebaseAnalyticsConfig.navigationObserver,
|
|
],
|
|
redirect: (context, state) {
|
|
final authState = ref.read(authProvider);
|
|
final isAuthenticated = authState.status == AuthStatus.authenticated;
|
|
final isLoading = authState.status == AuthStatus.loading;
|
|
|
|
final location = state.matchedLocation;
|
|
final isAuthRoute = authRoutes.contains(location);
|
|
final isPublicRoute = publicRoutes.any(
|
|
(route) => location == route || location.startsWith('$route/'),
|
|
);
|
|
|
|
// Never redirect away from splash or onboarding
|
|
if (location == '/splash' || location == '/onboarding') return null;
|
|
|
|
// While checking auth status, don't redirect
|
|
if (isLoading) return null;
|
|
|
|
// Public routes and auth routes are always accessible (no auth required)
|
|
if (isPublicRoute || isAuthRoute) {
|
|
// Only redirect: if authenticated user is on login/signup, send to home
|
|
if (isAuthenticated && isAuthRoute) return '/home';
|
|
return null;
|
|
}
|
|
|
|
// Protected routes require authentication
|
|
if (!isAuthenticated) return '/login';
|
|
|
|
return null;
|
|
},
|
|
routes: [
|
|
// Splash screen (full screen, no shell)
|
|
GoRoute(
|
|
path: '/splash',
|
|
builder: (context, state) => const SplashScreen(),
|
|
),
|
|
|
|
// Onboarding (shown only once on first install)
|
|
GoRoute(
|
|
path: '/onboarding',
|
|
builder: (context, state) => const OnboardingScreen(),
|
|
),
|
|
|
|
// Auth routes (no shell — full screen)
|
|
GoRoute(
|
|
path: '/login',
|
|
builder: (context, state) => const LoginScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/signup',
|
|
builder: (context, state) => const SignupScreen(),
|
|
),
|
|
GoRoute(
|
|
path: '/verify-2fa',
|
|
builder: (context, state) => const Verify2faScreen(),
|
|
),
|
|
|
|
// All main app routes wrapped in AppShell (shared header + bottom nav)
|
|
ShellRoute(
|
|
builder: (context, state, child) => AppShell(child: child),
|
|
routes: [
|
|
GoRoute(
|
|
path: '/home',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const _HomeRouteWrapper(),
|
|
transitionDuration: const Duration(milliseconds: 200),
|
|
reverseTransitionDuration: const Duration(milliseconds: 150),
|
|
transitionsBuilder: _fadeTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/agents/search',
|
|
pageBuilder: (context, state) {
|
|
final query = state.uri.queryParameters['q'];
|
|
final typeId = state.uri.queryParameters['type'];
|
|
final location = state.uri.queryParameters['location'];
|
|
final category = state.uri.queryParameters['category'];
|
|
return CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: AgentSearchScreen(
|
|
initialQuery: query,
|
|
initialAgentTypeId: typeId,
|
|
initialLocation: location,
|
|
initialCategory: category,
|
|
),
|
|
transitionsBuilder: _fadeTransition,
|
|
);
|
|
},
|
|
),
|
|
// ── Tab routes (fade) ──
|
|
GoRoute(
|
|
path: '/messages',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const ConversationsScreen(),
|
|
transitionDuration: const Duration(milliseconds: 200),
|
|
reverseTransitionDuration: const Duration(milliseconds: 150),
|
|
transitionsBuilder: _fadeTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/agent/network',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const AgentNetworkScreen(),
|
|
transitionDuration: const Duration(milliseconds: 200),
|
|
reverseTransitionDuration: const Duration(milliseconds: 150),
|
|
transitionsBuilder: _fadeTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/profile',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const ProfileSettingsScreen(),
|
|
transitionDuration: const Duration(milliseconds: 200),
|
|
reverseTransitionDuration: const Duration(milliseconds: 150),
|
|
transitionsBuilder: _fadeTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/notifications',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const NotificationsScreen(),
|
|
transitionDuration: const Duration(milliseconds: 200),
|
|
reverseTransitionDuration: const Duration(milliseconds: 150),
|
|
transitionsBuilder: _fadeTransition,
|
|
),
|
|
),
|
|
|
|
// ── Detail/push routes (slide from right) ──
|
|
GoRoute(
|
|
path: '/agents/detail/:id',
|
|
pageBuilder: (context, state) {
|
|
final id = state.pathParameters['id']!;
|
|
return CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: AgentDetailScreen(agentId: id),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/messages/chat/:id',
|
|
pageBuilder: (context, state) {
|
|
final id = state.pathParameters['id']!;
|
|
return CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: ChatScreen(conversationId: id),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
);
|
|
},
|
|
),
|
|
GoRoute(
|
|
path: '/coming-soon',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: ComingSoonScreen(
|
|
title: state.uri.queryParameters['title'] ?? 'Coming Soon',
|
|
),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/faq',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const FaqScreen(),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/contact',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const ContactScreen(),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/about',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const AboutScreen(),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/agent/edit-profile',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const AgentEditProfileScreen(),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/support/chat',
|
|
pageBuilder: (context, state) => CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: const SupportChatScreen(),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: '/payment-success',
|
|
pageBuilder: (context, state) {
|
|
final sessionId = state.uri.queryParameters['session_id'];
|
|
return CustomTransitionPage(
|
|
key: state.pageKey,
|
|
child: PaymentSuccessScreen(sessionId: sessionId),
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
reverseTransitionDuration: const Duration(milliseconds: 200),
|
|
transitionsBuilder: _slideTransition,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
});
|
|
|
|
/// Always shows the user HomeScreen for /home regardless of role.
|
|
/// Agents reach their own dashboard via the Edit Profile tab in /profile.
|
|
class _HomeRouteWrapper extends ConsumerWidget {
|
|
const _HomeRouteWrapper();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final authState = ref.watch(authProvider);
|
|
|
|
// Show loader only while auth is actively loading (checking token)
|
|
if (authState.status == AuthStatus.loading) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(
|
|
color: AppColors.accentOrange,
|
|
),
|
|
);
|
|
}
|
|
|
|
return const HomeScreen();
|
|
}
|
|
}
|
|
|
|
/// Slide from right transition for push-style navigation.
|
|
/// The entering page slides in from the right while the exiting page
|
|
/// slides slightly left + fades, giving a native iOS-like depth effect.
|
|
Widget _slideTransition(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
) {
|
|
// Entering page: slide in from right
|
|
final slideIn = Tween<Offset>(
|
|
begin: const Offset(1, 0),
|
|
end: Offset.zero,
|
|
).animate(CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.easeOutCubic,
|
|
));
|
|
|
|
// Exiting page: slide slightly left + dim
|
|
final slideOut = Tween<Offset>(
|
|
begin: Offset.zero,
|
|
end: const Offset(-0.25, 0),
|
|
).animate(CurvedAnimation(
|
|
parent: secondaryAnimation,
|
|
curve: Curves.easeOutCubic,
|
|
));
|
|
|
|
final fadeOut = Tween<double>(begin: 1.0, end: 0.9).animate(
|
|
CurvedAnimation(parent: secondaryAnimation, curve: Curves.easeOut),
|
|
);
|
|
|
|
return SlideTransition(
|
|
position: slideOut,
|
|
child: FadeTransition(
|
|
opacity: fadeOut,
|
|
child: SlideTransition(
|
|
position: slideIn,
|
|
child: DecoratedBox(
|
|
decoration: const BoxDecoration(color: Colors.white),
|
|
child: child,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Smooth crossfade transition for tab-to-tab navigation.
|
|
Widget _fadeTransition(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
) {
|
|
return FadeTransition(
|
|
opacity: CurvedAnimation(parent: animation, curve: Curves.easeInOut),
|
|
child: DecoratedBox(
|
|
decoration: const BoxDecoration(color: Colors.white),
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Notifier that triggers GoRouter refresh when auth state changes.
|
|
class _AuthRefreshNotifier extends ChangeNotifier {
|
|
void notify() {
|
|
notifyListeners();
|
|
}
|
|
}
|