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