Compare commits

..

2 Commits

9 changed files with 505 additions and 2 deletions

5
.gitignore vendored
View File

@ -57,6 +57,7 @@ linked_*.ds
unlinked.ds unlinked.ds
unlinked_spec.ds unlinked_spec.ds
# Android related # Android related
**/android/**/gradle-wrapper.jar **/android/**/gradle-wrapper.jar
.gradle/ .gradle/
@ -144,8 +145,8 @@ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ #lib/
lib64/ # lib64/
parts/ parts/
sdist/ sdist/
var/ var/

6
.vscode/launch.json vendored
View File

@ -4,6 +4,12 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Flutter",
"type": "dart",
"request": "launch",
"program": "lib/main.dart"
},
{ {
"name": "complycore_flutter", "name": "complycore_flutter",
"cwd": "frontend\\complycore_flutter", "cwd": "frontend\\complycore_flutter",

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'screens/splash_screen.dart';
Future<void> 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(),
);
}

View File

@ -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!')));
}

View File

@ -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<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
bool _loading = false;
String? _error;
Future<void> _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'),
),
],
),
],
),
),
),
),
),
);
}
}

View File

@ -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<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final _formKey = GlobalKey<FormState>();
final _fullNameController = TextEditingController();
final _companyController = TextEditingController();
bool _loading = false;
@override
void initState() {
super.initState();
_fetchProfile();
}
Future<void> _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<String, dynamic>? 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<void> _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<void> _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'),
),
],
),
),
),
);
}
}

View File

@ -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<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _loading = false;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _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'),
),
],
),
),
),
),
);
}
}

View File

@ -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<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@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()));
}

View File

@ -0,0 +1,15 @@
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthService {
static final _supabase = Supabase.instance.client;
Future<AuthResponse> signIn(String email, String password) =>
_supabase.auth.signInWithPassword(email: email, password: password);
Future<AuthResponse> signUp(String email, String password) =>
_supabase.auth.signUp(email: email, password: password);
Future<void> signOut() => _supabase.auth.signOut();
User? get currentUser => _supabase.auth.currentUser;
}