migration
This commit is contained in:
622
PediFoods/Views/Main/OrderDetailsView.swift
Normal file
622
PediFoods/Views/Main/OrderDetailsView.swift
Normal file
@@ -0,0 +1,622 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OrderDetailsView: View {
|
||||
let order: PublicOrderResult
|
||||
let orderId: String
|
||||
let initialShortId: String?
|
||||
@Binding var appState: AppState
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.openURL) var openURL
|
||||
@State private var storeContactPhone: String? = nil
|
||||
@State private var resolvedStoreLogoURL: String? = nil
|
||||
@State private var showCallAlert = false
|
||||
@State private var navigateToStore = false
|
||||
@State private var showClearCartAlert = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 14) {
|
||||
screenHeader
|
||||
statusCard
|
||||
storeCard
|
||||
itemsCard
|
||||
totalsCard
|
||||
if hasAddressInfo {
|
||||
addressCard
|
||||
}
|
||||
helpFooter
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 14)
|
||||
.padding(.bottom, UIDevice.bottomNotch)
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
.appHiddenNavigationBar()
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationDestination(isPresented: $navigateToStore) {
|
||||
if let storeId = order.storeId, storeId.isEmpty == false {
|
||||
StoreDetailView(
|
||||
storeId: storeId,
|
||||
storeName: order.storeName ?? "Loja",
|
||||
storeCoverURL: nil,
|
||||
storeLogoURL: order.storeLogoURL,
|
||||
storeCategory: nil,
|
||||
storeRating: nil,
|
||||
storeDistance: nil,
|
||||
storeDeliveryFee: nil,
|
||||
appState: $appState
|
||||
)
|
||||
}
|
||||
}
|
||||
.alert("Ligar para a loja?", isPresented: $showCallAlert) {
|
||||
Button("Ligar para \(order.storeName ?? "a loja")") {
|
||||
if let phone = storeContactPhone {
|
||||
openTel(phone)
|
||||
}
|
||||
}
|
||||
Button("Cancelar", role: .cancel) {}
|
||||
} message: {
|
||||
Text("WhatsApp não encontrado. Deseja ligar para \(order.storeName ?? "a loja")?")
|
||||
}
|
||||
.task {
|
||||
await loadStoreContactPhone()
|
||||
}
|
||||
.alert("Substituir carrinho?", isPresented: $showClearCartAlert) {
|
||||
Button("Limpar e adicionar", role: .destructive) {
|
||||
applyReorder(clearFirst: true)
|
||||
}
|
||||
Button("Cancelar", role: .cancel) {}
|
||||
} message: {
|
||||
Text("Seu carrinho tem itens de \(appState.cart.storeName ?? appState.cart.storeId ?? "outra loja"). Deseja limpar e adicionar itens de \(order.storeName ?? "esta loja")?")
|
||||
}
|
||||
.appBottomSafeAreaInset {
|
||||
VStack {
|
||||
reorderButton
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, UIDevice.bottomNotch + 60)
|
||||
}
|
||||
.background(AppColors.backgroundLight.opacity(0.94))
|
||||
}
|
||||
}
|
||||
|
||||
private var screenHeader: some View {
|
||||
ZStack {
|
||||
Text("Detalhes do Pedido")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
HStack {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 24, weight: .semibold))
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.frame(width: 52, height: 52)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(Circle())
|
||||
.shadow(color: .black.opacity(0.06), radius: 8, y: 2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var statusCard: some View {
|
||||
HStack(spacing: 14) {
|
||||
Circle()
|
||||
.fill(AppColors.brandSoft)
|
||||
.frame(width: 54, height: 54)
|
||||
.overlay(
|
||||
Image(systemName: statusIcon)
|
||||
.font(.system(size: 22, weight: .bold))
|
||||
.foregroundStyle(statusColor)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(statusTitle)
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
Text(statusDateText)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
|
||||
if let reason = cancellationReasonText {
|
||||
Text(reason)
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
if let addr = deliveryAddressSummary {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Text(addr)
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.lineLimit(2)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(16)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
|
||||
private var storeCard: some View {
|
||||
Button {
|
||||
if order.storeId?.isEmpty == false {
|
||||
navigateToStore = true
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
AsyncStoreImage(imageURL: resolvedMediaURL(resolvedStoreLogoURL ?? order.storeLogoURL))
|
||||
.frame(width: 54, height: 54)
|
||||
.clipShape(Circle())
|
||||
.background(AppColors.brandSoft, in: Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(order.storeName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (order.storeName ?? "Loja") : "Loja")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Text(storeSubtitle)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
if order.storeId?.isEmpty == false {
|
||||
Text("Ver loja")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(Color(hex: "#A5D645"))
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var itemsCard: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Itens do Pedido")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
ForEach(order.items) { item in
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
Text("\(max(1, item.qty ?? 1))")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(AppColors.backgroundLight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.name?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? (item.name ?? "Item") : "Item")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
if let price = item.price {
|
||||
Text(formatCurrency(price))
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
|
||||
private var totalsCard: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Resumo de Valores")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
HStack {
|
||||
Text("Subtotal")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Spacer()
|
||||
Text(formatCurrency(subtotalValue))
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Taxa de entrega")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Spacer()
|
||||
Text(formatCurrency(deliveryFeeValue))
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Desconto")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Spacer()
|
||||
Text("- \(formatCurrency(discountValue))")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(Color(hex: "#18A957"))
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Text("Total")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Text(formatCurrency(totalValue))
|
||||
.font(AppTypography.heading1)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
|
||||
private var addressCard: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(spacing: 10) {
|
||||
Circle()
|
||||
.fill(AppColors.backgroundLight)
|
||||
.frame(width: 34, height: 34)
|
||||
.overlay(
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
)
|
||||
Text("ENDEREÇO DE ENTREGA")
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
||||
Text(deliveryAddressLine)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
if deliveryAddressLine2.isEmpty == false {
|
||||
Text(deliveryAddressLine2)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
|
||||
private var reorderButton: some View {
|
||||
Button("Pedir Novamente") {
|
||||
reorder()
|
||||
}
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(Color(hex: "#0E1A06"))
|
||||
.frame(maxWidth: .infinity, minHeight: 56)
|
||||
.background(order.items.isEmpty ? Color(hex: "#C8F06E").opacity(0.45) : Color(hex: "#C8F06E"))
|
||||
.clipShape(Capsule())
|
||||
.buttonStyle(.plain)
|
||||
.disabled(order.items.isEmpty)
|
||||
}
|
||||
|
||||
private var helpFooter: some View {
|
||||
Button {
|
||||
handleHelpTap()
|
||||
} label: {
|
||||
Text("Precisa de ajuda com esse pedido?")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(Color(hex: "#A5D645"))
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var subtotal: Double {
|
||||
order.items.reduce(0) { partial, item in
|
||||
partial + (Double(max(1, item.qty ?? 1)) * (item.price ?? 0))
|
||||
}
|
||||
}
|
||||
|
||||
private var subtotalValue: Double {
|
||||
order.subtotal ?? subtotal
|
||||
}
|
||||
|
||||
private var deliveryFeeValue: Double {
|
||||
max(0, order.deliveryFee ?? 0)
|
||||
}
|
||||
|
||||
private var discountValue: Double {
|
||||
max(0, order.discount ?? 0)
|
||||
}
|
||||
|
||||
private var totalValue: Double {
|
||||
if let total = order.total {
|
||||
return total
|
||||
}
|
||||
let calculated = subtotalValue + deliveryFeeValue - discountValue
|
||||
return max(0, calculated)
|
||||
}
|
||||
|
||||
private var hasAddressInfo: Bool {
|
||||
deliveryAddressLine.isEmpty == false || deliveryAddressLine2.isEmpty == false
|
||||
}
|
||||
|
||||
private var deliveryAddressLine: String {
|
||||
guard let address = order.deliveryAddress else { return "" }
|
||||
let street = normalizedText(address.street)
|
||||
let number = normalizedText(address.number)
|
||||
let base = [street, number].filter { $0.isEmpty == false }.joined(separator: ", ")
|
||||
if base.isEmpty == false { return base }
|
||||
return normalizedText(address.label)
|
||||
}
|
||||
|
||||
private var deliveryAddressLine2: String {
|
||||
guard let address = order.deliveryAddress else { return "" }
|
||||
let neighborhood = normalizedText(address.neighborhood)
|
||||
let city = normalizedText(address.city)
|
||||
let state = normalizedText(address.state)
|
||||
let zip = normalizedText(address.zip)
|
||||
return [neighborhood, city, state, zip]
|
||||
.filter { $0.isEmpty == false }
|
||||
.joined(separator: " • ")
|
||||
}
|
||||
|
||||
private var storeSubtitle: String {
|
||||
if deliveryAddressLine2.isEmpty == false {
|
||||
return deliveryAddressLine2
|
||||
}
|
||||
return "Pedido #\(displayOrderTitle)"
|
||||
}
|
||||
|
||||
private var deliveryAddressSummary: String? {
|
||||
if let full = order.fullAddress, full.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
return full.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
let line1 = deliveryAddressLine
|
||||
let line2 = deliveryAddressLine2
|
||||
let combined = [line1, line2].filter { $0.isEmpty == false }.joined(separator: ", ")
|
||||
return combined.isEmpty ? nil : combined
|
||||
}
|
||||
|
||||
private var cancellationReasonText: String? {
|
||||
guard statusTitle.contains("cancelado"),
|
||||
let reason = order.cancellationReason,
|
||||
reason.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||
else { return nil }
|
||||
return "Motivo: \(reason.trimmingCharacters(in: .whitespacesAndNewlines))"
|
||||
}
|
||||
|
||||
private var statusTitle: String {
|
||||
let status = normalized(order.status)
|
||||
if status.contains("CANCEL") { return "Pedido cancelado" }
|
||||
if status.contains("COMPLETED") || status.contains("DELIVERED") {
|
||||
return normalized(order.deliveryType ?? order.deliveryTypeLabel).contains("PICKUP") ? "Pedido retirado" : "Pedido concluído"
|
||||
}
|
||||
if status.contains("DELIVER") || status.contains("ROTA") { return "Pedido em rota" }
|
||||
if status.contains("READY") { return "Pedido pronto" }
|
||||
if status.contains("PREPAR") { return "Pedido em produção" }
|
||||
return "Pedido confirmado"
|
||||
}
|
||||
|
||||
private var statusDateText: String {
|
||||
if let event = order.timeline.first,
|
||||
let date = event.date, date.isEmpty == false {
|
||||
let time = event.time.flatMap { $0.isEmpty ? nil : $0 }
|
||||
let combined = time.map { "\(date) às \($0)" } ?? date
|
||||
return "\(statusDatePrefix) \(combined)"
|
||||
}
|
||||
if let formatted = formatDate(order.updatedAt ?? order.createdAt) {
|
||||
return "\(statusDatePrefix) \(formatted)"
|
||||
}
|
||||
return statusDatePrefix
|
||||
}
|
||||
|
||||
private var statusDatePrefix: String {
|
||||
if statusTitle.contains("cancelado") { return "Cancelado em" }
|
||||
if statusTitle.contains("retirado") { return "Retirado em" }
|
||||
if statusTitle.contains("concluído") { return "Entregue em" }
|
||||
return "Atualizado em"
|
||||
}
|
||||
|
||||
private var statusIcon: String {
|
||||
statusTitle.contains("cancelado") ? "xmark" : "checkmark"
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
statusTitle.contains("cancelado") ? Color.red : AppColors.primary
|
||||
}
|
||||
|
||||
private var displayOrderTitle: String {
|
||||
if let short = order.shortId, short.isEmpty == false { return short }
|
||||
let orderIdValue = order.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if orderIdValue.isEmpty == false { return orderIdValue }
|
||||
if let initialShortId, initialShortId.isEmpty == false { return initialShortId }
|
||||
return String(orderId.prefix(6))
|
||||
}
|
||||
|
||||
private func formatCurrency(_ value: Double) -> String {
|
||||
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
|
||||
}
|
||||
|
||||
private func reorder() {
|
||||
guard order.items.isEmpty == false else { return }
|
||||
let cartStoreId = appState.cart.storeId ?? appState.cart.items.first?.storeId ?? ""
|
||||
let orderStoreId = order.storeId ?? ""
|
||||
let cartHasDifferentStore = cartStoreId.isEmpty == false
|
||||
&& orderStoreId.isEmpty == false
|
||||
&& cartStoreId != orderStoreId
|
||||
&& appState.cart.items.isEmpty == false
|
||||
if cartHasDifferentStore {
|
||||
showClearCartAlert = true
|
||||
} else {
|
||||
applyReorder(clearFirst: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyReorder(clearFirst: Bool) {
|
||||
if clearFirst {
|
||||
appState.cart.clear()
|
||||
}
|
||||
let storeId = order.storeId ?? ""
|
||||
if appState.cart.storeId == nil || appState.cart.storeId?.isEmpty == true {
|
||||
appState.cart.storeId = storeId
|
||||
appState.cart.storeName = order.storeName
|
||||
}
|
||||
var addedCount = 0
|
||||
for item in order.items {
|
||||
let name = item.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard name.isEmpty == false else { continue }
|
||||
let qty = max(1, item.qty ?? 1)
|
||||
let price = item.price ?? 0
|
||||
let cartItem = CartItemState(
|
||||
id: UUID().uuidString,
|
||||
productId: item.productId ?? item.id,
|
||||
storeId: storeId,
|
||||
name: name,
|
||||
imageURL: nil,
|
||||
details: nil,
|
||||
addons: [],
|
||||
quantity: qty,
|
||||
unitPrice: price
|
||||
)
|
||||
appState.cart.add(item: cartItem)
|
||||
addedCount += qty
|
||||
}
|
||||
let label = addedCount == 1 ? "1 item adicionado ao carrinho." : "\(addedCount) itens adicionados ao carrinho."
|
||||
SnackbarCenter.shared.show(title: label, style: .success, icon: "cart.badge.plus", duration: 2.5)
|
||||
}
|
||||
|
||||
private func formatDate(_ isoValue: String?) -> String? {
|
||||
guard let isoValue, isoValue.isEmpty == false else { return nil }
|
||||
|
||||
let iso = ISO8601DateFormatter()
|
||||
let optionSets: [ISO8601DateFormatter.Options] = [
|
||||
[.withInternetDateTime, .withFractionalSeconds],
|
||||
[.withInternetDateTime],
|
||||
[.withFullDate, .withTime, .withColonSeparatorInTime],
|
||||
[.withFullDate, .withTime, .withColonSeparatorInTime, .withTimeZone],
|
||||
[.withFullDate]
|
||||
]
|
||||
var date: Date? = nil
|
||||
for options in optionSets {
|
||||
iso.formatOptions = options
|
||||
if let d = iso.date(from: isoValue) {
|
||||
date = d
|
||||
break
|
||||
}
|
||||
}
|
||||
if date == nil {
|
||||
let fallback = DateFormatter()
|
||||
fallback.locale = Locale(identifier: "en_US_POSIX")
|
||||
for fmt in ["yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ssZ",
|
||||
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd"] {
|
||||
fallback.dateFormat = fmt
|
||||
if let d = fallback.date(from: isoValue) { date = d; break }
|
||||
}
|
||||
}
|
||||
guard let date else { return nil }
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "pt_BR")
|
||||
formatter.dateFormat = "dd MMM, HH:mm"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadStoreContactPhone() async {
|
||||
if let inline = order.storePhone, inline.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
storeContactPhone = inline
|
||||
}
|
||||
guard let storeId = order.storeId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
storeId.isEmpty == false else { return }
|
||||
do {
|
||||
let response = try await ApiService().storeInfo(storeId: storeId)
|
||||
if response.error == false, let result = response.result {
|
||||
let phone = (result.whatsapp ?? result.phone ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if phone.isEmpty == false {
|
||||
storeContactPhone = phone
|
||||
}
|
||||
if let logo = result.logo, logo.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
resolvedStoreLogoURL = logo
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private func handleHelpTap() {
|
||||
guard let phoneRaw = storeContactPhone,
|
||||
phoneRaw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else {
|
||||
SnackbarCenter.shared.show(
|
||||
title: "Telefone da loja indisponível.",
|
||||
style: .warning,
|
||||
icon: "exclamationmark.triangle.fill",
|
||||
duration: 2.8
|
||||
)
|
||||
return
|
||||
}
|
||||
if let waURL = makeWhatsAppURL(from: phoneRaw) {
|
||||
openURL(waURL)
|
||||
} else {
|
||||
showCallAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
private func openTel(_ phoneRaw: String) {
|
||||
let digits = phoneRaw.filter(\.isNumber)
|
||||
if digits.isEmpty { return }
|
||||
if let url = URL(string: "tel://\(digits)") {
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeWhatsAppURL(from phoneRaw: String) -> URL? {
|
||||
var digits = phoneRaw.filter(\.isNumber)
|
||||
if digits.isEmpty { return nil }
|
||||
if digits.hasPrefix("0") { digits = String(digits.drop(while: { $0 == "0" })) }
|
||||
if digits.hasPrefix("55") == false && (digits.count == 10 || digits.count == 11) {
|
||||
digits = "55" + digits
|
||||
}
|
||||
guard digits.count >= 12 else { return nil }
|
||||
return URL(string: "https://wa.me/\(digits)")
|
||||
}
|
||||
|
||||
private func normalized(_ value: String?) -> String {
|
||||
(value ?? "")
|
||||
.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
|
||||
.uppercased()
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private func resolvedMediaURL(_ raw: String?) -> String? {
|
||||
ImageSourceResolver.resolve(raw)
|
||||
}
|
||||
|
||||
private func normalizedText(_ value: String?) -> String {
|
||||
(value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user