This commit is contained in:
Daniel Arantes Loverde
2026-07-07 15:11:31 -03:00
parent 8b9ebdcb44
commit e51c99973f
293 changed files with 457 additions and 4919 deletions

View File

@@ -0,0 +1,441 @@
import Foundation
private struct PersistedAddressState: Codable {
let selectedId: String?
let display: String
let latitude: Double?
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
}
private struct PersistedTrackedOrdersState: Codable {
let orders: [PublicOrderResult]
}
struct OrderReviewRecord: Codable, Identifiable, Hashable {
var id: String { orderId }
let orderId: String
let storeId: String?
let shortId: String?
let storeName: String?
let storeLogoURL: String?
let createdAt: String?
let submittedAt: String
let rating: Int
let comment: String
let orderPositiveTags: [String]?
let orderImprovementTags: [String]?
let deliverySentiment: String?
let deliveryPositiveTags: [String]?
let deliveryNegativeTags: [String]?
let appNps: Int?
let platform: String?
let editableUntil: String?
let storeReplyUntil: String?
let reviewWindowExpiresAt: String?
let storeReplyMessage: String?
let storeReplyAt: String?
}
struct OrderReviewDraftState: Codable, Hashable {
var orderId: String
var orderRate: Int
var orderComment: String
var orderPositiveTags: [String]
var orderImprovementTags: [String]
var deliverySentiment: String
var deliveryPositiveTags: [String]
var deliveryNegativeTags: [String]
var appNps: Int
var platform: String
}
private struct PersistedOrderReviewsState: Codable {
let reviews: [OrderReviewRecord]
}
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."
private static let pendingCartOrderKeyPrefix = "session.cart.pending-order.v1."
private static let orderReviewsKeyPrefix = "session.orders.reviews.v1."
private static let orderReviewDraftKeyPrefix = "session.orders.review-draft.v1."
static func makeUserKey(profileId: String?, email: String?) -> String? {
let id = (profileId ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
if id.isEmpty == false {
return "id:\(id)"
}
let mail = (email ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
if mail.isEmpty == false {
return "email:\(mail)"
}
return nil
}
static func setActiveUserKey(_ userKey: String?) {
let trimmed = (userKey ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
UserDefaults.standard.removeObject(forKey: activeUserKey)
} else {
UserDefaults.standard.set(trimmed, forKey: activeUserKey)
}
}
static func loadActiveUserKey() -> String? {
let value = UserDefaults.standard.string(forKey: activeUserKey)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let value, value.isEmpty == false {
return value
}
return nil
}
private static func addressStorageKey(for userKey: String?) -> String {
let key = (userKey ?? loadActiveUserKey() ?? "anonymous")
.trimmingCharacters(in: .whitespacesAndNewlines)
let safe = key.replacingOccurrences(of: " ", with: "_")
return addressKeyPrefix + safe
}
static func loadAddress() -> AddressState? {
let defaults = UserDefaults.standard
let activeKey = loadActiveUserKey()
let scopedKey = addressStorageKey(for: activeKey)
if let data = defaults.data(forKey: scopedKey),
let decoded = try? JSONDecoder().decode(PersistedAddressState.self, from: data) {
return AddressState(
selectedId: decoded.selectedId,
display: decoded.display.isEmpty ? "Defina seu endereco" : decoded.display,
latitude: decoded.latitude,
longitude: decoded.longitude,
onboardingMessage: nil
)
}
// Backward-compatible fallback for data persisted before user scoping.
let anonymousKey = addressStorageKey(for: "anonymous")
if let data = defaults.data(forKey: anonymousKey),
let decoded = try? JSONDecoder().decode(PersistedAddressState.self, from: data) {
let recovered = AddressState(
selectedId: decoded.selectedId,
display: decoded.display.isEmpty ? "Defina seu endereco" : decoded.display,
latitude: decoded.latitude,
longitude: decoded.longitude,
onboardingMessage: nil
)
// Migrate anonymous cache into the current active user namespace.
if let activeKey, activeKey.isEmpty == false {
let payload = PersistedAddressState(
selectedId: recovered.selectedId,
display: recovered.display,
latitude: recovered.latitude,
longitude: recovered.longitude
)
if let migratedData = try? JSONEncoder().encode(payload) {
defaults.set(migratedData, forKey: scopedKey)
}
}
return recovered
}
guard let data = defaults.data(forKey: legacyAddressKey),
let decoded = try? JSONDecoder().decode(PersistedAddressState.self, from: data) else {
return nil
}
let migrated = AddressState(
selectedId: decoded.selectedId,
display: decoded.display.isEmpty ? "Defina seu endereco" : decoded.display,
latitude: decoded.latitude,
longitude: decoded.longitude,
onboardingMessage: nil
)
saveAddress(migrated)
defaults.removeObject(forKey: legacyAddressKey)
return migrated
}
static func saveAddress(_ state: AddressState) {
let payload = PersistedAddressState(
selectedId: state.selectedId,
display: state.display,
latitude: state.latitude,
longitude: state.longitude
)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: addressStorageKey(for: nil))
}
static func clearAddress() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: addressStorageKey(for: nil))
defaults.removeObject(forKey: legacyAddressKey)
}
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))
}
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
}
private static func pendingCartOrderStorageKey(for userKey: String?) -> String {
let key = (userKey ?? loadActiveUserKey() ?? "anonymous")
.trimmingCharacters(in: .whitespacesAndNewlines)
let safe = key.replacingOccurrences(of: " ", with: "_")
return pendingCartOrderKeyPrefix + safe
}
private static func orderReviewsStorageKey(for userKey: String?) -> String {
let key = (userKey ?? loadActiveUserKey() ?? "anonymous")
.trimmingCharacters(in: .whitespacesAndNewlines)
let safe = key.replacingOccurrences(of: " ", with: "_")
return orderReviewsKeyPrefix + safe
}
private static func orderReviewDraftStorageKey(for orderId: String, userKey: String?) -> String {
let scope = (userKey ?? loadActiveUserKey() ?? "anonymous")
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: " ", with: "_")
let id = orderId
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return "\(orderReviewDraftKeyPrefix)\(scope).\(id)"
}
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))
}
static func savePendingCartOrderId(_ orderId: String) {
let clean = orderId.trimmingCharacters(in: .whitespacesAndNewlines)
guard clean.isEmpty == false else {
clearPendingCartOrder()
return
}
UserDefaults.standard.set(clean, forKey: pendingCartOrderStorageKey(for: nil))
}
static func loadPendingCartOrderId() -> String? {
let value = UserDefaults.standard.string(forKey: pendingCartOrderStorageKey(for: nil))?
.trimmingCharacters(in: .whitespacesAndNewlines)
if let value, value.isEmpty == false {
return value
}
return nil
}
static func clearPendingCartOrder() {
UserDefaults.standard.removeObject(forKey: pendingCartOrderStorageKey(for: nil))
}
static func loadOrderReviews() -> [OrderReviewRecord] {
let key = orderReviewsStorageKey(for: nil)
guard let data = UserDefaults.standard.data(forKey: key),
let decoded = try? JSONDecoder().decode(PersistedOrderReviewsState.self, from: data) else {
return []
}
return decoded.reviews.sorted { lhs, rhs in
lhs.submittedAt > rhs.submittedAt
}
}
static func loadOrderReview(orderId: String) -> OrderReviewRecord? {
let normalized = orderId.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard normalized.isEmpty == false else { return nil }
return loadOrderReviews().first { review in
review.orderId.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized
}
}
static func hasOrderReview(orderId: String) -> Bool {
loadOrderReview(orderId: orderId) != nil
}
static func saveOrderReview(_ review: OrderReviewRecord) {
let cleanId = review.orderId.trimmingCharacters(in: .whitespacesAndNewlines)
guard cleanId.isEmpty == false else { return }
var reviews = loadOrderReviews()
if let index = reviews.firstIndex(where: {
$0.orderId.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == cleanId.lowercased()
}) {
reviews[index] = review
} else {
reviews.insert(review, at: 0)
}
let payload = PersistedOrderReviewsState(reviews: reviews)
guard let data = try? JSONEncoder().encode(payload) else { return }
UserDefaults.standard.set(data, forKey: orderReviewsStorageKey(for: nil))
}
static func loadOrderReviewDraft(orderId: String) -> OrderReviewDraftState? {
let key = orderReviewDraftStorageKey(for: orderId, userKey: nil)
guard let data = UserDefaults.standard.data(forKey: key),
let draft = try? JSONDecoder().decode(OrderReviewDraftState.self, from: data) else {
return nil
}
return draft
}
static func saveOrderReviewDraft(_ draft: OrderReviewDraftState) {
let key = orderReviewDraftStorageKey(for: draft.orderId, userKey: nil)
guard let data = try? JSONEncoder().encode(draft) else { return }
UserDefaults.standard.set(data, forKey: key)
}
static func clearOrderReviewDraft(orderId: String) {
let key = orderReviewDraftStorageKey(for: orderId, userKey: nil)
UserDefaults.standard.removeObject(forKey: key)
}
}