Main coves client
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}