import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif @MainActor final class OrderRealtimeTracker { // Keep realtime tracking on polling to avoid socket.io handshake failures // on environments where websocket upgrade is not available. private let useSocketRealtime = false private var pollingTask: Task? = nil private var socketClient: OrderSocketClient? = nil private var activeOrderId: String? = nil var onOrderUpdated: ((PublicOrderResult) -> Void)? func start(orderId: String, jwt: String?) { stop() activeOrderId = orderId pollingTask = Task { @MainActor [weak self] in guard let self else { return } await self.runPollingLoop(orderId: orderId) } guard useSocketRealtime, let jwt, jwt.isEmpty == false else { return } let socket = OrderSocketClient() socket.onOrderUpdate = { [weak self] update in guard let self else { return } guard update.id == orderId else { return } Task { @MainActor [weak self] in guard let self else { return } await self.fetchLatest(orderId: orderId) } } socket.connect(jwt: jwt) socketClient = socket } func stop() { pollingTask?.cancel() pollingTask = nil socketClient?.disconnect() socketClient = nil activeOrderId = nil } private func runPollingLoop(orderId: String) async { var elapsedSeconds = 0 while Task.isCancelled == false { if activeOrderId != orderId { return } let fetched = await fetchLatest(orderId: orderId) if fetched?.isFinalStatus == true { return } let delay = pollingDelay(for: elapsedSeconds) elapsedSeconds += delay do { try await Task.sleep(nanoseconds: UInt64(delay) * 1_000_000_000) } catch { return } } } private func pollingDelay(for elapsedSeconds: Int) -> Int { if elapsedSeconds < 60 { return 3 } if elapsedSeconds < 180 { return 5 } return 10 } @discardableResult private func fetchLatest(orderId: String) async -> PublicOrderResult? { do { logger.debug("OrderTracking poll request orderId=\(orderId, privacy: .public)") let response = try await ApiService().publicOrder(orderId: orderId) guard response.error == false, let order = response.result else { logger.error("OrderTracking poll API error orderId=\(orderId, privacy: .public) message=\((response.message ?? "unknown"), privacy: .public)") return nil } clearPendingCartIfNeeded(for: order) logger.info("OrderTracking poll success orderId=\(orderId, privacy: .public) status=\((order.status ?? "nil"), privacy: .public) paymentStatus=\((order.paymentStatus ?? "nil"), privacy: .public)") onOrderUpdated?(order) return order } catch { logger.error("OrderTracking poll failure orderId=\(orderId, privacy: .public) error=\(error.localizedDescription, privacy: .public)") return nil } } private func clearPendingCartIfNeeded(for order: PublicOrderResult) { guard let pendingId = SessionStateStore.loadPendingCartOrderId() else { return } let normalizedPending = pendingId.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() if normalizedPending.isEmpty { return } let ids = [order.id, order.realId] .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } guard ids.contains(normalizedPending) else { return } if shouldClearCart(for: order) == false { return } SessionStateStore.clearCart() SessionStateStore.clearPendingCartOrder() NotificationCenter.default.post(name: .cartDidReset, object: nil) } private func shouldClearCart(for order: PublicOrderResult) -> Bool { if order.isPaymentConfirmed { return true } let status = (order.status ?? "").uppercased() if status.contains("COMPLETED") || status.contains("DELIVERED") || status.contains("RECEIVED") { return true } return false } } final class OrderSocketClient: @unchecked Sendable { var onOrderUpdate: ((OrderRealtimeUpdate) -> Void)? #if os(iOS) || os(macOS) private var task: URLSessionWebSocketTask? = nil private let session = URLSession(configuration: .default) private var isConnected = false private var pendingJWT: String? = nil #endif func connect(jwt: String) { #if os(iOS) || os(macOS) disconnect() guard let url = makeSocketURL() else { return } let wsTask = session.webSocketTask(with: url) wsTask.resume() task = wsTask pendingJWT = jwt receiveLoop() #else _ = jwt #endif } func disconnect() { #if os(iOS) || os(macOS) isConnected = false pendingJWT = nil task?.cancel(with: .goingAway, reason: nil) task = nil #endif } #if os(iOS) || os(macOS) private func receiveLoop() { guard let task else { return } task.receive { [weak self] result in guard let self else { return } switch result { case .failure: self.disconnect() case .success(let message): self.handleMessage(message) self.receiveLoop() } } } private func handleMessage(_ message: URLSessionWebSocketTask.Message) { let text: String switch message { case .string(let value): text = value case .data(let data): text = String(data: data, encoding: .utf8) ?? "" @unknown default: return } guard text.isEmpty == false else { return } if text == "2" { task?.send(.string("3")) { _ in } return } if text.hasPrefix("0"), let jwt = pendingJWT { let authPacket = "40{\"token\":\"Bearer \(jwt)\"}" task?.send(.string(authPacket)) { _ in } pendingJWT = nil return } if text.hasPrefix("40") { isConnected = true return } guard text.hasPrefix("42") else { return } let eventPayload = String(text.dropFirst(2)) guard let data = eventPayload.data(using: .utf8) else { return } if let rawArray = try? JSONSerialization.jsonObject(with: data) as? [Any], rawArray.count >= 2, let eventName = rawArray[0] as? String, eventName == "order_update" { let payloadAny = rawArray[1] guard JSONSerialization.isValidJSONObject(payloadAny), let payloadData = try? JSONSerialization.data(withJSONObject: payloadAny), let update = try? JSONDecoder().decode(OrderRealtimeUpdate.self, from: payloadData) else { return } onOrderUpdate?(update) return } // Compat: alguns servidores podem encapsular o evento como objeto. if let rawObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let eventName = (rawObject["event"] as? String)?.lowercased(), eventName == "order_update", let payloadAny = rawObject["data"], JSONSerialization.isValidJSONObject(payloadAny), let payloadData = try? JSONSerialization.data(withJSONObject: payloadAny), let update = try? JSONDecoder().decode(OrderRealtimeUpdate.self, from: payloadData) { onOrderUpdate?(update) } } private func makeSocketURL() -> URL? { guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else { return nil } if components.scheme == "https" { components.scheme = "wss" } else { components.scheme = "ws" } components.path = "/socket.io/" components.queryItems = [ URLQueryItem(name: "EIO", value: "4"), URLQueryItem(name: "transport", value: "websocket") ] return components.url } #endif }