payfrit-app/lib/screens/account_screen.dart
John Mizerek 7b08fa73de Show user name in Account screen instead of User #ID
- Load user profile on Account screen init
- Display FirstName LastInitial format
- Fall back to User #ID if profile fails to load

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 10:49:18 -08:00

458 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import '../app/app_state.dart';
import '../app/app_router.dart';
import '../services/api.dart';
import '../services/auth_storage.dart';
class AccountScreen extends StatefulWidget {
const AccountScreen({super.key});
@override
State<AccountScreen> createState() => _AccountScreenState();
}
class _AccountScreenState extends State<AccountScreen> {
bool _isLoggingOut = false;
bool _isLoadingAvatar = true;
bool _isUploadingAvatar = false;
String? _avatarUrl;
String? _userName;
final ImagePicker _picker = ImagePicker();
@override
void initState() {
super.initState();
_loadAvatar();
_loadProfile();
}
Future<void> _loadAvatar() async {
// Don't try to load avatar if not logged in
final appState = context.read<AppState>();
if (!appState.isLoggedIn) {
setState(() => _isLoadingAvatar = false);
return;
}
try {
final avatarInfo = await Api.getAvatar();
if (mounted) {
setState(() {
_avatarUrl = avatarInfo.hasAvatar ? avatarInfo.avatarUrl : null;
_isLoadingAvatar = false;
});
}
} catch (e) {
debugPrint('Error loading avatar: $e');
if (mounted) {
setState(() => _isLoadingAvatar = false);
}
}
}
Future<void> _loadProfile() async {
final appState = context.read<AppState>();
if (!appState.isLoggedIn) return;
try {
final profile = await Api.getProfile();
if (mounted) {
final firstName = profile.firstName;
final lastInitial = profile.lastName.isNotEmpty
? '${profile.lastName[0]}.'
: '';
setState(() {
_userName = '$firstName $lastInitial'.trim();
});
}
} catch (e) {
debugPrint('Error loading profile: $e');
}
}
Future<void> _pickAndUploadAvatar() async {
// Check if user is logged in first
final appState = context.read<AppState>();
if (!appState.isLoggedIn) {
final shouldLogin = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Login Required'),
content: const Text('Please login to upload a profile photo.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Login'),
),
],
),
);
if (shouldLogin == true && mounted) {
Navigator.of(context).pushNamed(AppRoutes.login);
}
return;
}
// Show picker options
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (context) => SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Take Photo'),
onTap: () => Navigator.pop(context, ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Choose from Gallery'),
onTap: () => Navigator.pop(context, ImageSource.gallery),
),
],
),
),
);
if (source == null) return;
try {
final XFile? image = await _picker.pickImage(
source: source,
maxWidth: 500,
maxHeight: 500,
imageQuality: 85,
);
if (image == null) return;
setState(() => _isUploadingAvatar = true);
final newAvatarUrl = await Api.uploadAvatar(image.path);
if (mounted) {
setState(() {
_avatarUrl = newAvatarUrl;
_isUploadingAvatar = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Avatar updated! Servers can now recognize you.'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
debugPrint('Error uploading avatar: $e');
if (mounted) {
setState(() => _isUploadingAvatar = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to upload avatar: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _handleSignOut() async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Sign Out'),
content: const Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Sign Out'),
),
],
),
);
if (confirm != true || !mounted) return;
setState(() => _isLoggingOut = true);
try {
// Clear stored auth
await AuthStorage.clearAuth();
// Clear API token
Api.clearAuthToken();
// Clear app state
if (mounted) {
final appState = context.read<AppState>();
appState.clearAll();
// Navigate to login
Navigator.of(context).pushNamedAndRemoveUntil(
AppRoutes.login,
(route) => false,
);
}
} catch (e) {
if (mounted) {
setState(() => _isLoggingOut = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error signing out: $e')),
);
}
}
}
Widget _buildAvatar() {
if (_isLoadingAvatar) {
return CircleAvatar(
radius: 40,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
if (_isUploadingAvatar) {
return Stack(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
backgroundImage: _avatarUrl != null ? NetworkImage(_avatarUrl!) : null,
child: _avatarUrl == null
? Icon(
Icons.person,
size: 40,
color: Theme.of(context).colorScheme.onSurfaceVariant,
)
: null,
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black45,
shape: BoxShape.circle,
),
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
),
),
],
);
}
return GestureDetector(
onTap: _pickAndUploadAvatar,
child: Stack(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
backgroundImage: _avatarUrl != null ? NetworkImage(_avatarUrl!) : null,
child: _avatarUrl == null
? Icon(
Icons.person,
size: 40,
color: Theme.of(context).colorScheme.onSurfaceVariant,
)
: null,
),
Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
child: Icon(
Icons.camera_alt,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
// If not logged in, show login prompt
if (!appState.isLoggedIn) {
return Scaffold(
appBar: AppBar(
title: const Text('Account'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person_outline,
size: 80,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(height: 24),
Text(
'Sign in to access your account',
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'View order history, manage your profile, and more.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
FilledButton.icon(
onPressed: () {
Navigator.of(context).pushNamed(AppRoutes.login);
},
icon: const Icon(Icons.login),
label: const Text('Sign In'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
),
],
),
),
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('Account'),
),
body: ListView(
children: [
// User info header with avatar
Container(
padding: const EdgeInsets.all(24),
color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3),
child: Column(
children: [
_buildAvatar(),
const SizedBox(height: 16),
Text(
_userName ?? 'User #${appState.userId ?? "?"}',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
_avatarUrl != null
? 'Tap photo to update'
: 'Tap to add a photo for server recognition',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(height: 8),
// Order History
ListTile(
leading: const Icon(Icons.receipt_long),
title: const Text('Order History'),
subtitle: const Text('View your past orders'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.pushNamed(context, AppRoutes.orderHistory),
),
const Divider(height: 1),
// Delivery Addresses
ListTile(
leading: const Icon(Icons.location_on),
title: const Text('Delivery Addresses'),
subtitle: const Text('Manage your addresses'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.pushNamed(context, AppRoutes.addressList),
),
const Divider(height: 1),
// Profile Settings
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Profile Settings'),
subtitle: const Text('Update your name'),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.pushNamed(context, AppRoutes.profileSettings),
),
const Divider(height: 1),
const SizedBox(height: 24),
// Sign Out button
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: OutlinedButton.icon(
onPressed: _isLoggingOut ? null : _handleSignOut,
icon: _isLoggingOut
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.logout),
label: Text(_isLoggingOut ? 'Signing out...' : 'Sign Out'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
side: BorderSide(color: Theme.of(context).colorScheme.error),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(height: 32),
],
),
);
}
}