From 39ddf9c86c54f20b561dedf595f556332b18bf83 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Thu, 4 Jun 2026 16:06:22 -0300 Subject: [PATCH] fix(api): map API response error payload and validate card saving synchronously --- .../PediFoods/Services/ApiClient.swift | 88 ++++++++++++++++--- .../PediFoods/Views/Main/CheckoutView.swift | 13 ++- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/pedi-foods/Sources/PediFoods/Services/ApiClient.swift b/pedi-foods/Sources/PediFoods/Services/ApiClient.swift index a74256c..bd4b4c2 100644 --- a/pedi-foods/Sources/PediFoods/Services/ApiClient.swift +++ b/pedi-foods/Sources/PediFoods/Services/ApiClient.swift @@ -111,6 +111,11 @@ private extension ApiClient { guard let data = responseString.data(using: .utf8) else { throw NetworkError.decodeError("Resposta nao UTF-8") } + if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let errorFlag = object["error"] as? Bool, errorFlag { + let message = object["message"] as? String ?? object["msg"] as? String + throw NetworkError.httpError(200, message ?? "Erro no servidor") + } do { return try JSONDecoder().decode(T.self, from: data) } catch { @@ -247,6 +252,12 @@ private extension ApiClient { throw NetworkError.unauthorized(payload?.message) } + if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let errorFlag = object["error"] as? Bool, errorFlag { + let message = object["message"] as? String ?? object["msg"] as? String + throw NetworkError.httpError(http.statusCode, message ?? "Erro no servidor") + } + if http.statusCode == 429 { let retryAfter = Int(http.value(forHTTPHeaderField: "Retry-After") ?? "") throw NetworkError.rateLimited(retryAfter) @@ -309,6 +320,14 @@ private extension ApiClient { if let code = payload?.code, code.isEmpty == false { return "Erro: \(code)" } + + // fallback if error true is present but without a classic structure + if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let errorFlag = object["error"] as? Bool, errorFlag { + if let msg = object["message"] as? String ?? object["msg"] as? String { + return msg + } + } return String(data: data, encoding: .utf8) } @@ -318,9 +337,10 @@ private extension ApiClient { } if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + let errorFlag = object["error"] as? Bool ?? false let code = object["code"] as? String - let message = object["message"] as? String - if code != nil || message != nil { + let message = object["message"] as? String ?? object["msg"] as? String ?? object["error_description"] as? String + if errorFlag || code != nil || message != nil { return ApiErrorDescriptor(code: code, message: message) } } @@ -344,14 +364,42 @@ private extension ApiClient { #if canImport(LCEssentials) && os(iOS) func serverMessage(from error: NSError) -> String? { - if let reason = error.userInfo[NSLocalizedFailureReasonErrorKey] as? String, - !reason.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - if let data = reason.data(using: .utf8), - let parsed = serverMessage(from: data), - !parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - return parsed + // Tenta buscar no userInfo por chaves conhecidas + for key in [NSLocalizedFailureReasonErrorKey, "body", "responseBody", "data", "message"] { + if let value = error.userInfo[key] as? String, + !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if let data = value.data(using: .utf8), + let parsed = serverMessage(from: data), + !parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") { + continue + } + return parsed + } + if !value.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") { + return value + } + } else if let valueData = error.userInfo[key] as? Data { + if let parsed = serverMessage(from: valueData), + !parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, + !parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") { + return parsed + } + } + } + + // Tenta varrer qualquer chave do userInfo por strings que possam ser JSON ou mensagens + for (_, value) in error.userInfo { + if let str = value as? String, !str.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if str.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") { + if let data = str.data(using: .utf8), + let parsed = serverMessage(from: data), + !parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, + !parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") { + return parsed + } + } } - return reason } if let description = error.userInfo[NSLocalizedDescriptionKey] as? String, @@ -364,10 +412,24 @@ private extension ApiClient { } func serverPayload(from error: NSError) -> ApiErrorDescriptor? { - if let reason = error.userInfo[NSLocalizedFailureReasonErrorKey] as? String, - let data = reason.data(using: .utf8), - let payload = serverPayload(from: data) { - return payload + for key in [NSLocalizedFailureReasonErrorKey, "body", "responseBody", "data"] { + if let value = error.userInfo[key] as? String, + let data = value.data(using: .utf8), + let payload = serverPayload(from: data) { + return payload + } else if let valueData = error.userInfo[key] as? Data, + let payload = serverPayload(from: valueData) { + return payload + } + } + + for (_, value) in error.userInfo { + if let str = value as? String, + str.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{"), + let data = str.data(using: .utf8), + let payload = serverPayload(from: data) { + return payload + } } return nil } diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift index c36dd6a..21774b4 100644 --- a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift +++ b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift @@ -1332,9 +1332,7 @@ struct PaymentCardView: View { nickname: nickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : nickname, isDefault: false ) - Task { - do { _ = try await ApiService().saveCard(payload: savePayload) } catch {} - } + _ = try await ApiService().saveCard(payload: savePayload) } let response = try await ApiService().createOrder(storeId: context.storeId, payload: payload) @@ -1761,12 +1759,11 @@ struct ChangePaymentNewCardView: View { appState.profile.cpf = cleanCpf Task { do { _ = try await ApiService().updateProfileCpf(cpf: cleanCpf) } catch {} } } - if saveCard { - let sp = SaveCardPayload(creditCard: SaveCardCreditCardPayload(holderName: holderName, number: cleanNumber, expiryMonth: expiryMonth, expiryYear: expiryYear, ccv: cvv), creditCardHolderInfo: holderInfo, nickname: nickname.trimmingCharacters(in: .whitespaces).isEmpty ? nil : nickname, isDefault: false) - Task { do { _ = try await ApiService().saveCard(payload: sp) } catch {} } - } - do { + if saveCard { + let sp = SaveCardPayload(creditCard: SaveCardCreditCardPayload(holderName: holderName, number: cleanNumber, expiryMonth: expiryMonth, expiryYear: expiryYear, ccv: cvv), creditCardHolderInfo: holderInfo, nickname: nickname.trimmingCharacters(in: .whitespaces).isEmpty ? nil : nickname, isDefault: false) + _ = try await ApiService().saveCard(payload: sp) + } let response = try await ApiService().changePaymentMethod(storeId: pixContext.storeId, orderId: pixContext.orderId, payload: payload) if response.error { SnackbarCenter.shared.show(title: response.message ?? "Pagamento recusado.", style: .error, icon: "xmark.octagon.fill", duration: 4.0); return } onConfirmed(pixContext.orderId, pixContext.shortId)