This commit is contained in:
Sewmina 2025-12-07 15:52:59 +05:30
parent 8b61a46b07
commit 07bc805a63
10 changed files with 968 additions and 147 deletions

View File

@ -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 -->

View File

@ -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(),
},
);
}
}

View File

@ -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),
),
],
),

View 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,
),
],
),
),
);
}
}

View File

@ -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),
),
],
),

View File

@ -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),
),
],
),

View 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();
}
}

View 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];
}
}

View File

@ -449,7 +449,7 @@ packages:
source: hosted
version: "0.28.0"
shared_preferences:
dependency: transitive
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"

View File

@ -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: