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 ??
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}