From f2622e5615e0c0a3e48267dff85e261ee190c38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:18:56 +0200 Subject: [PATCH 01/21] Add animation for onboarding welcome screen --- .../Screens/OnboardingWelcomeView.swift | 56 ++++++++++++++----- .../Resources/en.lproj/Localizable.strings | 4 +- .../Shared/Resources/Swiftgen/Strings.swift | 4 +- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift index fd0e41ca9..9bce43639 100644 --- a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift @@ -4,16 +4,39 @@ import SwiftUI struct OnboardingWelcomeView: View { @State private var showLearnMore = false + @State private var showLogo = false + @State private var showButtons = false + @State private var logoScale = 0.9 + @State private var buttonYOffset: CGFloat = 10 var body: some View { VStack(spacing: .zero) { Spacer() - logoBlock - textBlock + Group { + logoBlock + textBlock + } + .opacity(showLogo ? 1 : 0) + .scaleEffect(logoScale) Spacer() continueButton + .opacity(showButtons ? 1 : 0) + .offset(y: buttonYOffset) } .frame(maxWidth: 600) + .onAppear { + withAnimation(.easeInOut(duration: 1.5)) { + showLogo = true + logoScale = 1.0 + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + withAnimation(.easeInOut(duration: 0.5)) { + showButtons = true + buttonYOffset = 0 + } + } + } + } .fullScreenCover(isPresented: $showLearnMore) { SafariWebView(url: URL(string: "http://www.home-assistant.io")!) } @@ -33,25 +56,28 @@ struct OnboardingWelcomeView: View { .multilineTextAlignment(.center) Text(verbatim: L10n.Onboarding.Welcome.description) .foregroundStyle(Color(uiColor: .secondaryLabel)) - Button(L10n.Onboarding.Welcome.learnMore) { - showLearnMore = true - } - .tint(Color.asset(Asset.Colors.haPrimary)) } .padding() } } private var continueButton: some View { - NavigationLink(destination: OnboardingScanningView()) { - Text(verbatim: L10n.continueLabel) - .font(.callout.bold()) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 55) - .background(Color.asset(Asset.Colors.haPrimary)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - }.padding(Spaces.two) + VStack { + NavigationLink(destination: OnboardingScanningView()) { + Text(verbatim: L10n.continueLabel) + .font(.callout.bold()) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 55) + .background(Color.asset(Asset.Colors.haPrimary)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + }.padding(Spaces.two) + Button(L10n.Onboarding.Welcome.getStarted) { + showLearnMore = true + } + .tint(Color.asset(Asset.Colors.haPrimary)) + .frame(minHeight: 40) + } } } diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 855215e0d..81e101a30 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -478,7 +478,7 @@ Tags will work on any device with Home Assistant installed which has hardware su "onboarding.welcome.description" = "This app connects to your Home Assistant server and allows integrating data about you and your phone.\ \ Home Assistant is free and open source home automation software with a focus on local control and privacy."; -"onboarding.welcome.learn_more" = "Learn more"; +"onboarding.welcome.get_started" = "Get started with Home Assistant"; "onboarding.welcome.title" = "Welcome to Home Assistant %@!"; "open_label" = "Open"; "permission.screen.bluetooth.secondary_button" = "Skip"; @@ -1179,4 +1179,4 @@ Home Assistant is free and open source home automation software with a focus on "widgets.sensors.description" = "Display state of sensors"; "widgets.sensors.not_configured" = "No Sensors Configured"; "widgets.sensors.title" = "Sensors"; -"yes_label" = "Yes"; \ No newline at end of file +"yes_label" = "Yes"; diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index fe39dbcdb..a74d17f9b 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -1786,8 +1786,8 @@ public enum L10n { /// /// Home Assistant is free and open source home automation software with a focus on local control and privacy. public static var description: String { return L10n.tr("Localizable", "onboarding.welcome.description") } - /// Learn more - public static var learnMore: String { return L10n.tr("Localizable", "onboarding.welcome.learn_more") } + /// Get started with Home Assistant + public static var getStarted: String { return L10n.tr("Localizable", "onboarding.welcome.get_started") } /// Welcome to Home Assistant %@! public static func title(_ p1: Any) -> String { return L10n.tr("Localizable", "onboarding.welcome.title", String(describing: p1)) From 18eca77a227a4ce66784885591ff3135a63ff11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:19:01 +0200 Subject: [PATCH 02/21] Create base SwiftUI view for servers selection --- HomeAssistant.xcodeproj/project.pbxproj | 44 +++++++++- .../OnboardingScanningInstanceRow.swift | 75 +++++++++++++++++ .../OnboardingScanningViewModel.swift | 36 +++++++++ .../OnboardingServersListView.swift | 81 +++++++++++++++++++ .../ServersScanAnimationView.swift | 38 +++++++++ .../Screens/OnboardingWelcomeView.swift | 15 ++-- .../Resources/en.lproj/Localizable.strings | 2 + Sources/Shared/Environment/AppConstants.swift | 6 ++ .../Shared/Resources/Swiftgen/Strings.swift | 10 +++ 9 files changed, 296 insertions(+), 11 deletions(-) create mode 100644 Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift create mode 100644 Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift create mode 100644 Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift create mode 100644 Sources/App/Onboarding/Screens/OnboardingServersList/ServersScanAnimationView.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index be5c64958..9cfceb87f 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -719,6 +719,10 @@ 427940812B836A1A001D7E14 /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = 42805A142B0226050095414C /* AppIntentVocabulary.plist */; }; 427E92BE2D65E43A0001566B /* WidgetInteractionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427E92BB2D65E3FE0001566B /* WidgetInteractionType.swift */; }; 427E92BF2D65E43A0001566B /* WidgetInteractionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427E92BB2D65E3FE0001566B /* WidgetInteractionType.swift */; }; + 427FEE032D9BE7690047C00C /* OnboardingServersListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE022D9BE7690047C00C /* OnboardingServersListView.swift */; }; + 427FEE072D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */; }; + 427FEE092D9C04050047C00C /* OnboardingScanningViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */; }; + 427FEE0B2D9C05EF0047C00C /* ServersScanAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */; }; 428338442BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 428338452BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 4285C5512D355F9900DADE45 /* WidgetCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5502D355F9900DADE45 /* WidgetCreationView.swift */; }; @@ -2126,6 +2130,10 @@ 42790C452C4808FA00E31B38 /* AppleLikeBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleLikeBottomSheet.swift; sourceTree = ""; }; 4279407E2B8369EA001D7E14 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = bg; path = bg.lproj/AppIntentVocabulary.plist; sourceTree = ""; }; 427E92BB2D65E3FE0001566B /* WidgetInteractionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetInteractionType.swift; sourceTree = ""; }; + 427FEE022D9BE7690047C00C /* OnboardingServersListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingServersListView.swift; sourceTree = ""; }; + 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScanningInstanceRow.swift; sourceTree = ""; }; + 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScanningViewModel.swift; sourceTree = ""; }; + 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersScanAnimationView.swift; sourceTree = ""; }; 42805A132B0226050095414C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/AppIntentVocabulary.plist; sourceTree = ""; }; 4283383F2BA1B17C004798C2 /* AssistRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistRequests.swift; sourceTree = ""; }; 428338422BA1BAFB004798C2 /* Spaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = ""; }; @@ -3096,16 +3104,17 @@ 1168BF35271811C200DD4D15 /* Screens */ = { isa = PBXGroup; children = ( - 429821122CD0DD71005ECD39 /* Bluetooth */, + 426EE49A2CA4194E00A5EF4F /* OnboardingWelcomeView.swift */, + 427FEE052D9C03C50047C00C /* OnboardingServersList */, B661FC87226D478300E541DD /* OnboardingScanningViewController.swift */, 11DA6B4E2713912F008ADFAF /* OnboardingPermissionViewController.swift */, B661FB79226C197900E541DD /* OnboardingManualURLViewController.swift */, 11E99A4F27156854003C8A65 /* OnboardingTerminalViewController.swift */, 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */, - 426EE49A2CA4194E00A5EF4F /* OnboardingWelcomeView.swift */, 42E95C582CA46AD50010ECE3 /* ActivityView.swift */, 42E95C542CA44FC90010ECE3 /* SafariWebView.swift */, 42DF6B2C2CCF8A2200D7EC14 /* PermissionRequestView.swift */, + 429821122CD0DD71005ECD39 /* Bluetooth */, ); path = Screens; sourceTree = ""; @@ -4199,6 +4208,17 @@ path = Views; sourceTree = ""; }; + 427FEE052D9C03C50047C00C /* OnboardingServersList */ = { + isa = PBXGroup; + children = ( + 427FEE022D9BE7690047C00C /* OnboardingServersListView.swift */, + 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */, + 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */, + 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */, + ); + path = OnboardingServersList; + sourceTree = ""; + }; 428338412BA1BAF3004798C2 /* Constants */ = { isa = PBXGroup; children = ( @@ -6716,10 +6736,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Tests-App/Pods-Tests-App-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Tests-App/Pods-Tests-App-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tests-App/Pods-Tests-App-frameworks.sh\"\n"; @@ -6905,10 +6929,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-App/Pods-iOS-App-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-App/Pods-iOS-App-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOS-App/Pods-iOS-App-frameworks.sh\"\n"; @@ -7030,10 +7058,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-watchOS-WatchExtension-Watch/Pods-watchOS-WatchExtension-Watch-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-watchOS-WatchExtension-Watch/Pods-watchOS-WatchExtension-Watch-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-watchOS-WatchExtension-Watch/Pods-watchOS-WatchExtension-Watch-frameworks.sh\"\n"; @@ -7047,10 +7079,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-Shared-iOS-Tests-Shared/Pods-iOS-Shared-iOS-Tests-Shared-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-Shared-iOS-Tests-Shared/Pods-iOS-Shared-iOS-Tests-Shared-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOS-Shared-iOS-Tests-Shared/Pods-iOS-Shared-iOS-Tests-Shared-frameworks.sh\"\n"; @@ -7276,6 +7312,7 @@ 1127383C2625512600F5E312 /* ButtonRowWithLoading.swift in Sources */, 42ABB0BB2C888BB10081461D /* CarPlayConfigurationViewModel.swift in Sources */, 42070EE82BAC43240031E96F /* AssistSession.swift in Sources */, + 427FEE092D9C04050047C00C /* OnboardingScanningViewModel.swift in Sources */, 420FE84E2B556CE500878E06 /* CarPlayEntitiesListViewModel.swift in Sources */, 11195F71267EFE2C003DF674 /* NotificationManagerLocalPushInterfaceUnsupported.swift in Sources */, 428CB3372CF7FC0800F1320E /* WidgetFamilySizes.swift in Sources */, @@ -7374,6 +7411,7 @@ 42EF0AD22D4D1A920088C91E /* UpdateWidgetItemConfirmationStateAppIntent.swift in Sources */, 42F1DA5B2B4BF7DF002729BC /* WindowSizeObserver.swift in Sources */, 429106892BA9D5F700D452F9 /* AssistView+Build.swift in Sources */, + 427FEE032D9BE7690047C00C /* OnboardingServersListView.swift in Sources */, 46CC96822D7136FF00F784CA /* Array+SafeSubscripting.swift in Sources */, 429BEA1A2D102F3A00F070F9 /* ConnectionErrorDetailsView.swift in Sources */, 11EFCDD624F5FA8D00314D85 /* WebViewSceneDelegate.swift in Sources */, @@ -7381,6 +7419,7 @@ 1185DF94271FBA6100ED7D9A /* OnboardingAuthDetails.swift in Sources */, 42BE698D2C4691EA00745ECA /* WebViewAccessoryViews.swift in Sources */, 4273C48B2C8858470065A5B4 /* ControlOpenPageValueProvider.swift in Sources */, + 427FEE072D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift in Sources */, 420FE84B2B556BB100878E06 /* CarPlayActionsTemplate+Build.swift in Sources */, 42DF6B2F2CCF918D00D7EC14 /* BluetoothPermissionView.swift in Sources */, 425573C72B5572AD00145217 /* CarPlayServerListTemplate+Build.swift in Sources */, @@ -7400,6 +7439,7 @@ 420E2AE72C474718004921D8 /* WidgetBasicViewModel.swift in Sources */, 1178C4E524D5CEB200FDEC3E /* ConnectionURLViewController.swift in Sources */, 42F1DA6B2B4ED1BF002729BC /* CarPlayAreasZonesTemplate.swift in Sources */, + 427FEE0B2D9C05EF0047C00C /* ServersScanAnimationView.swift in Sources */, B661FC88226D478300E541DD /* OnboardingScanningViewController.swift in Sources */, 11A48D8124CA8ADB0021BDD9 /* NotificationCategory+Observation.swift in Sources */, 42D0AE452D88259000D9715A /* WidgetDocumentationLink.swift in Sources */, diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift new file mode 100644 index 000000000..1062bfa71 --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift @@ -0,0 +1,75 @@ +import Shared +import SwiftUI + +struct OnboardingScanningInstanceRow: View { + let name: String + let internalURLString: String? + let externalURLString: String? + let internalOrExternalURLString: String + @Binding var isLoading: Bool + + var body: some View { + HStack { + icon + VStack(alignment: .leading) { + Text(name) + .font(.headline) + Text(internalURLString ?? internalOrExternalURLString) + .font(.subheadline) + .foregroundColor(.secondary) + if let externalURLString { + Text(externalURLString) + .font(.caption2) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + if isLoading { + ProgressView() + } + } + .frame(maxWidth: .infinity) + } + + private var icon: some View { + ZStack { + Image(uiImage: Asset.SharedAssets.logo.image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .padding(.trailing, Spaces.one) + if internalURLString == nil, externalURLString != nil { + Image(systemSymbol: .icloudCircleFill) + .foregroundStyle(Color(uiColor: Asset.Colors.haPrimary.color), .white) + .offset(x: 8, y: 12) + .shadow(color: .black.opacity(0.2), radius: 5) + } + } + } +} + +#Preview { + List { + OnboardingScanningInstanceRow( + name: "Home Assistant", + internalURLString: "https://example.com", + externalURLString: "https://example.com", + internalOrExternalURLString: "https://example.com", + isLoading: .constant(false) + ) + OnboardingScanningInstanceRow( + name: "Home Assistant", + internalURLString: "https://example.com", + externalURLString: "https://example.com", + internalOrExternalURLString: "https://example.com", + isLoading: .constant(false) + ) + OnboardingScanningInstanceRow( + name: "Home Assistant", + internalURLString: "https://example.com", + externalURLString: "https://example.com", + internalOrExternalURLString: "https://example.com", + isLoading: .constant(false) + ) + } +} diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift new file mode 100644 index 000000000..c026e6d24 --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift @@ -0,0 +1,36 @@ +import Combine +import Foundation +import Shared + +final class OnboardingScanningViewModel: ObservableObject { + @Published var discoveredInstances: [DiscoveredHomeAssistant] = [] + private let discovery = Bonjour() + private var cancellables = Set() + + init() { + discovery.observer = self + } + + func startDiscovery() { + discoveredInstances = [] + discovery.start() + } + + func stopDiscovery() { + discovery.stop() + } + + func selectInstance(_ instance: DiscoveredHomeAssistant) { + // Handle instance selection + } +} + +extension OnboardingScanningViewModel: BonjourObserver { + func bonjour(_ bonjour: Bonjour, didAdd instance: DiscoveredHomeAssistant) { + discoveredInstances.append(instance) + } + + func bonjour(_ bonjour: Bonjour, didRemoveInstanceWithName name: String) { + discoveredInstances.removeAll { $0.bonjourName == name } + } +} diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift new file mode 100644 index 000000000..f6064e1db --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -0,0 +1,81 @@ +import Combine +import Shared +import SwiftUI + +struct OnboardingServersListView: View { + @ObservedObject private var viewModel = OnboardingScanningViewModel() + @State private var isLoading = false + @State private var showDocumentation = false + + var body: some View { + List { + Section { + ServersScanAnimationView() + .listRowBackground(Color.clear) + .frame(maxWidth: .infinity, alignment: .center) + } + ForEach(viewModel.discoveredInstances, id: \.uuid) { instance in + if #available(iOS 17, *) { + Section { + serverRow(instance: instance) + } + .listSectionSpacing(.compact) + } else { + serverRow(instance: instance) + } + } + } + .animation(.easeInOut, value: viewModel.discoveredInstances.count) + .safeAreaInset(edge: .bottom) { + bottomsButtons + } + .navigationTitle(L10n.Onboarding.Scanning.title) + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + ProgressView() + .progressViewStyle(.circular) + .opacity(isLoading ? 1 : 0) + .animation(.easeInOut, value: isLoading) + } + }) + .onAppear { + viewModel.startDiscovery() + } + .onDisappear { + viewModel.stopDiscovery() + } + .fullScreenCover(isPresented: $showDocumentation) { + SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) + } + } + + private func serverRow(instance: DiscoveredHomeAssistant) -> some View { + OnboardingScanningInstanceRow( + name: instance.locationName, + internalURLString: instance.internalURL?.absoluteString, + externalURLString: instance.externalURL?.absoluteString, + internalOrExternalURLString: instance.internalOrExternalURL.absoluteString, + isLoading: $isLoading + ) + .onTapGesture { + viewModel.selectInstance(instance) + } + } + + private var bottomsButtons: some View { + VStack { + Button(action: {}) { + Text(L10n.Onboarding.Scanning.manual) + } + .buttonStyle(.primaryButton) + Button(action: { + showDocumentation = true + }) { + Text(L10n.Onboarding.Servers.Docs.read) + } + .buttonStyle(.secondaryButton) + } + .padding([.horizontal, .top]) + .background(.regularMaterial) + } +} diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/ServersScanAnimationView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/ServersScanAnimationView.swift new file mode 100644 index 000000000..72df9cdb9 --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/ServersScanAnimationView.swift @@ -0,0 +1,38 @@ +import SFSafeSymbols +import Shared +import SwiftUI + +struct ServersScanAnimationView: View { + @State private var isAnimating = false + var body: some View { + VStack(spacing: Spaces.two) { + if #available(iOS 18, *) { + mainIcon + .symbolEffect(.pulse.byLayer, options: .repeat(.continuous)) + } else { + mainIcon + } + Text(L10n.Onboarding.Servers.Search.message) + .font(.footnote) + .foregroundStyle(.gray) + } + } + + private var mainIcon: some View { + Image(systemSymbol: .textMagnifyingglass) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .foregroundColor(.gray) + .onAppear { + isAnimating = true + } + .onDisappear { + isAnimating = false + } + } +} + +#Preview { + ServersScanAnimationView() +} diff --git a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift index 9bce43639..6f2325d74 100644 --- a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift @@ -38,7 +38,7 @@ struct OnboardingWelcomeView: View { } } .fullScreenCover(isPresented: $showLearnMore) { - SafariWebView(url: URL(string: "http://www.home-assistant.io")!) + SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) } } @@ -63,20 +63,17 @@ struct OnboardingWelcomeView: View { private var continueButton: some View { VStack { - NavigationLink(destination: OnboardingScanningView()) { + NavigationLink(destination: OnboardingServersListView()) { Text(verbatim: L10n.continueLabel) - .font(.callout.bold()) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 55) - .background(Color.asset(Asset.Colors.haPrimary)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - }.padding(Spaces.two) + } + .buttonStyle(.primaryButton) + .padding(.horizontal, Spaces.two) Button(L10n.Onboarding.Welcome.getStarted) { showLearnMore = true } .tint(Color.asset(Asset.Colors.haPrimary)) .frame(minHeight: 40) + .buttonStyle(.secondaryButton) } } } diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 81e101a30..06e56f160 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -480,6 +480,8 @@ Tags will work on any device with Home Assistant installed which has hardware su Home Assistant is free and open source home automation software with a focus on local control and privacy."; "onboarding.welcome.get_started" = "Get started with Home Assistant"; "onboarding.welcome.title" = "Welcome to Home Assistant %@!"; +"onboarding.servers.search.message" = "Looking for servers nearby..."; +"onboarding.servers.docs.read" = "Read documentation"; "open_label" = "Open"; "permission.screen.bluetooth.secondary_button" = "Skip"; "permission.screen.bluetooth.subtitle" = "The Home Assistant app can find devices using Bluetooth of this device. Allow Bluetooth access for the Home Assistant app."; diff --git a/Sources/Shared/Environment/AppConstants.swift b/Sources/Shared/Environment/AppConstants.swift index 5bfb44337..a1cc2b880 100644 --- a/Sources/Shared/Environment/AppConstants.swift +++ b/Sources/Shared/Environment/AppConstants.swift @@ -5,6 +5,12 @@ import Version /// Contains shared constants public enum AppConstants { + public enum WebURLs { + public static var homeAssistant = URL(string: "https://www.home-assistant.io")! + public static var homeAssistantGetStarted = URL(string: "https://www.home-assistant.io/installation/")! + public static var companionAppDocs = URL(string: "https://companion.home-assistant.io")! + } + /// Home Assistant Blue public static var tintColor: UIColor { #if os(iOS) diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index a74d17f9b..b7eb136b1 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -1781,6 +1781,16 @@ public enum L10n { /// Scanning for Servers public static var title: String { return L10n.tr("Localizable", "onboarding.scanning.title") } } + public enum Servers { + public enum Docs { + /// Read documentation + public static var read: String { return L10n.tr("Localizable", "onboarding.servers.docs.read") } + } + public enum Search { + /// Looking for servers nearby... + public static var message: String { return L10n.tr("Localizable", "onboarding.servers.search.message") } + } + } public enum Welcome { /// This app connects to your Home Assistant server and allows integrating data about you and your phone. /// From 56dcec0d33f6193dafafd1e23eaa445943294148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:36:59 +0200 Subject: [PATCH 03/21] Add manual input URL screen in SwiftUI --- HomeAssistant.xcodeproj/project.pbxproj | 12 +++ .../App/Onboarding/API/OnboardingAuth.swift | 12 +++ .../Screens/OnboardingErrorView.swift | 23 ++-- .../ManualURLEntry/ManualURLEntryView.swift | 102 ++++++++++++++++++ .../OnboardingScanningInstanceRow.swift | 8 +- .../OnboardingScanningViewModel.swift | 70 +++++++++++- .../OnboardingServersListView.swift | 49 +++++++-- .../Resources/en.lproj/Localizable.strings | 5 + .../Responses/DiscoveredHomeAssistant.swift | 2 +- .../DesignSystem/Styles/HAButtonStyles.swift | 27 ++++- .../Shared/Resources/Swiftgen/Strings.swift | 16 +++ 11 files changed, 302 insertions(+), 24 deletions(-) create mode 100644 Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 9cfceb87f..28bdd3eac 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -723,6 +723,7 @@ 427FEE072D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */; }; 427FEE092D9C04050047C00C /* OnboardingScanningViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */; }; 427FEE0B2D9C05EF0047C00C /* ServersScanAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */; }; + 427FEE0E2D9C22310047C00C /* ManualURLEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE0D2D9C22310047C00C /* ManualURLEntryView.swift */; }; 428338442BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 428338452BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 4285C5512D355F9900DADE45 /* WidgetCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5502D355F9900DADE45 /* WidgetCreationView.swift */; }; @@ -2134,6 +2135,7 @@ 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScanningInstanceRow.swift; sourceTree = ""; }; 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScanningViewModel.swift; sourceTree = ""; }; 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersScanAnimationView.swift; sourceTree = ""; }; + 427FEE0D2D9C22310047C00C /* ManualURLEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualURLEntryView.swift; sourceTree = ""; }; 42805A132B0226050095414C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/AppIntentVocabulary.plist; sourceTree = ""; }; 4283383F2BA1B17C004798C2 /* AssistRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistRequests.swift; sourceTree = ""; }; 428338422BA1BAFB004798C2 /* Spaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = ""; }; @@ -4211,6 +4213,7 @@ 427FEE052D9C03C50047C00C /* OnboardingServersList */ = { isa = PBXGroup; children = ( + 427FEE0C2D9C221D0047C00C /* ManualURLEntry */, 427FEE022D9BE7690047C00C /* OnboardingServersListView.swift */, 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */, 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */, @@ -4219,6 +4222,14 @@ path = OnboardingServersList; sourceTree = ""; }; + 427FEE0C2D9C221D0047C00C /* ManualURLEntry */ = { + isa = PBXGroup; + children = ( + 427FEE0D2D9C22310047C00C /* ManualURLEntryView.swift */, + ); + path = ManualURLEntry; + sourceTree = ""; + }; 428338412BA1BAF3004798C2 /* Constants */ = { isa = PBXGroup; children = ( @@ -7505,6 +7516,7 @@ 425573CE2B5574F100145217 /* CarPlayAreasViewModel.swift in Sources */, 42BA1BC92C8864C200A2FC36 /* OpenPageAppIntent.swift in Sources */, 42333ADD2D0B1771001E8408 /* EntityRegistryListForDisplay.swift in Sources */, + 427FEE0E2D9C22310047C00C /* ManualURLEntryView.swift in Sources */, B661FB7A226C197900E541DD /* OnboardingManualURLViewController.swift in Sources */, 428D31A52D0B33AF0025B1D7 /* WidgetSensorsConfig.swift in Sources */, 119A827C252A3C4700D7000D /* NFCNDEFPayload+Additions.swift in Sources */, diff --git a/Sources/App/Onboarding/API/OnboardingAuth.swift b/Sources/App/Onboarding/API/OnboardingAuth.swift index 7916c095e..2b01b413b 100644 --- a/Sources/App/Onboarding/API/OnboardingAuth.swift +++ b/Sources/App/Onboarding/API/OnboardingAuth.swift @@ -5,6 +5,18 @@ import PromiseKit import Shared import SwiftUI +struct OnboardinSuccessController: UIViewControllerRepresentable { + let server: Server? + + func makeUIViewController(context: Context) -> some UIViewController { + OnboardingPermissionViewControllerFactory.next(server: server) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + /* no-op */ + } +} + class OnboardingAuth { func successController(server: Server?) -> UIViewController { OnboardingPermissionViewControllerFactory.next(server: server) diff --git a/Sources/App/Onboarding/Screens/OnboardingErrorView.swift b/Sources/App/Onboarding/Screens/OnboardingErrorView.swift index 4528673fe..e0b2c185b 100644 --- a/Sources/App/Onboarding/Screens/OnboardingErrorView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingErrorView.swift @@ -25,6 +25,7 @@ struct OnboardingErrorView: View { Button(L10n.Onboarding.ConnectionError.moreInfoButton) { openURL(documentationURL(for: error)) } + .buttonStyle(.secondaryButton) } } .onAppear { @@ -75,17 +76,21 @@ struct OnboardingErrorView: View { @ViewBuilder private var exportLogsView: some View { - if #available(iOS 16.0, *) { - if let archiveURL = Current.Log.archiveURL() { - ShareLink(item: archiveURL, label: { - Text(Current.Log.exportTitle) - }) - } - } else { - Button(Current.Log.exportTitle) { - showShareSheet = true + Group { + if #available(iOS 16.0, *) { + if let archiveURL = Current.Log.archiveURL() { + ShareLink(item: archiveURL, label: { + Text(Current.Log.exportTitle) + }) + } + } else { + Button(Current.Log.exportTitle) { + showShareSheet = true + } } } + .buttonStyle(.primaryButton) + .padding(.horizontal) } private func documentationURL(for error: Error) -> URL { diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift new file mode 100644 index 000000000..c2229e85e --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift @@ -0,0 +1,102 @@ +import Shared +import SwiftUI + +struct ManualURLEntryView: View { + @Environment(\.dismiss) private var dismiss + @State private var urlString = "" + @FocusState private var focused: Bool? + @State private var showInvalidURLError = false + + let connectAction: (URL) -> Void + + var body: some View { + NavigationView { + List { + Section(L10n.Onboarding.ManualSetup.TextField.title) { + TextField(L10n.Onboarding.ManualSetup.TextField.placeholder, text: $urlString) + .keyboardType(.URL) + .focused($focused, equals: true) + .onAppear { + focused = true + } + } + + httpOrHttpsSection + } + .navigationTitle(L10n.Onboarding.ManualSetup.title) + .navigationViewStyle(.stack) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + CloseButton { + dismiss() + } + } + } + .safeAreaInset(edge: .bottom) { + connectButton + } + .alert(isPresented: $showInvalidURLError) { + Alert( + title: Text(verbatim: L10n.Onboarding.ManualSetup.InputError.title), + message: Text(verbatim: L10n.Onboarding.ManualSetup.InputError.message), + dismissButton: .default(Text(verbatim: L10n.okLabel)) + ) + } + } + } + + @ViewBuilder + private var httpOrHttpsSection: some View { + let cleanedURL = urlString.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) + // 7 is the count of http:// chars + let minCharsToActivateSection = 7 + if !cleanedURL.isEmpty, + !cleanedURL.starts(with: "http://"), + !cleanedURL.starts(with: "https://"), + cleanedURL.count >= minCharsToActivateSection { + Section(L10n.Onboarding.ManualSetup.HelperSection.title) { + HStack { + Button(action: { + urlString = "http://\(urlString)" + }, label: { + Text(verbatim: "http://\(urlString)") + }) + } + Button(action: { + urlString = "https://\(urlString)" + }, label: { + Text(verbatim: "https://\(urlString)") + }) + } + .buttonStyle(.pillButton) + .listRowBackground(Color.clear) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowSeparator(.hidden) + } + } + + private var connectButton: some View { + Button { + if let url = URL(string: urlString.trimmingCharacters(in: .whitespacesAndNewlines)) { + dismiss() + connectAction(url) + } else { + showInvalidURLError = true + } + } label: { + Text(L10n.Onboarding.ManualSetup.connect) + } + .buttonStyle(.primaryButton) + .padding() + .disabled(urlString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } +} + +#Preview { + VStack {} + .sheet(isPresented: .constant(true)) { + ManualURLEntryView { _ in + } + } +} diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift index 1062bfa71..0918d8100 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningInstanceRow.swift @@ -6,7 +6,7 @@ struct OnboardingScanningInstanceRow: View { let internalURLString: String? let externalURLString: String? let internalOrExternalURLString: String - @Binding var isLoading: Bool + let isLoading: Bool var body: some View { HStack { @@ -55,21 +55,21 @@ struct OnboardingScanningInstanceRow: View { internalURLString: "https://example.com", externalURLString: "https://example.com", internalOrExternalURLString: "https://example.com", - isLoading: .constant(false) + isLoading: true ) OnboardingScanningInstanceRow( name: "Home Assistant", internalURLString: "https://example.com", externalURLString: "https://example.com", internalOrExternalURLString: "https://example.com", - isLoading: .constant(false) + isLoading: false ) OnboardingScanningInstanceRow( name: "Home Assistant", internalURLString: "https://example.com", externalURLString: "https://example.com", internalOrExternalURLString: "https://example.com", - isLoading: .constant(false) + isLoading: false ) } } diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift index c026e6d24..9cd969c3b 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift @@ -1,9 +1,21 @@ import Combine import Foundation import Shared +import SwiftUI final class OnboardingScanningViewModel: ObservableObject { + enum Destination { + case error(Error) + case next + } + @Published var discoveredInstances: [DiscoveredHomeAssistant] = [] + @Published var currentlyInstanceLoading: DiscoveredHomeAssistant? + @Published var nextDestination: Destination? + + /// Indicator for manual input loading + @Published var isLoading = false + private let discovery = Bonjour() private var cancellables = Set() @@ -14,6 +26,39 @@ final class OnboardingScanningViewModel: ObservableObject { func startDiscovery() { discoveredInstances = [] discovery.start() + + if Current.appConfiguration == .debug { + for (idx, instance) in [ + DiscoveredHomeAssistant( + manualURL: URL(string: "https://jigsaw.w3.org/HTTP/Basic")!, + name: "Basic Auth" + ), + DiscoveredHomeAssistant( + manualURL: URL(string: "http://httpbin.org/digest-auth/asdf")!, + name: "Digest Auth" + ), + DiscoveredHomeAssistant( + manualURL: URL(string: "https://self-signed.badssl.com/")!, + name: "Self signed SSL" + ), + DiscoveredHomeAssistant( + manualURL: URL(string: "https://client.badssl.com/")!, + name: "Client Cert" + ), + DiscoveredHomeAssistant( + manualURL: URL(string: "https://expired.badssl.com/")!, + name: "Expired" + ), + DiscoveredHomeAssistant( + manualURL: URL(string: "https://httpbin.org/statuses/404")!, + name: "Status Code 404" + ), + ].enumerated() { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500 * (idx + 1))) { [weak self] in + self?.discoveredInstances.append(instance) + } + } + } } func stopDiscovery() { @@ -21,7 +66,30 @@ final class OnboardingScanningViewModel: ObservableObject { } func selectInstance(_ instance: DiscoveredHomeAssistant) { - // Handle instance selection + Current.Log.verbose("Selected instance \(instance)") + + currentlyInstanceLoading = instance + + let authentication = OnboardingAuth() + guard let topViewController = UIApplication.shared.windows.first?.rootViewController else { return } + + authentication.authenticate(to: instance, sender: topViewController).pipe { [weak self] result in + DispatchQueue.main.async { + switch result { + case let .fulfilled(server): + self?.nextDestination = .next // AnyView(OnboardinSuccessController(server: server)) + case let .rejected(error): + self?.nextDestination = .error(error) + } + self?.isLoading = false + } + } + } + + func resetFlow() { + nextDestination = nil + currentlyInstanceLoading = nil + isLoading = false } } diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index f6064e1db..bd3704dcd 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -4,8 +4,8 @@ import SwiftUI struct OnboardingServersListView: View { @ObservedObject private var viewModel = OnboardingScanningViewModel() - @State private var isLoading = false @State private var showDocumentation = false + @State private var showManualInput = false var body: some View { List { @@ -24,18 +24,19 @@ struct OnboardingServersListView: View { serverRow(instance: instance) } } + .disabled(viewModel.currentlyInstanceLoading != nil) } .animation(.easeInOut, value: viewModel.discoveredInstances.count) .safeAreaInset(edge: .bottom) { - bottomsButtons + bottomButtons } .navigationTitle(L10n.Onboarding.Scanning.title) .toolbar(content: { ToolbarItem(placement: .topBarTrailing) { ProgressView() .progressViewStyle(.circular) - .opacity(isLoading ? 1 : 0) - .animation(.easeInOut, value: isLoading) + .opacity(viewModel.isLoading ? 1 : 0) + .animation(.easeInOut, value: viewModel.isLoading) } }) .onAppear { @@ -43,10 +44,40 @@ struct OnboardingServersListView: View { } .onDisappear { viewModel.stopDiscovery() + viewModel.currentlyInstanceLoading = nil } + .sheet(isPresented: $showManualInput, content: { + ManualURLEntryView { connectURL in + viewModel.isLoading = true + viewModel.selectInstance(.init(manualURL: connectURL)) + } + }) .fullScreenCover(isPresented: $showDocumentation) { SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) } + .fullScreenCover(isPresented: .init(get: { + viewModel.nextDestination != nil + }, set: { newValue in + if !newValue { + viewModel.resetFlow() + } + })) { + switch viewModel.nextDestination { + case .next: + EmptyView() + case let .error(error): + VStack { + CloseButton { + viewModel.resetFlow() + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding() + OnboardingErrorView(error: error) + } + case .none: + EmptyView() + } + } } private func serverRow(instance: DiscoveredHomeAssistant) -> some View { @@ -55,16 +86,18 @@ struct OnboardingServersListView: View { internalURLString: instance.internalURL?.absoluteString, externalURLString: instance.externalURL?.absoluteString, internalOrExternalURLString: instance.internalOrExternalURL.absoluteString, - isLoading: $isLoading + isLoading: instance == viewModel.currentlyInstanceLoading ) .onTapGesture { viewModel.selectInstance(instance) } } - private var bottomsButtons: some View { + private var bottomButtons: some View { VStack { - Button(action: {}) { + Button(action: { + showManualInput = true + }) { Text(L10n.Onboarding.Scanning.manual) } .buttonStyle(.primaryButton) @@ -76,6 +109,6 @@ struct OnboardingServersListView: View { .buttonStyle(.secondaryButton) } .padding([.horizontal, .top]) - .background(.regularMaterial) + .background(.ultraThinMaterial) } } diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 06e56f160..70ffdf7f5 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -439,6 +439,11 @@ Tags will work on any device with Home Assistant installed which has hardware su "onboarding.device_name_check.error.rename_action" = "Rename"; "onboarding.device_name_check.error.title" = "A device already exists with the name '%1$@'"; "onboarding.manual_setup.connect" = "Connect"; +"onboarding.manual_setup.input_error.title" = "Invalid URL"; +"onboarding.manual_setup.input_error.message" = "Make sure you have entered a valid URL."; +"onboarding.manual_setup.text_field.title" = "Your Home Assistant URL"; +"onboarding.manual_setup.text_field.placeholder" = "e.g. http://homeassistant.local:8123"; +"onboarding.manual_setup.helper_section.title" = "Did you mean..."; "onboarding.manual_setup.couldnt_make_url.message" = "The value '%@' was not a valid URL."; "onboarding.manual_setup.couldnt_make_url.title" = "Could not create a URL"; "onboarding.manual_setup.description" = "The URL of your Home Assistant server. Make sure it includes the protocol and port."; diff --git a/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift b/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift index 1fe7f8bec..4aaeb4878 100644 --- a/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift +++ b/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift @@ -3,7 +3,7 @@ import ObjectMapper import PromiseKit import Version -public struct DiscoveredHomeAssistant: ImmutableMappable { +public struct DiscoveredHomeAssistant: ImmutableMappable, Equatable { public var uuid: String? public var version: Version public var internalOrExternalURL: URL diff --git a/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift b/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift index 563c8d469..2cfc3209b 100644 --- a/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift +++ b/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift @@ -2,18 +2,23 @@ import Foundation import SwiftUI public struct HAButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled: Bool + public func makeBody(configuration: Configuration) -> some View { configuration.label .font(.callout.bold()) .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: 55) - .background(Color.asset(Asset.Colors.haPrimary)) + .background(isEnabled ? Color.asset(Asset.Colors.haPrimary) : Color.gray) .clipShape(RoundedRectangle(cornerRadius: 12)) + .opacity(isEnabled ? 1 : 0.5) } } public struct HASecondaryButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled: Bool + public func makeBody(configuration: Configuration) -> some View { configuration.label .font(.callout.bold()) @@ -21,6 +26,20 @@ public struct HASecondaryButtonStyle: ButtonStyle { .frame(maxWidth: .infinity) .frame(height: 55) .clipShape(RoundedRectangle(cornerRadius: 12)) + .opacity(isEnabled ? 1 : 0.5) + } +} + +public struct HAPillButtonStyle: ButtonStyle { + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.callout.bold()) + .foregroundColor(.white) + .padding(.vertical, Spaces.one) + .padding(.horizontal, Spaces.oneAndHalf) + .background(Color.asset(Asset.Colors.haPrimary)) + .frame(alignment: .leading) + .clipShape(Capsule()) } } @@ -71,3 +90,9 @@ public extension ButtonStyle where Self == HACriticalButtonStyle { HACriticalButtonStyle() } } + +public extension ButtonStyle where Self == HAPillButtonStyle { + static var pillButton: HAPillButtonStyle { + HAPillButtonStyle() + } +} diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index b7eb136b1..c5bc72e8b 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -1692,12 +1692,28 @@ public enum L10n { /// Could not create a URL public static var title: String { return L10n.tr("Localizable", "onboarding.manual_setup.couldnt_make_url.title") } } + public enum HelperSection { + /// Did you mean... + public static var title: String { return L10n.tr("Localizable", "onboarding.manual_setup.helper_section.title") } + } + public enum InputError { + /// Make sure you have entered a valid URL. + public static var message: String { return L10n.tr("Localizable", "onboarding.manual_setup.input_error.message") } + /// Invalid URL + public static var title: String { return L10n.tr("Localizable", "onboarding.manual_setup.input_error.title") } + } public enum NoScheme { /// Should we try connecting using http:// or https://? public static var message: String { return L10n.tr("Localizable", "onboarding.manual_setup.no_scheme.message") } /// URL entered without scheme public static var title: String { return L10n.tr("Localizable", "onboarding.manual_setup.no_scheme.title") } } + public enum TextField { + /// e.g. http://homeassistant.local:8123 + public static var placeholder: String { return L10n.tr("Localizable", "onboarding.manual_setup.text_field.placeholder") } + /// Your Home Assistant URL + public static var title: String { return L10n.tr("Localizable", "onboarding.manual_setup.text_field.title") } + } } public enum Permissions { /// Allow From 5bf9a2d9238b11924c4466533eae26ac325ec4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:47:34 +0200 Subject: [PATCH 04/21] Improve onboarding error screen --- .../Screens/OnboardingErrorView.swift | 23 +++++++++++-------- Sources/Shared/Environment/AppConstants.swift | 2 ++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Sources/App/Onboarding/Screens/OnboardingErrorView.swift b/Sources/App/Onboarding/Screens/OnboardingErrorView.swift index e0b2c185b..262f1ac7e 100644 --- a/Sources/App/Onboarding/Screens/OnboardingErrorView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingErrorView.swift @@ -23,7 +23,11 @@ struct OnboardingErrorView: View { VStack { exportLogsView Button(L10n.Onboarding.ConnectionError.moreInfoButton) { - openURL(documentationURL(for: error)) + if let url = documentationURL(for: error) { + openURL(url) + } else { + Current.Log.error("Failed to create documentation URL for error: \(error)") + } } .buttonStyle(.secondaryButton) } @@ -65,6 +69,7 @@ struct OnboardingErrorView: View { ForEach(errorComponents, id: \.self) { error in Text(AttributedString(error)) .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) } } .padding() @@ -77,12 +82,10 @@ struct OnboardingErrorView: View { @ViewBuilder private var exportLogsView: some View { Group { - if #available(iOS 16.0, *) { - if let archiveURL = Current.Log.archiveURL() { - ShareLink(item: archiveURL, label: { - Text(Current.Log.exportTitle) - }) - } + if #available(iOS 16.0, *), let archiveURL = Current.Log.archiveURL() { + ShareLink(item: archiveURL, label: { + Text(Current.Log.exportTitle) + }) } else { Button(Current.Log.exportTitle) { showShareSheet = true @@ -93,14 +96,14 @@ struct OnboardingErrorView: View { .padding(.horizontal) } - private func documentationURL(for error: Error) -> URL { - var string = "https://companion.home-assistant.io/docs/troubleshooting/errors" + private func documentationURL(for error: Error) -> URL? { + var string = AppConstants.WebURLs.companionAppDocsTroubleshooting.absoluteString if let error = error as? OnboardingAuthError { string += "#\(error.kind.documentationAnchor)" } - return URL(string: string)! + return URL(string: string) } } diff --git a/Sources/Shared/Environment/AppConstants.swift b/Sources/Shared/Environment/AppConstants.swift index a1cc2b880..c1f602bf5 100644 --- a/Sources/Shared/Environment/AppConstants.swift +++ b/Sources/Shared/Environment/AppConstants.swift @@ -9,6 +9,8 @@ public enum AppConstants { public static var homeAssistant = URL(string: "https://www.home-assistant.io")! public static var homeAssistantGetStarted = URL(string: "https://www.home-assistant.io/installation/")! public static var companionAppDocs = URL(string: "https://companion.home-assistant.io")! + public static var companionAppDocsTroubleshooting = + URL(string: "https://companion.home-assistant.io/docs/troubleshooting/errors")! } /// Home Assistant Blue From 1771384d17c4f4f7d5139b457cf5763b647c561e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 1 Apr 2025 17:15:14 +0200 Subject: [PATCH 05/21] Use same connection error built for webview in onboarding as well --- .../OnboardingNavigationViewController.swift | 12 ---- .../OnboardingServersListView.swift | 16 +++-- .../Resources/en.lproj/Localizable.strings | 2 +- .../WebView/ConnectionErrorDetailsView.swift | 61 +++++++++++++++---- .../Components/CollapsibleView.swift | 7 +++ .../Shared/Resources/Swiftgen/Strings.swift | 6 +- 6 files changed, 66 insertions(+), 38 deletions(-) diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift b/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift index 140165284..bb9e2fe4f 100644 --- a/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift +++ b/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift @@ -96,18 +96,6 @@ class OnboardingNavigationViewController: UINavigationController, RowControllerT super.viewDidLoad() delegate = self - view.tintColor = Asset.Colors.haPrimary.color - navigationController?.navigationBar.tintColor = Asset.Colors.haPrimary.color - - let appearance = with(UINavigationBarAppearance()) { - $0.configureWithOpaqueBackground() - $0.backgroundColor = .systemBackground - $0.shadowColor = .clear - $0.titleTextAttributes = [.foregroundColor: UIColor.label] - } - navigationBar.standardAppearance = appearance - navigationBar.scrollEdgeAppearance = appearance - navigationBar.tintColor = .label } @objc private func cancelTapped(_ sender: UIBarButtonItem) { diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index bd3704dcd..bb40c9cdb 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -55,7 +55,7 @@ struct OnboardingServersListView: View { .fullScreenCover(isPresented: $showDocumentation) { SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) } - .fullScreenCover(isPresented: .init(get: { + .sheet(isPresented: .init(get: { viewModel.nextDestination != nil }, set: { newValue in if !newValue { @@ -66,14 +66,12 @@ struct OnboardingServersListView: View { case .next: EmptyView() case let .error(error): - VStack { - CloseButton { - viewModel.resetFlow() - } - .frame(maxWidth: .infinity, alignment: .trailing) - .padding() - OnboardingErrorView(error: error) - } + ConnectionErrorDetailsView( + server: ServerFixture.standard, + error: error, + showSettingsEntry: false, + expandMoreDetails: true + ) case .none: EmptyView() } diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 70ffdf7f5..1d5d2b86f 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -212,7 +212,7 @@ "connection.permission.internal_url.footer" = "If you still want to use the local URL and don't want to provide location permission, you can tap the button below, but please, be aware of the security risks."; "connection.permission.internal_url.ignore.alert.title" = "Are you sure?"; "connection.permission.internal_url.title" = "Permission access"; -"connection_error.advanced_section.title" = "Advanced"; +"connection_error.more_details_section.title" = "More details"; "connection_error.open_settings.title" = "Open settings"; "continue_label" = "Continue"; "copy_label" = "Copy"; diff --git a/Sources/App/WebView/ConnectionErrorDetailsView.swift b/Sources/App/WebView/ConnectionErrorDetailsView.swift index 897b65e44..1fa79cf22 100644 --- a/Sources/App/WebView/ConnectionErrorDetailsView.swift +++ b/Sources/App/WebView/ConnectionErrorDetailsView.swift @@ -4,9 +4,21 @@ import SwiftUI struct ConnectionErrorDetailsView: View { @Environment(\.dismiss) private var dismiss + @State private var showExportLogsShareSheet: Bool = false + private let feedbackGenerator = UINotificationFeedbackGenerator() - let server: Server + let server: Server? let error: Error + let showSettingsEntry: Bool + let expandMoreDetails: Bool + + init(server: Server?, error: Error, showSettingsEntry: Bool = true, expandMoreDetails: Bool = false) { + self.server = server + self.error = error + self.showSettingsEntry = showSettingsEntry + self.expandMoreDetails = expandMoreDetails + } + var body: some View { NavigationView { ScrollView { @@ -34,7 +46,7 @@ struct ConnectionErrorDetailsView: View { .clipShape(RoundedRectangle(cornerRadius: 12)) } - if server.info.connection.canUseCloud, + if let server, server.info.connection.canUseCloud, let cloudText = try? AttributedString( markdown: L10n.Connection.Error.FailedConnect.Cloud .title @@ -48,9 +60,11 @@ struct ConnectionErrorDetailsView: View { } } } - openSettingsButton - CollapsibleView { - Text(L10n.ConnectionError.AdvancedSection.title) + if showSettingsEntry { + openSettingsButton + } + CollapsibleView(startExpanded: expandMoreDetails) { + Text(L10n.ConnectionError.MoreDetailsSection.title) .font(.body.bold()) } expandedContent: { advancedContent @@ -67,6 +81,7 @@ struct ConnectionErrorDetailsView: View { .frame(height: 1) .padding(Spaces.three) copyToClipboardButton + exportLogsButton documentationLink discordLink githubLink @@ -77,14 +92,16 @@ struct ConnectionErrorDetailsView: View { .ignoresSafeArea(edges: .top) .toolbar { ToolbarItem(placement: .topBarLeading) { - Button(action: { - openSettings() - }, label: { - Image(uiImage: MaterialDesignIcons.cogIcon.image( - ofSize: .init(width: 28, height: 28), - color: .white - )) - }) + if showSettingsEntry { + Button(action: { + openSettings() + }, label: { + Image(uiImage: MaterialDesignIcons.cogIcon.image( + ofSize: .init(width: 28, height: 28), + color: .white + )) + }) + } } ToolbarItem(placement: .topBarTrailing) { CloseButton(tint: .white) { @@ -92,6 +109,9 @@ struct ConnectionErrorDetailsView: View { } } } + .sheet(isPresented: $showExportLogsShareSheet, content: { + ActivityView(activityItems: [Current.Log.archiveURL()]) + }) } .navigationViewStyle(.stack) } @@ -169,6 +189,21 @@ struct ConnectionErrorDetailsView: View { } } + private var exportLogsButton: some View { + ActionLinkButton( + icon: Image(systemSymbol: .squareAndArrowUp), + title: Current.Log.exportTitle, + tint: .init(uiColor: Asset.Colors.haPrimary.color) + ) { + if Current.isCatalyst, let logsURL = Current.Log.archiveURL() { + UIApplication.shared.open(logsURL) + } else { + showExportLogsShareSheet = true + feedbackGenerator.notificationOccurred(.success) + } + } + } + private var openSettingsButton: some View { ActionLinkButton( icon: Image(uiImage: MaterialDesignIcons.cogIcon.image( diff --git a/Sources/Shared/DesignSystem/Components/CollapsibleView.swift b/Sources/Shared/DesignSystem/Components/CollapsibleView.swift index 91058fbde..5c87197c3 100644 --- a/Sources/Shared/DesignSystem/Components/CollapsibleView.swift +++ b/Sources/Shared/DesignSystem/Components/CollapsibleView.swift @@ -5,12 +5,16 @@ public struct CollapsibleView: Vi @ViewBuilder public let collapsedContent: () -> CollapsedContent @ViewBuilder public let expandedContent: () -> ExpandedContent + private let startExpanded: Bool + public init( + startExpanded: Bool = false, @ViewBuilder collapsedContent: @escaping () -> CollapsedContent, @ViewBuilder expandedContent: @escaping () -> ExpandedContent ) { self.collapsedContent = collapsedContent self.expandedContent = expandedContent + self.startExpanded = startExpanded } public var body: some View { @@ -23,6 +27,9 @@ public struct CollapsibleView: Vi } .frame(maxWidth: .infinity) .animation(nil, value: expanded) + .onAppear { + expanded = startExpanded + } .onTapGesture { withAnimation(.easeInOut) { expanded.toggle() diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index c5bc72e8b..6404e7a23 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -864,9 +864,9 @@ public enum L10n { } public enum ConnectionError { - public enum AdvancedSection { - /// Advanced - public static var title: String { return L10n.tr("Localizable", "connection_error.advanced_section.title") } + public enum MoreDetailsSection { + /// More details + public static var title: String { return L10n.tr("Localizable", "connection_error.more_details_section.title") } } public enum OpenSettings { /// Open settings From df14ac99f6222d749d84bb942362434f025e2279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Wed, 2 Apr 2025 10:54:51 +0200 Subject: [PATCH 06/21] Prevent error screen when user just cancels the auth flow --- .../OnboardingScanningViewModel.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift index 9cd969c3b..5e8f7822a 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift @@ -2,6 +2,7 @@ import Combine import Foundation import Shared import SwiftUI +import PromiseKit final class OnboardingScanningViewModel: ObservableObject { enum Destination { @@ -77,8 +78,14 @@ final class OnboardingScanningViewModel: ObservableObject { DispatchQueue.main.async { switch result { case let .fulfilled(server): + Current.Log.verbose("Onboarding authentication succeeded") self?.nextDestination = .next // AnyView(OnboardinSuccessController(server: server)) case let .rejected(error): + if case .cancelled = error as? PMKError { + Current.Log.verbose("Cancelled onboarding authentication (PMKError Cancelled)") + self?.resetFlow() + return + } self?.nextDestination = .error(error) } self?.isLoading = false From b7d0cbea40e21aad38ddfd5f4043dba7c5d966ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:16:57 +0200 Subject: [PATCH 07/21] Redirect back to UIKit flow after server auth --- .../App/Onboarding/API/OnboardingAuth.swift | 11 ++++++- .../OnboardingScanningViewModel.swift | 6 ++-- .../OnboardingServersListView.swift | 31 +++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Sources/App/Onboarding/API/OnboardingAuth.swift b/Sources/App/Onboarding/API/OnboardingAuth.swift index 2b01b413b..4f300f96c 100644 --- a/Sources/App/Onboarding/API/OnboardingAuth.swift +++ b/Sources/App/Onboarding/API/OnboardingAuth.swift @@ -13,7 +13,16 @@ struct OnboardinSuccessController: UIViewControllerRepresentable { } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { - /* no-op */ + if uiViewController is OnboardingTerminalViewController { + Current.onboardingObservation.complete() + + let firstOnboardingNavigationViewController = UIApplication.shared + .keyWindow? + .rootViewController? + .presentedViewController as? UINavigationController + + firstOnboardingNavigationViewController?.dismiss(animated: true, completion: nil) + } } } diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift index 5e8f7822a..b52c5d3c4 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift @@ -1,13 +1,13 @@ import Combine import Foundation +import PromiseKit import Shared import SwiftUI -import PromiseKit final class OnboardingScanningViewModel: ObservableObject { enum Destination { case error(Error) - case next + case next(Server) } @Published var discoveredInstances: [DiscoveredHomeAssistant] = [] @@ -79,7 +79,7 @@ final class OnboardingScanningViewModel: ObservableObject { switch result { case let .fulfilled(server): Current.Log.verbose("Onboarding authentication succeeded") - self?.nextDestination = .next // AnyView(OnboardinSuccessController(server: server)) + self?.nextDestination = .next(server) // AnyView(OnboardinSuccessController(server: server)) case let .rejected(error): if case .cancelled = error as? PMKError { Current.Log.verbose("Cancelled onboarding authentication (PMKError Cancelled)") diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index bb40c9cdb..0daf0d507 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -56,14 +56,19 @@ struct OnboardingServersListView: View { SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) } .sheet(isPresented: .init(get: { - viewModel.nextDestination != nil + if case .error = viewModel.nextDestination { + return true + } else { + return false + } }, set: { newValue in if !newValue { viewModel.resetFlow() } })) { switch viewModel.nextDestination { - case .next: + case .next, .none: + // This scenarios are handlded in full screen EmptyView() case let .error(error): ConnectionErrorDetailsView( @@ -72,7 +77,27 @@ struct OnboardingServersListView: View { showSettingsEntry: false, expandMoreDetails: true ) - case .none: + } + } + .fullScreenCover(isPresented: .init(get: { + if case .next = viewModel.nextDestination { + return true + } else { + return false + } + }, set: { newValue in + if !newValue { + viewModel.resetFlow() + } + })) { + switch viewModel.nextDestination { + case let .next(server): + NavigationView { + AnyView(OnboardinSuccessController(server: server)) + } + .navigationViewStyle(.stack) + case .error, .none: + // This scenarios are handlded in sheet EmptyView() } } From af4436267b92056cb6eec6e0486d832ba9cc6723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:50:04 +0200 Subject: [PATCH 08/21] Get rid of older permission screens and redirect user to home --- HomeAssistant.xcodeproj/project.pbxproj | 36 +-- .../App/Onboarding/API/OnboardingAuth.swift | 25 -- .../Container/OnboardingNavigationView.swift | 63 ++++ .../OnboardingNavigationViewController.swift | 154 +-------- .../OnboardingNavigationViewModel.swift | 23 ++ ...boardingPermissionWorkflowController.swift | 27 +- .../OnboardingManualURLViewController.swift | 301 ------------------ .../OnboardingPermissionViewController.swift | 132 -------- .../OnboardingScanningViewController.swift | 285 ----------------- .../OnboardingServersListView.swift | 146 +++++---- ...t => OnboardingServersListViewModel.swift} | 45 ++- .../OnboardingTerminalViewController.swift | 5 - .../Screens/OnboardingWelcomeView.swift | 14 +- .../App/Settings/SettingsViewController.swift | 12 +- .../App/WebView/WebViewWindowController.swift | 23 +- .../Responses/DiscoveredHomeAssistant.swift | 2 +- .../Shared/Common/Extensions/View+HA.swift | 16 + .../OnboardingStateObservation.swift | 4 +- 18 files changed, 265 insertions(+), 1048 deletions(-) create mode 100644 Sources/App/Onboarding/Container/OnboardingNavigationView.swift create mode 100644 Sources/App/Onboarding/Container/OnboardingNavigationViewModel.swift delete mode 100644 Sources/App/Onboarding/Screens/OnboardingManualURLViewController.swift delete mode 100644 Sources/App/Onboarding/Screens/OnboardingPermissionViewController.swift delete mode 100644 Sources/App/Onboarding/Screens/OnboardingScanningViewController.swift rename Sources/App/Onboarding/Screens/OnboardingServersList/{OnboardingScanningViewModel.swift => OnboardingServersListViewModel.swift} (70%) delete mode 100644 Sources/App/Onboarding/Screens/OnboardingTerminalViewController.swift create mode 100644 Sources/Shared/Common/Extensions/View+HA.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 28bdd3eac..2e2841f00 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -404,7 +404,6 @@ 11D826F124E39F2E005B8A86 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11D826F024E39F2D005B8A86 /* CoreNFC.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 11DA6B4B27137A60008ADFAF /* InputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DA6B4A27137A60008ADFAF /* InputAccessoryView.swift */; }; 11DA6B4D2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DA6B4C2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift */; }; - 11DA6B4F2713912F008ADFAF /* OnboardingPermissionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DA6B4E2713912F008ADFAF /* OnboardingPermissionViewController.swift */; }; 11DC6BAB24E23780002D9FDA /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B63CCDCF2164714900123C50 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; 11DE822E24FAC51100E636B8 /* IncomingURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DE822D24FAC51000E636B8 /* IncomingURLHandler.swift */; }; 11DE823024FAE66F00E636B8 /* UIWindow+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DE822F24FAE66F00E636B8 /* UIWindow+Additions.swift */; }; @@ -415,7 +414,6 @@ 11E1639B250B1B760076D612 /* OnboardingStateObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E16399250B1B760076D612 /* OnboardingStateObservation.swift */; }; 11E5CF8124BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E5CF8024BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift */; }; 11E5CF8224BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E5CF8024BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift */; }; - 11E99A5027156854003C8A65 /* OnboardingTerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E99A4F27156854003C8A65 /* OnboardingTerminalViewController.swift */; }; 11ED43962726599D00B5FD45 /* OnboardingAuthStepModels.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED43952726599D00B5FD45 /* OnboardingAuthStepModels.test.swift */; }; 11ED439827265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED439727265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift */; }; 11ED439A27265DE800B5FD45 /* OnboardingAuthStepRegister.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED439927265DE800B5FD45 /* OnboardingAuthStepRegister.test.swift */; }; @@ -721,9 +719,12 @@ 427E92BF2D65E43A0001566B /* WidgetInteractionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427E92BB2D65E3FE0001566B /* WidgetInteractionType.swift */; }; 427FEE032D9BE7690047C00C /* OnboardingServersListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE022D9BE7690047C00C /* OnboardingServersListView.swift */; }; 427FEE072D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */; }; - 427FEE092D9C04050047C00C /* OnboardingScanningViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */; }; + 427FEE092D9C04050047C00C /* OnboardingServersListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE082D9C04050047C00C /* OnboardingServersListViewModel.swift */; }; 427FEE0B2D9C05EF0047C00C /* ServersScanAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */; }; 427FEE0E2D9C22310047C00C /* ManualURLEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE0D2D9C22310047C00C /* ManualURLEntryView.swift */; }; + 427FEE562D9D39A50047C00C /* OnboardingNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE552D9D39A50047C00C /* OnboardingNavigationView.swift */; }; + 427FEE592D9D48120047C00C /* View+HA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE572D9D48120047C00C /* View+HA.swift */; }; + 427FEE632D9EA1400047C00C /* OnboardingNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE622D9EA1400047C00C /* OnboardingNavigationViewModel.swift */; }; 428338442BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 428338452BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 4285C5512D355F9900DADE45 /* WidgetCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5502D355F9900DADE45 /* WidgetCreationView.swift */; }; @@ -1109,9 +1110,7 @@ B661FB68226B961400E541DD /* WebSocketBridge.js in Resources */ = {isa = PBXBuildFile; fileRef = B661FB67226B961400E541DD /* WebSocketBridge.js */; }; B661FB6A226BBDA900E541DD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B661FB69226BBDA900E541DD /* SettingsViewController.swift */; }; B661FB6F226BCCAD00E541DD /* ConnectionSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B661FB6E226BCCAD00E541DD /* ConnectionSettingsViewController.swift */; }; - B661FB7A226C197900E541DD /* OnboardingManualURLViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B661FB79226C197900E541DD /* OnboardingManualURLViewController.swift */; }; B661FC7E226C87BB00E541DD /* home.json in Resources */ = {isa = PBXBuildFile; fileRef = B661FC7D226C87BB00E541DD /* home.json */; }; - B661FC88226D478300E541DD /* OnboardingScanningViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B661FC87226D478300E541DD /* OnboardingScanningViewController.swift */; }; B66C58A8215086F0004AB261 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66C58A7215086F0004AB261 /* IntentHandler.swift */; }; B66C58AC215086F0004AB261 /* HomeAssistant-Extensions-Intents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B66C58A5215086F0004AB261 /* HomeAssistant-Extensions-Intents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B672333E225DB68B0031D629 /* WebSocketMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B672333D225DB68B0031D629 /* WebSocketMessage.swift */; }; @@ -1853,7 +1852,6 @@ 11D826F024E39F2D005B8A86 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; }; 11DA6B4A27137A60008ADFAF /* InputAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputAccessoryView.swift; sourceTree = ""; }; 11DA6B4C2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPermissionWorkflowController.swift; sourceTree = ""; }; - 11DA6B4E2713912F008ADFAF /* OnboardingPermissionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPermissionViewController.swift; sourceTree = ""; }; 11DE822D24FAC51000E636B8 /* IncomingURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingURLHandler.swift; sourceTree = ""; }; 11DE822F24FAE66F00E636B8 /* UIWindow+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Additions.swift"; sourceTree = ""; }; 11DE9D8325B6103C0081C0ED /* Home Assistant Launcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Home Assistant Launcher.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1863,7 +1861,6 @@ 11DE9F3925B614EB0081C0ED /* Application.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Application.xib; sourceTree = ""; }; 11E16399250B1B760076D612 /* OnboardingStateObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStateObservation.swift; sourceTree = ""; }; 11E5CF8024BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+BackgroundTask.swift"; sourceTree = ""; }; - 11E99A4F27156854003C8A65 /* OnboardingTerminalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTerminalViewController.swift; sourceTree = ""; }; 11ED43952726599D00B5FD45 /* OnboardingAuthStepModels.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepModels.test.swift; sourceTree = ""; }; 11ED439727265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepNotify.test.swift; sourceTree = ""; }; 11ED439927265DE800B5FD45 /* OnboardingAuthStepRegister.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepRegister.test.swift; sourceTree = ""; }; @@ -2133,9 +2130,12 @@ 427E92BB2D65E3FE0001566B /* WidgetInteractionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetInteractionType.swift; sourceTree = ""; }; 427FEE022D9BE7690047C00C /* OnboardingServersListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingServersListView.swift; sourceTree = ""; }; 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScanningInstanceRow.swift; sourceTree = ""; }; - 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScanningViewModel.swift; sourceTree = ""; }; + 427FEE082D9C04050047C00C /* OnboardingServersListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingServersListViewModel.swift; sourceTree = ""; }; 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersScanAnimationView.swift; sourceTree = ""; }; 427FEE0D2D9C22310047C00C /* ManualURLEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualURLEntryView.swift; sourceTree = ""; }; + 427FEE552D9D39A50047C00C /* OnboardingNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationView.swift; sourceTree = ""; }; + 427FEE572D9D48120047C00C /* View+HA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+HA.swift"; sourceTree = ""; }; + 427FEE622D9EA1400047C00C /* OnboardingNavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationViewModel.swift; sourceTree = ""; }; 42805A132B0226050095414C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/AppIntentVocabulary.plist; sourceTree = ""; }; 4283383F2BA1B17C004798C2 /* AssistRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistRequests.swift; sourceTree = ""; }; 428338422BA1BAFB004798C2 /* Spaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = ""; }; @@ -2581,9 +2581,7 @@ B661FB67226B961400E541DD /* WebSocketBridge.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = WebSocketBridge.js; sourceTree = ""; }; B661FB69226BBDA900E541DD /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; B661FB6E226BCCAD00E541DD /* ConnectionSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionSettingsViewController.swift; sourceTree = ""; }; - B661FB79226C197900E541DD /* OnboardingManualURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManualURLViewController.swift; sourceTree = ""; }; B661FC7D226C87BB00E541DD /* home.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = home.json; sourceTree = ""; }; - B661FC87226D478300E541DD /* OnboardingScanningViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScanningViewController.swift; sourceTree = ""; }; B66C58A5215086F0004AB261 /* HomeAssistant-Extensions-Intents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HomeAssistant-Extensions-Intents.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; B66C58A7215086F0004AB261 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; B66C58A9215086F0004AB261 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -3108,10 +3106,6 @@ children = ( 426EE49A2CA4194E00A5EF4F /* OnboardingWelcomeView.swift */, 427FEE052D9C03C50047C00C /* OnboardingServersList */, - B661FC87226D478300E541DD /* OnboardingScanningViewController.swift */, - 11DA6B4E2713912F008ADFAF /* OnboardingPermissionViewController.swift */, - B661FB79226C197900E541DD /* OnboardingManualURLViewController.swift */, - 11E99A4F27156854003C8A65 /* OnboardingTerminalViewController.swift */, 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */, 42E95C582CA46AD50010ECE3 /* ActivityView.swift */, 42E95C542CA44FC90010ECE3 /* SafariWebView.swift */, @@ -3126,6 +3120,8 @@ children = ( B6022222226DBA3800E8DBFE /* OnboardingNavigationViewController.swift */, 11DA6B4C2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift */, + 427FEE552D9D39A50047C00C /* OnboardingNavigationView.swift */, + 427FEE622D9EA1400047C00C /* OnboardingNavigationViewModel.swift */, ); path = Container; sourceTree = ""; @@ -4216,7 +4212,7 @@ 427FEE0C2D9C221D0047C00C /* ManualURLEntry */, 427FEE022D9BE7690047C00C /* OnboardingServersListView.swift */, 427FEE062D9C03DE0047C00C /* OnboardingScanningInstanceRow.swift */, - 427FEE082D9C04050047C00C /* OnboardingScanningViewModel.swift */, + 427FEE082D9C04050047C00C /* OnboardingServersListViewModel.swift */, 427FEE0A2D9C05EF0047C00C /* ServersScanAnimationView.swift */, ); path = OnboardingServersList; @@ -5416,6 +5412,7 @@ D0A6367420DBE93400E5C49B /* Realm+Initialization.swift */, D0DD2CED213BCA8900C3D9F7 /* URL+Extensions.swift */, D0EEF302214D8F0300D1D360 /* String+HA.swift */, + 427FEE572D9D48120047C00C /* View+HA.swift */, 42D996E42D89863A001737A0 /* Bool+HA.swift */, 42E3B8B82D8AC63300F5D084 /* Float+HA.swift */, D0EEF304214DD0D400D1D360 /* UIColor+HA.swift */, @@ -7303,6 +7300,7 @@ B68EDD03215F0E2900DD6B28 /* NotificationCategoryConfigurator.swift in Sources */, 42D5ACCE2C636F2B00D9C4E2 /* WatchConfigurationViewModel.swift in Sources */, 117D8A0824A9347F00580913 /* UIColor+CSSRGB.swift in Sources */, + 427FEE562D9D39A50047C00C /* OnboardingNavigationView.swift in Sources */, 11F3D74C2495377B00C05BBA /* SensorListViewController.swift in Sources */, 42FCCFFA2B9B1C310057783F /* ThreadCredentialsSharingToKeychainViewModel.swift in Sources */, B6617EED1CFE79AD004DEE6D /* NSURL+QueryDictionary.swift in Sources */, @@ -7317,13 +7315,12 @@ 421155212D3525F500A71630 /* AppIconSelectorView.swift in Sources */, 42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */, 11DE823024FAE66F00E636B8 /* UIWindow+Additions.swift in Sources */, - 11DA6B4F2713912F008ADFAF /* OnboardingPermissionViewController.swift in Sources */, 42FCCFFB2B9B1C310057783F /* ThreadTransferCredentialToHAViewModel.swift in Sources */, 4296C36E2B90DB640051B63C /* PerformAction.swift in Sources */, 1127383C2625512600F5E312 /* ButtonRowWithLoading.swift in Sources */, 42ABB0BB2C888BB10081461D /* CarPlayConfigurationViewModel.swift in Sources */, 42070EE82BAC43240031E96F /* AssistSession.swift in Sources */, - 427FEE092D9C04050047C00C /* OnboardingScanningViewModel.swift in Sources */, + 427FEE092D9C04050047C00C /* OnboardingServersListViewModel.swift in Sources */, 420FE84E2B556CE500878E06 /* CarPlayEntitiesListViewModel.swift in Sources */, 11195F71267EFE2C003DF674 /* NotificationManagerLocalPushInterfaceUnsupported.swift in Sources */, 428CB3372CF7FC0800F1320E /* WidgetFamilySizes.swift in Sources */, @@ -7406,7 +7403,6 @@ 4080D5C42C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */, 11A71C7124A4648000D9565F /* ZoneManagerEquatableRegion.swift in Sources */, 42FCD0132B9B29740057783F /* ThreadCredentialsManagementViewModel.swift in Sources */, - 11E99A5027156854003C8A65 /* OnboardingTerminalViewController.swift in Sources */, 422F951F2CFDF7C5003B7514 /* HAApplicationShortcutItem.swift in Sources */, 1101568424D770B2009424C9 /* NFCWriter.swift in Sources */, 42B74A662D37C2C000C37304 /* WidgetCreationViewModel.swift in Sources */, @@ -7451,7 +7447,6 @@ 1178C4E524D5CEB200FDEC3E /* ConnectionURLViewController.swift in Sources */, 42F1DA6B2B4ED1BF002729BC /* CarPlayAreasZonesTemplate.swift in Sources */, 427FEE0B2D9C05EF0047C00C /* ServersScanAnimationView.swift in Sources */, - B661FC88226D478300E541DD /* OnboardingScanningViewController.swift in Sources */, 11A48D8124CA8ADB0021BDD9 /* NotificationCategory+Observation.swift in Sources */, 42D0AE452D88259000D9715A /* WidgetDocumentationLink.swift in Sources */, 1100D51D2496AECE00B1073C /* PermissionStatusRow.swift in Sources */, @@ -7504,6 +7499,7 @@ 429821142CD0DD85005ECD39 /* BluetoothPermissionViewModel.swift in Sources */, 4251AA992C6B9D4C004CCC9D /* MagicItemCustomizationView.swift in Sources */, 1185DFB2271FF53800ED7D9A /* OnboardingAuthStepDuplicate.swift in Sources */, + 427FEE632D9EA1400047C00C /* OnboardingNavigationViewModel.swift in Sources */, 42F1DA5D2B4BF85F002729BC /* WindowScenesManager.swift in Sources */, 11EFCDDA24F5FE0600314D85 /* SceneActivity.swift in Sources */, 11DA6B4D2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift in Sources */, @@ -7517,7 +7513,6 @@ 42BA1BC92C8864C200A2FC36 /* OpenPageAppIntent.swift in Sources */, 42333ADD2D0B1771001E8408 /* EntityRegistryListForDisplay.swift in Sources */, 427FEE0E2D9C22310047C00C /* ManualURLEntryView.swift in Sources */, - B661FB7A226C197900E541DD /* OnboardingManualURLViewController.swift in Sources */, 428D31A52D0B33AF0025B1D7 /* WidgetSensorsConfig.swift in Sources */, 119A827C252A3C4700D7000D /* NFCNDEFPayload+Additions.swift in Sources */, 42AC94A52CF872520050A62C /* TileCardStyleModifier.swift in Sources */, @@ -7942,6 +7937,7 @@ D0BE440E210437F900C74314 /* AuthenticationRoutes.swift in Sources */, 11E5CF8124BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift in Sources */, 42FCCFA82B9A05400057783F /* View+RoundedCorner.swift in Sources */, + 427FEE592D9D48120047C00C /* View+HA.swift in Sources */, 1164D9DE25FB1B9800515E8A /* UIBarButtonItem+Additions.swift in Sources */, 11B38EEA275C54A200205C7B /* PickAServerError.swift in Sources */, B6B74CBD228399AB00D58A68 /* Action.swift in Sources */, diff --git a/Sources/App/Onboarding/API/OnboardingAuth.swift b/Sources/App/Onboarding/API/OnboardingAuth.swift index 4f300f96c..dcb815c55 100644 --- a/Sources/App/Onboarding/API/OnboardingAuth.swift +++ b/Sources/App/Onboarding/API/OnboardingAuth.swift @@ -5,32 +5,7 @@ import PromiseKit import Shared import SwiftUI -struct OnboardinSuccessController: UIViewControllerRepresentable { - let server: Server? - - func makeUIViewController(context: Context) -> some UIViewController { - OnboardingPermissionViewControllerFactory.next(server: server) - } - - func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { - if uiViewController is OnboardingTerminalViewController { - Current.onboardingObservation.complete() - - let firstOnboardingNavigationViewController = UIApplication.shared - .keyWindow? - .rootViewController? - .presentedViewController as? UINavigationController - - firstOnboardingNavigationViewController?.dismiss(animated: true, completion: nil) - } - } -} - class OnboardingAuth { - func successController(server: Server?) -> UIViewController { - OnboardingPermissionViewControllerFactory.next(server: server) - } - func failureController(error: Error) -> UIViewController { UIHostingController(rootView: OnboardingErrorView(error: error)) } diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift new file mode 100644 index 000000000..08786fb44 --- /dev/null +++ b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift @@ -0,0 +1,63 @@ +import Shared +import SwiftUI + +struct OnboardingNavigationView: View { + static func controller(onboardingStyle: OnboardingStyle) -> UIViewController { + OnboardingNavigationView(onboardingStyle: onboardingStyle).embeddedInHostingController() + } + + @Environment(\.dismiss) private var dismiss + @StateObject public var viewModel = OnboardingNavigationViewModel() + public let onboardingStyle: OnboardingStyle + + init(onboardingStyle: OnboardingStyle) { + self.onboardingStyle = onboardingStyle + } + + var body: some View { + NavigationView { + Group { + switch onboardingStyle { + case .initial: + OnboardingWelcomeView(shouldDismissOnboarding: $viewModel.shouldDismiss) + case .secondary: + OnboardingServersListView(shouldDismissOnboarding: $viewModel.shouldDismiss) + case let .required(type): + switch type { + case .full: + OnboardingWelcomeView(shouldDismissOnboarding: $viewModel.shouldDismiss) + case .permissions: + Text("Unmapped flow (3)") + } + } + } + .navigationViewStyle(.stack) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if onboardingStyle.insertsCancelButton { + Button(action: { + closeOnboarding() + }) { + Text(L10n.cancelLabel) + } + } + } + } + } + .onChange(of: viewModel.shouldDismiss) { newValue in + if newValue { + closeOnboarding() + } + } + } + + private func closeOnboarding() { + if onboardingStyle == .secondary { + dismiss() + } else { + Current.sceneManager.webViewWindowControllerPromise.done { windowController in + windowController.setup() + } + } + } +} diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift b/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift index bb9e2fe4f..cda67c8ae 100644 --- a/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift +++ b/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift @@ -1,155 +1,31 @@ -import Eureka import Shared -import SwiftUI -import UIKit -enum OnboardingBarAppearance { - case normal - case hidden -} - -protocol OnboardingViewController { - var preferredBarAppearance: OnboardingBarAppearance { get } -} - -class OnboardingNavigationViewController: UINavigationController, RowControllerType { - enum OnboardingStyle { - enum RequiredType { - case full - case permissions - } - - case initial - case required(RequiredType) - case secondary +enum OnboardingStyle: Equatable { + enum RequiredType: Equatable { + case full + case permissions + } - var insertsCancelButton: Bool { - switch self { - case .initial, .required: return false - case .secondary: return true - } - } + case initial + case required(RequiredType) + case secondary - var modalPresentationStyle: UIModalPresentationStyle { - switch self { - case .initial, .required: return .fullScreen - case .secondary: - return .automatic - } + var insertsCancelButton: Bool { + switch self { + case .initial, .required: return false + case .secondary: return true } } +} +enum OnboardingNavigationViewController { public static var requiredOnboardingStyle: OnboardingStyle? { if Current.servers.all.isEmpty { return .required(.full) - } else if OnboardingPermissionViewControllerFactory.hasControllers { + } else if !OnboardingPermissionHandler.notDeterminedPermissions.isEmpty { return .required(.permissions) } else { return nil } } - - public let onboardingStyle: OnboardingStyle - public var onDismissCallback: ((UIViewController) -> Void)? - - public init(onboardingStyle: OnboardingStyle) { - self.onboardingStyle = onboardingStyle - - let rootViewController: UIViewController - - let onboardingWelcomeViewController = UIHostingController(rootView: OnboardingWelcomeView()) - - switch onboardingStyle { - case .initial: rootViewController = onboardingWelcomeViewController - case .secondary: rootViewController = OnboardingScanningViewController() - case let .required(type): - switch type { - case .full: - rootViewController = onboardingWelcomeViewController - case .permissions: - rootViewController = OnboardingPermissionViewControllerFactory.next(server: nil) - } - } - - super.init(rootViewController: rootViewController) - modalPresentationStyle = onboardingStyle.modalPresentationStyle - - if onboardingStyle.insertsCancelButton { - var leftItems = rootViewController.navigationItem.leftBarButtonItems ?? [] - leftItems.append( - UIBarButtonItem( - barButtonSystemItem: .cancel, - target: self, - action: #selector(cancelTapped(_:)) - ) - ) - rootViewController.navigationItem.leftBarButtonItems = leftItems - } - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - delegate = self - } - - @objc private func cancelTapped(_ sender: UIBarButtonItem) { - dismiss() - } - - func dismiss() { - dismiss(animated: true, completion: nil) - onDismissCallback?(self) - } - - override func show(_ vc: UIViewController, sender: Any?) { - if vc is OnboardingTerminalViewController { - Current.onboardingObservation.complete() - dismiss() - } else if sender as? UIViewController == topViewController { - super.show(vc, sender: sender) - } else { - Current.Log.error("unknown sender \(String(describing: sender)) wanted us to present: \(vc)") - } - } -} - -extension OnboardingNavigationViewController: UINavigationControllerDelegate { - private func updateNavigationBar(for controller: UIViewController?, animated: Bool) { - let appearance: OnboardingBarAppearance - - if let controller = controller as? OnboardingViewController { - appearance = controller.preferredBarAppearance - } else { - appearance = .normal - } - - switch appearance { - case .normal: - setNavigationBarHidden(false, animated: animated) - case .hidden: - setNavigationBarHidden(true, animated: animated) - } - } - - func navigationController( - _ navigationController: UINavigationController, - willShow viewController: UIViewController, - animated: Bool - ) { - updateNavigationBar(for: viewController, animated: animated) - - transitionCoordinator?.animate(alongsideTransition: { _ in - // putting the navigation bar change here causes the bar to animate in/out - }, completion: { [weak self] context in - if context.isCancelled { - self?.updateNavigationBar(for: self?.topViewController, animated: animated) - } - }) - } } diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationViewModel.swift b/Sources/App/Onboarding/Container/OnboardingNavigationViewModel.swift new file mode 100644 index 000000000..abd82edb1 --- /dev/null +++ b/Sources/App/Onboarding/Container/OnboardingNavigationViewModel.swift @@ -0,0 +1,23 @@ +import Foundation +import Shared + +final class OnboardingNavigationViewModel: ObservableObject { + @Published var shouldDismiss: Bool = false + + init() { + Current.onboardingObservation.register(observer: self) + } + + deinit { + Current.onboardingObservation.unregister(observer: self) + } +} + +extension OnboardingNavigationViewModel: OnboardingStateObserver { + func onboardingStateDidChange(to state: OnboardingState) { + guard state == .complete else { return } + DispatchQueue.main.async { [weak self] in + self?.shouldDismiss = true + } + } +} diff --git a/Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift b/Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift index a80736bac..dca49ebc5 100644 --- a/Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift +++ b/Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift @@ -1,33 +1,12 @@ import Shared import UIKit -class OnboardingPermissionViewControllerFactory { - static var hasControllers: Bool { - !permissions.isEmpty - } - - static func next(server: Server?) -> UIViewController { - if let permission = permissions.first { - return OnboardingPermissionViewController(server: server, permission: permission, factory: self) - } else { - return OnboardingTerminalViewController() - } - } - - private static var permissions: [PermissionType] { +enum OnboardingPermissionHandler { + static var notDeterminedPermissions: [PermissionType] { var permissions: [PermissionType] = [ - .notification, - .location, + // .location, ] - if Current.motion.isActivityAvailable() { - permissions.append(.motion) - } - - if Current.focusStatus.isAvailable() { - permissions.append(.focus) - } - return permissions.filter { $0.status == .notDetermined } } } diff --git a/Sources/App/Onboarding/Screens/OnboardingManualURLViewController.swift b/Sources/App/Onboarding/Screens/OnboardingManualURLViewController.swift deleted file mode 100644 index f8f3e321b..000000000 --- a/Sources/App/Onboarding/Screens/OnboardingManualURLViewController.swift +++ /dev/null @@ -1,301 +0,0 @@ -import PromiseKit -import Shared -import UIKit - -class OnboardingManualURLViewController: UIViewController, UITextFieldDelegate { - private let urlField = UITextField() - private var connectButton: UIButton? - private var connectLoading: UIActivityIndicatorView? - private var scrollView: UIScrollView? - private var bottomSpacer: UIView? - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - urlField.becomeFirstResponder() - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - - let (scrollView, stackView, equalSpacers) = UIView.contentStackView(in: view, scrolling: true) - self.scrollView = scrollView - - stackView.addArrangedSubview(with(UILabel()) { - $0.text = L10n.Onboarding.ManualSetup.title - Current.style.onboardingTitle($0) - }) - - stackView.addArrangedSubview(with(UILabel()) { - $0.text = L10n.Onboarding.ManualSetup.description - $0.font = .preferredFont(forTextStyle: .body) - $0.textColor = Asset.Colors.haPrimary.color - $0.textAlignment = .natural - $0.numberOfLines = 0 - }) - - stackView.addArrangedSubview(with(urlField) { - $0.delegate = self - $0.backgroundColor = UIColor(white: 0, alpha: 0.12) - $0.borderStyle = .roundedRect - $0.placeholder = "http://homeassistant.local:8123" - $0.textContentType = .URL - $0.keyboardType = .URL - $0.autocapitalizationType = .none - $0.autocorrectionType = .no - $0.spellCheckingType = .no - $0.smartDashesType = .no - $0.smartQuotesType = .no - $0.keyboardAppearance = .dark - $0.returnKeyType = .continue - $0.enablesReturnKeyAutomatically = true - $0.clearButtonMode = .whileEditing - - let font = UIFont.preferredFont(forTextStyle: .body) - $0.font = font - $0.heightAnchor.constraint(greaterThanOrEqualToConstant: font.lineHeight * 2.5) - .isActive = true - - NotificationCenter.default.addObserver( - self, - selector: #selector(updateConnectButton), - name: UITextField.textDidChangeNotification, - object: $0 - ) - }) - - switch traitCollection.userInterfaceIdiom { - case .pad, .mac: - urlField.widthAnchor.constraint(equalTo: stackView.readableContentGuide.widthAnchor) - .isActive = true - default: - urlField.widthAnchor.constraint(equalTo: stackView.layoutMarginsGuide.widthAnchor) - .isActive = true - } - - let button = with(UIButton(type: .custom)) { - $0.setTitle(L10n.Onboarding.ManualSetup.connect, for: .normal) - $0.addTarget(self, action: #selector(connectTapped(_:)), for: .touchUpInside) - Current.style.onboardingButtonPrimary($0) - - $0.translatesAutoresizingMaskIntoConstraints = false - $0.setContentCompressionResistancePriority(.required, for: .vertical) - } - let loading: UIActivityIndicatorView = { - let indicator: UIActivityIndicatorView - indicator = UIActivityIndicatorView(style: .medium) - - indicator.hidesWhenStopped = true - indicator.color = button.titleColor(for: .normal) - return indicator - }() - - connectButton = button - connectLoading = loading - - with(button) { - $0.addSubview(loading) - loading.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - loading.centerYAnchor.constraint(equalTo: $0.centerYAnchor), - loading.trailingAnchor.constraint(equalTo: $0.trailingAnchor, constant: -16), - ]) - } - - if Current.isCatalyst { - // iPad and iPhone unconditionally show the input view, but mac never does - stackView.addArrangedSubview(button) - } else { - urlField.inputAccessoryView = with(InputAccessoryView()) { - $0.directionalLayoutMargins = stackView.directionalLayoutMargins - $0.contentView = button - } - } - - stackView.addArrangedSubview(with(equalSpacers.next()) { - bottomSpacer = $0 - }) - - updateConnectButton() - - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardWillChangeFrame(_:)), - name: UIResponder.keyboardWillChangeFrameNotification, - object: nil - ) - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - connect() - return false - } - - func textField( - _ textField: UITextField, - shouldChangeCharactersIn range: NSRange, - replacementString string: String - ) -> Bool { - !isConnecting - } - - @objc private func connectTapped(_ sender: UIButton) { - Current.Log.verbose("Connect button tapped") - connect() - } - - @objc private func updateConnectButton() { - connectButton?.isEnabled = urlField.text?.isEmpty == false - } - - private var isConnecting: Bool = false { - didSet { - if isConnecting { - connectLoading?.startAnimating() - connectButton?.isUserInteractionEnabled = false - } else { - connectLoading?.stopAnimating() - connectButton?.isUserInteractionEnabled = true - } - } - } - - private func connect() { - guard !isConnecting else { return } - - isConnecting = true - - if let urlString = urlField.text { - urlField.text = removeTrailingSlash(from: urlString) - } - - let authentication = OnboardingAuth() - - firstly { - validatedURL(from: urlField.text) - }.recover { [self] error -> Promise in - Current.Log.error("Couldn't make a URL: \(error)") - - let alert = UIAlertController( - title: L10n.Onboarding.ManualSetup.CouldntMakeUrl.title, - message: L10n.Onboarding.ManualSetup.CouldntMakeUrl.message(urlField.text ?? ""), - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: L10n.okLabel, style: UIAlertAction.Style.default, handler: nil)) - present(alert, animated: true, completion: nil) - - return .init(error: PMKError.cancelled) - }.then { [self] (url: URL) -> Promise in - Current.Log.verbose("Onboard URL is \(url)") - // Removing path such as "/lovelace/home" to avoid wrong redirect after login - let url = URL(string: url.absoluteString.replacingOccurrences(of: url.path, with: "")) ?? url - Current.Log.verbose("Onboard URL after cleanup is \(url)") - - let instance = DiscoveredHomeAssistant(manualURL: url) - return authentication.authenticate(to: instance, sender: self) - }.ensure { [self] in - isConnecting = false - }.done { [self] server in - show(authentication.successController(server: server), sender: self) - }.catch { [self] error in - show(authentication.failureController(error: error), sender: self) - } - } - - enum ValidateError: Error, CancellableError { - case emptyString - case cannotConvert - case invalidScheme - case noSchemeCancelled - - var isCancelled: Bool { - switch self { - case .emptyString, .cannotConvert, .invalidScheme: - return false - case .noSchemeCancelled: - return true - } - } - } - - private func removeTrailingSlash(from string: String) -> String { - if string.hasSuffix("/") { - return String(string.dropLast()) - } else { - return string - } - } - - private func promptForScheme(for string: String) -> Promise { - Promise { seal in - let alert = UIAlertController( - title: L10n.Onboarding.ManualSetup.NoScheme.title, - message: L10n.Onboarding.ManualSetup.NoScheme.message, - preferredStyle: .actionSheet - ) - - with(alert.popoverPresentationController) { - $0?.sourceView = urlField - $0?.sourceRect = urlField.bounds - } - - func action(for scheme: String) -> UIAlertAction { - UIAlertAction(title: scheme, style: .default, handler: { _ in - seal.fulfill(scheme + string) - }) - } - - alert.addAction(action(for: "http://")) - alert.addAction(action(for: "https://")) - alert.addAction(UIAlertAction(title: L10n.cancelLabel, style: .cancel, handler: { _ in - seal.reject(ValidateError.noSchemeCancelled) - })) - self.present(alert, animated: true, completion: nil) - } - } - - private func validatedURL(from inputString: String?) -> Promise { - let start = Promise.value(inputString) - - return start - .map { (string: String?) -> String in - if let trimmed = string?.trimmingCharacters(in: .whitespacesAndNewlines), trimmed.isEmpty == false { - return trimmed - } else { - throw ValidateError.emptyString - } - }.then { (string: String) -> Promise in - if string.starts(with: "http://") || string.starts(with: "https://") { - return .value(string) - } else if string.contains("://") == false { - return self.promptForScheme(for: string) - } else { - throw ValidateError.invalidScheme - } - }.map { (string: String) -> URL in - if let url = URL(string: string) { - return url - } else { - throw ValidateError.cannotConvert - } - } - } - - @objc private func keyboardWillChangeFrame(_ note: Notification) { - guard let scrollView, - let frameValue = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { - return - } - - UIView.performWithoutAnimation { - view.layoutIfNeeded() - } - - let intersectHeight = view.convert(frameValue.cgRectValue, from: nil).intersection(scrollView.frame).height - let insetHeight = max(0, intersectHeight - (bottomSpacer?.bounds.height ?? 0)) - - scrollView.contentInset.bottom = insetHeight - scrollView.verticalScrollIndicatorInsets.bottom = insetHeight - } -} diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissionViewController.swift b/Sources/App/Onboarding/Screens/OnboardingPermissionViewController.swift deleted file mode 100644 index 3d7aecac1..000000000 --- a/Sources/App/Onboarding/Screens/OnboardingPermissionViewController.swift +++ /dev/null @@ -1,132 +0,0 @@ -import Shared -import UIKit - -class OnboardingPermissionViewController: UIViewController, OnboardingViewController { - let server: Server? - let permission: PermissionType - let factory: OnboardingPermissionViewControllerFactory.Type - - init(server: Server?, permission: PermissionType, factory: OnboardingPermissionViewControllerFactory.Type) { - self.server = server - self.permission = permission - self.factory = factory - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - var preferredBarAppearance: OnboardingBarAppearance { .hidden } - - private var headerImageView: UIImageView? - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - navigationItem.hidesBackButton = true - - let (_, stackView, equalSpacers) = UIView.contentStackView(in: view, scrolling: true) - - stackView.addArrangedSubview(with(UIImageView()) { - headerImageView = $0 - $0.image = permission.enableIcon.image(ofSize: CGSize(width: 128, height: 128), color: .black) - .withRenderingMode(.alwaysTemplate) - }) - - stackView.addArrangedSubview(with(UILabel()) { - $0.text = permission.title - Current.style.onboardingTitle($0) - }) - - let descriptionLabel = with(UILabel()) { - $0.text = permission.enableDescription - $0.font = .preferredFont(forTextStyle: .body) - $0.textColor = .secondaryLabel - $0.numberOfLines = 0 - $0.textAlignment = .center - } - stackView.addArrangedSubview(descriptionLabel) - stackView.setCustomSpacing(stackView.spacing * 2.0, after: descriptionLabel) - - for bulletPoint in permission.enableBulletPoints { - let view = with(UIStackView()) { - $0.axis = .horizontal - $0.alignment = .center - $0.spacing = 16.0 - $0.directionalLayoutMargins = .init(top: 0, leading: 16, bottom: 0, trailing: 16) - $0.isLayoutMarginsRelativeArrangement = true - - $0.addArrangedSubview(with(UIImageView()) { - $0.image = bulletPoint.0.image(ofSize: CGSize(width: 34, height: 34), color: .black) - .withRenderingMode(.alwaysTemplate) - $0.setContentCompressionResistancePriority(.required, for: .horizontal) - $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.setContentHuggingPriority(.required, for: .horizontal) - $0.setContentHuggingPriority(.required, for: .vertical) - }) - $0.addArrangedSubview(with(UILabel()) { - $0.text = bulletPoint.1 - $0.textColor = .label - $0.font = .boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize) - $0.numberOfLines = 0 - $0.setContentHuggingPriority(.defaultLow, for: .horizontal) - }) - } - - stackView.addArrangedSubview(view) - - switch traitCollection.userInterfaceIdiom { - case .pad, .mac: - view.widthAnchor.constraint(equalTo: stackView.readableContentGuide.widthAnchor) - .isActive = true - default: - view.widthAnchor.constraint(equalTo: stackView.layoutMarginsGuide.widthAnchor) - .isActive = true - } - } - - stackView.addArrangedSubview(equalSpacers.next()) - - stackView.addArrangedSubview(with(UIButton(type: .custom)) { - $0.setTitle(L10n.continueLabel, for: .normal) - $0.addTarget(self, action: #selector(continueTapped(_:)), for: .touchUpInside) - Current.style.onboardingButtonPrimary($0) - }) - - stackView.addArrangedSubview(with(UILabel()) { - $0.font = .preferredFont(forTextStyle: .footnote) - $0.textColor = Asset.Colors.haPrimary.color - $0.text = L10n.Onboarding.Permissions.changeLaterNote - $0.numberOfLines = 0 - $0.textAlignment = .center - }) - - updateHiddenStates() - } - - @objc private func continueTapped(_ sender: UIButton) { - sender.isUserInteractionEnabled = false - permission.request { [self] granted, _ in - if permission == .location, granted, let currentSSID = Current.connectivity.currentWiFiSSID() { - // update SSIDs if we have access to them, since we're gonna need it later - server?.info.connection.internalSSIDs = [currentSSID] - } - - sender.isUserInteractionEnabled = true - show(factory.next(server: server), sender: self) - } - } - - private func updateHiddenStates() { - let imageViewHidden = traitCollection.verticalSizeClass == .compact - headerImageView?.isHidden = imageViewHidden - } - - override func traitCollectionDidChange(_ traitCollection: UITraitCollection?) { - super.traitCollectionDidChange(traitCollection) - updateHiddenStates() - } -} diff --git a/Sources/App/Onboarding/Screens/OnboardingScanningViewController.swift b/Sources/App/Onboarding/Screens/OnboardingScanningViewController.swift deleted file mode 100644 index 9c7668039..000000000 --- a/Sources/App/Onboarding/Screens/OnboardingScanningViewController.swift +++ /dev/null @@ -1,285 +0,0 @@ -import PromiseKit -import Shared -import UIKit - -class OnboardingScanningInstanceCell: UITableViewCell { - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) - with(textLabel) { - $0?.textColor = .label - $0?.numberOfLines = 0 - $0?.font = UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize) - } - with(detailTextLabel) { - $0?.textColor = .secondaryLabel - $0?.numberOfLines = 0 - $0?.font = .preferredFont(forTextStyle: .body) - } - backgroundView = with(UIView()) { - $0.backgroundColor = .systemBackground - } - selectedBackgroundView = with(UIView()) { - $0.backgroundColor = .secondarySystemBackground - $0.layer.cornerRadius = 4.0 - } - backgroundColor = .clear - accessoryType = .disclosureIndicator - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public var isLoading: Bool = false { - didSet { - if isLoading { - accessoryType = .none - let activityIndicator: UIActivityIndicatorView = .init(style: .medium) - accessoryView = activityIndicator - activityIndicator.startAnimating() - } else { - accessoryView = nil - accessoryType = .disclosureIndicator - } - } - } -} - -class OnboardingScanningViewController: UIViewController { - private let discovery = Bonjour() - private var discoveredInstances: [DiscoveredHomeAssistant] = [] - - private var tableView: UITableView? - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - tableView?.indexPathsForSelectedRows?.forEach { indexPath in - tableView?.deselectRow(at: indexPath, animated: animated) - } - - discovery.start() - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - discovery.stop() - } - - override func viewDidLoad() { - super.viewDidLoad() - - let activityIndicator: UIActivityIndicatorView - - activityIndicator = UIActivityIndicatorView(style: .medium) - - navigationItem.rightBarButtonItems = [ - UIBarButtonItem(customView: activityIndicator), - ] - - activityIndicator.startAnimating() - - let (_, stackView, _) = UIView.contentStackView(in: view, scrolling: false) - - view.backgroundColor = .systemBackground - title = L10n.Onboarding.Scanning.title - - stackView.addArrangedSubview(with(UILabel()) { - $0.text = L10n.Onboarding.Scanning.title - Current.style.onboardingTitle($0) - }) - - stackView.addArrangedSubview(with(UITableView(frame: .zero, style: .plain)) { - tableView = $0 - $0.delegate = self - $0.dataSource = self - $0.cellLayoutMarginsFollowReadableWidth = true - - $0.backgroundColor = .systemBackground - $0.backgroundView = with(UIView()) { - $0.backgroundColor = .systemBackground - } - - // hides the empty separators - $0.tableFooterView = UIView() - - $0.register(OnboardingScanningInstanceCell.self, forCellReuseIdentifier: "OnboardingScanningInstanceCell") - }) - - NSLayoutConstraint.activate([ - tableView!.widthAnchor.constraint(equalTo: stackView.layoutMarginsGuide.widthAnchor), - ]) - - let manualHintLabel: UILabel = with(UILabel()) { - $0.text = L10n.Onboarding.Scanning.manualHint - $0.textColor = Asset.Colors.haPrimary.color - $0.font = .preferredFont(forTextStyle: .footnote) - $0.numberOfLines = 1 - $0.baselineAdjustment = .alignCenters - $0.minimumScaleFactor = 0.2 - $0.adjustsFontSizeToFitWidth = true - } - stackView.addArrangedSubview(manualHintLabel) - stackView.setCustomSpacing(stackView.spacing / 2.0, after: manualHintLabel) - - stackView.addArrangedSubview(with(UIButton(type: .custom)) { - $0.setTitle(L10n.Onboarding.Scanning.manual, for: .normal) - $0.addTarget(self, action: #selector(didSelectManual(_:)), for: .touchUpInside) - Current.style.onboardingButtonSecondary($0) - }) - - discovery.observer = self - - if Current.appConfiguration == .debug { - for (idx, instance) in [ - DiscoveredHomeAssistant( - manualURL: URL(string: "https://jigsaw.w3.org/HTTP/Basic")!, - name: "Basic Auth" - ), - DiscoveredHomeAssistant( - manualURL: URL(string: "http://httpbin.org/digest-auth/asdf")!, - name: "Digest Auth" - ), - DiscoveredHomeAssistant( - manualURL: URL(string: "https://self-signed.badssl.com/")!, - name: "Self signed SSL" - ), - DiscoveredHomeAssistant( - manualURL: URL(string: "https://client.badssl.com/")!, - name: "Client Cert" - ), - DiscoveredHomeAssistant( - manualURL: URL(string: "https://expired.badssl.com/")!, - name: "Expired" - ), - DiscoveredHomeAssistant( - manualURL: URL(string: "https://httpbin.org/statuses/404")!, - name: "Status Code 404" - ), - ].enumerated() { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500 * (idx + 1))) { [weak self] in - self?.add(discoveredInstance: instance) - } - } - } - } - - deinit { - discovery.stop() - } - - private func add(discoveredInstance: DiscoveredHomeAssistant) { - tableView?.performBatchUpdates({ - if let existing = discoveredInstances.firstIndex(where: { - ($0.uuid != nil && $0.uuid == discoveredInstance.uuid) - || $0.internalOrExternalURL == discoveredInstance.internalOrExternalURL - }) { - discoveredInstances[existing] = discoveredInstance - tableView?.reloadRows( - at: [IndexPath(row: existing, section: 0)], - with: .none - ) - } else { - UIAccessibility.post(notification: .announcement, argument: NSAttributedString( - string: L10n.Onboarding.Scanning.discoveredAnnouncement(discoveredInstance.locationName), - attributes: [.accessibilitySpeechQueueAnnouncement: true] - )) - discoveredInstances.append(discoveredInstance) - tableView?.insertRows( - at: [IndexPath(row: discoveredInstances.count - 1, section: 0)], - with: .automatic - ) - } - }, completion: nil) - } - - private func remove(forName name: String) { - tableView?.performBatchUpdates({ - if let existing = discoveredInstances.firstIndex(where: { - $0.bonjourName == name - }) { - discoveredInstances.remove(at: existing) - tableView?.deleteRows( - at: [IndexPath(row: existing, section: 0)], - with: .automatic - ) - } - }, completion: nil) - } - - @objc private func didSelectManual(_ sender: UIButton) { - navigationController?.pushViewController(OnboardingManualURLViewController(), animated: true) - } -} - -extension OnboardingScanningViewController: BonjourObserver { - func bonjour(_ bonjour: Bonjour, didAdd instance: DiscoveredHomeAssistant) { - add(discoveredInstance: instance) - } - - func bonjour(_ bonjour: Bonjour, didRemoveInstanceWithName name: String) { - remove(forName: name) - } -} - -extension OnboardingScanningViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let instance = discoveredInstances[indexPath.row] - let cell = tableView.cellForRow(at: indexPath) as? OnboardingScanningInstanceCell - - Current.Log.verbose("Selected row at \(indexPath.row) \(instance)") - - cell?.isLoading = true - tableView.isUserInteractionEnabled = false - - let authentication = OnboardingAuth() - - firstly { - authentication.authenticate(to: instance, sender: self) - }.ensure { - cell?.isLoading = false - tableView.isUserInteractionEnabled = true - tableView.deselectRow(at: indexPath, animated: true) - }.done { [self] server in - let controller = authentication.successController(server: server) - if controller is OnboardingTerminalViewController { - show(controller, sender: self) - } else { - navigationController?.pushViewController(controller, animated: true) - } - }.catch { [self] error in - navigationController?.pushViewController(authentication.failureController(error: error), animated: true) - } - } -} - -extension OnboardingScanningViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - discoveredInstances.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "OnboardingScanningInstanceCell", for: indexPath) - - let instance = discoveredInstances[indexPath.row] - - cell.textLabel?.text = instance.locationName - cell.detailTextLabel?.text = instance.internalOrExternalURL.absoluteString - cell.accessibilityLabel = instance.locationName - cell.accessibilityAttributedValue = with(NSMutableAttributedString()) { overall in - for part in [ - instance.internalOrExternalURL.host, - instance.internalOrExternalURL.port.flatMap { String(describing: $0) }, - ].compactMap({ $0 }) { - overall - .append(NSAttributedString( - string: part, - attributes: [.accessibilitySpeechPunctuation: true as NSNumber] - )) - overall.append(NSAttributedString(string: ", ")) - } - } - - return cell - } -} diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index 0daf0d507..a2c3d3123 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -3,34 +3,27 @@ import Shared import SwiftUI struct OnboardingServersListView: View { - @ObservedObject private var viewModel = OnboardingScanningViewModel() + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var hostingProvider: ViewControllerProvider + @ObservedObject private var viewModel = OnboardingServersListViewModel() + @State private var showDocumentation = false @State private var showManualInput = false + @State private var screenLoaded = false + + @Binding var shouldDismissOnboarding: Bool var body: some View { List { - Section { - ServersScanAnimationView() - .listRowBackground(Color.clear) - .frame(maxWidth: .infinity, alignment: .center) - } - ForEach(viewModel.discoveredInstances, id: \.uuid) { instance in - if #available(iOS 17, *) { - Section { - serverRow(instance: instance) - } - .listSectionSpacing(.compact) - } else { - serverRow(instance: instance) - } - } - .disabled(viewModel.currentlyInstanceLoading != nil) + headerView + list } .animation(.easeInOut, value: viewModel.discoveredInstances.count) .safeAreaInset(edge: .bottom) { bottomButtons } .navigationTitle(L10n.Onboarding.Scanning.title) + .navigationBarTitleDisplayMode(.inline) .toolbar(content: { ToolbarItem(placement: .topBarTrailing) { ProgressView() @@ -40,67 +33,73 @@ struct OnboardingServersListView: View { } }) .onAppear { - viewModel.startDiscovery() + onAppear() } .onDisappear { - viewModel.stopDiscovery() - viewModel.currentlyInstanceLoading = nil + onDisappear() } - .sheet(isPresented: $showManualInput, content: { - ManualURLEntryView { connectURL in - viewModel.isLoading = true - viewModel.selectInstance(.init(manualURL: connectURL)) - } - }) - .fullScreenCover(isPresented: $showDocumentation) { - SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) + .sheet(isPresented: $viewModel.showError) { + errorView } - .sheet(isPresented: .init(get: { - if case .error = viewModel.nextDestination { - return true - } else { - return false - } - }, set: { newValue in - if !newValue { - viewModel.resetFlow() - } - })) { - switch viewModel.nextDestination { - case .next, .none: - // This scenarios are handlded in full screen - EmptyView() - case let .error(error): - ConnectionErrorDetailsView( - server: ServerFixture.standard, - error: error, - showSettingsEntry: false, - expandMoreDetails: true - ) - } +// .fullScreenCover(isPresented: $viewModel.showLocationPermissionScreen) { +// VStack { +// Text("Location permission screen") +// } +// .onDisappear { +// +// } +// } + } + + private func onAppear() { + if !screenLoaded { + screenLoaded = true + viewModel.startDiscovery() } - .fullScreenCover(isPresented: .init(get: { - if case .next = viewModel.nextDestination { - return true - } else { - return false - } - }, set: { newValue in - if !newValue { + } + + private func onDisappear() { + viewModel.stopDiscovery() + viewModel.currentlyInstanceLoading = nil + } + + @ViewBuilder + private var errorView: some View { + if let error = viewModel.error { + ConnectionErrorDetailsView( + server: nil, + error: error, + showSettingsEntry: false, + expandMoreDetails: true + ) + .onDisappear { viewModel.resetFlow() } - })) { - switch viewModel.nextDestination { - case let .next(server): - NavigationView { - AnyView(OnboardinSuccessController(server: server)) + } else { + Text("Unmapped onboarding flow (1)") + } + } + + private var list: some View { + ForEach(viewModel.discoveredInstances, id: \.uuid) { instance in + if #available(iOS 17, *) { + Section { + serverRow(instance: instance) } - .navigationViewStyle(.stack) - case .error, .none: - // This scenarios are handlded in sheet - EmptyView() + .listSectionSpacing(.compact) + } else { + serverRow(instance: instance) } } + .disabled(viewModel.currentlyInstanceLoading != nil) + } + + private var headerView: some View { + Section { + ServersScanAnimationView() + .listRowBackground(Color.clear) + .frame(maxWidth: .infinity, alignment: .center) + } } private func serverRow(instance: DiscoveredHomeAssistant) -> some View { @@ -112,7 +111,7 @@ struct OnboardingServersListView: View { isLoading: instance == viewModel.currentlyInstanceLoading ) .onTapGesture { - viewModel.selectInstance(instance) + viewModel.selectInstance(instance, controller: hostingProvider.viewController) } } @@ -124,12 +123,21 @@ struct OnboardingServersListView: View { Text(L10n.Onboarding.Scanning.manual) } .buttonStyle(.primaryButton) + .sheet(isPresented: $showManualInput) { + ManualURLEntryView { connectURL in + viewModel.isLoading = true + viewModel.selectInstance(.init(manualURL: connectURL), controller: hostingProvider.viewController) + } + } Button(action: { showDocumentation = true }) { Text(L10n.Onboarding.Servers.Docs.read) } .buttonStyle(.secondaryButton) + .fullScreenCover(isPresented: $showDocumentation) { + SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) + } } .padding([.horizontal, .top]) .background(.ultraThinMaterial) diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift similarity index 70% rename from Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift rename to Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift index b52c5d3c4..1fc19befe 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingScanningViewModel.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift @@ -4,7 +4,7 @@ import PromiseKit import Shared import SwiftUI -final class OnboardingScanningViewModel: ObservableObject { +final class OnboardingServersListViewModel: ObservableObject { enum Destination { case error(Error) case next(Server) @@ -12,7 +12,11 @@ final class OnboardingScanningViewModel: ObservableObject { @Published var discoveredInstances: [DiscoveredHomeAssistant] = [] @Published var currentlyInstanceLoading: DiscoveredHomeAssistant? - @Published var nextDestination: Destination? + + @Published var showError = false + @Published var error: Error? + + @Published var showLocationPermissionScreen = false /// Indicator for manual input loading @Published var isLoading = false @@ -66,27 +70,26 @@ final class OnboardingScanningViewModel: ObservableObject { discovery.stop() } - func selectInstance(_ instance: DiscoveredHomeAssistant) { + func selectInstance(_ instance: DiscoveredHomeAssistant, controller: UIViewController?) { + guard let controller else { + Current.Log.error("No controller provided for onboarding") + return + } Current.Log.verbose("Selected instance \(instance)") currentlyInstanceLoading = instance let authentication = OnboardingAuth() - guard let topViewController = UIApplication.shared.windows.first?.rootViewController else { return } - authentication.authenticate(to: instance, sender: topViewController).pipe { [weak self] result in + authentication.authenticate(to: instance, sender: controller).pipe { [weak self] result in DispatchQueue.main.async { switch result { case let .fulfilled(server): Current.Log.verbose("Onboarding authentication succeeded") - self?.nextDestination = .next(server) // AnyView(OnboardinSuccessController(server: server)) + self?.authenticationSucceeded(server: server) case let .rejected(error): - if case .cancelled = error as? PMKError { - Current.Log.verbose("Cancelled onboarding authentication (PMKError Cancelled)") - self?.resetFlow() - return - } - self?.nextDestination = .error(error) + self?.error = error + self?.showError = true } self?.isLoading = false } @@ -94,18 +97,28 @@ final class OnboardingScanningViewModel: ObservableObject { } func resetFlow() { - nextDestination = nil currentlyInstanceLoading = nil isLoading = false } + + @MainActor + private func authenticationSucceeded(server: Server) { +// showLocationPermissionScreen = true + discovery.stop() + Current.onboardingObservation.complete() + } } -extension OnboardingScanningViewModel: BonjourObserver { +extension OnboardingServersListViewModel: BonjourObserver { func bonjour(_ bonjour: Bonjour, didAdd instance: DiscoveredHomeAssistant) { - discoveredInstances.append(instance) + DispatchQueue.main.async { [weak self] in + self?.discoveredInstances.append(instance) + } } func bonjour(_ bonjour: Bonjour, didRemoveInstanceWithName name: String) { - discoveredInstances.removeAll { $0.bonjourName == name } + DispatchQueue.main.async { [weak self] in + self?.discoveredInstances.removeAll { $0.bonjourName == name } + } } } diff --git a/Sources/App/Onboarding/Screens/OnboardingTerminalViewController.swift b/Sources/App/Onboarding/Screens/OnboardingTerminalViewController.swift deleted file mode 100644 index d62a5b7b1..000000000 --- a/Sources/App/Onboarding/Screens/OnboardingTerminalViewController.swift +++ /dev/null @@ -1,5 +0,0 @@ -import UIKit - -class OnboardingTerminalViewController: UIViewController { - // this is a dummy class whose presentation ends onboarding -} diff --git a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift index 6f2325d74..5e4a38f30 100644 --- a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift @@ -9,6 +9,8 @@ struct OnboardingWelcomeView: View { @State private var logoScale = 0.9 @State private var buttonYOffset: CGFloat = 10 + @Binding var shouldDismissOnboarding: Bool + var body: some View { VStack(spacing: .zero) { Spacer() @@ -63,7 +65,7 @@ struct OnboardingWelcomeView: View { private var continueButton: some View { VStack { - NavigationLink(destination: OnboardingServersListView()) { + NavigationLink(destination: OnboardingServersListView(shouldDismissOnboarding: $shouldDismissOnboarding)) { Text(verbatim: L10n.continueLabel) } .buttonStyle(.primaryButton) @@ -79,13 +81,5 @@ struct OnboardingWelcomeView: View { } #Preview { - OnboardingWelcomeView() -} - -struct OnboardingScanningView: UIViewControllerRepresentable { - func makeUIViewController(context: Context) -> UIViewController { - OnboardingScanningViewController() - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} + OnboardingWelcomeView(shouldDismissOnboarding: .constant(false)) } diff --git a/Sources/App/Settings/SettingsViewController.swift b/Sources/App/Settings/SettingsViewController.swift index 3b4e778b6..c5543e4dc 100644 --- a/Sources/App/Settings/SettingsViewController.swift +++ b/Sources/App/Settings/SettingsViewController.swift @@ -26,7 +26,7 @@ class SettingsViewController: HAFormViewController { super.init() } - class func servers() -> (Section, deallocate: () -> Void) { + class func servers(controller: UIViewController) -> (Section, deallocate: () -> Void) { class Observer: ServerObserver { let updateRows: () -> Void init(updateRows: @escaping () -> Void) { @@ -56,11 +56,13 @@ class SettingsViewController: HAFormViewController { rows.append(HomeAssistantAccountRow { $0.value = .add - $0.presentationMode = .show(controllerProvider: .callback(builder: { () -> UIViewController in - OnboardingNavigationViewController(onboardingStyle: .secondary) - }), onDismiss: nil) $0.onCellSelection { _, row in row.deselect(animated: true) + controller.present( + OnboardingNavigationView.controller(onboardingStyle: .secondary), + animated: true, + completion: nil + ) } }) @@ -103,7 +105,7 @@ class SettingsViewController: HAFormViewController { } if contentSections.contains(.servers) { - let (section, deallocate) = Self.servers() + let (section, deallocate) = Self.servers(controller: self) form +++ section after(life: self).done(deallocate) } diff --git a/Sources/App/WebView/WebViewWindowController.swift b/Sources/App/WebView/WebViewWindowController.swift index ca3cb8a8f..9b58d737f 100644 --- a/Sources/App/WebView/WebViewWindowController.swift +++ b/Sources/App/WebView/WebViewWindowController.swift @@ -2,6 +2,7 @@ import Foundation import MBProgressHUD import PromiseKit import Shared +import SwiftUI import UIKit final class WebViewWindowController { @@ -61,21 +62,15 @@ final class WebViewWindowController { func setup() { if let style = OnboardingNavigationViewController.requiredOnboardingStyle { - Current.Log.info("showing onboarding \(style)") - updateRootViewController(to: OnboardingNavigationViewController(onboardingStyle: style)) + Current.Log.info("Showing onboarding \(style)") + updateRootViewController(to: OnboardingNavigationView.controller(onboardingStyle: style)) } else { - if let rootController = window.rootViewController, !rootController.children.isEmpty { - Current.Log.info("[iOS 12] state restoration loaded controller, not creating a new one") - // not changing anything, but handle the promises - updateRootViewController(to: rootController) + if let webViewController = makeWebViewIfNotInCache(restorationType: .init(restorationActivity)) { + updateRootViewController(to: webViewNavigationController(rootViewController: webViewController)) } else { - if let webViewController = makeWebViewIfNotInCache(restorationType: .init(restorationActivity)) { - updateRootViewController(to: webViewNavigationController(rootViewController: webViewController)) - } else { - updateRootViewController(to: OnboardingNavigationViewController(onboardingStyle: .initial)) - } - restorationActivity = nil + updateRootViewController(to: OnboardingNavigationView.controller(onboardingStyle: .initial)) } + restorationActivity = nil } } @@ -414,7 +409,7 @@ extension WebViewWindowController: OnboardingStateObserver { func onboardingStateDidChange(to state: OnboardingState) { switch state { case let .needed(type): - guard !(window.rootViewController is OnboardingNavigationViewController) else { + if window.rootViewController as? UIHostingController != nil { return } @@ -427,7 +422,7 @@ extension WebViewWindowController: OnboardingStateObserver { switch type { case .error, .logout: if Current.servers.all.isEmpty { - let controller = OnboardingNavigationViewController(onboardingStyle: .initial) + let controller = OnboardingNavigationView.controller(onboardingStyle: .initial) updateRootViewController(to: controller) if type.shouldShowError { diff --git a/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift b/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift index 4aaeb4878..b6058dcba 100644 --- a/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift +++ b/Sources/Shared/API/Responses/DiscoveredHomeAssistant.swift @@ -14,7 +14,7 @@ public struct DiscoveredHomeAssistant: ImmutableMappable, Equatable { public init(manualURL: URL, name: String = "Home") { self.version = Version(major: 2022, minor: 4) - self.uuid = nil + self.uuid = "autogenerated-\(UUID().uuidString)" self.internalOrExternalURL = manualURL self.internalURL = nil self.externalURL = manualURL diff --git a/Sources/Shared/Common/Extensions/View+HA.swift b/Sources/Shared/Common/Extensions/View+HA.swift new file mode 100644 index 000000000..e39c2cde5 --- /dev/null +++ b/Sources/Shared/Common/Extensions/View+HA.swift @@ -0,0 +1,16 @@ +import Foundation +import SwiftUI + +public extension View { + func embeddedInHostingController() -> UIHostingController { + let provider = ViewControllerProvider() + let hostingAccessingView = environmentObject(provider) + let hostingController = UIHostingController(rootView: hostingAccessingView) + provider.viewController = hostingController + return hostingController + } +} + +public final class ViewControllerProvider: ObservableObject { + public fileprivate(set) weak var viewController: UIViewController? +} diff --git a/Sources/Shared/Environment/OnboardingStateObservation.swift b/Sources/Shared/Environment/OnboardingStateObservation.swift index 74c4013a2..d20bdb55e 100644 --- a/Sources/Shared/Environment/OnboardingStateObservation.swift +++ b/Sources/Shared/Environment/OnboardingStateObservation.swift @@ -1,7 +1,7 @@ import Foundation -public enum OnboardingState { - public enum NeededType { +public enum OnboardingState: Equatable { + public enum NeededType: Equatable { case logout case error case unauthenticated(_ serverId: String, _ code: Int) From 4a30c8c1f03f3b9bf670d434c0158d2be59f9751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:15:16 +0200 Subject: [PATCH 09/21] New permission screen WIP --- HomeAssistant.xcodeproj/project.pbxproj | 24 +++- .../Container/OnboardingNavigationView.swift | 2 +- ...boardingPermissionWorkflowController.swift | 12 -- .../LocationPermissionView.swift | 112 +++++++++++++++++ .../OnboardingPermissionsNavigationView.swift | 39 ++++++ .../OnboardingServersListView.swift | 11 +- .../OnboardingServersListViewModel.swift | 5 +- .../Resources/en.lproj/Localizable.strings | 8 ++ Sources/App/Utilities/Permissions.swift | 32 +++-- .../Components/PrivacyNoteView.swift | 118 ++++++++++++++++++ .../DesignSystem/Styles/HAButtonStyles.swift | 62 +++++++++ .../Shared/Resources/Swiftgen/Strings.swift | 26 ++++ 12 files changed, 410 insertions(+), 41 deletions(-) delete mode 100644 Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift create mode 100644 Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift create mode 100644 Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift create mode 100644 Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 2e2841f00..99585e8cb 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -403,7 +403,6 @@ 11CFD785273662DF0082D557 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CFD783273662DF0082D557 /* Server.swift */; }; 11D826F124E39F2E005B8A86 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11D826F024E39F2D005B8A86 /* CoreNFC.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 11DA6B4B27137A60008ADFAF /* InputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DA6B4A27137A60008ADFAF /* InputAccessoryView.swift */; }; - 11DA6B4D2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DA6B4C2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift */; }; 11DC6BAB24E23780002D9FDA /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = B63CCDCF2164714900123C50 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; 11DE822E24FAC51100E636B8 /* IncomingURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DE822D24FAC51000E636B8 /* IncomingURLHandler.swift */; }; 11DE823024FAE66F00E636B8 /* UIWindow+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11DE822F24FAE66F00E636B8 /* UIWindow+Additions.swift */; }; @@ -629,6 +628,7 @@ 424151FD2CD8F27100D7A6F9 /* CarPlayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42ABB0B82C888AA10081461D /* CarPlayConfig.swift */; }; 424627332C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424627322C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift */; }; 424627342C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424627322C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift */; }; + 42462E692D9ED75900ECC8A7 /* LocationPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E682D9ED75900ECC8A7 /* LocationPermissionView.swift */; }; 424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; }; 424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; }; 424D2D102C89DACE00C610F1 /* HAAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */; }; @@ -725,6 +725,8 @@ 427FEE562D9D39A50047C00C /* OnboardingNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE552D9D39A50047C00C /* OnboardingNavigationView.swift */; }; 427FEE592D9D48120047C00C /* View+HA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE572D9D48120047C00C /* View+HA.swift */; }; 427FEE632D9EA1400047C00C /* OnboardingNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE622D9EA1400047C00C /* OnboardingNavigationViewModel.swift */; }; + 427FEE662D9EBC430047C00C /* OnboardingPermissionsNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE652D9EBC430047C00C /* OnboardingPermissionsNavigationView.swift */; }; + 427FEE682D9ECFD70047C00C /* PrivacyNoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427FEE672D9ECFD70047C00C /* PrivacyNoteView.swift */; }; 428338442BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 428338452BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 4285C5512D355F9900DADE45 /* WidgetCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5502D355F9900DADE45 /* WidgetCreationView.swift */; }; @@ -1851,7 +1853,6 @@ 11CFD783273662DF0082D557 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; 11D826F024E39F2D005B8A86 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; }; 11DA6B4A27137A60008ADFAF /* InputAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputAccessoryView.swift; sourceTree = ""; }; - 11DA6B4C2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPermissionWorkflowController.swift; sourceTree = ""; }; 11DE822D24FAC51000E636B8 /* IncomingURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingURLHandler.swift; sourceTree = ""; }; 11DE822F24FAE66F00E636B8 /* UIWindow+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Additions.swift"; sourceTree = ""; }; 11DE9D8325B6103C0081C0ED /* Home Assistant Launcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Home Assistant Launcher.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2040,6 +2041,7 @@ 4242A2D02B2B5C9F00E9F001 /* es-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "es-ES"; path = "es-ES.lproj/AppIntentVocabulary.plist"; sourceTree = ""; }; 4242A2D12B2B5C9F00E9F001 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "es-MX"; path = "es-MX.lproj/AppIntentVocabulary.plist"; sourceTree = ""; }; 424627322C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBasicViewTintedWrapper.swift; sourceTree = ""; }; + 42462E682D9ED75900ECC8A7 /* LocationPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionView.swift; sourceTree = ""; }; 424A7F452B188946008C8DF3 /* WidgetBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBackground.swift; sourceTree = ""; }; 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetContentMargin.swift; sourceTree = ""; }; 424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HAAppEntity.swift; sourceTree = ""; }; @@ -2136,6 +2138,8 @@ 427FEE552D9D39A50047C00C /* OnboardingNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationView.swift; sourceTree = ""; }; 427FEE572D9D48120047C00C /* View+HA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+HA.swift"; sourceTree = ""; }; 427FEE622D9EA1400047C00C /* OnboardingNavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationViewModel.swift; sourceTree = ""; }; + 427FEE652D9EBC430047C00C /* OnboardingPermissionsNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPermissionsNavigationView.swift; sourceTree = ""; }; + 427FEE672D9ECFD70047C00C /* PrivacyNoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyNoteView.swift; sourceTree = ""; }; 42805A132B0226050095414C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/AppIntentVocabulary.plist; sourceTree = ""; }; 4283383F2BA1B17C004798C2 /* AssistRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistRequests.swift; sourceTree = ""; }; 428338422BA1BAFB004798C2 /* Spaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = ""; }; @@ -3106,6 +3110,7 @@ children = ( 426EE49A2CA4194E00A5EF4F /* OnboardingWelcomeView.swift */, 427FEE052D9C03C50047C00C /* OnboardingServersList */, + 427FEE642D9EBC340047C00C /* OnboardingPermissions */, 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */, 42E95C582CA46AD50010ECE3 /* ActivityView.swift */, 42E95C542CA44FC90010ECE3 /* SafariWebView.swift */, @@ -3119,7 +3124,6 @@ isa = PBXGroup; children = ( B6022222226DBA3800E8DBFE /* OnboardingNavigationViewController.swift */, - 11DA6B4C2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift */, 427FEE552D9D39A50047C00C /* OnboardingNavigationView.swift */, 427FEE622D9EA1400047C00C /* OnboardingNavigationViewModel.swift */, ); @@ -4226,6 +4230,15 @@ path = ManualURLEntry; sourceTree = ""; }; + 427FEE642D9EBC340047C00C /* OnboardingPermissions */ = { + isa = PBXGroup; + children = ( + 427FEE652D9EBC430047C00C /* OnboardingPermissionsNavigationView.swift */, + 42462E682D9ED75900ECC8A7 /* LocationPermissionView.swift */, + ); + path = OnboardingPermissions; + sourceTree = ""; + }; 428338412BA1BAF3004798C2 /* Constants */ = { isa = PBXGroup; children = ( @@ -4448,6 +4461,7 @@ 4254C4CC2D103F7B00245021 /* ExternalLinkButton.swift */, 42B74A5C2D36A47E00C37304 /* CloseButton.swift */, 42C131CF2D66084C00AF48E6 /* PillView.swift */, + 427FEE672D9ECFD70047C00C /* PrivacyNoteView.swift */, ); path = Components; sourceTree = ""; @@ -7448,6 +7462,7 @@ 42F1DA6B2B4ED1BF002729BC /* CarPlayAreasZonesTemplate.swift in Sources */, 427FEE0B2D9C05EF0047C00C /* ServersScanAnimationView.swift in Sources */, 11A48D8124CA8ADB0021BDD9 /* NotificationCategory+Observation.swift in Sources */, + 42462E692D9ED75900ECC8A7 /* LocationPermissionView.swift in Sources */, 42D0AE452D88259000D9715A /* WidgetDocumentationLink.swift in Sources */, 1100D51D2496AECE00B1073C /* PermissionStatusRow.swift in Sources */, 42E6C08A2CE4F4FA007CA622 /* DownloadManagerView.swift in Sources */, @@ -7502,7 +7517,7 @@ 427FEE632D9EA1400047C00C /* OnboardingNavigationViewModel.swift in Sources */, 42F1DA5D2B4BF85F002729BC /* WindowScenesManager.swift in Sources */, 11EFCDDA24F5FE0600314D85 /* SceneActivity.swift in Sources */, - 11DA6B4D2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift in Sources */, + 427FEE662D9EBC430047C00C /* OnboardingPermissionsNavigationView.swift in Sources */, 1185DFB1271FF53800ED7D9A /* OnboardingAuthStepNotify.swift in Sources */, 4278CB852D01F0B200CFAAC9 /* GesturesSetupViewModel.swift in Sources */, B65C0B522282BA13007E057B /* NotificationSettingsViewController.swift in Sources */, @@ -7908,6 +7923,7 @@ 42790C462C4808FA00E31B38 /* AppleLikeBottomSheet.swift in Sources */, B6723344225DBACF0031D629 /* AuthRequestMessage.swift in Sources */, D0DD2CEE213BCA8900C3D9F7 /* URL+Extensions.swift in Sources */, + 427FEE682D9ECFD70047C00C /* PrivacyNoteView.swift in Sources */, 11BA5EC92759AC0300FC40E8 /* XCGLogger+Export.swift in Sources */, 11B38EE4275C54A200205C7B /* FireEventIntentHandler.swift in Sources */, 1120C5842749C6350046C38B /* ServerProviding.swift in Sources */, diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift index 08786fb44..f3bc17fa9 100644 --- a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift +++ b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift @@ -27,7 +27,7 @@ struct OnboardingNavigationView: View { case .full: OnboardingWelcomeView(shouldDismissOnboarding: $viewModel.shouldDismiss) case .permissions: - Text("Unmapped flow (3)") + OnboardingPermissionsNavigationView() } } } diff --git a/Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift b/Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift deleted file mode 100644 index dca49ebc5..000000000 --- a/Sources/App/Onboarding/Container/OnboardingPermissionWorkflowController.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Shared -import UIKit - -enum OnboardingPermissionHandler { - static var notDeterminedPermissions: [PermissionType] { - var permissions: [PermissionType] = [ - // .location, - ] - - return permissions.filter { $0.status == .notDetermined } - } -} diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift new file mode 100644 index 000000000..4b2e16c47 --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift @@ -0,0 +1,112 @@ +import CoreLocation +import Shared +import SwiftUI + +struct LocationPermissionView: View { + @StateObject private var viewModel = LocationPermissionViewModel() + let permission: PermissionType + + var body: some View { + VStack(spacing: Spaces.three) { + header + Spacer() + actionButtons + } + .padding() + .alert( + L10n.Onboarding.Permission.Location.Deny.Alert.title, + isPresented: $viewModel.showDenyAlert, + actions: { + Button(L10n.continueLabel, role: .destructive) { + viewModel.requestLocationPermission() + } + }, + message: { + Text(verbatim: L10n.Onboarding.Permission.Location.Deny.Alert.message) + } + ) + } + + @ViewBuilder + private var header: some View { + VStack(spacing: Spaces.two) { + Image(uiImage: permission.enableIcon.image( + ofSize: .init(width: 100, height: 100), + color: nil + ).withRenderingMode(.alwaysTemplate)) + .foregroundStyle(Color.asset(Asset.Colors.haPrimary)) + Text(verbatim: permission.title) + .font(.title.bold()) + Text(verbatim: L10n.Onboarding.Permission.Location.description) + .multilineTextAlignment(.center) + .opacity(0.5) + PrivacyNoteView(content: L10n.Onboarding.Permission.Location.privacyNote) + } + .frame(maxWidth: .infinity, alignment: .center) + } + + private var bullets: some View { + Group { + ForEach(permission.enableBulletPoints, id: \.id) { bulletPoint in + Text(verbatim: bulletPoint.text) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var actionButtons: some View { + VStack(spacing: Spaces.one) { + Button { + viewModel.requestLocationPermission() + } label: { + Text(L10n.Onboarding.Permission.Location.Buttons.allowAndShare) + } + .buttonStyle(.primaryButton) + Button {} label: { + Text(L10n.Onboarding.Permission.Location.Buttons.allowForApp) + } + .buttonStyle(.primaryButton) + Button { + viewModel.showDenyAlert = true + } label: { + Text(L10n.Onboarding.Permission.Location.Buttons.deny) + } + .buttonStyle(.secondaryNegativeButton) + } + } +} + +#Preview { + LocationPermissionView(permission: .location) +} + +final class LocationPermissionViewModel: NSObject, ObservableObject { + @Published var showDenyAlert: Bool = false + private let locationManager = CLLocationManager() + + func requestLocationPermission() { + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + } +} + +extension LocationPermissionViewModel: CLLocationManagerDelegate { + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + switch manager.authorizationStatus { + case .notDetermined: + break + case .restricted: + break + case .denied: + break + case .authorizedAlways: + break + case .authorizedWhenInUse: + manager.requestAlwaysAuthorization() + case .authorized: + break + @unknown default: + break + } + } +} diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift new file mode 100644 index 000000000..5aef2b020 --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift @@ -0,0 +1,39 @@ +import Shared +import SwiftUI + +enum OnboardingPermissionHandler { + static var notDeterminedPermissions: [PermissionType] { + let permissions: [PermissionType] = [ + .location, + ] + + return permissions.filter { $0.status == .notDetermined } + } +} + +struct OnboardingPermissionsNavigationView: View { + var body: some View { + if let permission = OnboardingPermissionHandler.notDeterminedPermissions.first { + if permission == .location { + LocationPermissionView(permission: permission) + } else { + // If we endup enforcing other permissions during onboarding + // we need to handle them here + flowEnd + } + } else { + flowEnd + } + } + + private var flowEnd: some View { + EmptyView() + .onAppear { + Current.onboardingObservation.complete() + } + } +} + +#Preview { + OnboardingPermissionsNavigationView() +} diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index a2c3d3123..bd980725c 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -41,14 +41,9 @@ struct OnboardingServersListView: View { .sheet(isPresented: $viewModel.showError) { errorView } -// .fullScreenCover(isPresented: $viewModel.showLocationPermissionScreen) { -// VStack { -// Text("Location permission screen") -// } -// .onDisappear { -// -// } -// } + .fullScreenCover(isPresented: $viewModel.showPermissionsFlow) { + OnboardingPermissionsNavigationView() + } } private func onAppear() { diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift index 1fc19befe..76f000015 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift @@ -16,7 +16,7 @@ final class OnboardingServersListViewModel: ObservableObject { @Published var showError = false @Published var error: Error? - @Published var showLocationPermissionScreen = false + @Published var showPermissionsFlow = false /// Indicator for manual input loading @Published var isLoading = false @@ -103,9 +103,8 @@ final class OnboardingServersListViewModel: ObservableObject { @MainActor private func authenticationSucceeded(server: Server) { -// showLocationPermissionScreen = true discovery.stop() - Current.onboardingObservation.complete() + showPermissionsFlow = true } } diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 1d5d2b86f..9d2e2d185 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -32,6 +32,7 @@ "actions_configurator.visual_section.scene_hint_footer" = "You can also change these by customizing the Scene attributes: %@"; "actions_configurator.visual_section.server_defined" = "The appearance of this action is controlled by the server configuration."; "addButtonLabel" = "Add"; +"privacyLabel" = "Privacy"; "alert.confirmation.delete_entities.message" = "This will clean your entities from database and it will only reload the next time you open the app from zero."; "alert.confirmation.generic.title" = "Are you sure?"; "alerts.action_automation_editor.unavailable.body" = "To automatically create an automation for an Action please update your Home Assistant to at least version 2024.2"; @@ -1187,3 +1188,10 @@ Home Assistant is free and open source home automation software with a focus on "widgets.sensors.not_configured" = "No Sensors Configured"; "widgets.sensors.title" = "Sensors"; "yes_label" = "Yes"; +"onboarding.permission.location.description" = "To identify if you are at home and connect locally to Home Assistant, Apple requires that we ask for your location permission."; +"onboarding.permission.location.privacy_note" = "Your location won't be shared with Home Assistant unless you select 'Allow & Share with Home Assistant'"; +"onboarding.permission.location.buttons.allow_and_share" = "Allow & Share with Home Assistant"; +"onboarding.permission.location.buttons.allow_for_app" = "Allow for App use only"; +"onboarding.permission.location.buttons.deny" = "Deny"; +"onboarding.permission.location.deny.alert.title" = "Are you sure?"; +"onboarding.permission.location.deny.alert.message" = "Without location permission future versions of the App may deny access to your local Home Assistant server due to privacy concerns. If you are sure, please continue and tap 'Deny' on the next popup as well. By doing that we recommend you use your internal URL as external, since it is the only URL the app will try to access."; diff --git a/Sources/App/Utilities/Permissions.swift b/Sources/App/Utilities/Permissions.swift index 00c2c4113..7c729bb44 100644 --- a/Sources/App/Utilities/Permissions.swift +++ b/Sources/App/Utilities/Permissions.swift @@ -145,38 +145,44 @@ public enum PermissionType { } } - var enableBulletPoints: [(MaterialDesignIcons, String)] { + struct BulletPoint { + var id = UUID().uuidString + let icon: MaterialDesignIcons + let text: String + } + + var enableBulletPoints: [BulletPoint] { switch self { case .location: return [ - (.homeAutomationIcon, L10n.Onboarding.Permissions.Location.Bullet.automations), - (.mapOutlineIcon, L10n.Onboarding.Permissions.Location.Bullet.history), - (.wifiIcon, L10n.Onboarding.Permissions.Location.Bullet.wifi), + BulletPoint(icon: .homeAutomationIcon, text: L10n.Onboarding.Permissions.Location.Bullet.automations), + BulletPoint(icon: .mapOutlineIcon, text: L10n.Onboarding.Permissions.Location.Bullet.history), + BulletPoint(icon: .wifiIcon, text: L10n.Onboarding.Permissions.Location.Bullet.wifi), ] case .motion: return [ - (.walkIcon, L10n.Onboarding.Permissions.Motion.Bullet.steps), - (.mapMarkerDistanceIcon, L10n.Onboarding.Permissions.Motion.Bullet.distance), - (.bikeIcon, L10n.Onboarding.Permissions.Motion.Bullet.activity), + BulletPoint(icon: .walkIcon, text: L10n.Onboarding.Permissions.Motion.Bullet.steps), + BulletPoint(icon: .mapMarkerDistanceIcon, text: L10n.Onboarding.Permissions.Motion.Bullet.distance), + BulletPoint(icon: .bikeIcon, text: L10n.Onboarding.Permissions.Motion.Bullet.activity), ] case .notification: return [ - (.alertDecagramIcon, L10n.Onboarding.Permissions.Notification.Bullet.alert), - (.textIcon, L10n.Onboarding.Permissions.Notification.Bullet.commands), - (.bellBadgeOutlineIcon, L10n.Onboarding.Permissions.Notification.Bullet.badge), + BulletPoint(icon: .alertDecagramIcon, text: L10n.Onboarding.Permissions.Notification.Bullet.alert), + BulletPoint(icon: .textIcon, text: L10n.Onboarding.Permissions.Notification.Bullet.commands), + BulletPoint(icon: .bellBadgeOutlineIcon, text: L10n.Onboarding.Permissions.Notification.Bullet.badge), ] case .focus: return [ - (.homeAutomationIcon, L10n.Onboarding.Permissions.Focus.Bullet.automations), - (.cancelIcon, L10n.Onboarding.Permissions.Focus.Bullet.instant), + BulletPoint(icon: .homeAutomationIcon, text: L10n.Onboarding.Permissions.Focus.Bullet.automations), + BulletPoint(icon: .cancelIcon, text: L10n.Onboarding.Permissions.Focus.Bullet.instant), ] } } var status: PermissionStatus { - let locationManager = CLLocationManager() switch self { case .location: + let locationManager = CLLocationManager() return locationManager.authorizationStatus.genericStatus case .motion: return CMMotionActivityManager.authorizationStatus().genericStatus diff --git a/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift b/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift new file mode 100644 index 000000000..f92826132 --- /dev/null +++ b/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift @@ -0,0 +1,118 @@ +import SwiftUI + +public struct PrivacyNoteView: View { + @State private var background: AnyView + @State private var startPoint: UnitPoint = .topLeading + @State private var endPoint: UnitPoint = .bottomTrailing + @State private var timer: Timer? + + let cornerRadius: CGFloat = 10 + let content: String + + public init(content: String) { + self.content = content + + self.background = AnyView( + LinearGradient( + colors: [.purple, .blue], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + } + + public var body: some View { + VStack(spacing: Spaces.one) { + Text(L10n.privacyLabel) + .font(.caption.bold()) + .padding(.horizontal, Spaces.one) + .padding(.vertical, Spaces.half) + .background(.regularMaterial) + .foregroundStyle(.gray) + .clipShape(Capsule()) + .frame(maxWidth: .infinity, alignment: .leading) + Text(verbatim: content) + .font(.caption) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .foregroundStyle(.gray) + } + .padding(Spaces.one) + .background(background) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .shadow(color: Color(uiColor: .label).opacity(0.2), radius: 5) + .padding(.top) + .onAppear { + reverse() + startTimer() + } + .onDisappear { + stopTimer() + } + } + + private func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in + reverse() + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + + private func reverse() { + startPoint = rotatePoint(startPoint) + endPoint = rotatePoint(endPoint) + withAnimation(.easeIn(duration: 2)) { + background = AnyView( + LinearGradient( + colors: [.purple, .blue], + startPoint: startPoint, + endPoint: endPoint + ) + .overlay(content: { + material + }) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + ) + } + } + + private func rotatePoint(_ point: UnitPoint) -> UnitPoint { + switch point { + case .topLeading: + return .top + case .top: + return .topTrailing + case .topTrailing: + return .trailing + case .trailing: + return .bottomTrailing + case .bottomTrailing: + return .bottom + case .bottom: + return .bottomLeading + case .bottomLeading: + return .leading + case .leading: + return .topLeading + default: + return .topLeading + } + } + + private var material: some View { + VStack {} + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.thickMaterial) + } +} + +#Preview { + PrivacyNoteView( + content: "This is a privacy note. It contains important information about your data and how it is used." + ) + .padding() +} diff --git a/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift b/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift index 2cfc3209b..166440d3e 100644 --- a/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift +++ b/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift @@ -16,6 +16,36 @@ public struct HAButtonStyle: ButtonStyle { } } +public struct HANeutralButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled: Bool + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.callout.bold()) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 55) + .background(Color.gray) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .opacity(isEnabled ? 1 : 0.5) + } +} + +public struct HANegativeButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled: Bool + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.callout.bold()) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 55) + .background(isEnabled ? .red : Color.gray) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .opacity(isEnabled ? 1 : 0.5) + } +} + public struct HASecondaryButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled: Bool @@ -30,6 +60,20 @@ public struct HASecondaryButtonStyle: ButtonStyle { } } +public struct HASecondaryNegativeButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled: Bool + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.callout.bold()) + .foregroundColor(.red) + .frame(maxWidth: .infinity) + .frame(height: 55) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .opacity(isEnabled ? 1 : 0.5) + } +} + public struct HAPillButtonStyle: ButtonStyle { public func makeBody(configuration: Configuration) -> some View { configuration.label @@ -73,12 +117,30 @@ public extension ButtonStyle where Self == HAButtonStyle { } } +public extension ButtonStyle where Self == HANegativeButtonStyle { + static var negativeButton: HANegativeButtonStyle { + HANegativeButtonStyle() + } +} + +public extension ButtonStyle where Self == HANeutralButtonStyle { + static var neutralButton: HANeutralButtonStyle { + HANeutralButtonStyle() + } +} + public extension ButtonStyle where Self == HASecondaryButtonStyle { static var secondaryButton: HASecondaryButtonStyle { HASecondaryButtonStyle() } } +public extension ButtonStyle where Self == HASecondaryNegativeButtonStyle { + static var secondaryNegativeButton: HASecondaryNegativeButtonStyle { + HASecondaryNegativeButtonStyle() + } +} + public extension ButtonStyle where Self == HALinkButtonStyle { static var linkButton: HALinkButtonStyle { HALinkButtonStyle() diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 6404e7a23..f3b693df2 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -44,6 +44,8 @@ public enum L10n { public static var openLabel: String { return L10n.tr("Localizable", "open_label") } /// Preview Output public static var previewOutput: String { return L10n.tr("Localizable", "preview_output") } + /// Privacy + public static var privacyLabel: String { return L10n.tr("Localizable", "privacyLabel") } /// Reload public static var reloadLabel: String { return L10n.tr("Localizable", "reload_label") } /// Requires %@ or later. @@ -1715,6 +1717,30 @@ public enum L10n { public static var title: String { return L10n.tr("Localizable", "onboarding.manual_setup.text_field.title") } } } + public enum Permission { + public enum Location { + /// To identify if you are at home and connect locally to Home Assistant, Apple requires that we ask for your location permission. + public static var description: String { return L10n.tr("Localizable", "onboarding.permission.location.description") } + /// Your location won't be shared with Home Assistant unless you select 'Allow & Share with Home Assistant' + public static var privacyNote: String { return L10n.tr("Localizable", "onboarding.permission.location.privacy_note") } + public enum Buttons { + /// Allow & Share with Home Assistant + public static var allowAndShare: String { return L10n.tr("Localizable", "onboarding.permission.location.buttons.allow_and_share") } + /// Allow for App use only + public static var allowForApp: String { return L10n.tr("Localizable", "onboarding.permission.location.buttons.allow_for_app") } + /// Deny + public static var deny: String { return L10n.tr("Localizable", "onboarding.permission.location.buttons.deny") } + } + public enum Deny { + public enum Alert { + /// Without location permission future versions of the App may deny access to your local Home Assistant server due to privacy concerns. If you are sure, please continue and tap 'Deny' on the next popup as well. By doing that we recommend you use your internal URL as external, since it is the only URL the app will try to access. + public static var message: String { return L10n.tr("Localizable", "onboarding.permission.location.deny.alert.message") } + /// Are you sure? + public static var title: String { return L10n.tr("Localizable", "onboarding.permission.location.deny.alert.title") } + } + } + } + } public enum Permissions { /// Allow public static var allow: String { return L10n.tr("Localizable", "onboarding.permissions.allow") } From a46ba4057bdc6b97e7bce53ba07d01f5aff07788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:39:04 +0200 Subject: [PATCH 10/21] Reorganizing folders and improvements to onboarding welcome screen --- HomeAssistant.xcodeproj/project.pbxproj | 50 +++++++++++------- .../Screens/OnboardingErrorView.swift | 2 +- .../Screens/OnboardingWelcomeView.swift | 9 ++-- ...vityView.swift => ShareActivityView.swift} | 6 +-- .../Bluetooth/BluetoothPermissionView.swift | 0 .../BluetoothPermissionViewModel.swift | 0 .../PermissionRequestView.swift | 0 .../Resources/en.lproj/Localizable.strings | 1 - .../WebView/ConnectionErrorDetailsView.swift | 2 +- .../{App => }/Improv/ImprovDiscoverView.swift | 0 .../Improv/Views/ImprovFailureView.swift | 0 .../Improv/Views/ImprovSuccessView.swift | 0 Sources/Shared/Assets/Assets.swift | 1 + .../Contents.json | 22 ++++++++ .../logo-horizontal-text-dark.png | Bin 0 -> 29299 bytes .../logo-horizontal-text.png | Bin 0 -> 29612 bytes .../Shared/Resources/Swiftgen/Strings.swift | 4 -- .../ThreadCredentialsManagementView.swift | 0 ...ThreadCredentialsManagementViewModel.swift | 0 .../Views/ThreadCredentialDetailsView.swift | 0 .../ThreadCredentialsSharing+build.swift | 0 .../ThreadCredentialsSharingView.swift | 0 ...dCredentialsSharingViewModelProtocol.swift | 0 ...redentialsSharingToKeychainViewModel.swift | 0 ...hreadTransferCredentialToHAViewModel.swift | 0 .../WatchCommunicatorService.swift | 0 26 files changed, 64 insertions(+), 33 deletions(-) rename Sources/App/Onboarding/Screens/{ActivityView.swift => ShareActivityView.swift} (75%) rename Sources/App/{Onboarding/Screens => PermissionScreen}/Bluetooth/BluetoothPermissionView.swift (100%) rename Sources/App/{Onboarding/Screens => PermissionScreen}/Bluetooth/BluetoothPermissionViewModel.swift (100%) rename Sources/App/{Onboarding/Screens => PermissionScreen}/PermissionRequestView.swift (100%) rename Sources/{App => }/Improv/ImprovDiscoverView.swift (100%) rename Sources/{App => }/Improv/Views/ImprovFailureView.swift (100%) rename Sources/{App => }/Improv/Views/ImprovSuccessView.swift (100%) create mode 100644 Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/Contents.json create mode 100644 Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/logo-horizontal-text-dark.png create mode 100644 Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/logo-horizontal-text.png rename Sources/{App => }/Thread/CredentialsManagement/ThreadCredentialsManagementView.swift (100%) rename Sources/{App => }/Thread/CredentialsManagement/ThreadCredentialsManagementViewModel.swift (100%) rename Sources/{App => }/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift (100%) rename Sources/{App => }/Thread/CredentialsSharing/ThreadCredentialsSharing+build.swift (100%) rename Sources/{App => }/Thread/CredentialsSharing/ThreadCredentialsSharingView.swift (100%) rename Sources/{App => }/Thread/CredentialsSharing/ThreadCredentialsSharingViewModelProtocol.swift (100%) rename Sources/{App => }/Thread/CredentialsSharing/ToAppleKeychain/ThreadCredentialsSharingToKeychainViewModel.swift (100%) rename Sources/{App => }/Thread/CredentialsSharing/ToHomeAssistant/ThreadTransferCredentialToHAViewModel.swift (100%) rename Sources/{App => Watch}/WatchCommunicatorService.swift (100%) diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 99585e8cb..c3477fb58 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -870,7 +870,7 @@ 42E6C08C2CE4F7A8007CA622 /* DownloadManagerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E6C08B2CE4F7A8007CA622 /* DownloadManagerViewModel.swift */; }; 42E95C552CA44FC90010ECE3 /* SafariWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C542CA44FC90010ECE3 /* SafariWebView.swift */; }; 42E95C572CA45EFA0010ECE3 /* OnboardingErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */; }; - 42E95C592CA46AD50010ECE3 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C582CA46AD50010ECE3 /* ActivityView.swift */; }; + 42E95C592CA46AD50010ECE3 /* ShareActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C582CA46AD50010ECE3 /* ShareActivityView.swift */; }; 42E9AFFF2CE63944009DDA46 /* AudioOutputSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E9AFFE2CE63944009DDA46 /* AudioOutputSensor.swift */; }; 42E9B0002CE63944009DDA46 /* AudioOutputSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E9AFFE2CE63944009DDA46 /* AudioOutputSensor.swift */; }; 42EB030A2C6E4D0E00A184A6 /* WatchMagicViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EB03092C6E4D0E00A184A6 /* WatchMagicViewRow.swift */; }; @@ -2263,7 +2263,7 @@ 42E6C08B2CE4F7A8007CA622 /* DownloadManagerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManagerViewModel.swift; sourceTree = ""; }; 42E95C542CA44FC90010ECE3 /* SafariWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebView.swift; sourceTree = ""; }; 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingErrorView.swift; sourceTree = ""; }; - 42E95C582CA46AD50010ECE3 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; + 42E95C582CA46AD50010ECE3 /* ShareActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareActivityView.swift; sourceTree = ""; }; 42E9AFFE2CE63944009DDA46 /* AudioOutputSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioOutputSensor.swift; sourceTree = ""; }; 42EB03092C6E4D0E00A184A6 /* WatchMagicViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMagicViewRow.swift; sourceTree = ""; }; 42EF0ACC2D4CDC0C0088C91E /* ResetAllCustomWidgetConfirmationAppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetAllCustomWidgetConfirmationAppIntent.swift; sourceTree = ""; }; @@ -2941,6 +2941,9 @@ children = ( B657A8E81CA646EB00121384 /* App */, FD3BC66429BA000A00B19FBE /* CarPlay */, + 42A47A8E2C4548BC00C9B43D /* Improv */, + 42FCCFF72B9B1C310057783F /* Thread */, + 42462E6A2DA3CF8500ECC8A7 /* Watch */, 111501A72528412C00DCFA94 /* Extensions */, 11DE9D8425B6103C0081C0ED /* Launcher */, 1167402325198F9A00F51626 /* MacBridge */, @@ -3112,10 +3115,8 @@ 427FEE052D9C03C50047C00C /* OnboardingServersList */, 427FEE642D9EBC340047C00C /* OnboardingPermissions */, 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */, - 42E95C582CA46AD50010ECE3 /* ActivityView.swift */, + 42E95C582CA46AD50010ECE3 /* ShareActivityView.swift */, 42E95C542CA44FC90010ECE3 /* SafariWebView.swift */, - 42DF6B2C2CCF8A2200D7EC14 /* PermissionRequestView.swift */, - 429821122CD0DD71005ECD39 /* Bluetooth */, ); path = Screens; sourceTree = ""; @@ -4036,6 +4037,23 @@ path = MagicItem; sourceTree = ""; }; + 42462E6A2DA3CF8500ECC8A7 /* Watch */ = { + isa = PBXGroup; + children = ( + 42B1A7442C1305C300904548 /* WatchCommunicatorService.swift */, + ); + path = Watch; + sourceTree = ""; + }; + 42462E6B2DA3D05500ECC8A7 /* PermissionScreen */ = { + isa = PBXGroup; + children = ( + 42DF6B2C2CCF8A2200D7EC14 /* PermissionRequestView.swift */, + 429821122CD0DD71005ECD39 /* Bluetooth */, + ); + path = PermissionScreen; + sourceTree = ""; + }; 4251AA972C6B9D30004CCC9D /* Edit */ = { isa = PBXGroup; children = ( @@ -5033,24 +5051,22 @@ B657A8E81CA646EB00121384 /* App */ = { isa = PBXGroup; children = ( - 42B94BD92B9606CD00DEE060 /* Assist */, - 42FCCFDD2B9B1AB00057783F /* BarcodeScanner */, B657A8E91CA646EB00121384 /* AppDelegate.swift */, + 11A183B22511BCF300CA326A /* LifecycleManager.swift */, 422F951E2CFDF7C5003B7514 /* HAApplicationShortcutItem.swift */, - 42B1A7442C1305C300904548 /* WatchCommunicatorService.swift */, + 11EFCDD424F5FA7E00314D85 /* Scenes */, + 11AF1ED82528FBAA00AAE364 /* Onboarding */, + 11AD2E2A2528FDB700FBC437 /* WebView */, + 42462E6B2DA3D05500ECC8A7 /* PermissionScreen */, + 42B94BD92B9606CD00DEE060 /* Assist */, + 42FCCFDD2B9B1AB00057783F /* BarcodeScanner */, D03D893720E0AF1B00D4F28D /* ClientEvents */, - 42A47A8E2C4548BC00C9B43D /* Improv */, - 11A183B22511BCF300CA326A /* LifecycleManager.swift */, 117EB13E2569AD3000049541 /* Notifications */, - 11AF1ED82528FBAA00AAE364 /* Onboarding */, - B69933961E232AF50054453D /* Resources */, - 11EFCDD424F5FA7E00314D85 /* Scenes */, B661FB6B226BCC8500E541DD /* Settings */, 11B7ECD9274DA521009AD634 /* Servers */, - 42FCCFF72B9B1C310057783F /* Thread */, B679B1FA1E1F3D020071D366 /* Utilities */, - 11AD2E2A2528FDB700FBC437 /* WebView */, 11A71C6924A463EE00D9565F /* ZoneManager */, + B69933961E232AF50054453D /* Resources */, ); path = App; sourceTree = ""; @@ -7469,7 +7485,7 @@ 42E95C572CA45EFA0010ECE3 /* OnboardingErrorView.swift in Sources */, B641BC1F1E2097EF002CCBC1 /* AboutViewController.swift in Sources */, 420C57C72D0A6DE700D2D9AC /* NoActiveURLView.swift in Sources */, - 42E95C592CA46AD50010ECE3 /* ActivityView.swift in Sources */, + 42E95C592CA46AD50010ECE3 /* ShareActivityView.swift in Sources */, B675ECC3221BB0E600C65D31 /* SearchPushRow.swift in Sources */, 11C05F2D254919210031D038 /* AccountInitialsImage.swift in Sources */, B605C891226E9DAC00EF46DD /* Permissions.swift in Sources */, @@ -8559,7 +8575,6 @@ B657A8F51CA646EB00121384 /* Base */, ); name = LaunchScreen.storyboard; - path = .; sourceTree = ""; }; B678DB351EA9999C0045312F /* MainInterface.storyboard */ = { @@ -8609,7 +8624,6 @@ 42F1DA672B4D993B002729BC /* bg */, ); name = Localizable.strings; - path = .; sourceTree = ""; }; B6CC5D842159D10D00833E5D /* Interface.storyboard */ = { diff --git a/Sources/App/Onboarding/Screens/OnboardingErrorView.swift b/Sources/App/Onboarding/Screens/OnboardingErrorView.swift index 262f1ac7e..c16c6c9d6 100644 --- a/Sources/App/Onboarding/Screens/OnboardingErrorView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingErrorView.swift @@ -36,7 +36,7 @@ struct OnboardingErrorView: View { viewAppeared = true } .sheet(isPresented: $showShareSheet, content: { - ActivityView(activityItems: [Current.Log.archiveURL()]) + ShareActivityView(activityItems: [Current.Log.archiveURL()]) }) } diff --git a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift index 5e4a38f30..661e3d1a9 100644 --- a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift @@ -45,17 +45,16 @@ struct OnboardingWelcomeView: View { } private var logoBlock: some View { - Image(uiImage: Asset.SharedAssets.logo.image) + Image(uiImage: Asset.SharedAssets.logoHorizontalText.image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 300) .padding(.vertical, Spaces.four) } private var textBlock: some View { ScrollView { VStack(spacing: Spaces.two) { - Text(verbatim: L10n.Onboarding.Welcome.title(Current.device.systemName())) - .font(.title.bold()) - .frame(maxWidth: .infinity, alignment: .center) - .multilineTextAlignment(.center) Text(verbatim: L10n.Onboarding.Welcome.description) .foregroundStyle(Color(uiColor: .secondaryLabel)) } diff --git a/Sources/App/Onboarding/Screens/ActivityView.swift b/Sources/App/Onboarding/Screens/ShareActivityView.swift similarity index 75% rename from Sources/App/Onboarding/Screens/ActivityView.swift rename to Sources/App/Onboarding/Screens/ShareActivityView.swift index 2352ca96b..107999870 100644 --- a/Sources/App/Onboarding/Screens/ActivityView.swift +++ b/Sources/App/Onboarding/Screens/ShareActivityView.swift @@ -1,11 +1,11 @@ import SwiftUI import UIKit -struct ActivityView: UIViewControllerRepresentable { +struct ShareActivityView: UIViewControllerRepresentable { var activityItems: [Any] var applicationActivities: [UIActivity]? = nil - func makeUIViewController(context: UIViewControllerRepresentableContext) + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { let controller = UIActivityViewController( activityItems: activityItems, @@ -16,6 +16,6 @@ struct ActivityView: UIViewControllerRepresentable { func updateUIViewController( _ uiViewController: UIActivityViewController, - context: UIViewControllerRepresentableContext + context: UIViewControllerRepresentableContext ) {} } diff --git a/Sources/App/Onboarding/Screens/Bluetooth/BluetoothPermissionView.swift b/Sources/App/PermissionScreen/Bluetooth/BluetoothPermissionView.swift similarity index 100% rename from Sources/App/Onboarding/Screens/Bluetooth/BluetoothPermissionView.swift rename to Sources/App/PermissionScreen/Bluetooth/BluetoothPermissionView.swift diff --git a/Sources/App/Onboarding/Screens/Bluetooth/BluetoothPermissionViewModel.swift b/Sources/App/PermissionScreen/Bluetooth/BluetoothPermissionViewModel.swift similarity index 100% rename from Sources/App/Onboarding/Screens/Bluetooth/BluetoothPermissionViewModel.swift rename to Sources/App/PermissionScreen/Bluetooth/BluetoothPermissionViewModel.swift diff --git a/Sources/App/Onboarding/Screens/PermissionRequestView.swift b/Sources/App/PermissionScreen/PermissionRequestView.swift similarity index 100% rename from Sources/App/Onboarding/Screens/PermissionRequestView.swift rename to Sources/App/PermissionScreen/PermissionRequestView.swift diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 9d2e2d185..0245bddcd 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -485,7 +485,6 @@ Tags will work on any device with Home Assistant installed which has hardware su \ Home Assistant is free and open source home automation software with a focus on local control and privacy."; "onboarding.welcome.get_started" = "Get started with Home Assistant"; -"onboarding.welcome.title" = "Welcome to Home Assistant %@!"; "onboarding.servers.search.message" = "Looking for servers nearby..."; "onboarding.servers.docs.read" = "Read documentation"; "open_label" = "Open"; diff --git a/Sources/App/WebView/ConnectionErrorDetailsView.swift b/Sources/App/WebView/ConnectionErrorDetailsView.swift index 1fa79cf22..12cb3d7d7 100644 --- a/Sources/App/WebView/ConnectionErrorDetailsView.swift +++ b/Sources/App/WebView/ConnectionErrorDetailsView.swift @@ -110,7 +110,7 @@ struct ConnectionErrorDetailsView: View { } } .sheet(isPresented: $showExportLogsShareSheet, content: { - ActivityView(activityItems: [Current.Log.archiveURL()]) + ShareActivityView(activityItems: [Current.Log.archiveURL()]) }) } .navigationViewStyle(.stack) diff --git a/Sources/App/Improv/ImprovDiscoverView.swift b/Sources/Improv/ImprovDiscoverView.swift similarity index 100% rename from Sources/App/Improv/ImprovDiscoverView.swift rename to Sources/Improv/ImprovDiscoverView.swift diff --git a/Sources/App/Improv/Views/ImprovFailureView.swift b/Sources/Improv/Views/ImprovFailureView.swift similarity index 100% rename from Sources/App/Improv/Views/ImprovFailureView.swift rename to Sources/Improv/Views/ImprovFailureView.swift diff --git a/Sources/App/Improv/Views/ImprovSuccessView.swift b/Sources/Improv/Views/ImprovSuccessView.swift similarity index 100% rename from Sources/App/Improv/Views/ImprovSuccessView.swift rename to Sources/Improv/Views/ImprovSuccessView.swift diff --git a/Sources/Shared/Assets/Assets.swift b/Sources/Shared/Assets/Assets.swift index 6c4fed888..02502206c 100644 --- a/Sources/Shared/Assets/Assets.swift +++ b/Sources/Shared/Assets/Assets.swift @@ -34,6 +34,7 @@ public enum Asset { public static let casita = ImageAsset(name: "casita") public static let haCloudLogo = ImageAsset(name: "ha-cloud-logo") public static let improvLogo = ImageAsset(name: "improv-logo") + public static let logoHorizontalText = ImageAsset(name: "logo-horizontal-text") public static let statusItemIcon = ImageAsset(name: "statusItemIcon") public static let thread = ImageAsset(name: "thread") } diff --git a/Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/Contents.json b/Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/Contents.json new file mode 100644 index 000000000..c672f8657 --- /dev/null +++ b/Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "logo-horizontal-text.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo-horizontal-text-dark.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/logo-horizontal-text-dark.png b/Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/logo-horizontal-text-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..cef3ae43ac89554e80c0ba99492d72b2699b05d1 GIT binary patch literal 29299 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 R_5lC@002ovPDHLkV1m2q#Pt9G literal 0 HcmV?d00001 diff --git a/Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/logo-horizontal-text.png b/Sources/Shared/Assets/SharedAssets.xcassets/logo-horizontal-text.imageset/logo-horizontal-text.png new file mode 100644 index 0000000000000000000000000000000000000000..22d50d7757f0b64c7f7a6f0477dcee9bab33e300 GIT binary patch literal 29612 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

A0W6YY@O%N78=N!MEy?hYT~j;vF6kpD}zQm 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�WbQ2F%vzF&y(v7u}0V;Gh+3K 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 M07*qoM6N<$f*!0=5dZ)H literal 0 HcmV?d00001 diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index f3b693df2..9595ab017 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -1840,10 +1840,6 @@ public enum L10n { public static var description: String { return L10n.tr("Localizable", "onboarding.welcome.description") } /// Get started with Home Assistant public static var getStarted: String { return L10n.tr("Localizable", "onboarding.welcome.get_started") } - /// Welcome to Home Assistant %@! - public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "onboarding.welcome.title", String(describing: p1)) - } } } diff --git a/Sources/App/Thread/CredentialsManagement/ThreadCredentialsManagementView.swift b/Sources/Thread/CredentialsManagement/ThreadCredentialsManagementView.swift similarity index 100% rename from Sources/App/Thread/CredentialsManagement/ThreadCredentialsManagementView.swift rename to Sources/Thread/CredentialsManagement/ThreadCredentialsManagementView.swift diff --git a/Sources/App/Thread/CredentialsManagement/ThreadCredentialsManagementViewModel.swift b/Sources/Thread/CredentialsManagement/ThreadCredentialsManagementViewModel.swift similarity index 100% rename from Sources/App/Thread/CredentialsManagement/ThreadCredentialsManagementViewModel.swift rename to Sources/Thread/CredentialsManagement/ThreadCredentialsManagementViewModel.swift diff --git a/Sources/App/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift b/Sources/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift similarity index 100% rename from Sources/App/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift rename to Sources/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift diff --git a/Sources/App/Thread/CredentialsSharing/ThreadCredentialsSharing+build.swift b/Sources/Thread/CredentialsSharing/ThreadCredentialsSharing+build.swift similarity index 100% rename from Sources/App/Thread/CredentialsSharing/ThreadCredentialsSharing+build.swift rename to Sources/Thread/CredentialsSharing/ThreadCredentialsSharing+build.swift diff --git a/Sources/App/Thread/CredentialsSharing/ThreadCredentialsSharingView.swift b/Sources/Thread/CredentialsSharing/ThreadCredentialsSharingView.swift similarity index 100% rename from Sources/App/Thread/CredentialsSharing/ThreadCredentialsSharingView.swift rename to Sources/Thread/CredentialsSharing/ThreadCredentialsSharingView.swift diff --git a/Sources/App/Thread/CredentialsSharing/ThreadCredentialsSharingViewModelProtocol.swift b/Sources/Thread/CredentialsSharing/ThreadCredentialsSharingViewModelProtocol.swift similarity index 100% rename from Sources/App/Thread/CredentialsSharing/ThreadCredentialsSharingViewModelProtocol.swift rename to Sources/Thread/CredentialsSharing/ThreadCredentialsSharingViewModelProtocol.swift diff --git a/Sources/App/Thread/CredentialsSharing/ToAppleKeychain/ThreadCredentialsSharingToKeychainViewModel.swift b/Sources/Thread/CredentialsSharing/ToAppleKeychain/ThreadCredentialsSharingToKeychainViewModel.swift similarity index 100% rename from Sources/App/Thread/CredentialsSharing/ToAppleKeychain/ThreadCredentialsSharingToKeychainViewModel.swift rename to Sources/Thread/CredentialsSharing/ToAppleKeychain/ThreadCredentialsSharingToKeychainViewModel.swift diff --git a/Sources/App/Thread/CredentialsSharing/ToHomeAssistant/ThreadTransferCredentialToHAViewModel.swift b/Sources/Thread/CredentialsSharing/ToHomeAssistant/ThreadTransferCredentialToHAViewModel.swift similarity index 100% rename from Sources/App/Thread/CredentialsSharing/ToHomeAssistant/ThreadTransferCredentialToHAViewModel.swift rename to Sources/Thread/CredentialsSharing/ToHomeAssistant/ThreadTransferCredentialToHAViewModel.swift diff --git a/Sources/App/WatchCommunicatorService.swift b/Sources/Watch/WatchCommunicatorService.swift similarity index 100% rename from Sources/App/WatchCommunicatorService.swift rename to Sources/Watch/WatchCommunicatorService.swift From 2d00230259f179050a415c60865cbb740f61ae4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:33:55 +0200 Subject: [PATCH 11/21] Fix secondary server onboarding flow --- HomeAssistant.xcodeproj/project.pbxproj | 4 --- .../Container/OnboardingNavigationView.swift | 30 ++++++++++++++++++ .../OnboardingNavigationViewController.swift | 31 ------------------- .../LocationPermissionView.swift | 18 +++++++++-- .../OnboardingPermissionsNavigationView.swift | 18 +++++------ .../OnboardingServersListView.swift | 2 +- .../App/WebView/WebViewWindowController.swift | 4 +-- 7 files changed, 58 insertions(+), 49 deletions(-) delete mode 100644 Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index c3477fb58..06edd6c8e 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -946,7 +946,6 @@ 8E5FA96C740F1D671966CEA9 /* Pods-iOS-Extensions-NotificationContent-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = B613440AEDD4209862503F5D /* Pods-iOS-Extensions-NotificationContent-metadata.plist */; }; A5A3C1932BE1F4A40EA78754 /* Pods-iOS-Extensions-Matter-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 392B0C44197C98E2653932A5 /* Pods-iOS-Extensions-Matter-metadata.plist */; }; B6022213226DAC9D00E8DBFE /* ScaledFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6022212226DAC9D00E8DBFE /* ScaledFont.swift */; }; - B6022223226DBA3800E8DBFE /* OnboardingNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6022222226DBA3800E8DBFE /* OnboardingNavigationViewController.swift */; }; B60248001FBD343000998205 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B60247FE1FBD343000998205 /* InfoPlist.strings */; }; B605C891226E9DAC00EF46DD /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B605C890226E9DAC00EF46DD /* Permissions.swift */; }; B60615BB1D1F117700249C11 /* MorganFreemanSounds.csv in Resources */ = {isa = PBXBuildFile; fileRef = B60614B51D1F117700249C11 /* MorganFreemanSounds.csv */; }; @@ -2357,7 +2356,6 @@ B086E41966E89AE531E3C1A5 /* Pods-iOS-Extensions-Widgets.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-Extensions-Widgets.debug.xcconfig"; path = "Pods/Target Support Files/Pods-iOS-Extensions-Widgets/Pods-iOS-Extensions-Widgets.debug.xcconfig"; sourceTree = ""; }; B2F5238669D8A7416FBD2B55 /* Pods-iOS-Shared-iOS-Tests-Shared-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-iOS-Shared-iOS-Tests-Shared-metadata.plist"; path = "Pods/Pods-iOS-Shared-iOS-Tests-Shared-metadata.plist"; sourceTree = ""; }; B6022212226DAC9D00E8DBFE /* ScaledFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledFont.swift; sourceTree = ""; }; - B6022222226DBA3800E8DBFE /* OnboardingNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationViewController.swift; sourceTree = ""; }; B60247ED1FBD21C600998205 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; B60247FF1FBD343000998205 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; B60248011FBD349000998205 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -3124,7 +3122,6 @@ 1168BF36271811D800DD4D15 /* Container */ = { isa = PBXGroup; children = ( - B6022222226DBA3800E8DBFE /* OnboardingNavigationViewController.swift */, 427FEE552D9D39A50047C00C /* OnboardingNavigationView.swift */, 427FEE622D9EA1400047C00C /* OnboardingNavigationViewModel.swift */, ); @@ -7361,7 +7358,6 @@ 1112AEBB25F717E9007A541A /* LocationHistoryDetailViewController.swift in Sources */, 11BD7B4D25B53D7F001826F0 /* AppMacBridgeStatusItemConfiguration.swift in Sources */, 11F55EED25D3B088003977AC /* NotificationDebugNotificationsViewController.swift in Sources */, - B6022223226DBA3800E8DBFE /* OnboardingNavigationViewController.swift in Sources */, 424123882CDCEB66007EDE70 /* AreaProvider.swift in Sources */, 42F1DA6D2B4ED29C002729BC /* CarPlayPaginatedListTemplate.swift in Sources */, 11DA6B4B27137A60008ADFAF /* InputAccessoryView.swift in Sources */, diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift index f3bc17fa9..435fefad5 100644 --- a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift +++ b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift @@ -1,6 +1,36 @@ import Shared import SwiftUI +enum OnboardingStyle: Equatable { + enum RequiredType: Equatable { + case full + case permissions + } + + case initial + case required(RequiredType) + case secondary + + var insertsCancelButton: Bool { + switch self { + case .initial, .required: return false + case .secondary: return true + } + } +} + +enum OnboardingNavigation { + public static var requiredOnboardingStyle: OnboardingStyle? { + if Current.servers.all.isEmpty { + return .required(.full) + } else if !OnboardingPermissionHandler.notDeterminedPermissions.isEmpty { + return .required(.permissions) + } else { + return nil + } + } +} + struct OnboardingNavigationView: View { static func controller(onboardingStyle: OnboardingStyle) -> UIViewController { OnboardingNavigationView(onboardingStyle: onboardingStyle).embeddedInHostingController() diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift b/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift deleted file mode 100644 index cda67c8ae..000000000 --- a/Sources/App/Onboarding/Container/OnboardingNavigationViewController.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Shared - -enum OnboardingStyle: Equatable { - enum RequiredType: Equatable { - case full - case permissions - } - - case initial - case required(RequiredType) - case secondary - - var insertsCancelButton: Bool { - switch self { - case .initial, .required: return false - case .secondary: return true - } - } -} - -enum OnboardingNavigationViewController { - public static var requiredOnboardingStyle: OnboardingStyle? { - if Current.servers.all.isEmpty { - return .required(.full) - } else if !OnboardingPermissionHandler.notDeterminedPermissions.isEmpty { - return .required(.permissions) - } else { - return nil - } - } -} diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift index 4b2e16c47..77871890e 100644 --- a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift @@ -5,6 +5,7 @@ import SwiftUI struct LocationPermissionView: View { @StateObject private var viewModel = LocationPermissionViewModel() let permission: PermissionType + let completeAction: () -> Void var body: some View { VStack(spacing: Spaces.three) { @@ -25,6 +26,11 @@ struct LocationPermissionView: View { Text(verbatim: L10n.Onboarding.Permission.Location.Deny.Alert.message) } ) + .onChange(of: viewModel.shouldComplete) { newValue in + if newValue { + completeAction() + } + } } @ViewBuilder @@ -62,7 +68,9 @@ struct LocationPermissionView: View { Text(L10n.Onboarding.Permission.Location.Buttons.allowAndShare) } .buttonStyle(.primaryButton) - Button {} label: { + Button { + viewModel.requestLocationPermission() + } label: { Text(L10n.Onboarding.Permission.Location.Buttons.allowForApp) } .buttonStyle(.primaryButton) @@ -77,11 +85,12 @@ struct LocationPermissionView: View { } #Preview { - LocationPermissionView(permission: .location) + LocationPermissionView(permission: .location) {} } final class LocationPermissionViewModel: NSObject, ObservableObject { @Published var showDenyAlert: Bool = false + @Published var shouldComplete: Bool = false private let locationManager = CLLocationManager() func requestLocationPermission() { @@ -108,5 +117,10 @@ extension LocationPermissionViewModel: CLLocationManagerDelegate { @unknown default: break } + + guard manager.authorizationStatus != .notDetermined else { return } + DispatchQueue.main.async { [weak self] in + self?.shouldComplete = true + } } } diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift index 5aef2b020..9f6f1d3c0 100644 --- a/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift @@ -13,13 +13,9 @@ enum OnboardingPermissionHandler { struct OnboardingPermissionsNavigationView: View { var body: some View { - if let permission = OnboardingPermissionHandler.notDeterminedPermissions.first { - if permission == .location { - LocationPermissionView(permission: permission) - } else { - // If we endup enforcing other permissions during onboarding - // we need to handle them here - flowEnd + if let permission = OnboardingPermissionHandler.notDeterminedPermissions.first, permission == .location { + LocationPermissionView(permission: permission) { + Current.onboardingObservation.complete() } } else { flowEnd @@ -27,9 +23,13 @@ struct OnboardingPermissionsNavigationView: View { } private var flowEnd: some View { - EmptyView() + Image(systemSymbol: .checkmark) + .foregroundStyle(.green) + .font(.system(size: 100)) .onAppear { - Current.onboardingObservation.complete() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Current.onboardingObservation.complete() + } } } } diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index bd980725c..1ab180536 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -5,7 +5,7 @@ import SwiftUI struct OnboardingServersListView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject var hostingProvider: ViewControllerProvider - @ObservedObject private var viewModel = OnboardingServersListViewModel() + @StateObject private var viewModel = OnboardingServersListViewModel() @State private var showDocumentation = false @State private var showManualInput = false diff --git a/Sources/App/WebView/WebViewWindowController.swift b/Sources/App/WebView/WebViewWindowController.swift index 9b58d737f..f5f1aafe7 100644 --- a/Sources/App/WebView/WebViewWindowController.swift +++ b/Sources/App/WebView/WebViewWindowController.swift @@ -61,7 +61,7 @@ final class WebViewWindowController { } func setup() { - if let style = OnboardingNavigationViewController.requiredOnboardingStyle { + if let style = OnboardingNavigation.requiredOnboardingStyle { Current.Log.info("Showing onboarding \(style)") updateRootViewController(to: OnboardingNavigationView.controller(onboardingStyle: style)) } else { @@ -457,7 +457,7 @@ extension WebViewWindowController: OnboardingStateObserver { shouldLoadImmediately: true ) case .complete: - if window.rootViewController is OnboardingNavigationViewController { + if window.rootViewController as? UIHostingController != nil { let controller: WebViewController? if let preload = onboardingPreloadWebViewController { From 844b8c0efffab3ba4bc1075c30ca2609340b0a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:08:55 +0200 Subject: [PATCH 12/21] Set SSID when onboard new server with location permission --- .../Container/OnboardingNavigationView.swift | 2 +- .../OnboardingPermissionsNavigationView.swift | 26 ++++++++++++++----- .../OnboardingServersListView.swift | 8 ++++-- .../OnboardingServersListViewModel.swift | 2 ++ .../Environment/ConnectivityWrapper.swift | 3 ++- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift index 435fefad5..def4213c1 100644 --- a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift +++ b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift @@ -57,7 +57,7 @@ struct OnboardingNavigationView: View { case .full: OnboardingWelcomeView(shouldDismissOnboarding: $viewModel.shouldDismiss) case .permissions: - OnboardingPermissionsNavigationView() + OnboardingPermissionsNavigationView(onboardingServer: nil) } } } diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift index 9f6f1d3c0..5a8849d5a 100644 --- a/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/OnboardingPermissionsNavigationView.swift @@ -12,13 +12,27 @@ enum OnboardingPermissionHandler { } struct OnboardingPermissionsNavigationView: View { + let onboardingServer: Server? + var body: some View { - if let permission = OnboardingPermissionHandler.notDeterminedPermissions.first, permission == .location { - LocationPermissionView(permission: permission) { - Current.onboardingObservation.complete() + Group { + if let permission = OnboardingPermissionHandler.notDeterminedPermissions.first, permission == .location { + LocationPermissionView(permission: permission) { + Current.onboardingObservation.complete() + } + } else { + flowEnd + } + } + .onDisappear { + Current.connectivity.syncNetworkInformation { + if let onboardingServer, let currentSSID = Current.connectivity.currentWiFiSSID() { + // Update SSIDs if we have access to them, since we're gonna need it later + onboardingServer.info.connection.internalSSIDs = [currentSSID] + } else { + Current.Log.verbose("No onboarding server or no SSID available") + } } - } else { - flowEnd } } @@ -35,5 +49,5 @@ struct OnboardingPermissionsNavigationView: View { } #Preview { - OnboardingPermissionsNavigationView() + OnboardingPermissionsNavigationView(onboardingServer: ServerFixture.standard) } diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index 1ab180536..9b5b8ab05 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -41,8 +41,12 @@ struct OnboardingServersListView: View { .sheet(isPresented: $viewModel.showError) { errorView } - .fullScreenCover(isPresented: $viewModel.showPermissionsFlow) { - OnboardingPermissionsNavigationView() + .fullScreenCover(isPresented: .init(get: { + viewModel.showPermissionsFlow && viewModel.onboardingServer != nil + }, set: { newValue in + viewModel.showPermissionsFlow = newValue + })) { + OnboardingPermissionsNavigationView(onboardingServer: viewModel.onboardingServer) } } diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift index 76f000015..7724949ac 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift @@ -17,6 +17,7 @@ final class OnboardingServersListViewModel: ObservableObject { @Published var error: Error? @Published var showPermissionsFlow = false + @Published var onboardingServer: Server? /// Indicator for manual input loading @Published var isLoading = false @@ -104,6 +105,7 @@ final class OnboardingServersListViewModel: ObservableObject { @MainActor private func authenticationSucceeded(server: Server) { discovery.stop() + onboardingServer = server showPermissionsFlow = true } } diff --git a/Sources/Shared/Environment/ConnectivityWrapper.swift b/Sources/Shared/Environment/ConnectivityWrapper.swift index 45e4b43b9..e202c7ad1 100644 --- a/Sources/Shared/Environment/ConnectivityWrapper.swift +++ b/Sources/Shared/Environment/ConnectivityWrapper.swift @@ -107,7 +107,7 @@ public class ConnectivityWrapper { syncNetworkInformation() } - private func syncNetworkInformation() { + public func syncNetworkInformation(completion: (() -> Void)? = nil) { NEHotspotNetwork.fetchCurrent { hotspotNetwork in Current.Log .verbose( @@ -122,6 +122,7 @@ public class ConnectivityWrapper { } let bssid = hotspotNetwork?.bssid self.currentWiFiBSSID = { bssid } + completion?() } } } From 1710d913205f819a29c7bc6b9f1d94ff6f48e101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:36:48 +0200 Subject: [PATCH 13/21] Adjusts for iPad and mac --- HomeAssistant.xcodeproj/project.pbxproj | 16 ++++- .../Container/OnboardingNavigationView.swift | 1 + .../LocationPermissionView.swift | 49 +++++++++++++ .../OnboardingServersListView.swift | 68 +++++++++++-------- .../Screens/OnboardingWelcomeView.swift | 2 +- .../accentColor.colorset/Contents.json | 20 ++++++ .../Resources/en.lproj/Localizable.strings | 2 +- .../AccentColor.colorset/Contents.json | 38 ----------- .../{Sensors => }/SensorProvider.swift | 0 .../API/Webhook/Sensors/ActiveSensor.swift | 2 +- .../API/Webhook/Sensors/ActivitySensor.swift | 2 +- .../Webhook/Sensors/AppVersionSensor.swift | 2 +- .../Webhook/Sensors/AudioOutputSensor.swift | 2 +- .../Webhook/Sensors/ConnectivitySensor.swift | 9 ++- .../API/Webhook/Sensors/DisplaySensor.swift | 6 +- .../Webhook/Sensors/FrontmostAppSensor.swift | 2 +- .../API/Webhook/Sensors/GeocoderSensor.swift | 5 +- .../Sensors/InputOutputDeviceSensor.swift | 6 +- .../Webhook/Sensors/LastUpdateSensor.swift | 2 +- .../Sensors/LocationPermissionSensor.swift | 2 +- .../API/Webhook/Sensors/StorageSensor.swift | 2 +- .../Webhook/Sensors/WatchBatterySensor.swift | 4 +- .../Shared/API/Webhook/WebhookSensorId.swift | 24 +++++++ .../Shared/DesignSystem/Constants/Sizes.swift | 5 ++ .../Shared/Resources/Swiftgen/Strings.swift | 2 +- 25 files changed, 183 insertions(+), 90 deletions(-) create mode 100644 Sources/App/Resources/Assets.xcassets/accentColor.colorset/Contents.json delete mode 100644 Sources/Extensions/Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename Sources/Shared/API/Webhook/{Sensors => }/SensorProvider.swift (100%) create mode 100644 Sources/Shared/API/Webhook/WebhookSensorId.swift create mode 100644 Sources/Shared/DesignSystem/Constants/Sizes.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 06edd6c8e..ccf262bef 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -629,6 +629,11 @@ 424627332C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424627322C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift */; }; 424627342C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424627322C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift */; }; 42462E692D9ED75900ECC8A7 /* LocationPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E682D9ED75900ECC8A7 /* LocationPermissionView.swift */; }; + 42462E6D2DA4094300ECC8A7 /* WebhookSensorId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E6C2DA4094300ECC8A7 /* WebhookSensorId.swift */; }; + 42462E6E2DA4094300ECC8A7 /* WebhookSensorId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E6C2DA4094300ECC8A7 /* WebhookSensorId.swift */; }; + 42462E712DA4114800ECC8A7 /* Sizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E702DA4114800ECC8A7 /* Sizes.swift */; }; + 42462E722DA4114800ECC8A7 /* Sizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E702DA4114800ECC8A7 /* Sizes.swift */; }; + 42462E732DA4114800ECC8A7 /* Sizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E702DA4114800ECC8A7 /* Sizes.swift */; }; 424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; }; 424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; }; 424D2D102C89DACE00C610F1 /* HAAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */; }; @@ -2041,6 +2046,8 @@ 4242A2D12B2B5C9F00E9F001 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "es-MX"; path = "es-MX.lproj/AppIntentVocabulary.plist"; sourceTree = ""; }; 424627322C98D8E900EF7B43 /* WidgetBasicViewTintedWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBasicViewTintedWrapper.swift; sourceTree = ""; }; 42462E682D9ED75900ECC8A7 /* LocationPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionView.swift; sourceTree = ""; }; + 42462E6C2DA4094300ECC8A7 /* WebhookSensorId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookSensorId.swift; sourceTree = ""; }; + 42462E702DA4114800ECC8A7 /* Sizes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizes.swift; sourceTree = ""; }; 424A7F452B188946008C8DF3 /* WidgetBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBackground.swift; sourceTree = ""; }; 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetContentMargin.swift; sourceTree = ""; }; 424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HAAppEntity.swift; sourceTree = ""; }; @@ -3447,7 +3454,6 @@ 11AF4D24249D1931006C74C0 /* LastUpdateSensor.swift */, 11AF4D18249C8253006C74C0 /* PedometerSensor.swift */, 119385A3249E8E360097F497 /* StorageSensor.swift */, - 1109F81E24A1C011002590F2 /* SensorProvider.swift */, B613936824F728F8002B8C5D /* InputOutputDeviceSensor.swift */, 11358AEB24FC9F300074C4E2 /* ActiveSensor.swift */, 110ED56225A563D600489AF7 /* DisplaySensor.swift */, @@ -4258,6 +4264,7 @@ isa = PBXGroup; children = ( 428338422BA1BAFB004798C2 /* Spaces.swift */, + 42462E702DA4114800ECC8A7 /* Sizes.swift */, ); path = Constants; sourceTree = ""; @@ -5269,6 +5276,8 @@ B6E91C212232482A0014CB8D /* Webhook */ = { isa = PBXGroup; children = ( + 42462E6C2DA4094300ECC8A7 /* WebhookSensorId.swift */, + 1109F81E24A1C011002590F2 /* SensorProvider.swift */, 11AF4D0F249C7DD8006C74C0 /* Sensors */, B6A258442232485300ADD202 /* Alamofire+EncryptedResponses.swift */, 1141182824AFA0F000E6525C /* Networking */, @@ -7359,6 +7368,7 @@ 11BD7B4D25B53D7F001826F0 /* AppMacBridgeStatusItemConfiguration.swift in Sources */, 11F55EED25D3B088003977AC /* NotificationDebugNotificationsViewController.swift in Sources */, 424123882CDCEB66007EDE70 /* AreaProvider.swift in Sources */, + 42462E732DA4114800ECC8A7 /* Sizes.swift in Sources */, 42F1DA6D2B4ED29C002729BC /* CarPlayPaginatedListTemplate.swift in Sources */, 11DA6B4B27137A60008ADFAF /* InputAccessoryView.swift in Sources */, 4278CB882D01F65300CFAAC9 /* AppleLikeListTopRowHeader.swift in Sources */, @@ -7714,6 +7724,7 @@ 11B38EF5275C54A300205C7B /* GetCameraImageIntentHandler.swift in Sources */, 42CE8FB72B46D14C00C707F9 /* FrontendStrings+Values.swift in Sources */, 11521BBD25400284009C5C72 /* CrashReporter.swift in Sources */, + 42462E6E2DA4094300ECC8A7 /* WebhookSensorId.swift in Sources */, 424151FD2CD8F27100D7A6F9 /* CarPlayConfig.swift in Sources */, 113A8D4A283C7B1700B9DA32 /* PeriodicUpdateManager.swift in Sources */, 4264906C2C0F1B60002155CC /* AssistChatItem.swift in Sources */, @@ -7791,6 +7802,7 @@ 1104FD06253292CD00B8BE34 /* Guarantee+Additions.swift in Sources */, B67CE8B722200F220034C1D0 /* UIImage+Icons.swift in Sources */, 1121CD4A27128A970071C2AA /* UIView+StackView.swift in Sources */, + 42462E712DA4114800ECC8A7 /* Sizes.swift in Sources */, 11B6B593294917E800B8B552 /* MatterWrapper.swift in Sources */, 11E1639B250B1B760076D612 /* OnboardingStateObservation.swift in Sources */, 115BC8292676F44E00452430 /* FocusSensor.swift in Sources */, @@ -7909,6 +7921,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 42462E722DA4114800ECC8A7 /* Sizes.swift in Sources */, 118F046924CB895A00CBBD5C /* UIColor+CSS3+Hex.swift in Sources */, 1109F81F24A1C011002590F2 /* SensorProvider.swift in Sources */, 4254C4CA2D103ABB00245021 /* ExternalLink.swift in Sources */, @@ -8085,6 +8098,7 @@ 116570772702B0F6003906A7 /* DiskCache.swift in Sources */, 11657050270188E4003906A7 /* URLComponents+WidgetAuthenticity.swift in Sources */, B672334A225DDF410031D629 /* Event.swift in Sources */, + 42462E6D2DA4094300ECC8A7 /* WebhookSensorId.swift in Sources */, B6C091232151F90300A326DC /* LocationHistory.swift in Sources */, D0BE440A2104224600C74314 /* TokenInfo.swift in Sources */, 119EC3C724D5119300617D51 /* MobileAppConfigAction.swift in Sources */, diff --git a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift index def4213c1..1aaa1e1bd 100644 --- a/Sources/App/Onboarding/Container/OnboardingNavigationView.swift +++ b/Sources/App/Onboarding/Container/OnboardingNavigationView.swift @@ -74,6 +74,7 @@ struct OnboardingNavigationView: View { } } } + .navigationViewStyle(.stack) .onChange(of: viewModel.shouldDismiss) { newValue in if newValue { closeOnboarding() diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift index 77871890e..8bcdf3864 100644 --- a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift @@ -13,6 +13,7 @@ struct LocationPermissionView: View { Spacer() actionButtons } + .frame(maxWidth: Sizes.maxWidthForLargerScreens) .padding() .alert( L10n.Onboarding.Permission.Location.Deny.Alert.title, @@ -63,18 +64,21 @@ struct LocationPermissionView: View { private var actionButtons: some View { VStack(spacing: Spaces.one) { Button { + viewModel.enableLocationSensor() viewModel.requestLocationPermission() } label: { Text(L10n.Onboarding.Permission.Location.Buttons.allowAndShare) } .buttonStyle(.primaryButton) Button { + viewModel.disableLocationSensor() viewModel.requestLocationPermission() } label: { Text(L10n.Onboarding.Permission.Location.Buttons.allowForApp) } .buttonStyle(.primaryButton) Button { + viewModel.disableLocationSensor() viewModel.showDenyAlert = true } label: { Text(L10n.Onboarding.Permission.Location.Buttons.deny) @@ -92,11 +96,56 @@ final class LocationPermissionViewModel: NSObject, ObservableObject { @Published var showDenyAlert: Bool = false @Published var shouldComplete: Bool = false private let locationManager = CLLocationManager() + private var webhookSensors: [WebhookSensor] = [] + + private let sensorIdsToEnableDisable: [WebhookSensorId] = [ + .geocodedLocation, + .connectivityBSID, + .connectivitySSID, + ] + + override init() { + super.init() + Current.sensors.register(observer: self) + } func requestLocationPermission() { locationManager.delegate = self locationManager.requestWhenInUseAuthorization() } + + func disableLocationSensor() { + let sensorsToDisable = webhookSensors.filter { sensor in + sensorIdsToEnableDisable.map(\.rawValue).contains(sensor.UniqueID) + } + for sensor in sensorsToDisable { + Current.sensors.setEnabled(false, for: sensor) + } + } + + func enableLocationSensor() { + let sensorsToEnable = webhookSensors.filter { sensor in + sensorIdsToEnableDisable.map(\.rawValue).contains(sensor.UniqueID) + } + for sensor in sensorsToEnable { + Current.sensors.setEnabled(true, for: sensor) + } + } +} + +extension LocationPermissionViewModel: SensorObserver { + func sensorContainer( + _ container: Shared.SensorContainer, + didSignalForUpdateBecause reason: Shared.SensorContainerUpdateReason + ) { + /* no-op */ + } + + func sensorContainer(_ container: SensorContainer, didUpdate update: SensorObserverUpdate) { + update.sensors.done { [weak self] sensors in + self?.webhookSensors = sensors + } + } } extension LocationPermissionViewModel: CLLocationManagerDelegate { diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift index 9b5b8ab05..929eeed8c 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift @@ -4,6 +4,8 @@ import SwiftUI struct OnboardingServersListView: View { @Environment(\.dismiss) private var dismiss + @Environment(\.horizontalSizeClass) private var sizeClass + @EnvironmentObject var hostingProvider: ViewControllerProvider @StateObject private var viewModel = OnboardingServersListViewModel() @@ -85,6 +87,7 @@ struct OnboardingServersListView: View { Section { serverRow(instance: instance) } + .frame(minHeight: Current.isCatalyst ? 60 : nil) .listSectionSpacing(.compact) } else { serverRow(instance: instance) @@ -102,42 +105,51 @@ struct OnboardingServersListView: View { } private func serverRow(instance: DiscoveredHomeAssistant) -> some View { - OnboardingScanningInstanceRow( - name: instance.locationName, - internalURLString: instance.internalURL?.absoluteString, - externalURLString: instance.externalURL?.absoluteString, - internalOrExternalURLString: instance.internalOrExternalURL.absoluteString, - isLoading: instance == viewModel.currentlyInstanceLoading - ) - .onTapGesture { + Button(action: { viewModel.selectInstance(instance, controller: hostingProvider.viewController) - } + }, label: { + OnboardingScanningInstanceRow( + name: instance.locationName, + internalURLString: instance.internalURL?.absoluteString, + externalURLString: instance.externalURL?.absoluteString, + internalOrExternalURLString: instance.internalOrExternalURL.absoluteString, + isLoading: instance == viewModel.currentlyInstanceLoading + ) + }) + .tint(Color(uiColor: .label)) } private var bottomButtons: some View { VStack { - Button(action: { - showManualInput = true - }) { - Text(L10n.Onboarding.Scanning.manual) - } - .buttonStyle(.primaryButton) - .sheet(isPresented: $showManualInput) { - ManualURLEntryView { connectURL in - viewModel.isLoading = true - viewModel.selectInstance(.init(manualURL: connectURL), controller: hostingProvider.viewController) + VStack { + Button(action: { + showManualInput = true + }) { + Text(L10n.Onboarding.Scanning.manual) + } + .buttonStyle(.primaryButton) + .sheet(isPresented: $showManualInput) { + ManualURLEntryView { connectURL in + viewModel.isLoading = true + viewModel.selectInstance( + .init(manualURL: connectURL), + controller: hostingProvider.viewController + ) + } + } + Button(action: { + showDocumentation = true + }) { + Text(L10n.Onboarding.Servers.Docs.read) + } + .buttonStyle(.secondaryButton) + .fullScreenCover(isPresented: $showDocumentation) { + SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) } } - Button(action: { - showDocumentation = true - }) { - Text(L10n.Onboarding.Servers.Docs.read) - } - .buttonStyle(.secondaryButton) - .fullScreenCover(isPresented: $showDocumentation) { - SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted) - } + .frame(maxWidth: Sizes.maxWidthForLargerScreens) } + .frame(maxWidth: .infinity) .padding([.horizontal, .top]) .background(.ultraThinMaterial) } diff --git a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift index 661e3d1a9..c634f54ca 100644 --- a/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift @@ -25,7 +25,7 @@ struct OnboardingWelcomeView: View { .opacity(showButtons ? 1 : 0) .offset(y: buttonYOffset) } - .frame(maxWidth: 600) + .frame(maxWidth: Sizes.maxWidthForLargerScreens) .onAppear { withAnimation(.easeInOut(duration: 1.5)) { showLogo = true diff --git a/Sources/App/Resources/Assets.xcassets/accentColor.colorset/Contents.json b/Sources/App/Resources/Assets.xcassets/accentColor.colorset/Contents.json new file mode 100644 index 000000000..bac5a794b --- /dev/null +++ b/Sources/App/Resources/Assets.xcassets/accentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "extended-srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF4", + "green" : "0xA9", + "red" : "0x01" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 0245bddcd..c94d92cfe 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -1188,7 +1188,7 @@ Home Assistant is free and open source home automation software with a focus on "widgets.sensors.title" = "Sensors"; "yes_label" = "Yes"; "onboarding.permission.location.description" = "To identify if you are at home and connect locally to Home Assistant, Apple requires that we ask for your location permission."; -"onboarding.permission.location.privacy_note" = "Your location won't be shared with Home Assistant unless you select 'Allow & Share with Home Assistant'"; +"onboarding.permission.location.privacy_note" = "Your location won't be shared with your local Home Assistant server unless you select 'Allow & Share with Home Assistant'. You can choose to share later in companion app settings > sensors."; "onboarding.permission.location.buttons.allow_and_share" = "Allow & Share with Home Assistant"; "onboarding.permission.location.buttons.allow_for_app" = "Allow for App use only"; "onboarding.permission.location.buttons.deny" = "Deny"; diff --git a/Sources/Extensions/Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Sources/Extensions/Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index ae0e01f5c..000000000 --- a/Sources/Extensions/Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.960", - "green" : "0.659", - "red" : "0.010" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.960", - "green" : "0.659", - "red" : "0.010" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/Shared/API/Webhook/Sensors/SensorProvider.swift b/Sources/Shared/API/Webhook/SensorProvider.swift similarity index 100% rename from Sources/Shared/API/Webhook/Sensors/SensorProvider.swift rename to Sources/Shared/API/Webhook/SensorProvider.swift diff --git a/Sources/Shared/API/Webhook/Sensors/ActiveSensor.swift b/Sources/Shared/API/Webhook/Sensors/ActiveSensor.swift index 9efed5ec6..bdeba6244 100644 --- a/Sources/Shared/API/Webhook/Sensors/ActiveSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/ActiveSensor.swift @@ -35,7 +35,7 @@ final class ActiveSensor: SensorProvider { let sensor = WebhookSensor( name: "Active", - uniqueID: "active", + uniqueID: WebhookSensorId.active.rawValue, icon: isActive ? "mdi:monitor" : "mdi:monitor-off", state: isActive ) diff --git a/Sources/Shared/API/Webhook/Sensors/ActivitySensor.swift b/Sources/Shared/API/Webhook/Sensors/ActivitySensor.swift index 7f9cc8d2a..9ff09f29e 100644 --- a/Sources/Shared/API/Webhook/Sensors/ActivitySensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/ActivitySensor.swift @@ -18,7 +18,7 @@ public class ActivitySensor: SensorProvider { firstly { Self.latestMotionActivity() }.map { activity in - with(WebhookSensor(name: "Activity", uniqueID: "activity")) { + with(WebhookSensor(name: "Activity", uniqueID: WebhookSensorId.activity.rawValue)) { $0.State = activity.activityTypes.first $0.Attributes = [ "Confidence": activity.confidence.description, diff --git a/Sources/Shared/API/Webhook/Sensors/AppVersionSensor.swift b/Sources/Shared/API/Webhook/Sensors/AppVersionSensor.swift index da0f9279a..5ff66a855 100644 --- a/Sources/Shared/API/Webhook/Sensors/AppVersionSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/AppVersionSensor.swift @@ -10,7 +10,7 @@ final class AppVersionSensor: SensorProvider { func sensors() -> Promise<[WebhookSensor]> { let sensor = WebhookSensor( name: "App Version", - uniqueID: "app-version", + uniqueID: WebhookSensorId.appVersion.rawValue, icon: nil, state: AppConstants.version, entityCategory: "diagnostic" diff --git a/Sources/Shared/API/Webhook/Sensors/AudioOutputSensor.swift b/Sources/Shared/API/Webhook/Sensors/AudioOutputSensor.swift index f445519af..02ba69ee5 100644 --- a/Sources/Shared/API/Webhook/Sensors/AudioOutputSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/AudioOutputSensor.swift @@ -29,7 +29,7 @@ final class AudioOutputSensor: SensorProvider { let audioOutput = getAudioOutput().compactMap(\.type).joined(separator: ", ") sensors.append(.init( name: "Audio Output", - uniqueID: "iphone-audio-output", + uniqueID: WebhookSensorId.iPhoneAudioOutput.rawValue, icon: "mdi:volume-high", state: audioOutput )) diff --git a/Sources/Shared/API/Webhook/Sensors/ConnectivitySensor.swift b/Sources/Shared/API/Webhook/Sensors/ConnectivitySensor.swift index 5f68423a2..ad65d7c75 100644 --- a/Sources/Shared/API/Webhook/Sensors/ConnectivitySensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/ConnectivitySensor.swift @@ -73,7 +73,7 @@ public class ConnectivitySensor: SensorProvider { } return .value([ - with(WebhookSensor(name: "SSID", uniqueID: "connectivity_ssid")) { sensor in + with(WebhookSensor(name: "SSID", uniqueID: WebhookSensorId.connectivitySSID.rawValue)) { sensor in if let ssid = Current.connectivity.currentWiFiSSID() { sensor.State = ssid sensor.Icon = "mdi:wifi" @@ -82,7 +82,7 @@ public class ConnectivitySensor: SensorProvider { sensor.Icon = "mdi:wifi-off" } }, - with(WebhookSensor(name: "BSSID", uniqueID: "connectivity_bssid")) { sensor in + with(WebhookSensor(name: "BSSID", uniqueID: WebhookSensorId.connectivityBSID.rawValue)) { sensor in if let bssid = Current.connectivity.currentWiFiBSSID() { sensor.State = bssid sensor.Icon = "mdi:wifi-star" @@ -102,7 +102,10 @@ public class ConnectivitySensor: SensorProvider { let simple = Current.connectivity.simpleNetworkType() return .value([ - with(WebhookSensor(name: "Connection Type", uniqueID: "connectivity_connection_type")) { sensor in + with(WebhookSensor( + name: "Connection Type", + uniqueID: WebhookSensorId.connectivityConnectionType.rawValue + )) { sensor in sensor.State = simple.description sensor.Icon = simple.icon diff --git a/Sources/Shared/API/Webhook/Sensors/DisplaySensor.swift b/Sources/Shared/API/Webhook/Sensors/DisplaySensor.swift index 12c3b902d..be79f8e8b 100644 --- a/Sources/Shared/API/Webhook/Sensors/DisplaySensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/DisplaySensor.swift @@ -46,7 +46,7 @@ final class DisplaySensor: SensorProvider { sensors.append(with(WebhookSensor( name: "Displays", - uniqueID: "displays_count", + uniqueID: WebhookSensorId.displaysCount.rawValue, icon: "mdi:monitor-multiple", state: screens.count )) { @@ -58,14 +58,14 @@ final class DisplaySensor: SensorProvider { sensors.append(WebhookSensor( name: "Primary Display Name", - uniqueID: "primary_display_name", + uniqueID: WebhookSensorId.primaryDisplayName.rawValue, icon: "mdi:monitor-star", state: screens.first.map(\.name) ?? "None" )) sensors.append(WebhookSensor( name: "Primary Display ID", - uniqueID: "primary_display_id", + uniqueID: WebhookSensorId.primaryDisplayId.rawValue, icon: "mdi:monitor-star", state: screens.first.map(\.identifier) ?? "None" )) diff --git a/Sources/Shared/API/Webhook/Sensors/FrontmostAppSensor.swift b/Sources/Shared/API/Webhook/Sensors/FrontmostAppSensor.swift index 01c847605..497f8c7b0 100644 --- a/Sources/Shared/API/Webhook/Sensors/FrontmostAppSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/FrontmostAppSensor.swift @@ -43,7 +43,7 @@ final class FrontmostAppSensor: SensorProvider { sensors.append(with(WebhookSensor( name: "Frontmost App", - uniqueID: "frontmost_app", + uniqueID: WebhookSensorId.frontmostApp.rawValue, icon: "mdi:traffic-light", state: frontmost?.localizedName ?? "None" )) { diff --git a/Sources/Shared/API/Webhook/Sensors/GeocoderSensor.swift b/Sources/Shared/API/Webhook/Sensors/GeocoderSensor.swift index 7a7c0a8f3..30566cf59 100644 --- a/Sources/Shared/API/Webhook/Sensors/GeocoderSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/GeocoderSensor.swift @@ -34,7 +34,10 @@ public class GeocoderSensor: SensorProvider { guard case GeocoderError.noLocation = error, case .registration = request.reason else { throw error } return .value([]) }.map { [request] (placemarks: [CLPlacemark]) -> [WebhookSensor] in - let sensor = with(WebhookSensor(name: "Geocoded Location", uniqueID: "geocoded_location")) { + let sensor = with(WebhookSensor( + name: "Geocoded Location", + uniqueID: WebhookSensorId.geocodedLocation.rawValue + )) { $0.State = "Unknown" $0.Icon = "mdi:\(MaterialDesignIcons.mapIcon.name)" $0.Settings = [ diff --git a/Sources/Shared/API/Webhook/Sensors/InputOutputDeviceSensor.swift b/Sources/Shared/API/Webhook/Sensors/InputOutputDeviceSensor.swift index 671d343b2..a067e0d0e 100644 --- a/Sources/Shared/API/Webhook/Sensors/InputOutputDeviceSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/InputOutputDeviceSensor.swift @@ -143,21 +143,21 @@ public class InputOutputDeviceSensor: SensorProvider { return Self.sensors( name: "Camera", - uniqueID: "camera", + uniqueID: WebhookSensorId.camera.rawValue, iconOn: "mdi:camera", iconOff: "mdi:camera-off", all: cameras.map { $0.name ?? cameraFallback }, active: cameras.filter(\.isOn).map { $0.name ?? cameraFallback } ) + Self.sensors( name: "Audio Input", - uniqueID: "microphone", + uniqueID: WebhookSensorId.microphone.rawValue, iconOn: "mdi:microphone", iconOff: "mdi:microphone-off", all: audioInputs.map { $0.name ?? audioInputFallback }, active: audioInputs.filter(\.isOn).map { $0.name ?? audioInputFallback } ) + Self.sensors( name: "Audio Output", - uniqueID: "audio_output", + uniqueID: WebhookSensorId.audioOutput.rawValue, iconOn: "mdi:volume-high", iconOff: "mdi:volume-low", all: audioOutputs.map { $0.name ?? audioOutputFallback }, diff --git a/Sources/Shared/API/Webhook/Sensors/LastUpdateSensor.swift b/Sources/Shared/API/Webhook/Sensors/LastUpdateSensor.swift index bfecb97fa..16f41625e 100644 --- a/Sources/Shared/API/Webhook/Sensors/LastUpdateSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/LastUpdateSensor.swift @@ -21,7 +21,7 @@ public class LastUpdateSensor: SensorProvider { } return .value([ - with(WebhookSensor(name: "Last Update Trigger", uniqueID: "last_update_trigger")) { + with(WebhookSensor(name: "Last Update Trigger", uniqueID: WebhookSensorId.lastUpdateTrigger.rawValue)) { $0.Icon = icon $0.State = request.lastUpdateReason }, diff --git a/Sources/Shared/API/Webhook/Sensors/LocationPermissionSensor.swift b/Sources/Shared/API/Webhook/Sensors/LocationPermissionSensor.swift index 061e99cf1..247aa240a 100644 --- a/Sources/Shared/API/Webhook/Sensors/LocationPermissionSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/LocationPermissionSensor.swift @@ -9,7 +9,7 @@ public class LocationPermissionSensor: SensorProvider { } public func sensors() -> Promise<[WebhookSensor]> { - let sensor = WebhookSensor(name: "Location permission", uniqueID: "location-permission") + let sensor = WebhookSensor(name: "Location permission", uniqueID: WebhookSensorId.locationPermission.rawValue) sensor.State = CLLocationManager().authorizationStatus.description sensor.Icon = "mdi:\(MaterialDesignIcons.mapIcon.name)" return .value([sensor]) diff --git a/Sources/Shared/API/Webhook/Sensors/StorageSensor.swift b/Sources/Shared/API/Webhook/Sensors/StorageSensor.swift index d4359b3ec..45d9499c1 100644 --- a/Sources/Shared/API/Webhook/Sensors/StorageSensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/StorageSensor.swift @@ -35,7 +35,7 @@ public class StorageSensor: SensorProvider { private static func sensor(for volumes: [URLResourceKey: Int64]) throws -> WebhookSensor { let sensor = WebhookSensor( name: "Storage", - uniqueID: "storage", + uniqueID: WebhookSensorId.storage.rawValue, icon: .databaseIcon, state: "Unknown" ) diff --git a/Sources/Shared/API/Webhook/Sensors/WatchBatterySensor.swift b/Sources/Shared/API/Webhook/Sensors/WatchBatterySensor.swift index 270a092a5..63ba43626 100644 --- a/Sources/Shared/API/Webhook/Sensors/WatchBatterySensor.swift +++ b/Sources/Shared/API/Webhook/Sensors/WatchBatterySensor.swift @@ -38,7 +38,7 @@ final class WatchBatterySensor: SensorProvider { if let batteryLevel { sensors.append(WebhookSensor( name: "Watch Battery Level", - uniqueID: "watch-battery", + uniqueID: WebhookSensorId.watchBattery.rawValue, icon: icon, deviceClass: .battery, state: batteryLevel, @@ -49,7 +49,7 @@ final class WatchBatterySensor: SensorProvider { if let batteryState { sensors.append(WebhookSensor( name: "Watch Battery State", - uniqueID: "watch-battery-state", + uniqueID: WebhookSensorId.watchBatteryState.rawValue, icon: icon, state: batteryState.description )) diff --git a/Sources/Shared/API/Webhook/WebhookSensorId.swift b/Sources/Shared/API/Webhook/WebhookSensorId.swift new file mode 100644 index 000000000..590cba413 --- /dev/null +++ b/Sources/Shared/API/Webhook/WebhookSensorId.swift @@ -0,0 +1,24 @@ +import Foundation + +public enum WebhookSensorId: String { + case iPhoneAudioOutput = "iphone-audio-output" + case activity = "activity" + case connectivitySSID = "connectivity-ssid" + case connectivityBSID = "connectivity_bssid" + case connectivityConnectionType = "connectivity_connection_type" + case geocodedLocation = "geocoded_location" + case lastUpdateTrigger = "last_update_trigger" + case storage = "storage" + case camera = "camera" + case microphone = "microphone" + case audioOutput = "audio_output" + case active = "active" + case displaysCount = "displays_count" + case primaryDisplayName = "primary_display_name" + case primaryDisplayId = "primary_display_id" + case frontmostApp = "frontmost_app" + case watchBattery = "watch-battery" + case watchBatteryState = "watch-battery-state" + case appVersion = "app-version" + case locationPermission = "location-permission" +} diff --git a/Sources/Shared/DesignSystem/Constants/Sizes.swift b/Sources/Shared/DesignSystem/Constants/Sizes.swift new file mode 100644 index 000000000..882e3fd42 --- /dev/null +++ b/Sources/Shared/DesignSystem/Constants/Sizes.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum Sizes { + static var maxWidthForLargerScreens: CGFloat = 600 +} diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 9595ab017..e684895f3 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -1721,7 +1721,7 @@ public enum L10n { public enum Location { /// To identify if you are at home and connect locally to Home Assistant, Apple requires that we ask for your location permission. public static var description: String { return L10n.tr("Localizable", "onboarding.permission.location.description") } - /// Your location won't be shared with Home Assistant unless you select 'Allow & Share with Home Assistant' + /// Your location won't be shared with your local Home Assistant server unless you select 'Allow & Share with Home Assistant'. You can choose to share later in companion app settings > sensors. public static var privacyNote: String { return L10n.tr("Localizable", "onboarding.permission.location.privacy_note") } public enum Buttons { /// Allow & Share with Home Assistant From 5165e2a2623be01cf92b25c01d8b8374dcfced87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:31:00 +0200 Subject: [PATCH 14/21] Request notification permission post onboarding --- .../Resources/en.lproj/Localizable.strings | 2 + Sources/App/WebView/WebViewController.swift | 52 +++++++++++++++++++ Sources/Shared/Environment/Environment.swift | 5 ++ .../Shared/Resources/Swiftgen/Strings.swift | 11 ++++ 4 files changed, 70 insertions(+) diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index c94d92cfe..8958599a3 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -1194,3 +1194,5 @@ Home Assistant is free and open source home automation software with a focus on "onboarding.permission.location.buttons.deny" = "Deny"; "onboarding.permission.location.deny.alert.title" = "Are you sure?"; "onboarding.permission.location.deny.alert.message" = "Without location permission future versions of the App may deny access to your local Home Assistant server due to privacy concerns. If you are sure, please continue and tap 'Deny' on the next popup as well. By doing that we recommend you use your internal URL as external, since it is the only URL the app will try to access."; +"post_onboarding.permission.notification.title" = "Do you want to receive notifications?"; +"post_onboarding.permission.notification.message" = "Notifications can be useful in your automations. Tap the icon to allow or deny."; diff --git a/Sources/App/WebView/WebViewController.swift b/Sources/App/WebView/WebViewController.swift index ee13c432a..365352ccc 100644 --- a/Sources/App/WebView/WebViewController.swift +++ b/Sources/App/WebView/WebViewController.swift @@ -128,6 +128,8 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg if #available(iOS 16.4, *) { webView.isInspectable = true } + + postOnboardingNotificationPermission() } private func observeConnectionNotifications() { @@ -1358,3 +1360,53 @@ extension WebViewController { hud.hide(animated: true, afterDelay: 1.0) } } + +// MARK: - Post onboarding + +extension WebViewController { + private func postOnboardingNotificationPermission() { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + Task { + let notificationCenter = Current.userNotificationCenter + let settings = await notificationCenter.notificationSettings() + if ![.authorized, .denied].contains(settings.authorizationStatus) { + self?.showNotificationPermissionRequest() + } + } + } + } + + private func showNotificationPermissionRequest() { + let view = MessageView.viewFromNib(layout: .cardView) + var config = SwiftMessages.Config() + config.duration = .forever + config.presentationStyle = .top + config.dimMode = .gray(interactive: true) + view.configureContent( + title: L10n.PostOnboarding.Permission.Notification.title, + body: L10n.PostOnboarding.Permission.Notification.message, + iconImage: nil, + iconText: nil, + buttonImage: MaterialDesignIcons.arrowRightBoldCircleIcon.image( + ofSize: .init(width: 35, height: 35), + color: Asset.Colors.haPrimary.color + ), + buttonTitle: nil, + buttonTapHandler: { _ in + SwiftMessages.hide() + UNUserNotificationCenter.current().requestAuthorization(options: .defaultOptions) { _, error in + if let error { + Current.Log.error("Error when requesting notifications permissions: \(error)") + } + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } + } + ) + view.titleLabel?.numberOfLines = 0 + view.bodyLabel?.numberOfLines = 0 + + SwiftMessages.show(config: config, view: view) + } +} diff --git a/Sources/Shared/Environment/Environment.swift b/Sources/Shared/Environment/Environment.swift index a491300e2..102fddb0d 100644 --- a/Sources/Shared/Environment/Environment.swift +++ b/Sources/Shared/Environment/Environment.swift @@ -6,6 +6,7 @@ import GRDB import HAKit import PromiseKit import RealmSwift +import UserNotifications import Version import XCGLogger @@ -432,4 +433,8 @@ public class AppEnvironment { public var bluetoothPermissionStatus: CBManagerAuthorization { CBCentralManager.authorization } + + public var userNotificationCenter: UNUserNotificationCenter { + UNUserNotificationCenter.current() + } } diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index e684895f3..514af3224 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -1856,6 +1856,17 @@ public enum L10n { } } + public enum PostOnboarding { + public enum Permission { + public enum Notification { + /// Notifications can be useful in your automations. Tap the icon to allow or deny. + public static var message: String { return L10n.tr("Localizable", "post_onboarding.permission.notification.message") } + /// Do you want to receive notifications? + public static var title: String { return L10n.tr("Localizable", "post_onboarding.permission.notification.title") } + } + } + } + public enum Sensors { public enum Active { public enum Setting { From cdd5467f09b0de3f5c9c211bde5d3827a181f211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:56:34 +0200 Subject: [PATCH 15/21] Fix wrong ID for sensor --- Sources/Shared/API/Webhook/WebhookSensorId.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Shared/API/Webhook/WebhookSensorId.swift b/Sources/Shared/API/Webhook/WebhookSensorId.swift index 590cba413..26c1f853e 100644 --- a/Sources/Shared/API/Webhook/WebhookSensorId.swift +++ b/Sources/Shared/API/Webhook/WebhookSensorId.swift @@ -3,7 +3,7 @@ import Foundation public enum WebhookSensorId: String { case iPhoneAudioOutput = "iphone-audio-output" case activity = "activity" - case connectivitySSID = "connectivity-ssid" + case connectivitySSID = "connectivity_ssid" case connectivityBSID = "connectivity_bssid" case connectivityConnectionType = "connectivity_connection_type" case geocodedLocation = "geocoded_location" From 46b9a6f44e1816645b6fa0542799d48021720f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:44:43 +0200 Subject: [PATCH 16/21] Fix PR comments --- HomeAssistant.xcodeproj/project.pbxproj | 12 +++ Sources/App/Assist/AssistView.swift | 2 +- .../LocationPermissionView.swift | 92 ------------------- .../LocationPermissionViewModel.swift | 85 +++++++++++++++++ .../ManualURLEntry/ManualURLEntryView.swift | 1 + .../WatchConfigurationView.swift | 2 +- .../Widgets/Builder/WidgetCreationView.swift | 2 +- Sources/App/Settings/Widgets/TileCard.swift | 6 +- .../WebView/ConnectionErrorDetailsView.swift | 4 +- .../DownloadManager/DownloadManagerView.swift | 6 +- Sources/App/WebView/WebViewController.swift | 6 +- .../Extensions/Watch/Home/WatchHomeView.swift | 5 +- Sources/Improv/Views/ImprovFailureView.swift | 2 +- .../Components/ExternalLinkButton.swift | 4 +- .../Components/PrivacyNoteView.swift | 1 + .../Constants/CornerRadiusSizes.swift | 18 ++++ .../DesignSystem/Styles/HAButtonStyles.swift | 45 +++++---- .../Views/ThreadCredentialDetailsView.swift | 2 +- 18 files changed, 167 insertions(+), 128 deletions(-) create mode 100644 Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionViewModel.swift create mode 100644 Sources/Shared/DesignSystem/Constants/CornerRadiusSizes.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index ccf262bef..caa40929e 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -634,6 +634,10 @@ 42462E712DA4114800ECC8A7 /* Sizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E702DA4114800ECC8A7 /* Sizes.swift */; }; 42462E722DA4114800ECC8A7 /* Sizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E702DA4114800ECC8A7 /* Sizes.swift */; }; 42462E732DA4114800ECC8A7 /* Sizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E702DA4114800ECC8A7 /* Sizes.swift */; }; + 42462E752DA53F7200ECC8A7 /* LocationPermissionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E742DA53F7200ECC8A7 /* LocationPermissionViewModel.swift */; }; + 42462E772DA5421D00ECC8A7 /* CornerRadiusSizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E762DA5421D00ECC8A7 /* CornerRadiusSizes.swift */; }; + 42462E782DA5421D00ECC8A7 /* CornerRadiusSizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E762DA5421D00ECC8A7 /* CornerRadiusSizes.swift */; }; + 42462E792DA5421D00ECC8A7 /* CornerRadiusSizes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42462E762DA5421D00ECC8A7 /* CornerRadiusSizes.swift */; }; 424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; }; 424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; }; 424D2D102C89DACE00C610F1 /* HAAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */; }; @@ -2048,6 +2052,8 @@ 42462E682D9ED75900ECC8A7 /* LocationPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionView.swift; sourceTree = ""; }; 42462E6C2DA4094300ECC8A7 /* WebhookSensorId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookSensorId.swift; sourceTree = ""; }; 42462E702DA4114800ECC8A7 /* Sizes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizes.swift; sourceTree = ""; }; + 42462E742DA53F7200ECC8A7 /* LocationPermissionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionViewModel.swift; sourceTree = ""; }; + 42462E762DA5421D00ECC8A7 /* CornerRadiusSizes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadiusSizes.swift; sourceTree = ""; }; 424A7F452B188946008C8DF3 /* WidgetBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBackground.swift; sourceTree = ""; }; 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetContentMargin.swift; sourceTree = ""; }; 424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HAAppEntity.swift; sourceTree = ""; }; @@ -4256,6 +4262,7 @@ children = ( 427FEE652D9EBC430047C00C /* OnboardingPermissionsNavigationView.swift */, 42462E682D9ED75900ECC8A7 /* LocationPermissionView.swift */, + 42462E742DA53F7200ECC8A7 /* LocationPermissionViewModel.swift */, ); path = OnboardingPermissions; sourceTree = ""; @@ -4265,6 +4272,7 @@ children = ( 428338422BA1BAFB004798C2 /* Spaces.swift */, 42462E702DA4114800ECC8A7 /* Sizes.swift */, + 42462E762DA5421D00ECC8A7 /* CornerRadiusSizes.swift */, ); path = Constants; sourceTree = ""; @@ -7326,6 +7334,7 @@ 425573E92B58396600145217 /* HAEntity+CarPlay.swift in Sources */, B68EDD05215F12C900DD6B28 /* NotificationActionConfigurator.swift in Sources */, 423B5E0D2D677BB90000CB95 /* WidgetContentMargin.swift in Sources */, + 42462E782DA5421D00ECC8A7 /* CornerRadiusSizes.swift in Sources */, B616B299227ED68E00828165 /* Bonjour.swift in Sources */, 420E2AE62C474710004921D8 /* WidgetBasicButtonView.swift in Sources */, 11A48D7F24CA7E820021BDD9 /* Action+Observation.swift in Sources */, @@ -7402,6 +7411,7 @@ 426BB0C72D394BFF0062D905 /* AssistPipelinePicker.swift in Sources */, FD3BC66E29BA010A00B19FBE /* CarPlayDomainsListTemplate.swift in Sources */, 42D5ACD92C64C0E000D9C4E2 /* MagicItemAddView.swift in Sources */, + 42462E752DA53F7200ECC8A7 /* LocationPermissionViewModel.swift in Sources */, 425C5A072CF756DF00206B5B /* AssistMicAnimationView.swift in Sources */, 4285C5512D355F9900DADE45 /* WidgetCreationView.swift in Sources */, 1185DFAF271FF53800ED7D9A /* OnboardingAuthStepRegister.swift in Sources */, @@ -7743,6 +7753,7 @@ B6B74CBC228398DD00D58A68 /* WKInterfaceDevice+Size.swift in Sources */, 11F20BC8274C60FF00DFB163 /* PushProviderConfiguration.swift in Sources */, B658AA7F2250B2A100C9BFE3 /* MobileAppUpdateRegistrationRequest.swift in Sources */, + 42462E792DA5421D00ECC8A7 /* CornerRadiusSizes.swift in Sources */, 11A48D7C24CA7D7F0021BDD9 /* NotificationAction.swift in Sources */, B67CE8BA22200F220034C1D0 /* AppConstants.swift in Sources */, 11B6774E28303D35006E9B1A /* SecurityExceptions.swift in Sources */, @@ -8130,6 +8141,7 @@ D0EEF303214D8F0300D1D360 /* String+HA.swift in Sources */, 11B38EE6275C54A200205C7B /* CallServiceIntentHandler.swift in Sources */, 4254C4CD2D103F7B00245021 /* ExternalLinkButton.swift in Sources */, + 42462E772DA5421D00ECC8A7 /* CornerRadiusSizes.swift in Sources */, 491E98FF25D543560077BBE3 /* LogbookEntry.swift in Sources */, 3997926A2B7F904A00231B54 /* MobileAppConfigPushCategory.swift in Sources */, 42CE8FA72B45D1E900C707F9 /* CoreStrings.swift in Sources */, diff --git a/Sources/App/Assist/AssistView.swift b/Sources/App/Assist/AssistView.swift index 88b2f3421..b1d0a9842 100644 --- a/Sources/App/Assist/AssistView.swift +++ b/Sources/App/Assist/AssistView.swift @@ -173,7 +173,7 @@ struct AssistView: View { .frame(height: 45) .padding(.horizontal, viewModel.isRecording ? .zero : Spaces.two) .overlay(content: { - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: CornerRadiusSizes.one) .stroke(.gray) }) .opacity(viewModel.isRecording ? 0 : 1) diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift index 8bcdf3864..b57e6c726 100644 --- a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionView.swift @@ -1,4 +1,3 @@ -import CoreLocation import Shared import SwiftUI @@ -52,15 +51,6 @@ struct LocationPermissionView: View { .frame(maxWidth: .infinity, alignment: .center) } - private var bullets: some View { - Group { - ForEach(permission.enableBulletPoints, id: \.id) { bulletPoint in - Text(verbatim: bulletPoint.text) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - private var actionButtons: some View { VStack(spacing: Spaces.one) { Button { @@ -91,85 +81,3 @@ struct LocationPermissionView: View { #Preview { LocationPermissionView(permission: .location) {} } - -final class LocationPermissionViewModel: NSObject, ObservableObject { - @Published var showDenyAlert: Bool = false - @Published var shouldComplete: Bool = false - private let locationManager = CLLocationManager() - private var webhookSensors: [WebhookSensor] = [] - - private let sensorIdsToEnableDisable: [WebhookSensorId] = [ - .geocodedLocation, - .connectivityBSID, - .connectivitySSID, - ] - - override init() { - super.init() - Current.sensors.register(observer: self) - } - - func requestLocationPermission() { - locationManager.delegate = self - locationManager.requestWhenInUseAuthorization() - } - - func disableLocationSensor() { - let sensorsToDisable = webhookSensors.filter { sensor in - sensorIdsToEnableDisable.map(\.rawValue).contains(sensor.UniqueID) - } - for sensor in sensorsToDisable { - Current.sensors.setEnabled(false, for: sensor) - } - } - - func enableLocationSensor() { - let sensorsToEnable = webhookSensors.filter { sensor in - sensorIdsToEnableDisable.map(\.rawValue).contains(sensor.UniqueID) - } - for sensor in sensorsToEnable { - Current.sensors.setEnabled(true, for: sensor) - } - } -} - -extension LocationPermissionViewModel: SensorObserver { - func sensorContainer( - _ container: Shared.SensorContainer, - didSignalForUpdateBecause reason: Shared.SensorContainerUpdateReason - ) { - /* no-op */ - } - - func sensorContainer(_ container: SensorContainer, didUpdate update: SensorObserverUpdate) { - update.sensors.done { [weak self] sensors in - self?.webhookSensors = sensors - } - } -} - -extension LocationPermissionViewModel: CLLocationManagerDelegate { - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - switch manager.authorizationStatus { - case .notDetermined: - break - case .restricted: - break - case .denied: - break - case .authorizedAlways: - break - case .authorizedWhenInUse: - manager.requestAlwaysAuthorization() - case .authorized: - break - @unknown default: - break - } - - guard manager.authorizationStatus != .notDetermined else { return } - DispatchQueue.main.async { [weak self] in - self?.shouldComplete = true - } - } -} diff --git a/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionViewModel.swift new file mode 100644 index 000000000..3bcd5d302 --- /dev/null +++ b/Sources/App/Onboarding/Screens/OnboardingPermissions/LocationPermissionViewModel.swift @@ -0,0 +1,85 @@ +import CoreLocation +import Foundation +import Shared + +final class LocationPermissionViewModel: NSObject, ObservableObject { + @Published var showDenyAlert: Bool = false + @Published var shouldComplete: Bool = false + private let locationManager = CLLocationManager() + private var webhookSensors: [WebhookSensor] = [] + + private let sensorIdsToEnableDisable: [WebhookSensorId] = [ + .geocodedLocation, + .connectivityBSID, + .connectivitySSID, + ] + + override init() { + super.init() + Current.sensors.register(observer: self) + } + + func requestLocationPermission() { + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + } + + func disableLocationSensor() { + for sensor in locationRelatedSensors() { + Current.sensors.setEnabled(false, for: sensor) + } + } + + func enableLocationSensor() { + for sensor in locationRelatedSensors() { + Current.sensors.setEnabled(true, for: sensor) + } + } + + private func locationRelatedSensors() -> [WebhookSensor] { + webhookSensors.filter { sensor in + sensorIdsToEnableDisable.map(\.rawValue).contains(sensor.UniqueID) + } + } +} + +extension LocationPermissionViewModel: SensorObserver { + func sensorContainer( + _ container: Shared.SensorContainer, + didSignalForUpdateBecause reason: Shared.SensorContainerUpdateReason + ) { + /* no-op */ + } + + func sensorContainer(_ container: SensorContainer, didUpdate update: SensorObserverUpdate) { + update.sensors.done { [weak self] sensors in + self?.webhookSensors = sensors + } + } +} + +extension LocationPermissionViewModel: CLLocationManagerDelegate { + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + switch manager.authorizationStatus { + case .notDetermined: + break + case .restricted: + break + case .denied: + break + case .authorizedAlways: + break + case .authorizedWhenInUse: + manager.requestAlwaysAuthorization() + case .authorized: + break + @unknown default: + break + } + + guard manager.authorizationStatus != .notDetermined else { return } + DispatchQueue.main.async { [weak self] in + self?.shouldComplete = true + } + } +} diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift index c2229e85e..027241279 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/ManualURLEntry/ManualURLEntryView.swift @@ -46,6 +46,7 @@ struct ManualURLEntryView: View { } } + // View which displays helpers to add http or https to the URL @ViewBuilder private var httpOrHttpsSection: some View { let cleanedURL = urlString.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Sources/App/Settings/AppleWatch/HomeCustomization/WatchConfigurationView.swift b/Sources/App/Settings/AppleWatch/HomeCustomization/WatchConfigurationView.swift index 634053bb3..f9d262430 100644 --- a/Sources/App/Settings/AppleWatch/HomeCustomization/WatchConfigurationView.swift +++ b/Sources/App/Settings/AppleWatch/HomeCustomization/WatchConfigurationView.swift @@ -292,7 +292,7 @@ struct WatchConfigurationView: View { .font(.footnote) .padding(Spaces.one) .background(.gray.opacity(0.3)) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.one)) } } diff --git a/Sources/App/Settings/Widgets/Builder/WidgetCreationView.swift b/Sources/App/Settings/Widgets/Builder/WidgetCreationView.swift index 104f44046..49ddc103d 100644 --- a/Sources/App/Settings/Widgets/Builder/WidgetCreationView.swift +++ b/Sources/App/Settings/Widgets/Builder/WidgetCreationView.swift @@ -77,7 +77,7 @@ struct WidgetCreationView: View { } .frame(width: widthForPreview(), height: heightForPreview()) .background(Color.asset(Asset.Colors.primaryBackground)) - .clipShape(RoundedRectangle(cornerRadius: 18)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.twoAndMicro)) .shadow(color: .black.opacity(0.1), radius: 2) .padding(.vertical) Spacer() diff --git a/Sources/App/Settings/Widgets/TileCard.swift b/Sources/App/Settings/Widgets/TileCard.swift index 531b56964..ae40a7ec8 100644 --- a/Sources/App/Settings/Widgets/TileCard.swift +++ b/Sources/App/Settings/Widgets/TileCard.swift @@ -32,9 +32,9 @@ struct TileCard: View { .frame(maxWidth: .infinity) .frame(height: 55) .background(Color.asset(Asset.Colors.tileBackground)) - .clipShape(RoundedRectangle(cornerRadius: 10)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndMicro)) .overlay { - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndMicro) .stroke(Color.asset(Asset.Colors.tileBorder), lineWidth: 1) } } @@ -55,7 +55,7 @@ struct TileCard: View { } .frame(maxWidth: .infinity, alignment: .center) .background(Color.asset(Asset.Colors.primaryBackground)) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.two)) .shadow(color: .black.opacity(0.2), radius: 10) .padding() } diff --git a/Sources/App/WebView/ConnectionErrorDetailsView.swift b/Sources/App/WebView/ConnectionErrorDetailsView.swift index 12cb3d7d7..5040f58cf 100644 --- a/Sources/App/WebView/ConnectionErrorDetailsView.swift +++ b/Sources/App/WebView/ConnectionErrorDetailsView.swift @@ -43,7 +43,7 @@ struct ConnectionErrorDetailsView: View { .frame(maxWidth: 600) .padding() .background(Color(uiColor: .secondarySystemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) } if let server, server.info.connection.canUseCloud, @@ -72,7 +72,7 @@ struct ConnectionErrorDetailsView: View { .frame(maxWidth: 600) .padding() .background(Color(uiColor: .secondarySystemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) .padding(.top) Rectangle() diff --git a/Sources/App/WebView/DownloadManager/DownloadManagerView.swift b/Sources/App/WebView/DownloadManager/DownloadManagerView.swift index 39a29fa2d..c5400e9c1 100644 --- a/Sources/App/WebView/DownloadManager/DownloadManagerView.swift +++ b/Sources/App/WebView/DownloadManager/DownloadManagerView.swift @@ -90,7 +90,7 @@ struct DownloadManagerView: View { .padding() .foregroundStyle(.white) .background(Color.asset(Asset.Colors.haPrimary)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) .padding() .onAppear(perform: { shareWrapper = .init(url: url) @@ -111,7 +111,7 @@ struct DownloadManagerView: View { .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(.gray.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) .padding() } @@ -121,7 +121,7 @@ struct DownloadManagerView: View { .multilineTextAlignment(.leading) .padding() .background(.red.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) .padding() } } diff --git a/Sources/App/WebView/WebViewController.swift b/Sources/App/WebView/WebViewController.swift index 365352ccc..bd26c831d 100644 --- a/Sources/App/WebView/WebViewController.swift +++ b/Sources/App/WebView/WebViewController.swift @@ -1055,7 +1055,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg } .padding() .background(Color.asset(Asset.Colors.haPrimary).opacity(0.2)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) .padding(Spaces.one) DebugView() .toolbar { @@ -1365,7 +1365,9 @@ extension WebViewController { extension WebViewController { private func postOnboardingNotificationPermission() { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in + // 3 seconds feels a good timin to show this notification after the user has onboarded + let delayedSeconds: CGFloat = 3 + DispatchQueue.main.asyncAfter(deadline: .now() + delayedSeconds) { [weak self] in Task { let notificationCenter = Current.userNotificationCenter let settings = await notificationCenter.notificationSettings() diff --git a/Sources/Extensions/Watch/Home/WatchHomeView.swift b/Sources/Extensions/Watch/Home/WatchHomeView.swift index 71f023024..a46d5e444 100644 --- a/Sources/Extensions/Watch/Home/WatchHomeView.swift +++ b/Sources/Extensions/Watch/Home/WatchHomeView.swift @@ -88,7 +88,10 @@ struct WatchHomeView: View { if viewModel.showError { Text(viewModel.errorMessage) .font(.footnote) - .listRowBackground(Color.red.opacity(0.5).clipShape(RoundedRectangle(cornerRadius: 12))) + .listRowBackground( + Color.red.opacity(0.5) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) + ) } if viewModel.watchConfig.items.isEmpty { Text(verbatim: L10n.Watch.Labels.noConfig) diff --git a/Sources/Improv/Views/ImprovFailureView.swift b/Sources/Improv/Views/ImprovFailureView.swift index 945cdf6d7..dd1349f8b 100644 --- a/Sources/Improv/Views/ImprovFailureView.swift +++ b/Sources/Improv/Views/ImprovFailureView.swift @@ -25,7 +25,7 @@ struct ImprovFailureView: View { } .frame(maxWidth: .infinity) .background(Color(uiColor: .systemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.two)) } } diff --git a/Sources/Shared/DesignSystem/Components/ExternalLinkButton.swift b/Sources/Shared/DesignSystem/Components/ExternalLinkButton.swift index 84407056a..6b1171168 100644 --- a/Sources/Shared/DesignSystem/Components/ExternalLinkButton.swift +++ b/Sources/Shared/DesignSystem/Components/ExternalLinkButton.swift @@ -41,7 +41,7 @@ public struct ExternalLinkButton: View { .frame(maxWidth: 600) .padding() .background(background) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) } } @@ -77,7 +77,7 @@ public struct ActionLinkButton: View { .frame(maxWidth: 600) .padding() .background(Color(uiColor: .secondarySystemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndHalf)) } } diff --git a/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift b/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift index f92826132..92f61e5a6 100644 --- a/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift +++ b/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift @@ -1,5 +1,6 @@ import SwiftUI +/// View used to display and highlight privacy related information public struct PrivacyNoteView: View { @State private var background: AnyView @State private var startPoint: UnitPoint = .topLeading diff --git a/Sources/Shared/DesignSystem/Constants/CornerRadiusSizes.swift b/Sources/Shared/DesignSystem/Constants/CornerRadiusSizes.swift new file mode 100644 index 000000000..7fabe3f39 --- /dev/null +++ b/Sources/Shared/DesignSystem/Constants/CornerRadiusSizes.swift @@ -0,0 +1,18 @@ +import Foundation + +// 8-point system +public enum CornerRadiusSizes { + public static var half: CGFloat = 4 + public static var one: CGFloat = 8 + public static var oneAndHalf: CGFloat = 12 + public static var two: CGFloat = 16 + public static var three: CGFloat = 24 + public static var four: CGFloat = 32 + public static var five: CGFloat = 40 + public static var six: CGFloat = 48 + + // Out of system points + public static var micro: CGFloat = 2 + public static var oneAndMicro: CGFloat = 10 + public static var twoAndMicro: CGFloat = 18 +} diff --git a/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift b/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift index 166440d3e..40470aa91 100644 --- a/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift +++ b/Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift @@ -1,6 +1,12 @@ import Foundation import SwiftUI +enum HAButtonStylesConstants { + static var height: CGFloat = 55 + static var cornerRadius: CGFloat = 12 + static var disabledOpacity: CGFloat = 0.5 +} + public struct HAButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled: Bool @@ -9,10 +15,10 @@ public struct HAButtonStyle: ButtonStyle { .font(.callout.bold()) .foregroundColor(.white) .frame(maxWidth: .infinity) - .frame(height: 55) + .frame(height: HAButtonStylesConstants.height) .background(isEnabled ? Color.asset(Asset.Colors.haPrimary) : Color.gray) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .opacity(isEnabled ? 1 : 0.5) + .clipShape(RoundedRectangle(cornerRadius: HAButtonStylesConstants.cornerRadius)) + .opacity(isEnabled ? 1 : HAButtonStylesConstants.disabledOpacity) } } @@ -24,10 +30,10 @@ public struct HANeutralButtonStyle: ButtonStyle { .font(.callout.bold()) .foregroundColor(.white) .frame(maxWidth: .infinity) - .frame(height: 55) + .frame(height: HAButtonStylesConstants.height) .background(Color.gray) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .opacity(isEnabled ? 1 : 0.5) + .clipShape(RoundedRectangle(cornerRadius: HAButtonStylesConstants.cornerRadius)) + .opacity(isEnabled ? 1 : HAButtonStylesConstants.disabledOpacity) } } @@ -39,10 +45,10 @@ public struct HANegativeButtonStyle: ButtonStyle { .font(.callout.bold()) .foregroundColor(.white) .frame(maxWidth: .infinity) - .frame(height: 55) + .frame(height: HAButtonStylesConstants.height) .background(isEnabled ? .red : Color.gray) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .opacity(isEnabled ? 1 : 0.5) + .clipShape(RoundedRectangle(cornerRadius: HAButtonStylesConstants.cornerRadius)) + .opacity(isEnabled ? 1 : HAButtonStylesConstants.disabledOpacity) } } @@ -54,9 +60,9 @@ public struct HASecondaryButtonStyle: ButtonStyle { .font(.callout.bold()) .foregroundColor(Color.asset(Asset.Colors.haPrimary)) .frame(maxWidth: .infinity) - .frame(height: 55) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .opacity(isEnabled ? 1 : 0.5) + .frame(height: HAButtonStylesConstants.height) + .clipShape(RoundedRectangle(cornerRadius: HAButtonStylesConstants.cornerRadius)) + .opacity(isEnabled ? 1 : HAButtonStylesConstants.disabledOpacity) } } @@ -68,9 +74,9 @@ public struct HASecondaryNegativeButtonStyle: ButtonStyle { .font(.callout.bold()) .foregroundColor(.red) .frame(maxWidth: .infinity) - .frame(height: 55) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .opacity(isEnabled ? 1 : 0.5) + .frame(height: HAButtonStylesConstants.height) + .clipShape(RoundedRectangle(cornerRadius: HAButtonStylesConstants.cornerRadius)) + .opacity(isEnabled ? 1 : HAButtonStylesConstants.disabledOpacity) } } @@ -94,11 +100,14 @@ public struct HACriticalButtonStyle: ButtonStyle { .font(.callout.bold()) .foregroundColor(.black) .frame(maxWidth: .infinity) - .frame(height: 55) + .frame(height: HAButtonStylesConstants.height) .padding() .background(.red.opacity(0.5)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.red, lineWidth: 1)) + .clipShape(RoundedRectangle(cornerRadius: HAButtonStylesConstants.cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: HAButtonStylesConstants.cornerRadius).stroke( + Color.red, + lineWidth: 1 + )) } } diff --git a/Sources/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift b/Sources/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift index 6234b6ae8..721cb9f54 100644 --- a/Sources/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift +++ b/Sources/Thread/CredentialsManagement/Views/ThreadCredentialDetailsView.swift @@ -65,7 +65,7 @@ struct ThreadCredentialDetailsView: View { .frame(height: 50) .background(actionButtonbackgroundColor) .foregroundColor(.white) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.one)) .padding(.top) } From 6251cea3158cd943b44b42d93e2db845b452292e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:05:24 +0200 Subject: [PATCH 17/21] Inject Bonjour as a dependency in onboard flow --- HomeAssistant.xcodeproj/project.pbxproj | 4 ++-- Sources/App/Onboarding/API/Bonjour.swift | 9 +++++++-- .../OnboardingServersListViewModel.swift | 2 +- Sources/Shared/Environment/Environment.swift | 6 ++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index caa40929e..b41e1cfca 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -584,6 +584,7 @@ 4214388C2CF5F1D700E2D44D /* ServerFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F5CABB2B10AE1A00409816 /* ServerFixture.swift */; }; 4214388D2CF5F1D700E2D44D /* ServerFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F5CABB2B10AE1A00409816 /* ServerFixture.swift */; }; 421960702CA2AE1600F7134E /* WidgetAssistViewTintedWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4219606F2CA2AE1600F7134E /* WidgetAssistViewTintedWrapper.swift */; }; + 42196ACE2DA5A49600BD501E /* Bonjour.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B25BCC2130CAB400678C2C /* Bonjour.swift */; }; 421B1C182BD6524E001ED18C /* WidgetBuilderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C172BD6524E001ED18C /* WidgetBuilderViewModel.swift */; }; 421B1C1D2BD65C04001ED18C /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */; }; 4221ED352D009EF700BAE3EB /* AppDatabaseUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4221ED332D009BD000BAE3EB /* AppDatabaseUpdater.swift */; }; @@ -1089,7 +1090,6 @@ B60616BB1D1F117800249C11 /* US-EN-Daisy-Water-Heater-Leak.wav in Resources */ = {isa = PBXBuildFile; fileRef = B60615BA1D1F117700249C11 /* US-EN-Daisy-Water-Heater-Leak.wav */; }; B613936924F728F8002B8C5D /* InputOutputDeviceSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B613936824F728F8002B8C5D /* InputOutputDeviceSensor.swift */; }; B613936A24F728F8002B8C5D /* InputOutputDeviceSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B613936824F728F8002B8C5D /* InputOutputDeviceSensor.swift */; }; - B616B299227ED68E00828165 /* Bonjour.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B25BCC2130CAB400678C2C /* Bonjour.swift */; }; B6221F6522266F9F00502A30 /* WebhookRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6221F6122266C4000502A30 /* WebhookRequest.swift */; }; B6221F6622266FA000502A30 /* WebhookRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6221F6122266C4000502A30 /* WebhookRequest.swift */; }; B626AAF11D8F972800A0D225 /* SettingsDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626AAF01D8F972800A0D225 /* SettingsDetailViewController.swift */; }; @@ -7335,7 +7335,6 @@ B68EDD05215F12C900DD6B28 /* NotificationActionConfigurator.swift in Sources */, 423B5E0D2D677BB90000CB95 /* WidgetContentMargin.swift in Sources */, 42462E782DA5421D00ECC8A7 /* CornerRadiusSizes.swift in Sources */, - B616B299227ED68E00828165 /* Bonjour.swift in Sources */, 420E2AE62C474710004921D8 /* WidgetBasicButtonView.swift in Sources */, 11A48D7F24CA7E820021BDD9 /* Action+Observation.swift in Sources */, 11195F6B267EFB1F003DF674 /* NotificationManagerLocalPushInterface.swift in Sources */, @@ -8006,6 +8005,7 @@ 11C4629624B19FC700031902 /* URLSessionTask+WebhookPersisted.swift in Sources */, 11F2F25E25871D6000F61F7C /* NotificationAttachmentParserCamera.swift in Sources */, 11B63B0A2979A07000D908ED /* AssistIntentHandler.swift in Sources */, + 42196ACE2DA5A49600BD501E /* Bonjour.swift in Sources */, 1133F59C25F1DA5D00AD776F /* CLLocation+Sanitize.swift in Sources */, 11AF4D1C249C8AA0006C74C0 /* BatterySensor.swift in Sources */, D014EEA92128E192008EA6F5 /* ConnectionInfo.swift in Sources */, diff --git a/Sources/App/Onboarding/API/Bonjour.swift b/Sources/App/Onboarding/API/Bonjour.swift index fa7cc2aa3..fda1b00a3 100644 --- a/Sources/App/Onboarding/API/Bonjour.swift +++ b/Sources/App/Onboarding/API/Bonjour.swift @@ -1,12 +1,17 @@ import Foundation -import Shared public protocol BonjourObserver: AnyObject { func bonjour(_ bonjour: Bonjour, didAdd instance: DiscoveredHomeAssistant) func bonjour(_ bonjour: Bonjour, didRemoveInstanceWithName name: String) } -public class Bonjour: NSObject, NetServiceBrowserDelegate, NetServiceDelegate { +public protocol BonjourProtocol { + var observer: BonjourObserver? { get set } + func start() + func stop() +} + +public class Bonjour: NSObject, NetServiceBrowserDelegate, NetServiceDelegate, BonjourProtocol { public weak var observer: BonjourObserver? private var browser: NetServiceBrowser diff --git a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift index 7724949ac..9fc61e251 100644 --- a/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift +++ b/Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListViewModel.swift @@ -22,7 +22,7 @@ final class OnboardingServersListViewModel: ObservableObject { /// Indicator for manual input loading @Published var isLoading = false - private let discovery = Bonjour() + private var discovery = Current.bonjour() private var cancellables = Set() init() { diff --git a/Sources/Shared/Environment/Environment.swift b/Sources/Shared/Environment/Environment.swift index 102fddb0d..45faa2856 100644 --- a/Sources/Shared/Environment/Environment.swift +++ b/Sources/Shared/Environment/Environment.swift @@ -437,4 +437,10 @@ public class AppEnvironment { public var userNotificationCenter: UNUserNotificationCenter { UNUserNotificationCenter.current() } + + #if !os(watchOS) + public var bonjour: () -> BonjourProtocol = { + Bonjour() + } + #endif } From acdfa8312032561c8c42bb40195709a47a861e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:25:47 +0200 Subject: [PATCH 18/21] Add onboarding server list viewModel tests --- HomeAssistant.xcodeproj/project.pbxproj | 32 +++++++++ .../OnboardingServersListViewModelTests.swift | 67 +++++++++++++++++++ .../OnboardingServersListViewModelTests.swift | 67 +++++++++++++++++++ Tests/Mocks/MockBonjour.swift | 17 +++++ 4 files changed, 183 insertions(+) create mode 100644 Tests/App/Onboarding/ServersList/OnboardingServersListViewModelTests.swift create mode 100644 Tests/App/OnboardingServersListViewModelTests.swift create mode 100644 Tests/Mocks/MockBonjour.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index b41e1cfca..4dc721c07 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -585,6 +585,8 @@ 4214388D2CF5F1D700E2D44D /* ServerFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F5CABB2B10AE1A00409816 /* ServerFixture.swift */; }; 421960702CA2AE1600F7134E /* WidgetAssistViewTintedWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4219606F2CA2AE1600F7134E /* WidgetAssistViewTintedWrapper.swift */; }; 42196ACE2DA5A49600BD501E /* Bonjour.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B25BCC2130CAB400678C2C /* Bonjour.swift */; }; + 42196AD22DA5AF3D00BD501E /* MockBonjour.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42196AD02DA5AF3D00BD501E /* MockBonjour.swift */; }; + 42196ADA2DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42196AD52DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift */; }; 421B1C182BD6524E001ED18C /* WidgetBuilderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C172BD6524E001ED18C /* WidgetBuilderViewModel.swift */; }; 421B1C1D2BD65C04001ED18C /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */; }; 4221ED352D009EF700BAE3EB /* AppDatabaseUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4221ED332D009BD000BAE3EB /* AppDatabaseUpdater.swift */; }; @@ -1991,6 +1993,8 @@ 421155222D354F3F00A71630 /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; 421155242D355C6700A71630 /* WidgetBuilderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBuilderView.swift; sourceTree = ""; }; 4219606F2CA2AE1600F7134E /* WidgetAssistViewTintedWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetAssistViewTintedWrapper.swift; sourceTree = ""; }; + 42196AD02DA5AF3D00BD501E /* MockBonjour.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBonjour.swift; sourceTree = ""; }; + 42196AD52DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingServersListViewModelTests.swift; sourceTree = ""; }; 421B1C172BD6524E001ED18C /* WidgetBuilderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBuilderViewModel.swift; sourceTree = ""; }; 421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConditionalModifier.swift"; sourceTree = ""; }; 4221ED332D009BD000BAE3EB /* AppDatabaseUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDatabaseUpdater.swift; sourceTree = ""; }; @@ -2985,6 +2989,7 @@ 111501A82528414000DCFA94 /* Tests */ = { isa = PBXGroup; children = ( + 42196AD12DA5AF3D00BD501E /* Mocks */, 420E64B72D676A4200A31E86 /* Widgets */, B657A8FF1CA646EB00121384 /* App */, D03D894320E0BC1800D4F28D /* Shared */, @@ -3943,6 +3948,30 @@ path = AppIcon; sourceTree = ""; }; + 42196AD12DA5AF3D00BD501E /* Mocks */ = { + isa = PBXGroup; + children = ( + 42196AD02DA5AF3D00BD501E /* MockBonjour.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 42196AD62DA5AF7A00BD501E /* ServersList */ = { + isa = PBXGroup; + children = ( + 42196AD52DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift */, + ); + path = ServersList; + sourceTree = ""; + }; + 42196AD92DA5AF7A00BD501E /* Onboarding */ = { + isa = PBXGroup; + children = ( + 42196AD62DA5AF7A00BD501E /* ServersList */, + ); + path = Onboarding; + sourceTree = ""; + }; 421B1C142BD65238001ED18C /* Widgets */ = { isa = PBXGroup; children = ( @@ -5086,6 +5115,7 @@ B657A8FF1CA646EB00121384 /* App */ = { isa = PBXGroup; children = ( + 42196AD92DA5AF7A00BD501E /* Onboarding */, 42E3B8BB2D8ACDC100F5D084 /* Extensions */, 46F103242D7214F7002BC586 /* ClientEvents */, 422E626A2CDCF00000987BD0 /* Area */, @@ -7609,10 +7639,12 @@ 42D3E4A12C5BCD1100444BE6 /* WatchContext.test.swift in Sources */, 42A818E32BBEA9780083D045 /* MockAudioRecorder.swift in Sources */, 11ED439A27265DE800B5FD45 /* OnboardingAuthStepRegister.test.swift in Sources */, + 42196AD22DA5AF3D00BD501E /* MockBonjour.swift in Sources */, 11EFD3C027261AA4000AF78B /* OnboardingAuthStepDuplicate.test.swift in Sources */, 42A818E52BBEAA3A0083D045 /* MockAudioPlayer.swift in Sources */, 11A71C9124A598AB00D9565F /* ZoneManagerProcessor.test.swift in Sources */, 11ED439827265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift in Sources */, + 42196ADA2DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift in Sources */, 42A818E72BBEAAE80083D045 /* MockAssistService.swift in Sources */, 420E64BA2D676A5800A31E86 /* WidgetsSnapshot.test.swift in Sources */, 11EFD3C327264306000AF78B /* UIAlertAction+Additions.swift in Sources */, diff --git a/Tests/App/Onboarding/ServersList/OnboardingServersListViewModelTests.swift b/Tests/App/Onboarding/ServersList/OnboardingServersListViewModelTests.swift new file mode 100644 index 000000000..feadfcbfa --- /dev/null +++ b/Tests/App/Onboarding/ServersList/OnboardingServersListViewModelTests.swift @@ -0,0 +1,67 @@ +@testable import HomeAssistant +@testable import Shared +import Testing + +@Suite(.serialized) +struct OnboardingServersListViewModelTests { + @Test func testInitAddsSelfAsObserver() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + assert(sut.discoveredInstances.isEmpty) + assert((mockBonjour.observer as? OnboardingServersListViewModel) != nil) + } + + @Test func testStartDiscovery() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + + sut.startDiscovery() + assert(sut.discoveredInstances.isEmpty) + assert(mockBonjour.startCalled) + } + + @Test func testStopDiscovery() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + + sut.stopDiscovery() + assert(mockBonjour.stopCalled) + } + + @Test func testSelectInstance() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + let instance = DiscoveredHomeAssistant( + manualURL: URL(string: "http://192.168.0.1:8123")!, + name: "Home" + ) + let dummyController = await UIViewController() + sut.selectInstance(instance, controller: dummyController) + + assert(sut.currentlyInstanceLoading == instance) + } + + @Test func testResetFlow() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + + sut.resetFlow() + assert(sut.currentlyInstanceLoading == nil) + assert(sut.isLoading == false) + } +} diff --git a/Tests/App/OnboardingServersListViewModelTests.swift b/Tests/App/OnboardingServersListViewModelTests.swift new file mode 100644 index 000000000..feadfcbfa --- /dev/null +++ b/Tests/App/OnboardingServersListViewModelTests.swift @@ -0,0 +1,67 @@ +@testable import HomeAssistant +@testable import Shared +import Testing + +@Suite(.serialized) +struct OnboardingServersListViewModelTests { + @Test func testInitAddsSelfAsObserver() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + assert(sut.discoveredInstances.isEmpty) + assert((mockBonjour.observer as? OnboardingServersListViewModel) != nil) + } + + @Test func testStartDiscovery() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + + sut.startDiscovery() + assert(sut.discoveredInstances.isEmpty) + assert(mockBonjour.startCalled) + } + + @Test func testStopDiscovery() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + + sut.stopDiscovery() + assert(mockBonjour.stopCalled) + } + + @Test func testSelectInstance() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + let instance = DiscoveredHomeAssistant( + manualURL: URL(string: "http://192.168.0.1:8123")!, + name: "Home" + ) + let dummyController = await UIViewController() + sut.selectInstance(instance, controller: dummyController) + + assert(sut.currentlyInstanceLoading == instance) + } + + @Test func testResetFlow() async throws { + let mockBonjour = MockBonjour() + Current.bonjour = { + mockBonjour + } + let sut = OnboardingServersListViewModel() + + sut.resetFlow() + assert(sut.currentlyInstanceLoading == nil) + assert(sut.isLoading == false) + } +} diff --git a/Tests/Mocks/MockBonjour.swift b/Tests/Mocks/MockBonjour.swift new file mode 100644 index 000000000..ffe4120eb --- /dev/null +++ b/Tests/Mocks/MockBonjour.swift @@ -0,0 +1,17 @@ +import Foundation +import Shared + +final class MockBonjour: BonjourProtocol { + var observer: (any BonjourObserver)? + + var startCalled = false + var stopCalled = false + + func start() { + startCalled = true + } + + func stop() { + stopCalled = true + } +} From eb76ceb5b456bdac39561cbbf7e7dea14b863d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:53:18 +0200 Subject: [PATCH 19/21] Run swift testing in series instead of parallel due to dependency injection pattern --- .../xcshareddata/xcschemes/App-Debug.xcscheme | 6 ++++-- .../xcshareddata/xcschemes/Shared-iOS.xcscheme | 3 ++- .../xcshareddata/xcschemes/Tests-Unit.xcscheme | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/HomeAssistant.xcodeproj/xcshareddata/xcschemes/App-Debug.xcscheme b/HomeAssistant.xcodeproj/xcshareddata/xcschemes/App-Debug.xcscheme index 6d053cd02..364f54144 100644 --- a/HomeAssistant.xcodeproj/xcshareddata/xcschemes/App-Debug.xcscheme +++ b/HomeAssistant.xcodeproj/xcshareddata/xcschemes/App-Debug.xcscheme @@ -47,7 +47,8 @@ + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> + skipped = "NO" + parallelizable = "NO"> Date: Tue, 8 Apr 2025 22:53:31 +0200 Subject: [PATCH 20/21] Add onboarding navigation tests --- HomeAssistant.xcodeproj/project.pbxproj | 4 ++++ Tests/App/Onboarding/OnboardingNavigationTests.swift | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 Tests/App/Onboarding/OnboardingNavigationTests.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 4dc721c07..722607675 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -587,6 +587,7 @@ 42196ACE2DA5A49600BD501E /* Bonjour.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B25BCC2130CAB400678C2C /* Bonjour.swift */; }; 42196AD22DA5AF3D00BD501E /* MockBonjour.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42196AD02DA5AF3D00BD501E /* MockBonjour.swift */; }; 42196ADA2DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42196AD52DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift */; }; + 42196ADE2DA5B58200BD501E /* OnboardingNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42196ADD2DA5B58200BD501E /* OnboardingNavigationTests.swift */; }; 421B1C182BD6524E001ED18C /* WidgetBuilderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C172BD6524E001ED18C /* WidgetBuilderViewModel.swift */; }; 421B1C1D2BD65C04001ED18C /* View+ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */; }; 4221ED352D009EF700BAE3EB /* AppDatabaseUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4221ED332D009BD000BAE3EB /* AppDatabaseUpdater.swift */; }; @@ -1995,6 +1996,7 @@ 4219606F2CA2AE1600F7134E /* WidgetAssistViewTintedWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetAssistViewTintedWrapper.swift; sourceTree = ""; }; 42196AD02DA5AF3D00BD501E /* MockBonjour.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBonjour.swift; sourceTree = ""; }; 42196AD52DA5AF7A00BD501E /* OnboardingServersListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingServersListViewModelTests.swift; sourceTree = ""; }; + 42196ADD2DA5B58200BD501E /* OnboardingNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationTests.swift; sourceTree = ""; }; 421B1C172BD6524E001ED18C /* WidgetBuilderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBuilderViewModel.swift; sourceTree = ""; }; 421B1C1B2BD65BFA001ED18C /* View+ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConditionalModifier.swift"; sourceTree = ""; }; 4221ED332D009BD000BAE3EB /* AppDatabaseUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDatabaseUpdater.swift; sourceTree = ""; }; @@ -3967,6 +3969,7 @@ 42196AD92DA5AF7A00BD501E /* Onboarding */ = { isa = PBXGroup; children = ( + 42196ADD2DA5B58200BD501E /* OnboardingNavigationTests.swift */, 42196AD62DA5AF7A00BD501E /* ServersList */, ); path = Onboarding; @@ -7635,6 +7638,7 @@ 42E3B8C02D8ACE0400F5D084 /* OptionalsDefaultTests.swift in Sources */, 42FCD0032B9B1CB70057783F /* ThreadCredentialsSharingViewModel.test.swift in Sources */, 42FCD0042B9B1CB70057783F /* ThreadCredentialsSharing.test.swift in Sources */, + 42196ADE2DA5B58200BD501E /* OnboardingNavigationTests.swift in Sources */, 4272B9A92CDCE15C008CC262 /* CarPlayConfig.test.swift in Sources */, 42D3E4A12C5BCD1100444BE6 /* WatchContext.test.swift in Sources */, 42A818E32BBEA9780083D045 /* MockAudioRecorder.swift in Sources */, diff --git a/Tests/App/Onboarding/OnboardingNavigationTests.swift b/Tests/App/Onboarding/OnboardingNavigationTests.swift new file mode 100644 index 000000000..435e1474c --- /dev/null +++ b/Tests/App/Onboarding/OnboardingNavigationTests.swift @@ -0,0 +1,11 @@ +@testable import HomeAssistant +@testable import Shared +import Testing + +struct OnboardingNavigationTests { + @Test func testOnboardingNavigationWhenNoServers() async throws { + Current.servers = FakeServerManager(initial: 0) + let result = OnboardingNavigation.requiredOnboardingStyle + assert(result == .required(.full)) + } +} From b883d0f9ea63b0cf48ce89acea1dbd3253d1a892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Tue, 8 Apr 2025 23:35:15 +0200 Subject: [PATCH 21/21] Add more tests --- HomeAssistant.xcodeproj/project.pbxproj | 56 +++++++++++++++ .../Shared/API/Webhook/WebhookSensorId.swift | 2 +- .../Components/PrivacyNoteView.swift | 65 +++++++++++------- .../Components/CollapsibleViewTests.swift | 28 ++++++++ .../Components/ExternalLinkButtonTests.swift | 19 +++++ .../Components/HAButtonStylesTests.swift | 38 ++++++++++ .../testCollapsibleViewCollapsed.dark.png | Bin 0 -> 72547 bytes .../testCollapsibleViewCollapsed.light.png | Bin 0 -> 64855 bytes .../testCollapsibleViewOpen.dark.png | Bin 0 -> 78269 bytes .../testCollapsibleViewOpen.light.png | Bin 0 -> 69926 bytes .../testExternalLinkButton.dark.png | Bin 0 -> 75276 bytes .../testExternalLinkButton.light.png | Bin 0 -> 68630 bytes .../testAppButtonStyles.dark.png | Bin 0 -> 122525 bytes .../testAppButtonStyles.light.png | Bin 0 -> 119319 bytes Tests/App/Sizes/CornerRadiusSizesTests.swift | 19 +++++ Tests/App/Sizes/SpacesTests.swift | 16 +++++ Tests/App/Webhook/WebhookSensorIdTests.swift | 31 +++++++++ 17 files changed, 248 insertions(+), 26 deletions(-) create mode 100644 Tests/App/DesignSystem/Components/CollapsibleViewTests.swift create mode 100644 Tests/App/DesignSystem/Components/ExternalLinkButtonTests.swift create mode 100644 Tests/App/DesignSystem/Components/HAButtonStylesTests.swift create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewCollapsed.dark.png create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewCollapsed.light.png create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewOpen.dark.png create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewOpen.light.png create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/ExternalLinkButtonTests/testExternalLinkButton.dark.png create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/ExternalLinkButtonTests/testExternalLinkButton.light.png create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/HAButtonStylesTests/testAppButtonStyles.dark.png create mode 100644 Tests/App/DesignSystem/Components/__Snapshots__/HAButtonStylesTests/testAppButtonStyles.light.png create mode 100644 Tests/App/Sizes/CornerRadiusSizesTests.swift create mode 100644 Tests/App/Sizes/SpacesTests.swift create mode 100644 Tests/App/Webhook/WebhookSensorIdTests.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 722607675..f14bb3d14 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -744,6 +744,12 @@ 428338452BA1BB4F004798C2 /* Spaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428338422BA1BAFB004798C2 /* Spaces.swift */; }; 4285C5512D355F9900DADE45 /* WidgetCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5502D355F9900DADE45 /* WidgetCreationView.swift */; }; 4285C5532D35658000DADE45 /* TileCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5522D35658000DADE45 /* TileCard.swift */; }; + 428626052DA5CCAE00D58D13 /* CollapsibleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428626022DA5CCAE00D58D13 /* CollapsibleViewTests.swift */; }; + 428626062DA5CCAE00D58D13 /* ExternalLinkButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428626032DA5CCAE00D58D13 /* ExternalLinkButtonTests.swift */; }; + 428626072DA5CCAE00D58D13 /* HAButtonStylesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428626042DA5CCAE00D58D13 /* HAButtonStylesTests.swift */; }; + 4286260D2DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428626082DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift */; }; + 4286260E2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4286260B2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift */; }; + 4286260F2DA5CD1B00D58D13 /* SpacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428626092DA5CD1B00D58D13 /* SpacesTests.swift */; }; 428830EB2C6E3A8D0012373D /* WatchHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428830EA2C6E3A8D0012373D /* WatchHomeView.swift */; }; 428830ED2C6E3A9A0012373D /* WatchHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428830EC2C6E3A9A0012373D /* WatchHomeViewModel.swift */; }; 4289DDAA2C85AB4C003591C2 /* AssistAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425FF0552C8216B3000AA641 /* AssistAppIntent.swift */; }; @@ -2163,6 +2169,12 @@ 428338422BA1BAFB004798C2 /* Spaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spaces.swift; sourceTree = ""; }; 4285C5502D355F9900DADE45 /* WidgetCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetCreationView.swift; sourceTree = ""; }; 4285C5522D35658000DADE45 /* TileCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileCard.swift; sourceTree = ""; }; + 428626022DA5CCAE00D58D13 /* CollapsibleViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleViewTests.swift; sourceTree = ""; }; + 428626032DA5CCAE00D58D13 /* ExternalLinkButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLinkButtonTests.swift; sourceTree = ""; }; + 428626042DA5CCAE00D58D13 /* HAButtonStylesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HAButtonStylesTests.swift; sourceTree = ""; }; + 428626082DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadiusSizesTests.swift; sourceTree = ""; }; + 428626092DA5CD1B00D58D13 /* SpacesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesTests.swift; sourceTree = ""; }; + 4286260B2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookSensorIdTests.swift; sourceTree = ""; }; 428830EA2C6E3A8D0012373D /* WatchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeView.swift; sourceTree = ""; }; 428830EC2C6E3A9A0012373D /* WatchHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeViewModel.swift; sourceTree = ""; }; 4289DDAE2C85D5C4003591C2 /* ControlScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlScene.swift; sourceTree = ""; }; @@ -4321,6 +4333,41 @@ path = Builder; sourceTree = ""; }; + 428626002DA5CC8400D58D13 /* DesignSystem */ = { + isa = PBXGroup; + children = ( + 428626012DA5CC8B00D58D13 /* Components */, + ); + path = DesignSystem; + sourceTree = ""; + }; + 428626012DA5CC8B00D58D13 /* Components */ = { + isa = PBXGroup; + children = ( + 428626022DA5CCAE00D58D13 /* CollapsibleViewTests.swift */, + 428626032DA5CCAE00D58D13 /* ExternalLinkButtonTests.swift */, + 428626042DA5CCAE00D58D13 /* HAButtonStylesTests.swift */, + ); + path = Components; + sourceTree = ""; + }; + 4286260A2DA5CD1B00D58D13 /* Sizes */ = { + isa = PBXGroup; + children = ( + 428626082DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift */, + 428626092DA5CD1B00D58D13 /* SpacesTests.swift */, + ); + path = Sizes; + sourceTree = ""; + }; + 4286260C2DA5CD1B00D58D13 /* Webhook */ = { + isa = PBXGroup; + children = ( + 4286260B2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift */, + ); + path = Webhook; + sourceTree = ""; + }; 4289DDAC2C85D595003591C2 /* Scene */ = { isa = PBXGroup; children = ( @@ -5118,6 +5165,9 @@ B657A8FF1CA646EB00121384 /* App */ = { isa = PBXGroup; children = ( + 4286260A2DA5CD1B00D58D13 /* Sizes */, + 4286260C2DA5CD1B00D58D13 /* Webhook */, + 428626002DA5CC8400D58D13 /* DesignSystem */, 42196AD92DA5AF7A00BD501E /* Onboarding */, 42E3B8BB2D8ACDC100F5D084 /* Extensions */, 46F103242D7214F7002BC586 /* ClientEvents */, @@ -7631,6 +7681,9 @@ 11C95E3628BC20EA00171F1C /* OnboardingAuthLoginViewController.test.swift in Sources */, 42B942F82CAA1ECC00E36E02 /* PayloadConstants.test.swift in Sources */, 46C3EB1C2D721022009A893F /* Array+SafeSubscripting.test.swift in Sources */, + 4286260D2DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift in Sources */, + 4286260E2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift in Sources */, + 4286260F2DA5CD1B00D58D13 /* SpacesTests.swift in Sources */, 117D8A0A24A9381F00580913 /* UIColor+CSSRGB.test.swift in Sources */, 425573DA2B57DDE000145217 /* WindowScenesManager.test.swift in Sources */, 11ED43A027279AFA00B5FD45 /* OnboardingAuthLoginImpl.test.swift in Sources */, @@ -7641,6 +7694,9 @@ 42196ADE2DA5B58200BD501E /* OnboardingNavigationTests.swift in Sources */, 4272B9A92CDCE15C008CC262 /* CarPlayConfig.test.swift in Sources */, 42D3E4A12C5BCD1100444BE6 /* WatchContext.test.swift in Sources */, + 428626052DA5CCAE00D58D13 /* CollapsibleViewTests.swift in Sources */, + 428626062DA5CCAE00D58D13 /* ExternalLinkButtonTests.swift in Sources */, + 428626072DA5CCAE00D58D13 /* HAButtonStylesTests.swift in Sources */, 42A818E32BBEA9780083D045 /* MockAudioRecorder.swift in Sources */, 11ED439A27265DE800B5FD45 /* OnboardingAuthStepRegister.test.swift in Sources */, 42196AD22DA5AF3D00BD501E /* MockBonjour.swift in Sources */, diff --git a/Sources/Shared/API/Webhook/WebhookSensorId.swift b/Sources/Shared/API/Webhook/WebhookSensorId.swift index 26c1f853e..55cc9aaa6 100644 --- a/Sources/Shared/API/Webhook/WebhookSensorId.swift +++ b/Sources/Shared/API/Webhook/WebhookSensorId.swift @@ -1,6 +1,6 @@ import Foundation -public enum WebhookSensorId: String { +public enum WebhookSensorId: String, CaseIterable { case iPhoneAudioOutput = "iphone-audio-output" case activity = "activity" case connectivitySSID = "connectivity_ssid" diff --git a/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift b/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift index 92f61e5a6..46b42f260 100644 --- a/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift +++ b/Sources/Shared/DesignSystem/Components/PrivacyNoteView.swift @@ -2,24 +2,27 @@ import SwiftUI /// View used to display and highlight privacy related information public struct PrivacyNoteView: View { - @State private var background: AnyView @State private var startPoint: UnitPoint = .topLeading @State private var endPoint: UnitPoint = .bottomTrailing @State private var timer: Timer? + @State private var background: AnyView = .init( + LinearGradient( + colors: [.purple, .blue], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .overlay(content: { + ThickMaterialOverlay() + }) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndMicro)) + ) - let cornerRadius: CGFloat = 10 - let content: String + private let content: String + private let animating: Bool - public init(content: String) { + public init(content: String, animating: Bool = true) { self.content = content - - self.background = AnyView( - LinearGradient( - colors: [.purple, .blue], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) + self.animating = animating } public var body: some View { @@ -28,11 +31,12 @@ public struct PrivacyNoteView: View { .font(.caption.bold()) .padding(.horizontal, Spaces.one) .padding(.vertical, Spaces.half) - .background(.regularMaterial) + .background(.thickMaterial) .foregroundStyle(.gray) .clipShape(Capsule()) .frame(maxWidth: .infinity, alignment: .leading) Text(verbatim: content) + .frame(maxWidth: .infinity, alignment: .leading) .font(.caption) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) @@ -40,12 +44,14 @@ public struct PrivacyNoteView: View { } .padding(Spaces.one) .background(background) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndMicro)) .shadow(color: Color(uiColor: .label).opacity(0.2), radius: 5) .padding(.top) .onAppear { - reverse() - startTimer() + if animating { + rotareLinearBackgroundPointsForBackgroundAnimation() + startTimer() + } } .onDisappear { stopTimer() @@ -54,7 +60,7 @@ public struct PrivacyNoteView: View { private func startTimer() { timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in - reverse() + rotareLinearBackgroundPointsForBackgroundAnimation() } } @@ -63,7 +69,7 @@ public struct PrivacyNoteView: View { timer = nil } - private func reverse() { + private func rotareLinearBackgroundPointsForBackgroundAnimation() { startPoint = rotatePoint(startPoint) endPoint = rotatePoint(endPoint) withAnimation(.easeIn(duration: 2)) { @@ -74,9 +80,9 @@ public struct PrivacyNoteView: View { endPoint: endPoint ) .overlay(content: { - material + ThickMaterialOverlay() }) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .clipShape(RoundedRectangle(cornerRadius: CornerRadiusSizes.oneAndMicro)) ) } } @@ -103,8 +109,10 @@ public struct PrivacyNoteView: View { return .topLeading } } +} - private var material: some View { +struct ThickMaterialOverlay: View { + var body: some View { VStack {} .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.thickMaterial) @@ -112,8 +120,15 @@ public struct PrivacyNoteView: View { } #Preview { - PrivacyNoteView( - content: "This is a privacy note. It contains important information about your data and how it is used." - ) - .padding() + VStack { + PrivacyNoteView( + content: "This is a privacy note. It contains important information about your data and how it is used." + ) + .padding() + PrivacyNoteView( + content: "This is a privacy note. It contains important information about your data and how it is used.", + animating: false + ) + .padding() + } } diff --git a/Tests/App/DesignSystem/Components/CollapsibleViewTests.swift b/Tests/App/DesignSystem/Components/CollapsibleViewTests.swift new file mode 100644 index 000000000..99c35909c --- /dev/null +++ b/Tests/App/DesignSystem/Components/CollapsibleViewTests.swift @@ -0,0 +1,28 @@ +@testable import Shared +import SharedTesting +import SwiftUI +import Testing + +struct CollapsibleViewTests { + @MainActor + @Test func testCollapsibleViewCollapsed() async throws { + let view = CollapsibleView { + Text("This is a header") + } expandedContent: { + Text("This is a content") + } + + assertLightDarkSnapshots(of: view) + } + + @MainActor + @Test func testCollapsibleViewOpen() async throws { + let view = CollapsibleView(startExpanded: true) { + Text("This is a header") + } expandedContent: { + Text("This is a content") + } + + assertLightDarkSnapshots(of: view) + } +} diff --git a/Tests/App/DesignSystem/Components/ExternalLinkButtonTests.swift b/Tests/App/DesignSystem/Components/ExternalLinkButtonTests.swift new file mode 100644 index 000000000..5f05bf4ee --- /dev/null +++ b/Tests/App/DesignSystem/Components/ExternalLinkButtonTests.swift @@ -0,0 +1,19 @@ +@testable import Shared +import SharedTesting +import SwiftUI +import Testing + +struct ExternalLinkButtonTests { + @MainActor + @Test func testExternalLinkButton() async throws { + let view = ExternalLinkButton( + icon: Image(systemSymbol: .heart), + title: "This is a title", + url: URL(string: "https://google.com")!, + tint: .blue, + background: Color(uiColor: .secondarySystemFill) + ) + + assertLightDarkSnapshots(of: view) + } +} diff --git a/Tests/App/DesignSystem/Components/HAButtonStylesTests.swift b/Tests/App/DesignSystem/Components/HAButtonStylesTests.swift new file mode 100644 index 000000000..3ac0f67d3 --- /dev/null +++ b/Tests/App/DesignSystem/Components/HAButtonStylesTests.swift @@ -0,0 +1,38 @@ +@testable import Shared +import SharedTesting +import SwiftUI +import Testing +import WidgetKit + +struct HAButtonStylesTests { + @MainActor + @Test func testAppButtonStyles() async throws { + let listOfButtons = AnyView( + List { + VStack { + Button("primaryButton") {} + .buttonStyle(.primaryButton) + Button("secondaryButton") {} + .buttonStyle(.secondaryButton) + Button("negativeButton") {} + .buttonStyle(.negativeButton) + Button("neutralButton") {} + .buttonStyle(.neutralButton) + Button("secondaryNegativeButton") {} + .buttonStyle(.secondaryNegativeButton) + Button("linkButton") {} + .buttonStyle(.linkButton) + Button("criticalButton") {} + .buttonStyle(.criticalButton) + Button("pillButton") {} + .buttonStyle(.pillButton) + } + .padding(.horizontal) + .listRowSeparator(.hidden) + } + .listStyle(.plain) + ) + + assertLightDarkSnapshots(of: listOfButtons) + } +} diff --git a/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewCollapsed.dark.png b/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewCollapsed.dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d77c3fcf4888e0230e76b72b813f7cf4c5a21ea7 GIT binary patch literal 72547 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 literal 0 HcmV?d00001 diff --git a/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewCollapsed.light.png b/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewCollapsed.light.png new file mode 100644 index 0000000000000000000000000000000000000000..a4d38a1ead918c3a985e3f4062236e47fcf8089c GIT binary patch literal 64855 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 literal 0 HcmV?d00001 diff --git a/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewOpen.dark.png b/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewOpen.dark.png new file mode 100644 index 0000000000000000000000000000000000000000..cb24261f454b16096da73c10dd15adf6ea5b4716 GIT binary patch literal 78269 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 F{{lIFXes~z literal 0 HcmV?d00001 diff --git a/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewOpen.light.png b/Tests/App/DesignSystem/Components/__Snapshots__/CollapsibleViewTests/testCollapsibleViewOpen.light.png new file mode 100644 index 0000000000000000000000000000000000000000..419386984c86d834fbe33e4dba23d4d0e571e83a GIT binary patch literal 69926 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 literal 0 HcmV?d00001 diff --git a/Tests/App/DesignSystem/Components/__Snapshots__/ExternalLinkButtonTests/testExternalLinkButton.dark.png b/Tests/App/DesignSystem/Components/__Snapshots__/ExternalLinkButtonTests/testExternalLinkButton.dark.png new file mode 100644 index 0000000000000000000000000000000000000000..99898435df993776731e3af0fb00a2aac5dcf750 GIT binary patch literal 75276 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 Sz2pPFjE|ci%RB0D{l5UQ#)88D literal 0 HcmV?d00001 diff --git a/Tests/App/DesignSystem/Components/__Snapshots__/ExternalLinkButtonTests/testExternalLinkButton.light.png b/Tests/App/DesignSystem/Components/__Snapshots__/ExternalLinkButtonTests/testExternalLinkButton.light.png new file mode 100644 index 0000000000000000000000000000000000000000..b95393fe6bf6b0e27eef669457f17e902dcc418b GIT binary patch literal 68630 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 literal 0 HcmV?d00001 diff --git a/Tests/App/DesignSystem/Components/__Snapshots__/HAButtonStylesTests/testAppButtonStyles.dark.png b/Tests/App/DesignSystem/Components/__Snapshots__/HAButtonStylesTests/testAppButtonStyles.dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1ad86a844eb03f631a9c00c2d5ee42517f6d9d54 GIT binary patch literal 122525 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

@v-5#rp_$!Y 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˩s-UHHge^WA? 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 J8OJTJ{vQaBFN**G literal 0 HcmV?d00001 diff --git a/Tests/App/Sizes/CornerRadiusSizesTests.swift b/Tests/App/Sizes/CornerRadiusSizesTests.swift new file mode 100644 index 000000000..7eebc3e38 --- /dev/null +++ b/Tests/App/Sizes/CornerRadiusSizesTests.swift @@ -0,0 +1,19 @@ +@testable import Shared +import SharedTesting +import Testing + +struct CornerRadiusSizesTests { + @Test func testCornerRadiusSizes() async throws { + assert(CornerRadiusSizes.micro == 2) + assert(CornerRadiusSizes.half == 4) + assert(CornerRadiusSizes.one == 8) + assert(CornerRadiusSizes.oneAndHalf == 12) + assert(CornerRadiusSizes.two == 16) + assert(CornerRadiusSizes.three == 24) + assert(CornerRadiusSizes.four == 32) + assert(CornerRadiusSizes.five == 40) + assert(CornerRadiusSizes.six == 48) + assert(CornerRadiusSizes.oneAndMicro == 10) + assert(CornerRadiusSizes.twoAndMicro == 18) + } +} diff --git a/Tests/App/Sizes/SpacesTests.swift b/Tests/App/Sizes/SpacesTests.swift new file mode 100644 index 000000000..054e7a1e8 --- /dev/null +++ b/Tests/App/Sizes/SpacesTests.swift @@ -0,0 +1,16 @@ +@testable import Shared +import SharedTesting +import Testing + +struct SpacesTests { + @Test func testSpacesSizes() async throws { + assert(Spaces.half == 4) + assert(Spaces.one == 8) + assert(Spaces.oneAndHalf == 12) + assert(Spaces.two == 16) + assert(Spaces.three == 24) + assert(Spaces.four == 32) + assert(Spaces.five == 40) + assert(Spaces.six == 48) + } +} diff --git a/Tests/App/Webhook/WebhookSensorIdTests.swift b/Tests/App/Webhook/WebhookSensorIdTests.swift new file mode 100644 index 000000000..78c9ffdc1 --- /dev/null +++ b/Tests/App/Webhook/WebhookSensorIdTests.swift @@ -0,0 +1,31 @@ +@testable import Shared +import Testing + +struct WebhookSensorIdTests { + @Test func testWebhookSensorIdRawValues() async throws { + assert(WebhookSensorId.iPhoneAudioOutput.rawValue == "iphone-audio-output") + assert(WebhookSensorId.activity.rawValue == "activity") + assert(WebhookSensorId.connectivitySSID.rawValue == "connectivity_ssid") + assert(WebhookSensorId.connectivityBSID.rawValue == "connectivity_bssid") + assert(WebhookSensorId.connectivityConnectionType.rawValue == "connectivity_connection_type") + assert(WebhookSensorId.geocodedLocation.rawValue == "geocoded_location") + assert(WebhookSensorId.lastUpdateTrigger.rawValue == "last_update_trigger") + assert(WebhookSensorId.storage.rawValue == "storage") + assert(WebhookSensorId.camera.rawValue == "camera") + assert(WebhookSensorId.microphone.rawValue == "microphone") + assert(WebhookSensorId.audioOutput.rawValue == "audio_output") + assert(WebhookSensorId.active.rawValue == "active") + assert(WebhookSensorId.displaysCount.rawValue == "displays_count") + assert(WebhookSensorId.primaryDisplayName.rawValue == "primary_display_name") + assert(WebhookSensorId.primaryDisplayId.rawValue == "primary_display_id") + assert(WebhookSensorId.frontmostApp.rawValue == "frontmost_app") + assert(WebhookSensorId.watchBattery.rawValue == "watch-battery") + assert(WebhookSensorId.watchBatteryState.rawValue == "watch-battery-state") + assert(WebhookSensorId.appVersion.rawValue == "app-version") + assert(WebhookSensorId.locationPermission.rawValue == "location-permission") + assert( + WebhookSensorId.allCases.count == 20, + "WebhookSensorId has different number of cases than defined in test, \(WebhookSensorId.allCases.count)" + ) + } +}