This commit is contained in:
Daniel Arantes Loverde
2026-02-15 08:18:06 -03:00
parent d7b8b1864a
commit 0f5ad4c268
35 changed files with 4090 additions and 309 deletions

View File

@@ -0,0 +1,51 @@
import Foundation
struct PublicCategory: Decodable {
let id: String
let name: String
let icon: 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
}
}

View File

@@ -66,11 +66,49 @@ struct StoreSummary: Decodable {
let cover: String?
let category: String?
let rating: Double?
let reviewsCount: Int?
let positiveReviews: Int?
let deliveryTime: String?
let deliveryFee: Double?
let distance: Double?
let isOpen: Bool?
let statusLabel: String?
enum CodingKeys: String, CodingKey {
case id
case name
case logo
case cover
case category
case rating
case reviewsCount
case totalReviews
case reviews
case positiveReviews
case positive_reviews
case deliveryTime
case deliveryFee
case distance
case isOpen
case statusLabel
}
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)) ?? "Loja"
logo = try? container.decode(String.self, forKey: .logo)
cover = try? container.decode(String.self, forKey: .cover)
category = try? container.decode(String.self, forKey: .category)
rating = ApiService.decodeFlexibleDouble(from: container, keys: [.rating])
reviewsCount = ApiService.decodeFlexibleInt(from: container, keys: [.reviewsCount, .totalReviews, .reviews])
positiveReviews = ApiService.decodeFlexibleInt(from: container, keys: [.positiveReviews, .positive_reviews])
deliveryTime = try? container.decode(String.self, forKey: .deliveryTime)
deliveryFee = ApiService.decodeFlexibleDouble(from: container, keys: [.deliveryFee])
distance = ApiService.decodeFlexibleDouble(from: container, keys: [.distance])
isOpen = try? container.decode(Bool.self, forKey: .isOpen)
statusLabel = try? container.decode(String.self, forKey: .statusLabel)
}
}
struct StoreInfoResult: Decodable {
@@ -107,6 +145,7 @@ struct StoreAddressInfo: Decodable {
let neighborhood: String?
let city: String?
let state: String?
let zipCode: String?
let latitude: Double?
let longitude: Double?
@@ -116,6 +155,8 @@ struct StoreAddressInfo: Decodable {
case neighborhood
case city
case state
case zipCode
case zipcode
case latitude
case longitude
}
@@ -127,26 +168,112 @@ struct StoreAddressInfo: Decodable {
neighborhood = try? container.decode(String.self, forKey: .neighborhood)
city = try? container.decode(String.self, forKey: .city)
state = try? container.decode(String.self, forKey: .state)
zipCode = (try? container.decode(String.self, forKey: .zipCode))
?? (try? container.decode(String.self, forKey: .zipcode))
latitude = ApiService.decodeFlexibleDouble(from: container, keys: [.latitude])
longitude = ApiService.decodeFlexibleDouble(from: container, keys: [.longitude])
}
}
struct StorePaymentMethodsInfo: Decodable {
let paymentOnDelivery: Bool?
let paymentOnPickup: Bool?
let acceptPix: Bool?
let acceptCash: Bool?
let acceptCreditCard: Bool?
let acceptDebitCard: Bool?
let acceptCreditVisa: Bool?
let acceptCreditMaster: Bool?
let acceptCreditElo: Bool?
let acceptCreditAmex: Bool?
let acceptCreditHipercard: Bool?
let acceptDebitVisa: Bool?
let acceptDebitMaster: Bool?
let acceptDebitElo: Bool?
let acceptVoucherAlelo: Bool?
let acceptVoucherSodexo: Bool?
let acceptVoucherTicket: Bool?
let acceptVoucherVR: Bool?
enum CodingKeys: String, CodingKey {
case paymentOnDelivery
case paymentOnPickup
case acceptPix
case acceptCash
case acceptCreditCard
case acceptDebitCard
case acceptCreditVisa
case acceptCreditMaster
case acceptCreditElo
case acceptCreditAmex
case acceptCreditHipercard
case acceptDebitVisa
case acceptDebitMaster
case acceptDebitElo
case acceptVoucherAlelo
case acceptVoucherSodexo
case acceptVoucherTicket
case acceptVoucherVR
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
paymentOnDelivery = try? container.decode(Bool.self, forKey: .paymentOnDelivery)
paymentOnPickup = try? container.decode(Bool.self, forKey: .paymentOnPickup)
acceptPix = try? container.decode(Bool.self, forKey: .acceptPix)
acceptCash = try? container.decode(Bool.self, forKey: .acceptCash)
acceptCreditCard = try? container.decode(Bool.self, forKey: .acceptCreditCard)
acceptDebitCard = try? container.decode(Bool.self, forKey: .acceptDebitCard)
acceptCreditVisa = try? container.decode(Bool.self, forKey: .acceptCreditVisa)
acceptCreditMaster = try? container.decode(Bool.self, forKey: .acceptCreditMaster)
acceptCreditElo = try? container.decode(Bool.self, forKey: .acceptCreditElo)
acceptCreditAmex = try? container.decode(Bool.self, forKey: .acceptCreditAmex)
acceptCreditHipercard = try? container.decode(Bool.self, forKey: .acceptCreditHipercard)
acceptDebitVisa = try? container.decode(Bool.self, forKey: .acceptDebitVisa)
acceptDebitMaster = try? container.decode(Bool.self, forKey: .acceptDebitMaster)
acceptDebitElo = try? container.decode(Bool.self, forKey: .acceptDebitElo)
acceptVoucherAlelo = try? container.decode(Bool.self, forKey: .acceptVoucherAlelo)
acceptVoucherSodexo = try? container.decode(Bool.self, forKey: .acceptVoucherSodexo)
acceptVoucherTicket = try? container.decode(Bool.self, forKey: .acceptVoucherTicket)
acceptVoucherVR = try? container.decode(Bool.self, forKey: .acceptVoucherVR)
}
var hasAnyCreditCard: Bool {
(acceptCreditCard ?? false)
|| (acceptCreditVisa ?? false)
|| (acceptCreditMaster ?? false)
|| (acceptCreditElo ?? false)
|| (acceptCreditAmex ?? false)
|| (acceptCreditHipercard ?? false)
}
var hasAnyDebitCard: Bool {
(acceptDebitCard ?? false)
|| (acceptDebitVisa ?? false)
|| (acceptDebitMaster ?? false)
|| (acceptDebitElo ?? false)
}
var hasAnyVoucher: Bool {
(acceptVoucherAlelo ?? false)
|| (acceptVoucherSodexo ?? false)
|| (acceptVoucherTicket ?? false)
|| (acceptVoucherVR ?? false)
}
}
struct StoreCatalogCategory: Decodable {
let id: String
let name: String
let isPizzaCategory: Bool
let pizzaConfig: StorePizzaConfig?
let products: [StoreCatalogProduct]
enum CodingKeys: String, CodingKey {
case id
case name
case isPizzaCategory
case pizzaConfig
case products
}
@@ -154,21 +281,26 @@ struct StoreCatalogCategory: Decodable {
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)) ?? "Categoria"
isPizzaCategory = (try? container.decode(Bool.self, forKey: .isPizzaCategory)) ?? false
pizzaConfig = try? container.decode(StorePizzaConfig.self, forKey: .pizzaConfig)
products = (try? container.decode([StoreCatalogProduct].self, forKey: .products)) ?? []
}
}
struct StoreCatalogProduct: Decodable, Identifiable {
let id: String
let type: String?
let name: String
let description: String?
let image: String?
let price: Double?
let originalPrice: Double?
let pizzaPrices: [String: Double]
let addonGroups: [StoreAddonGroup]
enum CodingKeys: String, CodingKey {
case id
case type
case name
case description
case desc
@@ -178,6 +310,7 @@ struct StoreCatalogProduct: Decodable, Identifiable {
case price
case originalPrice
case oldPrice
case pizzaPrices
case addonGroups
case addons
}
@@ -185,6 +318,7 @@ struct StoreCatalogProduct: Decodable, Identifiable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = (try? container.decode(String.self, forKey: .id)) ?? UUID().uuidString
type = try? container.decode(String.self, forKey: .type)
name = (try? container.decode(String.self, forKey: .name)) ?? "Produto"
description = (try? container.decode(String.self, forKey: .description)) ?? (try? container.decode(String.self, forKey: .desc))
image = (try? container.decode(String.self, forKey: .image))
@@ -192,10 +326,33 @@ struct StoreCatalogProduct: Decodable, Identifiable {
?? (try? container.decode(String.self, forKey: .photo))
price = ApiService.decodeFlexibleDouble(from: container, keys: [.price])
originalPrice = ApiService.decodeFlexibleDouble(from: container, keys: [.originalPrice, .oldPrice])
pizzaPrices = StoreCatalogProduct.decodePizzaPrices(container: container)
addonGroups = (try? container.decode([StoreAddonGroup].self, forKey: .addonGroups))
?? (try? container.decode([StoreAddonGroup].self, forKey: .addons))
?? []
}
private static func decodePizzaPrices(container: KeyedDecodingContainer<CodingKeys>) -> [String: Double] {
if let direct = try? container.decode([String: Double].self, forKey: .pizzaPrices) {
return direct
}
if let asInt = try? container.decode([String: Int].self, forKey: .pizzaPrices) {
return asInt.mapValues { Double($0) }
}
if let asString = try? container.decode([String: String].self, forKey: .pizzaPrices) {
var parsed: [String: Double] = [:]
for (key, value) in asString {
let normalized = value
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: ",", with: ".")
if let number = Double(normalized) {
parsed[key] = number
}
}
return parsed
}
return [:]
}
}
struct StoreAddonGroup: Decodable, Identifiable {
@@ -242,56 +399,6 @@ struct StoreAddonItem: Decodable, Identifiable {
}
}
struct PublicCategory: Decodable {
let id: String
let name: String
let icon: 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?

View File

@@ -0,0 +1,145 @@
import Foundation
struct CreateOrderPayload: Encodable {
let customer: CreateOrderCustomerPayload
let items: [CreateOrderItemPayload]
let total: Double
let paymentMethod: String
let deliveryType: String
let address: CreateOrderAddressPayload?
}
struct CreateOrderCustomerPayload: Encodable {
let name: String
let phone: String
let email: String
let asaasId: String?
}
struct CreateOrderItemPayload: Encodable {
let productId: String
let name: String
let qty: Int
let price: Double
let addons: [CreateOrderAddonPayload]
}
struct CreateOrderAddonPayload: Encodable {
let addonId: String
let name: String
let qty: Int
let price: Double
}
struct CreateOrderAddressPayload: Encodable {
let street: String
let number: String
let neighborhood: String
let city: String?
let state: String?
let zip: String?
let complement: String?
}
struct CreateOrderResult: Decodable {
let id: String?
let shortId: String?
let status: String?
let paymentStatus: String?
let paymentMethod: String?
let paymentPayload: CreateOrderPaymentPayload?
let payment: CreateOrderPaymentInfo?
enum CodingKeys: String, CodingKey {
case id
case shortId
case status
case paymentStatus
case paymentMethod
case paymentPayload
case payment
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try? container.decode(String.self, forKey: .id)
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)
payment = try? container.decode(CreateOrderPaymentInfo.self, forKey: .payment)
if let objectPayload = try? container.decode(CreateOrderPaymentPayload.self, forKey: .paymentPayload) {
paymentPayload = objectPayload
} else if let stringPayload = try? container.decode(String.self, forKey: .paymentPayload) {
paymentPayload = CreateOrderPaymentPayload(
copyPaste: stringPayload,
qrCodeImage: nil,
expirationDate: nil
)
} else {
paymentPayload = nil
}
}
}
struct CreateOrderPaymentInfo: Decodable {
let method: String?
let status: String?
let pix: CreateOrderPaymentPayload?
}
struct CreateOrderPaymentPayload: Decodable {
let copyPaste: String?
let qrCodeImage: String?
let expirationDate: String?
enum CodingKeys: String, CodingKey {
case copyPaste
case payload
case qrCodeImage
case encodedImage
case expirationDate
}
init(copyPaste: String?, qrCodeImage: String?, expirationDate: String?) {
self.copyPaste = copyPaste
self.qrCodeImage = qrCodeImage
self.expirationDate = expirationDate
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
copyPaste = (try? container.decode(String.self, forKey: .copyPaste))
?? (try? container.decode(String.self, forKey: .payload))
qrCodeImage = (try? container.decode(String.self, forKey: .qrCodeImage))
?? (try? container.decode(String.self, forKey: .encodedImage))
expirationDate = try? container.decode(String.self, forKey: .expirationDate)
}
}
struct ValidateDeliveryAddressPayload: Encodable {
let address: ValidateDeliveryAddressDataPayload
}
struct ValidateDeliveryAddressDataPayload: Encodable {
let street: String?
let number: String?
let neighborhood: String?
let city: String?
let state: String?
let zip: String?
let lat: Double?
let lng: Double?
}
struct ValidateDeliveryAddressResult: Decodable {
let deliveryAllowed: Bool?
let reasonCode: String?
let reasonMessage: String?
let deliveryMode: String?
let distance: Double?
let deliveryFee: Double?
let deliveryTime: String?
let sameCity: Bool?
}

View File

@@ -0,0 +1,80 @@
import Foundation
struct StorePizzaConfig: Decodable {
let sizes: [StorePizzaSize]
let doughs: [StorePizzaDough]
let crusts: [StorePizzaCrust]
enum CodingKeys: String, CodingKey {
case sizes
case doughs
case crusts
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
sizes = (try? container.decode([StorePizzaSize].self, forKey: .sizes)) ?? []
doughs = (try? container.decode([StorePizzaDough].self, forKey: .doughs)) ?? []
crusts = (try? container.decode([StorePizzaCrust].self, forKey: .crusts)) ?? []
}
}
struct StorePizzaSize: Decodable, Identifiable {
let id: String
let name: String?
let slices: Int?
let maxFlavors: Int?
enum CodingKeys: String, CodingKey {
case id
case name
case slices
case maxFlavors
}
}
struct StorePizzaDough: Decodable, Identifiable {
let id: String
let name: String?
let active: Bool?
enum CodingKeys: String, CodingKey {
case id
case name
case active
}
}
struct StorePizzaCrust: Decodable, Identifiable {
let id: String
let name: String?
let active: Bool?
let priceModifier: Double?
enum CodingKeys: String, CodingKey {
case id
case name
case active
case priceModifier
}
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)
active = try? container.decode(Bool.self, forKey: .active)
if let value = try? container.decode(Double.self, forKey: .priceModifier) {
priceModifier = value
} else if let value = try? container.decode(Int.self, forKey: .priceModifier) {
priceModifier = Double(value)
} else if let value = try? container.decode(String.self, forKey: .priceModifier) {
let normalized = value
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: ",", with: ".")
priceModifier = Double(normalized)
} else {
priceModifier = nil
}
}
}

View File

@@ -235,6 +235,18 @@ final class ApiService {
let req = ApiRequest(path: "/api/store/\(storeId)/catalog", method: "GET", module: .store, requiresAuth: true)
return try await sendEnvelope(req)
}
func createOrder(storeId: String, payload: CreateOrderPayload) async throws -> ApiEnvelope<CreateOrderResult> {
let body = try JSONEncoder().encode(payload)
let req = ApiRequest(path: "/api/store/\(storeId)/orders", method: "POST", module: .store, requiresAuth: true, body: body)
return try await sendEnvelope(req)
}
func validateDeliveryAddress(storeId: String, payload: ValidateDeliveryAddressPayload) async throws -> ApiEnvelope<ValidateDeliveryAddressResult> {
let body = try JSONEncoder().encode(payload)
let req = ApiRequest(path: "/api/store/\(storeId)/delivery/validate-address", method: "POST", module: .store, requiresAuth: true, body: body)
return try await sendEnvelope(req)
}
}
extension ApiService {
static func decodeFlexibleDouble<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> Double? {
@@ -256,4 +268,25 @@ extension ApiService {
}
return nil
}
static func decodeFlexibleInt<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> Int? {
for key in keys {
if let value = try? container.decode(Int.self, forKey: key) {
return value
}
if let asDouble = try? container.decode(Double.self, forKey: key) {
return Int(asDouble)
}
if let asString = try? container.decode(String.self, forKey: key) {
let normalized = asString
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: ".", with: "")
.replacingOccurrences(of: ",", with: "")
if let parsed = Int(normalized) {
return parsed
}
}
}
return nil
}
}

View File

@@ -0,0 +1,145 @@
import Foundation
#if canImport(UIKit)
import UIKit
typealias PlatformImage = UIImage
#elseif canImport(AppKit)
import AppKit
typealias PlatformImage = NSImage
#endif
final class AppContentCache: @unchecked Sendable {
static let shared = AppContentCache()
private struct Entry {
let value: Any
let expiry: Date
}
private var entries: [String: Entry] = [:]
private let queue = DispatchQueue(label: "com.pedifoods.content-cache", qos: .userInitiated)
private init() {}
func value<T>(for key: String, as type: T.Type = T.self) -> T? {
queue.sync {
guard let entry = entries[key] else { return nil }
if entry.expiry <= Date() {
entries.removeValue(forKey: key)
return nil
}
return entry.value as? T
}
}
func set<T>(_ value: T, for key: String, ttl: TimeInterval) {
queue.sync {
entries[key] = Entry(value: value, expiry: Date().addingTimeInterval(ttl))
}
}
func invalidate(prefix: String? = nil) {
queue.sync {
guard let prefix, prefix.isEmpty == false else {
entries.removeAll()
return
}
let keys = entries.keys.filter { $0.hasPrefix(prefix) }
for key in keys {
entries.removeValue(forKey: key)
}
}
}
}
#if canImport(UIKit) || canImport(AppKit)
final class AppImageCache: @unchecked Sendable {
static let shared = AppImageCache()
private struct Entry {
let image: PlatformImage
let expiry: Date
}
private var entries: [String: Entry] = [:]
private let queue = DispatchQueue(label: "com.pedifoods.image-cache", qos: .userInitiated)
private init() {
configureURLCacheIfNeeded()
}
func image(for url: URL, ttl: TimeInterval, forceRefresh: Bool = false) async -> PlatformImage? {
let key = url.absoluteString
let now = Date()
if forceRefresh == false {
let cached = queue.sync { entries[key] }
if let cached, cached.expiry > now {
return cached.image
}
}
var request = URLRequest(url: url)
request.timeoutInterval = 20
request.cachePolicy = forceRefresh ? .reloadIgnoringLocalCacheData : .returnCacheDataElseLoad
if forceRefresh == false,
let diskCached = URLCache.shared.cachedResponse(for: request),
let image = platformImage(from: diskCached.data) {
queue.sync {
entries[key] = Entry(image: image, expiry: now.addingTimeInterval(ttl))
}
return image
}
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let image = platformImage(from: data) else { return nil }
URLCache.shared.storeCachedResponse(CachedURLResponse(response: response, data: data), for: request)
queue.sync {
entries[key] = Entry(image: image, expiry: now.addingTimeInterval(ttl))
}
return image
} catch {
return nil
}
}
func invalidateAll() {
queue.sync {
entries.removeAll()
}
URLCache.shared.removeAllCachedResponses()
}
private func configureURLCacheIfNeeded() {
let current = URLCache.shared
let minMemoryCapacity = 64 * 1024 * 1024
let minDiskCapacity = 256 * 1024 * 1024
if current.memoryCapacity < minMemoryCapacity || current.diskCapacity < minDiskCapacity {
URLCache.shared = URLCache(memoryCapacity: minMemoryCapacity, diskCapacity: minDiskCapacity)
}
}
private func platformImage(from data: Data) -> PlatformImage? {
#if canImport(UIKit)
return UIImage(data: data)
#elseif canImport(AppKit)
return NSImage(data: data)
#else
return nil
#endif
}
}
#endif
#if !(canImport(UIKit) || canImport(AppKit))
final class AppImageCache: @unchecked Sendable {
static let shared = AppImageCache()
private init() {}
func invalidateAll() {}
}
#endif

View File

@@ -7,10 +7,37 @@ private struct PersistedAddressState: Codable {
let longitude: Double?
}
private struct PersistedCartAddonState: Codable {
let id: String
let name: String
let quantity: Int
let unitPrice: Double
}
private struct PersistedCartItemState: Codable {
let id: String
let productId: String
let storeId: String
let name: String
let imageURL: String?
let details: String?
let addons: [PersistedCartAddonState]
let quantity: Int
let unitPrice: Double
}
private struct PersistedCartState: Codable {
let storeId: String?
let storeName: String?
let items: [PersistedCartItemState]
let total: Double
}
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."
static func makeUserKey(profileId: String?, email: String?) -> String? {
let id = (profileId ?? "")
@@ -136,4 +163,81 @@ enum SessionStateStore {
static func clearActiveUser() {
UserDefaults.standard.removeObject(forKey: activeUserKey)
}
private static func cartStorageKey(for userKey: String?) -> String {
let key = (userKey ?? loadActiveUserKey() ?? "anonymous")
.trimmingCharacters(in: .whitespacesAndNewlines)
let safe = key.replacingOccurrences(of: " ", with: "_")
return cartKeyPrefix + safe
}
static func loadCart() -> CartState? {
let defaults = UserDefaults.standard
let key = cartStorageKey(for: nil)
if let data = defaults.data(forKey: key),
let decoded = try? JSONDecoder().decode(PersistedCartState.self, from: data) {
return CartState(
storeId: decoded.storeId,
storeName: decoded.storeName,
items: decoded.items.map {
CartItemState(
id: $0.id,
productId: $0.productId,
storeId: $0.storeId,
name: $0.name,
imageURL: $0.imageURL,
details: $0.details,
addons: $0.addons.map {
CartItemAddonState(
id: $0.id,
name: $0.name,
quantity: $0.quantity,
unitPrice: $0.unitPrice
)
},
quantity: $0.quantity,
unitPrice: $0.unitPrice
)
},
total: decoded.total
)
}
return nil
}
static func saveCart(_ state: CartState) {
let payload = PersistedCartState(
storeId: state.storeId,
storeName: state.storeName,
items: state.items.map {
PersistedCartItemState(
id: $0.id,
productId: $0.productId,
storeId: $0.storeId,
name: $0.name,
imageURL: $0.imageURL,
details: $0.details,
addons: $0.addons.map {
PersistedCartAddonState(
id: $0.id,
name: $0.name,
quantity: $0.quantity,
unitPrice: $0.unitPrice
)
},
quantity: $0.quantity,
unitPrice: $0.unitPrice
)
},
total: state.total
)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: cartStorageKey(for: nil))
}
static func clearCart() {
UserDefaults.standard.removeObject(forKey: cartStorageKey(for: nil))
}
}