Fix login

This commit is contained in:
Daniel Arantes Loverde
2026-02-21 18:51:39 -03:00
parent 0f5ad4c268
commit 3a3dc7217b
17 changed files with 1296 additions and 91 deletions

View File

@@ -10,8 +10,8 @@ enum ApiModule: Sendable {
enum ApiConfig {
static var baseURL: URL {
let raw = ProcessInfo.processInfo.environment["ATOMENTA_API_URL"] ?? "http://127.0.0.1:3001"
return URL(string: raw) ?? URL(string: "http://127.0.0.1:3001")!
let raw = ProcessInfo.processInfo.environment["ATOMENTA_API_URL"] ?? "https://atomenta.com.br"
return URL(string: raw) ?? URL(string: "https://atomenta.com.br")!
}
// Tokens provided by backend modules

View File

@@ -0,0 +1,287 @@
import Foundation
struct AppOrderSummary: Decodable, Identifiable {
let id: String
let shortId: String?
let total: Double?
let status: String?
let paymentStatus: String?
let paymentMethod: String?
let deliveryType: String?
let storeName: String?
let createdAt: String?
let updatedAt: String?
enum CodingKeys: String, CodingKey {
case id
case shortId
case total
case status
case paymentStatus
case paymentMethod
case deliveryType
case storeName
case createdAt
case updatedAt
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = (try? container.decode(String.self, forKey: .id)) ?? UUID().uuidString
shortId = try? container.decode(String.self, forKey: .shortId)
total = ApiService.decodeFlexibleDouble(from: container, keys: [.total])
status = try? container.decode(String.self, forKey: .status)
paymentStatus = try? container.decode(String.self, forKey: .paymentStatus)
paymentMethod = try? container.decode(String.self, forKey: .paymentMethod)
deliveryType = try? container.decode(String.self, forKey: .deliveryType)
storeName = try? container.decode(String.self, forKey: .storeName)
createdAt = try? container.decode(String.self, forKey: .createdAt)
updatedAt = try? container.decode(String.self, forKey: .updatedAt)
}
}
struct PublicOrderResult: Codable, Identifiable {
let id: String
let shortId: String?
let status: String?
let paymentStatus: String?
let paymentMethod: String?
let deliveryType: String?
let total: Double?
let storeName: String?
let createdAt: String?
let updatedAt: String?
let otp: String?
let customerOtp: String?
let confirmOtp: String?
let items: [PublicOrderItem]
let timeline: [PublicOrderTimelineEvent]
enum CodingKeys: String, CodingKey {
case id
case shortId
case status
case paymentStatus
case paymentMethod
case deliveryType
case total
case storeName
case createdAt
case updatedAt
case otp
case customerOtp
case confirmOtp
case items
case timeline
}
init(
id: String,
shortId: String? = nil,
status: String? = nil,
paymentStatus: String? = nil,
paymentMethod: String? = nil,
deliveryType: String? = nil,
total: Double? = nil,
storeName: String? = nil,
createdAt: String? = nil,
updatedAt: String? = nil,
otp: String? = nil,
customerOtp: String? = nil,
confirmOtp: String? = nil,
items: [PublicOrderItem] = [],
timeline: [PublicOrderTimelineEvent] = []
) {
self.id = id
self.shortId = shortId
self.status = status
self.paymentStatus = paymentStatus
self.paymentMethod = paymentMethod
self.deliveryType = deliveryType
self.total = total
self.storeName = storeName
self.createdAt = createdAt
self.updatedAt = updatedAt
self.otp = otp
self.customerOtp = customerOtp
self.confirmOtp = confirmOtp
self.items = items
self.timeline = timeline
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = (try? container.decode(String.self, forKey: .id)) ?? UUID().uuidString
shortId = try? container.decode(String.self, forKey: .shortId)
status = try? container.decode(String.self, forKey: .status)
paymentStatus = try? container.decode(String.self, forKey: .paymentStatus)
paymentMethod = try? container.decode(String.self, forKey: .paymentMethod)
deliveryType = try? container.decode(String.self, forKey: .deliveryType)
total = ApiService.decodeFlexibleDouble(from: container, keys: [.total])
storeName = try? container.decode(String.self, forKey: .storeName)
createdAt = try? container.decode(String.self, forKey: .createdAt)
updatedAt = try? container.decode(String.self, forKey: .updatedAt)
otp = try? container.decode(String.self, forKey: .otp)
customerOtp = try? container.decode(String.self, forKey: .customerOtp)
confirmOtp = try? container.decode(String.self, forKey: .confirmOtp)
items = (try? container.decode([PublicOrderItem].self, forKey: .items)) ?? []
timeline = (try? container.decode([PublicOrderTimelineEvent].self, forKey: .timeline)) ?? []
}
var displayOtpCode: String? {
let values = [customerOtp, otp, confirmOtp]
for value in values {
let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty == false { return trimmed }
}
return nil
}
var isInDeliveryRoute: Bool {
let normalized = (status ?? "").uppercased()
if normalized.contains("OUT_FOR_DELIVERY") { return true }
if normalized.contains("EM_ROTA") { return true }
if normalized.contains("ON_ROUTE") { return true }
if normalized.contains("ROTA") { return true }
return false
}
var isFinalStatus: Bool {
let normalized = (status ?? "").uppercased()
return normalized == "COMPLETED" || normalized == "CANCELED" || normalized == "REFUNDED"
}
var isPaymentConfirmed: Bool {
let payment = (paymentStatus ?? "").uppercased()
let currentStatus = (status ?? "").uppercased()
if Self.looksConfirmed(payment) || Self.looksConfirmed(currentStatus) {
return true
}
// Fallback: alguns ambientes atualizam apenas a timeline primeiro.
if timeline.contains(where: { event in
let statusValue = (event.status ?? "").uppercased()
let messageValue = (event.message ?? "").uppercased()
return Self.looksConfirmed(statusValue) || Self.looksConfirmed(messageValue)
}) {
return true
}
return false
}
private static func looksConfirmed(_ value: String) -> Bool {
if value.isEmpty { return false }
if value.contains("PENDING") || value.contains("AWAIT") { return false }
if value.contains("FAILED") || value.contains("ERROR") { return false }
if value.contains("CANCEL") || value.contains("REFUND") { return false }
if value.contains("CONFIRM") { return true }
if value.contains("APPROV") { return true }
if value.contains("PAID") { return true }
if value.contains("RECEIV") { return true }
return value == "SUCCESS" || value == "DONE"
}
}
struct PublicOrderItem: Codable, Identifiable {
let id: String
let name: String?
let qty: Int?
let price: Double?
enum CodingKeys: String, CodingKey {
case id
case name
case qty
case quantity
case price
}
init(id: String = UUID().uuidString, name: String?, qty: Int?, price: Double?) {
self.id = id
self.name = name
self.qty = qty
self.price = price
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = (try? container.decode(String.self, forKey: .id)) ?? UUID().uuidString
name = try? container.decode(String.self, forKey: .name)
qty = ApiService.decodeFlexibleInt(from: container, keys: [.qty, .quantity])
price = ApiService.decodeFlexibleDouble(from: container, keys: [.price])
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encodeIfPresent(name, forKey: .name)
try container.encodeIfPresent(qty, forKey: .qty)
try container.encodeIfPresent(price, forKey: .price)
}
}
struct PublicOrderTimelineEvent: Codable, Identifiable {
let id: String
let status: String?
let message: String?
let time: String?
enum CodingKeys: String, CodingKey {
case id
case status
case message
case time
case createdAt
case updatedAt
}
init(id: String = UUID().uuidString, status: String?, message: String?, time: String?) {
self.id = id
self.status = status
self.message = message
self.time = time
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = (try? container.decode(String.self, forKey: .id)) ?? UUID().uuidString
status = try? container.decode(String.self, forKey: .status)
message = try? container.decode(String.self, forKey: .message)
time = (try? container.decode(String.self, forKey: .time))
?? (try? container.decode(String.self, forKey: .createdAt))
?? (try? container.decode(String.self, forKey: .updatedAt))
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encodeIfPresent(status, forKey: .status)
try container.encodeIfPresent(message, forKey: .message)
try container.encodeIfPresent(time, forKey: .time)
}
}
struct OrderRealtimeUpdate: Decodable {
let id: String?
let shortId: String?
let storeId: String?
let userId: String?
let status: String?
let paymentStatus: String?
let updatedAt: String?
}
extension CreateOrderResult {
func asPublicOrderResult() -> PublicOrderResult {
PublicOrderResult(
id: id ?? UUID().uuidString,
shortId: shortId,
status: status,
paymentStatus: paymentStatus,
paymentMethod: paymentMethod
)
}
}

View File

@@ -247,6 +247,16 @@ final class ApiService {
let req = ApiRequest(path: "/api/store/\(storeId)/delivery/validate-address", method: "POST", module: .store, requiresAuth: true, body: body)
return try await sendEnvelope(req)
}
func listOrders() async throws -> ApiEnvelope<[AppOrderSummary]> {
let req = ApiRequest(path: "/api/app/orders", method: "GET", module: .app, requiresAuth: true)
return try await sendEnvelope(req)
}
func publicOrder(orderId: String) async throws -> ApiEnvelope<PublicOrderResult> {
let req = ApiRequest(path: "/api/public/orders/\(orderId)", method: "GET", module: .none, requiresAuth: true)
return try await sendEnvelope(req)
}
}
extension ApiService {
static func decodeFlexibleDouble<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> Double? {

View File

@@ -0,0 +1,215 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@MainActor
final class OrderRealtimeTracker {
private var pollingTask: Task<Void, Never>? = 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 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 {
let response = try await ApiService().publicOrder(orderId: orderId)
guard response.error == false, let order = response.result else { return nil }
SessionStateStore.saveTrackedOrder(order)
onOrderUpdated?(order)
return order
} catch {
return nil
}
}
}
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
}

View File

@@ -33,11 +33,16 @@ private struct PersistedCartState: Codable {
let total: Double
}
private struct PersistedTrackedOrdersState: Codable {
let orders: [PublicOrderResult]
}
enum SessionStateStore {
private static let legacyAddressKey = "session.address.state.v1"
private static let addressKeyPrefix = "session.address.state.v2."
private static let activeUserKey = "session.active.user.v1"
private static let cartKeyPrefix = "session.cart.state.v1."
private static let trackedOrdersKeyPrefix = "session.orders.tracking.v1."
static func makeUserKey(profileId: String?, email: String?) -> String? {
let id = (profileId ?? "")
@@ -240,4 +245,44 @@ enum SessionStateStore {
static func clearCart() {
UserDefaults.standard.removeObject(forKey: cartStorageKey(for: nil))
}
private static func trackedOrdersStorageKey(for userKey: String?) -> String {
let key = (userKey ?? loadActiveUserKey() ?? "anonymous")
.trimmingCharacters(in: .whitespacesAndNewlines)
let safe = key.replacingOccurrences(of: " ", with: "_")
return trackedOrdersKeyPrefix + safe
}
static func loadTrackedOrders() -> [PublicOrderResult] {
let defaults = UserDefaults.standard
let key = trackedOrdersStorageKey(for: nil)
guard let data = defaults.data(forKey: key),
let decoded = try? JSONDecoder().decode(PersistedTrackedOrdersState.self, from: data) else {
return []
}
return decoded.orders
}
static func loadTrackedOrder(orderId: String) -> PublicOrderResult? {
loadTrackedOrders().first(where: { $0.id == orderId })
}
static func saveTrackedOrder(_ order: PublicOrderResult) {
var orders = loadTrackedOrders()
if let index = orders.firstIndex(where: { $0.id == order.id }) {
orders[index] = order
} else {
orders.insert(order, at: 0)
}
if orders.count > 60 {
orders = Array(orders.prefix(60))
}
let payload = PersistedTrackedOrdersState(orders: orders)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: trackedOrdersStorageKey(for: nil))
}
static func clearTrackedOrders() {
UserDefaults.standard.removeObject(forKey: trackedOrdersStorageKey(for: nil))
}
}