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 '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<SplashScreen> {
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<SignInPage> {
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');
}

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
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>(
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>(
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 '../../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<ProfilePage> {
).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<ProfilePage> {
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<SettingsPage> {
String? fcmToken;
final TextEditingController _tokenController = TextEditingController();
bool isLoading = false;
// Admin user creation
Map<String, dynamic>? userData;
bool isLoadingUser = true;
@ -353,52 +371,18 @@ class _SettingsPageState extends State<SettingsPage> {
@override
void initState() {
super.initState();
_loadFCMToken();
_loadUserData();
_generatePassword();
}
@override
void dispose() {
_tokenController.dispose();
_emailController.dispose();
_passwordController.dispose();
_displayNameController.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 {
setState(() {
@ -715,17 +699,18 @@ class _SettingsPageState extends State<SettingsPage> {
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<SettingsPage> {
],
Text(
'Development Tools',
'App Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
@ -743,92 +728,88 @@ class _SettingsPageState extends State<SettingsPage> {
),
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>(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),
),
),
),

View File

@ -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<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
if (!forceRefresh) {
final cached = CacheService.getCached<InvitationsData>(CacheService.invitationsAllKey);
if (cached != null && !CacheService.isCacheExpired(CacheService.invitationsAllKey, CacheService.invitationsCacheDuration)) {
final cached = CacheService.getCached<InvitationsData>(
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<InvitationsData>(CacheService.invitationsAllKey);
final stale = CacheService.getCachedStale<InvitationsData>(
CacheService.invitationsAllKey,
);
if (stale != null && !forceRefresh) {
// Trigger background refresh
_fetchAndCacheInvitations();
@ -37,7 +48,9 @@ class InvitationsService {
return await _fetchAndCacheInvitations();
}
static Future<Map<String, dynamic>> _fetchAndCacheInvitations([bool fromPolling = false]) async {
static Future<Map<String, dynamic>> _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<InvitationsData> getInvitationsStream() {
return CacheService.getStream<InvitationsData>(CacheService.invitationsAllKey);
return CacheService.getStream<InvitationsData>(
CacheService.invitationsAllKey,
);
}
/// 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 {
final response = await HttpService.post(
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 {
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) {

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({
required String email,
required String password,