From f490b69a208d4d0d6bb9520ec90a5aa6490b9fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alonso=20David=20De=20Le=C3=B3n=20Rodarte?= <153755137+DataDave-Dev@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:17:10 -0600 Subject: [PATCH] Reopen the wifi passphrase prompt after a wrong saved password (#6584) * fix: reopen wifi passphrase prompt after a wrong saved password A failed first connection attempt leaves the network profile saved, so the network shows up as known. Clicking it again reconnects with the stored wrong PSK and fails with WifiAuthTimeout, but the inline passphrase prompt only reopened on NoSecrets, leaving no way to re-enter the password short of forgetting the network. Treat an auth timeout on a protected network as a wrong saved passphrase and reopen the prompt; connectWithPsk overwrites the stored PSK on submit. Fixes #6582 * Scope the wifi passphrase reprompt to panel-initiated connects Background auto-connect retries also fire connectionFailed; without a gate they would pop the passphrase prompt open unbidden, stealing focus and wiping a passphrase mid-entry when another network fails. For the gate to see the failure, the action safety-net timer must outlast NetworkManager's 25s supplicant timeout -- at 15s it cleared the action state before WifiAuthTimeout arrived, so a wrong saved password showed "Timed out connecting" instead of "Wrong password". Bump it to 30s. Also share the one ConnectionFailReason map between the Model.js helpers instead of building a second partial copy inline. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: David Heinemeier Hansson Co-authored-by: Claude Fable 5 --- shell/plugins/panels/network/Model.js | 14 ++++++++- shell/plugins/panels/network/Panel.qml | 41 ++++++++++++++++++-------- test/shell.d/network-test.sh | 5 ++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/shell/plugins/panels/network/Model.js b/shell/plugins/panels/network/Model.js index e9b0594a..ff10f489 100644 --- a/shell/plugins/panels/network/Model.js +++ b/shell/plugins/panels/network/Model.js @@ -322,6 +322,17 @@ function networkFailureReason(reason, reasons) { return "Failed to connect" } +// Whether a failed connect should reopen the passphrase prompt. NoSecrets +// always means credentials are missing. An auth timeout on a protected +// network means the saved passphrase is wrong (the same profile a first +// failed attempt leaves behind as "known"), so the user needs a chance to +// re-enter it -- connectWithPsk overwrites the stored PSK on submit. +function shouldRepromptPassphrase(reason, isProtected, reasons) { + var r = reasons || {} + if (reason === r.NoSecrets) return true + return !!isProtected && reason === r.WifiAuthTimeout +} + if (typeof module !== "undefined") { module.exports = { parseNetworkStatus: parseNetworkStatus, @@ -348,6 +359,7 @@ if (typeof module !== "undefined") { wifiSectionTitle: wifiSectionTitle, isProtected: isProtected, enterpriseConnectScript: enterpriseConnectScript, - networkFailureReason: networkFailureReason + networkFailureReason: networkFailureReason, + shouldRepromptPassphrase: shouldRepromptPassphrase } } diff --git a/shell/plugins/panels/network/Panel.qml b/shell/plugins/panels/network/Panel.qml index af9f144a..bb160f07 100644 --- a/shell/plugins/panels/network/Panel.qml +++ b/shell/plugins/panels/network/Panel.qml @@ -98,6 +98,16 @@ Panel { property string passwordText: "" property string identityText: "" + // ConnectionFailReason values as a plain object, so Model.js helpers stay + // pure JS and Node-testable. + readonly property var connectionFailReasons: ({ + NoSecrets: ConnectionFailReason.NoSecrets, + WifiAuthTimeout: ConnectionFailReason.WifiAuthTimeout, + WifiNetworkLost: ConnectionFailReason.WifiNetworkLost, + WifiClientDisconnected: ConnectionFailReason.WifiClientDisconnected, + WifiClientFailed: ConnectionFailReason.WifiClientFailed + }) + // True while any wifi action is mid-flight. Rows // disable themselves on this so clicks on the other rows don't silently // no-op against runNetworkAction's serialized guard. @@ -700,13 +710,11 @@ Panel { } function networkFailureReason(reason) { - return Model.networkFailureReason(reason, { - NoSecrets: ConnectionFailReason.NoSecrets, - WifiAuthTimeout: ConnectionFailReason.WifiAuthTimeout, - WifiNetworkLost: ConnectionFailReason.WifiNetworkLost, - WifiClientDisconnected: ConnectionFailReason.WifiClientDisconnected, - WifiClientFailed: ConnectionFailReason.WifiClientFailed - }) + return Model.networkFailureReason(reason, connectionFailReasons) + } + + function shouldRepromptPassphrase(reason, isProtected) { + return Model.shouldRepromptPassphrase(reason, isProtected, connectionFailReasons) } function checkActionCompletion(network) { @@ -882,7 +890,11 @@ Panel { Timer { id: actionTimeout - interval: 15000 + // Must outlast NetworkManager's 25s supplicant timeout: a wrong saved + // PSK fails with WifiAuthTimeout at ~25s, and that failure has to land + // while the action is still tracked to show "Wrong password" and reopen + // the passphrase prompt. + interval: 30000 repeat: false onTriggered: { if (!root.actionKind) return @@ -1567,8 +1579,12 @@ Panel { Connections { target: row.net ? row.net.network : null function onConnectionFailed(reason) { + // Background auto-connect retries fire this too; only reprompt for + // the connect started from this panel. Checked before + // failNetworkAction, which clears the action state. + var ours = root.actionKind === "connect" && root.actionSsid === (row.net.ssid || "") root.failNetworkAction(row.net.network, reason) - if (reason === ConnectionFailReason.NoSecrets) root.openPasswordPrompt(row.net.ssid) + if (ours && root.shouldRepromptPassphrase(reason, row.isProtected)) root.openPasswordPrompt(row.net.ssid) } function onConnectedChanged() { if (row.net) root.checkActionCompletion(row.net.network) @@ -1750,9 +1766,10 @@ Panel { } } - // Inline passphrase prompt — only shown when we hit a protected network - // we don't have saved credentials for. Submitting (Enter or the check - // button) fires connect; Esc cancels back to the row. + // Inline passphrase prompt — shown when we hit a protected network we + // don't have saved credentials for, or when a connect fails because the + // saved passphrase is wrong. Submitting (Enter or the check button) fires + // connect; Esc cancels back to the row. Item { id: passwordPanel visible: row.isPasswordOpen diff --git a/test/shell.d/network-test.sh b/test/shell.d/network-test.sh index 2d17d256..18007aea 100644 --- a/test/shell.d/network-test.sh +++ b/test/shell.d/network-test.sh @@ -110,6 +110,11 @@ assertEqual(network.networkFailureReason(1, reasons), 'Passphrase required', 'ne assertEqual(network.networkFailureReason(2, reasons), 'Wrong password', 'network maps auth timeout failures') assertEqual(network.networkFailureReason(99, reasons), 'Failed to connect', 'network maps unknown failures') +assertEqual(network.shouldRepromptPassphrase(reasons.NoSecrets, false, reasons), true, 'network reprompts when secrets are missing') +assertEqual(network.shouldRepromptPassphrase(reasons.WifiAuthTimeout, true, reasons), true, 'network reprompts a protected network after a wrong password') +assertEqual(network.shouldRepromptPassphrase(reasons.WifiAuthTimeout, false, reasons), false, 'network does not reprompt an open network on auth timeout') +assertEqual(network.shouldRepromptPassphrase(reasons.WifiClientFailed, true, reasons), false, 'network does not reprompt on generic connection failures') + assertEqual(network.bandLabel('2.4'), '2.4ghz', 'network labels the 2.4GHz band') assertEqual(network.bandLabel('6'), '6ghz', 'network labels the 6GHz band')