Fix login
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
215
pedi-foods/Sources/PediFoods/Services/OrderRealtimeTracker.swift
Normal file
215
pedi-foods/Sources/PediFoods/Services/OrderRealtimeTracker.swift
Normal 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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user