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