import SwiftUI #if canImport(LCEssentials) import LCEssentials #endif import UIKit struct ProfileView: View { @Binding var selectedTab: MainTab let tokenStore: TokenStore @Binding var appState: AppState let enterAuth: () -> Void @State var openAddressesOnboarding = false @State var onboardingMessage: String? = nil @State var showLogoutAlert = false @State var showDeleteAccountAlert = false @State var isDeletingAccount = false @State private var openOrders = false let tabBarClearance: CGFloat = 120 var body: some View { ScrollView(showsIndicators: false) { VStack(spacing: 18) { NavigationLink { UserProfileView(appState: $appState) } label: { header } .buttonStyle(.plain) VStack(spacing: 14) { NavigationLink { OrdersView(appState: $appState) } label: { ProfileMenuRow(icon: "list.bullet.clipboard.fill", title: "Meus Pedidos") } .buttonStyle(.plain) NavigationLink { AddressesView(message: nil, appState: $appState) } label: { ProfileMenuRow(icon: "mappin.circle.fill", title: "Meus Endereços") } .buttonStyle(.plain) NavigationLink { SavedCardsView(appState: $appState) } label: { ProfileMenuRow(icon: "creditcard.fill", title: "Meus Cartões") } .buttonStyle(.plain) NavigationLink { MyReviewsView() } label: { ProfileMenuRow(icon: "star.fill", title: "Minhas Avaliações") } .buttonStyle(.plain) if appState.featureFlags.isEnabled("at.cupons") { NavigationLink { Text("Cupons de Desconto") } label: { ProfileMenuRow(icon: "ticket.fill", title: "Cupons de Desconto", badge: "NOVO") } .buttonStyle(.plain) } // NavigationLink { // Text("Ajuda") // } label: { // ProfileMenuRow(icon: "gearshape.fill", title: "Configurações") // } // .buttonStyle(.plain) } .padding(.horizontal, 20) Button(action: { showLogoutAlert = true }) { HStack(spacing: 10) { Image(systemName: "rectangle.portrait.and.arrow.right") .font(.system(size: 18, weight: .semibold)) Text("Sair da Conta") .font(AppTypography.heading3) } .foregroundStyle(Color.red) } .buttonStyle(.plain) .padding(.top, 10) .padding(.horizontal, 20) Button(action: { showDeleteAccountAlert = true }) { HStack(spacing: 10) { if isDeletingAccount { ProgressView() .tint(Color.red) } else { Image(systemName: "trash.fill") .font(.system(size: 16, weight: .semibold)) } Text("Excluir Conta") .font(AppTypography.body) } .foregroundStyle(Color.red.opacity(0.7)) } .buttonStyle(.plain) .disabled(isDeletingAccount) .padding(.top, 2) .padding(.horizontal, 20) Text("Versão 1.0b") .font(.system(size: 16, weight: .medium)) .foregroundStyle(AppColors.textMuted) HStack(spacing: 6) { NavigationLink { TermsOfUseView() } label: { Text("Termos de Uso") } .buttonStyle(.plain) Text("·") NavigationLink { PrivacyPolicyView() } label: { Text("Política de Privacidade") } .buttonStyle(.plain) } .font(.system(size: 14, weight: .medium)) .foregroundStyle(AppColors.textMuted) .padding(.bottom, 12) } } .ignoresSafeArea(edges: .top) .appBottomSafeAreaInset { Rectangle() .fill(AppColors.backgroundLight.opacity(0.8)) .frame(height: tabBarClearance) .padding(.bottom, -UIDevice.bottomNotch) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.backgroundLight) .alert("Sair da conta?", isPresented: $showLogoutAlert) { Button("Cancelar", role: .cancel) {} Button("Sair", role: .destructive) { logout() } } message: { Text("Tem certeza que deseja sair da sua conta?") } .alert("Excluir sua conta?", isPresented: $showDeleteAccountAlert) { Button("Cancelar", role: .cancel) {} Button("Excluir", role: .destructive) { Task { await deleteAccount() } } } message: { Text("Essa ação é permanente. Seus dados de perfil, endereços, cartões e favoritos serão excluídos e não poderão ser recuperados.") } .onAppear { guard let message = appState.address.onboardingMessage else { return } onboardingMessage = message appState.address.onboardingMessage = nil openAddressesOnboarding = true } .sheet(isPresented: $openAddressesOnboarding) { NavigationStack { AddressesView(message: onboardingMessage, appState: $appState) } } .navigationDestination(isPresented: $openOrders) { OrdersView(appState: $appState) } .onChange(of: appState.shouldNavigateToOrders) { _, val in if val { appState.shouldNavigateToOrders = false openOrders = true } } .onChange(of: appState.pendingOrderDeepLink) { _, val in if val != nil { openOrders = true } } } private var header: some View { VStack(spacing: 10) { ZStack(alignment: .bottomTrailing) { Circle() .fill(Color.white.opacity(0.18)) .frame(width: 96, height: 96) .overlay( Group { if let picture = profilePictureURL { AsyncStoreImage(imageURL: picture) .frame(width: 92, height: 92) .clipShape(Circle()) } else { Text(profileInitials) .font(.system(size: 30, weight: .bold)) .foregroundStyle(AppColors.textInverse) } } ) Circle() .fill(AppColors.tertiary) .frame(width: 36, height: 36) .overlay( Image(systemName: "pencil") .font(.system(size: 15, weight: .bold)) .foregroundStyle(AppColors.textPrimary) ) .overlay( Circle() .stroke(Color.black.opacity(0.15), lineWidth: 1) ) } Text(profileName) .font(.system(size: 22, weight: .heavy)) .foregroundStyle(AppColors.textInverse) .lineLimit(1) .minimumScaleFactor(0.8) HStack(spacing: 8) { Text("Ver Perfil") .font(.system(size: 16, weight: .medium)) Image(systemName: "arrow.right") .font(.system(size: 14, weight: .semibold)) } .foregroundStyle(AppColors.tertiary) } .frame(maxWidth: .infinity) .padding(.top, 54) .padding(.bottom, 32) .background(headerGradient) .clipShape( ProfileHeaderShape( topLeadingRadius: 0, bottomLeadingRadius: 42, bottomTrailingRadius: 42, topTrailingRadius: 0 ) ) .ignoresSafeArea(edges: .top) } private var headerGradient: LinearGradient { LinearGradient( colors: [Color(hex: "#123221"), Color(hex: "#0F2A1C")], startPoint: .topLeading, endPoint: .bottomTrailing ) } private var profileName: String { let trimmed = appState.profile.name.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? "Alex Silva" : trimmed } private var profileInitials: String { let parts = profileName.split(separator: " ").prefix(2) let joined = parts.compactMap { $0.first }.map(String.init).joined() return joined.isEmpty ? "AS" : joined.uppercased() } private var profilePictureURL: String? { let raw = appState.profile.profilePicture.trimmingCharacters(in: .whitespacesAndNewlines) guard raw.isEmpty == false else { return nil } return ImageSourceResolver.resolve(raw) } private func logout() { tokenStore.clear() SessionStateStore.clearActiveUser() SessionStateStore.clearTrackedOrders() AppContentCache.shared.invalidate() AppImageCache.shared.invalidateAll() appState = AppState() enterAuth() } @MainActor private func deleteAccount() async { guard isDeletingAccount == false else { return } isDeletingAccount = true defer { isDeletingAccount = false } do { let response = try await ApiService().deleteAccount() guard response.error == false else { SnackbarCenter.shared.show( title: response.message ?? "Não foi possível excluir sua conta.", style: .error, icon: "xmark.octagon.fill", duration: 3.5 ) return } logout() } catch { SnackbarCenter.shared.show( title: "Não foi possível excluir sua conta. Tente novamente.", style: .error, icon: "xmark.octagon.fill", duration: 3.5 ) } } } struct ProfileLoggedOutView: View { let enterAuth: () -> Void var body: some View { VStack(spacing: 18) { Spacer() Image(systemName: "person.crop.circle.badge.questionmark") .font(.system(size: 56, weight: .regular)) .foregroundStyle(AppColors.textMuted) Text("Entre na sua conta") .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) Text("Faça login ou cadastre-se para ver seu perfil, pedidos e endereços.") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .multilineTextAlignment(.center) .padding(.horizontal, 32) Button { enterAuth() } label: { Text("Entrar ou Cadastrar") .font(AppTypography.heading3) .foregroundStyle(AppColors.textInverse) .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(AppColors.primary) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) } .buttonStyle(.plain) .padding(.horizontal, 32) .padding(.top, 8) Spacer() Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.backgroundLight) } } struct ProfileMenuRow: View { let icon: String let title: String var badge: String? = nil var body: some View { HStack(spacing: 12) { Circle() .fill(Color(hex: "#E9F0E2")) .frame(width: 44, height: 44) .overlay( Image(systemName: icon) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(Color(hex: "#173824")) ) Text(title) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(Color(hex: "#0F1A34")) Spacer(minLength: 10) if let badge { Text(badge) .font(.system(size: 12, weight: .bold)) .foregroundStyle(Color(hex: "#1C2A1C")) .padding(.horizontal, 10) .padding(.vertical, 6) .background(Color(hex: "#EAF1D6")) .clipShape(Capsule()) } Image(systemName: "chevron.right") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(Color(hex: "#BFC7D4")) } .padding(.horizontal, 18) .padding(.vertical, 14) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .shadow(color: Color.black.opacity(0.02), radius: 6, y: 2) } } struct ProfileHeaderShape: Shape { var topLeadingRadius: CGFloat var bottomLeadingRadius: CGFloat var bottomTrailingRadius: CGFloat var topTrailingRadius: CGFloat func path(in rect: CGRect) -> Path { let tl = min(min(topLeadingRadius, rect.width / 2), rect.height / 2) let tr = min(min(topTrailingRadius, rect.width / 2), rect.height / 2) let bl = min(min(bottomLeadingRadius, rect.width / 2), rect.height / 2) let br = min(min(bottomTrailingRadius, rect.width / 2), rect.height / 2) var path = Path() path.move(to: CGPoint(x: rect.minX + tl, y: rect.minY)) path.addLine(to: CGPoint(x: rect.maxX - tr, y: rect.minY)) path.addArc(center: CGPoint(x: rect.maxX - tr, y: rect.minY + tr), radius: tr, startAngle: .degrees(-90), endAngle: .degrees(0), clockwise: false) path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - br)) path.addArc(center: CGPoint(x: rect.maxX - br, y: rect.maxY - br), radius: br, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: false) path.addLine(to: CGPoint(x: rect.minX + bl, y: rect.maxY)) path.addArc(center: CGPoint(x: rect.minX + bl, y: rect.maxY - bl), radius: bl, startAngle: .degrees(90), endAngle: .degrees(180), clockwise: false) path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + tl)) path.addArc(center: CGPoint(x: rect.minX + tl, y: rect.minY + tl), radius: tl, startAngle: .degrees(180), endAngle: .degrees(270), clockwise: false) path.closeSubpath() return path } }