Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"alerts.open_url_from_notification.title" = "Open URL?";
"alerts.prompt.cancel" = "Cancel";
"alerts.prompt.ok" = "OK";
"alerts.navigation_error.message" = "This page cannot be displayed because it's outside your Home Assistant server or the page was not found.";
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message combines two distinct error scenarios (external domain navigation and 404 errors) into a single message. This could be confusing for users since they describe different problems with different causes. Consider either:

  1. Using separate alert messages for external domain vs HTTP error scenarios
  2. Rephrasing the message to be more generic and clear
Suggested change
"alerts.navigation_error.message" = "This page cannot be displayed because it's outside your Home Assistant server or the page was not found.";
"alerts.navigation_error.message" = "This page cannot be displayed due to a navigation error.";

Copilot uses AI. Check for mistakes.
"alerts.navigation_error.title" = "Navigation Error";
"always_open_label" = "Always Open";
"announcement.drop_support.button" = "Continue";
"announcement.drop_support.subtitle" = "After careful consideration, we will be discontinuing support for iOS 12, 13 and 14 in our upcoming updates.";
Expand Down
68 changes: 66 additions & 2 deletions Sources/App/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,47 @@ extension WebViewController {
}
}

func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
// Only check domain for user-initiated link clicks
guard navigationAction.navigationType == .linkActivated else {
// Allow all other navigation types (back/forward, reload, programmatic, etc.)
decisionHandler(.allow)
return
}

guard let targetURL = navigationAction.request.url else {
decisionHandler(.allow)
return
}

// Allow special schemes like about:blank
if let scheme = targetURL.scheme?.lowercased(), ["about", "file"].contains(scheme) {
decisionHandler(.allow)
return
}

// Check if the target URL belongs to the active server domain
guard let activeURL = server.info.connection.activeURL() else {
// If there's no active URL, allow navigation (let other error handling deal with it)
decisionHandler(.allow)
return
}

// Allow navigation within the same domain
if targetURL.baseIsEqual(to: activeURL) {
decisionHandler(.allow)
} else {
// URL is outside the active domain - cancel and show alert
Current.Log.warning("Navigation blocked: URL \(targetURL) is outside active domain \(activeURL)")
decisionHandler(.cancel)
showNavigationErrorAlert()
}
}

func webView(
_ webView: WKWebView,
decidePolicyFor navigationResponse: WKNavigationResponse,
Expand All @@ -1311,8 +1352,13 @@ extension WebViewController {

// error response, let's inspect if it's restoring a page or normal navigation
if navigationResponse.response.url != initialURL {
// just a normal loading error
decisionHandler(.allow)
// Normal loading error (not initial URL restoration)
// Cancel the navigation, go back if possible, and show alert
decisionHandler(.cancel)
if webView.canGoBack {
webView.goBack()
}
showNavigationErrorAlert()
Comment on lines +1355 to +1361
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change modifies existing behavior for HTTP error responses. Previously, the error page was allowed to display (with decisionHandler(.allow)). Now, navigation is cancelled and the user is taken back. This could break scenarios where displaying the server's error page is intentional (e.g., custom 404 pages, authentication error pages). Consider whether all HTTP 4xx and 5xx errors should trigger a go-back behavior, or if some status codes should still be allowed to display.

Copilot uses AI. Check for mistakes.
} else {
// first: clear that saved url, it's bad
initialURL = nil
Expand All @@ -1329,6 +1375,24 @@ extension WebViewController {
}
}

private func showNavigationErrorAlert() {
let alert = UIAlertController(
title: L10n.Alerts.NavigationError.title,
message: L10n.Alerts.NavigationError.message,
preferredStyle: .alert
)
alert.addAction(.init(title: L10n.okLabel, style: .default))

DispatchQueue.main.async { [weak self] in
guard let self else { return }
if presentedViewController != nil {
Current.Log.info("Navigation error alert not shown because another view is already presented")
return
}
present(alert, animated: true)
}
Comment on lines +1386 to +1393
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition in alert presentation. Since the alert is shown asynchronously on the main queue, multiple navigation errors in quick succession could attempt to show alerts before the first one is presented. The presentedViewController check on line 1388 might not be effective if called before the first alert's present() completes. Consider adding a flag to track whether an alert is queued for presentation, or debouncing the alert display.

Copilot uses AI. Check for mistakes.
}

// WKUIDelegate
func webView(
_ webView: WKWebView,
Expand Down
6 changes: 6 additions & 0 deletions Sources/Shared/Resources/Swiftgen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ public enum L10n {
public static var title: String { return L10n.tr("Localizable", "alerts.deprecations.notification_category.title") }
}
}
public enum NavigationError {
/// This page cannot be displayed because it's outside your Home Assistant server or the page was not found.
public static var message: String { return L10n.tr("Localizable", "alerts.navigation_error.message") }
/// Navigation Error
public static var title: String { return L10n.tr("Localizable", "alerts.navigation_error.title") }
}
public enum OpenUrlFromDeepLink {
/// Open URL (%@) from deep link?
public static func message(_ p1: Any) -> String {
Expand Down
Loading