import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'theme/app_theme.dart'; import 'services/collection_service.dart'; import 'services/main_collection_sync.dart'; import 'screens/login_screen.dart'; import 'screens/home_shell.dart'; import 'utils/error_utils.dart'; // Re-export so other files can `import '../main.dart'` for these. export 'package:supabase_flutter/supabase_flutter.dart' show AuthException, UserAttributes; // ── Supabase credentials ────────────────────────────────────────────── const _supabaseUrl = String.fromEnvironment( 'SUPABASE_URL', defaultValue: 'https://yaopcyubateifnicpywp.supabase.co', ); const _supabaseAnonKey = String.fromEnvironment( 'SUPABASE_ANON_KEY', defaultValue: 'sb_publishable_a7czIl7-TGeBJvid9z2XZA_3ElImliL', ); const _usePkceAuthFlow = bool.fromEnvironment( 'SUPABASE_USE_PKCE', defaultValue: true, ); Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Supabase.initialize( url: _supabaseUrl, anonKey: _supabaseAnonKey, authOptions: FlutterAuthClientOptions( authFlowType: _usePkceAuthFlow ? AuthFlowType.pkce : AuthFlowType.implicit, ), ); runApp(const Car64App()); } /// Convenience accessor used throughout the app. final supabase = Supabase.instance.client; /// Global keys so dialogs & snackbars survive widget-tree rebuilds. final navigatorKey = GlobalKey(); final scaffoldMessengerKey = GlobalKey(); /// Show a snackbar safely through the global key. void showGlobalSnackBar(String message, {bool isError = false}) { final messenger = scaffoldMessengerKey.currentState; if (messenger == null) return; messenger ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? Colors.red : null, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.fromLTRB(16, 0, 16, 96), duration: const Duration(seconds: 3), ), ); } void showGlobalError( Object error, { String fallback = 'Something went wrong. Please try again.', }) { logError('ui', error); showGlobalSnackBar( userMessageForError(error, fallback: fallback), isError: true, ); } /// Show a dialog safely through the global navigator key. Future showGlobalDialog({required WidgetBuilder builder}) { final context = navigatorKey.currentContext; if (context == null) { return Future.value(null); } return showDialog( context: context, builder: builder, ); } // ── Root App Widget ─────────────────────────────────────────────────── class Car64App extends StatelessWidget { const Car64App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'car64', debugShowCheckedModeBanner: false, navigatorKey: navigatorKey, scaffoldMessengerKey: scaffoldMessengerKey, theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: ThemeMode.system, home: const AuthGate(), ); } } // ── Auth Gate ───────────────────────────────────────────────────────── class AuthGate extends StatefulWidget { const AuthGate({super.key}); @override State createState() => _AuthGateState(); } class _AuthGateState extends State { static const _activeCollectionPrefKey = 'active_collection_id'; bool _isLoading = true; bool _isInPasswordRecoveryFlow = false; Session? _session; String? _lastEnsuredUserId; @override void initState() { super.initState(); _session = supabase.auth.currentSession; _ensureDefaultCollectionIfNeeded(); supabase.auth.onAuthStateChange.listen( (AuthState authState) { if (!mounted) return; final wasLoggedIn = _session != null; final isLoggedIn = authState.session != null; _session = authState.session; if (wasLoggedIn != isLoggedIn) { setState(() {}); } _ensureDefaultCollectionIfNeeded(); if (authState.event == AuthChangeEvent.passwordRecovery) { setState(() => _isInPasswordRecoveryFlow = true); _showResetPasswordDialog(); } }, onError: (error) { showGlobalError( error, fallback: 'Authentication error. Please sign in again.', ); }, ); setState(() => _isLoading = false); } Future _ensureDefaultCollectionIfNeeded() async { final userId = _session?.user.id; if (userId == null || userId == _lastEnsuredUserId) return; _lastEnsuredUserId = userId; try { final defaultCollectionId = await CollectionService.ensureDefaultCollection(); await _ensureMainCollectionPreference(defaultCollectionId); } catch (e) { showGlobalError( e, fallback: 'Collection setup failed. Please try again.', ); } } Future _ensureMainCollectionPreference(String? defaultCollectionId) async { final userId = _session?.user.id; if (userId == null) return; final prefs = await SharedPreferences.getInstance(); final persisted = prefs.getString(_activeCollectionPrefKey); Future hasMembership(String collectionId) async { final membership = await supabase .from('collection_members') .select('id') .eq('user_id', userId) .eq('collection_id', collectionId) .maybeSingle(); return membership != null; } if (persisted != null && await hasMembership(persisted)) { return; } String? nextActiveId; if (defaultCollectionId != null && await hasMembership(defaultCollectionId)) { nextActiveId = defaultCollectionId; } else { final firstMembership = await supabase .from('collection_members') .select('collection_id') .eq('user_id', userId) .limit(1); if (firstMembership.isNotEmpty) { nextActiveId = firstMembership.first['collection_id'] as String; } } if (nextActiveId == null) { await prefs.remove(_activeCollectionPrefKey); return; } await prefs.setString(_activeCollectionPrefKey, nextActiveId); MainCollectionSync.notifyChanged(); } Future _showResetPasswordDialog() async { final context = navigatorKey.currentContext; if (context == null) { _isInPasswordRecoveryFlow = false; return; } await showDialog( context: context, barrierDismissible: false, builder: (_) => const _ResetPasswordDialog(), ); if (mounted) { setState(() => _isInPasswordRecoveryFlow = false); } } @override Widget build(BuildContext context) { if (_isLoading) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } if (_isInPasswordRecoveryFlow) return const LoginScreen(); return _session != null ? const HomeShell() : const LoginScreen(); } } // ── Reset Password Dialog ───────────────────────────────────────────── class _ResetPasswordDialog extends StatefulWidget { const _ResetPasswordDialog(); @override State<_ResetPasswordDialog> createState() => _ResetPasswordDialogState(); } class _ResetPasswordDialogState extends State<_ResetPasswordDialog> { final _passwordController = TextEditingController(); bool _isSaving = false; @override void dispose() { _passwordController.dispose(); super.dispose(); } Future _save() async { final newPassword = _passwordController.text.trim(); if (newPassword.length < 6) { showGlobalSnackBar('Password must be at least 6 characters.'); return; } setState(() => _isSaving = true); try { await supabase.auth.updateUser( UserAttributes(password: newPassword), ); if (!mounted) return; Navigator.of(context).pop(); showGlobalSnackBar('Password updated successfully!'); } on AuthException catch (e) { if (!mounted) return; setState(() => _isSaving = false); showGlobalSnackBar(e.message, isError: true); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Set New Password'), content: TextField( controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: 'New password'), onSubmitted: (_) => _save(), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), ElevatedButton( onPressed: _isSaving ? null : _save, child: _isSaving ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Save'), ), ], ); } }