From 622db644fab076fd79fa793cfb71bf8278247580 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Mon, 21 Jul 2025 09:36:31 +0300 Subject: [PATCH 1/7] feat: login page with sucessful login authentication into secure shared preferences --- frontend/lib/constants/api_constants.dart | 9 + frontend/lib/main.dart | 346 +++++---------- frontend/lib/screens/pages/profile_page.dart | 405 +++++++++++------- frontend/lib/services/auth_service.dart | 72 ++++ frontend/lib/services/http_service.dart | 40 ++ frontend/lib/services/user_service.dart | 25 ++ .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 4 + frontend/pubspec.lock | 138 +++++- frontend/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 641 insertions(+), 408 deletions(-) create mode 100644 frontend/lib/constants/api_constants.dart create mode 100644 frontend/lib/services/auth_service.dart create mode 100644 frontend/lib/services/http_service.dart create mode 100644 frontend/lib/services/user_service.dart diff --git a/frontend/lib/constants/api_constants.dart b/frontend/lib/constants/api_constants.dart new file mode 100644 index 0000000..1f91e4c --- /dev/null +++ b/frontend/lib/constants/api_constants.dart @@ -0,0 +1,9 @@ +class ApiConstants { + static const String baseUrl = 'http://localhost:8080'; + + // Auth endpoints + static const String loginEndpoint = '/login'; + + // User endpoints + static const String getUserEndpoint = '/getUser'; +} \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index ed7dd02..313a62e 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -3,6 +3,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; import 'screens/home_screen.dart'; import 'services/notification_service.dart'; +import 'services/auth_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -28,63 +29,16 @@ class LandingPage extends StatefulWidget { _LandingPageState createState() => _LandingPageState(); } -class _LandingPageState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _slideAnimation; - bool _showBottomSheet = false; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: Duration(milliseconds: 800), - vsync: this, - ); - _slideAnimation = Tween(begin: 1.0, end: 0.0).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - void _showGetStartedBottomSheet() { - setState(() { - _showBottomSheet = true; - }); - _animationController.forward(); - +class _LandingPageState extends State { + void _navigateDirectlyToLogin() { // Request notification permissions on user action NotificationService().requestPermissionAndSetup(); - } - void _hideBottomSheet() { - _animationController.reverse().then((_) { - setState(() { - _showBottomSheet = false; - }); - }); - } - - void _navigateToSignIn() { Navigator.of( context, ).push(MaterialPageRoute(builder: (context) => SignInPage())); } - void _navigateToHome() { - // Request notification permissions on user action - NotificationService().requestPermissionAndSetup(); - - Navigator.of( - context, - ).push(MaterialPageRoute(builder: (context) => HomeScreen())); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -156,7 +110,7 @@ class _LandingPageState extends State width: 280, height: 56, child: ElevatedButton( - onPressed: _showGetStartedBottomSheet, + onPressed: _navigateDirectlyToLogin, style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF6A4C93), foregroundColor: Colors.white, @@ -183,25 +137,6 @@ class _LandingPageState extends State ), SizedBox(height: 24), - - // Login link - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: _navigateToHome, - child: Text( - 'Skip to Home', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - ), - ), - ], - ), ], ), ), @@ -209,158 +144,6 @@ class _LandingPageState extends State ], ), ), - - // Bottom sheet overlay - if (_showBottomSheet) - AnimatedBuilder( - animation: _slideAnimation, - builder: (context, child) { - return Positioned( - bottom: 0, - left: 0, - right: 0, - child: Transform.translate( - offset: Offset(0, _slideAnimation.value * 400), - child: Container( - height: 400, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 20, - offset: Offset(0, -5), - ), - ], - ), - child: Column( - children: [ - // Handle bar - Container( - margin: EdgeInsets.only(top: 12), - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(2), - ), - ), - - // Close button - Align( - alignment: Alignment.topRight, - child: IconButton( - onPressed: _hideBottomSheet, - icon: Icon(Icons.close, color: Colors.grey[600]), - ), - ), - - Expanded( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Welcome In', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - - SizedBox(height: 12), - - Text( - 'Join your community.. change your social life!', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - - SizedBox(height: 40), - - // Sign Up button - Container( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: () { - // Handle sign up - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text('Sign Up pressed'), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF6A4C93), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 12, - ), - ), - elevation: 2, - ), - child: Text( - 'Sign Up Now', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - - SizedBox(height: 16), - - // Login button - Container( - width: double.infinity, - height: 56, - child: OutlinedButton( - onPressed: _navigateToSignIn, - style: OutlinedButton.styleFrom( - foregroundColor: Color(0xFF6A4C93), - side: BorderSide( - color: Color(0xFF6A4C93), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 12, - ), - ), - ), - child: Text( - 'Login', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - }, - ), ], ), ); @@ -379,6 +162,72 @@ class _SignInPageState extends State { bool _isPasswordVisible = false; bool _isLoading = false; + void _showHelpBottomSheet() { + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) => Container( + padding: EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + SizedBox(height: 24), + Icon(Icons.contact_support, size: 48, color: Color(0xFF6A4C93)), + SizedBox(height: 16), + Text( + 'Need Help?', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 16), + Text( + 'For account creation or password reset, please contact ERP Management Group from your Aramco email address.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.grey[700], + height: 1.4, + ), + ), + SizedBox(height: 24), + Container( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.symmetric(vertical: 16), + ), + child: Text( + 'Got It', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ), + SizedBox(height: 16), + ], + ), + ), + ); + } + @override void dispose() { _emailController.dispose(); @@ -392,19 +241,44 @@ class _SignInPageState extends State { _isLoading = true; }); - // Simulate API call - await Future.delayed(Duration(seconds: 2)); + final result = await AuthService.login( + _emailController.text.trim(), + _passwordController.text, + ); setState(() { _isLoading = false; }); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Sign in successful!'))); + if (result['success'] == true) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => HomeScreen()), + ); + } else { + _showErrorAlert(result['message'] ?? 'Login failed'); + } } } + void _showErrorAlert(String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Login Failed'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'OK', + style: TextStyle(color: Color(0xFF6A4C93)), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -493,7 +367,7 @@ class _SignInPageState extends State { SizedBox(height: 8), Text( - 'Sign in to your account', + 'Sign in to socialize with your colleagues\nand transform your social life!', style: TextStyle( fontSize: 16, color: Colors.grey[600], @@ -575,13 +449,7 @@ class _SignInPageState extends State { Align( alignment: Alignment.centerRight, child: TextButton( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Forgot password pressed'), - ), - ); - }, + onPressed: _showHelpBottomSheet, child: Text( 'Forgot Password?', style: TextStyle( @@ -625,7 +493,7 @@ class _SignInPageState extends State { SizedBox(height: 20), - // Sign up link + // Contact link for new accounts Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -634,13 +502,9 @@ class _SignInPageState extends State { style: TextStyle(color: Colors.grey[600]), ), GestureDetector( - onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Sign up pressed')), - ); - }, + onTap: _showHelpBottomSheet, child: Text( - 'Sign Up', + 'Contact Support', style: TextStyle( color: Color(0xFF6A4C93), fontWeight: FontWeight.w600, diff --git a/frontend/lib/screens/pages/profile_page.dart b/frontend/lib/screens/pages/profile_page.dart index 481d55e..6cd5dbc 100644 --- a/frontend/lib/screens/pages/profile_page.dart +++ b/frontend/lib/screens/pages/profile_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../services/notification_service.dart'; +import '../../services/user_service.dart'; class ProfilePage extends StatefulWidget { @override @@ -11,6 +12,8 @@ class _ProfilePageState extends State { String? fcmToken; final TextEditingController _tokenController = TextEditingController(); bool isLoading = false; + Map? userData; + bool isLoadingUser = true; final List> mockUserPosts = [ { @@ -46,6 +49,7 @@ class _ProfilePageState extends State { void initState() { super.initState(); _loadFCMToken(); + _loadUserData(); } @override @@ -75,6 +79,43 @@ class _ProfilePageState extends State { } } + Future _loadUserData() async { + setState(() { + isLoadingUser = true; + }); + + final result = await UserService.getCurrentUser(); + + setState(() { + isLoadingUser = false; + if (result['success'] == true) { + userData = result['data']; + } else { + userData = null; + _showErrorAlert(result['message'] ?? 'Failed to load user data'); + } + }); + } + + void _showErrorAlert(String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Error'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'OK', + style: TextStyle(color: Color(0xFF6A4C93)), + ), + ), + ], + ), + ); + } + Future _copyToClipboard() async { if (fcmToken != null && fcmToken!.isNotEmpty) { await Clipboard.setData(ClipboardData(text: fcmToken!)); @@ -119,200 +160,232 @@ class _ProfilePageState extends State { padding: EdgeInsets.all(24), child: Column( children: [ - CircleAvatar( - radius: 50, - backgroundColor: Color(0xFF6A4C93), - child: Text( - 'A', + if (isLoadingUser) + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFF6A4C93)), + ) + else if (userData != null) ...[ + CircleAvatar( + radius: 50, + backgroundColor: Color(0xFF6A4C93), + child: Text( + (userData!['displayName'] ?? 'U').substring(0, 1).toUpperCase(), + style: TextStyle( + color: Colors.white, + fontSize: 36, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(height: 16), + Text( + userData!['displayName'] ?? 'Unknown User', style: TextStyle( - color: Colors.white, - fontSize: 36, + fontSize: 24, fontWeight: FontWeight.bold, + color: Colors.black87, ), ), - ), - SizedBox(height: 16), - Text( - 'Abu Norah', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black87, + SizedBox(height: 4), + Text( + '@${userData!['username'] ?? 'unknown'}', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), ), - ), - SizedBox(height: 4), - Text( - '@yasser_hajri', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), + ] else ...[ + Icon( + Icons.error_outline, + size: 50, + color: Colors.grey[400], + ), + SizedBox(height: 16), + Text( + 'Failed to load user data', + style: TextStyle( + fontSize: 18, + color: Colors.grey[600], + ), + ), + SizedBox(height: 8), + ElevatedButton( + onPressed: _loadUserData, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + ), + child: Text('Retry'), + ), + ], SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - Text( - '${mockUserPosts.length}', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF6A4C93), - ), - ), - Text( - 'Posts', - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), - ), - ], - ), - Column( - children: [ - Text( - '127', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF6A4C93), - ), - ), - Text( - 'Followers', - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), - ), - ], - ), - ], - ), - ], - ), - ), - Container( - height: 1, - color: Colors.grey[200], - margin: EdgeInsets.symmetric(horizontal: 16), - ), - SizedBox(height: 16), - ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 16), - itemCount: mockUserPosts.length, - itemBuilder: (context, index) { - final post = mockUserPosts[index]; - return Container( - margin: EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: Offset(0, 2), - ), - ], - ), - child: Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (userData != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Row( + Column( children: [ - CircleAvatar( - radius: 16, - backgroundColor: Color(0xFF6A4C93), - child: Text( - 'J', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 14, - ), + Text( + '${mockUserPosts.length}', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF6A4C93), ), ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Abu Norah', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Colors.black87, - ), - ), - Text( - post['timestamp'], - style: TextStyle( - color: Colors.grey[600], - fontSize: 12, - ), - ), - ], + Text( + 'Posts', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], ), ), ], ), - SizedBox(height: 12), - Text( - post['content'], - style: TextStyle( - fontSize: 15, - height: 1.4, - color: Colors.black87, - ), - ), - SizedBox(height: 12), - Row( + Column( children: [ - Icon( - post['isLiked'] - ? Icons.favorite - : Icons.favorite_border, - color: post['isLiked'] - ? Colors.red - : Colors.grey[600], - size: 20, - ), - SizedBox(width: 4), Text( - '${post['likes']}', + '127', style: TextStyle( - color: Colors.grey[700], - fontSize: 14, + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF6A4C93), ), ), - SizedBox(width: 16), - Icon( - Icons.chat_bubble_outline, - color: Colors.grey[600], - size: 20, - ), - SizedBox(width: 4), Text( - '${post['comments']}', + 'Followers', style: TextStyle( - color: Colors.grey[700], fontSize: 14, + color: Colors.grey[600], ), ), ], ), ], ), - ), - ); - }, + ], + ), ), + if (userData != null) ...[ + Container( + height: 1, + color: Colors.grey[200], + margin: EdgeInsets.symmetric(horizontal: 16), + ), + SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 16), + itemCount: mockUserPosts.length, + itemBuilder: (context, index) { + final post = mockUserPosts[index]; + return Container( + margin: EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 16, + backgroundColor: Color(0xFF6A4C93), + child: Text( + (userData!['displayName'] ?? 'U').substring(0, 1).toUpperCase(), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userData!['displayName'] ?? 'Unknown User', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.black87, + ), + ), + Text( + post['timestamp'], + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + SizedBox(height: 12), + Text( + post['content'], + style: TextStyle( + fontSize: 15, + height: 1.4, + color: Colors.black87, + ), + ), + SizedBox(height: 12), + Row( + children: [ + Icon( + post['isLiked'] + ? Icons.favorite + : Icons.favorite_border, + color: post['isLiked'] + ? Colors.red + : Colors.grey[600], + size: 20, + ), + SizedBox(width: 4), + Text( + '${post['likes']}', + style: TextStyle( + color: Colors.grey[700], + fontSize: 14, + ), + ), + SizedBox(width: 16), + Icon( + Icons.chat_bubble_outline, + color: Colors.grey[600], + size: 20, + ), + SizedBox(width: 4), + Text( + '${post['comments']}', + style: TextStyle( + color: Colors.grey[700], + fontSize: 14, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ], SizedBox(height: 24), ], ), @@ -525,4 +598,4 @@ class _SettingsPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart new file mode 100644 index 0000000..3711a66 --- /dev/null +++ b/frontend/lib/services/auth_service.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:http/http.dart' as http; +import '../constants/api_constants.dart'; + +class AuthService { + static const FlutterSecureStorage _storage = FlutterSecureStorage(); + static const String _tokenKey = 'jwt_token'; + + static Future> login( + String emailOrUsername, + String password, + ) async { + try { + final response = await http.post( + Uri.parse('${ApiConstants.baseUrl}${ApiConstants.loginEndpoint}'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'emailOrUsername': emailOrUsername, + 'password': password, + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final token = data['token']; + + if (token != null) { + await _storage.write(key: _tokenKey, value: token); + return {'success': true}; + } + return {'success': false, 'message': 'No token received'}; + } else if (response.statusCode == 403 || response.statusCode == 400) { + return {'success': false, 'message': 'Invalid credentials'}; + } else if (response.statusCode == 401) { + return {'success': false, 'message': 'Invalid credentials'}; + } else { + return { + 'success': false, + 'message': 'Server error (${response.statusCode})', + }; + } + } catch (e) { + print('Login error: $e'); + return { + 'success': false, + 'message': 'Network error. Please check your connection.', + }; + } + } + + static Future getToken() async { + return await _storage.read(key: _tokenKey); + } + + static Future isLoggedIn() async { + final token = await getToken(); + return token != null && token.isNotEmpty; + } + + static Future logout() async { + await _storage.delete(key: _tokenKey); + } + + static Future> getAuthHeaders() async { + final token = await getToken(); + return { + 'Content-Type': 'application/json', + if (token != null) 'Authorization': 'Bearer $token', + }; + } +} diff --git a/frontend/lib/services/http_service.dart b/frontend/lib/services/http_service.dart new file mode 100644 index 0000000..7409501 --- /dev/null +++ b/frontend/lib/services/http_service.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../constants/api_constants.dart'; +import 'auth_service.dart'; + +class HttpService { + static Future get(String endpoint) async { + final headers = await AuthService.getAuthHeaders(); + return await http.get( + Uri.parse('${ApiConstants.baseUrl}$endpoint'), + headers: headers, + ); + } + + static Future post(String endpoint, Map body) async { + final headers = await AuthService.getAuthHeaders(); + return await http.post( + Uri.parse('${ApiConstants.baseUrl}$endpoint'), + headers: headers, + body: jsonEncode(body), + ); + } + + static Future put(String endpoint, Map body) async { + final headers = await AuthService.getAuthHeaders(); + return await http.put( + Uri.parse('${ApiConstants.baseUrl}$endpoint'), + headers: headers, + body: jsonEncode(body), + ); + } + + static Future delete(String endpoint) async { + final headers = await AuthService.getAuthHeaders(); + return await http.delete( + Uri.parse('${ApiConstants.baseUrl}$endpoint'), + headers: headers, + ); + } +} \ No newline at end of file diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart new file mode 100644 index 0000000..0904f8d --- /dev/null +++ b/frontend/lib/services/user_service.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; +import '../constants/api_constants.dart'; +import 'http_service.dart'; + +class UserService { + static Future> getCurrentUser() async { + try { + final response = await HttpService.get(ApiConstants.getUserEndpoint); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return {'success': true, 'data': data}; + } else if (response.statusCode == 401) { + return {'success': false, 'message': 'Session expired. Please login again.'}; + } else if (response.statusCode == 403) { + return {'success': false, 'message': 'Access denied. Invalid credentials.'}; + } else { + return {'success': false, 'message': 'Server error (${response.statusCode})'}; + } + } catch (e) { + print('Error fetching user: $e'); + return {'success': false, 'message': 'Network error. Please check your connection.'}; + } + } +} \ No newline at end of file diff --git a/frontend/linux/flutter/generated_plugin_registrant.cc b/frontend/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/frontend/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/frontend/linux/flutter/generated_plugins.cmake b/frontend/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/frontend/linux/flutter/generated_plugins.cmake +++ b/frontend/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 9ad992d..c220951 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,12 @@ import Foundation import firebase_core import firebase_messaging +import flutter_secure_storage_macos +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 2d5126b..3c6f560 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" firebase_core: dependency: "direct main" description: @@ -142,6 +150,54 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -184,6 +240,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -248,6 +312,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -341,6 +461,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.27.0" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index d128e2c..98ec267 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: firebase_messaging: ^15.2.9 http: ^1.1.0 googleapis_auth: ^1.6.0 + flutter_secure_storage: ^9.2.2 dev_dependencies: flutter_test: diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index 1a82e7d..39cedd3 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake index fa8a39b..1f5d05f 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 0c7298e6260f65d43d8f629baa5a3506d24ebcd7 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Mon, 21 Jul 2025 09:53:30 +0300 Subject: [PATCH 2/7] feat: skip main page if authenticatd already --- frontend/lib/main.dart | 88 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 313a62e..04a03c2 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -4,6 +4,7 @@ import 'firebase_options.dart'; import 'screens/home_screen.dart'; import 'services/notification_service.dart'; import 'services/auth_service.dart'; +import 'services/user_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -18,12 +19,95 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Wesal', theme: ThemeData(primarySwatch: Colors.blue, fontFamily: 'Roboto'), - home: LandingPage(), + home: SplashScreen(), debugShowCheckedModeBanner: false, ); } } +class SplashScreen extends StatefulWidget { + @override + _SplashScreenState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + _checkAuthenticationStatus(); + } + + Future _checkAuthenticationStatus() async { + await Future.delayed(Duration(milliseconds: 500)); + + final isLoggedIn = await AuthService.isLoggedIn(); + + if (isLoggedIn) { + final userResult = await UserService.getCurrentUser(); + if (userResult['success'] == true) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => HomeScreen()), + ); + } else { + await AuthService.logout(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => LandingPage()), + ); + } + } else { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => LandingPage()), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF32B0A5), + Color(0xFF4600B9), + ], + stops: [0.0, 0.5], + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'وصال', + style: TextStyle( + fontSize: 160, + fontWeight: FontWeight.w200, + fontFamily: 'Blaka', + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(2, 2), + blurRadius: 4, + color: Colors.black.withOpacity(0.3), + ), + ], + ), + ), + SizedBox(height: 40), + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ], + ), + ), + ), + ); + } +} + class LandingPage extends StatefulWidget { @override _LandingPageState createState() => _LandingPageState(); @@ -251,6 +335,8 @@ class _SignInPageState extends State { }); if (result['success'] == true) { + final userResult = await UserService.getCurrentUser(forceRefresh: true); + Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => HomeScreen()), ); From 5870b29a9b25d95cfdbd71aa443c8d50ea125f61 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Mon, 21 Jul 2025 09:54:20 +0300 Subject: [PATCH 3/7] feat: cache the user profile information for performance --- frontend/lib/services/auth_service.dart | 18 ++++++++++++++++++ frontend/lib/services/user_service.dart | 11 ++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart index 3711a66..c945135 100644 --- a/frontend/lib/services/auth_service.dart +++ b/frontend/lib/services/auth_service.dart @@ -6,6 +6,7 @@ import '../constants/api_constants.dart'; class AuthService { static const FlutterSecureStorage _storage = FlutterSecureStorage(); static const String _tokenKey = 'jwt_token'; + static const String _userDataKey = 'user_data'; static Future> login( String emailOrUsername, @@ -60,6 +61,7 @@ class AuthService { static Future logout() async { await _storage.delete(key: _tokenKey); + await _storage.delete(key: _userDataKey); } static Future> getAuthHeaders() async { @@ -69,4 +71,20 @@ class AuthService { if (token != null) 'Authorization': 'Bearer $token', }; } + + static Future saveUserData(Map userData) async { + await _storage.write(key: _userDataKey, value: jsonEncode(userData)); + } + + static Future?> getCachedUserData() async { + final userData = await _storage.read(key: _userDataKey); + if (userData != null) { + return jsonDecode(userData); + } + return null; + } + + static Future clearUserData() async { + await _storage.delete(key: _userDataKey); + } } diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index 0904f8d..00bde84 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -1,14 +1,23 @@ import 'dart:convert'; import '../constants/api_constants.dart'; import 'http_service.dart'; +import 'auth_service.dart'; class UserService { - static Future> getCurrentUser() async { + static Future> getCurrentUser({bool forceRefresh = false}) async { + if (!forceRefresh) { + final cachedData = await AuthService.getCachedUserData(); + if (cachedData != null) { + return {'success': true, 'data': cachedData}; + } + } + try { final response = await HttpService.get(ApiConstants.getUserEndpoint); if (response.statusCode == 200) { final data = jsonDecode(response.body); + await AuthService.saveUserData(data); return {'success': true, 'data': data}; } else if (response.statusCode == 401) { return {'success': false, 'message': 'Session expired. Please login again.'}; From eb90aa15acada189927e3c0d9e5211e62e4a9d29 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Mon, 21 Jul 2025 10:11:46 +0300 Subject: [PATCH 4/7] feat: implement sign out button --- frontend/lib/screens/pages/profile_page.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/lib/screens/pages/profile_page.dart b/frontend/lib/screens/pages/profile_page.dart index 6cd5dbc..b08beaf 100644 --- a/frontend/lib/screens/pages/profile_page.dart +++ b/frontend/lib/screens/pages/profile_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../services/notification_service.dart'; import '../../services/user_service.dart'; +import '../../services/auth_service.dart'; class ProfilePage extends StatefulWidget { @override @@ -449,7 +450,7 @@ class _SettingsPageState extends State { } } - void _signOut() { + void _signOut() async { showDialog( context: context, builder: (BuildContext context) { @@ -462,10 +463,14 @@ class _SettingsPageState extends State { child: Text('Cancel'), ), TextButton( - onPressed: () { + onPressed: () async { Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Signed out successfully')), + + await AuthService.logout(); + + Navigator.of(context).pushNamedAndRemoveUntil( + '/', + (route) => false, ); }, child: Text('Sign Out', style: TextStyle(color: Colors.red)), From 0905a51ec5bf5d26167a670b1876297c8abaa581 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Mon, 21 Jul 2025 10:12:18 +0300 Subject: [PATCH 5/7] feat: add endpoint to update user information --- .../wesal/controller/AuthController.java | 17 +++++++ .../wesal/wesal/dto/UpdateUserRequest.java | 49 +++++++++++++++++++ .../wesal/wesal/service/UserService.java | 22 +++++++++ 3 files changed, 88 insertions(+) create mode 100644 backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java diff --git a/backend/src/main/java/online/wesal/wesal/controller/AuthController.java b/backend/src/main/java/online/wesal/wesal/controller/AuthController.java index 632d7a8..2479b64 100644 --- a/backend/src/main/java/online/wesal/wesal/controller/AuthController.java +++ b/backend/src/main/java/online/wesal/wesal/controller/AuthController.java @@ -6,6 +6,7 @@ import jakarta.validation.Valid; import online.wesal.wesal.dto.CreateUserRequest; import online.wesal.wesal.dto.LoginRequest; import online.wesal.wesal.dto.LoginResponse; +import online.wesal.wesal.dto.UpdateUserRequest; import online.wesal.wesal.dto.UsernameSelectionRequest; import online.wesal.wesal.entity.User; import online.wesal.wesal.service.AuthService; @@ -67,6 +68,22 @@ public class AuthController { } } + @PostMapping("/updateUser") + @Operation(summary = "Update user information", description = "Update authenticated user's fcmToken, displayName, avatar, or password") + public ResponseEntity> updateUser(@Valid @RequestBody UpdateUserRequest request) { + try { + User user = userService.updateUser( + request.getFcmToken(), + request.getDisplayName(), + request.getAvatar(), + request.getPassword() + ); + return ResponseEntity.ok(Map.of("status", 200, "user", user)); + } catch (RuntimeException e) { + return ResponseEntity.badRequest().body(Map.of("status", 400, "message", e.getMessage())); + } + } + @PostMapping("/admin/createUser") @Operation(summary = "Create new user (Admin only)", description = "Creates a new user - requires admin privileges") public ResponseEntity createUser(@Valid @RequestBody CreateUserRequest request) { diff --git a/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java b/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java new file mode 100644 index 0000000..1f5b171 --- /dev/null +++ b/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java @@ -0,0 +1,49 @@ +package online.wesal.wesal.dto; + +import jakarta.validation.constraints.Size; + +public class UpdateUserRequest { + + private String fcmToken; + + private String displayName; + + private String avatar; + + @Size(min = 8, message = "Password must be at least 8 characters long") + private String password; + + public UpdateUserRequest() {} + + public String getFcmToken() { + return fcmToken; + } + + public void setFcmToken(String fcmToken) { + this.fcmToken = fcmToken; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/service/UserService.java b/backend/src/main/java/online/wesal/wesal/service/UserService.java index 9a0e790..9dd0f91 100644 --- a/backend/src/main/java/online/wesal/wesal/service/UserService.java +++ b/backend/src/main/java/online/wesal/wesal/service/UserService.java @@ -55,4 +55,26 @@ public class UserService { User user = new User(email, passwordEncoder.encode(password), displayName); return userRepository.save(user); } + + public User updateUser(String fcmToken, String displayName, String avatar, String password) { + User user = getCurrentUser(); + + if (fcmToken != null && !fcmToken.trim().isEmpty()) { + user.setFcmToken(fcmToken); + } + + if (displayName != null && !displayName.trim().isEmpty()) { + user.setDisplayName(displayName); + } + + if (avatar != null) { + user.setAvatar(avatar); + } + + if (password != null && !password.trim().isEmpty()) { + user.setPassword(passwordEncoder.encode(password)); + } + + return userRepository.save(user); + } } \ No newline at end of file From 87c4072d92fdf63e889b43cab62536a89632e470 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Mon, 21 Jul 2025 10:12:48 +0300 Subject: [PATCH 6/7] feat: add permissions screen after login and send token to server --- frontend/lib/constants/api_constants.dart | 1 + frontend/lib/main.dart | 6 +- .../notification_permission_screen.dart | 242 ++++++++++++++++++ frontend/lib/services/user_service.dart | 61 ++++- 4 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 frontend/lib/screens/notification_permission_screen.dart diff --git a/frontend/lib/constants/api_constants.dart b/frontend/lib/constants/api_constants.dart index 1f91e4c..7b4f6ad 100644 --- a/frontend/lib/constants/api_constants.dart +++ b/frontend/lib/constants/api_constants.dart @@ -6,4 +6,5 @@ class ApiConstants { // User endpoints static const String getUserEndpoint = '/getUser'; + static const String updateUserEndpoint = '/updateUser'; } \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 04a03c2..d30af18 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; import 'screens/home_screen.dart'; +import 'screens/notification_permission_screen.dart'; import 'services/notification_service.dart'; import 'services/auth_service.dart'; import 'services/user_service.dart'; @@ -115,9 +116,6 @@ class LandingPage extends StatefulWidget { class _LandingPageState extends State { void _navigateDirectlyToLogin() { - // Request notification permissions on user action - NotificationService().requestPermissionAndSetup(); - Navigator.of( context, ).push(MaterialPageRoute(builder: (context) => SignInPage())); @@ -338,7 +336,7 @@ class _SignInPageState extends State { final userResult = await UserService.getCurrentUser(forceRefresh: true); Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => HomeScreen()), + MaterialPageRoute(builder: (context) => NotificationPermissionScreen()), ); } else { _showErrorAlert(result['message'] ?? 'Login failed'); diff --git a/frontend/lib/screens/notification_permission_screen.dart b/frontend/lib/screens/notification_permission_screen.dart new file mode 100644 index 0000000..3f5e2f5 --- /dev/null +++ b/frontend/lib/screens/notification_permission_screen.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import '../services/notification_service.dart'; +import '../services/user_service.dart'; +import 'home_screen.dart'; + +class NotificationPermissionScreen extends StatefulWidget { + @override + _NotificationPermissionScreenState createState() => + _NotificationPermissionScreenState(); +} + +class _NotificationPermissionScreenState + extends State { + bool _isLoading = false; + + Future _requestNotificationPermission() async { + setState(() { + _isLoading = true; + }); + + try { + final notificationService = NotificationService(); + await notificationService.requestPermissionAndSetup(); + + final fcmToken = await notificationService.getToken(); + + if (fcmToken != null) { + final result = await UserService.updateFCMToken(fcmToken); + + if (result['success'] == true) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => HomeScreen()), + ); + } else { + _showErrorAndContinue( + result['message'] ?? 'Failed to update notification settings', + ); + } + } else { + _showErrorAndContinue('Failed to get notification token'); + } + } catch (e) { + _showErrorAndContinue('Error setting up notifications: $e'); + } + + setState(() { + _isLoading = false; + }); + } + + void _showErrorAndContinue(String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Notification Setup'), + content: Text( + '$message\n\nYou can enable notifications later in settings.', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => HomeScreen()), + ); + }, + child: Text('Continue', style: TextStyle(color: Color(0xFF6A4C93))), + ), + ], + ), + ); + } + + void _skipNotifications() { + Navigator.of( + context, + ).pushReplacement(MaterialPageRoute(builder: (context) => HomeScreen())); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], + stops: [0.0, 0.5], + ), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + Icons.rocket_launch, + size: 100, + color: Colors.white, + ), + ), + + SizedBox(height: 40), + + Text( + 'All Set!', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + + SizedBox(height: 16), + + Text( + 'But one last thing...', + style: TextStyle( + fontSize: 18, + color: Colors.white.withOpacity(0.9), + ), + ), + + SizedBox(height: 40), + + Container( + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Column( + children: [ + Icon( + Icons.notifications_active, + size: 40, + color: Colors.white, + ), + SizedBox(height: 16), + Text( + 'Stay Connected', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + SizedBox(height: 8), + Text( + 'We will only send you updates on your own invitations or posts. You can always change your notification settings later.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.white.withOpacity(0.8), + height: 1.4, + ), + ), + ], + ), + ), + ], + ), + ), + + Column( + children: [ + Container( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isLoading + ? null + : _requestNotificationPermission, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Color(0xFF6A4C93), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + ), + child: _isLoading + ? CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xFF6A4C93), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.notifications, size: 20), + SizedBox(width: 8), + Text( + 'Enable Notifications', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + + SizedBox(height: 16), + + TextButton( + onPressed: _isLoading ? null : _skipNotifications, + child: Text( + 'Skip for now', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 16, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index 00bde84..6793c47 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -4,7 +4,9 @@ import 'http_service.dart'; import 'auth_service.dart'; class UserService { - static Future> getCurrentUser({bool forceRefresh = false}) async { + static Future> getCurrentUser({ + bool forceRefresh = false, + }) async { if (!forceRefresh) { final cachedData = await AuthService.getCachedUserData(); if (cachedData != null) { @@ -14,21 +16,66 @@ class UserService { try { final response = await HttpService.get(ApiConstants.getUserEndpoint); - + if (response.statusCode == 200) { final data = jsonDecode(response.body); await AuthService.saveUserData(data); return {'success': true, 'data': data}; } else if (response.statusCode == 401) { - return {'success': false, 'message': 'Session expired. Please login again.'}; + return { + 'success': false, + 'message': 'Session expired. Please login again.', + }; } else if (response.statusCode == 403) { - return {'success': false, 'message': 'Access denied. Invalid credentials.'}; + return { + 'success': false, + 'message': 'Access denied. Invalid credentials.', + }; } else { - return {'success': false, 'message': 'Server error (${response.statusCode})'}; + return { + 'success': false, + 'message': 'Server error (${response.statusCode})', + }; } } catch (e) { print('Error fetching user: $e'); - return {'success': false, 'message': 'Network error. Please check your connection.'}; + return { + 'success': false, + 'message': 'Network error. Please check your connection.', + }; } } -} \ No newline at end of file + + static Future> updateFCMToken(String fcmToken) async { + try { + final response = await HttpService.post(ApiConstants.updateUserEndpoint, { + 'fcmToken': fcmToken, + }); + + if (response.statusCode == 200) { + return {'success': true}; + } else if (response.statusCode == 401) { + return { + 'success': false, + 'message': 'Session expired. Please login again.', + }; + } else if (response.statusCode == 403) { + return { + 'success': false, + 'message': 'Access denied. Invalid credentials.', + }; + } else { + return { + 'success': false, + 'message': 'Server error (${response.statusCode})', + }; + } + } catch (e) { + print('Error updating FCM token: $e'); + return { + 'success': false, + 'message': 'Network error. Please check your connection.', + }; + } + } +} From fa626992dc4d1751418d6173db6e493a60fa5e54 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Mon, 21 Jul 2025 10:17:12 +0300 Subject: [PATCH 7/7] feat: delete back button from main home screens --- frontend/lib/screens/pages/feed_page.dart | 1 + .../lib/screens/pages/invitations_page.dart | 5 ++- frontend/lib/screens/pages/profile_page.dart | 41 ++++++++++--------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/frontend/lib/screens/pages/feed_page.dart b/frontend/lib/screens/pages/feed_page.dart index a380c4b..855bd7b 100644 --- a/frontend/lib/screens/pages/feed_page.dart +++ b/frontend/lib/screens/pages/feed_page.dart @@ -76,6 +76,7 @@ class _FeedPageState extends State { preferredSize: Size.fromHeight(1), child: Container(height: 1, color: Colors.grey[200]), ), + automaticallyImplyLeading: false, ), body: RefreshIndicator( onRefresh: () async { diff --git a/frontend/lib/screens/pages/invitations_page.dart b/frontend/lib/screens/pages/invitations_page.dart index 80d8680..a4e6367 100644 --- a/frontend/lib/screens/pages/invitations_page.dart +++ b/frontend/lib/screens/pages/invitations_page.dart @@ -79,6 +79,7 @@ class _InvitationsPageState extends State { preferredSize: Size.fromHeight(1), child: Container(height: 1, color: Colors.grey[200]), ), + automaticallyImplyLeading: false, ), body: Padding( padding: EdgeInsets.all(16), @@ -256,7 +257,9 @@ class _InvitationsPageState extends State { floatingActionButton: FloatingActionButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Create invitation functionality coming soon!')), + SnackBar( + content: Text('Create invitation functionality coming soon!'), + ), ); }, backgroundColor: Color(0xFF6A4C93), diff --git a/frontend/lib/screens/pages/profile_page.dart b/frontend/lib/screens/pages/profile_page.dart index b08beaf..d0a42ab 100644 --- a/frontend/lib/screens/pages/profile_page.dart +++ b/frontend/lib/screens/pages/profile_page.dart @@ -86,7 +86,7 @@ class _ProfilePageState extends State { }); final result = await UserService.getCurrentUser(); - + setState(() { isLoadingUser = false; if (result['success'] == true) { @@ -107,10 +107,7 @@ class _ProfilePageState extends State { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text( - 'OK', - style: TextStyle(color: Color(0xFF6A4C93)), - ), + child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))), ), ], ), @@ -163,14 +160,18 @@ class _ProfilePageState extends State { children: [ if (isLoadingUser) CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFF6A4C93)), + valueColor: AlwaysStoppedAnimation( + Color(0xFF6A4C93), + ), ) else if (userData != null) ...[ CircleAvatar( radius: 50, backgroundColor: Color(0xFF6A4C93), child: Text( - (userData!['displayName'] ?? 'U').substring(0, 1).toUpperCase(), + (userData!['displayName'] ?? 'U') + .substring(0, 1) + .toUpperCase(), style: TextStyle( color: Colors.white, fontSize: 36, @@ -201,10 +202,7 @@ class _ProfilePageState extends State { SizedBox(height: 16), Text( 'Failed to load user data', - style: TextStyle( - fontSize: 18, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 18, color: Colors.grey[600]), ), SizedBox(height: 8), ElevatedButton( @@ -302,7 +300,9 @@ class _ProfilePageState extends State { radius: 16, backgroundColor: Color(0xFF6A4C93), child: Text( - (userData!['displayName'] ?? 'U').substring(0, 1).toUpperCase(), + (userData!['displayName'] ?? 'U') + .substring(0, 1) + .toUpperCase(), style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -316,7 +316,8 @@ class _ProfilePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - userData!['displayName'] ?? 'Unknown User', + userData!['displayName'] ?? + 'Unknown User', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, @@ -465,13 +466,12 @@ class _SettingsPageState extends State { TextButton( onPressed: () async { Navigator.of(context).pop(); - + await AuthService.logout(); - - Navigator.of(context).pushNamedAndRemoveUntil( - '/', - (route) => false, - ); + + Navigator.of( + context, + ).pushNamedAndRemoveUntil('/', (route) => false); }, child: Text('Sign Out', style: TextStyle(color: Colors.red)), ), @@ -493,6 +493,7 @@ class _SettingsPageState extends State { preferredSize: Size.fromHeight(1), child: Container(height: 1, color: Colors.grey[200]), ), + automaticallyImplyLeading: false, ), body: Padding( padding: EdgeInsets.all(16), @@ -603,4 +604,4 @@ class _SettingsPageState extends State { ), ); } -} \ No newline at end of file +}