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 ?? FeedProvider(auth, voteProvider: vote); 76 }, 77 ), 78 ChangeNotifierProxyProvider2< 79 AuthProvider, 80 VoteProvider, 81 CommentsProvider 82 >( 83 create: 84 (context) => CommentsProvider( 85 authProvider, 86 voteProvider: context.read<VoteProvider>(), 87 commentService: commentService, 88 ), 89 update: (context, auth, vote, previous) { 90 // Reuse existing provider to maintain state across rebuilds 91 return previous ?? 92 CommentsProvider( 93 auth, 94 voteProvider: vote, 95 commentService: commentService, 96 ); 97 }, 98 ), 99 // StreamableService for video embeds 100 Provider<StreamableService>(create: (_) => StreamableService()), 101 ], 102 child: const CovesApp(), 103 ), 104 ); 105} 106 107class CovesApp extends StatelessWidget { 108 const CovesApp({super.key}); 109 110 @override 111 Widget build(BuildContext context) { 112 final authProvider = Provider.of<AuthProvider>(context, listen: false); 113 114 return MaterialApp.router( 115 title: 'Coves', 116 theme: ThemeData( 117 colorScheme: ColorScheme.fromSeed( 118 seedColor: AppColors.primary, 119 brightness: Brightness.dark, 120 ), 121 useMaterial3: true, 122 ), 123 routerConfig: _createRouter(authProvider), 124 restorationScopeId: 'app', 125 debugShowCheckedModeBanner: false, 126 ); 127 } 128} 129 130// GoRouter configuration factory 131GoRouter _createRouter(AuthProvider authProvider) { 132 return GoRouter( 133 routes: [ 134 GoRoute(path: '/', builder: (context, state) => const LandingScreen()), 135 GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), 136 GoRoute( 137 path: '/feed', 138 builder: (context, state) => const MainShellScreen(), 139 ), 140 GoRoute( 141 path: '/post/:postUri', 142 builder: (context, state) { 143 // Extract post from state.extra 144 final post = state.extra as FeedViewPost?; 145 146 // If no post provided via extra, show user-friendly error 147 if (post == null) { 148 if (kDebugMode) { 149 print('⚠️ PostDetailScreen: No post provided in route extras'); 150 } 151 // Show not found screen with option to go back 152 return NotFoundError( 153 title: 'Post Not Found', 154 message: 155 'This post could not be loaded. It may have been ' 156 'deleted or the link is invalid.', 157 onBackPressed: () { 158 // Navigate back to feed 159 context.go('/feed'); 160 }, 161 ); 162 } 163 164 return PostDetailScreen(post: post); 165 }, 166 ), 167 ], 168 refreshListenable: authProvider, 169 redirect: (context, state) { 170 final isAuthenticated = authProvider.isAuthenticated; 171 final isLoading = authProvider.isLoading; 172 final currentPath = state.uri.path; 173 174 // Don't redirect while loading initial auth state 175 if (isLoading) { 176 return null; 177 } 178 179 // If authenticated and on landing/login screen, redirect to feed 180 if (isAuthenticated && (currentPath == '/' || currentPath == '/login')) { 181 if (kDebugMode) { 182 print('🔄 User authenticated, redirecting to /feed'); 183 } 184 return '/feed'; 185 } 186 187 // Allow anonymous users to access /feed for browsing 188 // Sign-out redirect is handled explicitly in the sign-out action 189 return null; 190 }, 191 errorBuilder: (context, state) { 192 // Check if this is an OAuth callback 193 if (state.uri.scheme == OAuthConfig.customScheme) { 194 if (kDebugMode) { 195 print( 196 '⚠️ OAuth callback in errorBuilder - ' 197 'flutter_web_auth_2 should handle it', 198 ); 199 print(' URI: ${state.uri}'); 200 } 201 // Return nothing - just stay on current screen 202 // flutter_web_auth_2 will process the callback at native level 203 return const SizedBox.shrink(); 204 } 205 206 // For other errors, show landing page 207 if (kDebugMode) { 208 print('⚠️ Router error: ${state.uri}'); 209 } 210 return const LandingScreen(); 211 }, 212 ); 213}