Files
mobile-app/lib/routing/app_router.dart

268 lines
9.8 KiB
Dart
Raw Normal View History

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/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/agents/presentation/screens/agent_home_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';
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'];
return GoRouter(
initialLocation: '/splash',
refreshListenable: authRefreshNotifier,
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 — they handle their own navigation
if (location == '/splash' || location == '/onboarding') return null;
// While checking auth status, don't redirect
if (isLoading) return null;
// If authenticated and on auth page (login/signup), go to home
if (isAuthenticated && isAuthRoute) return '/home';
// Public routes are always accessible
if (isPublicRoute || isAuthRoute) return null;
// Protected routes require authentication — redirect to login
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(),
),
// 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(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/agents/search',
pageBuilder: (context, state) {
final query = state.uri.queryParameters['q'];
return CustomTransitionPage(
key: state.pageKey,
child: AgentSearchScreen(initialQuery: query),
transitionsBuilder: _fadeTransition,
);
},
),
GoRoute(
path: '/agents/detail/:id',
pageBuilder: (context, state) {
final id = state.pathParameters['id']!;
return CustomTransitionPage(
key: state.pageKey,
child: AgentDetailScreen(agentId: id),
transitionsBuilder: _fadeTransition,
);
},
),
GoRoute(
path: '/messages',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const ConversationsScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/messages/chat/:id',
pageBuilder: (context, state) {
final id = state.pathParameters['id']!;
return CustomTransitionPage(
key: state.pageKey,
child: ChatScreen(conversationId: id),
transitionsBuilder: _fadeTransition,
);
},
),
GoRoute(
path: '/faq',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const FaqScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/contact',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const ContactScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/about',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const AboutScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/agent/network',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const AgentNetworkScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/profile',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const ProfileSettingsScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/agent/edit-profile',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const AgentEditProfileScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/notifications',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const NotificationsScreen(),
transitionsBuilder: _fadeTransition,
),
),
GoRoute(
path: '/payment-success',
pageBuilder: (context, state) {
final sessionId = state.uri.queryParameters['session_id'];
return CustomTransitionPage(
key: state.pageKey,
child: PaymentSuccessScreen(sessionId: sessionId),
transitionsBuilder: _fadeTransition,
);
},
),
],
),
],
);
});
/// Reactively switches between HomeScreen and AgentHomeScreen based on auth.
/// Shows a loading indicator while auth status is being determined to prevent
/// flashing the wrong screen on refresh.
class _HomeRouteWrapper extends ConsumerWidget {
const _HomeRouteWrapper();
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
// Show loader while auth is initializing or loading
if (authState.status == AuthStatus.initial ||
authState.status == AuthStatus.loading) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.accentOrange,
),
);
}
if (authState.status == AuthStatus.authenticated &&
authState.user?.role == 'AGENT') {
return const AgentHomeScreen();
}
return const HomeScreen();
}
}
/// Smooth fade transition for tab-to-tab navigation.
Widget _fadeTransition(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(
opacity: CurveTween(curve: Curves.easeInOut).animate(animation),
child: child,
);
}
/// Notifier that triggers GoRouter refresh when auth state changes.
class _AuthRefreshNotifier extends ChangeNotifier {
void notify() {
notifyListeners();
}
}