1import 'package:flutter/foundation.dart'; 2import 'package:flutter/material.dart'; 3import 'package:flutter/services.dart'; 4import 'package:go_router/go_router.dart'; 5import 'package:provider/provider.dart'; 6 7import 'config/oauth_config.dart'; 8import 'constants/app_colors.dart'; 9import 'models/post.dart'; 10import 'providers/auth_provider.dart'; 11import 'providers/comments_provider.dart'; 12import 'providers/feed_provider.dart'; 13import 'providers/vote_provider.dart'; 14import 'screens/auth/login_screen.dart'; 15import 'screens/home/main_shell_screen.dart'; 16import 'screens/home/post_detail_screen.dart'; 17import 'screens/landing_screen.dart'; 18import 'services/streamable_service.dart'; 19import 'services/vote_service.dart'; 20import 'widgets/loading_error_states.dart'; 21 22void main() async { 23 WidgetsFlutterBinding.ensureInitialized(); 24 25 // Set system UI overlay style (Android navigation bar) 26 SystemChrome.setSystemUIOverlayStyle( 27 const SystemUiOverlayStyle( 28 systemNavigationBarColor: Color(0xFF0B0F14), 29 systemNavigationBarIconBrightness: Brightness.light, 30 ), 31 ); 32 33 // Initialize auth provider 34 final authProvider = AuthProvider(); 35 await authProvider.initialize(); 36 37 // Initialize vote service with auth callbacks 38 // Votes go through the Coves backend (which proxies to PDS with DPoP) 39 // Includes token refresh and sign-out handlers for automatic 401 recovery 40 final voteService = VoteService( 41 sessionGetter: () async => authProvider.session, 42 didGetter: () => authProvider.did, 43 tokenRefresher: authProvider.refreshToken, 44 signOutHandler: authProvider.signOut, 45 ); 46 47 runApp( 48 MultiProvider( 49 providers: [ 50 ChangeNotifierProvider.value(value: authProvider), 51 ChangeNotifierProvider( 52 create: 53 (_) => VoteProvider( 54 voteService: voteService, 55 authProvider: authProvider, 56 ), 57 ), 58 ChangeNotifierProxyProvider2<AuthProvider, VoteProvider, FeedProvider>( 59 create: 60 (context) => FeedProvider( 61 authProvider, 62 voteProvider: context.read<VoteProvider>(), 63 ), 64 update: (context, auth, vote, previous) { 65 // Reuse existing provider to maintain state across rebuilds 66 return previous ?? 67 FeedProvider( 68 auth, 69 voteProvider: vote, 70 ); 71 }, 72 ), 73 ChangeNotifierProxyProvider2< 74 AuthProvider, 75 VoteProvider, 76 CommentsProvider 77 >( 78 create: 79 (context) => CommentsProvider( 80 authProvider, 81 voteProvider: context.read<VoteProvider>(), 82 ), 83 update: (context, auth, vote, previous) { 84 // Reuse existing provider to maintain state across rebuilds 85 return previous ?? 86 CommentsProvider( 87 auth, 88 voteProvider: vote, 89 ); 90 }, 91 ), 92 // StreamableService for video embeds 93 Provider<StreamableService>(create: (_) => StreamableService()), 94 ], 95 child: const CovesApp(), 96 ), 97 ); 98} 99 100class CovesApp extends StatelessWidget { 101 const CovesApp({super.key}); 102 103 @override 104 Widget build(BuildContext context) { 105 final authProvider = Provider.of<AuthProvider>(context, listen: false); 106 107 return MaterialApp.router( 108 title: 'Coves', 109 theme: ThemeData( 110 colorScheme: ColorScheme.fromSeed( 111 seedColor: AppColors.primary, 112 brightness: Brightness.dark, 113 ), 114 useMaterial3: true, 115 ), 116 routerConfig: _createRouter(authProvider), 117 restorationScopeId: 'app', 118 debugShowCheckedModeBanner: false, 119 ); 120 } 121} 122 123// GoRouter configuration factory 124GoRouter _createRouter(AuthProvider authProvider) { 125 return GoRouter( 126 routes: [ 127 GoRoute(path: '/', builder: (context, state) => const LandingScreen()), 128 GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), 129 GoRoute( 130 path: '/feed', 131 builder: (context, state) => const MainShellScreen(), 132 ), 133 GoRoute( 134 path: '/post/:postUri', 135 builder: (context, state) { 136 // Extract post from state.extra 137 final post = state.extra as FeedViewPost?; 138 139 // If no post provided via extra, show user-friendly error 140 if (post == null) { 141 if (kDebugMode) { 142 print('⚠️ PostDetailScreen: No post provided in route extras'); 143 } 144 // Show not found screen with option to go back 145 return NotFoundError( 146 title: 'Post Not Found', 147 message: 148 'This post could not be loaded. It may have been ' 149 'deleted or the link is invalid.', 150 onBackPressed: () { 151 // Navigate back to feed 152 context.go('/feed'); 153 }, 154 ); 155 } 156 157 return PostDetailScreen(post: post); 158 }, 159 ), 160 ], 161 refreshListenable: authProvider, 162 redirect: (context, state) { 163 final isAuthenticated = authProvider.isAuthenticated; 164 final isLoading = authProvider.isLoading; 165 final currentPath = state.uri.path; 166 167 // Don't redirect while loading initial auth state 168 if (isLoading) { 169 return null; 170 } 171 172 // If authenticated and on landing/login screen, redirect to feed 173 if (isAuthenticated && (currentPath == '/' || currentPath == '/login')) { 174 if (kDebugMode) { 175 print('🔄 User authenticated, redirecting to /feed'); 176 } 177 return '/feed'; 178 } 179 180 // Allow anonymous users to access /feed for browsing 181 // Sign-out redirect is handled explicitly in the sign-out action 182 return null; 183 }, 184 errorBuilder: (context, state) { 185 // Check if this is an OAuth callback 186 if (state.uri.scheme == OAuthConfig.customScheme) { 187 if (kDebugMode) { 188 print( 189 '⚠️ OAuth callback in errorBuilder - ' 190 'flutter_web_auth_2 should handle it', 191 ); 192 print(' URI: ${state.uri}'); 193 } 194 // Return nothing - just stay on current screen 195 // flutter_web_auth_2 will process the callback at native level 196 return const SizedBox.shrink(); 197 } 198 199 // For other errors, show landing page 200 if (kDebugMode) { 201 print('⚠️ Router error: ${state.uri}'); 202 } 203 return const LandingScreen(); 204 }, 205 ); 206}