From 4849a2938ece40ab33e3e507639e07ff673438fc Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Mon, 28 Jul 2025 10:08:14 -0400 Subject: [PATCH] fix .gitignore issue with flutter lib --- .gitignore | 5 +- .vscode/launch.json | 34 ++++ frontend/complycore_flutter/lib/main.dart | 32 ++++ .../lib/screens/home_screen.dart | 9 ++ .../lib/screens/login_screen.dart | 121 ++++++++++++++ .../lib/screens/profile_screen.dart | 153 ++++++++++++++++++ .../lib/screens/register_screen.dart | 124 ++++++++++++++ .../lib/screens/splash_screen.dart | 42 +++++ .../lib/services/auth_service.dart | 15 ++ 9 files changed, 533 insertions(+), 2 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 frontend/complycore_flutter/lib/main.dart create mode 100644 frontend/complycore_flutter/lib/screens/home_screen.dart create mode 100644 frontend/complycore_flutter/lib/screens/login_screen.dart create mode 100644 frontend/complycore_flutter/lib/screens/profile_screen.dart create mode 100644 frontend/complycore_flutter/lib/screens/register_screen.dart create mode 100644 frontend/complycore_flutter/lib/screens/splash_screen.dart create mode 100644 frontend/complycore_flutter/lib/services/auth_service.dart diff --git a/.gitignore b/.gitignore index 8e5e701..3168199 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ linked_*.ds unlinked.ds unlinked_spec.ds + # Android related **/android/**/gradle-wrapper.jar .gradle/ @@ -144,8 +145,8 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +#lib/ +# lib64/ parts/ sdist/ var/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..815acb3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "type": "dart", + "request": "launch", + "program": "lib/main.dart" + }, + { + "name": "complycore_flutter", + "cwd": "frontend\\complycore_flutter", + "request": "launch", + "type": "dart" + }, + { + "name": "complycore_flutter (profile mode)", + "cwd": "frontend\\complycore_flutter", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "complycore_flutter (release mode)", + "cwd": "frontend\\complycore_flutter", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/frontend/complycore_flutter/lib/main.dart b/frontend/complycore_flutter/lib/main.dart new file mode 100644 index 0000000..95b9df7 --- /dev/null +++ b/frontend/complycore_flutter/lib/main.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import 'screens/splash_screen.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Supabase.initialize( + url: const String.fromEnvironment( + 'SUPABASE_URL', + defaultValue: 'https://YOUR-SUPABASE-URL.supabase.co', + ), + anonKey: const String.fromEnvironment( + 'SUPABASE_ANON_KEY', + defaultValue: 'YOUR-SUPABASE-ANON-KEY', + ), + ); + + runApp(const ComplyCoreApp()); +} + +class ComplyCoreApp extends StatelessWidget { + const ComplyCoreApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + title: 'ComplyCore', + theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true), + home: const SplashScreen(), + ); +} diff --git a/frontend/complycore_flutter/lib/screens/home_screen.dart b/frontend/complycore_flutter/lib/screens/home_screen.dart new file mode 100644 index 0000000..b945285 --- /dev/null +++ b/frontend/complycore_flutter/lib/screens/home_screen.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) => + const Scaffold(body: Center(child: Text('Welcome to ComplyCore!'))); +} diff --git a/frontend/complycore_flutter/lib/screens/login_screen.dart b/frontend/complycore_flutter/lib/screens/login_screen.dart new file mode 100644 index 0000000..b1cd6df --- /dev/null +++ b/frontend/complycore_flutter/lib/screens/login_screen.dart @@ -0,0 +1,121 @@ +// lib/screens/login_screen.dart +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import 'register_screen.dart'; // <-- this must already exist in lib/screens/ + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _formKey = GlobalKey(); + final _emailCtrl = TextEditingController(); + final _passCtrl = TextEditingController(); + + bool _loading = false; + String? _error; + + Future _signIn() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _loading = true; + _error = null; + }); + + try { + final res = await Supabase.instance.client.auth.signInWithPassword( + email: _emailCtrl.text.trim(), + password: _passCtrl.text, + ); + if (res.session == null) { + setState(() => _error = 'Invalid email or password'); + } + } catch (e) { + setState(() => _error = e.toString()); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + minimum: const EdgeInsets.symmetric(horizontal: 24), + child: Center( + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'ComplyCore Login', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 32), + + // E-mail + TextFormField( + controller: _emailCtrl, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(labelText: 'Email'), + validator: (v) => v != null && v.contains('@') + ? null + : 'Enter a valid email', + ), + const SizedBox(height: 16), + + // Password + TextFormField( + controller: _passCtrl, + obscureText: true, + decoration: const InputDecoration(labelText: 'Password'), + validator: (v) => + v != null && v.length >= 6 ? null : 'Min 6 characters', + ), + const SizedBox(height: 32), + + ElevatedButton( + onPressed: _loading ? null : _signIn, + child: _loading + ? const CircularProgressIndicator() + : const Text('Sign In'), + ), + + if (_error != null) ...[ + const SizedBox(height: 16), + Text(_error!, style: const TextStyle(color: Colors.red)), + ], + + const SizedBox(height: 24), + + // ------------- NEW LINK ------------- + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Don't have an account?"), + TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const RegisterScreen(), + ), + ), + child: const Text('Register'), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/complycore_flutter/lib/screens/profile_screen.dart b/frontend/complycore_flutter/lib/screens/profile_screen.dart new file mode 100644 index 0000000..b6c7438 --- /dev/null +++ b/frontend/complycore_flutter/lib/screens/profile_screen.dart @@ -0,0 +1,153 @@ +// lib/screens/profile_screen.dart +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + final _formKey = GlobalKey(); + + final _fullNameController = TextEditingController(); + final _companyController = TextEditingController(); + + bool _loading = false; + + @override + void initState() { + super.initState(); + _fetchProfile(); + } + + Future _fetchProfile() async { + setState(() => _loading = true); + + final userId = Supabase.instance.client.auth.currentUser?.id; + if (userId == null) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('No active session.'))); + } + return; + } + + // Grab profile row + final Map? profile = await Supabase.instance.client + .from('profiles') + .select('*') + .eq('id', userId) + .maybeSingle(); + + if (!mounted) return; // ← guard BuildContext after await + + if (profile != null) { + _fullNameController.text = profile['full_name'] ?? ''; + _companyController.text = profile['company'] ?? ''; + } else { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Profile not found.'))); + } + + setState(() => _loading = false); + } + + Future _updateProfile() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _loading = true); + + final user = Supabase.instance.client.auth.currentUser; + if (user == null) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('No active session.'))); + } + return; + } + + final updates = { + 'id': user.id, + 'full_name': _fullNameController.text.trim(), + 'company': _companyController.text.trim(), + 'updated_at': DateTime.now().toIso8601String(), + }; + + try { + await Supabase.instance.client.from('profiles').upsert(updates); + if (!mounted) return; + + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Profile updated!'))); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Update failed: $e'))); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + Future _signOut() async { + await Supabase.instance.client.auth.signOut(); + if (!mounted) return; + Navigator.of(context).pushReplacementNamed('/login'); + } + + @override + void dispose() { + _fullNameController.dispose(); + _companyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('My Profile'), + actions: [ + IconButton(icon: const Icon(Icons.logout), onPressed: _signOut), + ], + ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + controller: _fullNameController, + decoration: const InputDecoration(labelText: 'Full name'), + validator: (v) => + v == null || v.isEmpty ? 'Required' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _companyController, + decoration: const InputDecoration( + labelText: 'Company name', + ), + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: _updateProfile, + child: const Text('Save changes'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/complycore_flutter/lib/screens/register_screen.dart b/frontend/complycore_flutter/lib/screens/register_screen.dart new file mode 100644 index 0000000..e8d1673 --- /dev/null +++ b/frontend/complycore_flutter/lib/screens/register_screen.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); // ← super-parameter ✔ + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + final _formKey = GlobalKey(); + + final _nameController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + bool _loading = false; + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _signUp() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _loading = true); + + try { + await Supabase.instance.client.auth.signUp( + email: _emailController.text.trim(), + password: _passwordController.text, + data: {'full_name': _nameController.text.trim()}, + ); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Registration successful! Confirm your email, then sign in.', + ), + ), + ); + + Navigator.of(context).pushReplacementNamed('/login'); + } on AuthException catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.message))); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Sign-up failed: $e'))); + } + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('ComplyCore • Register')), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: _nameController, + decoration: const InputDecoration(labelText: 'Full name'), + validator: (v) => (v == null || v.trim().isEmpty) + ? 'Enter your name' + : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _emailController, + decoration: const InputDecoration(labelText: 'Email'), + keyboardType: TextInputType.emailAddress, + validator: (v) => (v == null || !v.contains('@')) + ? 'Enter a valid email' + : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + validator: (v) => + (v == null || v.length < 6) ? 'Min 6 characters' : null, + ), + const SizedBox(height: 24), + _loading + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: _signUp, + child: const Text('Register'), + ), + const SizedBox(height: 12), + TextButton( + onPressed: () => + Navigator.of(context).pushReplacementNamed('/login'), + child: const Text('Already have an account? Sign in'), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/complycore_flutter/lib/screens/splash_screen.dart b/frontend/complycore_flutter/lib/screens/splash_screen.dart new file mode 100644 index 0000000..fc8ded6 --- /dev/null +++ b/frontend/complycore_flutter/lib/screens/splash_screen.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import 'home_screen.dart'; +import 'login_screen.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + + // Listen for auth changes first + Supabase.instance.client.auth.onAuthStateChange.listen((data) { + final session = data.session; + _routeAccordingToSession(session); + }); + + // Fallback: check once after short delay + Future.delayed(const Duration(seconds: 2), () { + if (!mounted) return; + _routeAccordingToSession(Supabase.instance.client.auth.currentSession); + }); + } + + void _routeAccordingToSession(Session? session) { + final target = session == null ? const LoginScreen() : const HomeScreen(); + Navigator.of( + context, + ).pushReplacement(MaterialPageRoute(builder: (_) => target)); + } + + @override + Widget build(BuildContext context) => + const Scaffold(body: Center(child: CircularProgressIndicator())); +} diff --git a/frontend/complycore_flutter/lib/services/auth_service.dart b/frontend/complycore_flutter/lib/services/auth_service.dart new file mode 100644 index 0000000..a461a12 --- /dev/null +++ b/frontend/complycore_flutter/lib/services/auth_service.dart @@ -0,0 +1,15 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; + +class AuthService { + static final _supabase = Supabase.instance.client; + + Future signIn(String email, String password) => + _supabase.auth.signInWithPassword(email: email, password: password); + + Future signUp(String email, String password) => + _supabase.auth.signUp(email: email, password: password); + + Future signOut() => _supabase.auth.signOut(); + + User? get currentUser => _supabase.auth.currentUser; +}