diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart index 695df01..7547a52 100644 --- a/lib/features/auth/presentation/providers/auth_provider.dart +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -64,9 +64,9 @@ class AuthState { class Auth extends _$Auth { @override AuthState build() { - // Don't call async operations in build + // Start with loading state to show splash screen // Use a separate method to initialize auth state - return const AuthState(); + return const AuthState(isLoading: true); } AuthRepository get _repository => ref.read(authRepositoryProvider); @@ -74,7 +74,9 @@ class Auth extends _$Auth { /// Initialize auth state - call this on app start Future initialize() async { print('🚀 Initializing auth state...'); - state = state.copyWith(isLoading: true); + + // Minimum loading time for smooth UX (prevent flashing) + final minimumLoadingTime = Future.delayed(const Duration(milliseconds: 800)); final isAuthenticated = await _repository.isAuthenticated(); print('🚀 isAuthenticated result: $isAuthenticated'); @@ -83,6 +85,10 @@ class Auth extends _$Auth { print('🚀 Token found, fetching user profile...'); // Get user profile final result = await _repository.getProfile(); + + // Wait for minimum loading time to complete + await minimumLoadingTime; + result.fold( (failure) { print('❌ Failed to get profile: ${failure.message}'); @@ -103,6 +109,10 @@ class Auth extends _$Auth { ); } else { print('❌ No token found, user needs to login'); + + // Wait for minimum loading time even when not authenticated + await minimumLoadingTime; + state = const AuthState( isAuthenticated: false, isLoading: false, diff --git a/lib/features/auth/presentation/widgets/auth_wrapper.dart b/lib/features/auth/presentation/widgets/auth_wrapper.dart index 75196a8..7336674 100644 --- a/lib/features/auth/presentation/widgets/auth_wrapper.dart +++ b/lib/features/auth/presentation/widgets/auth_wrapper.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/auth_provider.dart'; import '../pages/login_page.dart'; +import 'splash_screen.dart'; /// Wrapper widget that checks authentication status /// Shows login page if not authenticated, otherwise shows child widget @@ -16,21 +17,27 @@ class AuthWrapper extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final authState = ref.watch(authProvider); - print('AuthWrapper build: isAuthenticated=${authState.isAuthenticated}, isLoading=${authState.isLoading}'); - // Show loading indicator while checking auth status + print('AuthWrapper build: isAuthenticated=${authState.isAuthenticated}, isLoading=${authState.isLoading}'); + + // Show splash screen while checking auth status if (authState.isLoading && authState.user == null) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); + return const SplashScreen(); } - // Show child widget if authenticated, otherwise show login page - if (authState.isAuthenticated) { - return child; - } else { - return const LoginPage(); - } + // Smooth fade transition between screens + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: authState.isAuthenticated + ? KeyedSubtree( + key: const ValueKey('main_app'), + child: child, + ) + : const KeyedSubtree( + key: ValueKey('login_page'), + child: LoginPage(), + ), + ); } } diff --git a/lib/features/auth/presentation/widgets/splash_screen.dart b/lib/features/auth/presentation/widgets/splash_screen.dart new file mode 100644 index 0000000..c1283be --- /dev/null +++ b/lib/features/auth/presentation/widgets/splash_screen.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; + +/// Splash screen shown while checking authentication status +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeIn, + ), + ); + + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOutBack, + ), + ); + + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + backgroundColor: theme.colorScheme.primary, + body: SafeArea( + child: Center( + child: FadeTransition( + opacity: _fadeAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App Icon/Logo + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Icon( + Icons.point_of_sale_rounded, + size: 64, + color: theme.colorScheme.primary, + ), + ), + const SizedBox(height: 32), + + // App Name + Text( + 'Retail POS', + style: theme.textTheme.headlineMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 8), + + // Subtitle + Text( + 'Point of Sale System', + style: theme.textTheme.bodyLarge?.copyWith( + color: Colors.white.withOpacity(0.9), + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 48), + + // Loading Indicator + SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Colors.white.withOpacity(0.9), + ), + ), + ), + const SizedBox(height: 16), + + // Loading Text + Text( + 'Loading...', + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.white.withOpacity(0.8), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +}