Skip to content

Handle external link and open file failures - #767

Open
epicleafies wants to merge 8 commits into
bitcoin-core:qt6from
epicleafies:handle-url-failures
Open

Handle external link and open file failures#767
epicleafies wants to merge 8 commits into
bitcoin-core:qt6from
epicleafies:handle-url-failures

Conversation

@epicleafies

@epicleafies epicleafies commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Updated external-link opening so a failed open gives the user feedback instead of silently doing nothing. Fixes #597

  • Every external link now routes through the confirmation popup, and ExternalLink owns that popup itself, so a link cannot be built without the confirmation and failure handling. This includes the third-party transaction links on the activity details page, which previously opened with a single click and no confirmation
  • URLs only open if http/https, enforced in C++ at the single point where the app hands a URL to the OS
  • On external link failure the popup switches to an error layout with a copy-link fallback instead of closing silently. The dialog bounds its own width, wraps long URLs, and keeps keyboard focus through the state change
  • ExternalLink now gives hover feedback (highlight and pointing hand cursor) so standalone links read as clickable, and the activity details links are sized to their content
  • The debug log page surfaces failures in a banner (matching the Sign/Verify message result banner): a missing or unreadable log is reported persistently until it is readable again, with search and open-externally disabled meanwhile, and a failed external open is reported until the page is left
  • Fixed ToastBanner rendering nothing when created with its condition already true, and its text now renders as plain text so markup-like strings cannot be interpreted as rich text
  • Version link icon changed from caret to open externally (now matches other links)
  • Added unit, QML and functional tests

Known limitation: the platform only reports whether a handler process was launched. A browser that starts and then fails still reads as success, so the error handling covers the no-handler-available case.

About page open externally fail pop up

About link open failure

Debug log page open externally fail banner

Debug log link open failure

Activity details page link open pop-up

Activity details link pop-up

@epicleafies
epicleafies force-pushed the handle-url-failures branch 2 times, most recently from 5c63277 to e4f8bec Compare June 30, 2026 16:20
@epicleafies
epicleafies force-pushed the handle-url-failures branch from 72abf63 to 2d238fc Compare July 9, 2026 23:50
@epicleafies
epicleafies force-pushed the handle-url-failures branch from 2d238fc to dd4012e Compare July 30, 2026 17:23
The Version row on the About page opens an external download URL, like
the Website, Source code, and License rows, but it overrides the link
icon with a caret-right. That caret is the in-app navigation symbol used
by rows that push another page (for example Developer options), so it
wrongly signals an internal navigation for what is actually an external
link, and it is inconsistent with the other three external links.

Drop the caret-right override so the Version link uses the shared
external-link (export) icon, making all four About external links
consistent.
@epicleafies
epicleafies force-pushed the handle-url-failures branch 2 times, most recently from f840195 to 5570294 Compare July 31, 2026 20:23
Opening an external link ignored whether it actually succeeded.
ExternalPopup called Qt.openUrlExternally() and closed immediately, and
ExternalLink opened links directly, so when the OS or browser could not
open the URL the user was left with no feedback and no way to recover.

Give ExternalLink ownership of the confirmation dialog. A page can no
longer wire a link up wrong, because there is no way to build a link that
opens nothing: the control creates its own ExternalPopup on first click,
names it after itself so several links on one page stay individually
addressable from tests, and routes every open through it. ExternalPopup
and Separator move into qml/controls alongside it, since a control cannot
depend on qml/components.

Route the open through a new UrlOpener singleton rather than calling
Qt.openUrlExternally() from QML. That puts the http/https allowlist in
C++ at the single point where the app hands a URL to the operating
system, so a later caller cannot inherit an unchecked handoff to the
handler registry, and it makes the outcome injectable, so both paths are
testable without launching a browser while the shipped QML keeps one
unconditional call path.

On failure the dialog stays open, explains what happened, and offers to
copy the URL. It also takes focus while open, without which Tab kept
cycling the page behind the modal dialog instead of its own buttons, and
entering the error state moves focus onto the copy button so the swap is
not a visual-only change. Copying disables that button while it
confirms, so the label always describes what the button will do and a
repeat copy is not silently a no-op.

The dialog now bounds its own width and centres itself rather than
leaving that to each caller, which is what made it span the whole window
on the activity details page, and it wraps the URL mid-token so a txid
cannot spill past the dialog edge.
Add a bridge-driven functional test that reaches the About page from the
onboarding cover, opens the external-link confirmation popup from a link
row, and verifies that cancelling closes it without leaving the page. It
deliberately never confirms the open, since that would hand the URL to a
real handler and try to launch a browser; the success, failure, scheme
rejection and copy-URL paths are covered by the QML unit test, which
drives a stubbed UrlOpener.

Register the test in the GUI functional-test workflow so it runs in CI.
ToastBanner starts fully transparent and only becomes opaque from its
onVisibleChanged fade-in. That handler never runs for a banner whose
condition already holds when it is built inside an already visible page:
visible never changes, so the banner keeps taking up its space in the
layout while drawing nothing. Re-entering the debug log page with the
log still missing showed exactly that, a blank strip where the
persistent error banner should be.

Whether the fade-in runs depends on creation order. A page whose tree
becomes visible only after construction still gets the visibility change
and fades in, which is why every existing call site, which binds to
state that starts empty, never hit this. Bind the initial opacity to the
initial visibility so both orders draw the banner, and start the
auto-dismiss countdown at creation for a banner shown this way, since
the fade-in that normally arms it does not run.
DebugLogModel has always detected both ways the debug log can be
unavailable: the background reader records when the file cannot be read,
and openLogFile() records when it cannot be handed to another
application. Neither was ever shown. SettingsDebugLog discarded
openLogFile()'s return value and nothing read openError at all, so both
failures were silent.

Show them in a ToastBanner on the debug log page, matching the
Sign/Verify message result banner styling.

The two failures need separate fields because they have different
lifetimes. The reader's error describes the state of the log and has to
survive navigation, since that condition outlives any one visit. The
open error is the outcome of a single click and must not, or a stale
failure follows the user around; the page clears it on creation through
a new clearOpenError(). Sharing one field also let the reader wipe the
click failure on the next auto-refresh tick, leaving only a brief flash
of the message.

For the same reason openLogFile() no longer reports a missing file
itself. A missing log is a state of the log rather than the outcome of a
click, so reporting it as one let the page clear it on re-entry while
the file was still missing. The reader owns that message and keeps it
for as long as the condition holds.

Add logAvailable so the page can disable what a missing log makes
meaningless: searching its contents, and opening it in another
application. Refresh stays enabled, since it is how the user recovers
once the file exists again.

Cover it both ways: QML tests check that the error shows on a failed
open, clears on a later success, does not survive page re-creation, and
that an unreadable log disables search and export; C++ unit tests check
that the missing-file path is left to the reader, that the open failure
survives a successful background read, that clearOpenError() drops it,
and that logAvailable follows the reader in both directions.
ExternalLink only changed color when a surrounding row drove its
parentState, so standalone instances such as the third-party
transaction links on the activity details page gave no visual cue
that they are clickable, and no instance showed a pointing hand
cursor. Handle hover in the control itself so every ExternalLink
highlights on hover and shows a pointing hand cursor, while a
DISABLED parentState still suppresses both.
ToastBanner's text item had no textFormat set, so it defaulted to
AutoText, which silently upgrades markup-looking strings to rich text.
Banner text is always a message, never markup, and error messages can
interpolate externally influenced strings such as filesystem paths, so
render it as plain text.
The third-party transaction links were stretched to the full width of the
column. ExternalLink lets its label take the free space and pins the icon
to the trailing edge, which is right for the About rows, where the label
belongs on the left of a full-width settings row. Here it left the label
centred but the icon stranded at the far edge, and made the whole column
width clickable rather than the link.

Let each link size to its own content and centre it, so the icon sits
next to the label it belongs to and the clickable area is the link.
@epicleafies
epicleafies force-pushed the handle-url-failures branch from 5570294 to 76715d1 Compare August 1, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

About/Help links: handle external URL open failures

1 participant