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