import Foundation // MARK: - Chat Event Types enum ChatEventType { case joined, userJoined, userLeft, chatEnded, disconnected, error } struct ChatEvent { let type: ChatEventType let message: String? let data: [String: Any]? init(type: ChatEventType, message: String? = nil, data: [String: Any]? = nil) { self.type = type self.message = message self.data = data } } struct TypingEvent { let userType: String let userName: String let isTyping: Bool } // MARK: - Chat Service (WebSocket) @MainActor final class ChatService: ObservableObject { private static let wsBaseURL = "wss://app.payfrit.com:3001" @Published var isConnected = false @Published var chatClosed = false private var webSocketTask: URLSessionWebSocketTask? private var currentTaskId: Int? private var userType: String? private var pingTimer: Timer? // Callbacks var onMessage: ((ChatMessage) -> Void)? var onTyping: ((TypingEvent) -> Void)? var onEvent: ((ChatEvent) -> Void)? // MARK: - Connect func connect(taskId: Int, userToken: String, userType: String) async -> Bool { if webSocketTask != nil { disconnect() } currentTaskId = taskId self.userType = userType guard let url = URL(string: Self.wsBaseURL) else { return false } let session = URLSession(configuration: .default) webSocketTask = session.webSocketTask(with: url) webSocketTask?.resume() // Send join event let joinPayload: [String: Any] = [ "event": "join-chat", "task_id": taskId, "user_token": userToken, "user_type": userType ] guard let joinData = try? JSONSerialization.data(withJSONObject: joinPayload), let joinString = String(data: joinData, encoding: .utf8) else { return false } do { try await webSocketTask?.send(.string(joinString)) isConnected = true startListening() startPing() return true } catch { isConnected = false return false } } // MARK: - Listen private func startListening() { webSocketTask?.receive { [weak self] result in Task { @MainActor in guard let self = self else { return } switch result { case .success(let message): switch message { case .string(let text): self.handleMessage(text) case .data(let data): if let text = String(data: data, encoding: .utf8) { self.handleMessage(text) } @unknown default: break } self.startListening() case .failure: self.isConnected = false self.onEvent?(ChatEvent(type: .disconnected)) } } } } private func handleMessage(_ text: String) { guard let data = text.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let event = json["event"] as? String else { return } switch event { case "joined": onEvent?(ChatEvent(type: .joined, data: json)) case "new-message": if let msgData = json["data"] as? [String: Any] { let msg = ChatMessage(json: msgData) onMessage?(msg) } case "user-typing": let data = json["data"] as? [String: Any] ?? json onTyping?(TypingEvent( userType: data["userType"] as? String ?? "", userName: data["userName"] as? String ?? "", isTyping: data["isTyping"] as? Bool ?? false )) case "user-joined": onEvent?(ChatEvent(type: .userJoined, data: json["data"] as? [String: Any])) case "user-left": onEvent?(ChatEvent(type: .userLeft, data: json["data"] as? [String: Any])) case "chat-ended", "chat-closed": chatClosed = true onEvent?(ChatEvent(type: .chatEnded, message: (json["data"] as? [String: Any])?["message"] as? String ?? "Chat has ended")) case "error": onEvent?(ChatEvent(type: .error, message: (json["data"] as? [String: Any])?["message"] as? String ?? "Unknown error")) default: break } } // MARK: - Send func sendMessage(_ text: String) { guard let taskId = currentTaskId else { return } let payload: [String: Any] = [ "event": "send-message", "task_id": taskId, "message": text ] sendJSON(payload) } func setTyping(_ isTyping: Bool) { guard let taskId = currentTaskId else { return } let payload: [String: Any] = [ "event": "typing", "task_id": taskId, "is_typing": isTyping ] sendJSON(payload) } func closeChatWS() { guard let taskId = currentTaskId else { return } let payload: [String: Any] = [ "event": "chat-closed", "task_id": taskId ] sendJSON(payload) } private func sendJSON(_ payload: [String: Any]) { guard let data = try? JSONSerialization.data(withJSONObject: payload), let string = String(data: data, encoding: .utf8) else { return } webSocketTask?.send(.string(string)) { _ in } } // MARK: - Ping private func startPing() { pingTimer?.invalidate() pingTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in self?.webSocketTask?.sendPing { _ in } } } // MARK: - Disconnect func disconnect() { pingTimer?.invalidate() pingTimer = nil webSocketTask?.cancel(with: .goingAway, reason: nil) webSocketTask = nil isConnected = false currentTaskId = nil userType = nil } }