Compare commits
2 Commits
ecd216409d
...
00c5063723
| Author | SHA1 | Date |
|---|---|---|
|
|
00c5063723 | |
|
|
4849a2938e |
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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!')));
|
||||||
|
}
|
||||||
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue