Skip to content

Fix notification/deep-link URL being discarded on cold start (#4145)#4739

Draft
dimdal wants to merge 3 commits into
home-assistant:mainfrom
dimdal:marius/fix-4145-cold-start-notification-url
Draft

Fix notification/deep-link URL being discarded on cold start (#4145)#4739
dimdal wants to merge 3 commits into
home-assistant:mainfrom
dimdal:marius/fix-4145-cold-start-notification-url

Conversation

@dimdal

@dimdal dimdal commented Jun 13, 2026

Copy link
Copy Markdown

Summary

Fixes #4145. Tapping a notification with a url (or a URI action targeting the Home Assistant frontend) navigates correctly when the app is already running, but on a cold start the URL is discarded and the app just opens to its default page.

Root cause — a cold-start race. On cold start the webview starts blank (webView.url == nil). The notification tap drives open(inline:)load(notificationURL), but loadActiveURLIfNeeded() — fired independently from viewWillAppear and HomeAssistantAPI.didConnectNotification — loads the default server URL whenever webView.url == nil. Whichever load() lands last wins, and it is usually the default, so the notification URL is lost. When the app is already running webView.url is non-nil, loadActiveURLIfNeeded() early-returns, and open(inline:) always wins — which matches the reports (works when open, and one reporter seeing it work intermittently is the race surfacing).

Fix. Mirror the existing initialURL restoration mechanism with a pendingOpenInlineURL:

  • open(inline:) records the explicitly requested URL before loading it.
  • loadActiveURLIfNeeded() gives that pending URL priority over the restored/default URL (new pure helper prioritizedInlineURL(...)).
  • didFinish clears it once the navigation lands.

Because the pending URL is re-asserted while the webview is still blank and only cleared on a successful navigation, it wins in every ordering of the two loads, so the URL is no longer discarded. Deep links benefit too, since they share the same open(inline:) path.

Scope: this covers URLs that resolve to the HA frontend (server URLs and relative paths such as /frigate/review/...), which is the path in the issue. The separate action: URI → external browser path (openURLInBrowser) is not touched here.

Added unit tests in WebViewControllerTests for the priority helper and for open(inline:) recording the pending URL.

Screenshots

No UI change — this is a navigation/timing fix, so there is nothing visual to capture. Behaviour: with the app force-closed, tapping a notification carrying a url now lands on that URL instead of the default dashboard.

Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#

Any other notes

🤖 Generated with Claude Code

…t#4145)

On cold start webview starts blank, so loadActiveURLIfNeeded() (fired
from viewWillAppear + HA-connect notification) races open(inline:) and
loads the default server URL instead of the notification's URL.

Track the explicitly requested URL (pendingOpenInlineURL); give it
priority in loadActiveURLIfNeeded over restored/default; clear on
didFinish. Wins every load ordering, so URL no longer discarded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 13, 2026 13:16

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi @dimdal

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant home-assistant Bot marked this pull request as draft June 13, 2026 13:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for prioritizing an explicitly requested open(inline:) URL over the default/restored active server URL to prevent cold-start navigation races (re: #4145).

Changes:

  • Introduces pendingOpenInlineURL state to preserve an explicit deep-link/notification navigation until it completes.
  • Updates active URL loading to prefer a pending explicit URL when it targets the active server.
  • Adds unit tests for URL prioritization and for recording the pending inline URL.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Tests/App/WebView/WebViewControllerTests.swift Adds tests covering prioritized inline URL selection and recording pending inline URLs.
Sources/App/Frontend/WebView/WebViewController.swift Introduces pendingOpenInlineURL to track explicit navigations that should override default/restored loads.
Sources/App/Frontend/WebView/WebViewController+WebKitDelegates.swift Clears pendingOpenInlineURL when navigation finishes to stop forcing the explicit URL.
Sources/App/Frontend/WebView/WebViewController+URLLoading.swift Prefers the pending explicit inline URL in loadActiveURLIfNeeded() when it targets the active server.
Sources/App/Frontend/WebView/WebViewController+Navigation.swift Records open(inline:) requests into pendingOpenInlineURL to survive cold-start reload races.

Comment on lines +90 to +91
// the explicit navigation has landed; stop forcing it on subsequent active-URL loads (#4145)
pendingOpenInlineURL = nil
/// `loadActiveURLIfNeeded()` — fired from `viewWillAppear` and the HA-connect notification — would
/// otherwise race with and overwrite this navigation with the default server URL, discarding the
/// requested URL (#4145). Cleared once a navigation finishes (`didFinish`).
var pendingOpenInlineURL: URL?
Comment on lines +168 to +175
func testOpenInlineRecordsPendingURLForFrontendPath() {
let sut = makeSUT()
let url = URL(string: "https://example.com:8123/lovelace/default")!

sut.open(inline: url)

XCTAssertEqual(sut.pendingOpenInlineURL, url)
}
…lank

- Clear pendingOpenInlineURL on non-cancelled didFail/didFailProvisional
  so a permanently-failing URL doesn't get re-forced on later active-URL
  loads. Cancelled failures keep it (a superseding load must not lose it).
- didFinish: don't clear on about:blank (loaded by showNoActiveURLError),
  which would drop the pending URL before the real navigation runs.
- Note baseIsEqual same-server semantics on prioritizedInlineURL.
- Add tests for real-failure clear and cancelled-keeps-pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi @dimdal

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

… too

Move the pendingOpenInlineURL clear out of the URLError cast in didFail so
any non-cancelled committed-navigation failure clears it, not just URLError.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hi @dimdal

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Home Assistant Companion App ignores url parameter

2 participants