From 1410e32005ef74a665199097b1b287318cc0c3eb Mon Sep 17 00:00:00 2001 From: Dani B Date: Wed, 28 Jan 2026 11:05:02 -0500 Subject: [PATCH] feat(01-11): implement auth-aware router and splash screen - Created AppRouter with GoRouter for declarative navigation - Added protected routes that redirect to login if not authenticated - Implemented splash screen with loading state and auth checking - Set up route definitions for login, signup, home, and splash - Added error handling for navigation failures - Includes authentication state-based redirects Files modified: - lib/core/router/app_router.dart - lib/features/authentication/presentation/pages/splash_page.dart --- lib/core/router/app_router.dart | 159 ++++++++++++++++++ .../presentation/pages/splash_page.dart | 140 +++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 lib/core/router/app_router.dart create mode 100644 lib/features/authentication/presentation/pages/splash_page.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart new file mode 100644 index 0000000..e31d943 --- /dev/null +++ b/lib/core/router/app_router.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import '../../providers/auth_provider.dart'; +import '../../features/authentication/presentation/pages/login_page.dart'; +import '../../features/authentication/presentation/pages/signup_page.dart'; +import '../../features/home/presentation/pages/home_page.dart'; +import '../../features/authentication/presentation/pages/splash_page.dart'; + +/// Application router configuration +/// +/// Handles navigation with authentication state awareness and protected routes +class AppRouter { + static final GoRouter _router = GoRouter( + initialLocation: '/', + debugLogDiagnostics: true, + redirect: (context, state) { + final authState = state.extra as AuthState?; + + // If no auth state in extra, check from provider + if (authState == null) { + final container = ProviderScope.containerOf(context, listen: false); + final authNotifier = container.read(authStateProvider.notifier); + + // For now, we'll use Supabase directly for auth state checking + // This will be improved when auth provider is fully integrated + final currentUser = Supabase.instance.client.auth.currentUser; + + // Allow splash page regardless of auth state + if (state.location == '/splash') { + return null; + } + + // If not authenticated and trying to access protected route, redirect to login + if (currentUser == null && !state.location.startsWith('/login') && !state.location.startsWith('/signup')) { + return '/login'; + } + + // If authenticated and on auth pages, redirect to home + if (currentUser != null && (state.location.startsWith('/login') || state.location.startsWith('/signup'))) { + return '/home'; + } + + return null; + } + + // TODO: Update to use AuthState when fully integrated + // For now, use Supabase auth state directly + final currentUser = Supabase.instance.client.auth.currentUser; + + if (currentUser == null && !state.location.startsWith('/login') && !state.location.startsWith('/signup')) { + return '/login'; + } + + if (currentUser != null && (state.location.startsWith('/login') || state.location.startsWith('/signup'))) { + return '/home'; + } + + return null; + }, + routes: [ + // Splash route - initial loading screen + GoRoute( + path: '/splash', + name: 'splash', + builder: (context, state) => const SplashPage(), + ), + + // Authentication routes (public) + GoRoute( + path: '/login', + name: 'login', + builder: (context, state) => const LoginPage(), + ), + + GoRoute( + path: '/signup', + name: 'signup', + builder: (context, state) => const SignupPage(), + ), + + // Protected routes (require authentication) + GoRoute( + path: '/home', + name: 'home', + builder: (context, state) => const HomePage(), + ), + + // Root route - redirects based on auth state + GoRoute( + path: '/', + redirect: (context, state) { + final currentUser = Supabase.instance.client.auth.currentUser; + return currentUser != null ? '/home' : '/splash'; + }, + ), + + // Additional routes will be added here + // Example: + // GoRoute( + // path: '/inventory', + // name: 'inventory', + // builder: (context, state) => const InventoryPage(), + // ), + ], + errorBuilder: (context, state) => Scaffold( + appBar: AppBar( + title: const Text('Error'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + 'Page not found', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Could not find: ${state.location}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + // Navigate to home or login based on auth state + final currentUser = Supabase.instance.client.auth.currentUser; + if (currentUser != null) { + context.go('/home'); + } else { + context.go('/login'); + } + }, + child: const Text('Go Home'), + ), + ], + ), + ), + ), + ); + + /// Get the router instance + static GoRouter get router => _router; +} + +/// Router provider for Riverpod integration +final routerProvider = Provider((ref) { + return AppRouter.router; +}); \ No newline at end of file diff --git a/lib/features/authentication/presentation/pages/splash_page.dart b/lib/features/authentication/presentation/pages/splash_page.dart new file mode 100644 index 0000000..158733e --- /dev/null +++ b/lib/features/authentication/presentation/pages/splash_page.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +/// Splash page for initial loading and authentication state checking +/// +/// Shows loading indicator while checking authentication state +/// and redirects to appropriate screen based on auth status +class SplashPage extends ConsumerStatefulWidget { + const SplashPage({super.key}); + + @override + ConsumerState createState() => _SplashPageState(); +} + +class _SplashPageState extends ConsumerState { + @override + void initState() { + super.initState(); + _checkAuthState(); + } + + /// Check authentication state and navigate accordingly + Future _checkAuthState() async { + try { + // Add small delay for better UX - shows splash screen + await Future.delayed(const Duration(milliseconds: 1500)); + + if (!mounted) return; + + // Check current authentication state + final currentUser = Supabase.instance.client.auth.currentUser; + + if (currentUser != null) { + // User is authenticated, navigate to home + // TODO: Use GoRouter when integrated + // Navigator.of(context).pushReplacementNamed('/home'); + + // For now, we'll use a simple navigation + Navigator.of(context).pushReplacementNamed('/home'); + } else { + // User is not authenticated, navigate to login + // TODO: Use GoRouter when integrated + // Navigator.of(context).pushReplacementNamed('/login'); + + // For now, we'll use a simple navigation + Navigator.of(context).pushReplacementNamed('/login'); + } + } catch (e) { + // Handle authentication state check errors + debugPrint('Error checking auth state: $e'); + + if (!mounted) return; + + // On error, navigate to login for safety + Navigator.of(context).pushReplacementNamed('/login'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App logo/icon + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.primary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: const Icon( + Icons.inventory_2_outlined, + size: 60, + color: Colors.white, + ), + ), + + const SizedBox(height: 32), + + // App name + Text( + 'Sage', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + + const SizedBox(height: 8), + + // Tagline + Text( + 'Your Food Inventory Tracker', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + + const SizedBox(height: 48), + + // Loading indicator + Column( + children: [ + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Loading...', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ], + ), + ), + ); + } +} \ No newline at end of file