Colapse fix

This commit is contained in:
Daniel Arantes Loverde
2026-02-09 14:41:18 -03:00
parent 54686397b9
commit 3176b67914
8 changed files with 683 additions and 127 deletions

View File

@@ -33,6 +33,7 @@ final class ApiService {
} catch let error as NetworkError {
if case .unauthorized(let message) = error {
tokenStore.clear()
NotificationCenter.default.post(name: .sessionExpired, object: message)
throw ApiServiceError.sessionExpired(message)
}
throw error
@@ -56,13 +57,13 @@ final class ApiService {
}
func requestOtp(email: String, phoneNumber: String) async throws -> ApiEnvelope<EmptyResult> {
let body = try JSONEncoder().encode(["email": email, "phoneNumber": phoneNumber])
let body = try makeLoginBody(email: email, phoneNumber: phoneNumber, otp: nil)
let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body)
return try await send(req)
}
func validateOtp(email: String, phoneNumber: String, otp: String) async throws -> ApiEnvelope<LoginResult> {
let body = try JSONEncoder().encode(["email": email, "phoneNumber": phoneNumber, "otp": otp])
let body = try makeLoginBody(email: email, phoneNumber: phoneNumber, otp: otp)
let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body)
let response: ApiEnvelope<LoginResult> = try await send(req)
if let token = response.result?.token {
@@ -71,11 +72,65 @@ final class ApiService {
return response
}
private func makeLoginBody(email: String, phoneNumber: String, otp: String?) throws -> Data {
let sanitizedEmail = email.trimmingCharacters(in: .whitespacesAndNewlines)
let sanitizedPhone = phoneNumber.trimmingCharacters(in: .whitespacesAndNewlines)
guard sanitizedEmail.isEmpty == false, sanitizedPhone.isEmpty == false else {
throw NetworkError.httpError(400, "Email e telefone são obrigatórios.")
}
var payload: [String: String] = [
"email": sanitizedEmail,
"phoneNumber": sanitizedPhone,
"phone": sanitizedPhone
]
if let otp, otp.isEmpty == false {
payload["otp"] = otp
}
guard JSONSerialization.isValidJSONObject(payload) else {
throw NetworkError.invalidResponse
}
return try JSONSerialization.data(withJSONObject: payload, options: [])
}
func profile() async throws -> ApiEnvelope<CustomerProfile> {
let req = ApiRequest(path: "/api/customer/profile", method: "GET", module: .customer, requiresAuth: true)
return try await send(req)
}
func addCustomerAddress(_ address: CustomerAddress) async throws -> ApiEnvelope<CustomerProfile> {
let currentProfile = try await profile()
guard currentProfile.error == false, let customer = currentProfile.result else {
throw NetworkError.invalidResponse
}
var addressBook = (customer.addressBook ?? []).map(CustomerAddressPayload.init(from:))
addressBook.insert(CustomerAddressPayload(from: address), at: 0)
let payload = CustomerProfileUpdatePayload(addressBook: addressBook)
let body = try JSONEncoder().encode(payload)
let req = ApiRequest(path: "/api/customer/\(customer.id)", method: "POST", module: .customer, requiresAuth: true, body: body)
return try await send(req)
}
func lookupZipCode(_ zipCode: String) async throws -> ApiEnvelope<CepLookupResult> {
let digits = zipCode.filter(\.isNumber)
let normalized = String(digits.prefix(8))
let formatted: String
if normalized.count == 8 {
let prefix = String(normalized.prefix(5))
let suffix = String(normalized.dropFirst(5))
formatted = "\(prefix)-\(suffix)"
} else {
formatted = normalized
}
let req = ApiRequest(path: "/api/public/cep/\(formatted)", method: "GET", module: .none, requiresAuth: true)
return try await send(req)
}
// MARK: - Stores
func listStores(lat: Double, lng: Double, category: String? = nil, search: String? = nil) async throws -> ApiEnvelope<[StoreSummary]> {
@@ -166,3 +221,166 @@ struct StoreSummary: Decodable {
let isOpen: Bool?
let statusLabel: String?
}
struct CustomerProfileUpdatePayload: Encodable {
let addressBook: [CustomerAddressPayload]
enum CodingKeys: String, CodingKey {
case addressBook = "address_book"
}
}
struct CustomerAddressPayload: Encodable {
let label: String?
let address: String?
let number: String?
let complement: String?
let neighborhood: String?
let city: String?
let state: String?
let zipCode: String?
let latLong: [Double]?
enum CodingKeys: String, CodingKey {
case label
case address
case number
case complement
case neighborhood
case city
case state
case zipCode
case latLong = "lat_long"
}
init(from address: CustomerAddress) {
self.label = address.label
self.address = address.address
self.number = address.number
self.complement = address.complement
self.neighborhood = address.neighborhood
self.city = address.city
self.state = address.state
self.zipCode = address.zipCode
self.latLong = address.latLong
}
}
struct CepLookupResult: Decodable {
let zipCode: String?
let street: String?
let neighborhood: String?
let city: String?
let state: String?
let complement: String?
let latitude: Double?
let longitude: Double?
enum CodingKeys: String, CodingKey {
case zipCode
case cep
case zip
case normalized
case raw
case street
case logradouro
case address
case neighborhood
case bairro
case district
case city
case cidade
case localidade
case state
case estado
case uf
case complement
case complemento
case latitude
case lat
case longitude
case lng
}
enum NormalizedKeys: String, CodingKey {
case cep
case logradouro
case bairro
case cidade
case uf
case latitude
case longitude
}
enum RawKeys: String, CodingKey {
case cep
case address
case district
case city
case state
case lat
case lng
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let normalizedContainer = try? container.nestedContainer(keyedBy: NormalizedKeys.self, forKey: .normalized)
let rawContainer = try? container.nestedContainer(keyedBy: RawKeys.self, forKey: .raw)
let directZip = CepLookupResult.decodeString(from: container, keys: [.zipCode, .cep, .zip])
let directStreet = CepLookupResult.decodeString(from: container, keys: [.street, .logradouro, .address])
let directNeighborhood = CepLookupResult.decodeString(from: container, keys: [.neighborhood, .bairro, .district])
let directCity = CepLookupResult.decodeString(from: container, keys: [.city, .cidade, .localidade])
let directState = CepLookupResult.decodeString(from: container, keys: [.state, .estado, .uf])
let directComplement = CepLookupResult.decodeString(from: container, keys: [.complement, .complemento])
let directLatitude = CepLookupResult.decodeFlexibleDouble(from: container, keys: [.latitude, .lat])
let directLongitude = CepLookupResult.decodeFlexibleDouble(from: container, keys: [.longitude, .lng])
let normalizedZip = normalizedContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.cep]) }
let normalizedStreet = normalizedContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.logradouro]) }
let normalizedNeighborhood = normalizedContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.bairro]) }
let normalizedCity = normalizedContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.cidade]) }
let normalizedState = normalizedContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.uf]) }
let normalizedLatitude = normalizedContainer.flatMap { CepLookupResult.decodeFlexibleDouble(from: $0, keys: [.latitude]) }
let normalizedLongitude = normalizedContainer.flatMap { CepLookupResult.decodeFlexibleDouble(from: $0, keys: [.longitude]) }
let rawZip = rawContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.cep]) }
let rawStreet = rawContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.address]) }
let rawNeighborhood = rawContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.district]) }
let rawCity = rawContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.city]) }
let rawState = rawContainer.flatMap { CepLookupResult.decodeString(from: $0, keys: [.state]) }
let rawLatitude = rawContainer.flatMap { CepLookupResult.decodeFlexibleDouble(from: $0, keys: [.lat]) }
let rawLongitude = rawContainer.flatMap { CepLookupResult.decodeFlexibleDouble(from: $0, keys: [.lng]) }
zipCode = directZip ?? normalizedZip ?? rawZip
street = directStreet ?? normalizedStreet ?? rawStreet
neighborhood = directNeighborhood ?? normalizedNeighborhood ?? rawNeighborhood
city = directCity ?? normalizedCity ?? rawCity
state = directState ?? normalizedState ?? rawState
complement = directComplement
latitude = directLatitude ?? normalizedLatitude ?? rawLatitude
longitude = directLongitude ?? normalizedLongitude ?? rawLongitude
}
private static func decodeString<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> String? {
for key in keys {
if let value = try? container.decode(String.self, forKey: key) {
return value
}
}
return nil
}
private static func decodeFlexibleDouble<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> Double? {
for key in keys {
if let value = try? container.decode(Double.self, forKey: key) {
return value
}
if let valueAsString = try? container.decode(String.self, forKey: key),
let parsed = Double(valueAsString.replacingOccurrences(of: ",", with: ".")) {
return parsed
}
}
return nil
}
}