payfrit-app/lib/services/chat_service.dart
John Mizerek 768b882ca7 Clean up debug statements and sanitize error messages
- Remove 60+ debug print statements from services and screens
- Sanitize error messages to not expose internal API details
- Remove stack trace exposure in beacon_scan_screen
- Delete unused order_home_screen.dart
- Remove unused ChatScreen route from app_router
- Fix widget_test.dart to compile
- Remove unused foundation.dart import from menu_browse_screen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:44:58 -08:00

267 lines
6.3 KiB
Dart

import 'dart:async';
import 'package:socket_io_client/socket_io_client.dart' as io;
import '../models/chat_message.dart';
import 'api.dart';
/// Service for managing real-time chat via WebSocket
class ChatService {
static const String _wsBaseUrl = 'https://app.payfrit.com:3001';
io.Socket? _socket;
int? _currentTaskId;
String? _userType;
final StreamController<ChatMessage> _messageController =
StreamController<ChatMessage>.broadcast();
final StreamController<TypingEvent> _typingController =
StreamController<TypingEvent>.broadcast();
final StreamController<ChatEvent> _eventController =
StreamController<ChatEvent>.broadcast();
bool _isConnected = false;
/// Stream of incoming messages
Stream<ChatMessage> get messages => _messageController.stream;
/// Stream of typing events
Stream<TypingEvent> get typingEvents => _typingController.stream;
/// Stream of chat events (user joined/left, chat ended)
Stream<ChatEvent> get events => _eventController.stream;
/// Whether the socket is currently connected
bool get isConnected => _isConnected;
/// Current task ID
int? get currentTaskId => _currentTaskId;
/// Connect to chat for a specific task
Future<bool> connect({
required int taskId,
required String userToken,
required String userType,
}) async {
// Disconnect if already connected to a different task
if (_socket != null) {
await disconnect();
}
_currentTaskId = taskId;
_userType = userType;
final completer = Completer<bool>();
try {
_socket = io.io(
_wsBaseUrl,
io.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.build(),
);
_socket!.onConnect((_) {
_isConnected = true;
// Join the chat room
_socket!.emit('join-chat', {
'taskId': taskId,
'userToken': userToken,
'userType': userType,
});
});
_socket!.on('joined', (data) {
_eventController.add(ChatEvent(
type: ChatEventType.joined,
data: data,
));
if (!completer.isCompleted) {
completer.complete(true);
}
});
_socket!.on('error', (data) {
_eventController.add(ChatEvent(
type: ChatEventType.error,
message: data['message'] ?? 'Unknown error',
));
if (!completer.isCompleted) {
completer.complete(false);
}
});
_socket!.on('new-message', (data) {
final message = ChatMessage.fromJson(data as Map<String, dynamic>);
_messageController.add(message);
});
_socket!.on('user-typing', (data) {
_typingController.add(TypingEvent(
userType: data['userType'] ?? '',
userName: data['userName'] ?? '',
isTyping: data['isTyping'] ?? false,
));
});
_socket!.on('user-joined', (data) {
_eventController.add(ChatEvent(
type: ChatEventType.userJoined,
data: data,
));
});
_socket!.on('user-left', (data) {
_eventController.add(ChatEvent(
type: ChatEventType.userLeft,
data: data,
));
});
_socket!.on('chat-ended', (data) {
_eventController.add(ChatEvent(
type: ChatEventType.chatEnded,
message: data['message'] ?? 'Chat has ended',
));
});
_socket!.onDisconnect((_) {
_isConnected = false;
_eventController.add(ChatEvent(
type: ChatEventType.disconnected,
));
});
_socket!.onConnectError((error) {
_isConnected = false;
if (!completer.isCompleted) {
completer.complete(false);
}
});
_socket!.connect();
// Timeout after 10 seconds
return completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () => false,
);
} catch (e) {
return false;
}
}
/// Send a message via WebSocket
/// Returns true if message was sent, false if not connected
bool sendMessage(String text) {
if (_socket == null || !_isConnected || _currentTaskId == null) {
return false;
}
_socket!.emit('send-message', {
'taskId': _currentTaskId,
'message': text,
});
return true;
}
/// Send a message via HTTP (fallback)
Future<int> sendMessageHttp({
required int taskId,
required String message,
int? userId,
String? senderType,
}) async {
return Api.sendChatMessage(
taskId: taskId,
message: message,
userId: userId,
senderType: senderType,
);
}
/// Notify that user is typing
void setTyping(bool isTyping) {
if (_socket == null || !_isConnected || _currentTaskId == null) return;
_socket!.emit('typing', {
'taskId': _currentTaskId,
'isTyping': isTyping,
});
}
/// Close the chat (only workers can do this)
void closeChat() {
if (_socket == null || !_isConnected || _currentTaskId == null) return;
_socket!.emit('chat-closed', {
'taskId': _currentTaskId,
});
}
/// Leave the chat room
void leaveChat() {
if (_socket == null || _currentTaskId == null) return;
_socket!.emit('leave-chat', {
'taskId': _currentTaskId,
});
}
/// Disconnect from WebSocket
Future<void> disconnect() async {
if (_socket != null) {
leaveChat();
_socket!.disconnect();
_socket!.dispose();
_socket = null;
}
_isConnected = false;
_currentTaskId = null;
_userType = null;
}
/// Dispose the service and clean up streams
void dispose() {
disconnect();
_messageController.close();
_typingController.close();
_eventController.close();
}
}
/// Event for typing indicators
class TypingEvent {
final String userType;
final String userName;
final bool isTyping;
const TypingEvent({
required this.userType,
required this.userName,
required this.isTyping,
});
}
/// Event type for chat state changes
enum ChatEventType {
joined,
userJoined,
userLeft,
chatEnded,
disconnected,
error,
}
/// Event for chat state changes
class ChatEvent {
final ChatEventType type;
final String? message;
final dynamic data;
const ChatEvent({
required this.type,
this.message,
this.data,
});
}