Payments
This commit is contained in:
@@ -22,9 +22,11 @@ struct StoreDetailView: View {
|
||||
@State var categories: [StoreCatalogCategory] = []
|
||||
@State var selectedCategoryId: String? = nil
|
||||
@State var selectedProduct: StoreCatalogProduct? = nil
|
||||
@State var selectedPizzaCategoryId: String? = nil
|
||||
@State var showSwitchStoreAlert = false
|
||||
@State var pendingCartItem: CartItemState? = nil
|
||||
@State var pendingProductSheet: StoreCatalogProduct? = nil
|
||||
@State var pendingPizzaCategoryId: String? = nil
|
||||
@State var pendingCartAction: CartAction = .add
|
||||
@State var didLoad = false
|
||||
@State var categoryHeaderOffsets: [String: CGFloat] = [:]
|
||||
@@ -32,7 +34,8 @@ struct StoreDetailView: View {
|
||||
@State var scrollOffsetY: CGFloat = 0
|
||||
|
||||
let cardTopInset: CGFloat = 168
|
||||
let summaryCardHeight: CGFloat = 170
|
||||
let summaryCardBaseHeight: CGFloat = 170
|
||||
let closedBannerHeight: CGFloat = 44
|
||||
let coverVisibleUntilY: CGFloat = 253
|
||||
let storeLogoSize: CGFloat = 84
|
||||
|
||||
@@ -53,6 +56,9 @@ struct StoreDetailView: View {
|
||||
sectionedProducts
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
await loadStoreData(forceRefresh: true)
|
||||
}
|
||||
.ignoresSafeArea(edges: .top)
|
||||
.background(ScrollOffsetReader(offsetY: $scrollOffsetY))
|
||||
}
|
||||
@@ -64,6 +70,7 @@ struct StoreDetailView: View {
|
||||
.zIndex(20)
|
||||
}
|
||||
}
|
||||
.saturation(isStoreOpen ? 1 : 0)
|
||||
}
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
@@ -72,7 +79,7 @@ struct StoreDetailView: View {
|
||||
.task {
|
||||
guard didLoad == false else { return }
|
||||
didLoad = true
|
||||
await loadStoreData()
|
||||
await loadStoreData(forceRefresh: false)
|
||||
}
|
||||
.sheet(item: $selectedProduct) { product in
|
||||
NavigationStack {
|
||||
@@ -84,15 +91,52 @@ struct StoreDetailView: View {
|
||||
currentQuantity(forCartItemId: itemId)
|
||||
},
|
||||
onAdd: { item in
|
||||
guard isStoreOpen else {
|
||||
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
requestSetCartItem(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(
|
||||
isPresented: Binding(
|
||||
get: { selectedPizzaCategoryId != nil },
|
||||
set: { isPresented in
|
||||
if isPresented == false {
|
||||
selectedPizzaCategoryId = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
if let category = categories.first(where: { $0.id == selectedPizzaCategoryId }) {
|
||||
NavigationStack {
|
||||
PizzaProductDetailSheet(
|
||||
category: category,
|
||||
storeId: storeId,
|
||||
resolveImageURL: { raw in resolvedURL(raw) },
|
||||
currentQuantityForItemId: { itemId in
|
||||
currentQuantity(forCartItemId: itemId)
|
||||
},
|
||||
onAdd: { item in
|
||||
guard isStoreOpen else {
|
||||
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
requestSetCartItem(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.alert("Trocar de loja?", isPresented: $showSwitchStoreAlert) {
|
||||
Button("Cancelar", role: .cancel) {
|
||||
pendingCartItem = nil
|
||||
pendingProductSheet = nil
|
||||
pendingPizzaCategoryId = nil
|
||||
}
|
||||
Button("Limpar carrinho e adicionar", role: .destructive) {
|
||||
appState.cart.clear()
|
||||
@@ -106,9 +150,13 @@ struct StoreDetailView: View {
|
||||
case .openProductSheet:
|
||||
guard let pendingProductSheet else { return }
|
||||
selectedProduct = pendingProductSheet
|
||||
case .openPizzaSheet:
|
||||
guard let pendingPizzaCategoryId else { return }
|
||||
selectedPizzaCategoryId = pendingPizzaCategoryId
|
||||
}
|
||||
self.pendingCartItem = nil
|
||||
self.pendingProductSheet = nil
|
||||
self.pendingPizzaCategoryId = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Seu carrinho tem itens de outra loja. Deseja limpar o carrinho atual para adicionar este produto?")
|
||||
@@ -193,34 +241,44 @@ struct StoreDetailView: View {
|
||||
}
|
||||
|
||||
private var summaryCard: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(storeName)
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.lineLimit(2)
|
||||
.padding(.top, 30)
|
||||
VStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(storeName)
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.lineLimit(2)
|
||||
.padding(.top, 30)
|
||||
|
||||
Text(storeSubtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.lineLimit(1)
|
||||
Text(storeSubtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ratingChip
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ratingChip
|
||||
HStack(spacing: 0) {
|
||||
statItem(title: "TEMPO", value: info?.deliveryTime ?? "25-35 min")
|
||||
Divider().frame(height: 34)
|
||||
statItem(title: "ENTREGA", value: deliveryValueLabel)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
statItem(title: "TEMPO", value: info?.deliveryTime ?? "25-35 min")
|
||||
Divider().frame(height: 34)
|
||||
statItem(title: "ENTREGA", value: deliveryValueLabel)
|
||||
if isStoreOpen == false {
|
||||
Text(closedStoreBannerText)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(Color.white)
|
||||
.frame(maxWidth: .infinity, minHeight: closedBannerHeight)
|
||||
.background(AppColors.brandDark)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(height: summaryCardHeight)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
|
||||
@@ -256,8 +314,8 @@ struct StoreDetailView: View {
|
||||
ForEach(categories, id: \.id) { category in
|
||||
Section {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(category.products) { product in
|
||||
productCard(product)
|
||||
ForEach(listItems(for: category)) { item in
|
||||
productCard(item, in: category)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -331,16 +389,19 @@ struct StoreDetailView: View {
|
||||
max(8, safeTop - 44)
|
||||
}
|
||||
|
||||
private func productCard(_ product: StoreCatalogProduct) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
private func productCard(_ item: StoreCatalogListItem, in category: StoreCatalogCategory) -> some View {
|
||||
let product = item.product
|
||||
let hasSelectableAddons = product.addonGroups.contains { $0.items.isEmpty == false }
|
||||
|
||||
return HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(product.name)
|
||||
Text(item.title)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
if let description = product.description, description.isEmpty == false {
|
||||
if let description = item.description, description.isEmpty == false {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
@@ -348,7 +409,7 @@ struct StoreDetailView: View {
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
Text(formatCurrency(product.price))
|
||||
Text(listPriceLabel(for: product, in: category))
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
@@ -356,18 +417,27 @@ struct StoreDetailView: View {
|
||||
Spacer()
|
||||
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
AsyncStoreImage(imageURL: resolvedURL(product.image))
|
||||
AsyncStoreImage(imageURL: resolvedURL(item.imageURL))
|
||||
.frame(width: 92, height: 92)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
Button {
|
||||
if product.addonGroups.isEmpty {
|
||||
guard isStoreOpen else {
|
||||
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
if item.isPizzaSummary, let pizzaCategoryId = item.pizzaCategoryId {
|
||||
requestOpenPizzaSheet(categoryId: pizzaCategoryId)
|
||||
return
|
||||
}
|
||||
if hasSelectableAddons == false {
|
||||
let basePrice = product.price ?? 0
|
||||
let item = CartItemState(
|
||||
id: "\(storeId)::\(product.id)::base",
|
||||
productId: product.id,
|
||||
storeId: storeId,
|
||||
name: product.name,
|
||||
imageURL: resolvedURL(product.image),
|
||||
quantity: 1,
|
||||
unitPrice: basePrice
|
||||
)
|
||||
@@ -384,7 +454,7 @@ struct StoreDetailView: View {
|
||||
.background(AppColors.tertiary)
|
||||
.clipShape(Circle())
|
||||
|
||||
let qty = quantityInCart(for: product.id)
|
||||
let qty = quantityInCart(for: item)
|
||||
if qty > 0 {
|
||||
Text("\(qty)")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
@@ -399,14 +469,31 @@ struct StoreDetailView: View {
|
||||
}
|
||||
.offset(x: 3, y: -3)
|
||||
.frame(width: 30, height: 30)
|
||||
.contentShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.frame(width: 30, height: 30)
|
||||
.disabled(isStoreOpen == false)
|
||||
.opacity(isStoreOpen ? 1 : 0.65)
|
||||
.offset(x: 7, y: 7)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
.contentShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
.onTapGesture {
|
||||
guard isStoreOpen else {
|
||||
SnackbarCenter.shared.show(title: "Loja fechada no momento.", style: .warning, icon: "moon.zzz.fill", duration: 2.0)
|
||||
return
|
||||
}
|
||||
if item.isPizzaSummary, let pizzaCategoryId = item.pizzaCategoryId {
|
||||
requestOpenPizzaSheet(categoryId: pizzaCategoryId)
|
||||
return
|
||||
}
|
||||
guard hasSelectableAddons else { return }
|
||||
requestOpenProductSheet(product)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user