feat: Editing Profiles
This commit is contained in:
parent
7c2298df35
commit
481615260c
@ -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'];
|
||||||
|
|
||||||
|
// 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(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => NotificationPermissionScreen(),
|
builder: (context) => NotificationPermissionScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_showErrorAlert('Failed to load user data');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_showErrorAlert(result['message'] ?? 'Login failed');
|
_showErrorAlert(result['message'] ?? 'Login failed');
|
||||||
}
|
}
|
||||||
|
|||||||
428
frontend/lib/screens/edit_profile_screen.dart
Normal file
428
frontend/lib/screens/edit_profile_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -7,21 +7,32 @@ 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',
|
||||||
'invitations': cached,
|
'invitations': cached,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
||||||
@ -49,22 +62,30 @@ class InvitationsService {
|
|||||||
|
|
||||||
if (invitationsResponse.status) {
|
if (invitationsResponse.status) {
|
||||||
final invitationsData = invitationsResponse.data;
|
final invitationsData = invitationsResponse.data;
|
||||||
|
|
||||||
// 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 {
|
||||||
'success': true,
|
'success': true,
|
||||||
'invitations': invitationsData,
|
'invitations': invitationsData,
|
||||||
'message': invitationsResponse.message ?? '',
|
'message': invitationsResponse.message ?? '',
|
||||||
};
|
};
|
||||||
} 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) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user