diff --git a/pedi-foods/Darwin/Assets.xcassets/LaunchBackground.colorset/Contents.json b/pedi-foods/Darwin/Assets.xcassets/LaunchBackground.colorset/Contents.json
new file mode 100644
index 0000000..35fe5b3
--- /dev/null
+++ b/pedi-foods/Darwin/Assets.xcassets/LaunchBackground.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.969",
+ "green" : "0.961",
+ "red" : "0.953"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.102",
+ "green" : "0.102",
+ "red" : "0.102"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json b/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json
new file mode 100644
index 0000000..162c776
--- /dev/null
+++ b/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "placeholder-pizza.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png b/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png
new file mode 100644
index 0000000..e4072c0
Binary files /dev/null and b/pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png differ
diff --git a/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj b/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj
index 416a597..fd9fbd3 100644
--- a/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj
+++ b/pedi-foods/Darwin/PediFoods.xcodeproj/project.pbxproj
@@ -162,8 +162,6 @@
};
/* End PBXResourcesBuildPhase section */
-/* End PBXShellScriptBuildPhase section */
-
/* Begin PBXSourcesBuildPhase section */
499CD43A2AC5B799001AE8D8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -269,7 +267,7 @@
repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 1.0.7;
+ minimumVersion = 1.0.10;
};
};
/* End XCRemoteSwiftPackageReference section */
diff --git a/pedi-foods/Darwin/Sources/LaunchScreen.storyboard b/pedi-foods/Darwin/Sources/LaunchScreen.storyboard
index bd2b472..6383af8 100644
--- a/pedi-foods/Darwin/Sources/LaunchScreen.storyboard
+++ b/pedi-foods/Darwin/Sources/LaunchScreen.storyboard
@@ -23,7 +23,7 @@
-
+
diff --git a/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings b/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings
index 143f19f..7bab26e 100644
--- a/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings
+++ b/pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings
@@ -81,33 +81,11 @@
},
"0000 0000 0000 0000" : {
- },
- "1. Tamanho" : {
- "comment" : "A heading for the size selection step in the pizza product detail sheet.",
- "isCommentAutoGenerated" : true
},
"1km" : {
"comment" : "A label for the minimum distance filter option.",
"isCommentAutoGenerated" : true
},
- "2. Massa" : {
-
- },
- "3. Borda" : {
-
- },
- "4. Sabores (%lld/%lld)" : {
- "comment" : "A heading that describes the step where the user selects pizza flavors. The number in the parentheses indicates the maximum number of flavors that can be selected.",
- "isCommentAutoGenerated" : true,
- "localizations" : {
- "en" : {
- "stringUnit" : {
- "state" : "new",
- "value" : "4. Sabores (%1$lld/%2$lld)"
- }
- }
- }
- },
"10km" : {
"comment" : "A label displayed next to the far end of the distance slider in the filters modal.",
"isCommentAutoGenerated" : true
@@ -185,6 +163,9 @@
},
"Ao menos um cartão deve permanecer" : {
+ },
+ "Ao menos um endereço deve permanecer" : {
+
},
"Ao se cadastrar, você concorda com nossos [Termos de Uso](app://terms) e [Política de Privacidade](app://policy)" : {
"comment" : "A text that informs the user that registering implies acceptance of the app's terms of use and privacy policy. The text is clickable and navigates to the respective views when tapped.",
@@ -234,10 +215,6 @@
},
"Atualizando status do pedido..." : {
- },
- "Atualize os dados do endereço abaixo." : {
- "comment" : "A description below the fields in the \"Editar endereço\" form, instructing the user to update their address details.",
- "isCommentAutoGenerated" : true
},
"Avaliar Pedido" : {
@@ -382,6 +359,9 @@
"Cupom de Desconto" : {
"comment" : "A section header for entering and applying a discount coupon.",
"isCommentAutoGenerated" : true
+ },
+ "Cupons de Desconto" : {
+
},
"Dark" : {
"comment" : "Menu item indicating that the appearance should be in dark mode",
@@ -951,10 +931,6 @@
"comment" : "A label describing the price filter section in the filters modal.",
"isCommentAutoGenerated" : true
},
- "Preencha os dados abaixo para adicionar um endereço." : {
- "comment" : "A description below the form to add a new address, instructing the user to fill in the required information.",
- "isCommentAutoGenerated" : true
- },
"Preencha os dados abaixo para começar." : {
"comment" : "A description below the form fields in the registration view, instructing the user to fill them out to start.",
"isCommentAutoGenerated" : true
diff --git a/pedi-foods/Sources/PediFoods/Resources/placeholder-pizza.png b/pedi-foods/Sources/PediFoods/Resources/placeholder-pizza.png
new file mode 100644
index 0000000..e4072c0
Binary files /dev/null and b/pedi-foods/Sources/PediFoods/Resources/placeholder-pizza.png differ
diff --git a/pedi-foods/Sources/PediFoods/Services/ApiClient.swift b/pedi-foods/Sources/PediFoods/Services/ApiClient.swift
index bd4b4c2..f5cab1d 100644
--- a/pedi-foods/Sources/PediFoods/Services/ApiClient.swift
+++ b/pedi-foods/Sources/PediFoods/Services/ApiClient.swift
@@ -82,7 +82,6 @@ final class ApiClient {
}
func send(_ request: ApiRequest) async throws -> T {
-
#if canImport(LCEssentials) && os(iOS)
return try await sendWithLCEssentials(request)
#else
@@ -119,7 +118,7 @@ private extension ApiClient {
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
- throw NetworkError.decodeError(String(data: data, encoding: .utf8))
+ throw NetworkError.decodeError(sanitizedBody(data))
}
} catch {
let mapped = mapError(error)
@@ -156,6 +155,7 @@ private extension ApiClient {
case "POST": return .post
case "PUT": return .put
case "DELETE": return .delete
+ case "PATCH": return .patch
default: return .get
}
}
@@ -274,7 +274,7 @@ private extension ApiClient {
do {
return try JSONDecoder().decode(type, from: data)
} catch {
- throw NetworkError.decodeError(String(data: data, encoding: .utf8))
+ throw NetworkError.decodeError(sanitizedBody(data))
}
}
@@ -328,7 +328,7 @@ private extension ApiClient {
return msg
}
}
- return String(data: data, encoding: .utf8)
+ return sanitizedBody(data)
}
func serverPayload(from data: Data) -> ApiErrorDescriptor? {
@@ -364,48 +364,41 @@ private extension ApiClient {
#if canImport(LCEssentials) && os(iOS)
func serverMessage(from error: NSError) -> String? {
- // Tenta buscar no userInfo por chaves conhecidas
for key in [NSLocalizedFailureReasonErrorKey, "body", "responseBody", "data", "message"] {
if let value = error.userInfo[key] as? String,
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
if let data = value.data(using: .utf8),
let parsed = serverMessage(from: data),
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- if parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
- continue
- }
- return parsed
- }
- if !value.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
- return value
+ let t = parsed.trimmingCharacters(in: .whitespacesAndNewlines)
+ if t.hasPrefix("{") { continue }
+ if let safe = sanitizedMessage(t) { return safe }
}
+ let t = value.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !t.hasPrefix("{"), let safe = sanitizedMessage(t) { return safe }
} else if let valueData = error.userInfo[key] as? Data {
- if let parsed = serverMessage(from: valueData),
- !parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
- !parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
- return parsed
+ if let parsed = serverMessage(from: valueData) {
+ let t = parsed.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !t.hasPrefix("{"), let safe = sanitizedMessage(t) { return safe }
}
}
}
- // Tenta varrer qualquer chave do userInfo por strings que possam ser JSON ou mensagens
for (_, value) in error.userInfo {
- if let str = value as? String, !str.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- if str.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
- if let data = str.data(using: .utf8),
- let parsed = serverMessage(from: data),
- !parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
- !parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
- return parsed
- }
- }
+ if let str = value as? String,
+ str.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{"),
+ let data = str.data(using: .utf8),
+ let parsed = serverMessage(from: data) {
+ let t = parsed.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !t.hasPrefix("{"), let safe = sanitizedMessage(t) { return safe }
}
}
if let description = error.userInfo[NSLocalizedDescriptionKey] as? String,
!description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
- !description.lowercased().contains("nsurlerrordomain") {
- return description
+ !description.lowercased().contains("nsurlerrordomain"),
+ let safe = sanitizedMessage(description) {
+ return safe
}
return nil
@@ -435,6 +428,25 @@ private extension ApiClient {
}
#endif
+ func sanitizedBody(_ data: Data) -> String? {
+ guard let raw = String(data: data, encoding: .utf8) else { return nil }
+ return sanitizedMessage(raw)
+ }
+
+ func sanitizedMessage(_ raw: String) -> String? {
+ let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard trimmed.isEmpty == false else { return nil }
+ // Drop data URLs (base64 images)
+ if trimmed.lowercased().hasPrefix("data:image") { return "Erro ao processar imagem." }
+ // Drop fields containing base64,
+ if trimmed.contains("base64,") { return "Resposta do servidor inválida." }
+ // Truncate long strings (raw JSON bodies, etc.)
+ if trimmed.count > 300 {
+ return String(trimmed.prefix(300)) + "…"
+ }
+ return trimmed
+ }
+
func buildURL(path: String, query: [URLQueryItem]) throws -> URL {
guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
throw NetworkError.invalidURL
diff --git a/pedi-foods/Sources/PediFoods/Services/ApiModels.swift b/pedi-foods/Sources/PediFoods/Services/ApiModels.swift
index 6c6e077..dc73a2e 100644
--- a/pedi-foods/Sources/PediFoods/Services/ApiModels.swift
+++ b/pedi-foods/Sources/PediFoods/Services/ApiModels.swift
@@ -53,18 +53,23 @@ struct CustomerAddress: Decodable {
let state: String?
let zipCode: String?
let latLong: [Double]?
+ let isDefault: Bool?
+
+ init(id: String?, label: String?, address: String?, number: String?,
+ complement: String?, neighborhood: String?, city: String?,
+ state: String?, zipCode: String?, latLong: [Double]?, isDefault: Bool?) {
+ self.id = id; self.label = label; self.address = address
+ self.number = number; self.complement = complement
+ self.neighborhood = neighborhood; self.city = city
+ self.state = state; self.zipCode = zipCode
+ self.latLong = latLong; self.isDefault = isDefault
+ }
enum CodingKeys: String, CodingKey {
- case id
- case label
- case address
- case number
- case complement
- case neighborhood
- case city
- case state
- case zipCode
+ case id, label, address, number, complement
+ case neighborhood, city, state, zipCode
case latLong = "lat_long"
+ case isDefault
}
}
diff --git a/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift b/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift
index dd95608..b4627b5 100644
--- a/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift
+++ b/pedi-foods/Sources/PediFoods/Services/ApiOrderModels.swift
@@ -28,6 +28,7 @@ struct CreateOrderItemPayload: Codable {
let qty: Int
let price: Double
let addons: [CreateOrderAddonPayload]
+ let choices: [String]?
}
struct CreateOrderAddonPayload: Codable {
diff --git a/pedi-foods/Sources/PediFoods/Services/ApiService.swift b/pedi-foods/Sources/PediFoods/Services/ApiService.swift
index eec5989..4eb13a7 100644
--- a/pedi-foods/Sources/PediFoods/Services/ApiService.swift
+++ b/pedi-foods/Sources/PediFoods/Services/ApiService.swift
@@ -212,6 +212,13 @@ final class ApiService {
return try await updateCustomerAddressBook(customerId: customer.id, addressBook: updatedAddressBook)
}
+ func setDefaultAddress(addressId: String) async throws -> ApiEnvelope {
+ let req = ApiRequest(path: "/api/customer/addresses/\(addressId)/default", method: "PATCH", module: .customer, requiresAuth: true)
+ let envelope: ApiEnvelope = try await sendEnvelope(req)
+ AppContentCache.shared.invalidate(prefix: profileCachePrefix)
+ return envelope
+ }
+
func lookupZipCode(_ zipCode: String) async throws -> ApiEnvelope {
let digits = zipCode.filter(\.isNumber)
let normalized = String(digits.prefix(8))
diff --git a/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift b/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift
index 0c77e5b..09f12bf 100644
--- a/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift
+++ b/pedi-foods/Sources/PediFoods/Services/FeatureControlService.swift
@@ -258,7 +258,7 @@ final class FeatureControlService {
}
private func featureKeys() -> [String] {
- let raw = ProcessInfo.processInfo.environment["FEATURE_CONTROL_KEYS"] ?? "at.city.aguai,at.promo,at.ios.only,at.android.only"
+ let raw = ProcessInfo.processInfo.environment["FEATURE_CONTROL_KEYS"] ?? "at.city.aguai,at.promo,at.ios.only,at.android.only,at.cupons"
let items = raw
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
diff --git a/pedi-foods/Sources/PediFoods/State/AppState.swift b/pedi-foods/Sources/PediFoods/State/AppState.swift
index fab59f6..3c117bf 100644
--- a/pedi-foods/Sources/PediFoods/State/AppState.swift
+++ b/pedi-foods/Sources/PediFoods/State/AppState.swift
@@ -173,6 +173,7 @@ struct CartItemState: Identifiable {
var name: String
var imageURL: String? = nil
var details: String? = nil
+ var choices: [String]? = nil
var addons: [CartItemAddonState] = []
var quantity: Int
var unitPrice: Double
@@ -276,7 +277,8 @@ extension CartState {
qty: $0.quantity,
price: $0.unitPrice
)
- }
+ },
+ choices: item.choices?.isEmpty == false ? item.choices : nil
)
}
}
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift b/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift
index c391380..505b510 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/AddAddressFormView.swift
@@ -2,7 +2,6 @@ import SwiftUI
struct AddAddressFormView: View {
@Environment(\.dismiss) var dismiss
- @Environment(\.colorScheme) var colorScheme
@State var label = ""
@State var zipCode = ""
@State var address = ""
@@ -31,38 +30,18 @@ struct AddAddressFormView: View {
!state.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
- @ViewBuilder private var logoImage: some View {
- SwiftUI.Image("pedifoods")
- .resizable()
- }
-
var body: some View {
ZStack {
- (colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()
+ AppColors.backgroundLight.ignoresSafeArea()
- ScrollView {
+ ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
screenHeader(
title: existingAddress == nil ? "Novo endereço" : "Editar endereço",
onBack: { dismiss() }
)
- .padding(.horizontal, 24)
- .padding(.bottom, 12)
-
- logoImage
- .scaledToFit()
- .frame(width: 120, height: 120)
-
- Text(existingAddress == nil ? "Novo endereço" : "Editar endereço")
- .font(AppTypography.heading1)
- .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary)
-
- Text(existingAddress == nil ? "Preencha os dados abaixo para adicionar um endereço." : "Atualize os dados do endereço abaixo.")
- .font(AppTypography.body)
- .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary)
- .multilineTextAlignment(.center)
- .padding(.top, 8)
- .padding(.bottom, 20)
+ .padding(.horizontal, 20)
+ .padding(.bottom, 20)
VStack(alignment: .leading, spacing: 16) {
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", keyboardType: .default, text: $label)
@@ -127,7 +106,7 @@ struct AddAddressFormView: View {
Spacer().frame(height: 120)
}
}
- .padding(.top, 0)
+ .padding(.top, 18)
}
.navigationBarBackButtonHidden(true)
.appHiddenNavigationBar()
@@ -140,8 +119,7 @@ struct AddAddressFormView: View {
ZStack {
Text(title)
.font(AppTypography.heading2)
- .foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary)
-
+ .foregroundStyle(AppColors.textPrimary)
HStack {
Button(action: onBack) {
Image(systemName: "chevron.left")
@@ -177,7 +155,8 @@ struct AddAddressFormView: View {
city: clean(city),
state: clean(state),
zipCode: optional(normalizeZipCodeForAPI(zipCode)),
- latLong: latLong
+ latLong: latLong,
+ isDefault: existingAddress?.isDefault
)
isLoading = true
zipLookupMessage = nil
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddressComponents.swift b/pedi-foods/Sources/PediFoods/Views/Main/AddressComponents.swift
index d195279..04d2817 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/AddressComponents.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/AddressComponents.swift
@@ -118,6 +118,9 @@ struct AddressCard: View {
let item: AddressListItem
var onEdit: (() -> Void)? = nil
var onDelete: (() -> Void)? = nil
+ var onSetDefault: (() -> Void)? = nil
+
+ private var hasActions: Bool { onEdit != nil || onSetDefault != nil }
var body: some View {
HStack(spacing: 14) {
@@ -150,30 +153,30 @@ struct AddressCard: View {
Spacer(minLength: 8)
- if onEdit != nil || onDelete != nil {
+ if hasActions {
Rectangle()
.fill(AppColors.backgroundLight)
.frame(width: 1, height: 96)
}
- VStack(spacing: 24) {
+ VStack(spacing: 20) {
if let onEdit {
Button(action: onEdit) {
Image(systemName: "pencil")
- .font(.system(size: 22))
+ .font(.system(size: 20))
.foregroundStyle(AppColors.textMuted)
}
}
- if let onDelete {
- Button(action: onDelete) {
- Image(systemName: "trash")
- .font(.system(size: 22))
+ if let onSetDefault, item.isPrimary == false {
+ Button(action: onSetDefault) {
+ Image(systemName: "star")
+ .font(.system(size: 20))
.foregroundStyle(AppColors.textMuted)
}
}
}
- .frame(width: onEdit != nil || onDelete != nil ? 40 : 0)
+ .frame(width: hasActions ? 40 : 0)
}
.padding(.horizontal, 16)
.padding(.vertical, 20)
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift b/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift
index b15b176..9817781 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/AddressesView.swift
@@ -12,6 +12,7 @@ struct AddressesView: View {
@State var editingAddress: CustomerAddress? = nil
@State var openSwipeRowId: String? = nil
@State var deletingRowId: String? = nil
+ @State var settingDefaultRowId: String? = nil
let tabBarClearance: CGFloat = 96
@@ -54,7 +55,9 @@ struct AddressesView: View {
} else {
ForEach(Array(addresses.enumerated()), id: \.offset) { index, address in
let rowId = addressRowId(for: address, index: index)
- let isSelected: Bool = {
+ let isPrimary: Bool = {
+ if address.isDefault == true { return true }
+ if addresses.contains(where: { $0.isDefault == true }) { return false }
if let selectedId = appState.address.selectedId {
return address.id == selectedId
}
@@ -65,10 +68,10 @@ struct AddressesView: View {
Button {
selectAddress(address)
} label: {
- AddressCard(item: addressToListItem(address, isPrimary: isSelected))
+ AddressCard(item: addressToListItem(address, isPrimary: isPrimary))
}
.buttonStyle(.plain)
- } else {
+ } else if addresses.count > 1 {
SwipeToDeleteAddressRow(
rowId: rowId,
openRowId: $openSwipeRowId,
@@ -76,19 +79,30 @@ struct AddressesView: View {
onDelete: { deleteAddress(address, rowId: rowId) }
) {
AddressCard(
- item: addressToListItem(address, isPrimary: isSelected),
- onEdit: { beginEditing(address) }
+ item: addressToListItem(address, isPrimary: isPrimary),
+ onEdit: { beginEditing(address) },
+ onSetDefault: { setDefaultAddress(address, rowId: rowId) }
)
.appContentShape(Rectangle())
- .onTapGesture {
- if openSwipeRowId == rowId {
- openSwipeRowId = nil
- }
- }
+ .simultaneousGesture(TapGesture().onEnded {
+ if openSwipeRowId == rowId { openSwipeRowId = nil }
+ })
+ .opacity(settingDefaultRowId == rowId ? 0.6 : 1.0)
}
.id(rowId)
.opacity(deletingRowId == rowId ? 0.6 : 1.0)
- .disabled(deletingRowId != nil)
+ .disabled(deletingRowId != nil || settingDefaultRowId != nil)
+ } else {
+ AddressCard(
+ item: addressToListItem(address, isPrimary: isPrimary),
+ onEdit: { beginEditing(address) }
+ )
+ .overlay(alignment: .bottom) {
+ Text("Ao menos um endereço deve permanecer")
+ .font(AppTypography.caption)
+ .foregroundStyle(AppColors.textMuted.opacity(0.7))
+ .padding(.bottom, 8)
+ }
}
}
}
@@ -230,8 +244,65 @@ struct AddressesView: View {
return "idx:\(index):\(address.label ?? ""):\(address.address ?? ""):\(address.number ?? "")"
}
+ private func setDefaultAddress(_ address: CustomerAddress, rowId: String) {
+ guard settingDefaultRowId == nil else { return }
+ settingDefaultRowId = rowId
+ openSwipeRowId = nil
+
+ Task {
+ var resolvedId = address.id?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+
+ if resolvedId.isEmpty {
+ if let book = try? await ApiService().profile(forceRefresh: true).result?.addressBook {
+ await MainActor.run { addresses = book }
+ resolvedId = book.first {
+ $0.address == address.address &&
+ $0.number == address.number &&
+ $0.zipCode == address.zipCode
+ }?.id?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ }
+ }
+
+ guard resolvedId.isEmpty == false else {
+ await MainActor.run {
+ settingDefaultRowId = nil
+ SnackbarCenter.shared.show(title: "Não foi possível identificar o endereço.", style: .error, icon: "xmark.octagon.fill", duration: 3.5)
+ }
+ return
+ }
+
+ do {
+ let response = try await ApiService().setDefaultAddress(addressId: resolvedId)
+ await MainActor.run {
+ settingDefaultRowId = nil
+ if response.error == false {
+ addresses = addresses.map { addr in
+ let isTarget = (addr.id ?? "") == resolvedId
+ return CustomerAddress(
+ id: addr.id, label: addr.label, address: addr.address,
+ number: addr.number, complement: addr.complement,
+ neighborhood: addr.neighborhood, city: addr.city,
+ state: addr.state, zipCode: addr.zipCode,
+ latLong: addr.latLong, isDefault: isTarget
+ )
+ }
+ selectAddress(address)
+ SnackbarCenter.shared.show(title: "Endereço principal atualizado.", style: .success, icon: "star.fill", duration: 2.5)
+ } else {
+ SnackbarCenter.shared.show(title: response.message ?? "Não foi possível definir endereço principal.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
+ }
+ }
+ } catch {
+ await MainActor.run {
+ settingDefaultRowId = nil
+ SnackbarCenter.shared.show(title: "Erro ao atualizar endereço principal.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
+ }
+ }
+ }
+ }
+
private func deleteAddress(_ address: CustomerAddress, rowId: String) {
- guard deletingRowId == nil else { return }
+ guard addresses.count > 1, deletingRowId == nil else { return }
deletingRowId = rowId
openSwipeRowId = nil
@@ -302,7 +373,7 @@ struct AddressesView: View {
do {
let service = ApiService()
- let response = try await service.profile()
+ let response = try await service.profile(forceRefresh: true)
guard response.error == false else {
errorMessage = response.message ?? "Não foi possível carregar os endereços."
isLoading = false
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift
index 55629a6..438bd90 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView+Logic.swift
@@ -333,16 +333,6 @@ extension CheckoutView {
)
}
- @MainActor
- func loadSavedCards() async {
- do {
- let response = try await ApiService().listCards()
- if response.error == false, let cards = response.result {
- savedCards = cards
- }
- } catch {}
- }
-
@MainActor
func confirmOrderWithSavedCard(cardId: String, storeId: String) async {
let payloadResult = buildCreateOrderPayload(paymentMethod: .creditCard, savedCardId: cardId)
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift
index 47552e3..9180b4d 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift
@@ -28,7 +28,6 @@ struct CheckoutView: View {
@State var pixPaymentContext: PixPaymentContext? = nil
@State var cardPaymentContext: CardPaymentContext? = nil
@State var orderTrackingContext: OrderTrackingContext? = nil
- @State var savedCards: [SavedCard] = []
@State var showCardSelectionSheet = false
var paymentConfig: StorePaymentMethodsInfo? { storeInfo?.paymentMethods }
@@ -191,7 +190,6 @@ struct CheckoutView: View {
items: appState.cart.toOrderItemsPayload()
)
CardSelectionSheet(
- savedCards: savedCards,
cardContext: cardContext,
appState: $appState,
onSavedCardConfirmed: { cardId in
@@ -209,11 +207,6 @@ struct CheckoutView: View {
}
)
}
- .task(id: paymentMethod) {
- if paymentMethod == .creditCard && useInAppPayment {
- await loadSavedCards()
- }
- }
.navigationDestination(item: $orderTrackingContext) { context in
OrderTrackingView(orderId: context.orderId, initialShortId: context.shortId, postOrderBack: {
appState.shouldNavigateToOrders = true
@@ -957,24 +950,23 @@ struct PaymentPixView: View {
// MARK: - CardSelectionSheet
struct CardSelectionSheet: View {
- let savedCards: [SavedCard]
let cardContext: CardPaymentContext
@Binding var appState: AppState
let onSavedCardConfirmed: (String) -> Void
let onOrderCreated: (String, String?) -> Void
@Environment(\.dismiss) var dismiss
+ @State private var cards: [SavedCard] = []
+ @State private var isLoading = false
@State var selectedCardId: String?
@State var isSubmitting = false
@State var showNewCardSheet = false
- init(savedCards: [SavedCard], cardContext: CardPaymentContext, appState: Binding, onSavedCardConfirmed: @escaping (String) -> Void, onOrderCreated: @escaping (String, String?) -> Void) {
- self.savedCards = savedCards
+ init(cardContext: CardPaymentContext, appState: Binding, onSavedCardConfirmed: @escaping (String) -> Void, onOrderCreated: @escaping (String, String?) -> Void) {
self.cardContext = cardContext
self._appState = appState
self.onSavedCardConfirmed = onSavedCardConfirmed
self.onOrderCreated = onOrderCreated
- _selectedCardId = State(initialValue: savedCards.first(where: { $0.isDefault })?.id ?? savedCards.first?.id)
}
var body: some View {
@@ -1004,11 +996,15 @@ struct CardSelectionSheet: View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 16) {
- if savedCards.isEmpty == false {
+ if isLoading {
+ ProgressView()
+ .frame(maxWidth: .infinity)
+ .padding(.top, 32)
+ } else if cards.isEmpty == false {
VStack(alignment: .leading, spacing: 0) {
- ForEach(savedCards) { card in
+ ForEach(cards) { card in
savedCardRow(card)
- if card.id != savedCards.last?.id {
+ if card.id != cards.last?.id {
Divider().padding(.horizontal, 14)
}
}
@@ -1066,6 +1062,17 @@ struct CardSelectionSheet: View {
SnackbarOverlay(center: SnackbarCenter.shared)
}
+ .task { await fetchCards() }
+ }
+
+ @MainActor
+ private func fetchCards() async {
+ isLoading = true
+ defer { isLoading = false }
+ if let result = try? await ApiService().listCards(), result.error == false, let fetched = result.result {
+ cards = fetched
+ selectedCardId = fetched.first(where: { $0.isDefault })?.id ?? fetched.first?.id
+ }
}
private func savedCardRow(_ card: SavedCard) -> some View {
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift b/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift
index f96b628..30cfd6b 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet+Flow.swift
@@ -2,28 +2,39 @@ import Foundation
import SwiftUI
extension PizzaProductDetailSheet {
- var stepSizes: some View {
- let sizeItems = sizes
- return VStack(alignment: .leading, spacing: 10) {
- Text("1. Tamanho")
- .font(AppTypography.heading3)
- .foregroundStyle(AppColors.textPrimary)
- ForEach(0.. 0 ? "+ \(formatCurrency(crust.priceModifier ?? 0))" : nil,
+ subtitle: (crust.priceModifier ?? 0) > 0 ? "+ \(formatCurrency(crust.priceModifier ?? 0))" : nil,
isSelected: selectedCrustId == crust.id
) {
selectedCrustId = crust.id
+ withAnimation(.easeInOut(duration: 0.2)) {
+ expandedStep = 3
+ }
}
}
}
}
- .padding(12)
- .background(AppColors.surface)
- .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
var stepFlavors: some View {
- let flavorItems = flavors
- return VStack(alignment: .leading, spacing: 10) {
- Text("4. Sabores (\(selectedFlavorIds.count)/\(maxFlavorsAllowed))")
- .font(AppTypography.heading3)
- .foregroundStyle(AppColors.textPrimary)
-
+ accordionSection(
+ step: 3,
+ label: "Sabores (\(selectedFlavorIds.count)/\(maxFlavorsAllowed))",
+ summary: selectedFlavorIds.isEmpty ? nil
+ : selectedFlavorProducts.map(\.name).joined(separator: ", ")
+ ) {
Text("Toque no sabor para escolher adicionais.")
.font(.caption)
.foregroundStyle(AppColors.textMuted)
- ForEach(0..= maxFlavorsAllowed
- let disableSwitch = isSelected == false && maxReached
HStack(spacing: 10) {
+ AsyncStoreImage(imageURL: resolveImageURL(flavor.image))
+ .frame(width: 52, height: 52)
+ .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
+
VStack(alignment: .leading, spacing: 4) {
Text(flavor.name)
.font(AppTypography.body)
@@ -101,21 +117,15 @@ extension PizzaProductDetailSheet {
.foregroundStyle(AppColors.textMuted)
}
}
-
Spacer()
-
Toggle("", isOn: Binding(
get: { isSelected },
set: { value in
- if value {
- addFlavor(flavor.id)
- } else {
- removeFlavor(flavor.id)
- }
+ if value { addFlavor(flavor.id) } else { removeFlavor(flavor.id) }
}
))
.labelsHidden()
- .disabled(disableSwitch)
+ .disabled(!isSelected && maxReached)
}
.padding(.vertical, 2)
.appContentShape(Rectangle())
@@ -126,23 +136,90 @@ extension PizzaProductDetailSheet {
}
}
}
- .padding(12)
+ }
+
+ // MARK: - Accordion container
+
+ func accordionSection(
+ step: Int,
+ label: String,
+ summary: String?,
+ @ViewBuilder content: () -> some View
+ ) -> some View {
+ let isExpanded = expandedStep == step
+ let isDone = summary != nil
+
+ return VStack(spacing: 0) {
+ Button {
+ withAnimation(.easeInOut(duration: 0.2)) {
+ expandedStep = isExpanded ? -1 : step
+ }
+ } label: {
+ HStack(spacing: 10) {
+ ZStack {
+ Circle()
+ .fill(isDone || isExpanded ? AppColors.primary : AppColors.textMuted.opacity(0.25))
+ .frame(width: 26, height: 26)
+ if isDone && !isExpanded {
+ Image(systemName: "checkmark")
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.white)
+ } else {
+ Text("\(step + 1)")
+ .font(.system(size: 12, weight: .bold))
+ .foregroundStyle(.white)
+ }
+ }
+
+ Text(label)
+ .font(AppTypography.heading3)
+ .foregroundStyle(AppColors.textPrimary)
+
+ Spacer()
+
+ if let summary, !isExpanded {
+ Text(summary)
+ .font(AppTypography.caption)
+ .foregroundStyle(AppColors.textMuted)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .frame(maxWidth: 140, alignment: .trailing)
+ }
+
+ Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
+ .font(.system(size: 12, weight: .semibold))
+ .foregroundStyle(AppColors.textMuted)
+ }
+ .padding(14)
+ }
+ .buttonStyle(.plain)
+
+ if isExpanded {
+ Divider().padding(.horizontal, 14)
+ VStack(alignment: .leading, spacing: 10) {
+ content()
+ }
+ .padding(14)
+ .transition(.opacity.combined(with: .move(edge: .top)))
+ }
+ }
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
- func sizeRow(_ size: StorePizzaSize) -> some View {
- let title = size.name ?? "Tamanho"
- let subtitle = "Até \(max(1, size.maxFlavors ?? 1)) sabor(es)"
- let selected = selectedSizeId == size.id
+ // MARK: - Helpers
- return radioRow(
- title: title,
- subtitle: subtitle,
- isSelected: selected
- ) {
- selectedSizeId = size.id
+ func nextStep(after step: Int) -> Int {
+ if step == 0 {
+ if doughs.count > 1 { return 1 }
+ if crusts.count > 1 { return 2 }
+ return 3
}
+ if step == 1 {
+ if crusts.count > 1 { return 2 }
+ return 3
+ }
+ return 3
}
func radioRow(title: String, subtitle: String?, isSelected: Bool, action: @escaping () -> Void) -> some View {
@@ -158,7 +235,6 @@ extension PizzaProductDetailSheet {
.frame(width: 10, height: 10)
}
}
-
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(AppTypography.body)
@@ -169,7 +245,6 @@ extension PizzaProductDetailSheet {
.foregroundStyle(AppColors.textMuted)
}
}
-
Spacer()
}
.appContentShape(Rectangle())
@@ -181,40 +256,29 @@ extension PizzaProductDetailSheet {
guard let crust else { return "Sem borda especial" }
let name = crust.name ?? "Borda"
let modifier = crust.priceModifier ?? 0
- if modifier > 0 {
- return "\(name) (+ \(formatCurrency(modifier)))"
- }
- return name
+ return modifier > 0 ? "\(name) (+ \(formatCurrency(modifier)))" : name
}
func applyAutoSelections() {
if selectedSizeId != nil {
- if doughs.count == 1 {
- selectedDoughId = doughs.first?.id
- } else if doughs.isEmpty {
- selectedDoughId = "__none__"
- }
+ if doughs.count == 1 { selectedDoughId = doughs.first?.id }
+ else if doughs.isEmpty { selectedDoughId = "__none__" }
}
-
if isDoughReady {
- if crusts.count == 1 {
- selectedCrustId = crusts.first?.id
- } else if crusts.isEmpty {
- selectedCrustId = "__none__"
- }
+ if crusts.count == 1 { selectedCrustId = crusts.first?.id }
+ else if crusts.isEmpty { selectedCrustId = "__none__" }
}
}
func trimFlavorSelectionByLimit() {
let limit = maxFlavorsAllowed
guard selectedFlavorIds.count > limit else { return }
- let sorted = selectedFlavorIds.sorted()
- selectedFlavorIds = Set(sorted.prefix(limit))
+ selectedFlavorIds = Set(selectedFlavorIds.sorted().prefix(limit))
}
func addFlavor(_ flavorId: String) {
- if selectedFlavorIds.contains(flavorId) { return }
- if selectedFlavorIds.count >= maxFlavorsAllowed { return }
+ guard !selectedFlavorIds.contains(flavorId),
+ selectedFlavorIds.count < maxFlavorsAllowed else { return }
selectedFlavorIds.insert(flavorId)
}
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift b/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift
index 0ee5760..18c640c 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/PizzaProductDetailSheet.swift
@@ -17,6 +17,7 @@ struct PizzaProductDetailSheet: View {
@State var flavorAddonQuantities: [String: [String: Int]] = [:]
@State var selectedFlavorForAddons: StoreCatalogProduct? = nil
@State var quantity: Int = 1
+ @State var expandedStep: Int = 0
var flavors: [StoreCatalogProduct] {
category.products
@@ -45,7 +46,7 @@ struct PizzaProductDetailSheet: View {
return resolveImageURL(firstImage)
}
- private var selectedSize: StorePizzaSize? {
+ var selectedSize: StorePizzaSize? {
guard let selectedSizeId else { return nil }
return sizes.first(where: { $0.id == selectedSizeId })
}
@@ -76,7 +77,7 @@ struct PizzaProductDetailSheet: View {
isCrustReady
}
- private var selectedFlavorProducts: [StoreCatalogProduct] {
+ var selectedFlavorProducts: [StoreCatalogProduct] {
flavors
.filter { selectedFlavorIds.contains($0.id) }
.sorted { $0.name < $1.name }
@@ -102,12 +103,12 @@ struct PizzaProductDetailSheet: View {
}
private var basePizzaPrice: Double {
- selectedFlavorProducts
- .map { flavor in
- guard let selectedSizeId else { return flavor.price ?? 0 }
- return flavor.pizzaPrices[selectedSizeId] ?? flavor.price ?? 0
- }
- .max() ?? 0
+ let prices = selectedFlavorProducts.map { flavor in
+ guard let selectedSizeId else { return flavor.price ?? 0 }
+ return flavor.pizzaPrices[selectedSizeId] ?? flavor.price ?? 0
+ }
+ guard prices.isEmpty == false else { return 0 }
+ return prices.reduce(0, +) / Double(prices.count)
}
private var unitPrice: Double {
@@ -179,6 +180,34 @@ struct PizzaProductDetailSheet: View {
return chunks.isEmpty ? nil : chunks.joined(separator: " • ")
}
+ private var pizzaChoices: [String] {
+ var choices: [String] = []
+ if let sizeName = selectedSize?.name {
+ let sizePrice = basePizzaPrice
+ if sizePrice > 0 {
+ choices.append("Tamanho: \(sizeName) (+\(formatCurrency(sizePrice)))")
+ } else {
+ choices.append("Tamanho: \(sizeName)")
+ }
+ }
+ if let crustName = selectedCrust?.name, crustName.isEmpty == false {
+ let mod = crustPriceModifier
+ if mod > 0 {
+ choices.append("Borda: \(crustName) (+\(formatCurrency(mod)))")
+ } else {
+ choices.append("Borda: \(crustName)")
+ }
+ }
+ if let doughName = selectedDoughName, doughName.isEmpty == false {
+ choices.append("Massa: \(doughName)")
+ }
+ let flavorCount = selectedFlavorProducts.count
+ for flavor in selectedFlavorProducts {
+ choices.append(flavorCount > 1 ? "Meio \(flavor.name)" : flavor.name)
+ }
+ return choices
+ }
+
private var addButtonTitle: String {
if canConfirm == false {
return "Selecione as opções"
@@ -190,8 +219,16 @@ struct PizzaProductDetailSheet: View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 16) {
screenHeader
- AsyncStoreImage(imageURL: representativeImage)
+ Rectangle()
+ .fill(AppColors.brandSoft)
+ .frame(maxWidth: .infinity)
.frame(height: 220)
+ .overlay(
+ Image("placeholder-pizza")
+ .resizable()
+ .scaledToFill()
+ .clipped()
+ )
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
Text("Pizza de varios sabores")
@@ -207,18 +244,9 @@ struct PizzaProductDetailSheet: View {
.foregroundStyle(AppColors.primary)
stepSizes
-
- if selectedSizeId != nil {
- stepDoughs
- }
-
- if isDoughReady {
- stepCrusts
- }
-
- if canShowFlavors {
- stepFlavors
- }
+ if selectedSizeId != nil { stepDoughs }
+ if isDoughReady { stepCrusts }
+ if canShowFlavors { stepFlavors }
}
.padding(20)
.padding(.bottom, 90)
@@ -266,6 +294,7 @@ struct PizzaProductDetailSheet: View {
name: "Pizza de varios sabores",
imageURL: representativeImage,
details: selectedDetailsText,
+ choices: pizzaChoices.isEmpty ? nil : pizzaChoices,
addons: selectedAddonsPayload,
quantity: quantity,
unitPrice: unitPrice
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift b/pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift
index 1348e34..148fbc3 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift
@@ -56,12 +56,14 @@ struct ProfileView: View {
}
.buttonStyle(.plain)
-// NavigationLink {
-// Text("Cupons de Desconto")
-// } label: {
-// ProfileMenuRow(icon: "ticket.fill", title: "Cupons de Desconto", badge: "NOVO")
-// }
-// .buttonStyle(.plain)
+ if appState.featureFlags.isEnabled("at.cupons") {
+ NavigationLink {
+ Text("Cupons de Desconto")
+ } label: {
+ ProfileMenuRow(icon: "ticket.fill", title: "Cupons de Desconto", badge: "NOVO")
+ }
+ .buttonStyle(.plain)
+ }
// NavigationLink {
// Text("Ajuda")
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift b/pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift
index e1e3fb1..f14094b 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/ReviewsView.swift
@@ -848,6 +848,7 @@ struct OrderReviewView: View {
.frame(height: 110)
.padding(.horizontal, 8)
.padding(.vertical, 8)
+ .foregroundStyle(AppColors.textPrimary)
.scrollContentBackground(.hidden)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift
index 1e52f7d..c303c68 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailSupport.swift
@@ -47,6 +47,7 @@ struct ScrollOffsetReader: View {
struct AsyncStoreImage: View {
let imageURL: String?
+ var fallbackImageName: String = "placeholder-product"
var body: some View {
CachedRemoteImage(imageURL: imageURL) {
@@ -58,7 +59,7 @@ struct AsyncStoreImage: View {
}
private var fallback: some View {
- Image("placeholder-product")
+ Image(fallbackImageName)
.resizable()
.scaledToFill()
}
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift
index ecd0908..5924dbc 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/StoreDetailView+Components.swift
@@ -269,9 +269,17 @@ extension StoreDetailView {
Spacer()
ZStack(alignment: .bottomTrailing) {
- AsyncStoreImage(imageURL: resolvedURL(item.imageURL))
- .frame(width: 92, height: 92)
- .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
+ Group {
+ if item.isPizzaSummary {
+ Image("placeholder-pizza")
+ .resizable()
+ .scaledToFill()
+ } else {
+ AsyncStoreImage(imageURL: resolvedURL(item.imageURL))
+ }
+ }
+ .frame(width: 92, height: 92)
+ .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
Button {
guard isStoreOpen else {
diff --git a/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift b/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift
index a778f68..d9c3d81 100644
--- a/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift
+++ b/pedi-foods/Sources/PediFoods/Views/Main/UserProfileView.swift
@@ -297,8 +297,9 @@ struct UserProfileView: View {
guard let item else { return }
do {
guard let data = try await item.loadTransferable(type: Data.self) else { return }
- guard let image = UIImage(data: data),
- let jpegData = image.jpegData(compressionQuality: 0.82) else { return }
+ guard let image = UIImage(data: data) else { return }
+ let resized = resizedIfNeeded(image, maxSide: 600)
+ guard let jpegData = resized.jpegData(compressionQuality: 0.8) else { return }
profilePicture = "data:image/jpeg;base64,\(jpegData.base64EncodedString())"
} catch {
SnackbarCenter.shared.show(
@@ -309,5 +310,17 @@ struct UserProfileView: View {
)
}
}
+
+ private func resizedIfNeeded(_ image: UIImage, maxSide: CGFloat) -> UIImage {
+ let w = image.size.width
+ let h = image.size.height
+ guard w > maxSide || h > maxSide else { return image }
+ let scale = maxSide / max(w, h)
+ let newSize = CGSize(width: w * scale, height: h * scale)
+ let renderer = UIGraphicsImageRenderer(size: newSize)
+ return renderer.image { _ in
+ image.draw(in: CGRect(origin: .zero, size: newSize))
+ }
+ }
#endif
}