Payments
This commit is contained in:
@@ -40,6 +40,13 @@ struct HomeView: View {
|
||||
.padding(.top, headerExpandedHeight + contentTopSpacing)
|
||||
.padding(.bottom, contentBottomSpacing)
|
||||
}
|
||||
.refreshable {
|
||||
await bootstrapStoresFlow(
|
||||
forceLocationRefresh: true,
|
||||
category: selectedCategory == "all" ? nil : selectedCategory,
|
||||
refreshCategories: true
|
||||
)
|
||||
}
|
||||
.background(scrollOffsetObserver)
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
@@ -73,6 +80,20 @@ struct HomeView: View {
|
||||
}
|
||||
collapseBaseOffset = scrollOffset
|
||||
}
|
||||
.onChange(of: addressCacheScope) { _, _ in
|
||||
guard hasRequestedLocation else { return }
|
||||
Task {
|
||||
AppContentCache.shared.invalidate(prefix: "stores:")
|
||||
AppContentCache.shared.invalidate(prefix: "store-info:")
|
||||
AppContentCache.shared.invalidate(prefix: "store-catalog:")
|
||||
AppImageCache.shared.invalidateAll()
|
||||
await bootstrapStoresFlow(
|
||||
forceLocationRefresh: true,
|
||||
category: selectedCategory == "all" ? nil : selectedCategory,
|
||||
refreshCategories: true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var contentStack: some View {
|
||||
@@ -309,18 +330,32 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var sortedStores: [StoreSummary] {
|
||||
private var storesByPositiveReviews: [StoreSummary] {
|
||||
stores.sorted { lhs, rhs in
|
||||
(lhs.distance ?? .greatestFiniteMagnitude) < (rhs.distance ?? .greatestFiniteMagnitude)
|
||||
let lhsPositive = lhs.positiveReviews ?? lhs.reviewsCount ?? 0
|
||||
let rhsPositive = rhs.positiveReviews ?? rhs.reviewsCount ?? 0
|
||||
if lhsPositive != rhsPositive {
|
||||
return lhsPositive > rhsPositive
|
||||
}
|
||||
|
||||
let lhsRating = lhs.rating ?? 0
|
||||
let rhsRating = rhs.rating ?? 0
|
||||
if lhsRating != rhsRating {
|
||||
return lhsRating > rhsRating
|
||||
}
|
||||
|
||||
return (lhs.distance ?? .greatestFiniteMagnitude) < (rhs.distance ?? .greatestFiniteMagnitude)
|
||||
}
|
||||
}
|
||||
|
||||
private var featuredStoresCards: [FeaturedStoreCardModel] {
|
||||
Array(sortedStores.prefix(6)).map(mapStoreToCard)
|
||||
Array(storesByPositiveReviews.prefix(5)).map(mapStoreToCard)
|
||||
}
|
||||
|
||||
private var nearbyStoreCards: [FeaturedStoreCardModel] {
|
||||
Array(sortedStores.prefix(20)).map(mapStoreToCard)
|
||||
let featuredIds = Set(storesByPositiveReviews.prefix(5).map(\.id))
|
||||
let remaining = storesByPositiveReviews.filter { featuredIds.contains($0.id) == false }
|
||||
return Array(remaining.prefix(20)).map(mapStoreToCard)
|
||||
}
|
||||
|
||||
private func mapStoreToCard(_ store: StoreSummary) -> FeaturedStoreCardModel {
|
||||
@@ -330,7 +365,7 @@ struct HomeView: View {
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
rating: store.rating ?? 0,
|
||||
reviews: "0",
|
||||
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
|
||||
distance: formatDistance(store.distance),
|
||||
category: store.category ?? "Loja",
|
||||
promoText: nil,
|
||||
@@ -338,20 +373,24 @@ struct HomeView: View {
|
||||
iconName: "storefront",
|
||||
imageURL: coverURL ?? logoURL,
|
||||
logoURL: logoURL,
|
||||
coverURL: coverURL
|
||||
coverURL: coverURL,
|
||||
isOpen: store.isOpen ?? true,
|
||||
statusLabel: store.statusLabel
|
||||
)
|
||||
}
|
||||
|
||||
private func resolveStoreMediaURL(_ raw: String?) -> String? {
|
||||
guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
raw.isEmpty == false else { return nil }
|
||||
guard var normalized = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
normalized.isEmpty == false else { return nil }
|
||||
|
||||
if raw.lowercased().hasPrefix("http://") || raw.lowercased().hasPrefix("https://") {
|
||||
return raw
|
||||
normalized = normalized.replacingOccurrences(of: "\\/", with: "/")
|
||||
let lower = normalized.lowercased()
|
||||
if lower.hasPrefix("http://") || lower.hasPrefix("https://") || lower.hasPrefix("data:image") {
|
||||
return normalized
|
||||
}
|
||||
|
||||
let base = ApiConfig.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
|
||||
let path = normalized.hasPrefix("/") ? normalized : "/\(normalized)"
|
||||
return "\(base)\(path)"
|
||||
}
|
||||
|
||||
@@ -385,6 +424,26 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
do {
|
||||
let storesCacheKey = homeStoresCacheKey(
|
||||
lat: coordinate?.0,
|
||||
lng: coordinate?.1,
|
||||
category: category
|
||||
)
|
||||
|
||||
if forceLocationRefresh == false,
|
||||
let cachedStores: [StoreSummary] = AppContentCache.shared.value(for: storesCacheKey, as: [StoreSummary].self) {
|
||||
isLoadingStores = false
|
||||
stores = cachedStores
|
||||
if refreshCategories || (category == nil && categories.count <= 1) {
|
||||
await loadHomeCategories(withFallbackStores: cachedStores)
|
||||
if categories.contains(where: { $0.id == selectedCategory }) == false {
|
||||
selectedCategory = "all"
|
||||
}
|
||||
}
|
||||
storesError = nil
|
||||
return
|
||||
}
|
||||
|
||||
let response = try await ApiService().listStores(
|
||||
lat: coordinate?.0,
|
||||
lng: coordinate?.1,
|
||||
@@ -398,6 +457,7 @@ struct HomeView: View {
|
||||
}
|
||||
let results = response.result ?? []
|
||||
stores = results
|
||||
AppContentCache.shared.set(results, for: storesCacheKey, ttl: 180)
|
||||
if refreshCategories || (category == nil && categories.count <= 1) {
|
||||
await loadHomeCategories(withFallbackStores: results)
|
||||
if categories.contains(where: { $0.id == selectedCategory }) == false {
|
||||
@@ -412,7 +472,6 @@ struct HomeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var scrollOffsetObserver: some View {
|
||||
#if os(iOS)
|
||||
@@ -427,4 +486,19 @@ struct HomeView: View {
|
||||
EmptyView()
|
||||
#endif
|
||||
}
|
||||
|
||||
private var addressCacheScope: String {
|
||||
let selected = appState.address.selectedId ?? "nil"
|
||||
let display = appState.address.display
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased()
|
||||
return "\(selected)|\(display)"
|
||||
}
|
||||
|
||||
private func homeStoresCacheKey(lat: Double?, lng: Double?, category: String?) -> String {
|
||||
let normalizedCategory = category?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? "all"
|
||||
let latKey = lat.map { String(format: "%.4f", $0) } ?? "nil"
|
||||
let lngKey = lng.map { String(format: "%.4f", $0) } ?? "nil"
|
||||
return "stores:\(latKey):\(lngKey):\(normalizedCategory)"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user