diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 020825c..3d56fd3 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -208,6 +208,7 @@ class ApiClient { const authPaths = [ ApiConstants.authLogin, ApiConstants.authRegister, + ApiConstants.authLogout, ApiConstants.authForgotPassword, ApiConstants.authResetPassword, ApiConstants.authVerifyEmail, diff --git a/lib/features/agents/data/models/agent_profile.dart b/lib/features/agents/data/models/agent_profile.dart index 2d51022..6c70733 100644 --- a/lib/features/agents/data/models/agent_profile.dart +++ b/lib/features/agents/data/models/agent_profile.dart @@ -196,6 +196,38 @@ class AgentProfile { return tags; } + AgentProfile copyWith({bool? isAvailable}) { + return AgentProfile( + id: id, + userId: userId, + firstName: firstName, + lastName: lastName, + headline: headline, + bio: bio, + avatar: avatar, + companyName: companyName, + phone: phone, + website: website, + city: city, + state: state, + country: country, + averageRating: averageRating, + totalReviews: totalReviews, + isVerified: isVerified, + isFeatured: isFeatured, + isActive: isActive, + isAvailable: isAvailable ?? this.isAvailable, + email: email, + agentType: agentType, + user: user, + fieldValues: fieldValues, + createdAt: createdAt, + legacySpecializations: legacySpecializations, + languages: languages, + serviceAreas: serviceAreas, + ); + } + factory AgentProfile.fromJson(Map json) { return AgentProfile( id: json['id'] as String, diff --git a/lib/features/agents/presentation/providers/agent_detail_provider.dart b/lib/features/agents/presentation/providers/agent_detail_provider.dart index c0498d9..5fceabd 100644 --- a/lib/features/agents/presentation/providers/agent_detail_provider.dart +++ b/lib/features/agents/presentation/providers/agent_detail_provider.dart @@ -433,6 +433,13 @@ class AgentDetailNotifier extends StateNotifier { return success; } + void updateAvailability(bool isAvailable) { + if (state.agent == null) return; + state = state.copyWith( + agent: state.agent!.copyWith(isAvailable: isAvailable), + ); + } + Future cancelConnection() async { final requestId = state.connectionRequestId; if (requestId == null) return false; diff --git a/lib/features/agents/presentation/screens/agent_home_screen.dart b/lib/features/agents/presentation/screens/agent_home_screen.dart index d75f7d2..663d41c 100644 --- a/lib/features/agents/presentation/screens/agent_home_screen.dart +++ b/lib/features/agents/presentation/screens/agent_home_screen.dart @@ -87,6 +87,8 @@ class _AgentHomeContent extends ConsumerStatefulWidget { class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { bool _isUploadingAvatar = false; bool _isTogglingAvailability = false; + bool _showEmail = false; + bool _showPhone = false; File? _localAvatarFile; Future _pickAndUploadAvatar() async { @@ -179,13 +181,21 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { if (_isTogglingAvailability) return; setState(() => _isTogglingAvailability = true); + + // Optimistically update the UI immediately + ref + .read(agentDetailProvider(widget.agentProfileId).notifier) + .updateAvailability(newValue); + try { final repo = ProfileRepository(); await repo.updateProfile('AGENT', {'isAvailable': newValue}); - // Refresh agent detail to reflect new status - ref.invalidate(agentDetailProvider(widget.agentProfileId)); } catch (e) { + // Revert on failure if (mounted) { + ref + .read(agentDetailProvider(widget.agentProfileId).notifier) + .updateAvailability(!newValue); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to update availability: $e')), ); @@ -475,15 +485,32 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { const SizedBox(width: 16), Expanded( child: Text( - email.isNotEmpty ? state.maskEmail(email) : '-', + email.isNotEmpty + ? (_showEmail ? email : state.maskEmail(email)) + : '-', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - const Icon(Icons.visibility_off_outlined, - size: 18, color: AppColors.primaryDark), + if (email.isNotEmpty) + GestureDetector( + onTap: () => setState(() => _showEmail = !_showEmail), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + _showEmail + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + size: 18, + color: AppColors.primaryDark, + ), + ), + ), ], ), const SizedBox(height: 14), @@ -498,15 +525,32 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { const SizedBox(width: 16), Expanded( child: Text( - phone.isNotEmpty ? state.maskPhone(phone) : '-', + phone.isNotEmpty + ? (_showPhone ? phone : state.maskPhone(phone)) + : '-', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), - const Icon(Icons.visibility_off_outlined, - size: 18, color: AppColors.primaryDark), + if (phone.isNotEmpty) + GestureDetector( + onTap: () => setState(() => _showPhone = !_showPhone), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + _showPhone + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + size: 18, + color: AppColors.primaryDark, + ), + ), + ), ], ), ], diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart index 52e4cde..0bea744 100644 --- a/lib/features/auth/presentation/providers/auth_provider.dart +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -103,12 +103,15 @@ class AuthNotifier extends StateNotifier { } Future logout() async { - state = state.copyWith(status: AuthStatus.loading); - // Disconnect socket before logout + // Disconnect socket immediately SocketService().disconnect(); - // Unregister FCM token before clearing auth - await PushNotificationService().unregister(); - await _repository.logout(); + // Attempt FCM unregister and API logout (best-effort, don't block) + try { + await PushNotificationService().unregister(); + } catch (_) {} + try { + await _repository.logout(); + } catch (_) {} state = const AuthState(); } diff --git a/lib/features/messaging/data/socket_service.dart b/lib/features/messaging/data/socket_service.dart index acee2e7..b914bc8 100644 --- a/lib/features/messaging/data/socket_service.dart +++ b/lib/features/messaging/data/socket_service.dart @@ -18,6 +18,7 @@ class SocketService { io.Socket? _socket; bool _isConnected = false; + bool _intentionalDisconnect = false; String? _currentToken; // Stream controllers for events @@ -52,6 +53,7 @@ class SocketService { if (_socket?.connected == true && _currentToken == token) return; _currentToken = token; + _intentionalDisconnect = false; // Strip /api/v1 from base URL for socket connection final apiUrl = AppConfig.apiBaseUrl; @@ -82,7 +84,7 @@ class SocketService { _isConnected = false; _connectionController.add(false); - if (reason == 'io server disconnect') { + if (reason == 'io server disconnect' && !_intentionalDisconnect) { _log.w('Server rejected connection - token likely expired'); _reconnectWithFreshToken(); } @@ -151,6 +153,7 @@ class SocketService { } void disconnect() { + _intentionalDisconnect = true; _socket?.disconnect(); _socket?.dispose(); _socket = null;