feat: Editing Profiles

This commit is contained in:
sBubshait 2025-08-03 10:06:34 +03:00
parent 7c2298df35
commit 481615260c
6 changed files with 823 additions and 287 deletions

View File

@ -3,7 +3,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'screens/home_screen.dart'; import 'screens/home_screen.dart';
import 'screens/notification_permission_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/auth_service.dart';
import 'services/user_service.dart'; import 'services/user_service.dart';
import 'services/app_lifecycle_service.dart'; import 'services/app_lifecycle_service.dart';
@ -53,11 +53,25 @@ class _SplashScreenState extends State<SplashScreen> {
if (isLoggedIn) { if (isLoggedIn) {
final userResult = await UserService.getCurrentUser(); final userResult = await UserService.getCurrentUser();
if (userResult['success'] == true) { if (userResult['success'] == true) {
// Start polling services now that user is logged in and going to main screen final userData = userResult['data'];
AppLifecycleService.startAllPolling();
Navigator.of(context).pushReplacement( // Check if user needs onboarding (activated = 0)
MaterialPageRoute(builder: (context) => HomeScreen()), 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 { } else {
await AuthService.logout(); await AuthService.logout();
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
@ -344,14 +358,32 @@ class _SignInPageState extends State<SignInPage> {
if (result['success'] == true) { if (result['success'] == true) {
final userResult = await UserService.getCurrentUser(forceRefresh: true); final userResult = await UserService.getCurrentUser(forceRefresh: true);
// Start polling services now that user is logged in if (userResult['success'] == true) {
AppLifecycleService.startAllPolling(); final userData = userResult['data'];
Navigator.of(context).pushReplacement( // Check if user needs onboarding (activated = 0)
MaterialPageRoute( if (userData['activated'] == 0) {
builder: (context) => NotificationPermissionScreen(), 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(),
),
);
}
} else {
_showErrorAlert('Failed to load user data');
}
} else { } else {
_showErrorAlert(result['message'] ?? 'Login failed'); _showErrorAlert(result['message'] ?? 'Login failed');
} }

View File

@ -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<String, dynamic>? userData;
final bool isOnboarding;
const EditProfileScreen({
Key? key,
this.userData,
this.isOnboarding = false,
}) : super(key: key);
@override
_EditProfileScreenState createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _formKey = GlobalKey<FormState>();
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<void> _saveProfile() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
final Map<String, dynamic> 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>(Color(0xFF6A4C93)),
),
)
: Text(
widget.isOnboarding ? 'Complete Setup' : 'Save Changes',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -80,165 +80,166 @@ class _NotificationPermissionScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopScope( return PopScope(
canPop: false, // Prevent back navigation from notification permission screen canPop:
false, // Prevent back navigation from notification permission screen
child: Scaffold( child: Scaffold(
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
stops: [0.0, 0.5], stops: [0.0, 0.5],
),
), ),
), child: SafeArea(
child: SafeArea( child: Padding(
child: Padding( padding: EdgeInsets.all(24),
padding: EdgeInsets.all(24), child: Column(
child: Column( children: [
children: [ Expanded(
Expanded( child: Column(
child: Column( mainAxisAlignment: MainAxisAlignment.center,
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: [ children: [
Container( Container(
width: 200, width: double.infinity,
height: 200, height: 56,
decoration: BoxDecoration( child: ElevatedButton(
color: Colors.white.withOpacity(0.2), onPressed: _isLoading
shape: BoxShape.circle, ? null
), : _requestNotificationPermission,
child: Icon( style: ElevatedButton.styleFrom(
Icons.rocket_launch, backgroundColor: Colors.white,
size: 100, foregroundColor: Color(0xFF6A4C93),
color: Colors.white, shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(12),
), ),
elevation: 4,
SizedBox(height: 40), ),
child: _isLoading
Text( ? CircularProgressIndicator(
'All Set!', valueColor: AlwaysStoppedAnimation<Color>(
style: TextStyle( Color(0xFF6A4C93),
fontSize: 32, ),
fontWeight: FontWeight.bold, )
color: Colors.white, : 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), SizedBox(height: 16),
Text( TextButton(
'But one last thing...', onPressed: _isLoading ? null : _skipNotifications,
style: TextStyle( child: Text(
fontSize: 18, 'Skip for now',
color: Colors.white.withOpacity(0.9), style: TextStyle(
), color: Colors.white.withOpacity(0.8),
), fontSize: 16,
decoration: TextDecoration.underline,
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>(
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,
),
),
),
],
),
],
), ),
), ),
), ),
),
), ),
); );
} }

View File

@ -9,6 +9,9 @@ import '../../services/app_lifecycle_service.dart';
import '../../models/post_models.dart'; import '../../models/post_models.dart';
import '../../widgets/posts_list.dart'; import '../../widgets/posts_list.dart';
import '../../utils/password_generator.dart'; import '../../utils/password_generator.dart';
import '../edit_profile_screen.dart';
import '../support_screen.dart';
import '../information_screen.dart';
class ProfilePage extends StatefulWidget { class ProfilePage extends StatefulWidget {
@override @override
@ -168,6 +171,19 @@ class _ProfilePageState extends State<ProfilePage> {
).push(MaterialPageRoute(builder: (context) => SettingsPage())); ).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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopScope( return PopScope(
@ -185,6 +201,12 @@ class _ProfilePageState extends State<ProfilePage> {
foregroundColor: Color(0xFF6A4C93), foregroundColor: Color(0xFF6A4C93),
elevation: 0, elevation: 0,
actions: [ actions: [
if (userData != null)
IconButton(
onPressed: _navigateToEditProfile,
icon: Icon(Icons.edit),
tooltip: 'Edit Profile',
),
IconButton( IconButton(
onPressed: _navigateToSettings, onPressed: _navigateToSettings,
icon: Icon(Icons.settings), icon: Icon(Icons.settings),
@ -338,10 +360,6 @@ class SettingsPage extends StatefulWidget {
} }
class _SettingsPageState extends State<SettingsPage> { class _SettingsPageState extends State<SettingsPage> {
String? fcmToken;
final TextEditingController _tokenController = TextEditingController();
bool isLoading = false;
// Admin user creation // Admin user creation
Map<String, dynamic>? userData; Map<String, dynamic>? userData;
bool isLoadingUser = true; bool isLoadingUser = true;
@ -353,52 +371,18 @@ class _SettingsPageState extends State<SettingsPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadFCMToken();
_loadUserData(); _loadUserData();
_generatePassword(); _generatePassword();
} }
@override @override
void dispose() { void dispose() {
_tokenController.dispose();
_emailController.dispose(); _emailController.dispose();
_passwordController.dispose(); _passwordController.dispose();
_displayNameController.dispose(); _displayNameController.dispose();
super.dispose(); super.dispose();
} }
Future<void> _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<void> _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<void> _loadUserData() async { Future<void> _loadUserData() async {
setState(() { setState(() {
@ -715,17 +699,18 @@ class _SettingsPageState extends State<SettingsPage> {
Container( Container(
width: double.infinity, width: double.infinity,
height: 56,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => _showCreateAccountDialog(context), onPressed: () => _showCreateAccountDialog(context),
icon: Icon(Icons.person_add, size: 18), icon: Icon(Icons.person_add, size: 20),
label: Text('Create an Account'), label: Text('Create an Account'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93), backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder( 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<SettingsPage> {
], ],
Text( Text(
'Development Tools', 'App Information',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -743,92 +728,88 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
SizedBox(height: 16), SizedBox(height: 16),
Text( Container(
'FCM Token:', width: double.infinity,
style: TextStyle( height: 56,
fontSize: 16, child: ElevatedButton.icon(
fontWeight: FontWeight.w500, onPressed: () => Navigator.of(context).push(
color: Colors.black87, 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) SizedBox(height: 16),
Center(
child: CircularProgressIndicator( Container(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)), width: double.infinity,
height: 56,
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => InformationScreen()),
), ),
) icon: Icon(Icons.info_outline, size: 20),
else label: Text('Information'),
Column( style: ElevatedButton.styleFrom(
crossAxisAlignment: CrossAxisAlignment.start, backgroundColor: Color(0xFF6A4C93),
children: [ foregroundColor: Colors.white,
Container( shape: RoundedRectangleBorder(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(12),
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...',
),
),
), ),
SizedBox(height: 8), padding: EdgeInsets.symmetric(horizontal: 16),
),
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),
),
),
),
],
),
],
), ),
),
SizedBox(height: 32), SizedBox(height: 32),
Container( Container(
width: double.infinity, 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( child: ElevatedButton.icon(
onPressed: _signOut, 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)), label: Text('Sign Out', style: TextStyle(color: Colors.red)),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, backgroundColor: Colors.white,
side: BorderSide(color: Colors.red), side: BorderSide(color: Colors.red),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
), ),
padding: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(horizontal: 16),
), ),
), ),
), ),

View File

@ -7,12 +7,21 @@ import 'cache_service.dart';
class InvitationsService { class InvitationsService {
static bool _pollingStarted = false; static bool _pollingStarted = false;
/// Get all invitations with caching - returns cached data if available and fresh /// Get all invitations with caching - returns cached data if available and fresh
static Future<Map<String, dynamic>> getAllInvitations({bool forceRefresh = false}) async { static Future<Map<String, dynamic>> getAllInvitations({
bool forceRefresh = false,
}) async {
// Return cached data if available and not forcing refresh // Return cached data if available and not forcing refresh
if (!forceRefresh) { if (!forceRefresh) {
final cached = CacheService.getCached<InvitationsData>(CacheService.invitationsAllKey); final cached = CacheService.getCached<InvitationsData>(
if (cached != null && !CacheService.isCacheExpired(CacheService.invitationsAllKey, CacheService.invitationsCacheDuration)) { CacheService.invitationsAllKey,
);
if (cached != null &&
!CacheService.isCacheExpired(
CacheService.invitationsAllKey,
CacheService.invitationsCacheDuration,
)) {
return { return {
'success': true, 'success': true,
'message': 'Loaded from cache', 'message': 'Loaded from cache',
@ -21,7 +30,9 @@ class InvitationsService {
} }
// Return stale data while fetching fresh data in background // Return stale data while fetching fresh data in background
final stale = CacheService.getCachedStale<InvitationsData>(CacheService.invitationsAllKey); final stale = CacheService.getCachedStale<InvitationsData>(
CacheService.invitationsAllKey,
);
if (stale != null && !forceRefresh) { if (stale != null && !forceRefresh) {
// Trigger background refresh // Trigger background refresh
_fetchAndCacheInvitations(); _fetchAndCacheInvitations();
@ -37,7 +48,9 @@ class InvitationsService {
return await _fetchAndCacheInvitations(); return await _fetchAndCacheInvitations();
} }
static Future<Map<String, dynamic>> _fetchAndCacheInvitations([bool fromPolling = false]) async { static Future<Map<String, dynamic>> _fetchAndCacheInvitations([
bool fromPolling = false,
]) async {
try { try {
final response = await HttpService.get( final response = await HttpService.get(
ApiConstants.getAllInvitationsEndpoint, ApiConstants.getAllInvitationsEndpoint,
@ -52,7 +65,10 @@ class InvitationsService {
// Only cache when not called from polling to prevent conflicts // Only cache when not called from polling to prevent conflicts
if (!fromPolling) { if (!fromPolling) {
CacheService.setCached(CacheService.invitationsAllKey, invitationsData); CacheService.setCached(
CacheService.invitationsAllKey,
invitationsData,
);
} }
return { return {
@ -63,8 +79,13 @@ class InvitationsService {
} else { } else {
return { return {
'success': false, 'success': false,
'message': invitationsResponse.message ?? 'Failed to fetch invitations', 'message':
'invitations': InvitationsData(created: [], accepted: [], available: []), invitationsResponse.message ?? 'Failed to fetch invitations',
'invitations': InvitationsData(
created: [],
accepted: [],
available: [],
),
}; };
} }
} else if (response.statusCode == 401 || response.statusCode == 403) { } else if (response.statusCode == 401 || response.statusCode == 403) {
@ -72,13 +93,21 @@ class InvitationsService {
return { return {
'success': false, 'success': false,
'message': 'Session expired. Please login again.', 'message': 'Session expired. Please login again.',
'invitations': InvitationsData(created: [], accepted: [], available: []), 'invitations': InvitationsData(
created: [],
accepted: [],
available: [],
),
}; };
} else { } else {
return { return {
'success': false, 'success': false,
'message': 'Server error (${response.statusCode})', 'message': 'Server error (${response.statusCode})',
'invitations': InvitationsData(created: [], accepted: [], available: []), 'invitations': InvitationsData(
created: [],
accepted: [],
available: [],
),
}; };
} }
} catch (e) { } catch (e) {
@ -86,7 +115,11 @@ class InvitationsService {
return { return {
'success': false, 'success': false,
'message': 'Network error. Please check your connection.', '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 /// Get invitations stream for real-time updates
static Stream<InvitationsData> getInvitationsStream() { static Stream<InvitationsData> getInvitationsStream() {
return CacheService.getStream<InvitationsData>(CacheService.invitationsAllKey); return CacheService.getStream<InvitationsData>(
CacheService.invitationsAllKey,
);
} }
/// Check if polling is active /// Check if polling is active
@ -161,7 +196,9 @@ class InvitationsService {
} }
} }
static Future<Map<String, dynamic>> createInvitation(Map<String, dynamic> invitationData) async { static Future<Map<String, dynamic>> createInvitation(
Map<String, dynamic> invitationData,
) async {
try { try {
final response = await HttpService.post( final response = await HttpService.post(
ApiConstants.createInvitationEndpoint, ApiConstants.createInvitationEndpoint,
@ -209,7 +246,9 @@ class InvitationsService {
} }
} }
static Future<Map<String, dynamic>> getInvitationDetails(int invitationId) async { static Future<Map<String, dynamic>> getInvitationDetails(
int invitationId,
) async {
try { try {
final response = await HttpService.get( final response = await HttpService.get(
'${ApiConstants.invitationsEndpoint}/get?id=$invitationId', '${ApiConstants.invitationsEndpoint}/get?id=$invitationId',
@ -218,15 +257,15 @@ class InvitationsService {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final responseData = jsonDecode(response.body); final responseData = jsonDecode(response.body);
if (responseData['status'] == true) { if (responseData['status'] == true) {
final invitationDetails = InvitationDetails.fromJson(responseData['data']); final invitationDetails = InvitationDetails.fromJson(
return { responseData['data'],
'success': true, );
'data': invitationDetails, return {'success': true, 'data': invitationDetails};
};
} else { } else {
return { return {
'success': false, 'success': false,
'message': responseData['message'] ?? 'Failed to get invitation details', 'message':
responseData['message'] ?? 'Failed to get invitation details',
}; };
} }
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {

View File

@ -165,6 +165,61 @@ class UserService {
} }
} }
static Future<Map<String, dynamic>> updateUser({
String? displayName,
String? username,
String? avatar,
}) async {
try {
final Map<String, dynamic> 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<Map<String, dynamic>> createUser({ static Future<Map<String, dynamic>> createUser({
required String email, required String email,
required String password, required String password,