import SwiftUI struct ContentView: View { @State var root: RootFlow = DefaultTokenStore().jwt == nil ? .auth : .main @State var selectedTab: MainTab = .home private let tokenStore: TokenStore = DefaultTokenStore() @State var appState = AppState() @State var isBootstrappingSession = false #if os(iOS) @State private var sessionExpiredObserver: NSObjectProtocol? #endif #if os(Android) @State var snackbarCenter = SnackbarCenter.shared #else @StateObject var snackbarCenter = SnackbarCenter.shared #endif var body: some View { ZStack(alignment: .top) { Group { switch root { case .auth: AuthFlowView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore, appState: $appState) case .main: if isBootstrappingSession { sessionBootstrapLoadingView } else { MainTabView(selectedTab: $selectedTab, root: $root, tokenStore: tokenStore, appState: $appState) } } } SnackbarOverlay(center: snackbarCenter) } .sheet(item: $appState.activeModal) { modal in switch modal { case .addressPicker: AddressPickerModalView(appState: $appState, selectedTab: $selectedTab) case .filters: FiltersModalView() } } .onChange(of: appState.address.display) { _, _ in dismissAddressPickerIfAddressExists() } .onChange(of: appState.address.selectedId) { _, _ in dismissAddressPickerIfAddressExists() } .onChange(of: appState.address.latitude) { _, _ in dismissAddressPickerIfAddressExists() } .onChange(of: appState.address.longitude) { _, _ in dismissAddressPickerIfAddressExists() } .task(id: root) { await bootstrapSessionStateIfNeeded() } .onAppear { #if os(iOS) attachSessionExpiredObserverIfNeeded() #endif } .onDisappear { #if os(iOS) detachSessionExpiredObserver() #endif } } private var sessionBootstrapLoadingView: some View { VStack(spacing: 12) { ProgressView() Text("Carregando sua sessão...") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.backgroundLight) } @MainActor private func bootstrapSessionStateIfNeeded() async { guard root == .main else { return } guard isBootstrappingSession == false else { return } isBootstrappingSession = true if let jwt = tokenStore.jwt { appState.session.jwt = jwt appState.session.isAuthenticated = true } if let cachedAddress = SessionStateStore.loadAddress() { appState.address = cachedAddress dismissAddressPickerIfAddressExists() } // Only refresh profile when there is no local address cache. // This avoids forcing Profile flow on startup and still recovers // existing addresses already registered in backend. if hasConfiguredAddress() == false { do { let response = try await ApiService().profile() if response.error == false, let customer = response.result { hydrateAppState(with: customer) } } catch let error as ApiServiceError { if case .sessionExpired = error { forceLogoutToStart() } } catch { // Keep local state when backend refresh fails transiently. } } isBootstrappingSession = false } @MainActor private func hydrateAppState(with customer: CustomerProfile) { appState.profile.id = customer.id appState.profile.name = customer.name appState.profile.email = customer.email appState.profile.phone = customer.phoneNumber ?? "" SessionStateStore.setActiveUserKey( SessionStateStore.makeUserKey(profileId: customer.id, email: customer.email) ) let addresses = customer.addressBook ?? [] guard addresses.isEmpty == false else { return } let preferredAddress = addresses.first(where: { $0.id == appState.address.selectedId }) ?? addresses.first if let preferredAddress { applyAddress(preferredAddress) SessionStateStore.saveAddress(appState.address) } } @MainActor private func applyAddress(_ address: CustomerAddress) { appState.address.selectedId = address.id let cleanLabel = (address.label ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) if cleanLabel.isEmpty == false { appState.address.display = cleanLabel } if let lat = address.latLong?.first, let lng = address.latLong?.dropFirst().first { appState.address.latitude = lat appState.address.longitude = lng } } @MainActor private func forceLogoutToStart() { tokenStore.clear() SessionStateStore.clearActiveUser() isBootstrappingSession = false appState = AppState() selectedTab = .home root = .auth } private func hasConfiguredAddress() -> Bool { if appState.address.selectedId != nil { return true } if appState.address.latitude != nil, appState.address.longitude != nil { return true } let normalized = appState.address.display .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() return normalized.isEmpty == false && normalized != "defina seu endereco" } @MainActor private func dismissAddressPickerIfAddressExists() { guard appState.activeModal == .addressPicker else { return } if hasConfiguredAddress() { appState.activeModal = nil } } #if os(iOS) private func attachSessionExpiredObserverIfNeeded() { guard sessionExpiredObserver == nil else { return } sessionExpiredObserver = NotificationCenter.default.addObserver( forName: .sessionExpired, object: nil, queue: .main ) { _ in Task { @MainActor in forceLogoutToStart() } } } private func detachSessionExpiredObserver() { guard let observer = sessionExpiredObserver else { return } NotificationCenter.default.removeObserver(observer) sessionExpiredObserver = nil } #endif } struct AddressPickerModalView: View { @Binding var appState: AppState @Binding var selectedTab: MainTab var body: some View { NavigationStack { AddressesView( message: appState.address.onboardingMessage, appState: $appState, selectionMode: true ) .onAppear { appState.address.onboardingMessage = nil } } } } struct FiltersModalView: View { var body: some View { NavigationStack { VStack(spacing: 12) { Text("Filtros") .font(AppTypography.heading2) Text("Filtros de busca serao ligados na integracao real da Home com API.") .font(AppTypography.body) .multilineTextAlignment(.center) .foregroundStyle(AppColors.textMuted) } .padding(24) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(AppColors.backgroundLight) } } }