bck
This commit is contained in:
@@ -32,13 +32,8 @@ struct AddAddressFormView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private var logoImage: some View {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("pedifoods", bundle: .module)
|
||||
.resizable()
|
||||
#else
|
||||
SwiftUI.Image("pedifoods")
|
||||
.resizable()
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -70,8 +65,8 @@ struct AddAddressFormView: View {
|
||||
.padding(.bottom, 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", text: $label)
|
||||
LoginField(icon: "mail", placeholder: "CEP", text: $zipCode)
|
||||
LoginField(icon: "tag", placeholder: "Ex: Casa, Trabalho", keyboardType: .default, text: $label)
|
||||
LoginField(icon: "mail", placeholder: "CEP", keyboardType: .numberPad, text: $zipCode)
|
||||
.onChange(of: zipCode) { _, newValue in
|
||||
let masked = formatZipCodeBR(newValue)
|
||||
if masked != newValue {
|
||||
@@ -99,12 +94,12 @@ struct AddAddressFormView: View {
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.horizontal, 6)
|
||||
}
|
||||
LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", text: $address)
|
||||
LoginField(icon: "number", placeholder: "Número", text: $number)
|
||||
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", text: $complement)
|
||||
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", text: $neighborhood)
|
||||
LoginField(icon: "building.2", placeholder: "Cidade", text: $city)
|
||||
LoginField(icon: "map", placeholder: "Estado (UF)", text: $state)
|
||||
LoginField(icon: "mappin.and.ellipse", placeholder: "Rua / Avenida", keyboardType: .default, text: $address)
|
||||
LoginField(icon: "number", placeholder: "Número", keyboardType: .default, text: $number)
|
||||
LoginField(icon: "plus.app", placeholder: "Complemento (opcional)", keyboardType: .default, text: $complement)
|
||||
LoginField(icon: "square.grid.2x2", placeholder: "Bairro", keyboardType: .default, text: $neighborhood)
|
||||
LoginField(icon: "building.2", placeholder: "Cidade", keyboardType: .default, text: $city)
|
||||
LoginField(icon: "map", placeholder: "Estado (UF)", keyboardType: .default, text: $state)
|
||||
.onChange(of: state) { _, newValue in
|
||||
let normalized = String(newValue.uppercased().prefix(2))
|
||||
if normalized != newValue {
|
||||
|
||||
@@ -61,6 +61,7 @@ extension HomeView {
|
||||
rating: store.rating ?? 0,
|
||||
reviews: String(store.positiveReviews ?? store.reviewsCount ?? 0),
|
||||
distance: formatDistance(store.distance),
|
||||
deliveryFee: store.deliveryFee,
|
||||
category: store.category ?? "Loja",
|
||||
promoText: nil,
|
||||
isFavorite: appState.favorites.storeIds.contains(store.id),
|
||||
|
||||
@@ -101,6 +101,7 @@ struct HomeView: View {
|
||||
storeCategory: store.category,
|
||||
storeRating: store.rating,
|
||||
storeDistance: store.distance,
|
||||
storeDeliveryFee: store.deliveryFee,
|
||||
appState: $appState
|
||||
)
|
||||
} label: {
|
||||
@@ -158,6 +159,7 @@ struct HomeView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
@@ -179,6 +181,7 @@ struct HomeView: View {
|
||||
storeCategory: store.category,
|
||||
storeRating: store.rating,
|
||||
storeDistance: store.distance,
|
||||
storeDeliveryFee: store.deliveryFee,
|
||||
appState: $appState
|
||||
)
|
||||
} label: {
|
||||
|
||||
@@ -49,6 +49,7 @@ struct SearchBar: View {
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.frame(height: 52)
|
||||
|
||||
@@ -222,29 +222,15 @@ struct OrderTrackingView: View {
|
||||
|
||||
Group {
|
||||
if hasTrackingImage {
|
||||
#if os(Android)
|
||||
SwiftUI.Image(trackingImageName, bundle: .module)
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#else
|
||||
Image(trackingImageName)
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#endif
|
||||
} else if hasPlaceholderProductImage {
|
||||
#if os(Android)
|
||||
SwiftUI.Image("placeholder-product", bundle: .module)
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#else
|
||||
Image("placeholder-product")
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
#endif
|
||||
} else {
|
||||
ZStack {
|
||||
Color.black.opacity(0.08)
|
||||
|
||||
@@ -688,58 +688,57 @@ struct OrderEntryDestinationView: View {
|
||||
func resolveRoute() async {
|
||||
defer { isResolvingRoute = false }
|
||||
|
||||
if routeIntent == .tracking {
|
||||
return
|
||||
}
|
||||
|
||||
let order = await fetchOrderForRouting()
|
||||
guard let order else { return }
|
||||
|
||||
if routeIntent == .details {
|
||||
orderDetails = order
|
||||
return
|
||||
}
|
||||
if shouldOpenOrderDetails(for: order) {
|
||||
|
||||
// For both .tracking and .auto: show payment screen if payment is still pending.
|
||||
// Timeline only shows once payment is confirmed or method is off-app.
|
||||
if shouldOpenPaymentScreen(for: order) {
|
||||
let normalizedMethod = normalizePaymentMethod(order)
|
||||
if normalizedMethod.contains("PIX") {
|
||||
let pixFromPayment = order.payment?.pix
|
||||
let pixFromPayload = order.paymentPayload
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste
|
||||
: pixFromPayload?.copyPaste
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage
|
||||
: pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate
|
||||
: pixFromPayload?.expirationDate
|
||||
pixContext = PixPaymentContext(
|
||||
id: order.id,
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||
? (copyPaste ?? "")
|
||||
: "Código PIX indisponível no momento. Aguarde e tente novamente.",
|
||||
qrCodeImageBase64: qrCodeImage,
|
||||
expirationDate: expirationDate
|
||||
)
|
||||
return
|
||||
}
|
||||
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
||||
cardContext = CardPaymentContext(
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
total: order.total ?? fallbackTotal ?? 0
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For .auto only: route terminal/canceled orders to details instead of timeline.
|
||||
if routeIntent == .auto && shouldOpenOrderDetails(for: order) {
|
||||
orderDetails = order
|
||||
return
|
||||
}
|
||||
guard shouldOpenPaymentScreen(for: order) else { return }
|
||||
|
||||
let normalizedMethod = normalizePaymentMethod(order)
|
||||
if normalizedMethod.contains("PIX") {
|
||||
let pixFromPayment = order.payment?.pix
|
||||
let pixFromPayload = order.paymentPayload
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste
|
||||
: pixFromPayload?.copyPaste
|
||||
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage
|
||||
: pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate
|
||||
: pixFromPayload?.expirationDate
|
||||
|
||||
pixContext = PixPaymentContext(
|
||||
id: order.id,
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||
? (copyPaste ?? "")
|
||||
: "Código PIX indisponível no momento. Aguarde e tente novamente.",
|
||||
qrCodeImageBase64: qrCodeImage,
|
||||
expirationDate: expirationDate
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
||||
cardContext = CardPaymentContext(
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
total: order.total ?? fallbackTotal ?? 0
|
||||
)
|
||||
return
|
||||
}
|
||||
// .tracking (and .auto fallthrough) → nil states → body renders OrderTrackingView
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -81,6 +81,7 @@ struct PizzaFlavorAddonsSheet: View {
|
||||
.appInlineNavigationTitle()
|
||||
.appTopBarTrailingToolbar {
|
||||
Button("Concluir") { dismiss() }
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
#if os(Android)
|
||||
import SwiftUI
|
||||
|
||||
struct UserProfileView: View {
|
||||
@Binding var appState: AppState
|
||||
|
||||
@State var name: String = ""
|
||||
@State var email: String = ""
|
||||
@State var phone: String = ""
|
||||
@State var profilePicture: String = ""
|
||||
@State var isSaving = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 22) {
|
||||
avatarSection
|
||||
formSection
|
||||
saveButton
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 18)
|
||||
.padding(.bottom, UIDevice.bottomNotch + 24)
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
.navigationTitle("Meu Perfil")
|
||||
.appInlineNavigationTitle()
|
||||
.onAppear {
|
||||
hydrateFromAppState()
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarSection: some View {
|
||||
VStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(AppColors.brandSoft)
|
||||
.frame(width: 110, height: 110)
|
||||
.overlay {
|
||||
if let imageSource = resolvedProfilePicture {
|
||||
AsyncStoreImage(imageURL: imageSource)
|
||||
.frame(width: 104, height: 104)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Text(initials)
|
||||
.font(.system(size: 32, weight: .bold))
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Remover") {
|
||||
profilePicture = ""
|
||||
}
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.buttonStyle(.plain)
|
||||
.disabled(profilePicture.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private var formSection: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
textFieldSection(title: "Nome", placeholder: "Seu nome completo", text: $name)
|
||||
|
||||
textFieldSection(title: "E-mail", placeholder: "seu@email.com", text: $email)
|
||||
.appNoAutoCap()
|
||||
|
||||
textFieldSection(title: "Telefone", placeholder: "(00) 00000-0000", text: $phone)
|
||||
.onChange(of: phone) { _, newValue in
|
||||
let masked = formatPhoneBR(displayPhoneDigits(newValue))
|
||||
if masked != newValue {
|
||||
phone = masked
|
||||
}
|
||||
}
|
||||
.appNoAutoCap()
|
||||
|
||||
textFieldSection(
|
||||
title: "Foto (URL/Base64)",
|
||||
placeholder: "https://... ou data:image/jpeg;base64,...",
|
||||
text: $profilePicture
|
||||
)
|
||||
.appNoAutoCap()
|
||||
}
|
||||
.padding(16)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
|
||||
private var saveButton: some View {
|
||||
Button(isSaving ? "Salvando..." : "Salvar Alterações") {
|
||||
Task { await saveProfile() }
|
||||
}
|
||||
.font(AppTypography.button)
|
||||
.foregroundStyle(AppColors.textInverse)
|
||||
.frame(maxWidth: .infinity, minHeight: 56)
|
||||
.background(AppColors.primary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
.buttonStyle(.plain)
|
||||
.disabled(isSaving || canSave == false)
|
||||
.opacity((isSaving || canSave == false) ? 0.6 : 1.0)
|
||||
}
|
||||
|
||||
private var initials: String {
|
||||
let parts = name
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.split(separator: " ")
|
||||
.prefix(2)
|
||||
let letters = parts.compactMap { $0.first }.map(String.init).joined()
|
||||
return letters.isEmpty ? "PF" : letters.uppercased()
|
||||
}
|
||||
|
||||
private var resolvedProfilePicture: String? {
|
||||
let trimmed = profilePicture.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard trimmed.isEmpty == false else { return nil }
|
||||
return ImageSourceResolver.resolve(trimmed)
|
||||
}
|
||||
|
||||
private var canSave: Bool {
|
||||
let cleanName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let cleanEmail = email.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalizedPhone = normalizePhoneNumberForAPI(phone)
|
||||
return cleanName.isEmpty == false
|
||||
&& cleanEmail.isEmpty == false
|
||||
&& cleanEmail.contains("@")
|
||||
&& normalizedPhone.isEmpty == false
|
||||
}
|
||||
|
||||
private func hydrateFromAppState() {
|
||||
name = appState.profile.name
|
||||
email = appState.profile.email
|
||||
phone = formatPhoneForDisplay(appState.profile.phone)
|
||||
profilePicture = appState.profile.profilePicture
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func saveProfile() async {
|
||||
guard canSave else { return }
|
||||
|
||||
let cleanName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let cleanEmail = email.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalizedPhone = normalizePhoneNumberForAPI(phone)
|
||||
let cleanPhoto = profilePicture.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
isSaving = true
|
||||
defer { isSaving = false }
|
||||
|
||||
do {
|
||||
let response = try await ApiService().updateCustomerProfile(
|
||||
name: cleanName,
|
||||
email: cleanEmail,
|
||||
phoneNumber: normalizedPhone,
|
||||
profilePicture: cleanPhoto.isEmpty ? nil : cleanPhoto
|
||||
)
|
||||
|
||||
if response.error {
|
||||
SnackbarCenter.shared.show(
|
||||
title: response.message ?? "Não foi possível atualizar seu perfil.",
|
||||
style: .error,
|
||||
icon: "xmark.octagon.fill",
|
||||
duration: 3.0
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let customer = response.result
|
||||
appState.profile.id = customer?.id ?? appState.profile.id
|
||||
appState.profile.name = customer?.name ?? cleanName
|
||||
appState.profile.email = customer?.email ?? cleanEmail
|
||||
appState.profile.phone = customer?.phoneNumber ?? normalizedPhone
|
||||
appState.profile.profilePicture = customer?.profilePicture ?? cleanPhoto
|
||||
SessionStateStore.setActiveUserKey(
|
||||
SessionStateStore.makeUserKey(profileId: appState.profile.id, email: appState.profile.email)
|
||||
)
|
||||
|
||||
SnackbarCenter.shared.show(
|
||||
title: "Perfil atualizado com sucesso.",
|
||||
style: .success,
|
||||
icon: "checkmark.circle.fill",
|
||||
duration: 2.0
|
||||
)
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(
|
||||
title: "Não foi possível atualizar seu perfil.",
|
||||
style: .error,
|
||||
icon: "xmark.octagon.fill",
|
||||
duration: 3.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func formatPhoneForDisplay(_ raw: String) -> String {
|
||||
let digits = displayPhoneDigits(raw)
|
||||
if digits.isEmpty { return "" }
|
||||
return formatPhoneBR(digits)
|
||||
}
|
||||
|
||||
private func displayPhoneDigits(_ raw: String) -> String {
|
||||
var digits = raw.filter(\.isNumber)
|
||||
if digits.hasPrefix("55"), digits.count > 11 {
|
||||
digits = String(digits.dropFirst(2))
|
||||
}
|
||||
return String(digits.prefix(11))
|
||||
}
|
||||
|
||||
private func textFieldSection(title: String, placeholder: String, text: Binding<String>) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
|
||||
TextField(placeholder, text: text)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 50)
|
||||
.background(AppColors.backgroundLight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.stroke(AppColors.secondary.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,9 +1,6 @@
|
||||
#if !os(Android)
|
||||
import SwiftUI
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
import PhotosUI
|
||||
#endif
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@@ -17,8 +14,8 @@ struct UserProfileView: View {
|
||||
@State var profilePicture: String = ""
|
||||
@State var isSaving = false
|
||||
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
@State var selectedPhotoItem: PhotosPickerItem?
|
||||
#if os(iOS)
|
||||
@State private var selectedPhotoItem: PhotosPickerItem?
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
@@ -38,7 +35,7 @@ struct UserProfileView: View {
|
||||
.onAppear {
|
||||
hydrateFromAppState()
|
||||
}
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
.onChange(of: selectedPhotoItem) { _, newItem in
|
||||
Task { await applySelectedPhoto(newItem) }
|
||||
}
|
||||
@@ -63,7 +60,7 @@ struct UserProfileView: View {
|
||||
}
|
||||
|
||||
HStack(spacing: 10) {
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
PhotosPicker(selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared()) {
|
||||
Text("Trocar Foto")
|
||||
.font(AppTypography.caption)
|
||||
@@ -244,19 +241,15 @@ struct UserProfileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(PhotosUI) && os(iOS)
|
||||
#if os(iOS)
|
||||
@MainActor
|
||||
private func applySelectedPhoto(_ item: PhotosPickerItem?) async {
|
||||
guard let item else { return }
|
||||
do {
|
||||
guard let data = try await item.loadTransferable(type: Data.self) else { return }
|
||||
#if canImport(UIKit)
|
||||
guard let image = UIImage(data: data),
|
||||
let jpegData = image.jpegData(compressionQuality: 0.82) else { return }
|
||||
profilePicture = "data:image/jpeg;base64,\(jpegData.base64EncodedString())"
|
||||
#else
|
||||
profilePicture = "data:image/jpeg;base64,\(data.base64EncodedString())"
|
||||
#endif
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(
|
||||
title: "Não foi possível carregar a foto selecionada.",
|
||||
@@ -268,4 +261,3 @@ struct UserProfileView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user