Commit
This commit is contained in:
441
Sources/PediFoods/Services/SessionStateStore.swift
Normal file
441
Sources/PediFoods/Services/SessionStateStore.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user