diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 43ff9ec..f28a2ee 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -3,7 +3,7 @@ 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 'screens/edit_profile_screen.dart'; import 'services/auth_service.dart'; import 'services/user_service.dart'; import 'services/app_lifecycle_service.dart'; @@ -53,11 +53,25 @@ class _SplashScreenState extends State { if (isLoggedIn) { final userResult = await UserService.getCurrentUser(); if (userResult['success'] == true) { - // Start polling services now that user is logged in and going to main screen - AppLifecycleService.startAllPolling(); - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => HomeScreen()), - ); + final userData = userResult['data']; + + // Check if user needs onboarding (activated = 0) + if (userData['activated'] == 0) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => EditProfileScreen( + userData: userData, + isOnboarding: true, + ), + ), + ); + } else { + // Start polling services now that user is logged in and going to main screen + AppLifecycleService.startAllPolling(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => HomeScreen()), + ); + } } else { await AuthService.logout(); Navigator.of(context).pushReplacement( @@ -344,14 +358,32 @@ class _SignInPageState extends State { if (result['success'] == true) { final userResult = await UserService.getCurrentUser(forceRefresh: true); - // Start polling services now that user is logged in - AppLifecycleService.startAllPolling(); + if (userResult['success'] == true) { + final userData = userResult['data']; + + // Check if user needs onboarding (activated = 0) + if (userData['activated'] == 0) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => EditProfileScreen( + userData: userData, + isOnboarding: true, + ), + ), + ); + } else { + // Start polling services now that user is logged in + AppLifecycleService.startAllPolling(); - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => NotificationPermissionScreen(), - ), - ); + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => NotificationPermissionScreen(), + ), + ); + } + } else { + _showErrorAlert('Failed to load user data'); + } } else { _showErrorAlert(result['message'] ?? 'Login failed'); } diff --git a/frontend/lib/screens/edit_profile_screen.dart b/frontend/lib/screens/edit_profile_screen.dart new file mode 100644 index 0000000..c86a945 --- /dev/null +++ b/frontend/lib/screens/edit_profile_screen.dart @@ -0,0 +1,428 @@ +import 'package:flutter/material.dart'; +import '../services/user_service.dart'; +import '../services/app_lifecycle_service.dart'; +import 'notification_permission_screen.dart'; + +class EditProfileScreen extends StatefulWidget { + final Map? userData; + final bool isOnboarding; + + const EditProfileScreen({ + Key? key, + this.userData, + this.isOnboarding = false, + }) : super(key: key); + + @override + _EditProfileScreenState createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends State { + final _formKey = GlobalKey(); + final TextEditingController _displayNameController = TextEditingController(); + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _initializeFields(); + } + + void _initializeFields() { + if (widget.userData != null) { + _displayNameController.text = widget.userData!['displayName'] ?? ''; + _emailController.text = widget.userData!['email'] ?? ''; + if (!widget.isOnboarding) { + _usernameController.text = widget.userData!['username'] ?? ''; + } + } + } + + @override + void dispose() { + _displayNameController.dispose(); + _usernameController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + String? _validateDisplayName(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Display name is required'; + } + if (value.trim().length < 2) { + return 'Display name must be at least 2 characters'; + } + if (value.trim().length > 50) { + return 'Display name cannot exceed 50 characters'; + } + return null; + } + + String? _validateUsername(String? value) { + if (widget.isOnboarding) { + if (value == null || value.trim().isEmpty) { + return 'Username is required'; + } + } + if (value != null && value.trim().isNotEmpty) { + if (value.trim().length < 3) { + return 'Username must be at least 3 characters'; + } + if (value.trim().length > 30) { + return 'Username cannot exceed 30 characters'; + } + // Basic username validation + if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value.trim())) { + return 'Username can only contain letters, numbers, and underscores'; + } + } + return null; + } + + Future _saveProfile() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final Map updateData = { + 'displayName': _displayNameController.text.trim(), + }; + + if (widget.isOnboarding || _usernameController.text.trim().isNotEmpty) { + updateData['username'] = _usernameController.text.trim(); + } + + final result = await UserService.updateUser( + displayName: updateData['displayName'], + username: updateData['username'], + ); + + if (mounted) { + setState(() { + _isLoading = false; + }); + + if (result['success'] == true) { + _showSuccessMessage(result['message'] ?? 'Profile updated successfully'); + + if (widget.isOnboarding) { + // Start polling services now that user has completed onboarding + AppLifecycleService.startAllPolling(); + + // Navigate to notification permission screen after successful onboarding + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => NotificationPermissionScreen()), + ); + } else { + // Go back to profile page + Navigator.of(context).pop(true); // Return true to indicate update + } + } else { + _showErrorMessage(result['message'] ?? 'Failed to update profile'); + } + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + _showErrorMessage('An error occurred. Please try again.'); + } + } + } + + void _showSuccessMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Color(0xFF6A4C93), + behavior: SnackBarBehavior.floating, + ), + ); + } + + void _showErrorMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + widget.isOnboarding ? 'Setup Profile' : 'Edit Profile', + style: TextStyle(fontWeight: FontWeight.w600), + ), + backgroundColor: Colors.white, + foregroundColor: Color(0xFF6A4C93), + elevation: 0, + bottom: PreferredSize( + preferredSize: Size.fromHeight(1), + child: Container(height: 1, color: Colors.grey[200]), + ), + automaticallyImplyLeading: !widget.isOnboarding, + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF32B0A5), + Color(0xFF4600B9), + ], + ), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.isOnboarding) ...[ + SizedBox(height: 20), + Center( + child: Column( + children: [ + Text( + 'وصال', + style: TextStyle( + fontFamily: 'Blaka', + fontSize: 48, + fontWeight: FontWeight.w200, + color: Colors.white, + ), + ), + SizedBox(height: 8), + Text( + 'Complete your profile setup', + style: TextStyle( + fontSize: 18, + color: Colors.white.withOpacity(0.9), + ), + ), + ], + ), + ), + SizedBox(height: 40), + ] else ...[ + SizedBox(height: 20), + ], + + Container( + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 0, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Profile Information', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Color(0xFF6A4C93), + ), + ), + SizedBox(height: 24), + + // Display Name Field + Text( + 'Display Name', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + SizedBox(height: 8), + TextFormField( + controller: _displayNameController, + validator: _validateDisplayName, + decoration: InputDecoration( + hintText: 'Enter your display name', + prefixIcon: Icon(Icons.person, color: Color(0xFF6A4C93)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[300]!), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Color(0xFF6A4C93), width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.red, width: 2), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + ), + SizedBox(height: 20), + + // Email Field (Read-only) + Text( + 'Email', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + SizedBox(height: 8), + TextFormField( + controller: _emailController, + readOnly: true, + decoration: InputDecoration( + hintText: 'Email address', + prefixIcon: Icon(Icons.email, color: Colors.grey[400]), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[300]!), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[300]!), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[300]!), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[300]!), + ), + filled: true, + fillColor: Colors.grey[50], + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + style: TextStyle(color: Colors.grey[600]), + ), + SizedBox(height: 8), + Text( + 'Email cannot be changed', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + SizedBox(height: 20), + + // Username Field + Text( + 'Username${widget.isOnboarding ? ' *' : ''}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + SizedBox(height: 8), + TextFormField( + controller: _usernameController, + validator: _validateUsername, + decoration: InputDecoration( + hintText: widget.isOnboarding + ? 'Choose a username' + : 'Enter your username (optional)', + prefixIcon: Icon(Icons.alternate_email, color: Color(0xFF6A4C93)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[300]!), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Color(0xFF6A4C93), width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.red, width: 2), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + ), + SizedBox(height: 8), + Text( + 'Username can contain letters, numbers, and underscores only', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + + SizedBox(height: 24), + Container( + width: double.infinity, + child: ElevatedButton( + onPressed: _isLoading ? null : _saveProfile, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Color(0xFF6A4C93), + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: _isLoading + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Color(0xFF6A4C93)), + ), + ) + : Text( + widget.isOnboarding ? 'Complete Setup' : 'Save Changes', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/notification_permission_screen.dart b/frontend/lib/screens/notification_permission_screen.dart index 3f0cffc..5a54bc9 100644 --- a/frontend/lib/screens/notification_permission_screen.dart +++ b/frontend/lib/screens/notification_permission_screen.dart @@ -80,165 +80,166 @@ class _NotificationPermissionScreenState @override Widget build(BuildContext context) { return PopScope( - canPop: false, // Prevent back navigation from notification permission screen + canPop: + false, // Prevent back navigation from notification permission screen child: Scaffold( - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], - stops: [0.0, 0.5], + 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, + 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: 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, + 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), - 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), + TextButton( + onPressed: _isLoading ? null : _skipNotifications, + child: Text( + 'Skip for now', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 16, + decoration: TextDecoration.underline, ), ), - 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/screens/pages/profile_page.dart b/frontend/lib/screens/pages/profile_page.dart index 12c0dbb..d5e5116 100644 --- a/frontend/lib/screens/pages/profile_page.dart +++ b/frontend/lib/screens/pages/profile_page.dart @@ -9,6 +9,9 @@ import '../../services/app_lifecycle_service.dart'; import '../../models/post_models.dart'; import '../../widgets/posts_list.dart'; import '../../utils/password_generator.dart'; +import '../edit_profile_screen.dart'; +import '../support_screen.dart'; +import '../information_screen.dart'; class ProfilePage extends StatefulWidget { @override @@ -168,6 +171,19 @@ class _ProfilePageState extends State { ).push(MaterialPageRoute(builder: (context) => SettingsPage())); } + void _navigateToEditProfile() async { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => EditProfileScreen(userData: userData), + ), + ); + + if (result == true) { + // Profile was updated, refresh user data + _loadUserData(forceRefresh: true); + } + } + @override Widget build(BuildContext context) { return PopScope( @@ -185,6 +201,12 @@ class _ProfilePageState extends State { foregroundColor: Color(0xFF6A4C93), elevation: 0, actions: [ + if (userData != null) + IconButton( + onPressed: _navigateToEditProfile, + icon: Icon(Icons.edit), + tooltip: 'Edit Profile', + ), IconButton( onPressed: _navigateToSettings, icon: Icon(Icons.settings), @@ -338,10 +360,6 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - String? fcmToken; - final TextEditingController _tokenController = TextEditingController(); - bool isLoading = false; - // Admin user creation Map? userData; bool isLoadingUser = true; @@ -353,52 +371,18 @@ class _SettingsPageState extends State { @override void initState() { super.initState(); - _loadFCMToken(); _loadUserData(); _generatePassword(); } @override void dispose() { - _tokenController.dispose(); _emailController.dispose(); _passwordController.dispose(); _displayNameController.dispose(); super.dispose(); } - Future _loadFCMToken() async { - setState(() { - isLoading = true; - }); - - try { - final token = await NotificationService().getToken(); - setState(() { - fcmToken = token; - _tokenController.text = token ?? 'Token not available'; - isLoading = false; - }); - } catch (e) { - setState(() { - fcmToken = null; - _tokenController.text = 'Error loading token: $e'; - isLoading = false; - }); - } - } - - Future _copyToClipboard() async { - if (fcmToken != null && fcmToken!.isNotEmpty) { - await Clipboard.setData(ClipboardData(text: fcmToken!)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('FCM Token copied to clipboard'), - backgroundColor: Color(0xFF6A4C93), - ), - ); - } - } Future _loadUserData() async { setState(() { @@ -715,17 +699,18 @@ class _SettingsPageState extends State { Container( width: double.infinity, + height: 56, child: ElevatedButton.icon( onPressed: () => _showCreateAccountDialog(context), - icon: Icon(Icons.person_add, size: 18), + icon: Icon(Icons.person_add, size: 20), label: Text('Create an Account'), style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF6A4C93), foregroundColor: Colors.white, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), ), - padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + padding: EdgeInsets.symmetric(horizontal: 16), ), ), ), @@ -734,7 +719,7 @@ class _SettingsPageState extends State { ], Text( - 'Development Tools', + 'App Information', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, @@ -743,92 +728,88 @@ class _SettingsPageState extends State { ), SizedBox(height: 16), - Text( - 'FCM Token:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black87, + Container( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => SupportScreen()), + ), + icon: Icon(Icons.help_outline, size: 20), + label: Text('Support'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.symmetric(horizontal: 16), + ), ), ), - SizedBox(height: 8), - if (isLoading) - Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFF6A4C93)), + SizedBox(height: 16), + + Container( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => InformationScreen()), ), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey[300]!), - borderRadius: BorderRadius.circular(8), - ), - child: TextField( - controller: _tokenController, - readOnly: true, - maxLines: 4, - style: TextStyle(fontSize: 12, fontFamily: 'monospace'), - decoration: InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.all(12), - hintText: 'FCM Token will appear here...', - ), - ), + icon: Icon(Icons.info_outline, size: 20), + label: Text('Information'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - SizedBox(height: 8), - - Row( - children: [ - ElevatedButton.icon( - onPressed: fcmToken != null ? _copyToClipboard : null, - icon: Icon(Icons.copy, size: 18), - label: Text('Copy Token'), - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFF6A4C93), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - SizedBox(width: 12), - OutlinedButton.icon( - onPressed: _loadFCMToken, - icon: Icon(Icons.refresh, size: 18), - label: Text('Refresh'), - style: OutlinedButton.styleFrom( - foregroundColor: Color(0xFF6A4C93), - side: BorderSide(color: Color(0xFF6A4C93)), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ], - ), - ], + padding: EdgeInsets.symmetric(horizontal: 16), + ), ), + ), SizedBox(height: 32), Container( width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => EditProfileScreen(userData: userData), + ), + ), + icon: Icon(Icons.edit, size: 20), + label: Text('Update your Profile'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.symmetric(horizontal: 16), + ), + ), + ), + + SizedBox(height: 16), + + Container( + width: double.infinity, + height: 56, child: ElevatedButton.icon( onPressed: _signOut, - icon: Icon(Icons.logout, color: Colors.red), + icon: Icon(Icons.logout, color: Colors.red, size: 20), label: Text('Sign Out', style: TextStyle(color: Colors.red)), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, side: BorderSide(color: Colors.red), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), ), - padding: EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric(horizontal: 16), ), ), ), diff --git a/frontend/lib/services/invitations_service.dart b/frontend/lib/services/invitations_service.dart index 8a0e0b7..648be1b 100644 --- a/frontend/lib/services/invitations_service.dart +++ b/frontend/lib/services/invitations_service.dart @@ -7,21 +7,32 @@ import 'cache_service.dart'; class InvitationsService { static bool _pollingStarted = false; + /// Get all invitations with caching - returns cached data if available and fresh - static Future> getAllInvitations({bool forceRefresh = false}) async { + static Future> getAllInvitations({ + bool forceRefresh = false, + }) async { // Return cached data if available and not forcing refresh if (!forceRefresh) { - final cached = CacheService.getCached(CacheService.invitationsAllKey); - if (cached != null && !CacheService.isCacheExpired(CacheService.invitationsAllKey, CacheService.invitationsCacheDuration)) { + final cached = CacheService.getCached( + CacheService.invitationsAllKey, + ); + if (cached != null && + !CacheService.isCacheExpired( + CacheService.invitationsAllKey, + CacheService.invitationsCacheDuration, + )) { return { 'success': true, 'message': 'Loaded from cache', 'invitations': cached, }; } - + // Return stale data while fetching fresh data in background - final stale = CacheService.getCachedStale(CacheService.invitationsAllKey); + final stale = CacheService.getCachedStale( + CacheService.invitationsAllKey, + ); if (stale != null && !forceRefresh) { // Trigger background refresh _fetchAndCacheInvitations(); @@ -37,7 +48,9 @@ class InvitationsService { return await _fetchAndCacheInvitations(); } - static Future> _fetchAndCacheInvitations([bool fromPolling = false]) async { + static Future> _fetchAndCacheInvitations([ + bool fromPolling = false, + ]) async { try { final response = await HttpService.get( ApiConstants.getAllInvitationsEndpoint, @@ -49,22 +62,30 @@ class InvitationsService { if (invitationsResponse.status) { final invitationsData = invitationsResponse.data; - + // Only cache when not called from polling to prevent conflicts if (!fromPolling) { - CacheService.setCached(CacheService.invitationsAllKey, invitationsData); + CacheService.setCached( + CacheService.invitationsAllKey, + invitationsData, + ); } return { - 'success': true, + 'success': true, 'invitations': invitationsData, 'message': invitationsResponse.message ?? '', }; } else { return { 'success': false, - 'message': invitationsResponse.message ?? 'Failed to fetch invitations', - 'invitations': InvitationsData(created: [], accepted: [], available: []), + 'message': + invitationsResponse.message ?? 'Failed to fetch invitations', + 'invitations': InvitationsData( + created: [], + accepted: [], + available: [], + ), }; } } else if (response.statusCode == 401 || response.statusCode == 403) { @@ -72,13 +93,21 @@ class InvitationsService { return { 'success': false, 'message': 'Session expired. Please login again.', - 'invitations': InvitationsData(created: [], accepted: [], available: []), + 'invitations': InvitationsData( + created: [], + accepted: [], + available: [], + ), }; } else { return { 'success': false, 'message': 'Server error (${response.statusCode})', - 'invitations': InvitationsData(created: [], accepted: [], available: []), + 'invitations': InvitationsData( + created: [], + accepted: [], + available: [], + ), }; } } catch (e) { @@ -86,7 +115,11 @@ class InvitationsService { return { 'success': false, 'message': 'Network error. Please check your connection.', - 'invitations': InvitationsData(created: [], accepted: [], available: []), + 'invitations': InvitationsData( + created: [], + accepted: [], + available: [], + ), }; } } @@ -110,7 +143,9 @@ class InvitationsService { /// Get invitations stream for real-time updates static Stream getInvitationsStream() { - return CacheService.getStream(CacheService.invitationsAllKey); + return CacheService.getStream( + CacheService.invitationsAllKey, + ); } /// Check if polling is active @@ -161,7 +196,9 @@ class InvitationsService { } } - static Future> createInvitation(Map invitationData) async { + static Future> createInvitation( + Map invitationData, + ) async { try { final response = await HttpService.post( ApiConstants.createInvitationEndpoint, @@ -209,7 +246,9 @@ class InvitationsService { } } - static Future> getInvitationDetails(int invitationId) async { + static Future> getInvitationDetails( + int invitationId, + ) async { try { final response = await HttpService.get( '${ApiConstants.invitationsEndpoint}/get?id=$invitationId', @@ -218,15 +257,15 @@ class InvitationsService { if (response.statusCode == 200) { final responseData = jsonDecode(response.body); if (responseData['status'] == true) { - final invitationDetails = InvitationDetails.fromJson(responseData['data']); - return { - 'success': true, - 'data': invitationDetails, - }; + final invitationDetails = InvitationDetails.fromJson( + responseData['data'], + ); + return {'success': true, 'data': invitationDetails}; } else { return { 'success': false, - 'message': responseData['message'] ?? 'Failed to get invitation details', + 'message': + responseData['message'] ?? 'Failed to get invitation details', }; } } else if (response.statusCode == 401) { diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index b97927a..d93cc9d 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -165,6 +165,61 @@ class UserService { } } + static Future> updateUser({ + String? displayName, + String? username, + String? avatar, + }) async { + try { + final Map updateData = {}; + if (displayName != null) updateData['displayName'] = displayName; + if (username != null) updateData['username'] = username; + if (avatar != null) updateData['avatar'] = avatar; + + final response = await HttpService.post(ApiConstants.updateUserEndpoint, updateData); + + if (response.statusCode == 200) { + final responseData = jsonDecode(response.body); + + if (responseData['status'] == true) { + // Update cache with new user data + CacheService.setCached(CacheService.userProfileKey, responseData['data']); + // Also save to AuthService for backward compatibility + await AuthService.saveUserData(responseData['data']); + + return { + 'success': true, + 'message': responseData['message'] ?? 'Profile updated successfully', + 'data': responseData['data'], + }; + } else { + return { + 'success': false, + 'message': responseData['message'] ?? 'Failed to update profile', + }; + } + } else if (response.statusCode == 401 || response.statusCode == 403) { + await AuthService.handleAuthenticationError(); + return { + 'success': false, + 'message': 'Session expired. Please login again.', + }; + } else { + final responseData = jsonDecode(response.body); + return { + 'success': false, + 'message': responseData['message'] ?? 'Server error (${response.statusCode})', + }; + } + } catch (e) { + print('Error updating user: $e'); + return { + 'success': false, + 'message': 'Network error. Please check your connection.', + }; + } + } + static Future> createUser({ required String email, required String password,