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 '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'];
|
||||
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationPermissionScreen(),
|
||||
),
|
||||
);
|
||||
// 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showErrorAlert('Failed to load user data');
|
||||
}
|
||||
} else {
|
||||
_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
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -7,12 +7,21 @@ 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',
|
||||
@ -21,7 +30,9 @@ class InvitationsService {
|
||||
}
|
||||
|
||||
// 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,
|
||||
@ -52,7 +65,10 @@ class InvitationsService {
|
||||
|
||||
// Only cache when not called from polling to prevent conflicts
|
||||
if (!fromPolling) {
|
||||
CacheService.setCached(CacheService.invitationsAllKey, invitationsData);
|
||||
CacheService.setCached(
|
||||
CacheService.invitationsAllKey,
|
||||
invitationsData,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -63,8 +79,13 @@ class InvitationsService {
|
||||
} 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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user