Almost done
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
12
pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json
vendored
Normal file
12
pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "placeholder-pizza.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png
vendored
Normal file
BIN
pedi-foods/Darwin/Assets.xcassets/placeholder-pizza.imageset/placeholder-pizza.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 667 KiB |
@@ -162,8 +162,6 @@
|
|||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
499CD43A2AC5B799001AE8D8 /* Sources */ = {
|
499CD43A2AC5B799001AE8D8 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
@@ -269,7 +267,7 @@
|
|||||||
repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git";
|
repositoryURL = "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 1.0.7;
|
minimumVersion = 1.0.10;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||||
<color key="backgroundColor" red="0.9529411765" green="0.9607843137" blue="0.9686274510" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" name="LaunchBackground"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="W6R-s6-hHG" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="F0M-uu-fA3"/>
|
<constraint firstItem="W6R-s6-hHG" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="F0M-uu-fA3"/>
|
||||||
<constraint firstItem="W6R-s6-hHG" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="M3D-C3-C0c"/>
|
<constraint firstItem="W6R-s6-hHG" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="M3D-C3-C0c"/>
|
||||||
|
|||||||
@@ -81,33 +81,11 @@
|
|||||||
},
|
},
|
||||||
"0000 0000 0000 0000" : {
|
"0000 0000 0000 0000" : {
|
||||||
|
|
||||||
},
|
|
||||||
"1. Tamanho" : {
|
|
||||||
"comment" : "A heading for the size selection step in the pizza product detail sheet.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
},
|
||||||
"1km" : {
|
"1km" : {
|
||||||
"comment" : "A label for the minimum distance filter option.",
|
"comment" : "A label for the minimum distance filter option.",
|
||||||
"isCommentAutoGenerated" : true
|
"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" : {
|
"10km" : {
|
||||||
"comment" : "A label displayed next to the far end of the distance slider in the filters modal.",
|
"comment" : "A label displayed next to the far end of the distance slider in the filters modal.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@@ -185,6 +163,9 @@
|
|||||||
},
|
},
|
||||||
"Ao menos um cartão deve permanecer" : {
|
"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)" : {
|
"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.",
|
"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..." : {
|
"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" : {
|
"Avaliar Pedido" : {
|
||||||
|
|
||||||
@@ -382,6 +359,9 @@
|
|||||||
"Cupom de Desconto" : {
|
"Cupom de Desconto" : {
|
||||||
"comment" : "A section header for entering and applying a discount coupon.",
|
"comment" : "A section header for entering and applying a discount coupon.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Cupons de Desconto" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Dark" : {
|
"Dark" : {
|
||||||
"comment" : "Menu item indicating that the appearance should be in dark mode",
|
"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.",
|
"comment" : "A label describing the price filter section in the filters modal.",
|
||||||
"isCommentAutoGenerated" : true
|
"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." : {
|
"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.",
|
"comment" : "A description below the form fields in the registration view, instructing the user to fill them out to start.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|||||||
BIN
pedi-foods/Sources/PediFoods/Resources/placeholder-pizza.png
Normal file
BIN
pedi-foods/Sources/PediFoods/Resources/placeholder-pizza.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 667 KiB |
@@ -82,7 +82,6 @@ final class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func send<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
func send<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
||||||
|
|
||||||
#if canImport(LCEssentials) && os(iOS)
|
#if canImport(LCEssentials) && os(iOS)
|
||||||
return try await sendWithLCEssentials(request)
|
return try await sendWithLCEssentials(request)
|
||||||
#else
|
#else
|
||||||
@@ -119,7 +118,7 @@ private extension ApiClient {
|
|||||||
do {
|
do {
|
||||||
return try JSONDecoder().decode(T.self, from: data)
|
return try JSONDecoder().decode(T.self, from: data)
|
||||||
} catch {
|
} catch {
|
||||||
throw NetworkError.decodeError(String(data: data, encoding: .utf8))
|
throw NetworkError.decodeError(sanitizedBody(data))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
let mapped = mapError(error)
|
let mapped = mapError(error)
|
||||||
@@ -156,6 +155,7 @@ private extension ApiClient {
|
|||||||
case "POST": return .post
|
case "POST": return .post
|
||||||
case "PUT": return .put
|
case "PUT": return .put
|
||||||
case "DELETE": return .delete
|
case "DELETE": return .delete
|
||||||
|
case "PATCH": return .patch
|
||||||
default: return .get
|
default: return .get
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +274,7 @@ private extension ApiClient {
|
|||||||
do {
|
do {
|
||||||
return try JSONDecoder().decode(type, from: data)
|
return try JSONDecoder().decode(type, from: data)
|
||||||
} catch {
|
} catch {
|
||||||
throw NetworkError.decodeError(String(data: data, encoding: .utf8))
|
throw NetworkError.decodeError(sanitizedBody(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ private extension ApiClient {
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return String(data: data, encoding: .utf8)
|
return sanitizedBody(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serverPayload(from data: Data) -> ApiErrorDescriptor? {
|
func serverPayload(from data: Data) -> ApiErrorDescriptor? {
|
||||||
@@ -364,48 +364,41 @@ private extension ApiClient {
|
|||||||
|
|
||||||
#if canImport(LCEssentials) && os(iOS)
|
#if canImport(LCEssentials) && os(iOS)
|
||||||
func serverMessage(from error: NSError) -> String? {
|
func serverMessage(from error: NSError) -> String? {
|
||||||
// Tenta buscar no userInfo por chaves conhecidas
|
|
||||||
for key in [NSLocalizedFailureReasonErrorKey, "body", "responseBody", "data", "message"] {
|
for key in [NSLocalizedFailureReasonErrorKey, "body", "responseBody", "data", "message"] {
|
||||||
if let value = error.userInfo[key] as? String,
|
if let value = error.userInfo[key] as? String,
|
||||||
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
if let data = value.data(using: .utf8),
|
if let data = value.data(using: .utf8),
|
||||||
let parsed = serverMessage(from: data),
|
let parsed = serverMessage(from: data),
|
||||||
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
if parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
|
let t = parsed.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
continue
|
if t.hasPrefix("{") { continue }
|
||||||
}
|
if let safe = sanitizedMessage(t) { return safe }
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
if !value.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
let t = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !t.hasPrefix("{"), let safe = sanitizedMessage(t) { return safe }
|
||||||
} else if let valueData = error.userInfo[key] as? Data {
|
} else if let valueData = error.userInfo[key] as? Data {
|
||||||
if let parsed = serverMessage(from: valueData),
|
if let parsed = serverMessage(from: valueData) {
|
||||||
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
let t = parsed.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
|
if !t.hasPrefix("{"), let safe = sanitizedMessage(t) { return safe }
|
||||||
return parsed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenta varrer qualquer chave do userInfo por strings que possam ser JSON ou mensagens
|
|
||||||
for (_, value) in error.userInfo {
|
for (_, value) in error.userInfo {
|
||||||
if let str = value as? String, !str.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if let str = value as? String,
|
||||||
if str.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
|
str.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{"),
|
||||||
if let data = str.data(using: .utf8),
|
let data = str.data(using: .utf8),
|
||||||
let parsed = serverMessage(from: data),
|
let parsed = serverMessage(from: data) {
|
||||||
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
let t = parsed.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("{") {
|
if !t.hasPrefix("{"), let safe = sanitizedMessage(t) { return safe }
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let description = error.userInfo[NSLocalizedDescriptionKey] as? String,
|
if let description = error.userInfo[NSLocalizedDescriptionKey] as? String,
|
||||||
!description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
!description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||||
!description.lowercased().contains("nsurlerrordomain") {
|
!description.lowercased().contains("nsurlerrordomain"),
|
||||||
return description
|
let safe = sanitizedMessage(description) {
|
||||||
|
return safe
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -435,6 +428,25 @@ private extension ApiClient {
|
|||||||
}
|
}
|
||||||
#endif
|
#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 {
|
func buildURL(path: String, query: [URLQueryItem]) throws -> URL {
|
||||||
guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
|
guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
|
||||||
throw NetworkError.invalidURL
|
throw NetworkError.invalidURL
|
||||||
|
|||||||
@@ -53,18 +53,23 @@ struct CustomerAddress: Decodable {
|
|||||||
let state: String?
|
let state: String?
|
||||||
let zipCode: String?
|
let zipCode: String?
|
||||||
let latLong: [Double]?
|
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 {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id, label, address, number, complement
|
||||||
case label
|
case neighborhood, city, state, zipCode
|
||||||
case address
|
|
||||||
case number
|
|
||||||
case complement
|
|
||||||
case neighborhood
|
|
||||||
case city
|
|
||||||
case state
|
|
||||||
case zipCode
|
|
||||||
case latLong = "lat_long"
|
case latLong = "lat_long"
|
||||||
|
case isDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ struct CreateOrderItemPayload: Codable {
|
|||||||
let qty: Int
|
let qty: Int
|
||||||
let price: Double
|
let price: Double
|
||||||
let addons: [CreateOrderAddonPayload]
|
let addons: [CreateOrderAddonPayload]
|
||||||
|
let choices: [String]?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateOrderAddonPayload: Codable {
|
struct CreateOrderAddonPayload: Codable {
|
||||||
|
|||||||
@@ -212,6 +212,13 @@ final class ApiService {
|
|||||||
return try await updateCustomerAddressBook(customerId: customer.id, addressBook: updatedAddressBook)
|
return try await updateCustomerAddressBook(customerId: customer.id, addressBook: updatedAddressBook)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDefaultAddress(addressId: String) async throws -> ApiEnvelope<EmptyResult> {
|
||||||
|
let req = ApiRequest(path: "/api/customer/addresses/\(addressId)/default", method: "PATCH", module: .customer, requiresAuth: true)
|
||||||
|
let envelope: ApiEnvelope<EmptyResult> = try await sendEnvelope(req)
|
||||||
|
AppContentCache.shared.invalidate(prefix: profileCachePrefix)
|
||||||
|
return envelope
|
||||||
|
}
|
||||||
|
|
||||||
func lookupZipCode(_ zipCode: String) async throws -> ApiEnvelope<CepLookupResult> {
|
func lookupZipCode(_ zipCode: String) async throws -> ApiEnvelope<CepLookupResult> {
|
||||||
let digits = zipCode.filter(\.isNumber)
|
let digits = zipCode.filter(\.isNumber)
|
||||||
let normalized = String(digits.prefix(8))
|
let normalized = String(digits.prefix(8))
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ final class FeatureControlService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func featureKeys() -> [String] {
|
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
|
let items = raw
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ struct CartItemState: Identifiable {
|
|||||||
var name: String
|
var name: String
|
||||||
var imageURL: String? = nil
|
var imageURL: String? = nil
|
||||||
var details: String? = nil
|
var details: String? = nil
|
||||||
|
var choices: [String]? = nil
|
||||||
var addons: [CartItemAddonState] = []
|
var addons: [CartItemAddonState] = []
|
||||||
var quantity: Int
|
var quantity: Int
|
||||||
var unitPrice: Double
|
var unitPrice: Double
|
||||||
@@ -276,7 +277,8 @@ extension CartState {
|
|||||||
qty: $0.quantity,
|
qty: $0.quantity,
|
||||||
price: $0.unitPrice
|
price: $0.unitPrice
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
choices: item.choices?.isEmpty == false ? item.choices : nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import SwiftUI
|
|||||||
|
|
||||||
struct AddAddressFormView: View {
|
struct AddAddressFormView: View {
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
@State var label = ""
|
@State var label = ""
|
||||||
@State var zipCode = ""
|
@State var zipCode = ""
|
||||||
@State var address = ""
|
@State var address = ""
|
||||||
@@ -31,37 +30,17 @@ struct AddAddressFormView: View {
|
|||||||
!state.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
!state.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var logoImage: some View {
|
|
||||||
SwiftUI.Image("pedifoods")
|
|
||||||
.resizable()
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
(colorScheme == .dark ? Color.black : AppColors.backgroundLight).ignoresSafeArea()
|
AppColors.backgroundLight.ignoresSafeArea()
|
||||||
|
|
||||||
ScrollView {
|
ScrollView(showsIndicators: false) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
screenHeader(
|
screenHeader(
|
||||||
title: existingAddress == nil ? "Novo endereço" : "Editar endereço",
|
title: existingAddress == nil ? "Novo endereço" : "Editar endereço",
|
||||||
onBack: { dismiss() }
|
onBack: { dismiss() }
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 20)
|
||||||
.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(.bottom, 20)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
@@ -127,7 +106,7 @@ struct AddAddressFormView: View {
|
|||||||
Spacer().frame(height: 120)
|
Spacer().frame(height: 120)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 0)
|
.padding(.top, 18)
|
||||||
}
|
}
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
.appHiddenNavigationBar()
|
.appHiddenNavigationBar()
|
||||||
@@ -140,8 +119,7 @@ struct AddAddressFormView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(AppTypography.heading2)
|
.font(AppTypography.heading2)
|
||||||
.foregroundStyle(colorScheme == .dark ? Color.white : AppColors.textPrimary)
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Button(action: onBack) {
|
Button(action: onBack) {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
@@ -177,7 +155,8 @@ struct AddAddressFormView: View {
|
|||||||
city: clean(city),
|
city: clean(city),
|
||||||
state: clean(state),
|
state: clean(state),
|
||||||
zipCode: optional(normalizeZipCodeForAPI(zipCode)),
|
zipCode: optional(normalizeZipCodeForAPI(zipCode)),
|
||||||
latLong: latLong
|
latLong: latLong,
|
||||||
|
isDefault: existingAddress?.isDefault
|
||||||
)
|
)
|
||||||
isLoading = true
|
isLoading = true
|
||||||
zipLookupMessage = nil
|
zipLookupMessage = nil
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ struct AddressCard: View {
|
|||||||
let item: AddressListItem
|
let item: AddressListItem
|
||||||
var onEdit: (() -> Void)? = nil
|
var onEdit: (() -> Void)? = nil
|
||||||
var onDelete: (() -> Void)? = nil
|
var onDelete: (() -> Void)? = nil
|
||||||
|
var onSetDefault: (() -> Void)? = nil
|
||||||
|
|
||||||
|
private var hasActions: Bool { onEdit != nil || onSetDefault != nil }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 14) {
|
HStack(spacing: 14) {
|
||||||
@@ -150,30 +153,30 @@ struct AddressCard: View {
|
|||||||
|
|
||||||
Spacer(minLength: 8)
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
if onEdit != nil || onDelete != nil {
|
if hasActions {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(AppColors.backgroundLight)
|
.fill(AppColors.backgroundLight)
|
||||||
.frame(width: 1, height: 96)
|
.frame(width: 1, height: 96)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 20) {
|
||||||
if let onEdit {
|
if let onEdit {
|
||||||
Button(action: onEdit) {
|
Button(action: onEdit) {
|
||||||
Image(systemName: "pencil")
|
Image(systemName: "pencil")
|
||||||
.font(.system(size: 22))
|
.font(.system(size: 20))
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let onDelete {
|
if let onSetDefault, item.isPrimary == false {
|
||||||
Button(action: onDelete) {
|
Button(action: onSetDefault) {
|
||||||
Image(systemName: "trash")
|
Image(systemName: "star")
|
||||||
.font(.system(size: 22))
|
.font(.system(size: 20))
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: onEdit != nil || onDelete != nil ? 40 : 0)
|
.frame(width: hasActions ? 40 : 0)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 20)
|
.padding(.vertical, 20)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct AddressesView: View {
|
|||||||
@State var editingAddress: CustomerAddress? = nil
|
@State var editingAddress: CustomerAddress? = nil
|
||||||
@State var openSwipeRowId: String? = nil
|
@State var openSwipeRowId: String? = nil
|
||||||
@State var deletingRowId: String? = nil
|
@State var deletingRowId: String? = nil
|
||||||
|
@State var settingDefaultRowId: String? = nil
|
||||||
|
|
||||||
let tabBarClearance: CGFloat = 96
|
let tabBarClearance: CGFloat = 96
|
||||||
|
|
||||||
@@ -54,7 +55,9 @@ struct AddressesView: View {
|
|||||||
} else {
|
} else {
|
||||||
ForEach(Array(addresses.enumerated()), id: \.offset) { index, address in
|
ForEach(Array(addresses.enumerated()), id: \.offset) { index, address in
|
||||||
let rowId = addressRowId(for: address, index: index)
|
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 {
|
if let selectedId = appState.address.selectedId {
|
||||||
return address.id == selectedId
|
return address.id == selectedId
|
||||||
}
|
}
|
||||||
@@ -65,10 +68,10 @@ struct AddressesView: View {
|
|||||||
Button {
|
Button {
|
||||||
selectAddress(address)
|
selectAddress(address)
|
||||||
} label: {
|
} label: {
|
||||||
AddressCard(item: addressToListItem(address, isPrimary: isSelected))
|
AddressCard(item: addressToListItem(address, isPrimary: isPrimary))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
} else {
|
} else if addresses.count > 1 {
|
||||||
SwipeToDeleteAddressRow(
|
SwipeToDeleteAddressRow(
|
||||||
rowId: rowId,
|
rowId: rowId,
|
||||||
openRowId: $openSwipeRowId,
|
openRowId: $openSwipeRowId,
|
||||||
@@ -76,19 +79,30 @@ struct AddressesView: View {
|
|||||||
onDelete: { deleteAddress(address, rowId: rowId) }
|
onDelete: { deleteAddress(address, rowId: rowId) }
|
||||||
) {
|
) {
|
||||||
AddressCard(
|
AddressCard(
|
||||||
item: addressToListItem(address, isPrimary: isSelected),
|
item: addressToListItem(address, isPrimary: isPrimary),
|
||||||
onEdit: { beginEditing(address) }
|
onEdit: { beginEditing(address) },
|
||||||
|
onSetDefault: { setDefaultAddress(address, rowId: rowId) }
|
||||||
)
|
)
|
||||||
.appContentShape(Rectangle())
|
.appContentShape(Rectangle())
|
||||||
.onTapGesture {
|
.simultaneousGesture(TapGesture().onEnded {
|
||||||
if openSwipeRowId == rowId {
|
if openSwipeRowId == rowId { openSwipeRowId = nil }
|
||||||
openSwipeRowId = nil
|
})
|
||||||
}
|
.opacity(settingDefaultRowId == rowId ? 0.6 : 1.0)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.id(rowId)
|
.id(rowId)
|
||||||
.opacity(deletingRowId == rowId ? 0.6 : 1.0)
|
.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 ?? "")"
|
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) {
|
private func deleteAddress(_ address: CustomerAddress, rowId: String) {
|
||||||
guard deletingRowId == nil else { return }
|
guard addresses.count > 1, deletingRowId == nil else { return }
|
||||||
deletingRowId = rowId
|
deletingRowId = rowId
|
||||||
openSwipeRowId = nil
|
openSwipeRowId = nil
|
||||||
|
|
||||||
@@ -302,7 +373,7 @@ struct AddressesView: View {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let service = ApiService()
|
let service = ApiService()
|
||||||
let response = try await service.profile()
|
let response = try await service.profile(forceRefresh: true)
|
||||||
guard response.error == false else {
|
guard response.error == false else {
|
||||||
errorMessage = response.message ?? "Não foi possível carregar os endereços."
|
errorMessage = response.message ?? "Não foi possível carregar os endereços."
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|||||||
@@ -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
|
@MainActor
|
||||||
func confirmOrderWithSavedCard(cardId: String, storeId: String) async {
|
func confirmOrderWithSavedCard(cardId: String, storeId: String) async {
|
||||||
let payloadResult = buildCreateOrderPayload(paymentMethod: .creditCard, savedCardId: cardId)
|
let payloadResult = buildCreateOrderPayload(paymentMethod: .creditCard, savedCardId: cardId)
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ struct CheckoutView: View {
|
|||||||
@State var pixPaymentContext: PixPaymentContext? = nil
|
@State var pixPaymentContext: PixPaymentContext? = nil
|
||||||
@State var cardPaymentContext: CardPaymentContext? = nil
|
@State var cardPaymentContext: CardPaymentContext? = nil
|
||||||
@State var orderTrackingContext: OrderTrackingContext? = nil
|
@State var orderTrackingContext: OrderTrackingContext? = nil
|
||||||
@State var savedCards: [SavedCard] = []
|
|
||||||
@State var showCardSelectionSheet = false
|
@State var showCardSelectionSheet = false
|
||||||
|
|
||||||
var paymentConfig: StorePaymentMethodsInfo? { storeInfo?.paymentMethods }
|
var paymentConfig: StorePaymentMethodsInfo? { storeInfo?.paymentMethods }
|
||||||
@@ -191,7 +190,6 @@ struct CheckoutView: View {
|
|||||||
items: appState.cart.toOrderItemsPayload()
|
items: appState.cart.toOrderItemsPayload()
|
||||||
)
|
)
|
||||||
CardSelectionSheet(
|
CardSelectionSheet(
|
||||||
savedCards: savedCards,
|
|
||||||
cardContext: cardContext,
|
cardContext: cardContext,
|
||||||
appState: $appState,
|
appState: $appState,
|
||||||
onSavedCardConfirmed: { cardId in
|
onSavedCardConfirmed: { cardId in
|
||||||
@@ -209,11 +207,6 @@ struct CheckoutView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.task(id: paymentMethod) {
|
|
||||||
if paymentMethod == .creditCard && useInAppPayment {
|
|
||||||
await loadSavedCards()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationDestination(item: $orderTrackingContext) { context in
|
.navigationDestination(item: $orderTrackingContext) { context in
|
||||||
OrderTrackingView(orderId: context.orderId, initialShortId: context.shortId, postOrderBack: {
|
OrderTrackingView(orderId: context.orderId, initialShortId: context.shortId, postOrderBack: {
|
||||||
appState.shouldNavigateToOrders = true
|
appState.shouldNavigateToOrders = true
|
||||||
@@ -957,24 +950,23 @@ struct PaymentPixView: View {
|
|||||||
// MARK: - CardSelectionSheet
|
// MARK: - CardSelectionSheet
|
||||||
|
|
||||||
struct CardSelectionSheet: View {
|
struct CardSelectionSheet: View {
|
||||||
let savedCards: [SavedCard]
|
|
||||||
let cardContext: CardPaymentContext
|
let cardContext: CardPaymentContext
|
||||||
@Binding var appState: AppState
|
@Binding var appState: AppState
|
||||||
let onSavedCardConfirmed: (String) -> Void
|
let onSavedCardConfirmed: (String) -> Void
|
||||||
let onOrderCreated: (String, String?) -> Void
|
let onOrderCreated: (String, String?) -> Void
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@State private var cards: [SavedCard] = []
|
||||||
|
@State private var isLoading = false
|
||||||
@State var selectedCardId: String?
|
@State var selectedCardId: String?
|
||||||
@State var isSubmitting = false
|
@State var isSubmitting = false
|
||||||
@State var showNewCardSheet = false
|
@State var showNewCardSheet = false
|
||||||
|
|
||||||
init(savedCards: [SavedCard], cardContext: CardPaymentContext, appState: Binding<AppState>, onSavedCardConfirmed: @escaping (String) -> Void, onOrderCreated: @escaping (String, String?) -> Void) {
|
init(cardContext: CardPaymentContext, appState: Binding<AppState>, onSavedCardConfirmed: @escaping (String) -> Void, onOrderCreated: @escaping (String, String?) -> Void) {
|
||||||
self.savedCards = savedCards
|
|
||||||
self.cardContext = cardContext
|
self.cardContext = cardContext
|
||||||
self._appState = appState
|
self._appState = appState
|
||||||
self.onSavedCardConfirmed = onSavedCardConfirmed
|
self.onSavedCardConfirmed = onSavedCardConfirmed
|
||||||
self.onOrderCreated = onOrderCreated
|
self.onOrderCreated = onOrderCreated
|
||||||
_selectedCardId = State(initialValue: savedCards.first(where: { $0.isDefault })?.id ?? savedCards.first?.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -1004,11 +996,15 @@ struct CardSelectionSheet: View {
|
|||||||
|
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
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) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ForEach(savedCards) { card in
|
ForEach(cards) { card in
|
||||||
savedCardRow(card)
|
savedCardRow(card)
|
||||||
if card.id != savedCards.last?.id {
|
if card.id != cards.last?.id {
|
||||||
Divider().padding(.horizontal, 14)
|
Divider().padding(.horizontal, 14)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1062,17 @@ struct CardSelectionSheet: View {
|
|||||||
|
|
||||||
SnackbarOverlay(center: SnackbarCenter.shared)
|
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 {
|
private func savedCardRow(_ card: SavedCard) -> some View {
|
||||||
|
|||||||
@@ -2,28 +2,39 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension PizzaProductDetailSheet {
|
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..<sizeItems.count, id: \.self) { index in
|
// MARK: - Steps
|
||||||
sizeRow(sizeItems[index])
|
|
||||||
|
var stepSizes: some View {
|
||||||
|
accordionSection(
|
||||||
|
step: 0,
|
||||||
|
label: "Tamanho",
|
||||||
|
summary: selectedSize.map { "\($0.name ?? "") • Até \(max(1, $0.maxFlavors ?? 1)) sabor(es)" }
|
||||||
|
) {
|
||||||
|
ForEach(sizes) { size in
|
||||||
|
radioRow(
|
||||||
|
title: size.name ?? "Tamanho",
|
||||||
|
subtitle: "Até \(max(1, size.maxFlavors ?? 1)) sabor(es)",
|
||||||
|
isSelected: selectedSizeId == size.id
|
||||||
|
) {
|
||||||
|
selectedSizeId = size.id
|
||||||
|
applyAutoSelections()
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
expandedStep = nextStep(after: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(12)
|
|
||||||
.background(AppColors.surface)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stepDoughs: some View {
|
var stepDoughs: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
accordionSection(
|
||||||
Text("2. Massa")
|
step: 1,
|
||||||
.font(AppTypography.heading3)
|
label: "Massa",
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
summary: doughs.count <= 1
|
||||||
|
? (doughs.first?.name ?? "Tradicional")
|
||||||
|
: doughs.first(where: { $0.id == selectedDoughId })?.name
|
||||||
|
) {
|
||||||
if doughs.count <= 1 {
|
if doughs.count <= 1 {
|
||||||
Text(doughs.first?.name ?? "Massa tradicional")
|
Text(doughs.first?.name ?? "Massa tradicional")
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
@@ -36,21 +47,24 @@ extension PizzaProductDetailSheet {
|
|||||||
isSelected: selectedDoughId == dough.id
|
isSelected: selectedDoughId == dough.id
|
||||||
) {
|
) {
|
||||||
selectedDoughId = dough.id
|
selectedDoughId = dough.id
|
||||||
|
applyAutoSelections()
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
expandedStep = nextStep(after: 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(12)
|
|
||||||
.background(AppColors.surface)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stepCrusts: some View {
|
var stepCrusts: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
accordionSection(
|
||||||
Text("3. Borda")
|
step: 2,
|
||||||
.font(AppTypography.heading3)
|
label: "Borda",
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
summary: crusts.count <= 1
|
||||||
|
? crustDescription(crusts.first)
|
||||||
|
: crusts.first(where: { $0.id == selectedCrustId }).map { crustDescription($0) }
|
||||||
|
) {
|
||||||
if crusts.count <= 1 {
|
if crusts.count <= 1 {
|
||||||
Text(crustDescription(crusts.first))
|
Text(crustDescription(crusts.first))
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
@@ -59,38 +73,40 @@ extension PizzaProductDetailSheet {
|
|||||||
ForEach(crusts) { crust in
|
ForEach(crusts) { crust in
|
||||||
radioRow(
|
radioRow(
|
||||||
title: crust.name ?? "Borda",
|
title: crust.name ?? "Borda",
|
||||||
subtitle: crust.priceModifier ?? 0 > 0 ? "+ \(formatCurrency(crust.priceModifier ?? 0))" : nil,
|
subtitle: (crust.priceModifier ?? 0) > 0 ? "+ \(formatCurrency(crust.priceModifier ?? 0))" : nil,
|
||||||
isSelected: selectedCrustId == crust.id
|
isSelected: selectedCrustId == crust.id
|
||||||
) {
|
) {
|
||||||
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 {
|
var stepFlavors: some View {
|
||||||
let flavorItems = flavors
|
accordionSection(
|
||||||
return VStack(alignment: .leading, spacing: 10) {
|
step: 3,
|
||||||
Text("4. Sabores (\(selectedFlavorIds.count)/\(maxFlavorsAllowed))")
|
label: "Sabores (\(selectedFlavorIds.count)/\(maxFlavorsAllowed))",
|
||||||
.font(AppTypography.heading3)
|
summary: selectedFlavorIds.isEmpty ? nil
|
||||||
.foregroundStyle(AppColors.textPrimary)
|
: selectedFlavorProducts.map(\.name).joined(separator: ", ")
|
||||||
|
) {
|
||||||
Text("Toque no sabor para escolher adicionais.")
|
Text("Toque no sabor para escolher adicionais.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
|
||||||
ForEach(0..<flavorItems.count, id: \.self) { index in
|
ForEach(flavors) { flavor in
|
||||||
let flavor = flavorItems[index]
|
|
||||||
let isSelected = selectedFlavorIds.contains(flavor.id)
|
let isSelected = selectedFlavorIds.contains(flavor.id)
|
||||||
let price = selectedSizeId.flatMap { flavor.pizzaPrices[$0] } ?? flavor.price
|
let price = selectedSizeId.flatMap { flavor.pizzaPrices[$0] } ?? flavor.price
|
||||||
let maxReached = selectedFlavorIds.count >= maxFlavorsAllowed
|
let maxReached = selectedFlavorIds.count >= maxFlavorsAllowed
|
||||||
let disableSwitch = isSelected == false && maxReached
|
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
|
AsyncStoreImage(imageURL: resolveImageURL(flavor.image))
|
||||||
|
.frame(width: 52, height: 52)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(flavor.name)
|
Text(flavor.name)
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
@@ -101,21 +117,15 @@ extension PizzaProductDetailSheet {
|
|||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Toggle("", isOn: Binding(
|
Toggle("", isOn: Binding(
|
||||||
get: { isSelected },
|
get: { isSelected },
|
||||||
set: { value in
|
set: { value in
|
||||||
if value {
|
if value { addFlavor(flavor.id) } else { removeFlavor(flavor.id) }
|
||||||
addFlavor(flavor.id)
|
|
||||||
} else {
|
|
||||||
removeFlavor(flavor.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.disabled(disableSwitch)
|
.disabled(!isSelected && maxReached)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
.appContentShape(Rectangle())
|
.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)
|
.background(AppColors.surface)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sizeRow(_ size: StorePizzaSize) -> some View {
|
// MARK: - Helpers
|
||||||
let title = size.name ?? "Tamanho"
|
|
||||||
let subtitle = "Até \(max(1, size.maxFlavors ?? 1)) sabor(es)"
|
|
||||||
let selected = selectedSizeId == size.id
|
|
||||||
|
|
||||||
return radioRow(
|
func nextStep(after step: Int) -> Int {
|
||||||
title: title,
|
if step == 0 {
|
||||||
subtitle: subtitle,
|
if doughs.count > 1 { return 1 }
|
||||||
isSelected: selected
|
if crusts.count > 1 { return 2 }
|
||||||
) {
|
return 3
|
||||||
selectedSizeId = size.id
|
|
||||||
}
|
}
|
||||||
|
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 {
|
func radioRow(title: String, subtitle: String?, isSelected: Bool, action: @escaping () -> Void) -> some View {
|
||||||
@@ -158,7 +235,6 @@ extension PizzaProductDetailSheet {
|
|||||||
.frame(width: 10, height: 10)
|
.frame(width: 10, height: 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(AppTypography.body)
|
.font(AppTypography.body)
|
||||||
@@ -169,7 +245,6 @@ extension PizzaProductDetailSheet {
|
|||||||
.foregroundStyle(AppColors.textMuted)
|
.foregroundStyle(AppColors.textMuted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.appContentShape(Rectangle())
|
.appContentShape(Rectangle())
|
||||||
@@ -181,40 +256,29 @@ extension PizzaProductDetailSheet {
|
|||||||
guard let crust else { return "Sem borda especial" }
|
guard let crust else { return "Sem borda especial" }
|
||||||
let name = crust.name ?? "Borda"
|
let name = crust.name ?? "Borda"
|
||||||
let modifier = crust.priceModifier ?? 0
|
let modifier = crust.priceModifier ?? 0
|
||||||
if modifier > 0 {
|
return modifier > 0 ? "\(name) (+ \(formatCurrency(modifier)))" : name
|
||||||
return "\(name) (+ \(formatCurrency(modifier)))"
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAutoSelections() {
|
func applyAutoSelections() {
|
||||||
if selectedSizeId != nil {
|
if selectedSizeId != nil {
|
||||||
if doughs.count == 1 {
|
if doughs.count == 1 { selectedDoughId = doughs.first?.id }
|
||||||
selectedDoughId = doughs.first?.id
|
else if doughs.isEmpty { selectedDoughId = "__none__" }
|
||||||
} else if doughs.isEmpty {
|
|
||||||
selectedDoughId = "__none__"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if isDoughReady {
|
if isDoughReady {
|
||||||
if crusts.count == 1 {
|
if crusts.count == 1 { selectedCrustId = crusts.first?.id }
|
||||||
selectedCrustId = crusts.first?.id
|
else if crusts.isEmpty { selectedCrustId = "__none__" }
|
||||||
} else if crusts.isEmpty {
|
|
||||||
selectedCrustId = "__none__"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimFlavorSelectionByLimit() {
|
func trimFlavorSelectionByLimit() {
|
||||||
let limit = maxFlavorsAllowed
|
let limit = maxFlavorsAllowed
|
||||||
guard selectedFlavorIds.count > limit else { return }
|
guard selectedFlavorIds.count > limit else { return }
|
||||||
let sorted = selectedFlavorIds.sorted()
|
selectedFlavorIds = Set(selectedFlavorIds.sorted().prefix(limit))
|
||||||
selectedFlavorIds = Set(sorted.prefix(limit))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFlavor(_ flavorId: String) {
|
func addFlavor(_ flavorId: String) {
|
||||||
if selectedFlavorIds.contains(flavorId) { return }
|
guard !selectedFlavorIds.contains(flavorId),
|
||||||
if selectedFlavorIds.count >= maxFlavorsAllowed { return }
|
selectedFlavorIds.count < maxFlavorsAllowed else { return }
|
||||||
selectedFlavorIds.insert(flavorId)
|
selectedFlavorIds.insert(flavorId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct PizzaProductDetailSheet: View {
|
|||||||
@State var flavorAddonQuantities: [String: [String: Int]] = [:]
|
@State var flavorAddonQuantities: [String: [String: Int]] = [:]
|
||||||
@State var selectedFlavorForAddons: StoreCatalogProduct? = nil
|
@State var selectedFlavorForAddons: StoreCatalogProduct? = nil
|
||||||
@State var quantity: Int = 1
|
@State var quantity: Int = 1
|
||||||
|
@State var expandedStep: Int = 0
|
||||||
|
|
||||||
var flavors: [StoreCatalogProduct] {
|
var flavors: [StoreCatalogProduct] {
|
||||||
category.products
|
category.products
|
||||||
@@ -45,7 +46,7 @@ struct PizzaProductDetailSheet: View {
|
|||||||
return resolveImageURL(firstImage)
|
return resolveImageURL(firstImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectedSize: StorePizzaSize? {
|
var selectedSize: StorePizzaSize? {
|
||||||
guard let selectedSizeId else { return nil }
|
guard let selectedSizeId else { return nil }
|
||||||
return sizes.first(where: { $0.id == selectedSizeId })
|
return sizes.first(where: { $0.id == selectedSizeId })
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,7 @@ struct PizzaProductDetailSheet: View {
|
|||||||
isCrustReady
|
isCrustReady
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectedFlavorProducts: [StoreCatalogProduct] {
|
var selectedFlavorProducts: [StoreCatalogProduct] {
|
||||||
flavors
|
flavors
|
||||||
.filter { selectedFlavorIds.contains($0.id) }
|
.filter { selectedFlavorIds.contains($0.id) }
|
||||||
.sorted { $0.name < $1.name }
|
.sorted { $0.name < $1.name }
|
||||||
@@ -102,12 +103,12 @@ struct PizzaProductDetailSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var basePizzaPrice: Double {
|
private var basePizzaPrice: Double {
|
||||||
selectedFlavorProducts
|
let prices = selectedFlavorProducts.map { flavor in
|
||||||
.map { flavor in
|
|
||||||
guard let selectedSizeId else { return flavor.price ?? 0 }
|
guard let selectedSizeId else { return flavor.price ?? 0 }
|
||||||
return flavor.pizzaPrices[selectedSizeId] ?? flavor.price ?? 0
|
return flavor.pizzaPrices[selectedSizeId] ?? flavor.price ?? 0
|
||||||
}
|
}
|
||||||
.max() ?? 0
|
guard prices.isEmpty == false else { return 0 }
|
||||||
|
return prices.reduce(0, +) / Double(prices.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var unitPrice: Double {
|
private var unitPrice: Double {
|
||||||
@@ -179,6 +180,34 @@ struct PizzaProductDetailSheet: View {
|
|||||||
return chunks.isEmpty ? nil : chunks.joined(separator: " • ")
|
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 {
|
private var addButtonTitle: String {
|
||||||
if canConfirm == false {
|
if canConfirm == false {
|
||||||
return "Selecione as opções"
|
return "Selecione as opções"
|
||||||
@@ -190,8 +219,16 @@ struct PizzaProductDetailSheet: View {
|
|||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
screenHeader
|
screenHeader
|
||||||
AsyncStoreImage(imageURL: representativeImage)
|
Rectangle()
|
||||||
|
.fill(AppColors.brandSoft)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: 220)
|
.frame(height: 220)
|
||||||
|
.overlay(
|
||||||
|
Image("placeholder-pizza")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.clipped()
|
||||||
|
)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
|
||||||
|
|
||||||
Text("Pizza de varios sabores")
|
Text("Pizza de varios sabores")
|
||||||
@@ -207,18 +244,9 @@ struct PizzaProductDetailSheet: View {
|
|||||||
.foregroundStyle(AppColors.primary)
|
.foregroundStyle(AppColors.primary)
|
||||||
|
|
||||||
stepSizes
|
stepSizes
|
||||||
|
if selectedSizeId != nil { stepDoughs }
|
||||||
if selectedSizeId != nil {
|
if isDoughReady { stepCrusts }
|
||||||
stepDoughs
|
if canShowFlavors { stepFlavors }
|
||||||
}
|
|
||||||
|
|
||||||
if isDoughReady {
|
|
||||||
stepCrusts
|
|
||||||
}
|
|
||||||
|
|
||||||
if canShowFlavors {
|
|
||||||
stepFlavors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.padding(.bottom, 90)
|
.padding(.bottom, 90)
|
||||||
@@ -266,6 +294,7 @@ struct PizzaProductDetailSheet: View {
|
|||||||
name: "Pizza de varios sabores",
|
name: "Pizza de varios sabores",
|
||||||
imageURL: representativeImage,
|
imageURL: representativeImage,
|
||||||
details: selectedDetailsText,
|
details: selectedDetailsText,
|
||||||
|
choices: pizzaChoices.isEmpty ? nil : pizzaChoices,
|
||||||
addons: selectedAddonsPayload,
|
addons: selectedAddonsPayload,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
unitPrice: unitPrice
|
unitPrice: unitPrice
|
||||||
|
|||||||
@@ -56,12 +56,14 @@ struct ProfileView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
// NavigationLink {
|
if appState.featureFlags.isEnabled("at.cupons") {
|
||||||
// Text("Cupons de Desconto")
|
NavigationLink {
|
||||||
// } label: {
|
Text("Cupons de Desconto")
|
||||||
// ProfileMenuRow(icon: "ticket.fill", title: "Cupons de Desconto", badge: "NOVO")
|
} label: {
|
||||||
// }
|
ProfileMenuRow(icon: "ticket.fill", title: "Cupons de Desconto", badge: "NOVO")
|
||||||
// .buttonStyle(.plain)
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
// NavigationLink {
|
// NavigationLink {
|
||||||
// Text("Ajuda")
|
// Text("Ajuda")
|
||||||
|
|||||||
@@ -848,6 +848,7 @@ struct OrderReviewView: View {
|
|||||||
.frame(height: 110)
|
.frame(height: 110)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(AppColors.surface)
|
.background(AppColors.surface)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ struct ScrollOffsetReader: View {
|
|||||||
|
|
||||||
struct AsyncStoreImage: View {
|
struct AsyncStoreImage: View {
|
||||||
let imageURL: String?
|
let imageURL: String?
|
||||||
|
var fallbackImageName: String = "placeholder-product"
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CachedRemoteImage(imageURL: imageURL) {
|
CachedRemoteImage(imageURL: imageURL) {
|
||||||
@@ -58,7 +59,7 @@ struct AsyncStoreImage: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var fallback: some View {
|
private var fallback: some View {
|
||||||
Image("placeholder-product")
|
Image(fallbackImageName)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,15 @@ extension StoreDetailView {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
Group {
|
||||||
|
if item.isPizzaSummary {
|
||||||
|
Image("placeholder-pizza")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
} else {
|
||||||
AsyncStoreImage(imageURL: resolvedURL(item.imageURL))
|
AsyncStoreImage(imageURL: resolvedURL(item.imageURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
.frame(width: 92, height: 92)
|
.frame(width: 92, height: 92)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
|
|
||||||
|
|||||||
@@ -297,8 +297,9 @@ struct UserProfileView: View {
|
|||||||
guard let item else { return }
|
guard let item else { return }
|
||||||
do {
|
do {
|
||||||
guard let data = try await item.loadTransferable(type: Data.self) else { return }
|
guard let data = try await item.loadTransferable(type: Data.self) else { return }
|
||||||
guard let image = UIImage(data: data),
|
guard let image = UIImage(data: data) else { return }
|
||||||
let jpegData = image.jpegData(compressionQuality: 0.82) 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())"
|
profilePicture = "data:image/jpeg;base64,\(jpegData.base64EncodedString())"
|
||||||
} catch {
|
} catch {
|
||||||
SnackbarCenter.shared.show(
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user