From 07bc805a6394e0d8ae11b674090851d33d1f555c Mon Sep 17 00:00:00 2001 From: Sewmina Date: Sun, 7 Dec 2025 15:52:59 +0530 Subject: [PATCH] sync --- android/app/src/main/AndroidManifest.xml | 6 + lib/main.dart | 20 +- lib/screens/auth_screen.dart | 230 +++++++------ lib/screens/home_screen.dart | 416 +++++++++++++++++++++++ lib/screens/login_screen.dart | 78 +++-- lib/screens/register_screen.dart | 51 ++- lib/services/auth_service.dart | 214 ++++++++++++ lib/widgets/profile_avatar.dart | 97 ++++++ pubspec.lock | 2 +- pubspec.yaml | 1 + 10 files changed, 968 insertions(+), 147 deletions(-) create mode 100644 lib/screens/home_screen.dart create mode 100644 lib/services/auth_service.dart create mode 100644 lib/widgets/profile_avatar.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bb074e0..3c56346 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,6 +29,12 @@ + + + + + + diff --git a/lib/main.dart b/lib/main.dart index 66dac4d..d9bdd64 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(); - await Supabase.initialize( - url: 'https://pkaerjfdwgquztmsrfhy.supabase.co', - anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBrYWVyamZkd2dxdXp0bXNyZmh5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjAyODU1NjMsImV4cCI6MjA3NTg2MTU2M30.tNl04Rn-GquTF_hse0ea8OKNo9cJKAGVDoXP3ZVLSRg', - ); + 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(), + }, ); } } diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 3b67622..ff74b7e 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -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 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 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 _googleSignIn() async { + + Future _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 _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 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 _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 _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, ); -} - Future login(String email, String password) async { + // 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 register(String email, String password, String name) async { - + return response; +} + +Future oauthLoginWeb(OAuthProvider provider) async { + await supabase.auth.signInWithOAuth( + provider, + redirectTo: kIsWeb ? null : 'com.Xperience.TaskTracker.tasktracker://callback', +); +} + + Future login(String email, String password, bool rememberMe) async { + await _authService.loginWithPassword( + email: email, + password: password, + rememberMe: rememberMe, + ); + } + + Future register(String email, String password, String name, bool rememberMe) async { + await _authService.registerUser( + email: email, + password: password, + name: name, + rememberMe: rememberMe, + ); } Future logout() async { - + await _authService.logout(); } - Future oAuthLogin() async { - await _googleSignIn(); + Future 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 _googleSignIn() async { children: [ LoginScreen( onLogin: login, - onOAuthLogin: () => oAuthLogin(), + onOAuthLogin: (provider) => oAuthLogin(provider), ), RegisterScreen( onRegister: register, - onOAuthRegister: () => oAuthLogin(), + onOAuthRegister: (provider) => oAuthLogin(provider), ), ], ), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart new file mode 100644 index 0000000..0e538ad --- /dev/null +++ b/lib/screens/home_screen.dart @@ -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 createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + final AuthService _authService = AuthService(); + Map? _userInfo; + + @override + void initState() { + super.initState(); + _loadUserInfo(); + } + + Future _loadUserInfo() async { + final userInfo = await _authService.getUserInfo(); + setState(() { + _userInfo = userInfo; + }); + } + + Future _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, + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 1d4d2ba..09161d5 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -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 Function(String email, String password) onLogin; - final void Function() onOAuthLogin; + final Future Function(String email, String password, bool rememberMe) onLogin; + final void Function(OAuthProvider) onOAuthLogin; @override State createState() => _LoginScreenState(); @@ -24,6 +25,7 @@ class _LoginScreenState extends State { final _passwordController = TextEditingController(); bool _isPasswordVisible = false; bool _isLoading = false; + bool _rememberMe = false; @override void dispose() { @@ -39,7 +41,7 @@ class _LoginScreenState extends State { }); 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 { } } - void _handleOAuthLogin( ) { - widget.onOAuthLogin(); + void _handleOAuthLogin(OAuthProvider provider) { + widget.onOAuthLogin(provider); } @override @@ -130,24 +132,44 @@ class _LoginScreenState extends State { const SizedBox(height: 8), - // Forgot Password - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () { - // TODO: Implement forgot password - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Forgot password functionality will be implemented')), - ); - }, - child: Text( - 'Forgot Password?', + // 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: Theme.of(context).primaryColor, - fontWeight: FontWeight.w500, + color: Colors.grey[600], + fontSize: 14, ), ), - ), + const Spacer(), + // Forgot Password + TextButton( + onPressed: () { + // TODO: Implement forgot password + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Forgot password functionality will be implemented')), + ); + }, + child: Text( + 'Forgot Password?', + style: GoogleFonts.poppins( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), const SizedBox(height: 24), @@ -218,23 +240,23 @@ class _LoginScreenState extends State { 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), ), ], ), diff --git a/lib/screens/register_screen.dart b/lib/screens/register_screen.dart index fa757dc..df6905c 100644 --- a/lib/screens/register_screen.dart +++ b/lib/screens/register_screen.dart @@ -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 Function(String email, String password, String name) onRegister; - final void Function() onOAuthRegister; + final Future Function(String email, String password, String name, bool rememberMe) onRegister; + final void Function(OAuthProvider) onOAuthRegister; @override State createState() => _RegisterScreenState(); @@ -27,6 +28,7 @@ class _RegisterScreenState extends State { bool _isConfirmPasswordVisible = false; bool _isLoading = false; bool _agreeToTerms = false; + bool _rememberMe = false; @override void dispose() { @@ -48,6 +50,7 @@ class _RegisterScreenState extends State { _emailController.text, _passwordController.text, _nameController.text, + _rememberMe, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -80,8 +83,8 @@ class _RegisterScreenState extends State { } } - void _handleOAuthRegister() { - widget.onOAuthRegister(); + void _handleOAuthRegister(OAuthProvider provider) { + widget.onOAuthRegister(provider); } @override @@ -195,6 +198,30 @@ class _RegisterScreenState extends State { 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 { 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), ), ], ), diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart new file mode 100644 index 0000000..21cea84 --- /dev/null +++ b/lib/services/auth_service.dart @@ -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 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?> 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 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?> 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 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 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 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 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 logout() async { + await _supabase.auth.signOut(); + await clearSavedData(); + } +} diff --git a/lib/widgets/profile_avatar.dart b/lib/widgets/profile_avatar.dart new file mode 100644 index 0000000..2719d2e --- /dev/null +++ b/lib/widgets/profile_avatar.dart @@ -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]; + } +} diff --git a/pubspec.lock b/pubspec.lock index c52049a..536de92 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -449,7 +449,7 @@ packages: source: hosted version: "0.28.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" diff --git a/pubspec.yaml b/pubspec.yaml index b0b076d..fb80386 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: