Skip to content

Commit

Permalink
Warning when internal URL requires more permissions before it can be …
Browse files Browse the repository at this point in the history
…used (#3267)

<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

https://github.com/user-attachments/assets/3dcdf423-c302-4e30-84e3-433bba03bfa9

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->
  • Loading branch information
bgoncal authored Dec 12, 2024
1 parent 9eb6f4f commit a043a51
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 22 deletions.
4 changes: 4 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
420B100C2B1D204400D383D8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 420B100B2B1D204400D383D8 /* Assets.xcassets */; };
420C1BB22CF7DA9100AF22E7 /* ClientEventsLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420C1BB12CF7DA9100AF22E7 /* ClientEventsLogView.swift */; };
420C1BB52CF7DC1400AF22E7 /* ClientEventsLogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420C1BB42CF7DC1400AF22E7 /* ClientEventsLogViewModel.swift */; };
420C57C72D0A6DE700D2D9AC /* NoActiveURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420C57C62D0A6DE700D2D9AC /* NoActiveURLView.swift */; };
420D5AE32C5A860900624A08 /* LocationPermissionSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420D5AE22C5A860900624A08 /* LocationPermissionSensor.swift */; };
420D5AE42C5A860900624A08 /* LocationPermissionSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420D5AE22C5A860900624A08 /* LocationPermissionSensor.swift */; };
420E2AE32C4746BB004921D8 /* WidgetBasicViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420E2AE22C4746BB004921D8 /* WidgetBasicViewModel.swift */; };
Expand Down Expand Up @@ -1832,6 +1833,7 @@
420B100B2B1D204400D383D8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
420C1BB12CF7DA9100AF22E7 /* ClientEventsLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientEventsLogView.swift; sourceTree = "<group>"; };
420C1BB42CF7DC1400AF22E7 /* ClientEventsLogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientEventsLogViewModel.swift; sourceTree = "<group>"; };
420C57C62D0A6DE700D2D9AC /* NoActiveURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoActiveURLView.swift; sourceTree = "<group>"; };
420D5AE22C5A860900624A08 /* LocationPermissionSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionSensor.swift; sourceTree = "<group>"; };
420E2AE22C4746BB004921D8 /* WidgetBasicViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBasicViewModel.swift; sourceTree = "<group>"; };
420E2AE42C4746CD004921D8 /* WidgetBasicSizeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBasicSizeStyle.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3105,6 +3107,7 @@
113FB1122515A065000AC680 /* ScaleFactorMutator.swift */,
11DE822D24FAC51000E636B8 /* IncomingURLHandler.swift */,
B64BB3A71E9C6551001E8B46 /* WebViewController.swift */,
420C57C62D0A6DE700D2D9AC /* NoActiveURLView.swift */,
42A47A842C45218D00C9B43D /* WebViewExternalMessageHandler.swift */,
42A47A8B2C4547B800C9B43D /* WebViewExternalMessageHandler+Build.swift */,
42B95B512BE007E30070F2D4 /* SafeScriptMessageHandler.swift */,
Expand Down Expand Up @@ -6924,6 +6927,7 @@
42E6C08A2CE4F4FA007CA622 /* DownloadManagerView.swift in Sources */,
42E95C572CA45EFA0010ECE3 /* OnboardingErrorView.swift in Sources */,
B641BC1F1E2097EF002CCBC1 /* AboutViewController.swift in Sources */,
420C57C72D0A6DE700D2D9AC /* NoActiveURLView.swift in Sources */,
42E95C592CA46AD50010ECE3 /* ActivityView.swift in Sources */,
B675ECC3221BB0E600C65D31 /* SearchPushRow.swift in Sources */,
11C05F2D254919210031D038 /* AccountInitialsImage.swift in Sources */,
Expand Down
9 changes: 8 additions & 1 deletion Sources/App/Onboarding/API/OnboardingAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ class OnboardingAuth {
// not super necessary but prevents making a duplicate connection during this session
Current.cachedApis[api.server.identifier] = api
}.then { server in
steps(.complete).map { server }
server.update { info in
// Disable fallback to internal URL after onboarding
info.connection.alwaysFallbackToInternalURL = false
}
return steps(.complete).map { server }
}.recover(policy: .allErrors) { [self] error -> Promise<Server> in
when(resolved: undoConfigure(api: api)).then { _ in Promise<Server>(error: error) }
}
Expand Down Expand Up @@ -156,6 +160,9 @@ class OnboardingAuth {

var connectionInfo = ConnectionInfo(discovered: instance, authDetails: authDetails)

// During onboarding we need at least one URL available, this is disabled at the end of onboarding
connectionInfo.alwaysFallbackToInternalURL = true

return tokenExchange.tokenInfo(
code: code,
connectionInfo: &connectionInfo
Expand Down
8 changes: 8 additions & 0 deletions Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1087,3 +1087,11 @@ 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";
"connection.permission.internal_url.title" = "Permission access";
"connection.permission.internal_url.body1" = "To access Home Assistant locally in a secure way, you need to grant the location permission ('Always').";
"connection.permission.internal_url.body2" = "This permission allows Home Assistant to detect the wireless network that you're connected to and establish a local connection.";
"connection.permission.internal_url.body3" = "You are always in control if your location is shared with Home Assistant. You can change these settings in the companion app setting screen.";
"connection.permission.internal_url.button_configure" = "Configure local access";
"connection.permission.internal_url.button_ignore" = "I know what I am doing. Allow local connections without permission access.";
"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?";
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ final class ConnectionURLViewController: HAFormViewController, TypedRowControlle
let alert = UIAlertController(
title: L10n.Settings.ConnectionSection.AlwaysFallbackInternal.Confirmation.title,
message: L10n.Settings.ConnectionSection.AlwaysFallbackInternal.Confirmation.message,
preferredStyle: .actionSheet
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: L10n.cancelLabel, style: .cancel, handler: { _ in
self?.server.info.connection.alwaysFallbackToInternalURL = false
Expand Down
174 changes: 174 additions & 0 deletions Sources/App/WebView/NoActiveURLView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import SFSafeSymbols
import Shared
import SwiftUI

struct NoActiveURLView: View {
@Environment(\.dismiss) private var dismiss
let server: Server

@State private var showIgnoreConfirmation = false

var body: some View {
ScrollView {
VStack {
VStack {
header
Image(imageAsset: Asset.SharedAssets.logo)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 140)

textBlock
configureButton
}
.padding()
footer
}
}
.ignoresSafeArea(edges: .bottom)
.onDisappear {
Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise)
.done { webViewController in
webViewController.overlayAppController = nil
}
}
.alert(L10n.Connection.Permission.InternalUrl.Ignore.Alert.title, isPresented: $showIgnoreConfirmation) {
Button(L10n.yesLabel, role: .destructive) {
ignore()
}
}
}

private func ignore() {
server.update { info in
info.connection.alwaysFallbackToInternalURL = true

Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise)
.done { webViewController in
dismiss()
webViewController.reload()
}
}
}

private var configureButton: some View {
Button(L10n.Connection.Permission.InternalUrl.buttonConfigure) {
Current.Log.info("Tapped configure local access button in NoActiveURLView")
configure()
}
.buttonStyle(.primaryButton)
.padding(.vertical)
}

private func configure() {
Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise)
.done { webViewController in
let controller = ConnectionURLViewController(
server: server,
urlType: .internal,
row: .init(tag: "")
)
let navController = UINavigationController(rootViewController: controller)
controller.onDismissCallback = { _ in
navController.dismiss(animated: true) {
webViewController.reload()
}
}
webViewController.presentOverlayController(controller: navController, animated: true)
}
}

private var header: some View {
HStack {
Group {
Button {
Current.Log.info("Tapped settings button in NoActiveURLView")
showSettings()
} label: {
Image(systemSymbol: .gear)
}
Spacer()
Button {
Current.Log.info("Dismissed NoActiveURLView")
dismiss()
} label: {
Image(systemSymbol: .xmark)
}
}
.font(.title2)
.foregroundStyle(Color(uiColor: .secondaryLabel))
}
}

private func showSettings() {
Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise)
.done { webViewController in
webViewController.showSettingsViewController()
}
}

@ViewBuilder
private var textBlock: some View {
Text(L10n.Connection.Permission.InternalUrl.title)
.font(.title.bold())
.padding(.vertical)
VStack(spacing: Spaces.two) {
Group {
makeRow(icon: .map, text: L10n.Connection.Permission.InternalUrl.body1)
makeRow(icon: .wifi, text: L10n.Connection.Permission.InternalUrl.body2)
makeRow(icon: .lock, text: L10n.Connection.Permission.InternalUrl.body3)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}

private func makeRow(icon: SFSymbol, text: String) -> some View {
HStack(spacing: Spaces.two) {
VStack {
Image(systemSymbol: icon)
.font(.title)
.foregroundStyle(Color(uiColor: Asset.Colors.haPrimary.color))
}
.frame(width: 30, height: 30)
Text(text)
.font(.body)
}
}

private var footer: some View {
VStack {
Text(
L10n.Connection.Permission.InternalUrl.footer
)
.font(.footnote)
.multilineTextAlignment(.center)
Button(L10n.Connection.Permission.InternalUrl.buttonIgnore) {
showIgnoreConfirmation = true
}
.buttonStyle(.criticalButton)
.padding(.vertical)
}
.padding()
.padding(.vertical)
.background(Color(uiColor: .secondarySystemBackground))
}
}

#Preview {
VStack {}
.sheet(isPresented: .constant(true)) {
NoActiveURLView(server: ServerFixture.standard)
}
}

final class NoActiveURLViewController: UIHostingController<NoActiveURLView> {
init(server: Server) {
super.init(rootView: NoActiveURLView(server: server))
}

@available(*, unavailable)
@MainActor @preconcurrency dynamic required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
24 changes: 4 additions & 20 deletions Sources/App/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -601,26 +601,9 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg

private func showNoActiveURLError() {
Current.Log.info("Showing noActiveURLError")
var config = swiftMessagesConfig()
config.duration = .seconds(seconds: 15)
let view = MessageView.viewFromNib(layout: .messageView)
view.configureContent(
title: L10n.Network.Error.NoActiveUrl.title,
body: L10n.Network.Error.NoActiveUrl.body,
iconImage: nil,
iconText: nil,
buttonImage: MaterialDesignIcons.cogIcon.image(
ofSize: CGSize(width: 30, height: 30),
color: Asset.Colors.haPrimary.color
),
buttonTitle: nil,
buttonTapHandler: { [weak self] _ in
self?.showSettingsViewController()
SwiftMessages.hide()
}
)

SwiftMessages.show(config: config, view: view)
webView.scrollView.refreshControl?.endRefreshing()
guard !(overlayAppController is NoActiveURLViewController) else { return }
presentController(NoActiveURLViewController(server: server), animated: true)
}

@objc private func connectionInfoDidChange() {
Expand Down Expand Up @@ -1103,6 +1086,7 @@ extension WebViewController: WebViewControllerProtocol {
if let overlayAppController {
overlayAppController.dismiss(animated: false)
}
overlayAppController = controller
present(controller, animated: animated)
}
}
Expand Down
21 changes: 21 additions & 0 deletions Sources/Shared/DesignSystem/Styles/HAButtonStyles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ public struct HASecondaryButtonStyle: ButtonStyle {
}
}

public struct HACriticalButtonStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.multilineTextAlignment(.center)
.font(.callout.bold())
.foregroundColor(.black)
.frame(maxWidth: .infinity)
.frame(height: 55)
.padding()
.background(.red.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.red, lineWidth: 1))
}
}

public struct HALinkButtonStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
configuration.label
Expand All @@ -50,3 +65,9 @@ public extension ButtonStyle where Self == HALinkButtonStyle {
HALinkButtonStyle()
}
}

public extension ButtonStyle where Self == HACriticalButtonStyle {
static var criticalButton: HACriticalButtonStyle {
HACriticalButtonStyle()
}
}
27 changes: 27 additions & 0 deletions Sources/Shared/Resources/Swiftgen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,33 @@ public enum L10n {
}
}

public enum Connection {
public enum Permission {
public enum InternalUrl {
/// To access Home Assistant locally in a secure way, you need to grant the location permission ('Always').
public static var body1: String { return L10n.tr("Localizable", "connection.permission.internal_url.body1") }
/// This permission allows Home Assistant to detect the wireless network that you're connected to and establish a local connection.
public static var body2: String { return L10n.tr("Localizable", "connection.permission.internal_url.body2") }
/// You are always in control if your location is shared with Home Assistant. You can change these settings in the companion app setting screen.
public static var body3: String { return L10n.tr("Localizable", "connection.permission.internal_url.body3") }
/// Configure local access
public static var buttonConfigure: String { return L10n.tr("Localizable", "connection.permission.internal_url.button_configure") }
/// I know what I am doing. Allow local connections without permission access.
public static var buttonIgnore: String { return L10n.tr("Localizable", "connection.permission.internal_url.button_ignore") }
/// 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.
public static var footer: String { return L10n.tr("Localizable", "connection.permission.internal_url.footer") }
/// Permission access
public static var title: String { return L10n.tr("Localizable", "connection.permission.internal_url.title") }
public enum Ignore {
public enum Alert {
/// Are you sure?
public static var title: String { return L10n.tr("Localizable", "connection.permission.internal_url.ignore.alert.title") }
}
}
}
}
}

public enum Database {
public enum Problem {
/// Delete Database & Quit App
Expand Down

0 comments on commit a043a51

Please sign in to comment.