import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; // ── Supabase credentials ────────────────────────────────────────────── const _supabaseUrl = 'https://yaopcyubateifnicpywp.supabase.co'; // TODO: Paste your anon key from Supabase Dashboard → Settings → API. const _supabaseAnonKey = 'YOUR_ANON_KEY'; 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 (placeholder) ───────────────────────────────────────── 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: Text( 'Signed in as ${user?.email ?? 'unknown'}', style: Theme.of(context).textTheme.titleMedium, ), ), ); } }