login flow

This commit is contained in:
Daniel Arantes Loverde
2026-02-12 09:44:40 -03:00
parent 3176b67914
commit 38fc71718b
12 changed files with 710 additions and 111 deletions

View File

@@ -39,6 +39,11 @@ enum NetworkError: Error, LocalizedError {
}
}
private struct ApiErrorDescriptor {
let code: String?
let message: String?
}
struct ApiRequest: Sendable {
let path: String
let method: String
@@ -166,6 +171,10 @@ private extension ApiClient {
let nsError = error as NSError
let apiMessage = serverMessage(from: nsError)
let payload = serverPayload(from: nsError)
if isSessionExpiredPayload(code: payload?.code, message: payload?.message ?? apiMessage) {
return .unauthorized(payload?.message ?? apiMessage)
}
switch nsError.code {
case 401, 403:
@@ -228,6 +237,11 @@ private extension ApiClient {
throw NetworkError.invalidResponse
}
let payload = serverPayload(from: data)
if isSessionExpiredPayload(code: payload?.code, message: payload?.message) {
throw NetworkError.unauthorized(payload?.message)
}
if http.statusCode == 429 {
let retryAfter = Int(http.value(forHTTPHeaderField: "Retry-After") ?? "")
throw NetworkError.rateLimited(retryAfter)
@@ -283,19 +297,44 @@ private extension ApiClient {
}
func serverMessage(from data: Data) -> String? {
let payload = serverPayload(from: data)
if let message = payload?.message, message.isEmpty == false {
return message
}
if let code = payload?.code, code.isEmpty == false {
return "Erro: \(code)"
}
return String(data: data, encoding: .utf8)
}
func serverPayload(from data: Data) -> ApiErrorDescriptor? {
if let envelope = try? JSONDecoder().decode(ApiEnvelope<EmptyResult>.self, from: data) {
return envelope.message
return ApiErrorDescriptor(code: envelope.code, message: envelope.message)
}
if let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
if let message = object["message"] as? String {
return message
}
if let code = object["code"] as? String {
return "Erro: \(code)"
let code = object["code"] as? String
let message = object["message"] as? String
if code != nil || message != nil {
return ApiErrorDescriptor(code: code, message: message)
}
}
return String(data: data, encoding: .utf8)
return nil
}
func isSessionExpiredPayload(code: String?, message: String?) -> Bool {
let normalizedCode = (code ?? "").lowercased()
let normalizedMessage = (message ?? "").lowercased()
if normalizedCode.contains("auth") || normalizedCode.contains("token") || normalizedCode.contains("unauthorized") {
return true
}
if normalizedMessage.contains("token") && (normalizedMessage.contains("expir") || normalizedMessage.contains("invalid") || normalizedMessage.contains("sess")) {
return true
}
return false
}
#if canImport(LCEssentials) && os(iOS)
@@ -318,6 +357,15 @@ private extension ApiClient {
return nil
}
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
}
return nil
}
#endif
func buildURL(path: String, query: [URLQueryItem]) throws -> URL {