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 'providers/auth_provider.dart';
9import 'providers/feed_provider.dart';
10import 'providers/vote_provider.dart';
11import 'screens/auth/login_screen.dart';
12import 'screens/home/main_shell_screen.dart';
13import 'screens/landing_screen.dart';
14import 'services/vote_service.dart';
15
16void main() async {
17 WidgetsFlutterBinding.ensureInitialized();
18
19 // Set system UI overlay style (Android navigation bar)
20 SystemChrome.setSystemUIOverlayStyle(
21 const SystemUiOverlayStyle(
22 systemNavigationBarColor: Color(0xFF0B0F14),
23 systemNavigationBarIconBrightness: Brightness.light,
24 ),
25 );
26
27 // Initialize auth provider
28 final authProvider = AuthProvider();
29 await authProvider.initialize();
30
31 // Initialize vote service with auth callbacks for direct PDS writes
32 // Uses DPoP authentication (not Bearer tokens!)
33 final voteService = VoteService(
34 sessionGetter: () async => authProvider.session,
35 didGetter: () => authProvider.did,
36 pdsUrlGetter: authProvider.getPdsUrl,
37 );
38
39 runApp(
40 MultiProvider(
41 providers: [
42 ChangeNotifierProvider.value(value: authProvider),
43 ChangeNotifierProvider(create: (_) => FeedProvider(authProvider)),
44 ChangeNotifierProvider(
45 create: (_) => VoteProvider(
46 voteService: voteService,
47 authProvider: authProvider,
48 ),
49 ),
50 ],
51 child: const CovesApp(),
52 ),
53 );
54}
55
56class CovesApp extends StatelessWidget {
57 const CovesApp({super.key});
58
59 @override
60 Widget build(BuildContext context) {
61 final authProvider = Provider.of<AuthProvider>(context, listen: false);
62
63 return MaterialApp.router(
64 title: 'Coves',
65 theme: ThemeData(
66 colorScheme: ColorScheme.fromSeed(
67 seedColor: const Color(0xFFFF6B35),
68 brightness: Brightness.dark,
69 ),
70 useMaterial3: true,
71 ),
72 routerConfig: _createRouter(authProvider),
73 debugShowCheckedModeBanner: false,
74 );
75 }
76}
77
78// GoRouter configuration factory
79GoRouter _createRouter(AuthProvider authProvider) {
80 return GoRouter(
81 routes: [
82 GoRoute(path: '/', builder: (context, state) => const LandingScreen()),
83 GoRoute(path: '/login', builder: (context, state) => const LoginScreen()),
84 GoRoute(
85 path: '/feed',
86 builder: (context, state) => const MainShellScreen(),
87 ),
88 ],
89 refreshListenable: authProvider,
90 redirect: (context, state) {
91 final isAuthenticated = authProvider.isAuthenticated;
92 final isLoading = authProvider.isLoading;
93 final currentPath = state.uri.path;
94
95 // Don't redirect while loading initial auth state
96 if (isLoading) {
97 return null;
98 }
99
100 // If authenticated and on landing/login screen, redirect to feed
101 if (isAuthenticated && (currentPath == '/' || currentPath == '/login')) {
102 if (kDebugMode) {
103 print('🔄 User authenticated, redirecting to /feed');
104 }
105 return '/feed';
106 }
107
108 // Allow anonymous users to access /feed for browsing
109 // Sign-out redirect is handled explicitly in the sign-out action
110 return null;
111 },
112 errorBuilder: (context, state) {
113 // Check if this is an OAuth callback
114 if (state.uri.scheme == OAuthConfig.customScheme) {
115 if (kDebugMode) {
116 print(
117 '⚠️ OAuth callback in errorBuilder - '
118 'flutter_web_auth_2 should handle it',
119 );
120 print(' URI: ${state.uri}');
121 }
122 // Return nothing - just stay on current screen
123 // flutter_web_auth_2 will process the callback at native level
124 return const SizedBox.shrink();
125 }
126
127 // For other errors, show landing page
128 if (kDebugMode) {
129 print('⚠️ Router error: ${state.uri}');
130 }
131 return const LandingScreen();
132 },
133 );
134}