import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'scanner_screen.dart'; // ── Supabase credentials ────────────────────────────────────────────── const _supabaseUrl = 'https://yaopcyubateifnicpywp.supabase.co'; const _supabaseAnonKey = 'sb_publishable_a7czIl7-TGeBJvid9z2XZA_3ElImliL'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Supabase.initialize( url: _supabaseUrl, anonKey: _supabaseAnonKey, ); runApp(const HWHubApp()); } /// Convenience accessor used throughout the app. final supabase = Supabase.instance.client; // ── Root App Widget ─────────────────────────────────────────────────── class HWHubApp extends StatelessWidget { const HWHubApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'HW Hub', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const AuthGate(), ); } } // ── Auth Gate ───────────────────────────────────────────────────────── // Listens to Supabase auth state changes (including deep-link callbacks // for password resets) and routes to Login or Home. class AuthGate extends StatefulWidget { const AuthGate({super.key}); @override State createState() => _AuthGateState(); } class _AuthGateState extends State { bool _isLoading = true; Session? _session; @override void initState() { super.initState(); // 1. Check for an existing session on cold start. _session = supabase.auth.currentSession; // 2. Listen for auth state changes (sign-in, sign-out, // password-recovery deep link, token refresh, etc.) supabase.auth.onAuthStateChange.listen( (AuthState authState) { if (!mounted) return; setState(() { _session = authState.session; }); // If the user just clicked a password-reset link from their email, // Supabase fires a PASSWORD_RECOVERY event. We can navigate them // to a "set new password" screen here. if (authState.event == AuthChangeEvent.passwordRecovery) { _showResetPasswordDialog(); } }, onError: (error) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Auth error: $error'), backgroundColor: Colors.red), ); }, ); // Done checking – remove the loading state. setState(() => _isLoading = false); } /// Shows a dialog so the user can type a new password after clicking /// the "Reset Password" link from their email. Future _showResetPasswordDialog() async { final passwordController = TextEditingController(); await showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( title: const Text('Set New Password'), content: TextField( controller: passwordController, obscureText: true, decoration: const InputDecoration( labelText: 'New password', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancel'), ), ElevatedButton( onPressed: () async { final newPassword = passwordController.text.trim(); if (newPassword.length < 6) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Password must be at least 6 characters.')), ); return; } try { await supabase.auth.updateUser( UserAttributes(password: newPassword), ); if (!ctx.mounted) return; Navigator.of(ctx).pop(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Password updated successfully!')), ); } on AuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message), backgroundColor: Colors.red), ); } }, child: const Text('Save'), ), ], ), ); passwordController.dispose(); } @override Widget build(BuildContext context) { if (_isLoading) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } if (_session != null) { return const HomeScreen(); } return const LoginScreen(); } } // ── Login Screen ────────────────────────────────────────────────────── class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isLoading = false; /// Sign in with email + password. Future _signIn() async { final email = _emailController.text.trim(); final password = _passwordController.text; if (email.isEmpty || password.isEmpty) return; setState(() => _isLoading = true); try { await supabase.auth.signInWithPassword( email: email, password: password, ); // AuthGate listener will pick up the new session automatically. } on AuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message), backgroundColor: Colors.red), ); } finally { if (mounted) setState(() => _isLoading = false); } } /// Send a password-reset email. The link in the email uses the /// hwcollector://login deep link so it opens back in the app, /// which triggers the PASSWORD_RECOVERY event in AuthGate. Future _forgotPassword() async { final email = _emailController.text.trim(); if (email.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Enter your email first.')), ); return; } try { await supabase.auth.resetPasswordForEmail( email, redirectTo: 'hwcollector://login', ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Password reset email sent! Check your inbox.')), ); } on AuthException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message), backgroundColor: Colors.red), ); } } @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.directions_car, size: 72, color: Colors.deepPurple), const SizedBox(height: 16), const Text( 'Hot Wheels Tracker', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 32), TextField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: 'Email', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: _passwordController, obscureText: true, decoration: const InputDecoration( labelText: 'Password', border: OutlineInputBorder(), ), onSubmitted: (_) => _signIn(), ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: _forgotPassword, child: const Text('Forgot Password?'), ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _signIn, child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Sign In'), ), ), ], ), ), ), ); } } // ── Home Screen ─────────────────────────────────────────────────────── class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { final user = supabase.auth.currentUser; return Scaffold( appBar: AppBar( title: const Text('Garage is Online! 🏎️'), actions: [ IconButton( icon: const Icon(Icons.logout), onPressed: () async { await supabase.auth.signOut(); }, ), ], ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Signed in as ${user?.email ?? 'unknown'}', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 32), const Icon(Icons.directions_car, size: 80, color: Colors.deepPurple), const SizedBox(height: 16), const Text( 'Scan or enter a Hot Wheels ID\nto check your collection.', textAlign: TextAlign.center, ), ], ), ), floatingActionButton: FloatingActionButton.extended( icon: const Icon(Icons.camera_alt), label: const Text('Scan'), onPressed: () => _openScanner(context), ), ); } /// Opens the scanner, gets the hw_id, then queries the DB. Future _openScanner(BuildContext context) async { // 1. Navigate to the scanner screen and wait for the result. final hwId = await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ScannerScreen()), ); // User cancelled / went back without selecting an ID. if (hwId == null || !context.mounted) return; // 2. Query the database. try { final data = await supabase .from('hotwheels') .select() .eq('hw_id', hwId) .maybeSingle(); if (!context.mounted) return; if (data != null) { // ── Car already exists ── _showAlreadyExistsDialog(context, hwId); } else { // ── Car is new — offer to add it ── _showAddDialog(context, hwId); } } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('DB error: $e'), backgroundColor: Colors.red), ); } } /// Shows an alert: this car is already in the collection. void _showAlreadyExistsDialog(BuildContext context, String hwId) { showDialog( context: context, builder: (ctx) => AlertDialog( icon: const Icon(Icons.check_circle, color: Colors.green, size: 48), title: const Text('Already in Collection!'), content: Text('$hwId is already in your shared garage.'), actions: [ ElevatedButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Got it'), ), ], ), ); } /// Shows a confirmation dialog to add a new car. void _showAddDialog(BuildContext context, String hwId) { showDialog( context: context, builder: (ctx) => AlertDialog( icon: const Icon(Icons.add_circle_outline, color: Colors.deepPurple, size: 48), title: const Text('New Car Found!'), content: Text('$hwId is not in your collection yet.\nAdd it now?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancel'), ), ElevatedButton( onPressed: () async { try { await supabase.from('hotwheels').insert({ 'hw_id': hwId, 'user_id': supabase.auth.currentUser!.id, }); if (!ctx.mounted) return; Navigator.of(ctx).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$hwId added to your collection! 🎉')), ); } catch (e) { if (!ctx.mounted) return; Navigator.of(ctx).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to add: $e'), backgroundColor: Colors.red), ); } }, child: const Text('Add to Collection'), ), ], ), ); } }