sync
This commit is contained in:
parent
8b61a46b07
commit
07bc805a63
|
|
@ -29,6 +29,12 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="appwrite-callback-tasktracker" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="supabase_auth">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="com.Xperience.TaskTracker.tasktracker" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
|
|
|||
|
|
@ -2,17 +2,21 @@ import 'package:flutter/material.dart';
|
|||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'screens/auth_screen.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
|
||||
void main() async {
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
try {
|
||||
await Supabase.initialize(
|
||||
url: 'https://pkaerjfdwgquztmsrfhy.supabase.co',
|
||||
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBrYWVyamZkd2dxdXp0bXNyZmh5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjAyODU1NjMsImV4cCI6MjA3NTg2MTU2M30.tNl04Rn-GquTF_hse0ea8OKNo9cJKAGVDoXP3ZVLSRg',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Supabase initialization error: $e');
|
||||
}
|
||||
|
||||
runApp(TaskTrackerApp());
|
||||
runApp(const TaskTrackerApp());
|
||||
}
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
|
|
@ -81,6 +85,10 @@ class TaskTrackerApp extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
home: AuthScreen(),
|
||||
routes: {
|
||||
'/auth': (context) => const AuthScreen(),
|
||||
'/home': (context) => const HomeScreen(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'login_screen.dart';
|
||||
import 'register_screen.dart';
|
||||
import '../services/auth_service.dart';
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
class AuthScreen extends StatefulWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
|
@ -17,13 +21,18 @@ class AuthScreen extends StatefulWidget {
|
|||
class _AuthScreenState extends State<AuthScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final SupabaseClient supabase = Supabase.instance.client;
|
||||
final AuthService _authService = AuthService();
|
||||
late TabController _tabController;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debugPrint('AuthScreen initState started');
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_setupAuthListener();
|
||||
_checkAutoLogin();
|
||||
debugPrint('AuthScreen initState completed');
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -37,23 +46,68 @@ class _AuthScreenState extends State<AuthScreen>
|
|||
supabase.auth.onAuthStateChange.listen((data) {
|
||||
final event = data.event;
|
||||
if (event == AuthChangeEvent.signedIn) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const Scaffold(
|
||||
body: Center(
|
||||
child: Text('Signed in'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
// Navigate to home screen
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/home');
|
||||
}
|
||||
} else if (event == AuthChangeEvent.signedOut) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Future<AuthResponse> _googleSignIn() async {
|
||||
|
||||
Future<void> _checkAutoLogin() async {
|
||||
debugPrint('_checkAutoLogin started');
|
||||
try {
|
||||
// Add timeout to prevent hanging
|
||||
await Future.any([
|
||||
_performAutoLogin(),
|
||||
Future.delayed(const Duration(seconds: 10)),
|
||||
]);
|
||||
debugPrint('_checkAutoLogin completed');
|
||||
} catch (e) {
|
||||
debugPrint('_checkAutoLogin error: $e');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performAutoLogin() async {
|
||||
debugPrint('_performAutoLogin started');
|
||||
// Check if user is already authenticated
|
||||
if (_authService.isAuthenticated) {
|
||||
debugPrint('User already authenticated, navigating to home');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
// Navigate to home screen immediately
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/home');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('Attempting auto-login with saved credentials');
|
||||
// Try auto-login with saved credentials
|
||||
final success = await _authService.autoLogin();
|
||||
debugPrint('Auto-login result: $success');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
// Let the auth listener handle navigation if successful
|
||||
}
|
||||
|
||||
Future<AuthResponse> googleSignInNative() async {
|
||||
/// TODO: update the Web client ID with your own.
|
||||
///
|
||||
/// Web Client ID that you registered with Google Cloud.
|
||||
const webClientId = '311478513323-n2qut52hb4ms7g6g6r5ukni4mr49p6ff.apps.googleusercontent.com';
|
||||
const webClientId = '311478513323-4vj6v6d254jbs8tt334u4874see8i4vj.apps.googleusercontent.com';
|
||||
|
||||
/// TODO: update the iOS client ID with your own.
|
||||
///
|
||||
|
|
@ -69,7 +123,11 @@ Future<AuthResponse> _googleSignIn() async {
|
|||
);
|
||||
|
||||
final googleUser = await signIn.signIn();
|
||||
final googleAuth = await googleUser!.authentication;
|
||||
if (googleUser == null) {
|
||||
throw Exception('Google sign-in was cancelled');
|
||||
}
|
||||
|
||||
final googleAuth = await googleUser.authentication;
|
||||
final accessToken = googleAuth.accessToken;
|
||||
final idToken = googleAuth.idToken;
|
||||
|
||||
|
|
@ -80,114 +138,86 @@ Future<AuthResponse> _googleSignIn() async {
|
|||
throw Exception('No id token found');
|
||||
}
|
||||
|
||||
return supabase.auth.signInWithIdToken(
|
||||
final response = await supabase.auth.signInWithIdToken(
|
||||
provider: OAuthProvider.google,
|
||||
idToken: idToken,
|
||||
accessToken: accessToken,
|
||||
);
|
||||
|
||||
// Save user information after successful OAuth login
|
||||
final user = response.user;
|
||||
if (user != null) {
|
||||
final userName = googleUser.displayName ?? user.userMetadata?['name'] ?? 'User';
|
||||
final userEmail = googleUser.email;
|
||||
|
||||
await _authService.saveUserInfo(
|
||||
name: userName,
|
||||
email: userEmail,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<void> register(String email, String password, String name) async {
|
||||
Future<void> oauthLoginWeb(OAuthProvider provider) async {
|
||||
await supabase.auth.signInWithOAuth(
|
||||
provider,
|
||||
redirectTo: kIsWeb ? null : 'com.Xperience.TaskTracker.tasktracker://callback',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> login(String email, String password, bool rememberMe) async {
|
||||
await _authService.loginWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
rememberMe: rememberMe,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> register(String email, String password, String name, bool rememberMe) async {
|
||||
await _authService.registerUser(
|
||||
email: email,
|
||||
password: password,
|
||||
name: name,
|
||||
rememberMe: rememberMe,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
|
||||
await _authService.logout();
|
||||
}
|
||||
|
||||
|
||||
Future<void> oAuthLogin() async {
|
||||
await _googleSignIn();
|
||||
Future<void> oAuthLogin(OAuthProvider provider) async {
|
||||
try {
|
||||
if(Platform.isAndroid && provider == OAuthProvider.google){
|
||||
await googleSignInNative();
|
||||
} else {
|
||||
await oauthLoginWeb(provider);
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle OAuth login errors
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('OAuth login failed: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isLogged= false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
// If user is logged in, show logout screen
|
||||
if (isLogged) {
|
||||
// Show loading screen while checking authentication
|
||||
if (_isLoading) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).primaryColor.withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.task_alt,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Welcome back!',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"username",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"email or id",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ElevatedButton(
|
||||
onPressed: logout,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Text(
|
||||
'Logout',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -295,11 +325,11 @@ Future<AuthResponse> _googleSignIn() async {
|
|||
children: [
|
||||
LoginScreen(
|
||||
onLogin: login,
|
||||
onOAuthLogin: () => oAuthLogin(),
|
||||
onOAuthLogin: (provider) => oAuthLogin(provider),
|
||||
),
|
||||
RegisterScreen(
|
||||
onRegister: register,
|
||||
onOAuthRegister: () => oAuthLogin(),
|
||||
onOAuthRegister: (provider) => oAuthLogin(provider),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
416
lib/screens/home_screen.dart
Normal file
416
lib/screens/home_screen.dart
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../widgets/profile_avatar.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
final AuthService _authService = AuthService();
|
||||
Map<String, String>? _userInfo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserInfo();
|
||||
}
|
||||
|
||||
Future<void> _loadUserInfo() async {
|
||||
final userInfo = await _authService.getUserInfo();
|
||||
setState(() {
|
||||
_userInfo = userInfo;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleLogout() async {
|
||||
await _authService.logout();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/auth');
|
||||
}
|
||||
}
|
||||
|
||||
void _showProfileMenu() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle bar
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Profile info
|
||||
ProfileAvatar(
|
||||
name: _userInfo?['name'],
|
||||
size: 80,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
_userInfo?['name'] ?? 'User',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
Text(
|
||||
_userInfo?['email'] ?? 'user@example.com',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Menu items
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline, color: Color(0xFF6366F1)),
|
||||
title: Text(
|
||||
'Edit Profile',
|
||||
style: GoogleFonts.poppins(fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Navigate to edit profile
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Edit Profile coming soon!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings_outlined, color: Color(0xFF6366F1)),
|
||||
title: Text(
|
||||
'Settings',
|
||||
style: GoogleFonts.poppins(fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Navigate to settings
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Settings coming soon!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
title: Text(
|
||||
'Logout',
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_handleLogout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Greeting
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Hello,',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_userInfo?['name'] ?? 'User',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Profile Avatar
|
||||
ProfileAvatar(
|
||||
name: _userInfo?['name'],
|
||||
size: 48,
|
||||
onTap: _showProfileMenu,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Main Content
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
// Welcome Card
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6366F1).withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.task_alt,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Welcome to TaskTracker!',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Let\'s get things done',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Quick Actions
|
||||
Text(
|
||||
'Quick Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Action Cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
icon: Icons.add_task,
|
||||
title: 'Add Task',
|
||||
subtitle: 'Create new task',
|
||||
color: const Color(0xFF10B981),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Add Task coming soon!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
icon: Icons.list_alt,
|
||||
title: 'View Tasks',
|
||||
subtitle: 'See all tasks',
|
||||
color: const Color(0xFF3B82F6),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('View Tasks coming soon!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
icon: Icons.analytics_outlined,
|
||||
title: 'Analytics',
|
||||
subtitle: 'View progress',
|
||||
color: const Color(0xFF8B5CF6),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Analytics coming soon!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildActionCard(
|
||||
icon: Icons.settings_outlined,
|
||||
title: 'Settings',
|
||||
subtitle: 'App settings',
|
||||
color: const Color(0xFFF59E0B),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Settings coming soon!')),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionCard({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../widgets/oauth_button.dart';
|
||||
import '../widgets/custom_text_field.dart';
|
||||
|
||||
|
|
@ -11,8 +12,8 @@ class LoginScreen extends StatefulWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
final Future<void> Function(String email, String password) onLogin;
|
||||
final void Function() onOAuthLogin;
|
||||
final Future<void> Function(String email, String password, bool rememberMe) onLogin;
|
||||
final void Function(OAuthProvider) onOAuthLogin;
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
|
|
@ -24,6 +25,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
final _passwordController = TextEditingController();
|
||||
bool _isPasswordVisible = false;
|
||||
bool _isLoading = false;
|
||||
bool _rememberMe = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
@ -39,7 +41,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
});
|
||||
|
||||
try {
|
||||
await widget.onLogin(_emailController.text, _passwordController.text);
|
||||
await widget.onLogin(_emailController.text, _passwordController.text, _rememberMe);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
|
|
@ -67,8 +69,8 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
void _handleOAuthLogin( ) {
|
||||
widget.onOAuthLogin();
|
||||
void _handleOAuthLogin(OAuthProvider provider) {
|
||||
widget.onOAuthLogin(provider);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -130,10 +132,29 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Remember Me and Forgot Password Row
|
||||
Row(
|
||||
children: [
|
||||
// Remember Me Checkbox
|
||||
Checkbox(
|
||||
value: _rememberMe,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_rememberMe = value ?? false;
|
||||
});
|
||||
},
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
Text(
|
||||
'Remember me',
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Forgot Password
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Implement forgot password
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -148,6 +169,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
|
@ -218,23 +240,23 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
OAuthButton(
|
||||
provider: 'Google',
|
||||
icon: Icons.g_mobiledata,
|
||||
onPressed: () => _handleOAuthLogin(),
|
||||
onPressed: () => _handleOAuthLogin(OAuthProvider.google),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
OAuthButton(
|
||||
provider: 'Facebook',
|
||||
icon: Icons.facebook,
|
||||
onPressed: () => _handleOAuthLogin(),
|
||||
),
|
||||
// OAuthButton(
|
||||
// provider: 'Facebook',
|
||||
// icon: Icons.facebook,
|
||||
// onPressed: () => _handleOAuthLogin(OAuthProvider.facebook),
|
||||
// ),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
// const SizedBox(height: 12),
|
||||
|
||||
OAuthButton(
|
||||
provider: 'GitHub',
|
||||
icon: Icons.code,
|
||||
onPressed: () => _handleOAuthLogin(),
|
||||
onPressed: () => _handleOAuthLogin(OAuthProvider.github),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../widgets/oauth_button.dart';
|
||||
import '../widgets/custom_text_field.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
const RegisterScreen({
|
||||
|
|
@ -10,8 +11,8 @@ class RegisterScreen extends StatefulWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
final Future<void> Function(String email, String password, String name) onRegister;
|
||||
final void Function() onOAuthRegister;
|
||||
final Future<void> Function(String email, String password, String name, bool rememberMe) onRegister;
|
||||
final void Function(OAuthProvider) onOAuthRegister;
|
||||
|
||||
@override
|
||||
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||
|
|
@ -27,6 +28,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||
bool _isConfirmPasswordVisible = false;
|
||||
bool _isLoading = false;
|
||||
bool _agreeToTerms = false;
|
||||
bool _rememberMe = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
@ -48,6 +50,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||
_emailController.text,
|
||||
_passwordController.text,
|
||||
_nameController.text,
|
||||
_rememberMe,
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -80,8 +83,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
void _handleOAuthRegister() {
|
||||
widget.onOAuthRegister();
|
||||
void _handleOAuthRegister(OAuthProvider provider) {
|
||||
widget.onOAuthRegister(provider);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -195,6 +198,30 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Remember Me Checkbox
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _rememberMe,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_rememberMe = value ?? false;
|
||||
});
|
||||
},
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
Text(
|
||||
'Remember me',
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Terms and Conditions
|
||||
Row(
|
||||
children: [
|
||||
|
|
@ -306,23 +333,23 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||
OAuthButton(
|
||||
provider: 'Google',
|
||||
icon: Icons.g_mobiledata,
|
||||
onPressed: () => _handleOAuthRegister(),
|
||||
onPressed: () => _handleOAuthRegister(OAuthProvider.google),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
OAuthButton(
|
||||
provider: 'Facebook',
|
||||
icon: Icons.facebook,
|
||||
onPressed: () => _handleOAuthRegister(),
|
||||
),
|
||||
// OAuthButton(
|
||||
// provider: 'Facebook',
|
||||
// icon: Icons.facebook,
|
||||
// onPressed: () => _handleOAuthRegister(OAuthProvider.facebook),
|
||||
// ),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
// const SizedBox(height: 12),
|
||||
|
||||
OAuthButton(
|
||||
provider: 'GitHub',
|
||||
icon: Icons.code,
|
||||
onPressed: () => _handleOAuthRegister(),
|
||||
onPressed: () => _handleOAuthRegister(OAuthProvider.github),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
214
lib/services/auth_service.dart
Normal file
214
lib/services/auth_service.dart
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class AuthService {
|
||||
static const String _emailKey = 'saved_email';
|
||||
static const String _passwordKey = 'saved_password';
|
||||
static const String _rememberMeKey = 'remember_me';
|
||||
static const String _userNameKey = 'user_name';
|
||||
static const String _userEmailKey = 'user_email';
|
||||
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
|
||||
/// Save login credentials to SharedPreferences
|
||||
Future<void> saveCredentials({
|
||||
required String email,
|
||||
required String password,
|
||||
required bool rememberMe,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (rememberMe) {
|
||||
await prefs.setString(_emailKey, email);
|
||||
await prefs.setString(_passwordKey, password);
|
||||
await prefs.setBool(_rememberMeKey, true);
|
||||
} else {
|
||||
// Clear saved credentials if user doesn't want to be remembered
|
||||
await prefs.remove(_emailKey);
|
||||
await prefs.remove(_passwordKey);
|
||||
await prefs.setBool(_rememberMeKey, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get saved login credentials
|
||||
Future<Map<String, dynamic>?> getSavedCredentials() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance().timeout(
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
final rememberMe = prefs.getBool(_rememberMeKey) ?? false;
|
||||
|
||||
if (!rememberMe) return null;
|
||||
|
||||
final email = prefs.getString(_emailKey);
|
||||
final password = prefs.getString(_passwordKey);
|
||||
|
||||
if (email != null && password != null) {
|
||||
return {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'rememberMe': rememberMe,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error getting saved credentials: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Save user information after successful login
|
||||
Future<void> saveUserInfo({
|
||||
required String name,
|
||||
required String email,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_userNameKey, name);
|
||||
await prefs.setString(_userEmailKey, email);
|
||||
}
|
||||
|
||||
/// Get saved user information
|
||||
Future<Map<String, String>?> getUserInfo() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance().timeout(
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
final name = prefs.getString(_userNameKey);
|
||||
final email = prefs.getString(_userEmailKey);
|
||||
|
||||
if (name != null && email != null) {
|
||||
return {
|
||||
'name': name,
|
||||
'email': email,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('Error getting user info: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all saved data (for logout)
|
||||
Future<void> clearSavedData() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_emailKey);
|
||||
await prefs.remove(_passwordKey);
|
||||
await prefs.remove(_rememberMeKey);
|
||||
await prefs.remove(_userNameKey);
|
||||
await prefs.remove(_userEmailKey);
|
||||
}
|
||||
|
||||
/// Check if user is currently authenticated
|
||||
bool get isAuthenticated => _supabase.auth.currentUser != null;
|
||||
|
||||
/// Get current user
|
||||
User? get currentUser => _supabase.auth.currentUser;
|
||||
|
||||
/// Auto-login with saved credentials
|
||||
Future<bool> autoLogin() async {
|
||||
try {
|
||||
debugPrint('Auto-login started');
|
||||
final credentials = await getSavedCredentials();
|
||||
if (credentials == null) {
|
||||
debugPrint('No saved credentials found');
|
||||
return false;
|
||||
}
|
||||
|
||||
debugPrint('Attempting sign-in with saved credentials');
|
||||
final response = await _supabase.auth.signInWithPassword(
|
||||
email: credentials['email'],
|
||||
password: credentials['password'],
|
||||
).timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.user != null) {
|
||||
debugPrint('Auto-login successful');
|
||||
// Save user info if not already saved
|
||||
final userInfo = await getUserInfo();
|
||||
if (userInfo == null) {
|
||||
await saveUserInfo(
|
||||
name: response.user!.userMetadata?['name'] ?? 'User',
|
||||
email: response.user!.email ?? credentials['email'],
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
debugPrint('Auto-login failed: no user in response');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Auto-login error: $e');
|
||||
// If auto-login fails, clear saved credentials
|
||||
await clearSavedData();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Login with email and password
|
||||
Future<AuthResponse> loginWithPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
required bool rememberMe,
|
||||
}) async {
|
||||
final response = await _supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (response.user != null) {
|
||||
// Save credentials if remember me is checked
|
||||
await saveCredentials(
|
||||
email: email,
|
||||
password: password,
|
||||
rememberMe: rememberMe,
|
||||
);
|
||||
|
||||
// Save user info
|
||||
await saveUserInfo(
|
||||
name: response.user!.userMetadata?['name'] ?? 'User',
|
||||
email: response.user!.email ?? email,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// Register new user
|
||||
Future<AuthResponse> registerUser({
|
||||
required String email,
|
||||
required String password,
|
||||
required String name,
|
||||
required bool rememberMe,
|
||||
}) async {
|
||||
final response = await _supabase.auth.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
data: {'name': name},
|
||||
);
|
||||
|
||||
if (response.user != null) {
|
||||
// Save credentials if remember me is checked
|
||||
await saveCredentials(
|
||||
email: email,
|
||||
password: password,
|
||||
rememberMe: rememberMe,
|
||||
);
|
||||
|
||||
// Save user info
|
||||
await saveUserInfo(
|
||||
name: name,
|
||||
email: email,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// Logout user
|
||||
Future<void> logout() async {
|
||||
await _supabase.auth.signOut();
|
||||
await clearSavedData();
|
||||
}
|
||||
}
|
||||
97
lib/widgets/profile_avatar.dart
Normal file
97
lib/widgets/profile_avatar.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class ProfileAvatar extends StatelessWidget {
|
||||
final String? name;
|
||||
final String? imageUrl;
|
||||
final double size;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ProfileAvatar({
|
||||
super.key,
|
||||
this.name,
|
||||
this.imageUrl,
|
||||
this.size = 40,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _getAvatarColor(name ?? 'User'),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: imageUrl != null && imageUrl!.isNotEmpty
|
||||
? ClipOval(
|
||||
child: Image.network(
|
||||
imageUrl!,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _buildInitialsAvatar();
|
||||
},
|
||||
),
|
||||
)
|
||||
: _buildInitialsAvatar(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInitialsAvatar() {
|
||||
final initials = _getInitials(name ?? 'User');
|
||||
return Center(
|
||||
child: Text(
|
||||
initials,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: size * 0.4,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getInitials(String name) {
|
||||
final words = name.trim().split(' ');
|
||||
if (words.isEmpty) return 'U';
|
||||
|
||||
if (words.length == 1) {
|
||||
return words[0].substring(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
return '${words[0].substring(0, 1)}${words[1].substring(0, 1)}'.toUpperCase();
|
||||
}
|
||||
|
||||
Color _getAvatarColor(String name) {
|
||||
// Generate a consistent color based on the name
|
||||
final colors = [
|
||||
const Color(0xFF6366F1), // Indigo
|
||||
const Color(0xFF8B5CF6), // Purple
|
||||
const Color(0xFFEC4899), // Pink
|
||||
const Color(0xFFEF4444), // Red
|
||||
const Color(0xFFF59E0B), // Amber
|
||||
const Color(0xFF10B981), // Emerald
|
||||
const Color(0xFF06B6D4), // Cyan
|
||||
const Color(0xFF3B82F6), // Blue
|
||||
const Color(0xFF84CC16), // Lime
|
||||
const Color(0xFFF97316), // Orange
|
||||
];
|
||||
|
||||
// Use the name's hash to consistently pick the same color
|
||||
final hash = name.hashCode;
|
||||
return colors[hash.abs() % colors.length];
|
||||
}
|
||||
}
|
||||
|
|
@ -449,7 +449,7 @@ packages:
|
|||
source: hosted
|
||||
version: "0.28.0"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ dependencies:
|
|||
flutter_svg: ^2.0.10+1
|
||||
supabase_flutter: ^2.10.3
|
||||
google_sign_in: ^6.3.0
|
||||
shared_preferences: ^2.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user