diff --git a/0001-Support-for-web-login-and-unified-auth-mechanism.patch b/0001-Support-for-web-login-and-unified-auth-mechanism.patch index 4ff3a6c..24f1bb4 100644 --- a/0001-Support-for-web-login-and-unified-auth-mechanism.patch +++ b/0001-Support-for-web-login-and-unified-auth-mechanism.patch @@ -1,13 +1,15 @@ -From 27673e15fbde961e7f42a8517d2a5c4a6452843e Mon Sep 17 00:00:00 2001 +From 898c0e7eda41c0e3f7e53ec0d27bbb3fbee8e2dd Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 2 Oct 2025 10:59:57 +0200 -Subject: [PATCH 01/54] style: Add common login dialog button styles to avoid +Subject: [PATCH 01/72] style: Add common login dialog button styles to avoid duplication This will be used in next commits, when new login buttons are added. Also, add a missing insensitive_button_bg_color on lockscren buttons, without it the button was being dark when insensitive. + +Part-of: --- data/theme/gnome-shell-sass/_common.scss | 11 +++++++++++ data/theme/gnome-shell-sass/_drawing.scss | 1 + @@ -80,13 +82,13 @@ index b661e93c8..6cca1e28e 100644 padding: $base_padding * 1.5; -- -2.54.0 +2.55.0 -From 449e8ad3d5558cd00c3dae0d8ca99ef96acb8456 Mon Sep 17 00:00:00 2001 +From 622a9064b28f6f3b078c7871ff136a4ab18b6458 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:37:08 +0100 -Subject: [PATCH 02/54] unlockDialog: Vertically center dialog using fixed +Subject: [PATCH 02/72] unlockDialog: Vertically center dialog using fixed height Position the topY of the content based on a fixed prompt height estimation. @@ -96,6 +98,8 @@ positioned too high, so margin-top is added to keep them centered. This ensures all elements are positioned in the same way and allows taller elements to be centered in the future. + +Part-of: --- data/theme/gnome-shell-sass/widgets/_login-lock.scss | 2 ++ js/ui/unlockDialog.js | 10 ++++++---- @@ -157,17 +161,19 @@ index 63ba591ee..a014095bb 100644 actorBox.x1 = columnX1; -- -2.54.0 +2.55.0 -From dbdc98e1cb1adc03c30d701aa1eaed0257967453 Mon Sep 17 00:00:00 2001 +From 0e0c00dd905cf06bf8d90568d02dadf7ff8970be Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:38:04 +0100 -Subject: [PATCH 03/54] unlockDialog: Fix username reuse on reset +Subject: [PATCH 03/72] unlockDialog: Fix username reuse on reset The condition was checking for PROVIDE_USERNAME specifically, but should also handle REUSE_USERNAME. Check for not DONT_PROVIDE_USERNAME instead to correctly reuse the username when requested. + +Part-of: --- js/ui/unlockDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) @@ -186,18 +192,20 @@ index a014095bb..420dae4c6 100644 userName = this._userName; } else { -- -2.54.0 +2.55.0 -From a3696c5975c32da5afe20b4cf1fe1388f579322e Mon Sep 17 00:00:00 2001 +From 9053be8857ddb04ff70f6f65e73bcd57791f75f1 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:38:54 +0100 -Subject: [PATCH 04/54] unlockDialog: Wait for authPrompt destruction before +Subject: [PATCH 04/72] unlockDialog: Wait for authPrompt destruction before switching VT When switching to another user, wait until authPrompt is destroyed and the clock transition animation completes before switching to the login session VT. + +Part-of: --- js/ui/unlockDialog.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) @@ -217,48 +225,19 @@ index 420dae4c6..7a02f68ee 100644 } -- -2.54.0 +2.55.0 -From a0c1490f2312b1594430fc0a16916e8ec93bbe9f Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Tue, 10 Mar 2026 13:56:02 +0100 -Subject: [PATCH 05/54] authPrompt: Separate input well styles from prompt - layout - -Give _inputWell its own style class instead of reusing -login-dialog-prompt-layout. The prompt layout defines the width, -while the input well just fills horizontally within it. ---- - data/theme/gnome-shell-sass/widgets/_login-lock.scss | 3 +++ - 1 file changed, 3 insertions(+) - -diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 46805ebca..ca8d295ce 100644 ---- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss -+++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -15,6 +15,9 @@ $_gdm_dialog_width: 25em; - .login-dialog-prompt-layout { - width: $_gdm_dialog_width; - margin-top: 80px; -+ } -+ -+ .login-dialog-input-well { - spacing: $base_padding * 1.5; - } - } --- -2.54.0 - - -From 706c9c84b33d267172e57e4d67f2152b1a243680 Mon Sep 17 00:00:00 2001 +From 465e76688dd01cdb5f2bae01fc3ee01dda6fcefd Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 5 Feb 2026 18:55:38 +0100 -Subject: [PATCH 06/54] authPrompt: Use destructured object for +Subject: [PATCH 05/72] authPrompt: Use destructured object for updateSensitivity Replace the boolean parameter with a destructured object to make call sites self-documenting. + +Part-of: --- js/gdm/authPrompt.js | 18 +++++++++--------- js/gdm/loginDialog.js | 6 +++--- @@ -266,7 +245,7 @@ call sites self-documenting. 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 3b4a2f798..526dcdbaa 100644 +index b1e8202bd..2adfd1921 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -287,7 +287,7 @@ export const AuthPrompt = GObject.registerClass({ @@ -296,7 +275,7 @@ index 3b4a2f798..526dcdbaa 100644 this.emit('prompted'); } -@@ -419,7 +419,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -415,7 +415,7 @@ export const AuthPrompt = GObject.registerClass({ this.clear(); } @@ -305,7 +284,7 @@ index 3b4a2f798..526dcdbaa 100644 this.setActorInDefaultButtonWell(null); if (!canRetry) -@@ -540,12 +540,12 @@ export const AuthPrompt = GObject.registerClass({ +@@ -536,12 +536,12 @@ export const AuthPrompt = GObject.registerClass({ opacity: 0, visible: true, }); @@ -320,8 +299,8 @@ index 3b4a2f798..526dcdbaa 100644 }); } -@@ -611,7 +611,7 @@ export const AuthPrompt = GObject.registerClass({ - wiggle(this._message, wiggleParameters); +@@ -605,7 +605,7 @@ export const AuthPrompt = GObject.registerClass({ + } } - updateSensitivity(sensitive) { @@ -329,7 +308,7 @@ index 3b4a2f798..526dcdbaa 100644 let authWidget; if (this._authList.visible) -@@ -641,7 +641,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -635,7 +635,7 @@ export const AuthPrompt = GObject.registerClass({ this.setUser(null); @@ -338,7 +317,7 @@ index 3b4a2f798..526dcdbaa 100644 this._entry.set_text(''); } -@@ -734,7 +734,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -728,7 +728,7 @@ export const AuthPrompt = GObject.registerClass({ hold: null, }); @@ -385,26 +364,28 @@ index 7a02f68ee..85e469fb8 100644 } -- -2.54.0 +2.55.0 -From 7e7846011e53d45e7c532383fff5d213034bbbd5 Mon Sep 17 00:00:00 2001 +From a703c5d6561bb96b9dbb17c257e6ff4f2ca462bc Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:47:22 +0100 -Subject: [PATCH 07/54] authPrompt: Use array-based widget lookup in +Subject: [PATCH 06/72] authPrompt: Use array-based widget lookup in updateSensitivity Replace the if/else widget selection with array-based lookup to prepare for additional auth widgets in upcoming commits. + +Part-of: --- js/gdm/authPrompt.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 526dcdbaa..7967b60a8 100644 +index 2adfd1921..ab61e14f9 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -612,12 +612,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -606,12 +606,9 @@ export const AuthPrompt = GObject.registerClass({ } updateSensitivity({sensitive}) { @@ -421,27 +402,31 @@ index 526dcdbaa..7967b60a8 100644 if (authWidget.reactive === sensitive) return; -- -2.54.0 +2.55.0 -From 76e63f00fe54ed5c13a16bab4c3a3edd6d788d4b Mon Sep 17 00:00:00 2001 +From ccb5270afbae6869c4c90b7eeefb411b63542c05 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Mar 2026 17:10:58 +0100 -Subject: [PATCH 08/54] authPrompt: Generalize _fadeInChoiceList to accept any +Subject: [PATCH 07/72] authPrompt: Generalize _fadeInChoiceList to accept any element Rename to _fadeInElement and take the element as a parameter, allowing the fade-in animation to be reused for other widgets. -Also skip the animation if the element is already visible. + +Now sensitivity enablement is independent of fadeInElement to allow more +flexibility. + +Part-of: --- - js/gdm/authPrompt.js | 11 +++++++---- - 1 file changed, 7 insertions(+), 4 deletions(-) + js/gdm/authPrompt.js | 16 +++++++++------- + 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 7967b60a8..f19e639a7 100644 +index ab61e14f9..7033e05f0 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -535,13 +535,16 @@ export const AuthPrompt = GObject.registerClass({ +@@ -531,17 +531,18 @@ export const AuthPrompt = GObject.registerClass({ this._entry.grab_key_focus(); } @@ -455,42 +440,50 @@ index 7967b60a8..f19e639a7 100644 opacity: 0, visible: true, }); - this.updateSensitivity({sensitive: false}); +- this.updateSensitivity({sensitive: false}); - this._authList.ease({ + element.ease({ opacity: 255, duration: MESSAGE_FADE_OUT_ANIMATION_TIME, - transition: Clutter.AnimationMode.EASE_OUT_QUAD, -@@ -560,7 +563,7 @@ export const AuthPrompt = GObject.registerClass({ +- transition: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => this.updateSensitivity({sensitive: true}), ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + +@@ -556,7 +557,8 @@ export const AuthPrompt = GObject.registerClass({ this._entry.hide(); if (this._message.text === '') this._message.hide(); - this._fadeInChoiceList(); + this._fadeInElement(this._authList); ++ this.updateSensitivity({sensitive: true}); } getAnswer() { -- -2.54.0 +2.55.0 -From be6debccbd80cd595a725c6d3081313bd0275940 Mon Sep 17 00:00:00 2001 +From 502d46f69121ad2b5a4e2680b9e336a07127ba4d Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Fri, 9 Feb 2024 09:02:25 -0500 -Subject: [PATCH 09/54] authPrompt: Fade out input buttons/entry after +Subject: [PATCH 08/72] authPrompt: Fade out input buttons/entry after verification It's nice to just see the user image and post login messages once the user is done with the prompt. The buttons and to some extent the password entry can disrupt the natural login flow. + +Part-of: --- - js/gdm/authPrompt.js | 17 +++++++++++++++-- - js/gdm/loginDialog.js | 11 +++++++++++ - 2 files changed, 26 insertions(+), 2 deletions(-) + js/gdm/authPrompt.js | 15 +++++++++++++-- + js/gdm/loginDialog.js | 10 ++++++++++ + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index f19e639a7..e2ae3b047 100644 +index 7033e05f0..6e8666fe7 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -51,6 +51,7 @@ export const AuthPrompt = GObject.registerClass({ @@ -501,7 +494,7 @@ index f19e639a7..e2ae3b047 100644 }, }, class AuthPrompt extends St.BoxLayout { _init(gdmClient, mode) { -@@ -432,8 +433,16 @@ export const AuthPrompt = GObject.registerClass({ +@@ -428,8 +429,15 @@ export const AuthPrompt = GObject.registerClass({ _onVerificationComplete() { this.setActorInDefaultButtonWell(null); this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; @@ -509,7 +502,6 @@ index f19e639a7..e2ae3b047 100644 - this.cancelButton.can_focus = false; + + this._mainBox.reactive = false; -+ this._mainBox.can_focus = false; + this._mainBox.ease({ + opacity: 0, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, @@ -520,19 +512,18 @@ index f19e639a7..e2ae3b047 100644 } _onReset() { -@@ -520,6 +529,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -516,6 +524,9 @@ export const AuthPrompt = GObject.registerClass({ this.stopSpinning(); this._authList.clear(); this._authList.hide(); + + this._mainBox.opacity = 255; + this._mainBox.reactive = true; -+ this._mainBox.can_focus = true; } setQuestion(question) { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 9f76464ad..f75497065 100644 +index 9f76464ad..7992cadb4 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -590,6 +590,7 @@ export const LoginDialog = GObject.registerClass({ @@ -543,13 +534,12 @@ index 9f76464ad..f75497065 100644 this._authPrompt.hide(); this.add_child(this._authPrompt); -@@ -1091,6 +1092,16 @@ export const LoginDialog = GObject.registerClass({ +@@ -1091,6 +1092,15 @@ export const LoginDialog = GObject.registerClass({ } } + _onVerificationComplete() { + this._bottomButtonGroup.reactive = false; -+ this._bottomButtonGroup.can_focus = false; + this._bottomButtonGroup.ease({ + opacity: 0, + duration: _FADE_ANIMATION_TIME, @@ -561,13 +551,13 @@ index 9f76464ad..f75497065 100644 this._sessionMenuButton.setActiveSession(sessionId); } -- -2.54.0 +2.55.0 -From 19705a097a11e545a20703bdce7700ba6bb09477 Mon Sep 17 00:00:00 2001 +From 2e9f927a1971704f433f661bf7f51e90865ebb62 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:23:34 +0100 -Subject: [PATCH 10/54] authPrompt: Don't reset preemptiveAnswer when +Subject: [PATCH 09/72] authPrompt: Don't reset preemptiveAnswer when VERIFICATION_IN_PROGRESS PreemptiveAnswer wasn't being used in the case where verification is in @@ -575,15 +565,17 @@ progress and the smartcard is inserted, triggering a reset. This change ensures a preemptive answer will be used once smartcard service asks for the PIN. + +Part-of: --- js/gdm/authPrompt.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index e2ae3b047..eb7f36821 100644 +index 6e8666fe7..81863f78e 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -688,7 +688,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -679,7 +679,8 @@ export const AuthPrompt = GObject.registerClass({ this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; @@ -594,16 +586,18 @@ index e2ae3b047..eb7f36821 100644 if (this._preemptiveAnswerWatchId) this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); -- -2.54.0 +2.55.0 -From e7da2b6729fb2585f6f0d02099a8b9af2e7b9139 Mon Sep 17 00:00:00 2001 +From bc17c345307308f5fdc7b86df98db3dbbb778d88 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:05:32 +0100 -Subject: [PATCH 11/54] style: Increase hint-text left margin +Subject: [PATCH 10/72] style: Increase hint-text left margin The cursor was overlapping the hint-text, making it difficult to read. Increase the left margin to ensure proper readability. + +Part-of: --- data/theme/gnome-shell-sass/widgets/_entries.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) @@ -621,23 +615,25 @@ index 41e10f766..5a45e86d6 100644 } } -- -2.54.0 +2.55.0 -From 005ef41251c30e20cbaa8ec2a235ea7b0bc69ccb Mon Sep 17 00:00:00 2001 +From a7a989b82b4fede27cf05cb89dc83dfe19820dab Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 16 Feb 2026 16:05:17 +0100 -Subject: [PATCH 12/54] authPrompt: Use connectObject for userVerifier signals +Subject: [PATCH 11/72] authPrompt: Use connectObject for userVerifier signals This allows cleanly disconnecting all signals at once when the authPrompt is destroyed, preventing potential issues from stale signal handlers. + +Part-of: --- - js/gdm/authPrompt.js | 20 +++++++++++--------- - 1 file changed, 11 insertions(+), 9 deletions(-) + js/gdm/authPrompt.js | 28 +++++++++++++++------------- + 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index eb7f36821..bf1b8aea5 100644 +index 81863f78e..9fcf48590 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -80,14 +80,16 @@ export const AuthPrompt = GObject.registerClass({ @@ -653,14 +649,14 @@ index eb7f36821..bf1b8aea5 100644 - this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this)); - this._userVerifier.connect('credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this)); + this._userVerifier.connectObject( -+ 'ask-question', this._onAskQuestion.bind(this), -+ 'show-message', this._onShowMessage.bind(this), -+ 'show-choice-list', this._onShowChoiceList.bind(this), -+ 'verification-failed', this._onVerificationFailed.bind(this), -+ 'verification-complete', this._onVerificationComplete.bind(this), -+ 'reset', this._onReset.bind(this), -+ 'smartcard-status-changed', this._onSmartcardStatusChanged.bind(this), -+ 'credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this), ++ 'ask-question', (_, ...args) => this._onAskQuestion(...args), ++ 'show-message', (_, ...args) => this._onShowMessage(...args), ++ 'show-choice-list', (_, ...args) => this._onShowChoiceList(...args), ++ 'verification-failed', (_, ...args) => this._onVerificationFailed(...args), ++ 'verification-complete', () => this._onVerificationComplete(), ++ 'reset', () => this._onReset(), ++ 'smartcard-status-changed', () => this._onSmartcardStatusChanged(), ++ 'credential-manager-authenticated', () => this._onCredentialManagerAuthenticated(), + this); this.smartcardDetected = this._userVerifier.smartcardDetected; @@ -674,14 +670,79 @@ index eb7f36821..bf1b8aea5 100644 this._userVerifier.destroy(); this._userVerifier = null; this._entry = null; +@@ -330,7 +332,7 @@ export const AuthPrompt = GObject.registerClass({ + this._capsLockWarningLabel.visible = secret; + } + +- _onAskQuestion(verifier, serviceName, question, secret) { ++ _onAskQuestion(serviceName, question, secret) { + if (this._queryingService) + this.clear(); + +@@ -355,7 +357,7 @@ export const AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowChoiceList(userVerifier, serviceName, promptMessage, choiceList) { ++ _onShowChoiceList(serviceName, promptMessage, choiceList) { + if (this._queryingService) + this.clear(); + +@@ -394,7 +396,7 @@ export const AuthPrompt = GObject.registerClass({ + this.reset(); + } + +- _onShowMessage(_userVerifier, serviceName, message, type, showMessageResolver) { ++ _onShowMessage(serviceName, message, type, showMessageResolver) { + this.setMessage(message, type); + this.emit('prompted'); + +@@ -408,7 +410,7 @@ export const AuthPrompt = GObject.registerClass({ + showMessageResolver?.(wigglePromise); + } + +- _onVerificationFailed(userVerifier, serviceName, canRetry) { ++ _onVerificationFailed(serviceName, canRetry) { + const wasQueryingService = this._queryingService === serviceName; + + if (wasQueryingService) { -- -2.54.0 +2.55.0 -From be65b189e704c9273c5c841674a72e43586ae587 Mon Sep 17 00:00:00 2001 +From 1d6ee728c8972d771b8daa834687053c66a62ccc Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 12 Feb 2026 17:09:44 +0100 +Subject: [PATCH 12/72] authPrompt: Group animation constants together + +Move all animation-related constants together near the top of the +file to follow the style of the rest of the codebase. + +Part-of: +--- + js/gdm/authPrompt.js | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 9fcf48590..4bd0c8f6d 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -18,7 +18,6 @@ import {wiggle} from '../misc/animationUtils.js'; + const DEFAULT_BUTTON_WELL_ICON_SIZE = 16; + const DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; + const DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; +- + const MESSAGE_FADE_OUT_ANIMATION_TIME = 500; + + /** @enum {number} */ +-- +2.55.0 + + +From 453008139ebe53795a33f3a6d2dde54a7e8aed45 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:20:34 +0100 -Subject: [PATCH 13/54] authPrompt: Update entry layout based on mockups +Subject: [PATCH 13/72] authPrompt: Update entry layout based on mockups Implement the new authentication prompt layout based on the design mockups at: @@ -692,25 +753,24 @@ spinner appear inside the entry. The entry is horizontally centered as username and avatar. The cancelButton is placed on the left of the centered entry. + +Part-of: --- .../gnome-shell-sass/widgets/_login-lock.scss | 30 +++++++-- - js/gdm/authPrompt.js | 63 ++++++++++++++----- - 2 files changed, 75 insertions(+), 18 deletions(-) + js/gdm/authPrompt.js | 62 ++++++++++++++----- + 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index ca8d295ce..7024cc58d 100644 +index 46805ebca..3695014f8 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -13,13 +13,29 @@ $_gdm_dialog_width: 25em; +@@ -13,10 +13,26 @@ $_gdm_dialog_width: 25em; } .login-dialog-prompt-layout { - width: $_gdm_dialog_width; + width: $_gdm_dialog_width * 1.2; margin-top: 80px; - } - - .login-dialog-input-well { spacing: $base_padding * 1.5; } + @@ -732,7 +792,7 @@ index ca8d295ce..7024cc58d 100644 } // GDM Login Dialog -@@ -37,7 +53,7 @@ $_gdm_dialog_width: 25em; +@@ -34,7 +50,7 @@ $_gdm_dialog_width: 25em; // buttons on login screen .login-dialog-button { @@ -741,7 +801,7 @@ index ca8d295ce..7024cc58d 100644 &.a11y-button, &.cancel-button, &.switch-user-button, -@@ -48,13 +64,19 @@ $_gdm_dialog_width: 25em; +@@ -45,13 +61,19 @@ $_gdm_dialog_width: 25em; padding: to_em(16px); } @@ -764,14 +824,13 @@ index ca8d295ce..7024cc58d 100644 .conflicting-session-dialog-content { diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index bf1b8aea5..4367ca1de 100644 +index 4bd0c8f6d..23d3fce72 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -200,10 +200,20 @@ export const AuthPrompt = GObject.registerClass({ +@@ -199,10 +199,20 @@ export const AuthPrompt = GObject.registerClass({ }); this._mainBox.add_child(this._authList); -- let entryParams = { + this._entryArea = new St.Widget({ + style_class: 'login-dialog-prompt-entry-area', + layout_manager: new Clutter.BinLayout(), @@ -781,7 +840,7 @@ index bf1b8aea5..4367ca1de 100644 + }); + this._mainBox.add_child(this._entryArea); + -+ const entryParams = { + let entryParams = { style_class: 'login-dialog-prompt-entry', can_focus: true, x_expand: true, @@ -789,7 +848,7 @@ index bf1b8aea5..4367ca1de 100644 }; this._entry = null; -@@ -215,7 +225,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -214,7 +224,7 @@ export const AuthPrompt = GObject.registerClass({ ShellEntry.addContextMenu(this._passwordEntry, {actionMode: Shell.ActionMode.NONE}); this._entry = this._passwordEntry; @@ -798,7 +857,7 @@ index bf1b8aea5..4367ca1de 100644 this._entry.grab_key_focus(); this._inactiveEntry = this._textEntry; -@@ -241,17 +251,38 @@ export const AuthPrompt = GObject.registerClass({ +@@ -240,17 +250,38 @@ export const AuthPrompt = GObject.registerClass({ this._defaultButtonWell = new St.Widget({ layout_manager: new Clutter.BinLayout(), @@ -816,6 +875,7 @@ index bf1b8aea5..4367ca1de 100644 + + this._nextButton = new St.Button({ + style_class: 'login-dialog-button next-button', ++ accessible_name: _('Submit'), + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + reactive: true, + can_focus: false, @@ -830,10 +890,9 @@ index bf1b8aea5..4367ca1de 100644 + + this.setActorInDefaultButtonWell(this._nextButton); + -+ + // center elements inside _mainBox between the cancel + // button on the left and this spacer on the right -+ this._mainBox.add_child(new St.Widget({ ++ this._mainBox.add_child(new Clutter.Actor({ + constraints: new Clutter.BindConstraint({ + source: this.cancelButton, + coordinate: Clutter.BindCoordinate.WIDTH, @@ -842,7 +901,7 @@ index bf1b8aea5..4367ca1de 100644 } showTimedLoginIndicator(time) { -@@ -321,7 +352,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -320,7 +351,7 @@ export const AuthPrompt = GObject.registerClass({ } if (newEntry) { @@ -851,7 +910,7 @@ index bf1b8aea5..4367ca1de 100644 this._entry = newEntry; this._inactiveEntry = inactiveEntry; -@@ -423,17 +454,17 @@ export const AuthPrompt = GObject.registerClass({ +@@ -418,17 +449,17 @@ export const AuthPrompt = GObject.registerClass({ } this.updateSensitivity({sensitive: canRetry}); @@ -872,7 +931,7 @@ index bf1b8aea5..4367ca1de 100644 this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; this._mainBox.reactive = false; -@@ -546,7 +577,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -539,7 +570,8 @@ export const AuthPrompt = GObject.registerClass({ this._entry.hint_text = question; this._authList.hide(); @@ -882,7 +941,7 @@ index bf1b8aea5..4367ca1de 100644 this._entry.grab_key_focus(); } -@@ -575,7 +607,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -566,7 +598,7 @@ export const AuthPrompt = GObject.registerClass({ this._authList.addItem(key, text); } @@ -891,17 +950,18 @@ index bf1b8aea5..4367ca1de 100644 if (this._message.text === '') this._message.hide(); this._fadeInElement(this._authList); -@@ -637,6 +669,9 @@ export const AuthPrompt = GObject.registerClass({ - if (authWidget.reactive === sensitive) - return; +@@ -624,8 +656,8 @@ export const AuthPrompt = GObject.registerClass({ + this._authList, + ].find(widget => widget.visible) ?? this._entry; +- if (authWidget.reactive === sensitive) +- return; + if (authWidget === this._entry) + this._nextButton.reactive = sensitive; -+ + authWidget.reactive = sensitive; - if (sensitive) { -@@ -650,7 +685,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -640,7 +672,7 @@ export const AuthPrompt = GObject.registerClass({ } vfunc_hide() { @@ -911,13 +971,13 @@ index bf1b8aea5..4367ca1de 100644 this._message.opacity = 0; -- -2.54.0 +2.55.0 -From 5aae5b3b70845a57e74efeef18ca5c3465d52838 Mon Sep 17 00:00:00 2001 +From 406d4e52d3fc51ec16957dd137ebfc5b944ac812 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:41:48 +0100 -Subject: [PATCH 14/54] authPrompt: Fade in _entryArea instead of abruptly +Subject: [PATCH 14/72] authPrompt: Fade in _entryArea instead of abruptly showing it To support the new fade-in flow, the _entryArea is now hidden by default. @@ -927,18 +987,17 @@ In _onVerificationFailed(), the call to clear() was removed to prevent the area from being hidden prematurely. This is safe because a reset() (which calls clear()) is triggered later in the process. -_fadeInElement() will call updateSensitivity() to ensure the widget is -sensitive when the animation finishes, so there's no need to call it -explicitly in _onAskQuestion(). +Part-of: --- - js/gdm/authPrompt.js | 9 +++------ - 1 file changed, 3 insertions(+), 6 deletions(-) + js/gdm/authPrompt.js | 10 ++++------ + js/gdm/loginDialog.js | 1 - + 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 4367ca1de..9e0557c2f 100644 +index 23d3fce72..cf1046f94 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -384,7 +384,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -383,7 +383,6 @@ export const AuthPrompt = GObject.registerClass({ else this.setQuestion(question.replace(/[::] *$/, '').trim()); @@ -946,8 +1005,8 @@ index 4367ca1de..9e0557c2f 100644 this.emit('prompted'); } -@@ -448,10 +447,8 @@ export const AuthPrompt = GObject.registerClass({ - _onVerificationFailed(userVerifier, serviceName, canRetry) { +@@ -443,10 +442,8 @@ export const AuthPrompt = GObject.registerClass({ + _onVerificationFailed(serviceName, canRetry) { const wasQueryingService = this._queryingService === serviceName; - if (wasQueryingService) { @@ -958,7 +1017,7 @@ index 4367ca1de..9e0557c2f 100644 this.updateSensitivity({sensitive: canRetry}); this.setActorInDefaultButtonWell(this._nextButton); -@@ -557,6 +554,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -551,6 +548,7 @@ export const AuthPrompt = GObject.registerClass({ } clear() { @@ -966,74 +1025,93 @@ index 4367ca1de..9e0557c2f 100644 this._entry.text = ''; this._inactiveEntry.text = ''; this.stopSpinning(); -@@ -578,8 +576,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -571,8 +569,8 @@ export const AuthPrompt = GObject.registerClass({ this._authList.hide(); - this._entryArea.show(); - this._entry.grab_key_focus(); + this._fadeInElement(this._entryArea); ++ this.updateSensitivity({sensitive: true}); } _fadeInElement(element) { +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 7992cadb4..13605b7a4 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1168,7 +1168,6 @@ export const LoginDialog = GObject.registerClass({ + this._updateCancelButton(); + + this._sessionMenuButton.updateSensitivity(false); +- this._authPrompt.updateSensitivity({sensitive: true}); + this._showPrompt(); + } + -- -2.54.0 +2.55.0 -From b3b58f92b7f62c15c2b52a4725e501f30ec4932e Mon Sep 17 00:00:00 2001 +From 8d41aea60d8a313037d856364e40839621965f81 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:36:08 +0100 -Subject: [PATCH 15/54] authPrompt: Show entry area when displaying message +Subject: [PATCH 15/72] authPrompt: Show entry area when displaying message Since entryArea is hidden by default, we must explicitly make it visible when showing a message if no other widgets are visible. This allows getting a preemptive answer. + +Part-of: --- - js/gdm/authPrompt.js | 8 ++++++++ - 1 file changed, 8 insertions(+) + js/gdm/authPrompt.js | 10 ++++++++++ + 1 file changed, 10 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 9e0557c2f..2e3084535 100644 +index cf1046f94..6ede6be7c 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -442,6 +442,14 @@ export const AuthPrompt = GObject.registerClass({ - - this.setMessage(message, type, wiggleParameters); +@@ -429,6 +429,16 @@ export const AuthPrompt = GObject.registerClass({ + this.setMessage(message, type); this.emit('prompted'); -+ + + // If we're showing a message and no auth widget is currently visible, + // show the entry area to allow getting a preemptive answer + if (message && + type < GdmUtil.MessageType.ERROR && + !this._entryArea.visible && -+ !this._authList.visible) ++ !this._authList.visible) { + this._fadeInElement(this._entryArea); - } ++ this.updateSensitivity({sensitive: true}); ++ } ++ + const shouldWiggle = type === GdmUtil.MessageType.ERROR && + this._userVerifier.serviceIsFingerprint(serviceName); - _onVerificationFailed(userVerifier, serviceName, canRetry) { -- -2.54.0 +2.55.0 -From 040dcd66356f235e965c7ab15ea7f79b4c9b3119 Mon Sep 17 00:00:00 2001 +From a9755e89022db4cb4be4f325b8c37b1ad43197fb Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 19 May 2026 10:21:50 +0200 -Subject: [PATCH 16/54] authPrompt: Add loading signal +Subject: [PATCH 16/72] authPrompt: Add loading signal The signal indicates when the prompt is busy showing the spinner, so other components can adjust their UI accordingly. To make sure the signal is emitted consistently, always show and hide the spinner via `startSpinning()`/`stopSpinning()`. + +Part-of: --- js/gdm/authPrompt.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 2e3084535..aed460181 100644 +index 6ede6be7c..e27c1c329 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -52,6 +52,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -51,6 +51,7 @@ export const AuthPrompt = GObject.registerClass({ 'prompted': {}, 'reset': {param_types: [GObject.TYPE_UINT]}, 'verification-complete': {}, @@ -1041,7 +1119,7 @@ index 2e3084535..aed460181 100644 }, }, class AuthPrompt extends St.BoxLayout { _init(gdmClient, mode) { -@@ -459,7 +460,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -456,7 +457,7 @@ export const AuthPrompt = GObject.registerClass({ this._queryingService = null; this.updateSensitivity({sensitive: canRetry}); @@ -1050,7 +1128,7 @@ index 2e3084535..aed460181 100644 if (!canRetry) this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED; -@@ -469,7 +470,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -466,7 +467,7 @@ export const AuthPrompt = GObject.registerClass({ } _onVerificationComplete() { @@ -1059,7 +1137,7 @@ index 2e3084535..aed460181 100644 this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; this._mainBox.reactive = false; -@@ -553,12 +554,14 @@ export const AuthPrompt = GObject.registerClass({ +@@ -549,12 +550,14 @@ export const AuthPrompt = GObject.registerClass({ this._defaultButtonWellActor = actor; } @@ -1078,7 +1156,7 @@ index 2e3084535..aed460181 100644 } clear() { -@@ -690,7 +693,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -680,7 +683,7 @@ export const AuthPrompt = GObject.registerClass({ } vfunc_hide() { @@ -1088,13 +1166,13 @@ index 2e3084535..aed460181 100644 this._message.opacity = 0; -- -2.54.0 +2.55.0 -From 9e1d08ac5961c227d468acb0ee7ae3d7889de0a4 Mon Sep 17 00:00:00 2001 +From 7b06c441ad7dcd0195657ce2a18f844337d6ae7b Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 19 May 2026 10:24:38 +0200 -Subject: [PATCH 17/54] authPrompt: Don't delay spinner +Subject: [PATCH 17/72] authPrompt: Don't delay spinner We currently animate the spinner with a delay. This animation is intended to create a seamless transition of one icon @@ -1102,12 +1180,14 @@ to another. The delay must be used on the second animation to wait until the first one completes. So remove the first delay, and use the animation time as a delay. + +Part-of: --- js/gdm/authPrompt.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index aed460181..14bd6048d 100644 +index e27c1c329..45c77df39 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -16,7 +16,6 @@ import * as UserWidget from '../ui/userWidget.js'; @@ -1116,9 +1196,9 @@ index aed460181..14bd6048d 100644 const DEFAULT_BUTTON_WELL_ICON_SIZE = 16; -const DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; const DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; - const MESSAGE_FADE_OUT_ANIMATION_TIME = 500; -@@ -523,7 +522,6 @@ export const AuthPrompt = GObject.registerClass({ + +@@ -519,7 +518,6 @@ export const AuthPrompt = GObject.registerClass({ oldActor.ease({ opacity: 0, duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, @@ -1126,7 +1206,7 @@ index aed460181..14bd6048d 100644 mode: Clutter.AnimationMode.LINEAR, onComplete: () => { if (wasSpinner) { -@@ -545,7 +543,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -541,7 +539,7 @@ export const AuthPrompt = GObject.registerClass({ actor.ease({ opacity: 255, duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, @@ -1136,23 +1216,24 @@ index aed460181..14bd6048d 100644 }); } -- -2.54.0 +2.55.0 -From 6083120cf9c832d0abc55495c3c590b49bc44da2 Mon Sep 17 00:00:00 2001 +From c98ca7986406c6f2afff0842039286ce57173733 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 17:28:54 +0100 -Subject: [PATCH 18/54] authPrompt: Simplify some code +Subject: [PATCH 18/72] authPrompt: Simplify some code +Part-of: --- - js/gdm/authPrompt.js | 92 +++++++++++++++++++------------------------- - 1 file changed, 39 insertions(+), 53 deletions(-) + js/gdm/authPrompt.js | 94 +++++++++++++++++++------------------------- + 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 14bd6048d..ff9a7252b 100644 +index 45c77df39..0192db381 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -242,11 +242,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -241,11 +241,7 @@ export const AuthPrompt = GObject.registerClass({ this._fadeOutMessage(); }); @@ -1165,7 +1246,7 @@ index 14bd6048d..ff9a7252b 100644 }); this._defaultButtonWell = new St.Widget({ -@@ -319,13 +315,16 @@ export const AuthPrompt = GObject.registerClass({ +@@ -318,14 +314,17 @@ export const AuthPrompt = GObject.registerClass({ this._timedLoginIndicator.scale_x = 0.; } @@ -1177,15 +1258,17 @@ index 14bd6048d..ff9a7252b 100644 this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS; this.updateSensitivity({sensitive: false}); - if (this._queryingService) { +- if (this._queryingService) { - if (shouldSpin) - this.startSpinning(); -+ if (this._entry === this._passwordEntry) -+ this.startSpinning({animate: true}); ++ if (this._entry === this._passwordEntry) ++ this.startSpinning({animate: true}); ++ if (this._queryingService) { this._userVerifier.answerQuery(this._queryingService, this._entry.text); } else { -@@ -489,64 +488,51 @@ export const AuthPrompt = GObject.registerClass({ + this._preemptiveAnswer = this._entry.text; +@@ -485,64 +484,51 @@ export const AuthPrompt = GObject.registerClass({ } setActorInDefaultButtonWell(actor, animate) { @@ -1283,35 +1366,37 @@ index 14bd6048d..ff9a7252b 100644 this._defaultButtonWellActor = actor; -- -2.54.0 +2.55.0 -From cdb58710428b47550fe4aa489287fe705f5462fe Mon Sep 17 00:00:00 2001 +From d26fcef697855a6990b93c94832fa465877ed333 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 26 May 2026 16:48:43 +0200 -Subject: [PATCH 19/54] authPrompt: Ensure this._message is always visible +Subject: [PATCH 19/72] authPrompt: Ensure this._message is always visible this._message visibility is updated with opacity property. This way authPrompt layout isn't modified so all elements stay on the same position as expected. + +Part-of: --- js/gdm/authPrompt.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index ff9a7252b..935a1ab6c 100644 +index 0192db381..60a5a4300 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -600,8 +600,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -594,8 +594,6 @@ export const AuthPrompt = GObject.registerClass({ } this._entryArea.hide(); - if (this._message.text === '') - this._message.hide(); this._fadeInElement(this._authList); + this.updateSensitivity({sensitive: true}); } - -@@ -640,7 +638,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -635,7 +633,6 @@ export const AuthPrompt = GObject.registerClass({ else this._message.remove_style_class_name('login-dialog-message-hint'); @@ -1320,24 +1405,26 @@ index ff9a7252b..935a1ab6c 100644 this._message.remove_all_transitions(); this._message.text = message; -- -2.54.0 +2.55.0 -From 42df2462df05911e61613d8a8f4b7dd82861fb84 Mon Sep 17 00:00:00 2001 +From a25f4822dc914adb9aecf0b2d64c2921d040a550 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Mar 2026 18:32:48 +0100 -Subject: [PATCH 20/54] authPrompt: Skip reset after successful verification +Subject: [PATCH 20/72] authPrompt: Skip reset after successful verification Don't reset the prompt if verification has already succeeded. + +Part-of: --- js/gdm/authPrompt.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 935a1ab6c..692fc62cf 100644 +index 60a5a4300..0550d2659 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -483,6 +483,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -479,6 +479,9 @@ export const AuthPrompt = GObject.registerClass({ } _onReset() { @@ -1348,33 +1435,44 @@ index 935a1ab6c..692fc62cf 100644 this.reset(); } -- -2.54.0 +2.55.0 -From 281a61563e57726e0e1583b5978b22a9c41cda35 Mon Sep 17 00:00:00 2001 +From 4ae886f514dafa62f525f4a1f42249059b05a750 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:06:32 -0500 -Subject: [PATCH 21/54] authPrompt: Parameterize reset function +Subject: [PATCH 21/72] authPrompt: Parameterize reset function In the future, userVerifier will request a partial reset where some state is carried over or explicitly specified. This commit prepares for that by allowing the request type and reusing entry text to be specified at reset time. + +Part-of: --- - js/gdm/authPrompt.js | 36 ++++++++++++++++++++++++------------ - 1 file changed, 24 insertions(+), 12 deletions(-) + js/gdm/authPrompt.js | 38 +++++++++++++++++++++++++------------- + 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 692fc62cf..541927655 100644 +index 0550d2659..5d58d62af 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -482,12 +482,11 @@ export const AuthPrompt = GObject.registerClass({ +@@ -85,7 +85,7 @@ export const AuthPrompt = GObject.registerClass({ + 'show-choice-list', (_, ...args) => this._onShowChoiceList(...args), + 'verification-failed', (_, ...args) => this._onVerificationFailed(...args), + 'verification-complete', () => this._onVerificationComplete(), +- 'reset', () => this._onReset(), ++ 'reset', (_, ...args) => this._onReset(...args), + 'smartcard-status-changed', () => this._onSmartcardStatusChanged(), + 'credential-manager-authenticated', () => this._onCredentialManagerAuthenticated(), + this); +@@ -478,12 +478,11 @@ export const AuthPrompt = GObject.registerClass({ this.emit('verification-complete'); } - _onReset() { -+ _onReset(_userVerifier, resetParams) { ++ _onReset(resetParams) { if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) return; @@ -1384,7 +1482,7 @@ index 692fc62cf..541927655 100644 } setActorInDefaultButtonWell(actor, animate) { -@@ -551,11 +550,18 @@ export const AuthPrompt = GObject.registerClass({ +@@ -547,11 +546,18 @@ export const AuthPrompt = GObject.registerClass({ this.setActorInDefaultButtonWell(this._nextButton, animate); } @@ -1408,7 +1506,7 @@ index 692fc62cf..541927655 100644 this._authList.clear(); this._authList.hide(); -@@ -712,8 +718,13 @@ export const AuthPrompt = GObject.registerClass({ +@@ -702,8 +708,13 @@ export const AuthPrompt = GObject.registerClass({ this.updateSensitivity(false); } @@ -1424,7 +1522,7 @@ index 692fc62cf..541927655 100644 this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this.cancelButton.reactive = this._hasCancelButton; this.cancelButton.can_focus = this._hasCancelButton; -@@ -729,7 +740,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -719,7 +730,7 @@ export const AuthPrompt = GObject.registerClass({ this._userVerifier.cancel(); this._queryingService = null; @@ -1433,7 +1531,7 @@ index 692fc62cf..541927655 100644 this._message.opacity = 0; this.setUser(null); this._updateEntry(true); -@@ -752,7 +763,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -742,7 +753,8 @@ export const AuthPrompt = GObject.registerClass({ // We don't need to know the username if the user preempted the login screen // with a smartcard or with preauthenticated oVirt credentials beginRequestType = BeginRequestType.DONT_PROVIDE_USERNAME; @@ -1444,24 +1542,26 @@ index 692fc62cf..541927655 100644 beginRequestType = BeginRequestType.REUSE_USERNAME; } else { -- -2.54.0 +2.55.0 -From 9e6a60dd50153ca81c2bd938340f5672f4600f38 Mon Sep 17 00:00:00 2001 +From b5375704f551681027a73dd53d43ca10e6954c34 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 19 May 2026 15:18:23 +0200 -Subject: [PATCH 22/54] authPrompt: Remove this.stopSpinnning() in reset +Subject: [PATCH 22/72] authPrompt: Remove this.stopSpinnning() in reset It's already in this.clear() which is called from reset. + +Part-of: --- js/gdm/authPrompt.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 541927655..948dffc13 100644 +index 5d58d62af..4b92fd453 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -744,7 +744,6 @@ export const AuthPrompt = GObject.registerClass({ +@@ -734,7 +734,6 @@ export const AuthPrompt = GObject.registerClass({ this._message.opacity = 0; this.setUser(null); this._updateEntry(true); @@ -1470,46 +1570,128 @@ index 541927655..948dffc13 100644 if (oldStatus === AuthPromptStatus.VERIFICATION_FAILED) this.emit('failed'); -- -2.54.0 +2.55.0 -From f4fcb1738f2398f7486e30be6c093b3d770febcc Mon Sep 17 00:00:00 2001 +From 6769207733854f56fccf236a6309de25ee5924ae Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 21 Apr 2026 13:06:57 +0200 -Subject: [PATCH 23/54] authPrompt: Ensure hint_text in entries is cleared +Subject: [PATCH 23/72] authPrompt: Ensure hint_text in entries is cleared There can be cases when a new authentication is started after a reset, but hint_text isn't cleared, displaying the hint from previous authentication. This fixes it. + +Part-of: --- js/gdm/authPrompt.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 948dffc13..5d745db76 100644 +index 4b92fd453..1f8f555b0 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -555,6 +555,8 @@ export const AuthPrompt = GObject.registerClass({ - reuseEntryText: false, +@@ -552,6 +552,8 @@ export const AuthPrompt = GObject.registerClass({ }); -+ this._entry.hint_text = ''; -+ this._inactiveEntry.hint_text = ''; if (!reuseEntryText) { ++ this._entry.hint_text = ''; ++ this._inactiveEntry.hint_text = ''; this._entryArea.hide(); this._entry.text = ''; + this._inactiveEntry.text = ''; -- -2.54.0 +2.55.0 -From 383b002b0fb9c654ef17111c0364d436de70c91c Mon Sep 17 00:00:00 2001 +From 3652a7defc841415f77c05362c27837f6d34631d Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 3 Jul 2026 13:06:26 +0200 +Subject: [PATCH 24/72] authPrompt: Separate user and entry updates + +setUser(null) triggers _updateEntry(false), swapping the password +entry for the text entry, only for _updateEntry(true) right after +to swap it back. This double swap causes a visible blink. + +Make _updateEntry public and remove the automatic entry switch from +setUser. Now setUser only updates the avatar icon, and entry type +changes must be done explicitly via updateEntry. This gives callers +more control and avoids the blink issue while ensuring the user is +always properly set in reset(). + +Part-of: +--- + js/gdm/authPrompt.js | 9 +++------ + js/gdm/loginDialog.js | 1 + + 2 files changed, 4 insertions(+), 6 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 1f8f555b0..996b3710a 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -338,7 +338,7 @@ export const AuthPrompt = GObject.registerClass({ + this.emit('next'); + } + +- _updateEntry(secret) { ++ updateEntry(secret) { + let newEntry, inactiveEntry; + + if (secret && this._entry !== this._passwordEntry) { +@@ -372,7 +372,7 @@ export const AuthPrompt = GObject.registerClass({ + return; + } + +- this._updateEntry(secret); ++ this.updateEntry(secret); + + // Hack: The question string comes directly from PAM, if it's "Password:" + // we replace it with our own to allow localization, if it's something +@@ -692,9 +692,6 @@ export const AuthPrompt = GObject.registerClass({ + + let userWidget = new UserWidget.UserWidget(user, Clutter.Orientation.VERTICAL); + this._userWell.set_child(userWidget); +- +- if (!user) +- this._updateEntry(false); + } + + _onUserStoppedTypePreemptiveAnswer() { +@@ -735,7 +732,7 @@ export const AuthPrompt = GObject.registerClass({ + this.clear({reuseEntryText}); + this._message.opacity = 0; + this.setUser(null); +- this._updateEntry(true); ++ this.updateEntry(true); + + if (oldStatus === AuthPromptStatus.VERIFICATION_FAILED) + this.emit('failed'); +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 13605b7a4..2bfaf25f8 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1148,6 +1148,7 @@ export const LoginDialog = GObject.registerClass({ + + _askForUsernameAndBeginVerification() { + this._authPrompt.setUser(null); ++ this._authPrompt.updateEntry(false); + this._authPrompt.setQuestion(_('Username')); + + this._showRealmLoginHint(this._realmManager.loginFormat); +-- +2.55.0 + + +From 1989361b8a715346124e53489a1362ee069bc8a8 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 12 Feb 2026 16:56:48 +0100 -Subject: [PATCH 24/54] authPrompt: Rename BeginRequestType to ResetType +Subject: [PATCH 25/72] authPrompt: Rename BeginRequestType to ResetType The enum is emitted with the 'reset' signal and describes the type of reset being performed, so ResetType is a clearer name. + +Part-of: --- js/gdm/authPrompt.js | 14 +++++++------- js/gdm/loginDialog.js | 6 +++--- @@ -1517,10 +1699,10 @@ of reset being performed, so ResetType is a clearer name. 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 5d745db76..7f3752d82 100644 +index 996b3710a..e16d3f291 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -37,7 +37,7 @@ export const AuthPromptStatus = { +@@ -36,7 +36,7 @@ export const AuthPromptStatus = { }; /** @enum {number} */ @@ -1529,7 +1711,7 @@ index 5d745db76..7f3752d82 100644 PROVIDE_USERNAME: 0, DONT_PROVIDE_USERNAME: 1, REUSE_USERNAME: 2, -@@ -752,28 +752,28 @@ export const AuthPrompt = GObject.registerClass({ +@@ -739,28 +739,28 @@ export const AuthPrompt = GObject.registerClass({ else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) this.emit('cancelled'); @@ -1565,7 +1747,7 @@ index 5d745db76..7f3752d82 100644 addCharacter(unichar) { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index f75497065..f38de0c91 100644 +index 2bfaf25f8..8aa2feb3c 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -1066,7 +1066,7 @@ export const LoginDialog = GObject.registerClass({ @@ -1608,18 +1790,20 @@ index 85e469fb8..980353fe1 100644 userName = this._userName; } else { -- -2.54.0 +2.55.0 -From cf3bb980c1f312897a9f42e9ef578d908a9ef0ab Mon Sep 17 00:00:00 2001 +From be42e046e2170919cfff0e9bc9cfde933c2f47d8 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 11 Feb 2026 16:11:59 +0100 -Subject: [PATCH 25/54] unlockDialog: Use isprint() instead of isgraph() for +Subject: [PATCH 26/72] unlockDialog: Use isprint() instead of isgraph() for preemptive input isgraph() returns true for printable characters except space, while isprint() includes space as a valid character. This allows users to type passwords containing spaces during preemptive input. + +Part-of: --- js/ui/unlockDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) @@ -1638,13 +1822,13 @@ index 980353fe1..b0f51e533 100644 return Clutter.EVENT_PROPAGATE; -- -2.54.0 +2.55.0 -From 5fa48f56386c9e8255c6767a7c2864fc51ffdf1b Mon Sep 17 00:00:00 2001 +From ceb6e2b0e1e2c6a2715f3c116abbbd621d38097d Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 21 Jan 2026 14:34:11 +0100 -Subject: [PATCH 26/54] authPrompt: Capture preemptive input before entry is +Subject: [PATCH 27/72] authPrompt: Capture preemptive input before entry is sensitive The previous changes made the entry invisible and insensitive until @@ -1654,43 +1838,69 @@ typing before the PAM service is ready, keystrokes were being lost. Allow the entry to become sensitive during lock screen animation to ensure no keystrokes are lost. +While there's a preemptive answer we don't want to enable sensitivity of +authWidgets, there can be some cases now with a preemptiveInput where +authWidgets want to become sensitive, add a guard to ensure it never happens +when there's a preemptive answer. + Also, remove unsused addCharacter() in loginDialog.js. + +Part-of: --- - js/gdm/authPrompt.js | 26 ++++++++++++++++++-------- + js/gdm/authPrompt.js | 37 +++++++++++++++++++++++++------------ js/gdm/loginDialog.js | 4 ---- js/ui/unlockDialog.js | 2 +- - 3 files changed, 19 insertions(+), 13 deletions(-) + 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 7f3752d82..137361d24 100644 +index e16d3f291..6fa69293a 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -593,12 +593,22 @@ export const AuthPrompt = GObject.registerClass({ - opacity: 0, - visible: true, - }); -- this.updateSensitivity({sensitive: false}); -+ // Don't disable sensitivity on preemptive input -+ if (!this._preemptiveInput) -+ this.updateSensitivity({sensitive: false}); +@@ -335,6 +335,8 @@ export const AuthPrompt = GObject.registerClass({ + } + } + ++ this._preemptiveInput = false; + - element.ease({ - opacity: 255, - duration: MESSAGE_FADE_OUT_ANIMATION_TIME, - transition: Clutter.AnimationMode.EASE_OUT_QUAD, -- onComplete: () => this.updateSensitivity({sensitive: true}), -+ onComplete: () => { -+ this.updateSensitivity({sensitive: true}); -+ this._preemptiveInput = false; -+ }, -+ onStopped: isFinished => { -+ if (!isFinished) -+ this._preemptiveInput = false; -+ }, - }); + this.emit('next'); } -@@ -721,7 +731,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -366,9 +368,11 @@ export const AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- if (this._preemptiveAnswer) { +- this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); +- this._preemptiveAnswer = null; ++ ++ const preemptiveAnswer = this._preemptiveAnswer; ++ this._clearPreemptiveState(); ++ if (preemptiveAnswer) { ++ this._userVerifier.answerQuery(this._queryingService, preemptiveAnswer); + return; + } + +@@ -391,8 +395,7 @@ export const AuthPrompt = GObject.registerClass({ + + this._queryingService = serviceName; + +- if (this._preemptiveAnswer) +- this._preemptiveAnswer = null; ++ this._clearPreemptiveState(); + + this.setChoiceList(promptMessage, choiceList); + this.updateSensitivity({sensitive: true}); +@@ -655,6 +658,9 @@ export const AuthPrompt = GObject.registerClass({ + } + + updateSensitivity({sensitive}) { ++ if (sensitive && this._preemptiveAnswer) ++ return; ++ + const authWidget = [ + this._authList, + ].find(widget => widget.visible) ?? this._entry; +@@ -708,7 +714,7 @@ export const AuthPrompt = GObject.registerClass({ } reset(params) { @@ -1699,7 +1909,7 @@ index 7f3752d82..137361d24 100644 reuseEntryText: false, softReset: false, }); -@@ -741,6 +751,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -728,6 +734,8 @@ export const AuthPrompt = GObject.registerClass({ if (this._userVerifier) this._userVerifier.cancel(); @@ -1708,26 +1918,43 @@ index 7f3752d82..137361d24 100644 this._queryingService = null; this.clear({reuseEntryText}); this._message.opacity = 0; -@@ -776,11 +788,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -763,12 +771,16 @@ export const AuthPrompt = GObject.registerClass({ this.emit('reset', resetType); } - addCharacter(unichar) { - if (!this._entry.visible) - return; -- -- this._entry.grab_key_focus(); + startPreemptiveInput(unichar) { + this._preemptiveInput = true; + this.updateSensitivity({sensitive: true}); - this._entry.clutter_text.insert_unichar(unichar); ++ if (unichar) ++ this._entry.clutter_text.insert_unichar(unichar); ++ } + +- this._entry.grab_key_focus(); +- this._entry.clutter_text.insert_unichar(unichar); ++ _clearPreemptiveState() { ++ this._preemptiveInput = false; ++ this._preemptiveAnswer = null; } + begin(params) { +@@ -777,7 +789,8 @@ export const AuthPrompt = GObject.registerClass({ + hold: null, + }); + +- this.updateSensitivity({sensitive: false}); ++ if (!this._preemptiveInput) ++ this.updateSensitivity({sensitive: false}); + + let hold = params.hold; + if (!hold) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index f38de0c91..09e8073c6 100644 +index 8aa2feb3c..9355eef45 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -1593,10 +1593,6 @@ export const LoginDialog = GObject.registerClass({ +@@ -1592,10 +1592,6 @@ export const LoginDialog = GObject.registerClass({ this._authPrompt.cancel(); } @@ -1752,13 +1979,13 @@ index b0f51e533..100990acc 100644 return Clutter.EVENT_PROPAGATE; } -- -2.54.0 +2.55.0 -From f1228badd7cacfd5341b8be9ce7a51a461e45f16 Mon Sep 17 00:00:00 2001 +From ae380541ccdf30d525235c4d4f74e3c1b2311826 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Feb 2026 18:51:41 +0100 -Subject: [PATCH 27/54] authPrompt: On verificationFailed allow a +Subject: [PATCH 28/72] authPrompt: On verificationFailed allow a preemptiveInput There's a time window between the verification failing and a new @@ -1766,27 +1993,30 @@ verification request being started. During this time, allow input sensitivity so a new answer can be inserted before being requested. + +Part-of: --- - js/gdm/authPrompt.js | 7 +++++-- - 1 file changed, 5 insertions(+), 2 deletions(-) + js/gdm/authPrompt.js | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 137361d24..2799094ea 100644 +index 6fa69293a..9d2c2d0b4 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -457,7 +457,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -457,7 +457,11 @@ export const AuthPrompt = GObject.registerClass({ if (wasQueryingService) this._queryingService = null; - this.updateSensitivity({sensitive: canRetry}); + if (canRetry) { ++ this.verificationStatus = AuthPromptStatus.VERIFYING; + this._entry.text = ''; -+ this.updateSensitivity({sensitive: true}); ++ this.startPreemptiveInput(); + } this.stopSpinning(); if (!canRetry) -@@ -751,7 +754,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -734,7 +738,7 @@ export const AuthPrompt = GObject.registerClass({ if (this._userVerifier) this._userVerifier.cancel(); @@ -1796,13 +2026,13 @@ index 137361d24..2799094ea 100644 this._queryingService = null; this.clear({reuseEntryText}); -- -2.54.0 +2.55.0 -From f6df2443529f8b0a976dc21043337c7f6f0f80a1 Mon Sep 17 00:00:00 2001 +From caa66bae4e0356d444a109f045f989c1e0330bfd Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 30 Sep 2025 17:43:22 +0200 -Subject: [PATCH 28/54] authPrompt: Update authList style +Subject: [PATCH 29/72] authPrompt: Update authList style Make the AuthListItem buttons a bit bigger and more rounded. @@ -1816,18 +2046,20 @@ back button is visible and properly aligned. Use accessible_name. Register classes with the new style. + +Part-of: --- - .../gnome-shell-sass/widgets/_login-lock.scss | 95 ++++++++-- - js/gdm/authList.js | 164 +++++++++++++++--- - js/gdm/authPrompt.js | 43 +++-- + .../gnome-shell-sass/widgets/_login-lock.scss | 99 ++++++++-- + js/gdm/authList.js | 181 +++++++++++++++--- + js/gdm/authPrompt.js | 44 +++-- js/gdm/util.js | 6 +- - 4 files changed, 255 insertions(+), 53 deletions(-) + 4 files changed, 276 insertions(+), 54 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 7024cc58d..f498d1f5d 100644 +index 3695014f8..0fc13e061 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -149,42 +149,107 @@ $_gdm_dialog_width: 25em; +@@ -146,42 +146,111 @@ $_gdm_dialog_width: 25em; // Authentication methods list .login-dialog-auth-list-view { -st-vfade-offset: 3em; @@ -1866,6 +2098,10 @@ index 7024cc58d..f498d1f5d 100644 + padding: $base_padding * 1.5; + margin-bottom: $base_margin; + margin-right: $base_margin; ++ ++ &.compact { ++ min-height: 1.3em; ++ } } } @@ -1951,10 +2187,10 @@ index 7024cc58d..f498d1f5d 100644 // User list .login-dialog-user-list-view { diff --git a/js/gdm/authList.js b/js/gdm/authList.js -index 4873a05c5..ae90f9874 100644 +index 4873a05c5..81c204e7f 100644 --- a/js/gdm/authList.js +++ b/js/gdm/authList.js -@@ -18,30 +18,140 @@ +@@ -18,30 +18,143 @@ import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; @@ -1979,7 +2215,7 @@ index 4873a05c5..ae90f9874 100644 - style_class: 'login-dialog-auth-list-label', - y_align: Clutter.ActorAlign.CENTER, - x_expand: true, -+const ItemIconPopup = class extends PopupMenu.PopupMenu { ++class ItemIconPopup extends PopupMenu.PopupMenu { + constructor(sourceActor, title, subtitle) { + super(sourceActor, 0.5, St.Side.TOP); + @@ -1989,19 +2225,20 @@ index 4873a05c5..ae90f9874 100644 + const labels = new St.BoxLayout({ + orientation: Clutter.Orientation.VERTICAL, + style_class: 'login-dialog-item-icon-popup-labels', -+ }); + }); + labels.add_child(new St.Label({text: title})); + labels.add_child(new St.Label({text: subtitle})); -+ + +- super._init({ + const item = new PopupMenu.PopupBaseMenuItem({ + reactive: false, + can_focus: false, - }); ++ }); + item.add_child(labels); + this.addMenuItem(item); + -+ sourceActor.connect('clicked', () => this.toggle()); -+ sourceActor.connect('destroy', () => this.destroy()); ++ sourceActor.connectObject('clicked', () => this.toggle(), this); ++ sourceActor.connectObject('destroy', () => this.destroy(), this); + + this.actor.hide(); + @@ -2020,6 +2257,7 @@ index 4873a05c5..ae90f9874 100644 + constructor(iconName, iconTitle, iconSubtitle) { + super({ + style_class: 'login-dialog-item-icon', ++ button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, + iconName, + }); + @@ -2036,8 +2274,7 @@ index 4873a05c5..ae90f9874 100644 + static { + GObject.registerClass(this); + } - -- super._init({ ++ + constructor(key, content) { + const {title, subtitle, iconName, iconTitle, iconSubtitle} = content; + @@ -2103,11 +2340,13 @@ index 4873a05c5..ae90f9874 100644 + this._container.add_child(icon); + } + ++ this.compact = !subtitle && !iconName; ++ + this.set_child(this._container); this.connect('key-focus-in', () => this._setSelected(true)); -@@ -65,25 +175,26 @@ const AuthListItem = GObject.registerClass({ +@@ -65,25 +178,26 @@ const AuthListItem = GObject.registerClass({ this.remove_style_pseudo_class('selected'); } } @@ -2136,7 +2375,7 @@ index 4873a05c5..ae90f9874 100644 style_class: 'login-dialog-auth-list-layout', - x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, -+ x_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, }); - this.label = new St.Label({style_class: 'login-dialog-auth-list-title'}); @@ -2145,11 +2384,32 @@ index 4873a05c5..ae90f9874 100644 this._box = new St.BoxLayout({ orientation: Clutter.Orientation.VERTICAL, style_class: 'login-dialog-auth-list', -@@ -136,10 +247,10 @@ export const AuthList = GObject.registerClass({ +@@ -98,6 +212,10 @@ export const AuthList = GObject.registerClass({ + + this._items = new Map(); + ++ this._box.connectObject( ++ 'child-added', () => this._updateItemsLayout(), ++ 'child-removed', () => this._updateItemsLayout(), ++ this); + this.connect('key-focus-in', this._moveFocusToItems.bind(this)); + } + +@@ -136,10 +254,20 @@ export const AuthList = GObject.registerClass({ }); } - addItem(key, text) { ++ _updateItemsLayout() { ++ const items = this._box.get_children(); ++ ++ const compact = items.every(item => item.compact); ++ if (compact) ++ items.forEach(item => item.add_style_class_name('compact')); ++ else ++ items.forEach(item => item.remove_style_class_name('compact')); ++ } ++ + addItem(key, content) { this.removeItem(key); @@ -2158,7 +2418,7 @@ index 4873a05c5..ae90f9874 100644 this._box.add_child(item); this._items.set(key, item); -@@ -170,8 +281,7 @@ export const AuthList = GObject.registerClass({ +@@ -170,8 +298,7 @@ export const AuthList = GObject.registerClass({ } clear() { @@ -2169,10 +2429,10 @@ index 4873a05c5..ae90f9874 100644 -}); +} diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index 2799094ea..de53ced39 100644 +index 9d2c2d0b4..56dfef305 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -180,11 +180,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -179,25 +179,46 @@ export const AuthPrompt = GObject.registerClass({ this._mainBox.add_child(this.cancelButton); this._authList = new AuthList.AuthList(); @@ -2185,8 +2445,9 @@ index 2799094ea..de53ced39 100644 this._authList.connect('activate', (list, key) => { this._authList.reactive = false; this._authList.ease({ -@@ -192,13 +188,39 @@ export const AuthPrompt = GObject.registerClass({ - duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + opacity: 0, +- duration: MESSAGE_FADE_OUT_ANIMATION_TIME, ++ duration: MESSAGE_FADE_OUT_ANIMATION_TIME * 0.5, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { + this._authListTitle.child.text = ''; @@ -2197,17 +2458,13 @@ index 2799094ea..de53ced39 100644 }); }); - this._mainBox.add_child(this._authList); -+ this._inputWell.add_child(this._authList); ++ this.add_child(this._authList); + -+ // Use an insensitive button for the auth list title -+ // to get the same style as the auth list buttons -+ this._authListTitle = new St.Button({ ++ this._authListTitle = new St.Bin({ + style_class: 'login-dialog-auth-list-title', + x_expand: true, + y_expand: true, + child: new St.Label({style_class: 'login-dialog-auth-list-title-label'}), -+ reactive: false, -+ can_focus: false, + visible: false, + }); + this._authList.bind_property('visible', @@ -2218,15 +2475,18 @@ index 2799094ea..de53ced39 100644 + GObject.BindingFlags.DEFAULT); + this._mainBox.add_child(this._authListTitle); + -+ // Use bind_property instead of BindConstraint because -+ // BoxLayout allocation overrides constraint-set width. -+ this._authListTitle.bind_property('width', -+ this._authList, 'width', -+ GObject.BindingFlags.DEFAULT); ++ this._authList.add_constraint(new Clutter.BindConstraint({ ++ coordinate: Clutter.BindCoordinate.WIDTH, ++ source: this._authListTitle, ++ })); ++ this._authList.add_constraint(new Clutter.BindConstraint({ ++ coordinate: Clutter.BindCoordinate.X, ++ source: this._authListTitle, ++ })); this._entryArea = new St.Widget({ style_class: 'login-dialog-prompt-entry-area', -@@ -567,6 +589,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -567,6 +588,7 @@ export const AuthPrompt = GObject.registerClass({ this.stopSpinning(); } @@ -2234,7 +2494,7 @@ index 2799094ea..de53ced39 100644 this._authList.clear(); this._authList.hide(); -@@ -617,10 +640,10 @@ export const AuthPrompt = GObject.registerClass({ +@@ -605,10 +627,10 @@ export const AuthPrompt = GObject.registerClass({ setChoiceList(promptMessage, choiceList) { this._authList.clear(); @@ -2250,10 +2510,10 @@ index 2799094ea..de53ced39 100644 this._entryArea.hide(); diff --git a/js/gdm/util.js b/js/gdm/util.js -index 4b0f763f0..b71251458 100644 +index e529f84fa..72ee9d29b 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -723,7 +723,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -730,7 +730,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (!this.serviceIsForeground(serviceName)) return; @@ -2267,13 +2527,13 @@ index 4b0f763f0..b71251458 100644 _onInfo(client, serviceName, info) { -- -2.54.0 +2.55.0 -From 3ee1292db420b3314755a6ee4c67169d38c1d35f Mon Sep 17 00:00:00 2001 +From 87c6792190c9cbdd258ba78c262e8d63b7e1d43c Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 8 Oct 2025 18:48:07 +0200 -Subject: [PATCH 29/54] authPrompt: Let back button go back to step 1 instead +Subject: [PATCH 30/72] authPrompt: Let back button go back to step 1 instead of full reset There can be some auth methods that would require multiple steps. In the @@ -2282,15 +2542,17 @@ and display the user list. Now if we're in a multi-step flow (step > 1), go back to step 1 instead of full reset. Show backButton in unlockDialog for these cases. + +Part-of: --- - js/gdm/authPrompt.js | 36 ++++++++++++++++++++++++++++-------- - 1 file changed, 28 insertions(+), 8 deletions(-) + js/gdm/authPrompt.js | 38 +++++++++++++++++++++++++++----------- + 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index de53ced39..e283adfb7 100644 +index 56dfef305..034357d9f 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -69,6 +69,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -68,6 +68,7 @@ export const AuthPrompt = GObject.registerClass({ this._mode = mode; this._defaultButtonWellActor = null; this._cancelledRetries = 0; @@ -2298,9 +2560,21 @@ index de53ced39..e283adfb7 100644 this._idleMonitor = global.backend.get_core_idle_monitor(); -@@ -167,16 +168,14 @@ export const AuthPrompt = GObject.registerClass({ +@@ -99,8 +100,6 @@ export const AuthPrompt = GObject.registerClass({ + }); + this.add_child(this._userWell); + +- this._hasCancelButton = this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN; +- + this._initInputRow(); + + let capsLockPlaceholder = new St.Label(); +@@ -164,18 +163,16 @@ export const AuthPrompt = GObject.registerClass({ + + this.cancelButton = new St.Button({ style_class: 'login-dialog-button cancel-button', - accessible_name: _('Cancel'), +- accessible_name: _('Cancel'), ++ accessible_name: _('Back'), button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, - reactive: this._hasCancelButton, - can_focus: this._hasCancelButton, @@ -2319,7 +2593,7 @@ index de53ced39..e283adfb7 100644 this._mainBox.add_child(this.cancelButton); this._authList = new AuthList.AuthList(); -@@ -303,6 +302,16 @@ export const AuthPrompt = GObject.registerClass({ +@@ -301,6 +298,15 @@ export const AuthPrompt = GObject.registerClass({ })); } @@ -2330,32 +2604,30 @@ index de53ced39..e283adfb7 100644 + const cancelVisible = this._promptStep > 1; + this.cancelButton.opacity = cancelVisible ? 255 : 0; + this.cancelButton.reactive = cancelVisible; -+ this.cancelButton.can_focus = cancelVisible; + } + showTimedLoginIndicator(time) { let hold = new Batch.Hold(); -@@ -389,6 +398,9 @@ export const AuthPrompt = GObject.registerClass({ - this.clear(); - - this._queryingService = serviceName; -+ this._promptStep++; -+ this._updateCancelButton(); -+ - if (this._preemptiveAnswer) { - this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); - this._preemptiveAnswer = null; -@@ -413,6 +425,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -389,6 +395,8 @@ export const AuthPrompt = GObject.registerClass({ this.clear(); this._queryingService = serviceName; + this._promptStep++; + this._updateCancelButton(); - if (this._preemptiveAnswer) - this._preemptiveAnswer = null; -@@ -764,10 +778,10 @@ export const AuthPrompt = GObject.registerClass({ + const preemptiveAnswer = this._preemptiveAnswer; + this._clearPreemptiveState(); +@@ -415,6 +423,8 @@ export const AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; ++ this._promptStep++; ++ this._updateCancelButton(); + + this._clearPreemptiveState(); + +@@ -747,10 +757,10 @@ export const AuthPrompt = GObject.registerClass({ const oldStatus = this.verificationStatus; this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; @@ -2368,7 +2640,7 @@ index de53ced39..e283adfb7 100644 if (this._preemptiveAnswerWatchId) this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); -@@ -854,6 +868,12 @@ export const AuthPrompt = GObject.registerClass({ +@@ -844,6 +854,12 @@ export const AuthPrompt = GObject.registerClass({ if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) return; @@ -2382,31 +2654,33 @@ index de53ced39..e283adfb7 100644 this._cancelledRetries++; if (this._cancelledRetries > this._userVerifier.allowedFailures) -- -2.54.0 +2.55.0 -From 18ec89f28691df35659781d1c0abf9dfb1a303fd Mon Sep 17 00:00:00 2001 +From 778b7929c3ed03f78ea2ec1cea08ad82338d04e4 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 20 Oct 2025 17:24:40 +0200 -Subject: [PATCH 30/54] loginDialog: Vertically center authPrompt using fixed +Subject: [PATCH 31/72] loginDialog: Vertically center authPrompt using fixed height Use a fixed estimated height for centering so the position stays stable regardless of actual content height changes during user interaction. + +Part-of: --- js/gdm/loginDialog.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 09e8073c6..3a22bf161 100644 +index 9355eef45..b098891c4 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -45,6 +45,7 @@ import * as A11y from '../ui/status/accessibility.js'; const _FADE_ANIMATION_TIME = 250; const _SCROLL_ANIMATION_TIME = 500; -+const _FIXED_TOP_ACTOR_HEIGHT = 400; ++const _FIXED_TOP_ACTOR_HEIGHT = 550; const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; @@ -2449,19 +2723,21 @@ index 09e8073c6..3a22bf161 100644 } -- -2.54.0 +2.55.0 -From f086439b2e40afbad7c04fd2e3d3105b095d6c5f Mon Sep 17 00:00:00 2001 +From 728c933fd1bf44beb24075cd6356663baa5baa39 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 12 Nov 2024 14:26:30 -0500 -Subject: [PATCH 31/54] data: Add fingerprint and vcard icons +Subject: [PATCH 32/72] data: Add fingerprint and vcard icons Fingerprint icon will be used to inform when it's being run un the background. Vcard icon will be used to inform on smartcard certificates list when they have an Organization field in its subject. + +Part-of: --- data/gnome-shell-icons.gresource.xml | 2 ++ .../status/fingerprint-auth-symbolic.svg | 28 +++++++++++++++++++ @@ -2540,13 +2816,13 @@ index 000000000..1694f2364 + + -- -2.54.0 +2.55.0 -From 2df9a17d2e55f832b5041feca42e8859cf804f26 Mon Sep 17 00:00:00 2001 +From ebb66897244e15aee81d9417bed4df413b0e332d Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:30:50 +0200 -Subject: [PATCH 32/54] gdm: Extract authentication service and role constants +Subject: [PATCH 33/72] gdm: Extract authentication service and role constants to constants.js Create a dedicated const.js module to centralize GDM authentication-related @@ -2554,16 +2830,18 @@ constants that are shared across multiple files. This prepares the codebase for upcoming commits that will use these constants. + +Part-of: --- js/gdm/authPrompt.js | 3 ++- - js/gdm/constants.js | 10 ++++++++++ + js/gdm/constants.js | 8 ++++++++ js/gdm/util.js | 34 ++++++++++++++++------------------ js/js-resources.gresource.xml | 1 + - 4 files changed, 29 insertions(+), 19 deletions(-) + 4 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 js/gdm/constants.js diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index e283adfb7..a5b7132a3 100644 +index 034357d9f..1f21d5ba8 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -9,6 +9,7 @@ import St from 'gi://St'; @@ -2574,7 +2852,7 @@ index e283adfb7..a5b7132a3 100644 import * as GdmUtil from './util.js'; import * as Params from '../misc/params.js'; import * as ShellEntry from '../ui/shellEntry.js'; -@@ -451,7 +452,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -448,7 +449,7 @@ export const AuthPrompt = GObject.registerClass({ // with a smartcard // 2) Don't reset if we've already succeeded at verification and // the user is getting logged in. @@ -2585,12 +2863,10 @@ index e283adfb7..a5b7132a3 100644 this.smartcardDetected) diff --git a/js/gdm/constants.js b/js/gdm/constants.js new file mode 100644 -index 000000000..2f37446c8 +index 000000000..2b6061578 --- /dev/null +++ b/js/gdm/constants.js -@@ -0,0 +1,10 @@ -+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -+ +@@ -0,0 +1,8 @@ +export const PASSWORD_ROLE_NAME = 'password'; +export const SMARTCARD_ROLE_NAME = 'smartcard'; +export const FINGERPRINT_ROLE_NAME = 'fingerprint'; @@ -2600,7 +2876,7 @@ index 000000000..2f37446c8 +export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; +export const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; diff --git a/js/gdm/util.js b/js/gdm/util.js -index b71251458..0c8ce34d3 100644 +index 72ee9d29b..0d73d4f33 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -5,6 +5,7 @@ import GLib from 'gi://GLib'; @@ -2621,7 +2897,7 @@ index b71251458..0c8ce34d3 100644 const CLONE_FADE_ANIMATION_TIME = 250; export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; -@@ -431,7 +429,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -438,7 +436,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._updateDefaultService(); if (this._userVerifier && @@ -2630,7 +2906,7 @@ index b71251458..0c8ce34d3 100644 if (!this._hold?.isAcquired()) this._hold = new Batch.Hold(); await this._maybeStartFingerprintVerification(); -@@ -485,8 +483,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -492,8 +490,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.smartcardDetected = smartcardDetected; if (this.smartcardDetected) @@ -2641,7 +2917,7 @@ index b71251458..0c8ce34d3 100644 this._preemptingService = null; this._updateDefaultService(); -@@ -605,7 +603,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -612,7 +610,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { return true; } @@ -2650,7 +2926,7 @@ index b71251458..0c8ce34d3 100644 } serviceIsDefault(serviceName) { -@@ -614,7 +612,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -621,7 +619,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { serviceIsFingerprint(serviceName) { return this._fingerprintReaderType !== FingerprintReaderType.NONE && @@ -2659,7 +2935,7 @@ index b71251458..0c8ce34d3 100644 } _onSettingsChanged() { -@@ -631,7 +629,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -638,7 +636,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._fingerprintManager = null; this._fingerprintReaderType = FingerprintReaderType.NONE; @@ -2668,7 +2944,7 @@ index b71251458..0c8ce34d3 100644 needsReset = true; } -@@ -641,7 +639,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -648,7 +646,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._smartcardManager.disconnectObject(this); this._smartcardManager = null; @@ -2677,7 +2953,7 @@ index b71251458..0c8ce34d3 100644 needsReset = true; } -@@ -651,13 +649,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -658,13 +656,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { _getDetectedDefaultService() { if (this._smartcardManager?.loggedInWithToken()) @@ -2695,7 +2971,7 @@ index b71251458..0c8ce34d3 100644 return null; } -@@ -667,7 +665,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -674,7 +672,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (!this._defaultService) { log('no authentication service is enabled, using password authentication'); @@ -2704,7 +2980,7 @@ index b71251458..0c8ce34d3 100644 } if (oldDefaultService && -@@ -715,8 +713,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -722,8 +720,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { async _maybeStartFingerprintVerification() { if (this._userName && this._fingerprintReaderType !== FingerprintReaderType.NONE && @@ -2715,7 +2991,7 @@ index b71251458..0c8ce34d3 100644 } _onChoiceListQuery(client, serviceName, promptMessage, list) { -@@ -848,7 +846,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -855,7 +853,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _verificationFailed(serviceName, shouldRetry) { @@ -2737,13 +3013,13 @@ index e5e6167f1..041365e5d 100644 gdm/loginDialog.js gdm/oVirt.js -- -2.54.0 +2.55.0 -From c15539a8f8e194f3d7dfc7dc9d9b1d6bce603340 Mon Sep 17 00:00:00 2001 +From b1ba23ecb626825936df0ad06ca358e992acd80b Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 11 Mar 2026 11:41:40 +0100 -Subject: [PATCH 33/54] gdm/util: Add helper functions to get future mechanisms +Subject: [PATCH 34/72] gdm/util: Add helper functions to get future mechanisms metadata 1. isSelectable() to detect when a mechanism is selectable or runs in @@ -2751,12 +3027,14 @@ Subject: [PATCH 33/54] gdm/util: Add helper functions to get future mechanisms 2. getNonSelectableIconName() to get the icon name for a non selectable mechanism Next commits will use these functions. + +Part-of: --- js/gdm/util.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/js/gdm/util.js b/js/gdm/util.js -index 0c8ce34d3..f0b99a0ff 100644 +index 0d73d4f33..304459c71 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -99,6 +99,40 @@ export function cloneAndFadeOutActor(actor) { @@ -2801,13 +3079,13 @@ index 0c8ce34d3..f0b99a0ff 100644 constructor(client, params) { super(); -- -2.54.0 +2.55.0 -From 0fee132754e6a7cf9eb62324d0898988228cf696 Mon Sep 17 00:00:00 2001 +From 128705e5fbfb2ef76c305f6bf433cd27bd055008 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:04:20 -0500 -Subject: [PATCH 34/54] gdm: Add new AuthMenuButton control +Subject: [PATCH 35/72] gdm: Add new AuthMenuButton control The latest login screen designs show a new "Login Options" menu in the corner for session selection and login methods. @@ -2825,18 +3103,20 @@ methods in use, it's non-interactive. Nothing uses these new classes yet. A subsequent commit will change the sessions menu button code over to use it, and a commit after that will use it for Login Options. + +Part-of: --- .../gnome-shell-sass/widgets/_login-lock.scss | 59 ++ - js/gdm/authMenuButton.js | 504 ++++++++++++++++++ + js/gdm/authMenuButton.js | 514 ++++++++++++++++++ js/js-resources.gresource.xml | 1 + - 3 files changed, 564 insertions(+) + 3 files changed, 574 insertions(+) create mode 100644 js/gdm/authMenuButton.js diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index f498d1f5d..61ec97499 100644 +index 0fc13e061..e9a1e1a62 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -75,6 +75,65 @@ $_gdm_dialog_width: 25em; +@@ -72,6 +72,65 @@ $_gdm_dialog_width: 25em; } } @@ -2904,11 +3184,10 @@ index f498d1f5d..61ec97499 100644 } diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js new file mode 100644 -index 000000000..cbc1d7676 +index 000000000..5aaa45fd3 --- /dev/null +++ b/js/gdm/authMenuButton.js -@@ -0,0 +1,504 @@ -+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +@@ -0,0 +1,514 @@ +/* + * Copyright 2024 Red Hat, Inc + * @@ -3060,6 +3339,7 @@ index 000000000..cbc1d7676 + style_class: 'login-dialog-button login-dialog-auth-menu-button', + can_focus: true, + accessible_role: Atk.Role.MENU, ++ button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, + }); + this.bind_property('reactive', + this, 'can-focus', @@ -3173,6 +3453,7 @@ index 000000000..cbc1d7676 + } + }); + ++ this._updateOrnament(); + this.updateVisibility(); + } + @@ -3241,6 +3522,11 @@ index 000000000..cbc1d7676 + updateVisibility({visible = true} = {}) { + visible &&= this._canBeVisible(); + ++ if (this._targetVisibility === visible) ++ return; ++ ++ this._targetVisibility = visible; ++ + if (this.animateVisibility) { + if (visible) { + this.opacity = 0; @@ -3279,11 +3565,11 @@ index 000000000..cbc1d7676 + _resolveItem(searchCriteria) { + const itemKeys = this._findItems(searchCriteria); + -+ if (!itemKeys.length) -+ throw new Error(`Unknown item ${JSON.stringify(searchCriteria)}`); -+ -+ if (itemKeys.length > 1) -+ throw new Error(`Matched multiple items with criteria ${JSON.stringify(searchCriteria)}`); ++ if (itemKeys.length !== 1) { ++ if (itemKeys.length > 1) ++ logError(`Matched multiple items with criteria ${JSON.stringify(searchCriteria)}`); ++ return null; ++ } + + const item = JSON.parse(itemKeys[0]); + const menuItem = this._items.get(itemKeys[0]); @@ -3291,7 +3577,11 @@ index 000000000..cbc1d7676 + } + + setActiveItem(searchCriteria) { -+ const {item} = this._resolveItem(searchCriteria); ++ const resolved = this._resolveItem(searchCriteria); ++ if (!resolved) ++ return; ++ ++ const {item} = resolved; + const itemKey = JSON.stringify(item); + + if (this._activeItems.has(itemKey)) @@ -3425,28 +3715,30 @@ index 041365e5d..d8820876a 100644 gdm/batch.js gdm/constants.js -- -2.54.0 +2.55.0 -From 27df463d10c4bfbee415df0bbbb775e77b64e602 Mon Sep 17 00:00:00 2001 +From 8d057f6b1556553412e7225402578be8da0f2de1 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 11:11:32 -0500 -Subject: [PATCH 35/54] loginDialog: Port sessions menu over to AuthMenuButton +Subject: [PATCH 36/72] loginDialog: Port sessions menu over to AuthMenuButton Now that AuthMenuButton exists, we should use it. This commit changes the session menu over to use the new control. + +Part-of: --- .../gnome-shell-sass/widgets/_login-lock.scss | 1 + - js/gdm/loginDialog.js | 195 ++++++------------ - 2 files changed, 68 insertions(+), 128 deletions(-) + js/gdm/loginDialog.js | 194 ++++++------------ + 2 files changed, 68 insertions(+), 127 deletions(-) diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 61ec97499..078b5c5c2 100644 +index e9a1e1a62..572ce9f59 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -57,6 +57,7 @@ $_gdm_dialog_width: 25em; +@@ -54,6 +54,7 @@ $_gdm_dialog_width: 25em; &.a11y-button, &.cancel-button, &.switch-user-button, @@ -3455,29 +3747,26 @@ index 61ec97499..078b5c5c2 100644 @extend .icon-button; @extend %system_button; diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 3a22bf161..9187434a8 100644 +index b098891c4..deb9d9918 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -27,9 +27,9 @@ import Pango from 'gi://Pango'; +@@ -27,6 +27,7 @@ import Pango from 'gi://Pango'; import Shell from 'gi://Shell'; import St from 'gi://St'; +import * as AuthMenuButton from './authMenuButton.js'; import * as AuthPrompt from './authPrompt.js'; import * as Batch from './batch.js'; --import * as BoxPointer from '../ui/boxpointer.js'; - import * as CtrlAltTab from '../ui/ctrlAltTab.js'; - import * as GdmUtil from './util.js'; - import * as Layout from '../ui/layout.js'; -@@ -48,6 +48,7 @@ const _SCROLL_ANIMATION_TIME = 500; - const _FIXED_TOP_ACTOR_HEIGHT = 400; + import * as BoxPointer from '../ui/boxpointer.js'; +@@ -48,6 +49,7 @@ const _SCROLL_ANIMATION_TIME = 500; + const _FIXED_TOP_ACTOR_HEIGHT = 550; const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; +const _SESSION_TYPE_SECTION_NAME = _('Session Type'); const N_A11Y_MENU_COLUMNS = 2; -@@ -315,103 +316,6 @@ const UserList = GObject.registerClass({ +@@ -315,103 +317,6 @@ const UserList = GObject.registerClass({ } }); @@ -3581,7 +3870,7 @@ index 3a22bf161..9187434a8 100644 const A11yMenuButton = GObject.registerClass( class A11yMenuButton extends St.Button { constructor() { -@@ -592,6 +496,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -592,6 +497,7 @@ export const LoginDialog = GObject.registerClass({ this._authPrompt.connect('prompted', this._onPrompted.bind(this)); this._authPrompt.connect('reset', this._onReset.bind(this)); this._authPrompt.connect('verification-complete', this._onVerificationComplete.bind(this)); @@ -3589,7 +3878,7 @@ index 3a22bf161..9187434a8 100644 this._authPrompt.hide(); this.add_child(this._authPrompt); -@@ -645,14 +550,7 @@ export const LoginDialog = GObject.registerClass({ +@@ -645,14 +551,7 @@ export const LoginDialog = GObject.registerClass({ }); this.add_child(this._bottomButtonGroup); @@ -3605,7 +3894,7 @@ index 3a22bf161..9187434a8 100644 this._a11yMenuButton = new A11yMenuButton(); this._bottomButtonGroup.add_child(this._a11yMenuButton); -@@ -688,6 +586,56 @@ export const LoginDialog = GObject.registerClass({ +@@ -688,6 +587,56 @@ export const LoginDialog = GObject.registerClass({ this._updateDisableUserList.bind(this), this); } @@ -3640,7 +3929,7 @@ index 3a22bf161..9187434a8 100644 + return; + + const ids = Gdm.get_session_ids(); -+ if (ids.length === 0) ++ if (ids.length === 1) + return; + + const sessions = ids.map(id => { @@ -3662,7 +3951,7 @@ index 3a22bf161..9187434a8 100644 _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -1061,10 +1009,8 @@ export const LoginDialog = GObject.registerClass({ +@@ -1061,10 +1010,8 @@ export const LoginDialog = GObject.registerClass({ } _onPrompted() { @@ -3674,7 +3963,7 @@ index 3a22bf161..9187434a8 100644 this._showPrompt(); } -@@ -1091,7 +1037,6 @@ export const LoginDialog = GObject.registerClass({ +@@ -1091,7 +1038,6 @@ export const LoginDialog = GObject.registerClass({ _onReset(authPrompt, resetType) { this._ensureGreeterProxy(); @@ -3682,7 +3971,7 @@ index 3a22bf161..9187434a8 100644 const previousUser = this._user; this._user = null; -@@ -1125,23 +1070,15 @@ export const LoginDialog = GObject.registerClass({ +@@ -1124,23 +1070,15 @@ export const LoginDialog = GObject.registerClass({ }); } @@ -3713,7 +4002,7 @@ index 3a22bf161..9187434a8 100644 } _showPrompt() { -@@ -1185,13 +1122,14 @@ export const LoginDialog = GObject.registerClass({ +@@ -1185,13 +1123,14 @@ export const LoginDialog = GObject.registerClass({ this._authPrompt.updateSensitivity({sensitive: false}); const answer = this._authPrompt.getAnswer(); this._user = this._userManager.get_user(answer); @@ -3726,10 +4015,10 @@ index 3a22bf161..9187434a8 100644 - this._sessionMenuButton.updateSensitivity(false); + this._authMenuButton.updateVisibility({visible: false}); - this._authPrompt.updateSensitivity({sensitive: true}); this._showPrompt(); } -@@ -1505,8 +1443,7 @@ export const LoginDialog = GObject.registerClass({ + +@@ -1504,8 +1443,7 @@ export const LoginDialog = GObject.registerClass({ this._ensureUserListLoaded(); this._authPrompt.hide(); this._hideBannerView(); @@ -3739,7 +4028,7 @@ index 3a22bf161..9187434a8 100644 this._setUserListExpanded(true); this._notListedButton.show(); this._userList.grab_key_focus(); -@@ -1525,6 +1462,8 @@ export const LoginDialog = GObject.registerClass({ +@@ -1524,6 +1462,8 @@ export const LoginDialog = GObject.registerClass({ _onUserListActivated(activatedItem) { this._user = activatedItem.user; @@ -3749,48 +4038,13 @@ index 3a22bf161..9187434a8 100644 if (this._conflictingSessionNotification) -- -2.54.0 +2.55.0 -From 4246ce5f377c097ef55f3ec7c5016da0363dea9b Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Wed, 27 May 2026 18:27:35 +0200 -Subject: [PATCH 36/54] loginDialog: Use promisify with - this._greeter.call_select_session() - ---- - js/gdm/loginDialog.js | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) - -diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 9187434a8..5441e2dda 100644 ---- a/js/gdm/loginDialog.js -+++ b/js/gdm/loginDialog.js -@@ -53,6 +53,7 @@ const _SESSION_TYPE_SECTION_NAME = _('Session Type'); - const N_A11Y_MENU_COLUMNS = 2; - - Gio._promisify(Gio.File.prototype, 'load_contents_async'); -+Gio._promisify(Gdm.Greeter.prototype, 'call_select_session'); - - export const UserListItem = GObject.registerClass({ - Signals: {'activate': {}}, -@@ -600,7 +601,7 @@ export const LoginDialog = GObject.registerClass({ - return; - - if (sectionName === _SESSION_TYPE_SECTION_NAME) -- this._greeter.call_select_session_sync(item.id, null); -+ this._greeter.call_select_session(item.id, null).catch(logError); - - this._authMenuButton.closeMenu(); - }); --- -2.54.0 - - -From 04c458da6c4139e40cf2b45c3c04fd62312d4306 Mon Sep 17 00:00:00 2001 +From 0a0e3790160f59baf7e93fa46a0fbe04c36ade1d Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:40:26 -0500 -Subject: [PATCH 37/54] loginDialog: Add login options menu to AuthMenuButton +Subject: [PATCH 37/72] loginDialog: Add login options menu to AuthMenuButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -3811,6 +4065,7 @@ background, e.g. fingerprint. These ones won't be at _authMenuButton but at the future _authIndicatorButton to inform when they are enabled. Co-authored-by: Marco Trevisan (Treviño) +Part-of: --- js/gdm/authPrompt.js | 20 ++++++++++++++++++++ js/gdm/loginDialog.js | 43 +++++++++++++++++++++++++++++++++++++++++-- @@ -3818,10 +4073,10 @@ Co-authored-by: Marco Trevisan (Treviño) 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index a5b7132a3..c8c9e55ec 100644 +index 1f21d5ba8..cce46e178 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -50,6 +50,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -49,6 +49,7 @@ export const AuthPrompt = GObject.registerClass({ 'failed': {}, 'next': {}, 'prompted': {}, @@ -3829,15 +4084,15 @@ index a5b7132a3..c8c9e55ec 100644 'reset': {param_types: [GObject.TYPE_UINT]}, 'verification-complete': {}, 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, -@@ -86,6 +87,7 @@ export const AuthPrompt = GObject.registerClass({ - 'ask-question', this._onAskQuestion.bind(this), - 'show-message', this._onShowMessage.bind(this), - 'show-choice-list', this._onShowChoiceList.bind(this), +@@ -85,6 +86,7 @@ export const AuthPrompt = GObject.registerClass({ + 'ask-question', (_, ...args) => this._onAskQuestion(...args), + 'show-message', (_, ...args) => this._onShowMessage(...args), + 'show-choice-list', (_, ...args) => this._onShowChoiceList(...args), + 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), - 'verification-failed', this._onVerificationFailed.bind(this), - 'verification-complete', this._onVerificationComplete.bind(this), - 'reset', this._onReset.bind(this), -@@ -771,6 +773,24 @@ export const AuthPrompt = GObject.registerClass({ + 'verification-failed', (_, ...args) => this._onVerificationFailed(...args), + 'verification-complete', () => this._onVerificationComplete(), + 'reset', (_, ...args) => this._onReset(...args), +@@ -750,6 +752,24 @@ export const AuthPrompt = GObject.registerClass({ this.updateSensitivity(false); } @@ -3863,11 +4118,11 @@ index a5b7132a3..c8c9e55ec 100644 let {reuseEntryText, softReset} = Params.parse(params, { reuseEntryText: false, diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 5441e2dda..8656bdf29 100644 +index deb9d9918..f88310a6b 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js -@@ -48,6 +48,7 @@ const _SCROLL_ANIMATION_TIME = 500; - const _FIXED_TOP_ACTOR_HEIGHT = 400; +@@ -49,6 +49,7 @@ const _SCROLL_ANIMATION_TIME = 500; + const _FIXED_TOP_ACTOR_HEIGHT = 550; const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; +const _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); @@ -3899,7 +4154,7 @@ index 5441e2dda..8656bdf29 100644 + if (sectionName === _PRIMARY_LOGIN_METHOD_SECTION_NAME) + this._selectAuthMechanism(item); + else if (sectionName === _SESSION_TYPE_SECTION_NAME) - this._greeter.call_select_session(item.id, null).catch(logError); + this._greeter.call_select_session_sync(item.id, null); this._authMenuButton.closeMenu(); @@ -637,6 +641,20 @@ export const LoginDialog = GObject.registerClass({ @@ -3923,7 +4178,7 @@ index 5441e2dda..8656bdf29 100644 _getBannerAllocation(dialogBox) { let actorBox = new Clutter.ActorBox(); -@@ -1075,6 +1093,27 @@ export const LoginDialog = GObject.registerClass({ +@@ -1074,6 +1092,27 @@ export const LoginDialog = GObject.registerClass({ this._authMenuButton.reactive = !isLoading; } @@ -3952,14 +4207,14 @@ index 5441e2dda..8656bdf29 100644 this._authMenuButton.setActiveItem({ sectionName: _SESSION_TYPE_SECTION_NAME, diff --git a/js/gdm/util.js b/js/gdm/util.js -index f0b99a0ff..f6ff37603 100644 +index 304459c71..ad4236e88 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -219,6 +219,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._getUserVerifier(); } -+ selectMechanism(mechanism) { ++ selectMechanism() { + // TODO: Implement mechanism selection + return false; + } @@ -3968,13 +4223,72 @@ index f0b99a0ff..f6ff37603 100644 if (this._cancellable) this._cancellable.cancel(); -- -2.54.0 +2.55.0 -From 217f5b771d5f6646f98413282110f6bb304e18c7 Mon Sep 17 00:00:00 2001 +From d1b4d1b0a5a991b16fd6abeecd58a08893b05131 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 3 Jul 2026 13:54:39 +0200 +Subject: [PATCH 38/72] loginDialog: Restore bottom button group on reset + +The bottom button group is hidden on verification complete, +but there might be more authentication attempts that need +it visible again. Restore it on reset. + +Part-of: +--- + js/gdm/loginDialog.js | 16 +++++++++++++++- + 1 file changed, 15 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index f88310a6b..cdb64d322 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1056,6 +1056,7 @@ export const LoginDialog = GObject.registerClass({ + + _onReset(authPrompt, resetType) { + this._ensureGreeterProxy(); ++ this._showBottomButtonGroup(); + + const previousUser = this._user; + this._user = null; +@@ -1079,7 +1080,16 @@ export const LoginDialog = GObject.registerClass({ + } + } + +- _onVerificationComplete() { ++ _showBottomButtonGroup() { ++ this._bottomButtonGroup.reactive = true; ++ this._bottomButtonGroup.ease({ ++ opacity: 255, ++ duration: _FADE_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ }); ++ } ++ ++ _hideBottomButtonGroup() { + this._bottomButtonGroup.reactive = false; + this._bottomButtonGroup.ease({ + opacity: 0, +@@ -1088,6 +1098,10 @@ export const LoginDialog = GObject.registerClass({ + }); + } + ++ _onVerificationComplete() { ++ this._hideBottomButtonGroup(); ++ } ++ + _onLoading(_authPrompt, isLoading) { + this._authMenuButton.reactive = !isLoading; + } +-- +2.55.0 + + +From 15fee32aa9b2d3b75822f950d5986a7516808854 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 13:41:39 -0500 -Subject: [PATCH 38/54] unlockDialog: Add _authMenuButton and +Subject: [PATCH 39/72] unlockDialog: Add _authMenuButton and _authIndicatorButton _authMenuButton is used to select an available auth mechanism from the @@ -3985,12 +4299,14 @@ selectable) auth mechanisms. It's in the bottom left corner of the screen. Make _otherUserButton written out, instead of using an icon. And move it to the bottom right corner of the screen, with _authMenuButton. + +Part-of: --- - js/ui/unlockDialog.js | 158 +++++++++++++++++++++++++++++++++++++----- - 1 file changed, 140 insertions(+), 18 deletions(-) + js/ui/unlockDialog.js | 160 +++++++++++++++++++++++++++++++++++++----- + 1 file changed, 142 insertions(+), 18 deletions(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 100990acc..2c2fb1c54 100644 +index 100990acc..d54d21093 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -15,11 +15,16 @@ import * as Main from './main.js'; @@ -4076,7 +4392,7 @@ index 100990acc..2c2fb1c54 100644 } } }); -@@ -614,19 +638,46 @@ export const UnlockDialog = GObject.registerClass({ +@@ -614,19 +638,47 @@ export const UnlockDialog = GObject.registerClass({ this._notificationsBox = new NotificationsBox(); this._notificationsBox.connect('wake-up-screen', () => this.emit('wake-up-screen')); @@ -4105,6 +4421,7 @@ index 100990acc..2c2fb1c54 100644 + this._authMenuButton = new AuthMenuButton.AuthMenuButton({ + accessible_name: _('Login Options'), + visible: false, ++ y_align: Clutter.ActorAlign.END, + }); + this._authMenuButton.connect('active-item-changed', () => { + const authMechanism = this._authMenuButton.getActiveItem(); @@ -4126,7 +4443,7 @@ index 100990acc..2c2fb1c54 100644 this._screenSaverSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.screensaver'}); -@@ -649,11 +700,13 @@ export const UnlockDialog = GObject.registerClass({ +@@ -649,11 +701,13 @@ export const UnlockDialog = GObject.registerClass({ mainBox.add_constraint(new Layout.MonitorConstraint({primary: true})); mainBox.add_child(this._stack); mainBox.add_child(this._notificationsBox); @@ -4142,7 +4459,7 @@ index 100990acc..2c2fb1c54 100644 this.add_child(mainBox); this._idleMonitor = global.backend.get_core_idle_monitor(); -@@ -691,6 +744,20 @@ export const UnlockDialog = GObject.registerClass({ +@@ -691,6 +745,20 @@ export const UnlockDialog = GObject.registerClass({ return Clutter.EVENT_PROPAGATE; } @@ -4163,7 +4480,7 @@ index 100990acc..2c2fb1c54 100644 _createBackground(monitorIndex) { let monitor = Main.layoutManager.monitors[monitorIndex]; let widget = new St.Widget({ -@@ -747,6 +814,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -747,6 +815,8 @@ export const UnlockDialog = GObject.registerClass({ this._authPrompt.connect('failed', this._fail.bind(this)); this._authPrompt.connect('cancelled', this._fail.bind(this)); this._authPrompt.connect('reset', this._onReset.bind(this)); @@ -4172,7 +4489,15 @@ index 100990acc..2c2fb1c54 100644 this._promptBox.add_child(this._authPrompt); } -@@ -809,6 +878,12 @@ export const UnlockDialog = GObject.registerClass({ +@@ -771,6 +841,7 @@ export const UnlockDialog = GObject.registerClass({ + if (this._authPrompt) { + this._authPrompt.destroy(); + this._authPrompt = null; ++ this._authIndicatorButton.clearItems(); + } + } + +@@ -809,6 +880,12 @@ export const UnlockDialog = GObject.registerClass({ reactive: progress > 0, can_focus: progress > 0, }); @@ -4185,7 +4510,7 @@ index 100990acc..2c2fb1c54 100644 const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); -@@ -826,7 +901,7 @@ export const UnlockDialog = GObject.registerClass({ +@@ -826,7 +903,7 @@ export const UnlockDialog = GObject.registerClass({ translation_y: -FADE_OUT_TRANSLATION * progress * scaleFactor, }); @@ -4194,7 +4519,7 @@ index 100990acc..2c2fb1c54 100644 opacity: 255 * progress, scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress, -@@ -850,6 +925,52 @@ export const UnlockDialog = GObject.registerClass({ +@@ -850,6 +927,52 @@ export const UnlockDialog = GObject.registerClass({ this._authPrompt.begin({userName}); } @@ -4247,7 +4572,7 @@ index 100990acc..2c2fb1c54 100644 _escape() { if (this._authPrompt && this.allowCancel) this._authPrompt.cancel(); -@@ -912,7 +1033,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -912,7 +1035,8 @@ export const UnlockDialog = GObject.registerClass({ this._otherUserButton.visible = this._userManager.can_switch() && this._userManager.has_multiple_users && this._screenSaverSettings.get_boolean('user-switch-enabled') && @@ -4258,22 +4583,24 @@ index 100990acc..2c2fb1c54 100644 cancel() { -- -2.54.0 +2.55.0 -From e240976e3c207b232e968a5323f36535a67c55ab Mon Sep 17 00:00:00 2001 +From 1a8e582a2728590d1990f39c81529a0261a9a661 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 3 Dec 2024 07:39:32 -0500 -Subject: [PATCH 39/54] unlockDialog: Update hint text based on mockup +Subject: [PATCH 40/72] unlockDialog: Update hint text based on mockup This considers future mechanisms which might be the default ones, i.e. smartcard and passkey. And have special hint texts. + +Part-of: --- js/ui/unlockDialog.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 2c2fb1c54..af584470a 100644 +index d54d21093..38d7b97c4 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -422,10 +422,23 @@ class UnlockDialogClock extends St.BoxLayout { @@ -4303,7 +4630,7 @@ index 2c2fb1c54..af584470a 100644 } _onDestroy() { -@@ -756,6 +769,8 @@ export const UnlockDialog = GObject.registerClass({ +@@ -757,6 +770,8 @@ export const UnlockDialog = GObject.registerClass({ } this._selectedAuthMechanism = authMechanism; @@ -4313,23 +4640,25 @@ index 2c2fb1c54..af584470a 100644 _createBackground(monitorIndex) { -- -2.54.0 +2.55.0 -From 5ff80788f94268039f982f757d2c7b80dd88e152 Mon Sep 17 00:00:00 2001 +From 19b219118b53d8a9d28623fecbfe9985cfa9e8a5 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Oct 2025 19:19:15 +0200 -Subject: [PATCH 40/54] gdm/util: Increase time of messages based on new +Subject: [PATCH 41/72] gdm/util: Increase time of messages based on new environment variable called 'GDM_MESSAGE_TIME_MULTIPLIER'. This is used for testing purposes. When no set, the multiplier is 1 which does nothing. + +Part-of: --- js/gdm/util.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/gdm/util.js b/js/gdm/util.js -index f6ff37603..5eadcd011 100644 +index ad4236e88..a198bcc32 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -44,6 +44,10 @@ export const DISABLE_USER_LIST_KEY = 'disable-user-list'; @@ -4354,13 +4683,13 @@ index f6ff37603..5eadcd011 100644 finishMessageQueue() { -- -2.54.0 +2.55.0 -From b47fa82b3b6ce5d9dbe23846e95713d26a57b60f Mon Sep 17 00:00:00 2001 +From 36666e6ce2a3c180413b3ca784fe68f491f37e84 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 17:13:17 +0200 -Subject: [PATCH 41/54] gdm/util: Allow null _hold and don't recreate dummy +Subject: [PATCH 42/72] gdm/util: Allow null _hold and don't recreate dummy holds _hold property is used to inform the caller of begin method (authPrompt) @@ -4373,18 +4702,20 @@ externally. In the next commits the use of _hold will be simplified considerably, but for now, just accept getting null and dont create dummy ones. + +Part-of: --- js/gdm/authPrompt.js | 6 +----- - js/gdm/util.js | 15 ++++++--------- - 2 files changed, 7 insertions(+), 14 deletions(-) + js/gdm/util.js | 18 +++++++----------- + 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index c8c9e55ec..ff51c05f3 100644 +index cce46e178..00d6a6255 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -863,11 +863,7 @@ export const AuthPrompt = GObject.registerClass({ - - this.updateSensitivity({sensitive: false}); +@@ -849,11 +849,7 @@ export const AuthPrompt = GObject.registerClass({ + if (!this._preemptiveInput) + this.updateSensitivity({sensitive: false}); - let hold = params.hold; - if (!hold) @@ -4396,19 +4727,23 @@ index c8c9e55ec..ff51c05f3 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index 5eadcd011..45ee4efda 100644 +index a198bcc32..9df2c76d4 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -474,8 +474,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -480,11 +480,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._updateDefaultService(); if (this._userVerifier && - !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { +- !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { - if (!this._hold?.isAcquired()) - this._hold = new Batch.Hold(); ++ !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) await this._maybeStartFingerprintVerification(); - } +- } } -@@ -539,7 +537,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + _setFingerprintReaderType(fprintDeviceType) { +@@ -546,7 +543,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { _reportInitError(where, error, serviceName) { logError(error, where); @@ -4417,7 +4752,7 @@ index 5eadcd011..45ee4efda 100644 this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); this._failCounter++; -@@ -577,7 +575,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -584,7 +581,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this.reauthenticating = true; this._connectSignals(); this._beginVerification(); @@ -4426,7 +4761,7 @@ index 5eadcd011..45ee4efda 100644 } async _getUserVerifier() { -@@ -601,7 +599,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -608,7 +605,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._connectSignals(); this._beginVerification(); @@ -4435,7 +4770,7 @@ index 5eadcd011..45ee4efda 100644 } _connectSignals() { -@@ -719,7 +717,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -726,7 +723,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _startService(serviceName) { @@ -4444,7 +4779,7 @@ index 5eadcd011..45ee4efda 100644 try { this._activeServices.add(serviceName); if (this._userName) { -@@ -736,7 +734,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -743,7 +740,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (!this.serviceIsForeground(serviceName)) { logError(e, `Failed to start ${serviceName} for ${this._userName}`); @@ -4453,7 +4788,7 @@ index 5eadcd011..45ee4efda 100644 return; } this._reportInitError( -@@ -746,7 +744,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -753,7 +750,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { e, serviceName); return; } @@ -4462,7 +4797,7 @@ index 5eadcd011..45ee4efda 100644 } _beginVerification() { -@@ -879,7 +877,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -886,7 +883,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _retry(serviceName) { @@ -4471,13 +4806,13 @@ index 5eadcd011..45ee4efda 100644 this._startService(serviceName); } -- -2.54.0 +2.55.0 -From 6798ef06bac6d0d307f6e2c766ca7e0158d8c83d Mon Sep 17 00:00:00 2001 +From 6852a3faf63a79d26555c91f7d8e679a3d2ab171 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 20 Aug 2025 10:54:33 +0200 -Subject: [PATCH 42/54] gdm: Add FingerprintManager +Subject: [PATCH 43/72] gdm: Add FingerprintManager Move fingerprint bits to new fingerprintManager class. @@ -4486,11 +4821,13 @@ functionality inside fingerprintManager. This will be improved in the next commits when moving fingerprint authentication to a specific class instead of util. + +Part-of: --- - js/gdm/fingerprintManager.js | 138 +++++++++++++++++++++++++++++++ - js/gdm/util.js | 149 ++++++++-------------------------- + js/gdm/fingerprintManager.js | 138 ++++++++++++++++++++++++++++++ + js/gdm/util.js | 152 ++++++++-------------------------- js/js-resources.gresource.xml | 1 + - 3 files changed, 173 insertions(+), 115 deletions(-) + 3 files changed, 175 insertions(+), 116 deletions(-) create mode 100644 js/gdm/fingerprintManager.js diff --git a/js/gdm/fingerprintManager.js b/js/gdm/fingerprintManager.js @@ -4638,14 +4975,14 @@ index 000000000..2577facbb + } +} diff --git a/js/gdm/util.js b/js/gdm/util.js -index 45ee4efda..a1c4ff939 100644 +index 9df2c76d4..14d581714 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -6,19 +6,14 @@ import * as Signals from '../misc/signals.js'; import * as Batch from './batch.js'; import * as Constants from './constants.js'; -+import {FingerprintManager, FingerprintReaderType} from './fingerprintManager.js'; ++import * as FingerprintManager from './fingerprintManager.js'; import * as OVirt from './oVirt.js'; import * as Vmware from './vmware.js'; import * as Main from '../ui/main.js'; @@ -4675,10 +5012,12 @@ index 45ee4efda..a1c4ff939 100644 /** * @param {Clutter.Actor} actor */ -@@ -149,6 +138,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -148,7 +137,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._defaultService = null; this._preemptingService = null; - this._fingerprintReaderType = FingerprintReaderType.NONE; +- this._fingerprintReaderType = FingerprintReaderType.NONE; ++ this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; + this._fingerprintReaderFound = false; this._messageQueue = []; @@ -4701,7 +5040,7 @@ index 45ee4efda..a1c4ff939 100644 this._fingerprintManager = null; for (let service in this._credentialManagers) -@@ -378,112 +368,39 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -385,111 +375,39 @@ export class ShellUserVerifier extends Signals.EventEmitter { } async _initFingerprintManager() { @@ -4764,7 +5103,11 @@ index 45ee4efda..a1c4ff939 100644 - - _handleFingerprintError(e) { - this._fingerprintReaderType = FingerprintReaderType.NONE; -- ++ this._fingerprintManager = FingerprintManager.getFingerprintManager(); ++ this._fingerprintManager.connectObject( ++ 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), ++ this); + - if (e instanceof GLib.Error) { - if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) - return; @@ -4774,19 +5117,6 @@ index 45ee4efda..a1c4ff939 100644 - Gio.DBusError.get_remote_error(e) === - 'net.reactivated.Fprint.Error.NoSuchDevice') - return; -- } -- -- logError(e, 'Failed to interact with fprintd service'); -- } -+ this._fingerprintManager = new FingerprintManager(this._cancellable); -+ this._fingerprintManager.connectObject( -+ 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), -+ this); - -- async _checkForFingerprintReader() { -- if (!this._fprintManager) { -- this._updateDefaultService(); -- return; + if (!this._getDetectedDefaultService()) { + // Other authentication methods would have already been detected by + // now as possibilities if they were available. @@ -4803,12 +5133,21 @@ index 45ee4efda..a1c4ff939 100644 + this._fingerprintManager.checkReaderType(this._cancellable); } - +- logError(e, 'Failed to interact with fprintd service'); + } + +- async _checkForFingerprintReader() { +- if (!this._fprintManager) { +- this._updateDefaultService(); +- return; +- } +- - if (this._fingerprintReaderType !== FingerprintReaderType.NONE) - return; - - await this._updateFingerprintReaderType(this._fprintManager, this._cancellable); - } - +- } +- - async _updateFingerprintReaderType(fprintManager, cancellable) { - // Wrappers don't support null cancellable, so let's ignore it in case - const args = cancellable ? [cancellable] : []; @@ -4822,9 +5161,8 @@ index 45ee4efda..a1c4ff939 100644 this._updateDefaultService(); if (this._userVerifier && -- !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) { + !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) - await this._maybeStartFingerprintVerification(); -- } - } - - _setFingerprintReaderType(fprintDeviceType) { @@ -4833,12 +5171,11 @@ index 45ee4efda..a1c4ff939 100644 - - if (this._fingerprintReaderType === undefined) - throw new Error(`Unexpected fingerprint device type '${fprintDeviceType}'`); -+ !this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) + this._maybeStartFingerprintVerification(); } _onCredentialManagerAuthenticated(credentialManager, _token) { -@@ -653,7 +570,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -659,7 +577,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } serviceIsFingerprint(serviceName) { @@ -4847,7 +5184,7 @@ index 45ee4efda..a1c4ff939 100644 serviceName === Constants.FINGERPRINT_SERVICE_NAME; } -@@ -666,9 +583,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -672,10 +590,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { let needsReset = false; if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) { @@ -4856,11 +5193,13 @@ index 45ee4efda..a1c4ff939 100644 } else if (this._fingerprintManager) { + this._fingerprintManager.disconnectObject(this); this._fingerprintManager = null; +- this._fingerprintReaderType = FingerprintReaderType.NONE; + this._fingerprintReaderFound = false; - this._fingerprintReaderType = FingerprintReaderType.NONE; ++ this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) -@@ -696,7 +615,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + needsReset = true; +@@ -702,7 +622,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { return Constants.PASSWORD_SERVICE_NAME; else if (this._smartcardManager) return Constants.SMARTCARD_SERVICE_NAME; @@ -4869,7 +5208,7 @@ index 45ee4efda..a1c4ff939 100644 return Constants.FINGERPRINT_SERVICE_NAME; return null; } -@@ -754,7 +673,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -760,7 +680,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { async _maybeStartFingerprintVerification() { if (this._userName && @@ -4878,6 +5217,15 @@ index 45ee4efda..a1c4ff939 100644 !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) await this._startService(Constants.FINGERPRINT_SERVICE_NAME); } +@@ -783,7 +703,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + // We don't show fingerprint messages directly since it's + // not the main auth service. Instead we use the messages + // as a cue to display our own message. +- if (this._fingerprintReaderType === FingerprintReaderType.SWIPE) { ++ if (this._fingerprintReaderType === FingerprintManager.FingerprintReaderType.SWIPE) { + // Translators: this message is shown below the password entry field + // to indicate the user can swipe their finger on the fingerprint reader + this._queueMessage(serviceName, _('(or swipe finger across reader)'), diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index d8820876a..50e0b179a 100644 --- a/js/js-resources.gresource.xml @@ -4891,19 +5239,21 @@ index d8820876a..50e0b179a 100644 gdm/oVirt.js gdm/realmd.js -- -2.54.0 +2.55.0 -From 07865a6a750583ce50d66adbde51046a72aab73c Mon Sep 17 00:00:00 2001 +From e5a352f03c2139e78f48cfbcfa250080632346da Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 23 Sep 2025 16:45:59 +0200 -Subject: [PATCH 43/54] gdm: Add PasskeyDeviceManager +Subject: [PATCH 44/72] gdm: Add PasskeyDeviceManager This utility will be used in the next commits, when passkey authentication is implemented, to detect when a passkey has been inserted or removed. To detect if a sysfs device is a passkey (fido2), it's been used the implementation of systemd in fido_id_desc.c. + +Part-of: --- js/gdm/passkeyDeviceManager.js | 69 ++++++++++++++++++++++++++++++++++ js/js-resources.gresource.xml | 1 + @@ -5011,16 +5361,18 @@ index f8f98d487..c3c51aa2d 100644 import 'gi://IBus?version=1.0'; import 'gi://Pango?version=1.0'; -- -2.54.0 +2.55.0 -From 595c3331f83cc93b268b5afe25028f788c9d9522 Mon Sep 17 00:00:00 2001 +From 6996a2bbd8c5197be87dfe10594a02a61110d788 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 19 May 2026 20:33:29 +0200 -Subject: [PATCH 44/54] gdm: Move smartcardManager.js to gdm +Subject: [PATCH 45/72] gdm: Move smartcardManager.js to gdm This is a gdm component and was in misc. Move it here, where the other *manager.js siblings are. + +Part-of: --- js/{misc => gdm}/smartcardManager.js | 4 ++-- js/gdm/util.js | 2 +- @@ -5047,7 +5399,7 @@ index 51471e51d..21b28ea45 100644 const SmartcardTokenIface = ` diff --git a/js/gdm/util.js b/js/gdm/util.js -index a1c4ff939..db6488113 100644 +index 14d581714..ec0a6f84d 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -12,7 +12,7 @@ import * as Vmware from './vmware.js'; @@ -5093,17 +5445,19 @@ index 44b871502..e42b2974a 100644 import {adjustAnimationTime} from '../misc/animationUtils.js'; -- -2.54.0 +2.55.0 -From 38399275c7c5b5144d0a381207b6b80e5c0b6e08 Mon Sep 17 00:00:00 2001 +From 0aca4140bf6a8c2339fe012e0cc937d8505eeb0a Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 10 Mar 2026 14:27:50 +0100 -Subject: [PATCH 45/54] gdm/smartcardManager: Skip login_token aliases to avoid +Subject: [PATCH 46/72] gdm/smartcardManager: Skip login_token aliases to avoid duplicate events Tokens with '/login_token' paths are aliases mirroring already connected tokens. Connecting to both causes duplicate events. + +Part-of: --- js/gdm/smartcardManager.js | 6 ++++++ 1 file changed, 6 insertions(+) @@ -5126,13 +5480,13 @@ index 21b28ea45..98f762d7d 100644 token.connectObject('g-properties-changed', (proxy, properties) => { -- -2.54.0 +2.55.0 -From 2cc656533a3894a50daea881e3d896c8aaf951df Mon Sep 17 00:00:00 2001 +From 3117ad9977c60135501dc6dd5d481f52e0932425 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 18 Aug 2025 12:08:21 +0200 -Subject: [PATCH 46/54] gdm: Add AuthServices +Subject: [PATCH 47/72] gdm: Add AuthServices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -5201,33 +5555,85 @@ implemented in the child class called AuthServicesLegacy. Based on the previous work done by: - Marco Trevisan (Treviño) - Ray Strode + +Part-of: --- - js/gdm/authPrompt.js | 57 +-- - js/gdm/authServices.js | 473 ++++++++++++++++++++++ - js/gdm/authServicesLegacy.js | 334 ++++++++++++++++ - js/gdm/util.js | 731 ++++++++-------------------------- + js/gdm/authPrompt.js | 66 +-- + js/gdm/authServices.js | 498 ++++++++++++++++++++++ + js/gdm/authServicesLegacy.js | 351 +++++++++++++++ + js/gdm/loginDialog.js | 2 +- + js/gdm/util.js | 779 +++++++++------------------------- js/js-resources.gresource.xml | 2 + - po/POTFILES.in | 1 + - 6 files changed, 983 insertions(+), 615 deletions(-) + js/ui/unlockDialog.js | 2 +- + po/POTFILES.in | 2 + + 8 files changed, 1076 insertions(+), 626 deletions(-) create mode 100644 js/gdm/authServices.js create mode 100644 js/gdm/authServicesLegacy.js diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index ff51c05f3..db4c1647a 100644 +index 00d6a6255..ce78914d7 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -91,10 +91,7 @@ export const AuthPrompt = GObject.registerClass({ - 'verification-failed', this._onVerificationFailed.bind(this), - 'verification-complete', this._onVerificationComplete.bind(this), - 'reset', this._onReset.bind(this), -- 'smartcard-status-changed', this._onSmartcardStatusChanged.bind(this), -- 'credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this), +@@ -11,6 +11,7 @@ import * as AuthList from './authList.js'; + import * as Batch from './batch.js'; + import * as Constants from './constants.js'; + import * as GdmUtil from './util.js'; ++import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Params from '../misc/params.js'; + import * as ShellEntry from '../ui/shellEntry.js'; + import * as UserWidget from '../ui/userWidget.js'; +@@ -49,7 +50,7 @@ export const AuthPrompt = GObject.registerClass({ + 'failed': {}, + 'next': {}, + 'prompted': {}, +- 'mechanisms-changed': {param_types: [GObject.TYPE_JSOBJECT, GObject.TYPE_JSOBJECT]}, ++ 'mechanisms-changed': {param_types: [GObject.TYPE_JSOBJECT]}, + 'reset': {param_types: [GObject.TYPE_UINT]}, + 'verification-complete': {}, + 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, +@@ -83,17 +84,14 @@ export const AuthPrompt = GObject.registerClass({ + this._userVerifier = this._createUserVerifier(this._gdmClient, {reauthenticationOnly}); + + this._userVerifier.connectObject( +- 'ask-question', (_, ...args) => this._onAskQuestion(...args), +- 'show-message', (_, ...args) => this._onShowMessage(...args), +- 'show-choice-list', (_, ...args) => this._onShowChoiceList(...args), +- 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), +- 'verification-failed', (_, ...args) => this._onVerificationFailed(...args), ++ 'ask-question', (_, args) => this._onAskQuestion(args), ++ 'show-message', (_, args) => this._onShowMessage(args), ++ 'show-choice-list', (_, args) => this._onShowChoiceList(args), ++ 'mechanisms-changed', (_, args) => this.emit('mechanisms-changed', args), ++ 'verification-failed', (_, args) => this._onVerificationFailed(args), + 'verification-complete', () => this._onVerificationComplete(), +- 'reset', (_, ...args) => this._onReset(...args), +- 'smartcard-status-changed', () => this._onSmartcardStatusChanged(), +- 'credential-manager-authenticated', () => this._onCredentialManagerAuthenticated(), ++ 'reset', (_, args) => this._onReset(args), this); - this.smartcardDetected = this._userVerifier.smartcardDetected; this.connect('destroy', this._onDestroy.bind(this)); -@@ -439,46 +436,8 @@ export const AuthPrompt = GObject.registerClass({ +@@ -393,7 +391,7 @@ export const AuthPrompt = GObject.registerClass({ + this._capsLockWarningLabel.visible = secret; + } + +- _onAskQuestion(serviceName, question, secret) { ++ _onAskQuestion({serviceName, question, secret}) { + if (this._queryingService) + this.clear(); + +@@ -421,7 +419,7 @@ export const AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowChoiceList(serviceName, promptMessage, choiceList) { ++ _onShowChoiceList({serviceName, promptMessage, choiceList}) { + if (this._queryingService) + this.clear(); + +@@ -436,32 +434,7 @@ export const AuthPrompt = GObject.registerClass({ this.emit('prompted'); } @@ -5256,27 +5662,19 @@ index ff51c05f3..db4c1647a 100644 - this.reset(); - } - -- _onShowMessage(_userVerifier, serviceName, message, type) { -- let wiggleParameters = {duration: 0}; -- -- if (type === GdmUtil.MessageType.ERROR && -- this._userVerifier.serviceIsFingerprint(serviceName)) { -- // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue -- wiggleParameters = { -- duration: 65, -- wiggleCount: 3, -- }; -- this._userVerifier.increaseCurrentMessageTimeout( -- wiggleParameters.duration * (wiggleParameters.wiggleCount + 2)); -- } -- -- this.setMessage(message, type, wiggleParameters); -+ _onShowMessage(_userVerifier, serviceName, message, type, showMessageResolver) { -+ this.setMessage(message, type); +- _onShowMessage(serviceName, message, type, showMessageResolver) { ++ _onShowMessage({serviceName, message, type, showMessageResolver}) { + this.setMessage(message, type); this.emit('prompted'); - // If we're showing a message and no auth widget is currently visible, -@@ -496,7 +455,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -485,13 +458,15 @@ export const AuthPrompt = GObject.registerClass({ + showMessageResolver?.(wigglePromise); + } + +- _onVerificationFailed(serviceName, canRetry) { ++ _onVerificationFailed({serviceName, canRetry}) { + const wasQueryingService = this._queryingService === serviceName; + if (wasQueryingService) this._queryingService = null; @@ -5284,10 +5682,10 @@ index ff51c05f3..db4c1647a 100644 + // Only allow instant retrying with password authentication. + // The rest of authentications will retry through the reset flow. + if (canRetry && this._userVerifier.selectedMechanism?.role === Constants.PASSWORD_ROLE_NAME) { + this.verificationStatus = AuthPromptStatus.VERIFYING; this._entry.text = ''; - this.updateSensitivity({sensitive: true}); - } -@@ -809,8 +770,10 @@ export const AuthPrompt = GObject.registerClass({ + this.startPreemptiveInput(); +@@ -788,8 +763,10 @@ export const AuthPrompt = GObject.registerClass({ this._preemptiveAnswerWatchId = this._idleMonitor.add_idle_watch(3000, this._onUserStoppedTypePreemptiveAnswer.bind(this)); @@ -5300,7 +5698,7 @@ index ff51c05f3..db4c1647a 100644 reuseEntryText = reuseEntryText || !!this._preemptiveAnswer || this._preemptiveInput; -@@ -833,7 +796,7 @@ export const AuthPrompt = GObject.registerClass({ +@@ -812,7 +789,7 @@ export const AuthPrompt = GObject.registerClass({ if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED) return; resetType = ResetType.PROVIDE_USERNAME; @@ -5309,14 +5707,22 @@ index ff51c05f3..db4c1647a 100644 // We don't need to know the username if the user preempted the login screen // with a smartcard or with preauthenticated oVirt credentials resetType = ResetType.DONT_PROVIDE_USERNAME; +@@ -849,7 +826,8 @@ export const AuthPrompt = GObject.registerClass({ + if (!this._preemptiveInput) + this.updateSensitivity({sensitive: false}); + +- this._userVerifier.begin(params.userName, params.hold); ++ this._userVerifier.begin(params.userName, params.hold).catch( ++ logErrorUnlessCancelled); + this.verificationStatus = AuthPromptStatus.VERIFYING; + } + diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js new file mode 100644 -index 000000000..8f05a5d8f +index 000000000..fd27583c2 --- /dev/null +++ b/js/gdm/authServices.js -@@ -0,0 +1,473 @@ -+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -+ +@@ -0,0 +1,498 @@ +import * as Constants from './constants.js'; +import * as FingerprintManager from './fingerprintManager.js'; +import * as Params from '../misc/params.js'; @@ -5339,31 +5745,50 @@ index 000000000..8f05a5d8f +export class AuthServices extends GObject.Object { + static [GObject.signals] = { + 'destroy': {}, -+ 'queue-message': { -+ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], -+ }, -+ 'queue-priority-message': { -+ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_UINT], -+ }, -+ 'wait-pending-messages': { -+ param_types: [GObject.TYPE_JSOBJECT], -+ }, -+ 'filter-messages': { -+ param_types: [GObject.TYPE_STRING, GObject.TYPE_UINT], -+ }, -+ 'verification-failed': { -+ param_types: [GObject.TYPE_STRING, GObject.TYPE_BOOLEAN], -+ }, ++ /** ++ * @param {string} serviceName ++ * @param {string} message ++ * @param {MessageType} messageType ++ */ ++ 'queue-message': {param_types: [GObject.TYPE_JSOBJECT]}, ++ /** ++ * @param {string} serviceName ++ * @param {string} message ++ * @param {MessageType} messageType ++ */ ++ 'queue-priority-message': {param_types: [GObject.TYPE_JSOBJECT]}, ++ /** ++ * @param {Gio.Task} task ++ */ ++ 'wait-pending-messages': {param_types: [GObject.TYPE_JSOBJECT]}, ++ /** ++ * @param {string} serviceName ++ * @param {MessageType} messageType ++ */ ++ 'filter-messages': {param_types: [GObject.TYPE_JSOBJECT]}, ++ /** ++ * @param {string} serviceName ++ * @param {boolean} [canRetry] ++ */ ++ 'verification-failed': {param_types: [GObject.TYPE_JSOBJECT]}, + 'verification-complete': {}, -+ 'ask-question': { -+ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_BOOLEAN], -+ }, -+ 'reset': { -+ param_types: [GObject.TYPE_JSOBJECT], -+ }, -+ 'show-choice-list': { -+ param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT], -+ }, ++ /** ++ * @param {string} serviceName ++ * @param {string} question ++ * @param {boolean} [secret] ++ */ ++ 'ask-question': {param_types: [GObject.TYPE_JSOBJECT]}, ++ /** ++ * @param {boolean} [softReset] ++ * @param {boolean} [reuseEntryText] ++ */ ++ 'reset': {param_types: [GObject.TYPE_JSOBJECT]}, ++ /** ++ * @param {string} serviceName ++ * @param {string} promptMessage ++ * @param {object} [choiceList] - keys map to {title: string} ++ */ ++ 'show-choice-list': {param_types: [GObject.TYPE_JSOBJECT]}, + 'mechanisms-changed': {}, + }; + @@ -5440,7 +5865,7 @@ index 000000000..8f05a5d8f + this._updateUserVerifier(userVerifierProxies); + await this._startServices(this._cancellable); + } catch (e) { -+ if (e.error?.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ if (e.cause?.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + return; + + this._failCounter++; @@ -5450,15 +5875,17 @@ index 000000000..8f05a5d8f + this._handleBeginVerification(); + } + ++ _mechanismEquals(m1, m2) { ++ return m1?.serviceName === m2?.serviceName && ++ m1?.role === m2?.role; ++ } ++ + selectMechanism(mechanism) { -+ if (this._selectedMechanism?.role === mechanism.role && -+ this._selectedMechanism?.serviceName === mechanism.serviceName) ++ if (this._mechanismEquals(this._selectedMechanism, mechanism)) + return false; + -+ this._selectedMechanism = this._enabledMechanisms?.find(m => -+ m.role === mechanism.role && -+ m.serviceName === mechanism.serviceName -+ ); ++ this._selectedMechanism = this._enabledMechanisms?.find( ++ m => this._mechanismEquals(m, mechanism)); + + this._handleSelectMechanism(); + @@ -5493,6 +5920,7 @@ index 000000000..8f05a5d8f + this._activeServices.clear(); + + this._verificationComplete = false; ++ this._userName = null; + + this._clearUserVerifier(); + @@ -5514,6 +5942,7 @@ index 000000000..8f05a5d8f + } + + _updateEnabledMechanisms() { ++ this._selectedMechanism = null; + this._enabledMechanisms = []; + + this._handleUpdateEnabledMechanisms(); @@ -5559,7 +5988,7 @@ index 000000000..8f05a5d8f + } + }); + -+ this.emit('wait-pending-messages', task); ++ this.emit('wait-pending-messages', {task}); + + return promise; + } @@ -5623,7 +6052,7 @@ index 000000000..8f05a5d8f + _onConversationStopped(serviceName) { + this._activeServices.delete(serviceName); + -+ this.emit('filter-messages', serviceName, Util.MessageType.ERROR); ++ this.emit('filter-messages', {serviceName, messageType: Util.MessageType.ERROR}); + + this._handleOnConversationStopped(serviceName); + } @@ -5632,10 +6061,11 @@ index 000000000..8f05a5d8f + this._unavailableServices.add(serviceName); + + if (this._selectedMechanism?.serviceName === serviceName && errorMessage) { -+ this.emit('queue-message', ++ this.emit('queue-message', { + serviceName, -+ errorMessage, -+ Util.MessageType.ERROR); ++ message: errorMessage, ++ messageType: Util.MessageType.ERROR, ++ }); + } + + this._handleOnServiceUnavailable(serviceName, errorMessage); @@ -5664,7 +6094,7 @@ index 000000000..8f05a5d8f + + const doneTrying = !shouldRetry || !this._canRetry(); + -+ this.emit('verification-failed', serviceName, !doneTrying); ++ this.emit('verification-failed', {serviceName, canRetry: !doneTrying}); + + try { + await this._waitPendingMessages(); @@ -5736,7 +6166,10 @@ index 000000000..8f05a5d8f + + _handleBeginVerification() {} + -+ _handleSelectMechanism() {} ++ _handleSelectMechanism() { ++ throw new GObject.NotImplementedError( ++ `_handleSelectMechanism in ${this.constructor.name}`); ++ } + + _handleNeedsUsername() { + return true; @@ -5790,10 +6223,10 @@ index 000000000..8f05a5d8f +} diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js new file mode 100644 -index 000000000..a77c0fbc0 +index 000000000..ac2dcfb44 --- /dev/null +++ b/js/gdm/authServicesLegacy.js -@@ -0,0 +1,334 @@ +@@ -0,0 +1,351 @@ +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + @@ -5896,18 +6329,24 @@ index 000000000..a77c0fbc0 + + _handleClear() { + this._smartcardInProgress = false; ++ this._clearFingerprintSignalHandlers(); ++ } ++ ++ _clearFingerprintSignalHandlers() { ++ if (this._fingerprintFailedId) { ++ GLib.source_remove(this._fingerprintFailedId); ++ this._fingerprintFailedId = 0; ++ } + } + + _handleUpdateEnabledMechanisms() { ++ this._enabledMechanisms.push(...Mechanisms.filter(m => ++ this._enabledRoles.includes(m.role) ++ )); ++ + if (!this._fingerprintManager?.readerFound) { -+ this._enabledMechanisms.push(...Mechanisms.filter(m => -+ this._enabledRoles.includes(m.role) && -+ m.role !== Constants.FINGERPRINT_ROLE_NAME -+ )); -+ } else { -+ this._enabledMechanisms.push(...Mechanisms.filter(m => -+ this._enabledRoles.includes(m.role) -+ )); ++ this._enabledMechanisms = this._enabledMechanisms.filter(m => ++ m.role !== Constants.FINGERPRINT_ROLE_NAME); + } + } + @@ -5929,22 +6368,27 @@ index 000000000..a77c0fbc0 + + _handleOnInfo(serviceName, info) { + if (serviceName === this._selectedMechanism?.serviceName) { -+ this.emit('queue-message', serviceName, info, Util.MessageType.INFO); ++ this.emit('queue-message', { ++ serviceName, ++ message: info, ++ messageType: Util.MessageType.INFO, ++ }); + } else if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName)) { + // We don't show fingerprint messages directly since it's + // not the main auth service. Instead we use the messages + // as a cue to display our own message. -+ this.emit('queue-message', ++ this.emit('queue-message', { + serviceName, -+ this._fingerprintManager?.readerType === FingerprintReaderType.SWIPE ++ message: this._fingerprintManager?.readerType === FingerprintReaderType.SWIPE + // Translators: this message is shown below the password entry field + // to indicate the user can swipe their finger on the fingerprint reader + ? _('(or swipe finger across reader)') + // Translators: this message is shown below the password entry field + // to indicate the user can place their finger on the fingerprint reader instead + : _('(or place finger on reader)'), -+ Util.MessageType.HINT); ++ messageType: Util.MessageType.HINT, ++ }); + } + } + @@ -5952,10 +6396,11 @@ index 000000000..a77c0fbc0 + if (serviceName === this._selectedMechanism?.serviceName || + (serviceName === Constants.FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName))) { -+ this.emit('queue-priority-message', ++ this.emit('queue-priority-message', { + serviceName, -+ problem, -+ Util.MessageType.ERROR); ++ message: problem, ++ messageType: Util.MessageType.ERROR, ++ }); + } + + if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && @@ -5991,7 +6436,7 @@ index 000000000..a77c0fbc0 + if (serviceName !== this._selectedMechanism?.serviceName) + return; + -+ this.emit('ask-question', serviceName, question, false); ++ this.emit('ask-question', {serviceName, question}); + } + + _handleOnSecretInfoQuery(serviceName, secretQuestion) { @@ -6007,7 +6452,11 @@ index 000000000..a77c0fbc0 + return; + } + -+ this.emit('ask-question', serviceName, secretQuestion, true); ++ this.emit('ask-question', { ++ serviceName, ++ question: secretQuestion, ++ secret: true, ++ }); + } + + _handleOnConversationStopped(serviceName) { @@ -6050,10 +6499,11 @@ index 000000000..a77c0fbc0 + !errorMessage) + return; + -+ this.emit('queue-message', ++ this.emit('queue-message', { + serviceName, -+ errorMessage, -+ Util.MessageType.ERROR); ++ message: errorMessage, ++ messageType: Util.MessageType.ERROR, ++ }); + } + + _handleVerificationFailed(serviceName) { @@ -6081,7 +6531,7 @@ index 000000000..a77c0fbc0 + for (const [key, value] of Object.entries(list.deepUnpack())) + choiceList[key] = {title: value}; + -+ this.emit('show-choice-list', serviceName, promptMessage, choiceList); ++ this.emit('show-choice-list', {serviceName, promptMessage, choiceList}); + } + + _handleGetCredentialManagerServices() { @@ -6128,8 +6578,21 @@ index 000000000..a77c0fbc0 + this.emit('reset', {softReset: true}); + } +} +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index cdb64d322..61e671199 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1106,7 +1106,7 @@ export const LoginDialog = GObject.registerClass({ + this._authMenuButton.reactive = !isLoading; + } + +- _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { ++ _onMechanismsChanged(_authPrompt, {mechanisms, selectedMechanism}) { + this._authMenuButton.clearItems({ + sectionName: _PRIMARY_LOGIN_METHOD_SECTION_NAME, + }); diff --git a/js/gdm/util.js b/js/gdm/util.js -index db6488113..ddfecd0cf 100644 +index ec0a6f84d..97c2fdde0 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -1,24 +1,14 @@ @@ -6141,7 +6604,7 @@ index db6488113..ddfecd0cf 100644 import * as Batch from './batch.js'; import * as Constants from './constants.js'; --import {FingerprintManager, FingerprintReaderType} from './fingerprintManager.js'; +-import * as FingerprintManager from './fingerprintManager.js'; -import * as OVirt from './oVirt.js'; -import * as Vmware from './vmware.js'; import * as Main from '../ui/main.js'; @@ -6195,7 +6658,7 @@ index db6488113..ddfecd0cf 100644 - this._defaultService = null; - this._preemptingService = null; -- this._fingerprintReaderType = FingerprintReaderType.NONE; +- this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; - this._fingerprintReaderFound = false; - this._messageQueue = []; @@ -6244,7 +6707,7 @@ index db6488113..ddfecd0cf 100644 } get hasPendingMessages() { -@@ -196,84 +156,85 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -196,84 +156,100 @@ export class ShellUserVerifier extends Signals.EventEmitter { return this._messageQueue ? this._messageQueue[0] : null; } @@ -6259,11 +6722,13 @@ index db6488113..ddfecd0cf 100644 - this._fingerprintManager?.checkReaderType(this._cancellable); + try { + const proxies = await this._getUserVerifierProxies(userName, this._cancellable); ++ this._setUserVerifier(proxies.userVerifier); + await this._authServicesLegacy?.beginVerification(userName, proxies); -+ this._userVerifier = proxies.userVerifier; + } catch (e) { + if (e instanceof InitError) + this._reportInitError(e); ++ else ++ logErrorUnlessCancelled(e); + } - // If possible, reauthenticate an already running session, @@ -6275,9 +6740,10 @@ index db6488113..ddfecd0cf 100644 + hold?.release(); } - selectMechanism(mechanism) { +- selectMechanism() { - // TODO: Implement mechanism selection - return false; ++ selectMechanism(mechanism) { + return this._authServicesLegacy?.selectMechanism(mechanism); } @@ -6329,6 +6795,19 @@ index db6488113..ddfecd0cf 100644 + this._cancellable?.cancel(); + this._cancellable = null; + ++ this._clearUserVerifier(); ++ } ++ ++ _setUserVerifier(userVerifier) { ++ this._clearUserVerifier(); ++ this._userVerifier = userVerifier; ++ this._userVerifier.get_connection().connectObject( ++ 'closed', () => this._clearUserVerifier(), ++ this); ++ } ++ ++ _clearUserVerifier() { ++ this._userVerifier?.get_connection().disconnectObject(this); + this._userVerifier = null; } @@ -6378,7 +6857,7 @@ index db6488113..ddfecd0cf 100644 } _getIntervalForMessage(message) { -@@ -285,7 +246,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -285,7 +261,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { MESSAGE_TIME_MULTIPLIER; } @@ -6387,7 +6866,36 @@ index db6488113..ddfecd0cf 100644 if (!this.hasPendingMessages) return; -@@ -328,7 +289,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -298,12 +274,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { + return this._messageQueue.some(m => m.serviceName === serviceName); + } + +- _filterServiceMessages(serviceName, messageType) { ++ _filterServiceMessages({serviceName, messageType}) { + // This function allows to remove queued messages for the @serviceName + // whose type has lower priority than @messageType, replacing them + // with a null message that will lead to clearing the prompt once done. + if (this._serviceHasPendingMessages(serviceName)) +- this._queuePriorityMessage(serviceName, null, messageType); ++ this._queuePriorityMessage({serviceName, messageType}); + } + + async _queueMessageTimeout() { +@@ -314,7 +290,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { + const {promise, resolve} = Promise.withResolvers(); + this._showMessageResolver = resolve; + +- this.emit('show-message', message.serviceName, message.text, message.type, this._showMessageResolver); ++ this.emit('show-message', { ++ serviceName: message.serviceName, ++ message: message.text, ++ type: message.type, ++ showMessageResolver: this._showMessageResolver, ++ }); + + await promise.catch(logError); + if (!this._showMessageResolver) +@@ -329,7 +310,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._messageQueue.shift(); this._queueMessageTimeout(); } else { @@ -6396,7 +6904,30 @@ index db6488113..ddfecd0cf 100644 } return GLib.SOURCE_REMOVE; -@@ -358,7 +319,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -337,14 +318,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout'); + } + +- _queueMessage(serviceName, message, messageType) { +- let interval = this._getIntervalForMessage(message); ++ _queueMessage({serviceName, message, messageType}) { ++ const interval = this._getIntervalForMessage(message); + + this._messageQueue.push({serviceName, text: message, type: messageType, interval}); + this._queueMessageTimeout(); + } + +- _queuePriorityMessage(serviceName, message, messageType) { ++ _queuePriorityMessage({serviceName, message, messageType}) { + const newQueue = this._messageQueue.filter(m => { + if (m.serviceName !== serviceName || m.type >= messageType) + return m.text !== message; +@@ -355,11 +336,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._clearMessageQueue(); + + this._messageQueue = newQueue; +- this._queueMessage(serviceName, message, messageType); ++ this._queueMessage({serviceName, message, messageType}); } _clearMessageQueue() { @@ -6405,19 +6936,26 @@ index db6488113..ddfecd0cf 100644 if (this._messageQueueTimeoutId !== 0) { GLib.source_remove(this._messageQueueTimeoutId); -@@ -367,470 +328,147 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this.emit('show-message', null, null, MessageType.NONE); +@@ -371,473 +352,154 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._showMessageResolver = null; + } + +- this.emit('show-message', null, null, MessageType.NONE); ++ this.emit('show-message', {type: MessageType.NONE}); } - async _initFingerprintManager() { - if (this._fingerprintManager) - return; -- -- this._fingerprintManager = new FingerprintManager(this._cancellable); ++ _reportInitError(initError) { ++ const {cause, message, serviceName} = initError; + +- this._fingerprintManager = FingerprintManager.getFingerprintManager(); - this._fingerprintManager.connectObject( - 'reader-type-changed', () => this._onFingerprintReaderTypeChanged(), - this); -- ++ logError(cause, message); + - if (!this._getDetectedDefaultService()) { - // Other authentication methods would have already been detected by - // now as possibilities if they were available. @@ -6429,7 +6967,34 @@ index db6488113..ddfecd0cf 100644 - // we should fail early without having the shell to misbehave - this._fingerprintManager.setDefaultTimeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT); - await this._fingerprintManager.checkReaderType(this._cancellable); -- } else { ++ this._queueMessage({ ++ serviceName, ++ message: _('Authentication error'), ++ messageType: MessageType.ERROR, ++ }); ++ this._verificationFailed({serviceName}); ++ } ++ ++ async _getUserVerifierProxies(userName, cancellable) { ++ const proxies = {}; ++ ++ if (userName) { ++ try { ++ proxies.userVerifier = await this._client.open_reauthentication_channel( ++ userName, cancellable); ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ throw e; ++ if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) && ++ !this._reauthOnly) { ++ // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there ++ // is no session to reauthenticate. Fall back to performing ++ // verification from this login session ++ return this._getUserVerifierProxies(null, cancellable); ++ } ++ throw new InitError(e, 'Failed to open reauthentication channel'); ++ } + } else { - // Ensure fingerprint service starts, but do not wait for it - this._fingerprintManager.checkReaderType(this._cancellable); - } @@ -6489,9 +7054,7 @@ index db6488113..ddfecd0cf 100644 - this._preemptingService = null; - - this._updateDefaultService(); -+ _reportInitError(initError) { -+ const {cause, message, serviceName} = initError; - +- - this.emit('smartcard-status-changed'); - } - } @@ -6499,13 +7062,12 @@ index db6488113..ddfecd0cf 100644 - _reportInitError(where, error, serviceName) { - logError(error, where); - this._hold?.release(); -+ logError(cause, message); - - this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); +- +- this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR); - this._failCounter++; - this._verificationFailed(serviceName, false); - } - +- this._verificationFailed(serviceName, false); +- } +- - async _openReauthenticationChannel(userName) { - try { - this._clearUserVerifier(); @@ -6523,26 +7085,6 @@ index db6488113..ddfecd0cf 100644 - // verification from this login session - this._getUserVerifier(); - return; -+ async _getUserVerifierProxies(userName, cancellable) { -+ const proxies = {}; -+ -+ if (userName) { -+ try { -+ proxies.userVerifier = await this._client.open_reauthentication_channel( -+ userName, cancellable); -+ } catch (e) { -+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) -+ throw e; -+ if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) && -+ !this._reauthOnly) { -+ // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there -+ // is no session to reauthenticate. Fall back to performing -+ // verification from this login session -+ return this._getUserVerifierProxies(null, cancellable); -+ } -+ throw new InitError(e, 'Failed to open reauthentication channel'); -+ } -+ } else { + try { + proxies.userVerifier = await this._client.get_user_verifier( + cancellable); @@ -6667,7 +7209,7 @@ index db6488113..ddfecd0cf 100644 - this._fingerprintManager.disconnectObject(this); - this._fingerprintManager = null; - this._fingerprintReaderFound = false; -- this._fingerprintReaderType = FingerprintReaderType.NONE; +- this._fingerprintReaderType = FingerprintManager.FingerprintReaderType.NONE; - - if (this._activeServices.has(Constants.FINGERPRINT_SERVICE_NAME)) - needsReset = true; @@ -6754,24 +7296,24 @@ index db6488113..ddfecd0cf 100644 - } - this._hold?.release(); - } - +- - _beginVerification() { - this._startService(this._getForegroundService()); - this._maybeStartFingerprintVerification().catch(logError); - } -+ this._enabledRoles = enabledRoles; - +- - async _maybeStartFingerprintVerification() { - if (this._userName && - this._fingerprintReaderFound && - !this.serviceIsForeground(Constants.FINGERPRINT_SERVICE_NAME)) - await this._startService(Constants.FINGERPRINT_SERVICE_NAME); - } -- + - _onChoiceListQuery(client, serviceName, promptMessage, list) { - if (!this.serviceIsForeground(serviceName)) - return; -- ++ this._enabledRoles = enabledRoles; + - const choiceList = {}; - for (const [key, value] of Object.entries(list.deepUnpack())) - choiceList[key] = {title: value}; @@ -6786,7 +7328,7 @@ index db6488113..ddfecd0cf 100644 - // We don't show fingerprint messages directly since it's - // not the main auth service. Instead we use the messages - // as a cue to display our own message. -- if (this._fingerprintReaderType === FingerprintReaderType.SWIPE) { +- if (this._fingerprintReaderType === FingerprintManager.FingerprintReaderType.SWIPE) { - // Translators: this message is shown below the password entry field - // to indicate the user can swipe their finger on the fingerprint reader - this._queueMessage(serviceName, _('(or swipe finger across reader)'), @@ -6877,16 +7419,16 @@ index db6488113..ddfecd0cf 100644 + this._authServicesLegacy, + ].forEach(authServices => { + authServices?.connectObject( -+ 'ask-question', (_, ...args) => this.emit('ask-question', ...args), -+ 'queue-message', (_, ...args) => this._queueMessage(...args), -+ 'queue-priority-message', (_, ...args) => this._queuePriorityMessage(...args), -+ 'wait-pending-messages', (_, ...args) => this._waitPendingMessages(...args), -+ 'filter-messages', (_, ...args) => this._filterServiceMessages(...args), -+ 'verification-failed', (_, ...args) => this._verificationFailed(...args), -+ 'verification-complete', (_, ...args) => this.emit('verification-complete', ...args), -+ 'reset', (_, ...args) => this.emit('reset', ...args), -+ 'show-choice-list', (_, ...args) => this.emit('show-choice-list', ...args), -+ 'mechanisms-changed', (_, ...args) => this._onMechanismsChanged(...args), ++ 'ask-question', (_, args) => this.emit('ask-question', args), ++ 'queue-message', (_, args) => this._queueMessage(args), ++ 'queue-priority-message', (_, args) => this._queuePriorityMessage(args), ++ 'wait-pending-messages', (_, args) => this._waitPendingMessages(args), ++ 'filter-messages', (_, args) => this._filterServiceMessages(args), ++ 'verification-failed', (_, args) => this._verificationFailed(args), ++ 'verification-complete', () => this.emit('verification-complete'), ++ 'reset', (_, args) => this.emit('reset', args), ++ 'show-choice-list', (_, args) => this.emit('show-choice-list', args), ++ 'mechanisms-changed', () => this._onMechanismsChanged(), + this); + }); } @@ -6899,9 +7441,9 @@ index db6488113..ddfecd0cf 100644 - this._updateDefaultService(); - - this.emit('reset'); -+ _verificationFailed(serviceName, canRetry) { -+ this._filterServiceMessages(serviceName, MessageType.ERROR); -+ this.emit('verification-failed', serviceName, canRetry); ++ _verificationFailed({serviceName, canRetry}) { ++ this._filterServiceMessages({serviceName, messageType: MessageType.ERROR}); ++ this.emit('verification-failed', {serviceName, canRetry}); } - _onVerificationComplete(_client, serviceName) { @@ -6935,7 +7477,7 @@ index db6488113..ddfecd0cf 100644 - _canRetry() { - return this._userName && - (this._reauthOnly || this._failCounter < this.allowedFailures); -+ this.emit('mechanisms-changed', mechanisms, selectedMechanism); ++ this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); } - async _verificationFailed(serviceName, shouldRetry) { @@ -6953,7 +7495,7 @@ index db6488113..ddfecd0cf 100644 - const doneTrying = !shouldRetry || !this._canRetry(); - - this.emit('verification-failed', serviceName, !doneTrying); -+ async _waitPendingMessages(task) { ++ async _waitPendingMessages({task}) { try { - if (doneTrying) { - this._disconnectSignals(); @@ -6971,7 +7513,7 @@ index db6488113..ddfecd0cf 100644 } } -@@ -849,47 +487,4 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -856,47 +518,4 @@ export class ShellUserVerifier extends Signals.EventEmitter { }); }); } @@ -7032,26 +7574,40 @@ index 162e19d41..dce0c6acc 100644 gdm/batch.js gdm/constants.js gdm/credentialManager.js +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index 38d7b97c4..af43393e4 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -946,7 +946,7 @@ export const UnlockDialog = GObject.registerClass({ + this._authMenuButton.reactive = !isLoading; + } + +- _onMechanismsChanged(_authPrompt, mechanisms, selectedMechanism) { ++ _onMechanismsChanged(_authPrompt, {mechanisms, selectedMechanism}) { + this._authMenuButton.clearItems({ + sectionName: PRIMARY_UNLOCK_METHOD_SECTION_NAME, + }); diff --git a/po/POTFILES.in b/po/POTFILES.in -index ee0829c96..eb58487b4 100644 +index ee0829c96..7f534c75f 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -11,6 +11,7 @@ data/X-GNOME-Shell-Utilities.directory.desktop.in +@@ -11,6 +11,8 @@ data/X-GNOME-Shell-Utilities.directory.desktop.in js/dbusServices/extensions/extensionPrefsDialog.js js/dbusServices/extensions/ui/extension-error-page.ui js/gdm/authPrompt.js ++js/gdm/authServices.js +js/gdm/authServicesLegacy.js js/gdm/loginDialog.js js/gdm/util.js js/misc/breakManager.js -- -2.54.0 +2.55.0 -From 8a6e99edeb08b1fa93ab82338723e13080e20b34 Mon Sep 17 00:00:00 2001 +From c95867ff2c518df0414a6b9668ec83d3cbef57a4 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 16 Feb 2026 12:21:54 +0100 -Subject: [PATCH 47/54] gdm: Add fingerprint ready state to delay showing icon +Subject: [PATCH 48/72] gdm: Add fingerprint ready state to delay showing icon Fingerprint mechanism now has a ready state that controls when it appears in the authentication UI. When fingerprint authentication @@ -7064,16 +7620,18 @@ This prevents the fingerprint icon from briefly appearing in the UI for users who don't have enrolled fingerprints. In those cases, the fingerprint service starts but quickly stops with service-unavailable, so the timeout never completes and the icon never shows. + +Part-of: --- js/gdm/authServices.js | 2 +- - js/gdm/authServicesLegacy.js | 54 ++++++++++++++++++++++++++++++++---- - 2 files changed, 50 insertions(+), 6 deletions(-) + js/gdm/authServicesLegacy.js | 46 ++++++++++++++++++++++++++++++++---- + 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 8f05a5d8f..52fb5b27e 100644 +index fd27583c2..5abbb3e27 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -95,7 +95,7 @@ export class AuthServices extends GObject.Object { +@@ -112,7 +112,7 @@ export class AuthServices extends GObject.Object { } get enabledMechanisms() { @@ -7083,7 +7641,7 @@ index 8f05a5d8f..52fb5b27e 100644 get _roleToService() { diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index a77c0fbc0..85c55610e 100644 +index ac2dcfb44..1c1cce1e4 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js @@ -10,6 +10,7 @@ import * as Vmware from './vmware.js'; @@ -7103,14 +7661,10 @@ index a77c0fbc0..85c55610e 100644 } _handleSelectChoice(serviceName, key) { -@@ -100,6 +103,41 @@ export class AuthServicesLegacy extends AuthServices { - - _handleClear() { - this._smartcardInProgress = false; -+ this._clearFingerprintReadyTimeout(); -+ } -+ -+ _clearFingerprintReadyTimeout() { +@@ -108,6 +111,36 @@ export class AuthServicesLegacy extends AuthServices { + GLib.source_remove(this._fingerprintFailedId); + this._fingerprintFailedId = 0; + } + if (this._fingerprintReadyTimeoutId) { + GLib.source_remove(this._fingerprintReadyTimeoutId); + this._fingerprintReadyTimeoutId = 0; @@ -7140,23 +7694,22 @@ index a77c0fbc0..85c55610e 100644 + + mechanism.ready = ready; + -+ if (ready) -+ this.emit('mechanisms-changed'); ++ this.emit('mechanisms-changed'); } _handleUpdateEnabledMechanisms() { -@@ -112,6 +150,10 @@ export class AuthServicesLegacy extends AuthServices { - this._enabledMechanisms.push(...Mechanisms.filter(m => - this._enabledRoles.includes(m.role) - )); -+ +@@ -118,6 +151,10 @@ export class AuthServicesLegacy extends AuthServices { + if (!this._fingerprintManager?.readerFound) { + this._enabledMechanisms = this._enabledMechanisms.filter(m => + m.role !== Constants.FINGERPRINT_ROLE_NAME); ++ } else { + // Mark fingerprint as not ready until service confirms + // it's working for this user + this._setFingerprintReady(false); } } -@@ -229,11 +271,13 @@ export class AuthServicesLegacy extends AuthServices { +@@ -245,11 +282,10 @@ export class AuthServicesLegacy extends AuthServices { return; } @@ -7166,23 +7719,20 @@ index a77c0fbc0..85c55610e 100644 - .filter(m => m.serviceName !== serviceName); - this.emit('mechanisms-changed'); + if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { -+ this._clearFingerprintReadyTimeout(); -+ if (this._unavailableServices.has(serviceName)) { -+ this._enabledMechanisms = this._enabledMechanisms -+ .filter(m => m.serviceName !== serviceName); -+ this.emit('mechanisms-changed'); -+ } ++ this._clearFingerprintSignalHandlers(); ++ if (this._unavailableServices.has(serviceName)) ++ this._setFingerprintReady(false); } if (this._unavailableServices.has(serviceName)) -- -2.54.0 +2.55.0 -From ec86c48f1c238cae30342b7f241eddb2e98208aa Mon Sep 17 00:00:00 2001 +From e2af995e18e082d8e3e4ac2c6f0afb2fc517b81f Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 6 Feb 2024 14:09:34 -0500 -Subject: [PATCH 48/54] gdm: Add authServicesSSSDSwitchable +Subject: [PATCH 49/72] gdm: Add authServicesSSSDSwitchable This new authService child is used by SSSD to control multiple authentication mechanisms from a single PAM conversation using 'gdm-switchable-auth' @@ -7215,22 +7765,24 @@ When a mechanism is selected, both authServices try to store it. Only the one that has it in _enabledMechanisms keeps it; the other gets null. The stored selected mechanism indicates whether authServicesLegacy or authServicesSwitchable will handle interactions. + +Part-of: --- js/gdm/authServices.js | 1 + - js/gdm/authServicesSSSDSwitchable.js | 166 +++++++++++++++++++++++++++ + js/gdm/authServicesSSSDSwitchable.js | 168 +++++++++++++++++++++++++++ js/gdm/loginDialog.js | 5 +- js/gdm/util.js | 42 ++++++- js/js-resources.gresource.xml | 1 + js/ui/unlockDialog.js | 1 + - po/POTFILES.in | 1 + - 7 files changed, 210 insertions(+), 7 deletions(-) + po/POTFILES.in | 9 +- + 7 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 js/gdm/authServicesSSSDSwitchable.js diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 52fb5b27e..4394848ba 100644 +index 5abbb3e27..67783e82a 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -18,6 +18,7 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_use +@@ -16,6 +16,7 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification_for_use Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification'); Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); @@ -7240,10 +7792,10 @@ index 52fb5b27e..4394848ba 100644 static [GObject.signals] = { diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js new file mode 100644 -index 000000000..c67933b6f +index 000000000..04e97b076 --- /dev/null +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -0,0 +1,166 @@ +@@ -0,0 +1,168 @@ +import GObject from 'gi://GObject'; + +import * as Constants from './constants.js'; @@ -7264,10 +7816,6 @@ index 000000000..c67933b6f + GObject.registerClass(this); + } + -+ constructor(params) { -+ super(params); -+ } -+ + _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; @@ -7305,7 +7853,10 @@ index 000000000..c67933b6f + this._selectedMechanism = null; + } + -+ _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { ++ _handleOnCustomJSONRequest(serviceName, _protocol, _version, json) { ++ if (serviceName !== Constants.SWITCHABLE_AUTH_SERVICE_NAME) ++ return; ++ + let requestObject; + + try { @@ -7316,13 +7867,10 @@ index 000000000..c67933b6f + } + + const {authSelection} = requestObject; -+ if (authSelection) { -+ this._mechanisms = authSelection.mechanisms; -+ this._priorityList = authSelection.priority; ++ this._mechanisms = authSelection?.mechanisms ?? null; ++ this._priorityList = authSelection?.priority ?? null; + -+ if (this._mechanisms) -+ this._updateEnabledMechanisms(); -+ } ++ this._updateEnabledMechanisms(); + } + + _handleUpdateEnabledMechanisms() { @@ -7339,7 +7887,7 @@ index 000000000..c67933b6f + this._enabledMechanisms + .find(m => this._savedMechanism?.role === m.role) ?? + this._priorityList -+ .map(id => this._enabledMechanisms.find(m => m.id === id))[0] ?? ++ ?.map(id => this._enabledMechanisms.find(m => m.id === id))[0] ?? + this._enabledMechanisms[0]; + this.selectMechanism(selectedMechanism); + @@ -7347,16 +7895,22 @@ index 000000000..c67933b6f + } + + _handleOnInfo(serviceName, info) { -+ if (serviceName === this._selectedMechanism?.serviceName) -+ this.emit('queue-message', serviceName, info, Util.MessageType.INFO); ++ if (serviceName === this._selectedMechanism?.serviceName) { ++ this.emit('queue-message', { ++ serviceName, ++ message: info, ++ messageType: Util.MessageType.INFO, ++ }); ++ } + } + + _handleOnProblem(serviceName, problem) { + if (serviceName === this._selectedMechanism?.serviceName) { -+ this.emit('queue-priority-message', ++ this.emit('queue-priority-message', { + serviceName, -+ problem, -+ Util.MessageType.ERROR); ++ message: problem, ++ messageType: Util.MessageType.ERROR, ++ }); + } + } + @@ -7407,11 +7961,11 @@ index 000000000..c67933b6f + _startPasswordLogin() { + const {serviceName, prompt} = this._selectedMechanism; + -+ this.emit('ask-question', serviceName, prompt, true); ++ this.emit('ask-question', {serviceName, question: prompt, secret: true}); + } +} diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 8656bdf29..69573adfe 100644 +index 61e671199..6d0708b1f 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -453,7 +453,10 @@ export const LoginDialog = GObject.registerClass({ @@ -7427,7 +7981,7 @@ index 8656bdf29..69573adfe 100644 } diff --git a/js/gdm/util.js b/js/gdm/util.js -index ddfecd0cf..5952947e7 100644 +index 97c2fdde0..d5bac9c6a 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -9,6 +9,7 @@ import * as Main from '../ui/main.js'; @@ -7446,15 +8000,15 @@ index ddfecd0cf..5952947e7 100644 export const BANNER_MESSAGE_KEY = 'banner-message-enable'; export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; -@@ -162,6 +164,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - +@@ -163,6 +165,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { try { const proxies = await this._getUserVerifierProxies(userName, this._cancellable); + this._setUserVerifier(proxies.userVerifier); + await this._authServicesSSSDSwitchable?.beginVerification(userName, proxies); await this._authServicesLegacy?.beginVerification(userName, proxies); - this._userVerifier = proxies.userVerifier; } catch (e) { -@@ -173,14 +176,19 @@ export class ShellUserVerifier extends Signals.EventEmitter { + if (e instanceof InitError) +@@ -175,14 +178,19 @@ export class ShellUserVerifier extends Signals.EventEmitter { } selectMechanism(mechanism) { @@ -7476,7 +8030,7 @@ index ddfecd0cf..5952947e7 100644 this._authServicesLegacy?.reset(); this._userVerifier?.call_cancel_sync(null); -@@ -189,6 +197,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -191,6 +199,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } cancel() { @@ -7484,7 +8038,7 @@ index ddfecd0cf..5952947e7 100644 this._authServicesLegacy?.cancel(); this._userVerifier?.call_cancel_sync(null); -@@ -197,6 +206,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -199,6 +208,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } clear() { @@ -7492,7 +8046,7 @@ index ddfecd0cf..5952947e7 100644 this._authServicesLegacy?.clear(); this._clearMessageQueue(); -@@ -208,6 +218,9 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -223,6 +233,9 @@ export class ShellUserVerifier extends Signals.EventEmitter { } destroy() { @@ -7502,7 +8056,7 @@ index ddfecd0cf..5952947e7 100644 this._authServicesLegacy?.destroy(); this._authServicesLegacy = null; -@@ -218,6 +231,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -233,6 +246,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { } selectChoice(serviceName, key) { @@ -7510,7 +8064,7 @@ index ddfecd0cf..5952947e7 100644 this._authServicesLegacy?.selectChoice(serviceName, key); } -@@ -226,6 +240,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -241,6 +255,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { // ensure no messages get lost await this._handlePendingMessages().catch(logErrorUnlessCancelled); @@ -7518,7 +8072,7 @@ index ddfecd0cf..5952947e7 100644 this._authServicesLegacy?.answerQuery(serviceName, answer); } -@@ -397,10 +412,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -428,10 +443,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); @@ -7535,7 +8089,7 @@ index ddfecd0cf..5952947e7 100644 this._createAuthServices(); } -@@ -414,19 +434,25 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -445,19 +465,25 @@ export class ShellUserVerifier extends Signals.EventEmitter { allowedFailures: this.allowedFailures, reauthOnly: this._reauthOnly, }; @@ -7562,7 +8116,7 @@ index ddfecd0cf..5952947e7 100644 this._authServicesLegacy, ].forEach(authServices => { authServices?.connectObject( -@@ -450,11 +476,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -481,11 +507,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { } get selectedMechanism() { @@ -7593,7 +8147,7 @@ index dce0c6acc..93377b92c 100644 gdm/constants.js gdm/credentialManager.js diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index af584470a..50865198a 100644 +index af43393e4..84cd0fb08 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -567,6 +567,7 @@ export const UnlockDialog = GObject.registerClass({ @@ -7605,25 +8159,40 @@ index af584470a..50865198a 100644 } catch { } diff --git a/po/POTFILES.in b/po/POTFILES.in -index eb58487b4..d1e4d6487 100644 +index 7f534c75f..f0e1aed6e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -12,6 +12,7 @@ js/dbusServices/extensions/extensionPrefsDialog.js - js/dbusServices/extensions/ui/extension-error-page.ui +@@ -13,6 +13,7 @@ js/dbusServices/extensions/ui/extension-error-page.ui js/gdm/authPrompt.js + js/gdm/authServices.js js/gdm/authServicesLegacy.js +js/gdm/authServicesSSSDSwitchable.js js/gdm/loginDialog.js js/gdm/util.js js/misc/breakManager.js +@@ -97,14 +98,6 @@ src/shell-keyring-prompt.c + src/shell-polkit-authentication-agent.c + src/shell-util.c + src/st/st-icon-theme.c +-subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in +-subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in +-subprojects/extensions-app/js/extensionManager.js +-subprojects/extensions-app/js/extensionRow.js +-subprojects/extensions-app/js/extensionsWindow.js +-subprojects/extensions-app/data/ui/extension-row.ui +-subprojects/extensions-app/data/ui/extensions-window.ui +-subprojects/extensions-app/data/ui/shortcuts-dialog.ui + subprojects/extensions-tool/src/command-create.c + subprojects/extensions-tool/src/command-disable.c + subprojects/extensions-tool/src/command-enable.c -- -2.54.0 +2.55.0 -From 9fe8dcb095a27f0acc4090618eee101c9fcc0812 Mon Sep 17 00:00:00 2001 +From dd6723b0307b61000b21558cc0b9935ef5d29491 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Wed, 12 Nov 2025 17:25:33 +0100 -Subject: [PATCH 49/54] authServicesSSSDSwitchable: Allow resetting expired +Subject: [PATCH 50/72] authServicesSSSDSwitchable: Allow resetting expired password JSON protocol can't inform when a password is expired, so it's needed to @@ -7633,15 +8202,17 @@ When the password is expired, a resetting process will be started. It will have multiple requests to insert the current password and insert the new password. This has to be done using the old flow because the PAM JSON protocol doesn't support this process yet. + +Part-of: --- - js/gdm/authServicesSSSDSwitchable.js | 24 ++++++++++++++++++++++++ - 1 file changed, 24 insertions(+) + js/gdm/authServicesSSSDSwitchable.js | 29 ++++++++++++++++++++++++++++ + 1 file changed, 29 insertions(+) diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index c67933b6f..91bdde5c7 100644 +index 04e97b076..e8b2e9620 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -26,6 +26,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -22,6 +22,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { if (serviceName !== this._selectedMechanism?.serviceName) return; @@ -7656,7 +8227,7 @@ index c67933b6f..91bdde5c7 100644 let response; switch (this._selectedMechanism.role) { case Constants.PASSWORD_ROLE_NAME: -@@ -57,6 +65,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -53,6 +61,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._priorityList = null; this._enabledMechanisms = null; this._selectedMechanism = null; @@ -7664,8 +8235,8 @@ index c67933b6f..91bdde5c7 100644 + this._resettingPassword = false; } - _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { -@@ -101,6 +111,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + _handleOnCustomJSONRequest(serviceName, _protocol, _version, json) { +@@ -97,6 +107,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnInfo(serviceName, info) { @@ -7676,31 +8247,36 @@ index c67933b6f..91bdde5c7 100644 + info.includes('Password expired. Change your password now')) + this._resettingPassword = true; + - if (serviceName === this._selectedMechanism?.serviceName) - this.emit('queue-message', serviceName, info, Util.MessageType.INFO); - } -@@ -114,6 +131,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (serviceName === this._selectedMechanism?.serviceName) { + this.emit('queue-message', { + serviceName, +@@ -116,6 +133,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } + _handleOnSecretInfoQuery(serviceName, secretQuestion) { + if (serviceName === this._selectedMechanism?.serviceName && + this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && -+ this._resettingPassword) -+ this.emit('ask-question', serviceName, secretQuestion, true); ++ this._resettingPassword) { ++ this.emit('ask-question', { ++ serviceName, ++ question: secretQuestion, ++ secret: true, ++ }); ++ } + } + _handleOnConversationStopped(serviceName) { if (serviceName !== this._selectedMechanism?.serviceName) return; -- -2.54.0 +2.55.0 -From 2aebcf41fa56e742b7d0f70508df630b003da92e Mon Sep 17 00:00:00 2001 +From 286d3b30f6535fcbaf33c11f34cbbc8575550763 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Thu, 21 Aug 2025 22:25:02 +0200 -Subject: [PATCH 50/54] gdm: Allow starting authServicesLegacy as fallback +Subject: [PATCH 51/72] gdm: Allow starting authServicesLegacy as fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @@ -7729,18 +8305,20 @@ For authServicesSSSDSwitchable: This way authServicesSSSDSwitchable controls which roles authServicesLegacy handles, enabling coordination between both services. + +Part-of: --- js/gdm/authServices.js | 22 +++++++++ - js/gdm/authServicesLegacy.js | 5 +++ - js/gdm/authServicesSSSDSwitchable.js | 67 +++++++++++++++++++++++++++- - js/gdm/util.js | 28 ++++++++++-- - 4 files changed, 116 insertions(+), 6 deletions(-) + js/gdm/authServicesLegacy.js | 5 ++ + js/gdm/authServicesSSSDSwitchable.js | 70 +++++++++++++++++++++++++++- + js/gdm/util.js | 28 +++++++++-- + 4 files changed, 119 insertions(+), 6 deletions(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 4394848ba..58905d15a 100644 +index 67783e82a..28a07558d 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -107,6 +107,10 @@ export class AuthServices extends GObject.Object { +@@ -124,6 +124,10 @@ export class AuthServices extends GObject.Object { return this.constructor.SupportedRoles; } @@ -7751,7 +8329,7 @@ index 4394848ba..58905d15a 100644 selectChoice(serviceName, key) { this._handleSelectChoice(serviceName, key); } -@@ -183,6 +187,18 @@ export class AuthServices extends GObject.Object { +@@ -203,6 +207,18 @@ export class AuthServices extends GObject.Object { this._handleClear(); } @@ -7770,7 +8348,7 @@ index 4394848ba..58905d15a 100644 _clearUserVerifier() { this._disconnectUserVerifierSignals(); this._userVerifier = null; -@@ -414,6 +430,10 @@ export class AuthServices extends GObject.Object { +@@ -436,6 +452,10 @@ export class AuthServices extends GObject.Object { } } @@ -7781,7 +8359,7 @@ index 4394848ba..58905d15a 100644 _handleSelectChoice() {} _handleAnswerQuery() {} -@@ -432,6 +452,8 @@ export class AuthServices extends GObject.Object { +@@ -457,6 +477,8 @@ export class AuthServices extends GObject.Object { _handleClear() {} @@ -7791,11 +8369,11 @@ index 4394848ba..58905d15a 100644 throw new GObject.NotImplementedError( `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 85c55610e..132c5307c 100644 +index 1c1cce1e4..8faa82e0b 100644 --- a/js/gdm/authServicesLegacy.js +++ b/js/gdm/authServicesLegacy.js -@@ -140,6 +140,11 @@ export class AuthServicesLegacy extends AuthServices { - this.emit('mechanisms-changed'); +@@ -143,6 +143,11 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('mechanisms-changed'); } + _handleUpdateEnabledRoles() { @@ -7804,10 +8382,10 @@ index 85c55610e..132c5307c 100644 + } + _handleUpdateEnabledMechanisms() { - if (!this._fingerprintManager?.readerFound) { - this._enabledMechanisms.push(...Mechanisms.filter(m => + this._enabledMechanisms.push(...Mechanisms.filter(m => + this._enabledRoles.includes(m.role) diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 91bdde5c7..59f2765f6 100644 +index e8b2e9620..2a08be1f0 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -5,6 +5,12 @@ import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; @@ -7823,16 +8401,20 @@ index 91bdde5c7..59f2765f6 100644 export class AuthServicesSSSDSwitchable extends AuthServices { static SupportedRoles = [ Constants.PASSWORD_ROLE_NAME, -@@ -20,6 +26,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - - constructor(params) { - super(params); -+ -+ this._mechanismsStatus = MechanismsStatus.WAITING; +@@ -18,6 +24,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + GObject.registerClass(this); } ++ constructor(params) { ++ super(params); ++ ++ this._mechanismsStatus = MechanismsStatus.WAITING; ++ } ++ _handleAnswerQuery(serviceName, answer) { -@@ -51,13 +59,32 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (serviceName !== this._selectedMechanism?.serviceName) + return; +@@ -47,13 +59,32 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } @@ -7866,8 +8448,8 @@ index 91bdde5c7..59f2765f6 100644 } _handleClear() { -@@ -72,6 +99,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { +@@ -71,6 +102,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + let requestObject; + if (this._mechanismsStatus !== MechanismsStatus.WAITING) @@ -7876,18 +8458,17 @@ index 91bdde5c7..59f2765f6 100644 try { requestObject = JSON.parse(json); } catch (e) { -@@ -87,6 +117,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - if (this._mechanisms) - this._updateEnabledMechanisms(); - } -+ -+ this._mechanismsStatus = authSelection +@@ -81,6 +115,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + const {authSelection} = requestObject; + this._mechanisms = authSelection?.mechanisms ?? null; + this._priorityList = authSelection?.priority ?? null; ++ this._mechanismsStatus = this._mechanisms + ? MechanismsStatus.FOUND + : MechanismsStatus.NOT_FOUND; - } - _handleUpdateEnabledMechanisms() { -@@ -111,6 +145,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._updateEnabledMechanisms(); + } +@@ -107,6 +144,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnInfo(serviceName, info) { @@ -7897,7 +8478,7 @@ index 91bdde5c7..59f2765f6 100644 // sssd can't inform about expired password from JSON so it's needed // to check the info message and handle the reset using the old flow if (serviceName === this._selectedMechanism?.serviceName && -@@ -123,6 +160,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -124,6 +164,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _handleOnProblem(serviceName, problem) { @@ -7905,9 +8486,9 @@ index 91bdde5c7..59f2765f6 100644 + return; + if (serviceName === this._selectedMechanism?.serviceName) { - this.emit('queue-priority-message', + this.emit('queue-priority-message', { serviceName, -@@ -131,7 +171,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -133,7 +176,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } } @@ -7923,8 +8504,8 @@ index 91bdde5c7..59f2765f6 100644 + if (serviceName === this._selectedMechanism?.serviceName && this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && - this._resettingPassword) -@@ -151,7 +200,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._resettingPassword) { +@@ -158,7 +210,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { _handleCanStartService(serviceName) { return serviceName === Constants.SWITCHABLE_AUTH_SERVICE_NAME && @@ -7933,7 +8514,7 @@ index 91bdde5c7..59f2765f6 100644 } _formatResponse(answer) { -@@ -182,6 +231,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -189,6 +241,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { serviceName, JSON.stringify(response), this._cancellable).catch(logErrorUnlessCancelled); } @@ -7955,10 +8536,10 @@ index 91bdde5c7..59f2765f6 100644 const {serviceName, prompt} = this._selectedMechanism; diff --git a/js/gdm/util.js b/js/gdm/util.js -index 5952947e7..388afbc30 100644 +index d5bac9c6a..faf835cc9 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js -@@ -209,6 +209,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -211,6 +211,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { this._authServicesSSSDSwitchable?.clear(); this._authServicesLegacy?.clear(); @@ -7970,7 +8551,7 @@ index 5952947e7..388afbc30 100644 this._clearMessageQueue(); this._cancellable?.cancel(); -@@ -435,10 +440,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -466,10 +471,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { reauthOnly: this._reauthOnly, }; if (this._switchableAuthenticationEnabled && @@ -7987,7 +8568,7 @@ index 5952947e7..388afbc30 100644 this._connectAuthServices(); } -@@ -482,9 +491,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -513,9 +522,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { } _onMechanismsChanged() { @@ -8002,8 +8583,8 @@ index 5952947e7..388afbc30 100644 const selectedMechanism = this.selectedMechanism ?? mechanisms.find(m => isSelectable(m)) ?? -@@ -493,6 +505,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { - this.emit('mechanisms-changed', mechanisms, selectedMechanism); +@@ -524,6 +536,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); } + _enableFallbackMechanisms() { @@ -8014,17 +8595,17 @@ index 5952947e7..388afbc30 100644 + this._authServicesSSSDSwitchable.unsupportedRoles); + } + - async _waitPendingMessages(task) { + async _waitPendingMessages({task}) { try { await this._handlePendingMessages(); -- -2.54.0 +2.55.0 -From 7cff7141e54bae1982d0785c244695020b86b1d2 Mon Sep 17 00:00:00 2001 +From fd329502f09901a04c53fb86e3a7ff9e6d7ef791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Wed, 11 Feb 2026 03:36:54 +0100 -Subject: [PATCH 51/54] ui/qrCode: Add a QR Code widget +Subject: [PATCH 52/72] ui/qrCode: Add a QR Code widget The widget can only have a squared size that is picked using the maximum between the provided width ad height properties, using the minimum of @@ -8037,6 +8618,8 @@ while the foreground is colored following the theme. The texture is then draw using the nearest filter so that it will adjust to the actor size without the need to regenerate it on size changes theme + +Part-of: --- data/theme/gnome-shell-sass/_widgets.scss | 1 + .../gnome-shell-sass/widgets/_qr-code.scss | 14 +++ @@ -8240,13 +8823,208 @@ index 000000000..caf0999f3 + } +}; -- -2.54.0 +2.55.0 -From 0335ac55a34c7ba4899449c7489931ac8aca3d7e Mon Sep 17 00:00:00 2001 +From 8451bf466001cdac9616fae76365632c08cd7d56 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Mon, 20 Jul 2026 19:47:32 +0200 +Subject: [PATCH 53/72] authPrompt: Support show button + +Add a new button to mainBox. It will be used in the next commits to allow the +authPrompt to just display one single button to proceed with the next +authentication step. + +Part-of: +--- + .../gnome-shell-sass/widgets/_login-lock.scss | 23 +++++++++++ + js/gdm/authPrompt.js | 38 ++++++++++++++++++- + js/gdm/authServices.js | 6 +++ + js/gdm/util.js | 1 + + po/POTFILES.in | 8 ++++ + 5 files changed, 75 insertions(+), 1 deletion(-) + +diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +index 572ce9f59..318962df8 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +@@ -350,6 +350,29 @@ $_gdm_dialog_width: 25em; + } + } + ++@mixin login-buttons($context){ ++ .login-button { ++ @if $context == 'login' { @include login_dialog_item_button(); } ++ @if $context == 'unlock' { @extend %lockscreen_button; } ++ @include fontsize($base_font_size + 2); ++ color: $_gdm_fg; ++ min-width: 12em; ++ text-align: center; ++ font-weight: bold; ++ border-radius: $base_border_radius * 4; ++ padding: 0; ++ margin: 0.2em $base_margin * 5; ++ } ++} ++ ++.login-dialog { ++ @include login-buttons(login); ++} ++ ++.unlock-dialog { ++ @include login-buttons(unlock); ++} ++ + // Screen Shield + // a.k.a. the lockscreen, uses transparent styles + .unlock-dialog { +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index ce78914d7..75f8d4177 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -87,6 +87,7 @@ export const AuthPrompt = GObject.registerClass({ + 'ask-question', (_, args) => this._onAskQuestion(args), + 'show-message', (_, args) => this._onShowMessage(args), + 'show-choice-list', (_, args) => this._onShowChoiceList(args), ++ 'show-button', (_, args) => this._onShowButton(args), + 'mechanisms-changed', (_, args) => this.emit('mechanisms-changed', args), + 'verification-failed', (_, args) => this._onVerificationFailed(args), + 'verification-complete', () => this._onVerificationComplete(), +@@ -289,6 +290,16 @@ export const AuthPrompt = GObject.registerClass({ + + this.setActorInDefaultButtonWell(this._nextButton); + ++ this._authButton = new St.Button({ ++ style_class: 'login-button', ++ button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, ++ can_focus: true, ++ x_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, ++ y_expand: true, ++ }); ++ this._mainBox.add_child(this._authButton); ++ + // center elements inside _mainBox between the cancel + // button on the left and this spacer on the right + this._mainBox.add_child(new Clutter.Actor({ +@@ -443,7 +454,8 @@ export const AuthPrompt = GObject.registerClass({ + if (message && + type < GdmUtil.MessageType.ERROR && + !this._entryArea.visible && +- !this._authList.visible) { ++ !this._authList.visible && ++ !this._authButton.visible) { + this._fadeInElement(this._entryArea); + this.updateSensitivity({sensitive: true}); + } +@@ -458,6 +470,27 @@ export const AuthPrompt = GObject.registerClass({ + showMessageResolver?.(wigglePromise); + } + ++ _onShowButton({serviceName, label, callback}) { ++ if (this._queryingService) ++ this.clear(); ++ ++ this._queryingService = serviceName; ++ this._promptStep++; ++ ++ this._clearPreemptiveState(); ++ ++ const authButtonClickedId = this._authButton.connect('clicked', () => { ++ this._authButton.disconnect(authButtonClickedId); ++ callback(); ++ }); ++ ++ this._authButton.set_label(label); ++ ++ this._fadeInElement(this._authButton); ++ this.updateSensitivity({sensitive: true}); ++ this.emit('prompted'); ++ } ++ + _onVerificationFailed({serviceName, canRetry}) { + const wasQueryingService = this._queryingService === serviceName; + +@@ -579,6 +612,7 @@ export const AuthPrompt = GObject.registerClass({ + this._authListTitle.child.text = ''; + this._authList.clear(); + this._authList.hide(); ++ this._authButton.hide(); + + this._mainBox.opacity = 255; + this._mainBox.reactive = true; +@@ -593,6 +627,7 @@ export const AuthPrompt = GObject.registerClass({ + this._entry.hint_text = question; + + this._authList.hide(); ++ this._authButton.hide(); + + this._fadeInElement(this._entryArea); + this.updateSensitivity({sensitive: true}); +@@ -677,6 +712,7 @@ export const AuthPrompt = GObject.registerClass({ + + const authWidget = [ + this._authList, ++ this._authButton, + ].find(widget => widget.visible) ?? this._entry; + + if (authWidget === this._entry) +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 28a07558d..fc7861de6 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -65,6 +65,12 @@ export class AuthServices extends GObject.Object { + * @param {object} [choiceList] - keys map to {title: string} + */ + 'show-choice-list': {param_types: [GObject.TYPE_JSOBJECT]}, ++ /** ++ * @param {string} serviceName ++ * @param {string} label ++ * @param {Function} callback ++ */ ++ 'show-button': {param_types: [GObject.TYPE_JSOBJECT]}, + 'mechanisms-changed': {}, + }; + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index faf835cc9..8611cb090 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -505,6 +505,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + 'verification-complete', () => this.emit('verification-complete'), + 'reset', (_, args) => this.emit('reset', args), + 'show-choice-list', (_, args) => this.emit('show-choice-list', args), ++ 'show-button', (_, args) => this.emit('show-button', args), + 'mechanisms-changed', () => this._onMechanismsChanged(), + this); + }); +diff --git a/po/POTFILES.in b/po/POTFILES.in +index f0e1aed6e..fc90886d5 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -98,6 +98,14 @@ src/shell-keyring-prompt.c + src/shell-polkit-authentication-agent.c + src/shell-util.c + src/st/st-icon-theme.c ++subprojects/extensions-app/data/metainfo/org.gnome.Extensions.metainfo.xml.in ++subprojects/extensions-app/data/org.gnome.Extensions.desktop.in.in ++subprojects/extensions-app/js/extensionManager.js ++subprojects/extensions-app/js/extensionRow.js ++subprojects/extensions-app/js/extensionsWindow.js ++subprojects/extensions-app/data/ui/extension-row.ui ++subprojects/extensions-app/data/ui/extensions-window.ui ++subprojects/extensions-app/data/ui/shortcuts-dialog.ui + subprojects/extensions-tool/src/command-create.c + subprojects/extensions-tool/src/command-disable.c + subprojects/extensions-tool/src/command-enable.c +-- +2.55.0 + + +From 93ef9ce43f2f7690f9b5d8a720e36c1fe5f2a695 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 6 Feb 2024 14:18:24 -0500 -Subject: [PATCH 52/54] gdm: Add support for Web Login in +Subject: [PATCH 54/72] gdm: Add support for Web Login in authServicesSSSDSwitchable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 @@ -8261,29 +9039,29 @@ shows an url and a QR code. Use the bindings of the new GnomeQR library to generate the QR code. Co-authored-by: Marco Trevisan (Treviño) +Part-of: --- - .../gnome-shell-sass/widgets/_login-lock.scss | 86 +++++ - js/gdm/authPrompt.js | 106 +++++- - js/gdm/authServices.js | 6 + - js/gdm/authServicesSSSDSwitchable.js | 80 +++++ + .../gnome-shell-sass/widgets/_login-lock.scss | 63 +++- + js/gdm/authPrompt.js | 82 ++++- + js/gdm/authServices.js | 8 + + js/gdm/authServicesSSSDSwitchable.js | 96 ++++++ js/gdm/constants.js | 1 + - js/gdm/loginDialog.js | 2 +- js/gdm/util.js | 5 + - js/gdm/webLogin.js | 308 ++++++++++++++++++ + js/gdm/webLogin.js | 285 ++++++++++++++++++ js/js-resources.gresource.xml | 1 + js/ui/userWidget.js | 8 + po/POTFILES.in | 1 + - 11 files changed, 593 insertions(+), 11 deletions(-) + 10 files changed, 540 insertions(+), 10 deletions(-) create mode 100644 js/gdm/webLogin.js diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -index 078b5c5c2..1ec15e417 100644 +index 318962df8..04f24a789 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss -@@ -15,6 +15,11 @@ $_gdm_dialog_width: 25em; - .login-dialog-prompt-layout { +@@ -16,6 +16,11 @@ $_gdm_dialog_width: 25em; width: $_gdm_dialog_width * 1.2; margin-top: 80px; + spacing: $base_padding * 1.5; + + &.web-login-active { + width: $_gdm_dialog_width * 1.5; @@ -8291,8 +9069,8 @@ index 078b5c5c2..1ec15e417 100644 + } } - .login-dialog-input-well { -@@ -349,8 +354,89 @@ $_gdm_dialog_width: 25em; + .login-dialog-prompt-entry-area { +@@ -350,18 +355,72 @@ $_gdm_dialog_width: 25em; } } @@ -8340,53 +9118,38 @@ index 078b5c5c2..1ec15e417 100644 + spacing: 1.75em; +} + -+.web-login-button-label { -+ @include fontsize($base_font_size + 2); -+ color: $_gdm_fg; -+ min-width: 12em; -+ text-align: center; -+ font-weight: bold; -+} -+ -+@mixin web-login-buttons($context){ -+ .web-login-intro-button, + @mixin login-buttons($context){ +- .login-button { ++ .login-button, + .web-login-prompt-button { -+ @if $context == 'login' { @include login_dialog_item_button(); } -+ @if $context == 'unlock' { @extend %lockscreen_button; } -+ border-radius: $base_border_radius * 4; + @if $context == 'login' { @include login_dialog_item_button(); } + @if $context == 'unlock' { @extend %lockscreen_button; } + @include fontsize($base_font_size + 2); + color: $_gdm_fg; +- min-width: 12em; + text-align: center; + font-weight: bold; + border-radius: $base_border_radius * 4; + } + -+ .web-login-intro-button { -+ padding: 0; -+ margin: 0.2em $base_margin * 5; ++ .login-button { + padding: 0; + margin: 0.2em $base_margin * 5; ++ min-width: 12em; + } + + .web-login-prompt-button { + padding: $base_padding * 2.5 $base_padding * 4; + margin: $base_margin * 6 $base_margin * 2; + width: 8em; -+ } -+} -+ -+.login-dialog { -+ @include web-login-buttons(login); -+} -+ -+.unlock-dialog { -+ @include web-login-buttons(unlock); -+} -+ - // Screen Shield - // a.k.a. the lockscreen, uses transparent styles -+ - .unlock-dialog { - background-color: transparent; + } + } diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index db4c1647a..e511b3881 100644 +index 75f8d4177..afc5e0f00 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js -@@ -14,6 +14,7 @@ import * as GdmUtil from './util.js'; +@@ -15,6 +15,7 @@ import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; import * as Params from '../misc/params.js'; import * as ShellEntry from '../ui/shellEntry.js'; import * as UserWidget from '../ui/userWidget.js'; @@ -8394,56 +9157,48 @@ index db4c1647a..e511b3881 100644 import {wiggle} from '../misc/animationUtils.js'; const DEFAULT_BUTTON_WELL_ICON_SIZE = 16; -@@ -88,6 +89,7 @@ export const AuthPrompt = GObject.registerClass({ - 'show-message', this._onShowMessage.bind(this), - 'show-choice-list', this._onShowChoiceList.bind(this), - 'mechanisms-changed', (_, ...args) => this.emit('mechanisms-changed', ...args), -+ 'web-login', this._onWebLogin.bind(this), - 'verification-failed', this._onVerificationFailed.bind(this), - 'verification-complete', this._onVerificationComplete.bind(this), - 'reset', this._onReset.bind(this), -@@ -291,6 +293,29 @@ export const AuthPrompt = GObject.registerClass({ +@@ -89,6 +90,7 @@ export const AuthPrompt = GObject.registerClass({ + 'show-choice-list', (_, args) => this._onShowChoiceList(args), + 'show-button', (_, args) => this._onShowButton(args), + 'mechanisms-changed', (_, args) => this.emit('mechanisms-changed', args), ++ 'web-login', (_, args) => this._onWebLogin(args), + 'verification-failed', (_, args) => this._onVerificationFailed(args), + 'verification-complete', () => this._onVerificationComplete(), + 'reset', (_, args) => this._onReset(args), +@@ -300,6 +302,18 @@ export const AuthPrompt = GObject.registerClass({ + }); + this._mainBox.add_child(this._authButton); - this.setActorInDefaultButtonWell(this._nextButton); - -+ this._webLoginIntro = new WebLogin.WebLoginIntro(); -+ this._webLoginIntro.set({ -+ x_align: Clutter.ActorAlign.CENTER, -+ x_expand: true, -+ y_expand: true, -+ }); -+ this._webLoginIntro.connect('clicked', () => { -+ this._webLoginIntro.hide(); -+ this._openWebLoginDialog(); -+ }); -+ this._mainBox.add_child(this._webLoginIntro); -+ + this._webLoginDialog = new WebLogin.WebLoginDialog(); + this._webLoginDialog.connect('cancel', () => { + if (this._webLoginDialog.isLoading) { + this.reset({softReset: true}); + } else { + this._closeWebLoginDialog(); -+ this._fadeInElement(this._webLoginIntro); ++ this.updateSensitivity({sensitive: true}); + } + }); + this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); -+ this._inputWell.add_child(this._webLoginDialog); - ++ this.add_child(this._webLoginDialog); ++ // center elements inside _mainBox between the cancel // button on the left and this spacer on the right -@@ -445,10 +470,58 @@ export const AuthPrompt = GObject.registerClass({ - if (message && + this._mainBox.add_child(new Clutter.Actor({ +@@ -455,7 +469,8 @@ export const AuthPrompt = GObject.registerClass({ type < GdmUtil.MessageType.ERROR && !this._entryArea.visible && -- !this._authList.visible) -+ !this._authList.visible && -+ !this._webLoginIntro.visible && -+ !this._webLoginDialog.visible) + !this._authList.visible && +- !this._authButton.visible) { ++ !this._authButton.visible && ++ !this._webLoginDialog.visible) { this._fadeInElement(this._entryArea); + this.updateSensitivity({sensitive: true}); + } +@@ -491,6 +506,47 @@ export const AuthPrompt = GObject.registerClass({ + this.emit('prompted'); } -+ _onWebLogin(_userVerifier, serviceName, introMessage, message, url, code, buttons) { ++ _onWebLogin({serviceName, message, url, code, buttons}) { + if (this._queryingService) + this.clear(); + @@ -8455,15 +9210,9 @@ index db4c1647a..e511b3881 100644 + + this._entryArea.hide(); + -+ if (this._preemptiveAnswer) -+ this._preemptiveAnswer = null; ++ this._clearPreemptiveState(); + -+ if (!this._webLoginDialog.visible && introMessage) { -+ this._webLoginIntro.setMessage(introMessage); -+ this._fadeInElement(this._webLoginIntro); -+ } else { -+ this._openWebLoginDialog(); -+ } ++ this._openWebLoginDialog(); + + this.emit('prompted'); + } @@ -8484,27 +9233,26 @@ index db4c1647a..e511b3881 100644 + + this._webLoginDialog.update(this._webLoginParams); + this._fadeInElement(this._webLoginDialog); ++ this.updateSensitivity({sensitive: true}); + + this.webLoginActive = true; + this.add_style_class_name('web-login-active'); + } + - _onVerificationFailed(userVerifier, serviceName, canRetry) { + _onVerificationFailed({serviceName, canRetry}) { const wasQueryingService = this._queryingService === serviceName; -@@ -474,12 +547,14 @@ export const AuthPrompt = GObject.registerClass({ +@@ -517,11 +573,13 @@ export const AuthPrompt = GObject.registerClass({ this.stopSpinning({animate: true}); this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED; - this._mainBox.reactive = false; -- this._mainBox.can_focus = false; - this._mainBox.ease({ - opacity: 0, - duration: MESSAGE_FADE_OUT_ANIMATION_TIME, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, + [this._mainBox, this._webLoginDialog].forEach(widget => { + widget.reactive = false; -+ widget.can_focus = false; + widget.ease({ + opacity: 0, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, @@ -8513,7 +9261,7 @@ index db4c1647a..e511b3881 100644 }); this.emit('verification-complete'); -@@ -551,6 +626,9 @@ export const AuthPrompt = GObject.registerClass({ +@@ -593,6 +651,9 @@ export const AuthPrompt = GObject.registerClass({ stopSpinning({animate = false} = {}) { this.emit('loading', false); this.setActorInDefaultButtonWell(this._nextButton, animate); @@ -8523,61 +9271,58 @@ index db4c1647a..e511b3881 100644 } clear(params) { -@@ -570,10 +648,14 @@ export const AuthPrompt = GObject.registerClass({ - this._authListTitle.child.text = ''; +@@ -613,9 +674,12 @@ export const AuthPrompt = GObject.registerClass({ this._authList.clear(); this._authList.hide(); -+ this._webLoginIntro.hide(); + this._authButton.hide(); + this._closeWebLoginDialog(); - this._mainBox.opacity = 255; - this._mainBox.reactive = true; -- this._mainBox.can_focus = true; + [this._mainBox, this._webLoginDialog].forEach(widget => { + widget.opacity = 255; + widget.reactive = true; -+ widget.can_focus = true; + }); } setQuestion(question) { -@@ -585,6 +667,8 @@ export const AuthPrompt = GObject.registerClass({ - this._entry.hint_text = question; +@@ -628,6 +692,7 @@ export const AuthPrompt = GObject.registerClass({ this._authList.hide(); -+ this._webLoginIntro.hide(); + this._authButton.hide(); + this._closeWebLoginDialog(); this._fadeInElement(this._entryArea); - } -@@ -678,6 +762,8 @@ export const AuthPrompt = GObject.registerClass({ - updateSensitivity({sensitive}) { + this.updateSensitivity({sensitive: true}); +@@ -713,6 +778,7 @@ export const AuthPrompt = GObject.registerClass({ const authWidget = [ this._authList, -+ this._webLoginIntro, + this._authButton, + this._webLoginDialog, ].find(widget => widget.visible) ?? this._entry; - if (authWidget.reactive === sensitive) + if (authWidget === this._entry) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 58905d15a..12c2f3ebf 100644 +index fc7861de6..b5a8d870f 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -49,6 +49,12 @@ export class AuthServices extends GObject.Object { - param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT], - }, +@@ -72,6 +72,14 @@ export class AuthServices extends GObject.Object { + */ + 'show-button': {param_types: [GObject.TYPE_JSOBJECT]}, 'mechanisms-changed': {}, -+ 'web-login': { -+ param_types: [ -+ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, -+ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_JSOBJECT, -+ ], -+ }, ++ /** ++ * @param {string} serviceName ++ * @param {string} message ++ * @param {string} url ++ * @param {string} code ++ * @param {object[]} buttons ++ */ ++ 'web-login': {param_types: [GObject.TYPE_JSOBJECT]}, }; static { diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 59f2765f6..bddcd1396 100644 +index 2a08be1f0..a321f4489 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -1,3 +1,4 @@ @@ -8616,8 +9361,8 @@ index 59f2765f6..bddcd1396 100644 + this._clearWebLoginTimeout(); } - _handleOnCustomJSONRequest(_serviceName, _protocol, _version, json) { -@@ -133,6 +141,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + _handleOnCustomJSONRequest(serviceName, _protocol, _version, json) { +@@ -132,6 +140,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { // filter out mechanisms with roles that are not enabled .filter(m => this._enabledRoles.includes(m.role))); @@ -8626,7 +9371,7 @@ index 59f2765f6..bddcd1396 100644 const selectedMechanism = this._enabledMechanisms .find(m => this._savedMechanism?.role === m.role) ?? -@@ -144,6 +154,29 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -143,6 +153,29 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._savedMechanism = null; } @@ -8656,7 +9401,7 @@ index 59f2765f6..bddcd1396 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -212,6 +245,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -222,6 +255,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } @@ -8667,9 +9412,9 @@ index 59f2765f6..bddcd1396 100644 default: throw new GObject.NotImplementedError(`formatResponse: ${role}`); } -@@ -250,4 +287,47 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -260,4 +297,63 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - this.emit('ask-question', serviceName, prompt, true); + this.emit('ask-question', {serviceName, question: prompt, secret: true}); } + + _startWebLogin() { @@ -8694,7 +9439,23 @@ index 59f2765f6..bddcd1396 100644 + action: () => this._webLoginDone(), + }]; + -+ this.emit('web-login', serviceName, initPrompt, linkPrompt, uri, code, buttons); ++ const showWebLogin = () => this.emit('web-login', { ++ serviceName, ++ message: linkPrompt, ++ url: uri, ++ code, ++ buttons, ++ }); ++ ++ if (initPrompt) { ++ this.emit('show-button', { ++ serviceName, ++ label: initPrompt, ++ callback: showWebLogin, ++ }); ++ } else { ++ showWebLogin(); ++ } + } + + _webLoginDone() { @@ -8716,10 +9477,10 @@ index 59f2765f6..bddcd1396 100644 + } } diff --git a/js/gdm/constants.js b/js/gdm/constants.js -index 2f37446c8..e8ca48625 100644 +index 2b6061578..c3b790325 100644 --- a/js/gdm/constants.js +++ b/js/gdm/constants.js -@@ -3,6 +3,7 @@ +@@ -1,6 +1,7 @@ export const PASSWORD_ROLE_NAME = 'password'; export const SMARTCARD_ROLE_NAME = 'smartcard'; export const FINGERPRINT_ROLE_NAME = 'fingerprint'; @@ -8727,21 +9488,8 @@ index 2f37446c8..e8ca48625 100644 export const PASSWORD_SERVICE_NAME = 'gdm-password'; export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; -diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js -index 69573adfe..09ec340af 100644 ---- a/js/gdm/loginDialog.js -+++ b/js/gdm/loginDialog.js -@@ -45,7 +45,7 @@ import * as A11y from '../ui/status/accessibility.js'; - - const _FADE_ANIMATION_TIME = 250; - const _SCROLL_ANIMATION_TIME = 500; --const _FIXED_TOP_ACTOR_HEIGHT = 400; -+const _FIXED_TOP_ACTOR_HEIGHT = 550; - const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0; - const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60; - const _PRIMARY_LOGIN_METHOD_SECTION_NAME = _('Login Options'); diff --git a/js/gdm/util.js b/js/gdm/util.js -index 388afbc30..b36844fbb 100644 +index 8611cb090..815af4482 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -18,6 +18,7 @@ export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; @@ -8760,7 +9508,7 @@ index 388afbc30..b36844fbb 100644 return true; case Constants.FINGERPRINT_ROLE_NAME: return false; -@@ -416,6 +418,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -447,6 +449,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); @@ -8769,24 +9517,21 @@ index 388afbc30..b36844fbb 100644 const switchableAuthentication = this._settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); -@@ -475,6 +479,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { - 'reset', (_, ...args) => this.emit('reset', ...args), - 'show-choice-list', (_, ...args) => this.emit('show-choice-list', ...args), - 'mechanisms-changed', (_, ...args) => this._onMechanismsChanged(...args), -+ 'web-login', (_, ...args) => this.emit('web-login', ...args), +@@ -507,6 +511,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + 'show-choice-list', (_, args) => this.emit('show-choice-list', args), + 'show-button', (_, args) => this.emit('show-button', args), + 'mechanisms-changed', () => this._onMechanismsChanged(), ++ 'web-login', (_, args) => this.emit('web-login', args), this); }); } diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js new file mode 100644 -index 000000000..8f2a0d876 +index 000000000..3b6496296 --- /dev/null +++ b/js/gdm/webLogin.js -@@ -0,0 +1,308 @@ -+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -+// +@@ -0,0 +1,285 @@ +// A widget showing a URL for web login -+/* exported WebLoginPrompt */ + +import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; @@ -8800,8 +9545,11 @@ index 000000000..8f2a0d876 +const WEB_LOGIN_SPINNER_SIZE = 35; +const URL_LABEL_LONG_THRESHOLD = 45; + -+export const WebLoginPrompt = GObject.registerClass( -+class WebLoginPrompt extends St.BoxLayout { ++export class WebLoginPrompt extends St.BoxLayout { ++ static { ++ GObject.registerClass(this); ++ } ++ + constructor(params) { + const {qrSize: qrCodeSize, message, url, code} = Params.parse(params, { + qrSize: QR_CODE_SIZE, @@ -8907,14 +9655,18 @@ index 000000000..8f2a0d876 + + return url; + } -+}); ++}; + -+export const WebLoginDialog = GObject.registerClass({ -+ Signals: { ++export class WebLoginDialog extends St.Widget { ++ static [GObject.signals] = { + 'cancel': {}, + 'loading': {}, -+ }, -+}, class WebLoginDialog extends St.Widget { ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ + constructor(params) { + const {message, url, code, buttons} = Params.parse(params, { + message: null, @@ -8981,9 +9733,10 @@ index 000000000..8f2a0d876 + style_class: 'web-login-prompt-button', + can_focus: true, + accessible_name: b.label, ++ button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, + child: new St.Label({ + text: b.label, -+ style_class: 'web-login-button-label', ++ style_class: 'login-button-label', + }), + }); + @@ -9062,35 +9815,7 @@ index 000000000..8f2a0d876 + this.isLoading = false; + this.emit('loading'); + } -+}); -+ -+export var WebLoginIntro = GObject.registerClass( -+class WebLoginIntro extends St.Button { -+ constructor(params) { -+ const {message} = Params.parse(params, { -+ message: null, -+ }); -+ -+ const label = new St.Label({ -+ text: message, -+ style_class: 'web-login-button-label', -+ }); -+ -+ super({ -+ style_class: 'web-login-intro-button', -+ accessible_name: message, -+ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, -+ reactive: true, -+ can_focus: true, -+ child: label, -+ }); -+ } -+ -+ setMessage(message) { -+ this.child.text = message; -+ this.accessible_name = message; -+ } -+}); ++} diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 1da999963..ced2f28e2 100644 --- a/js/js-resources.gresource.xml @@ -9121,10 +9846,10 @@ index 0331a1d32..a497a3bf0 100644 + } }); diff --git a/po/POTFILES.in b/po/POTFILES.in -index d1e4d6487..9a7f8db8e 100644 +index fc90886d5..08c23cbff 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in -@@ -15,6 +15,7 @@ js/gdm/authServicesLegacy.js +@@ -16,6 +16,7 @@ js/gdm/authServicesLegacy.js js/gdm/authServicesSSSDSwitchable.js js/gdm/loginDialog.js js/gdm/util.js @@ -9133,13 +9858,13 @@ index d1e4d6487..9a7f8db8e 100644 js/misc/brightnessManager.js js/misc/systemActions.js -- -2.54.0 +2.55.0 -From 7e940666578fb759a237c568cde4d876e20c9e0c Mon Sep 17 00:00:00 2001 +From 87023c539945e0ef62befe9976c3b460e046e4c6 Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Tue, 14 Jan 2025 07:31:59 -0500 -Subject: [PATCH 53/54] gdm: Add support for Smartcard in +Subject: [PATCH 55/72] gdm: Add support for Smartcard in authServicesSSSDSwitchable This allows selecting smartcard as a login method. @@ -9151,12 +9876,14 @@ is inserted. When a smartcard is inserted, authServicesSSSDSwitchable will be restarted to check if a cert is available for the current user. This won't change the authentication state (if it was doing password auth, that won't change). + +Part-of: --- - js/gdm/authServicesSSSDSwitchable.js | 70 ++++++++++++++++++++++++++++ - 1 file changed, 70 insertions(+) + js/gdm/authServicesSSSDSwitchable.js | 78 ++++++++++++++++++++++++++++ + 1 file changed, 78 insertions(+) diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index bddcd1396..72335e337 100644 +index a321f4489..668637db0 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -15,11 +15,13 @@ const MechanismsStatus = { @@ -9173,7 +9900,7 @@ index bddcd1396..72335e337 100644 [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, }; -@@ -33,6 +35,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -33,6 +35,22 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._mechanismsStatus = MechanismsStatus.WAITING; } @@ -9185,14 +9912,18 @@ index bddcd1396..72335e337 100644 + const certificates = this._selectedMechanism.certificates; + const cert = certificates.find(c => c.keyId === key); + this._selectedSmartcard = cert; -+ this.emit('ask-question', serviceName, cert.pinPrompt, true); ++ this.emit('ask-question', { ++ serviceName, ++ question: cert.pinPrompt, ++ secret: true, ++ }); + } + } + _handleAnswerQuery(serviceName, answer) { if (serviceName !== this._selectedMechanism?.serviceName) return; -@@ -48,6 +62,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -48,6 +66,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { let response; switch (this._selectedMechanism.role) { case Constants.PASSWORD_ROLE_NAME: @@ -9200,7 +9931,7 @@ index bddcd1396..72335e337 100644 response = this._formatResponse(answer); this._sendResponse(response); break; -@@ -59,6 +74,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -59,6 +78,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { case Constants.PASSWORD_ROLE_NAME: this._startPasswordLogin(); break; @@ -9210,7 +9941,7 @@ index bddcd1396..72335e337 100644 case Constants.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; -@@ -98,6 +116,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -98,6 +120,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this._priorityList = null; this._enabledMechanisms = null; this._selectedMechanism = null; @@ -9218,7 +9949,7 @@ index bddcd1396..72335e337 100644 this._resettingPassword = false; -@@ -177,6 +196,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -176,6 +199,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { }); } @@ -9233,7 +9964,7 @@ index bddcd1396..72335e337 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -245,6 +272,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -255,6 +286,11 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {password: answer}; break; } @@ -9245,8 +9976,8 @@ index bddcd1396..72335e337 100644 case Constants.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -288,6 +320,44 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - this.emit('ask-question', serviceName, prompt, true); +@@ -298,6 +334,48 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('ask-question', {serviceName, question: prompt, secret: true}); } + _startSmartcardLogin() { @@ -9254,7 +9985,11 @@ index bddcd1396..72335e337 100644 + + if (certificates.length === 1) { + this._selectedSmartcard = certificates[0]; -+ this.emit('ask-question', serviceName, certificates[0].pinPrompt, true); ++ this.emit('ask-question', { ++ serviceName, ++ question: certificates[0].pinPrompt, ++ secret: true, ++ }); + return; + } + @@ -9262,11 +9997,11 @@ index bddcd1396..72335e337 100644 + for (const cert of certificates) + choiceList[cert.keyId] = this._parseCertInstruction(cert.certInstruction); + -+ const prompt = certificates.length === 0 ++ const promptMessage = certificates.length === 0 + ? _('Insert Smartcard') + : _('Select Identity'); + -+ this.emit('show-choice-list', serviceName, prompt, choiceList); ++ this.emit('show-choice-list', {serviceName, promptMessage, choiceList}); + } + + _parseCertInstruction(certInstruction) { @@ -9291,13 +10026,13 @@ index bddcd1396..72335e337 100644 const { serviceName, -- -2.54.0 +2.55.0 -From 8690be303d65d0fa60fc97bcb9d98976aa19a864 Mon Sep 17 00:00:00 2001 +From d870c1ef3359a6db913c76f050b86eadbca2419f Mon Sep 17 00:00:00 2001 From: Joan Torres Lopez Date: Mon, 15 Sep 2025 16:36:42 +0200 -Subject: [PATCH 54/54] gdm: Add support for Passkey in +Subject: [PATCH 56/72] gdm: Add support for Passkey in authServicesSSSDSwitchable This allows selecting passkey authentication mechanism. @@ -9308,19 +10043,21 @@ about the passkey. It's used 'show-choice-list' with an emtpy list, to display the touch instruction. + +Part-of: --- - js/gdm/authServices.js | 14 +++++++++ - js/gdm/authServicesSSSDSwitchable.js | 46 +++++++++++++++++++++++++++- + js/gdm/authServices.js | 14 ++++++++ + js/gdm/authServicesSSSDSwitchable.js | 52 +++++++++++++++++++++++++++- js/gdm/constants.js | 1 + js/gdm/util.js | 4 +++ js/ui/unlockDialog.js | 2 ++ - 5 files changed, 66 insertions(+), 1 deletion(-) + 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 12c2f3ebf..2726d960a 100644 +index b5a8d870f..6dd9bf221 100644 --- a/js/gdm/authServices.js +++ b/js/gdm/authServices.js -@@ -4,6 +4,7 @@ import * as Constants from './constants.js'; +@@ -2,6 +2,7 @@ import * as Constants from './constants.js'; import * as FingerprintManager from './fingerprintManager.js'; import * as Params from '../misc/params.js'; import {registerDestroyableType} from '../misc/signalTracker.js'; @@ -9328,7 +10065,7 @@ index 12c2f3ebf..2726d960a 100644 import * as SmartcardManager from './smartcardManager.js'; import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; import * as Util from './util.js'; -@@ -92,6 +93,9 @@ export class AuthServices extends GObject.Object { +@@ -117,6 +118,9 @@ export class AuthServices extends GObject.Object { if (this.supportedRoles.includes(Constants.SMARTCARD_ROLE_NAME) && this._enabledRoles.includes(Constants.SMARTCARD_ROLE_NAME)) this._connectSmartcardManager(); @@ -9338,7 +10075,7 @@ index 12c2f3ebf..2726d960a 100644 if (this.supportedRoles.includes(Constants.FINGERPRINT_ROLE_NAME) && this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) this._connectFingerprintManager(); -@@ -235,6 +239,14 @@ export class AuthServices extends GObject.Object { +@@ -264,6 +268,14 @@ export class AuthServices extends GObject.Object { this); } @@ -9353,7 +10090,7 @@ index 12c2f3ebf..2726d960a 100644 _connectFingerprintManager() { // Fingerprint can only work on lockscreen if (!this._reauthOnly) -@@ -467,6 +479,8 @@ export class AuthServices extends GObject.Object { +@@ -500,6 +512,8 @@ export class AuthServices extends GObject.Object { _handleSmartcardChanged() {} @@ -9363,7 +10100,7 @@ index 12c2f3ebf..2726d960a 100644 _handleOnInfo() {} diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 72335e337..4c8eb1ae1 100644 +index 668637db0..c37ca3890 100644 --- a/js/gdm/authServicesSSSDSwitchable.js +++ b/js/gdm/authServicesSSSDSwitchable.js @@ -16,12 +16,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { @@ -9381,7 +10118,7 @@ index 72335e337..4c8eb1ae1 100644 [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, }; -@@ -66,6 +68,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -70,6 +72,15 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = this._formatResponse(answer); this._sendResponse(response); break; @@ -9389,13 +10126,15 @@ index 72335e337..4c8eb1ae1 100644 + response = this._formatResponse(answer); + this._sendResponse(response); + -+ this.emit('show-choice-list', serviceName, -+ this._selectedMechanism.touchInstruction, {}); ++ this.emit('show-choice-list', { ++ serviceName, ++ promptMessage: this._selectedMechanism.touchInstruction, ++ }); + break; } } -@@ -77,6 +86,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -81,6 +92,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { case Constants.SMARTCARD_ROLE_NAME: this._startSmartcardLogin(); break; @@ -9405,7 +10144,7 @@ index 72335e337..4c8eb1ae1 100644 case Constants.WEB_LOGIN_ROLE_NAME: this._startWebLogin(); break; -@@ -204,6 +216,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -207,6 +221,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { this.emit('reset', {softReset: true, reuseEntryText: true}); } @@ -9420,7 +10159,7 @@ index 72335e337..4c8eb1ae1 100644 _handleOnInfo(serviceName, info) { if (!this._eventExpected()) return; -@@ -264,7 +284,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -278,7 +300,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { } _formatResponse(answer) { @@ -9429,7 +10168,7 @@ index 72335e337..4c8eb1ae1 100644 let response; switch (role) { -@@ -277,6 +297,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -291,6 +313,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { response = {pin: answer, tokenName, moduleName, keyId, label}; break; } @@ -9440,7 +10179,7 @@ index 72335e337..4c8eb1ae1 100644 case Constants.WEB_LOGIN_ROLE_NAME: { response = {}; break; -@@ -358,6 +382,26 @@ export class AuthServicesSSSDSwitchable extends AuthServices { +@@ -376,6 +402,30 @@ export class AuthServicesSSSDSwitchable extends AuthServices { }; } @@ -9452,15 +10191,19 @@ index 72335e337..4c8eb1ae1 100644 + } = this._selectedMechanism; + + if (!keyConnected) { -+ this.emit('show-choice-list', serviceName, initInstruction, {}); ++ this.emit('show-choice-list', {serviceName, promptMessage: initInstruction}); + return; + } + -+ this.emit('ask-question', serviceName, pinPrompt, true); ++ this.emit('ask-question', {serviceName, question: pinPrompt, secret: true}); + + if (pinAttempts <= 3 && pinAttempts > 0) { + const message = _('You have %d attempts left. If the passkey gets locked, you may not able to access your account.').format(pinAttempts); -+ this.emit('queue-message', serviceName, message, Util.MessageType.INFO); ++ this.emit('queue-message', { ++ serviceName, ++ message, ++ messageType: Util.MessageType.INFO, ++ }); + } + } + @@ -9468,10 +10211,10 @@ index 72335e337..4c8eb1ae1 100644 const { serviceName, diff --git a/js/gdm/constants.js b/js/gdm/constants.js -index e8ca48625..7c48599e6 100644 +index c3b790325..a829d9866 100644 --- a/js/gdm/constants.js +++ b/js/gdm/constants.js -@@ -3,6 +3,7 @@ +@@ -1,6 +1,7 @@ export const PASSWORD_ROLE_NAME = 'password'; export const SMARTCARD_ROLE_NAME = 'smartcard'; export const FINGERPRINT_ROLE_NAME = 'fingerprint'; @@ -9480,7 +10223,7 @@ index e8ca48625..7c48599e6 100644 export const PASSWORD_SERVICE_NAME = 'gdm-password'; diff --git a/js/gdm/util.js b/js/gdm/util.js -index b36844fbb..a6339ec47 100644 +index 815af4482..605b0950f 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -17,6 +17,7 @@ export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; @@ -9499,7 +10242,7 @@ index b36844fbb..a6339ec47 100644 case Constants.WEB_LOGIN_ROLE_NAME: return true; case Constants.FINGERPRINT_ROLE_NAME: -@@ -416,6 +418,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { +@@ -447,6 +449,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { enabledRoles.push(Constants.PASSWORD_ROLE_NAME); if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); @@ -9509,7 +10252,7 @@ index b36844fbb..a6339ec47 100644 enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); if (this._settings.get_boolean(WEB_AUTHENTICATION_KEY)) diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js -index 50865198a..f30bf9bdf 100644 +index 84cd0fb08..d8d825da6 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -433,6 +433,8 @@ class UnlockDialogClock extends St.BoxLayout { @@ -9522,5 +10265,5022 @@ index 50865198a..f30bf9bdf 100644 text = _('Swipe up'); else -- -2.54.0 +2.55.0 + + +From b2292cc9bc6c61d40626edac89d18b9f974dd4b2 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Wed, 8 Jul 2026 20:01:04 +0200 +Subject: [PATCH 57/72] gdm/util: Generalize authServices orchestration + +Replace hardcoded _authServicesSSSDSwitchable and _authServicesLegacy +fields with a priority-ordered AuthServicesClasses registry. Adding a +new authServices only requires creating a subclass and registering it +in AuthServicesClasses. + +Subclasses declare static isEnabled(settings) to gate on GSettings +keys without ShellUserVerifier knowing about them. _updateAuthServices +tracks both enabled roles and enabled classes to detect changes. + +_enableFallbackMechanisms is renamed to _redistributeRoles. It serves +the same purpose: when an authServices cannot provide a role, unsupported +roles cascade to lower-priority authServices. A _redistributingRoles guard +flag prevents re-entrant mechanisms-changed emissions during the +cascade, so only one aggregated event is emitted. + +Mechanism aggregation deduplicates by role, keeping only the +highest-priority mechanism per role. + +Part-of: +--- + js/gdm/authServices.js | 10 +- + js/gdm/authServicesSSSDSwitchable.js | 21 ++-- + js/gdm/util.js | 148 ++++++++++++++------------- + 3 files changed, 98 insertions(+), 81 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 6dd9bf221..137b07fb4 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -95,6 +95,10 @@ export class AuthServices extends GObject.Object { + return roles.some(r => this.SupportedRoles.includes(r)); + } + ++ static isEnabled(_settings) { ++ return true; ++ } ++ + constructor(params) { + super(); + params = Params.parse(params, { +@@ -479,7 +483,7 @@ export class AuthServices extends GObject.Object { + } + + _handleGetUnsupportedRoles() { +- return []; ++ return this._enabledRoles.filter(r => !this.supportedRoles.includes(r)); + } + + _handleSelectChoice() {} +@@ -546,4 +550,8 @@ export class AuthServices extends GObject.Object { + throw new GObject.NotImplementedError( + `_handleCanStartService in ${this.constructor.name}`); + } ++ ++ addCredentialManager(_serviceName, _credentialManager) {} ++ ++ removeCredentialManager(_serviceName) {} + } +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index c37ca3890..ee018e7b9 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -6,6 +6,8 @@ import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Util from './util.js'; + import {AuthServices} from './authServices.js'; + ++const SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; ++ + const MechanismsStatus = { + WAITING: 0, + NOT_FOUND: 1, +@@ -31,6 +33,10 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + GObject.registerClass(this); + } + ++ static isEnabled(settings) { ++ return settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); ++ } ++ + constructor(params) { + super(params); + +@@ -102,19 +108,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _handleGetUnsupportedRoles() { +- // While waiting for mechanisms info (WAITING) or when mechanisms are +- // found (FOUND), use supportedRoles to get unsupported ones. + // When we couldn't get mechanisms (NOT_FOUND), assume all roles +- // are unsupported. +- switch (this._mechanismsStatus) { +- case MechanismsStatus.WAITING: +- case MechanismsStatus.FOUND: +- return this._enabledRoles.filter(r => !this.supportedRoles.includes(r)); +- case MechanismsStatus.NOT_FOUND: ++ // are unsupported so they cascade to lower-priority authServices. ++ if (this._mechanismsStatus === MechanismsStatus.NOT_FOUND) + return this._enabledRoles; +- default: +- throw new GObject.NotImplementedError(`invalid MechanismStatus: ${this._mechanismsStatus}`); +- } ++ ++ return super._handleGetUnsupportedRoles(); + } + + _handleReset() { +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 605b0950f..f7abecbfe 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -18,7 +18,6 @@ export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; + export const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; + export const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; + export const PASSKEY_AUTHENTICATION_KEY = 'enable-passkey-authentication'; +-export const SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; + export const WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; + export const BANNER_MESSAGE_KEY = 'banner-message-enable'; + export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; +@@ -50,6 +49,14 @@ export const MessageType = { + ERROR: 3, + }; + ++// Priority-ordered: earlier entries take precedence for shared roles. ++// Each authServices claims the roles it supports; unsupported roles ++// cascade to the next authServices in the array. ++const AuthServicesClasses = [ ++ AuthServicesSSSDSwitchable, ++ AuthServicesLegacy, ++]; ++ + /** + * Error thrown during the authentication initialization phase. + * +@@ -141,6 +148,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + this._client = client; + this._cancellable = null; ++ this._authServices = []; + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; +@@ -169,8 +177,10 @@ export class ShellUserVerifier extends Signals.EventEmitter { + try { + const proxies = await this._getUserVerifierProxies(userName, this._cancellable); + this._setUserVerifier(proxies.userVerifier); +- await this._authServicesSSSDSwitchable?.beginVerification(userName, proxies); +- await this._authServicesLegacy?.beginVerification(userName, proxies); ++ for (const s of this._authServices) { ++ // eslint-disable-next-line no-await-in-loop ++ await s.beginVerification(userName, proxies); ++ } + } catch (e) { + if (e instanceof InitError) + this._reportInitError(e); +@@ -182,20 +192,18 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + selectMechanism(mechanism) { +- let selected = false; +- selected |= this._authServicesSSSDSwitchable?.selectMechanism(mechanism); +- selected |= this._authServicesLegacy?.selectMechanism(mechanism); +- return selected; ++ // Every authServices needs to update its selected mechanism ++ return this._authServices ++ .map(s => s.selectMechanism(mechanism)) ++ .some(Boolean); + } + + needsUsername() { +- return this._authServicesSSSDSwitchable?.needsUsername() || +- this._authServicesLegacy?.needsUsername(); ++ return this._authServices.some(s => s.needsUsername()); + } + + reset() { +- this._authServicesSSSDSwitchable?.reset(); +- this._authServicesLegacy?.reset(); ++ this._authServices.forEach(s => s.reset()); + + this._userVerifier?.call_cancel_sync(null); + +@@ -203,8 +211,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + cancel() { +- this._authServicesSSSDSwitchable?.cancel(); +- this._authServicesLegacy?.cancel(); ++ this._authServices.forEach(s => s.cancel()); + + this._userVerifier?.call_cancel_sync(null); + +@@ -212,13 +219,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + clear() { +- this._authServicesSSSDSwitchable?.clear(); +- this._authServicesLegacy?.clear(); +- +- if (this._authServicesSSSDSwitchable) { +- this._authServicesLegacy?.updateEnabledRoles( +- this._authServicesSSSDSwitchable.unsupportedRoles); +- } ++ this._authServices.forEach(s => s.clear()); ++ this._redistributeRoles(); + + this._clearMessageQueue(); + +@@ -242,11 +244,8 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + destroy() { +- this._authServicesSSSDSwitchable?.destroy(); +- this._authServicesSSSDSwitchable = null; +- +- this._authServicesLegacy?.destroy(); +- this._authServicesLegacy = null; ++ this._authServices.forEach(s => s.destroy()); ++ this._authServices = []; + + this.cancel(); + +@@ -255,8 +254,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + selectChoice(serviceName, key) { +- this._authServicesSSSDSwitchable?.selectChoice(serviceName, key); +- this._authServicesLegacy?.selectChoice(serviceName, key); ++ this._authServices.forEach(s => s.selectChoice(serviceName, key)); + } + + async answerQuery(serviceName, answer) { +@@ -264,16 +262,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { + // ensure no messages get lost + await this._handlePendingMessages().catch(logErrorUnlessCancelled); + +- this._authServicesSSSDSwitchable?.answerQuery(serviceName, answer); +- this._authServicesLegacy?.answerQuery(serviceName, answer); ++ this._authServices.forEach(s => s.answerQuery(serviceName, answer)); + } + + addCredentialManager(serviceName, credentialManager) { +- this._authServicesLegacy?.addCredentialManager(serviceName, credentialManager); ++ this._authServices.forEach(s => s.addCredentialManager(serviceName, credentialManager)); + } + + removeCredentialManager(serviceName) { +- this._authServicesLegacy?.removeCredentialManager(serviceName); ++ this._authServices.forEach(s => s.removeCredentialManager(serviceName)); + } + + _getIntervalForMessage(message) { +@@ -456,15 +453,16 @@ export class ShellUserVerifier extends Signals.EventEmitter { + if (this._settings.get_boolean(WEB_AUTHENTICATION_KEY)) + enabledRoles.push(Constants.WEB_LOGIN_ROLE_NAME); + +- const switchableAuthentication = +- this._settings.get_boolean(SWITCHABLE_AUTHENTICATION_KEY); ++ const enabledAuthServicesClasses = AuthServicesClasses ++ .filter(C => C.isEnabled(this._settings)); + + if (JSON.stringify(enabledRoles) === JSON.stringify(this._enabledRoles) && +- switchableAuthentication === this._switchableAuthenticationEnabled) ++ enabledAuthServicesClasses.length === this._enabledAuthServicesClasses?.length && ++ enabledAuthServicesClasses.every(c => this._enabledAuthServicesClasses.includes(c))) + return; + + this._enabledRoles = enabledRoles; +- this._switchableAuthenticationEnabled = switchableAuthentication; ++ this._enabledAuthServicesClasses = enabledAuthServicesClasses; + + this._createAuthServices(); + } +@@ -474,36 +472,34 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + const params = { + client: this._client, +- enabledRoles: this._enabledRoles, + allowedFailures: this.allowedFailures, + reauthOnly: this._reauthOnly, + }; +- if (this._switchableAuthenticationEnabled && +- AuthServicesSSSDSwitchable.supportsAny(this._enabledRoles)) { +- this._authServicesSSSDSwitchable = new AuthServicesSSSDSwitchable(params); +- +- params.enabledRoles = this._authServicesSSSDSwitchable.unsupportedRoles; +- this._authServicesLegacy = new AuthServicesLegacy(params); +- } else if (AuthServicesLegacy.supportsAny(this._enabledRoles)) { +- this._authServicesLegacy = new AuthServicesLegacy(params); +- } ++ ++ this._enabledAuthServicesClasses ++ .filter(AuthServicesClass => ++ AuthServicesClass.supportsAny(this._enabledRoles)) ++ .forEach(AuthServicesClass => { ++ const enabledRoles = ++ this._authServices.at(-1)?.unsupportedRoles ?? ++ this._enabledRoles; ++ this._authServices.push(new AuthServicesClass({ ++ ...params, ++ enabledRoles, ++ })); ++ }); + + this._connectAuthServices(); + } + + _clearAuthServices() { +- this._authServicesSSSDSwitchable?.destroy(); +- this._authServicesSSSDSwitchable = null; +- this._authServicesLegacy?.destroy(); +- this._authServicesLegacy = null; ++ this._authServices.forEach(s => s.destroy()); ++ this._authServices = []; + } + + _connectAuthServices() { +- [ +- this._authServicesSSSDSwitchable, +- this._authServicesLegacy, +- ].forEach(authServices => { +- authServices?.connectObject( ++ this._authServices.forEach(authServices => { ++ authServices.connectObject( + 'ask-question', (_, args) => this.emit('ask-question', args), + 'queue-message', (_, args) => this._queueMessage(args), + 'queue-priority-message', (_, args) => this._queuePriorityMessage(args), +@@ -526,18 +522,40 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + get selectedMechanism() { +- return this._authServicesSSSDSwitchable?.selectedMechanism ?? +- this._authServicesLegacy?.selectedMechanism ?? +- null; ++ return this._authServices ++ .find(s => s.selectedMechanism)?.selectedMechanism ?? null; ++ } ++ ++ _redistributeRoles() { ++ if (this._authServices.length < 2) ++ return; ++ ++ this._redistributingRoles = true; ++ ++ // Each authServices receives the roles unsupported by the one ++ // before it, cascading down the priority chain ++ const authServices = this._authServices; ++ for (let i = 1; i < authServices.length; i++) { ++ const prev = authServices[i - 1]; ++ const current = authServices[i]; ++ current.updateEnabledRoles(prev.unsupportedRoles); ++ } ++ ++ this._redistributingRoles = false; + } + + _onMechanismsChanged() { +- if (this._enableFallbackMechanisms()) ++ if (this._redistributingRoles) + return; + +- const mechanismsSSSDSwitchable = this._authServicesSSSDSwitchable?.enabledMechanisms ?? []; +- const mechanismsLegacy = this._authServicesLegacy?.enabledMechanisms ?? []; +- const mechanisms = [...mechanismsSSSDSwitchable, ...mechanismsLegacy]; ++ this._redistributeRoles(); ++ ++ // Collect mechanisms from all authServices in priority order, ++ // keeping only the first mechanism per role ++ const seenRoles = new Set(); ++ const mechanisms = this._authServices ++ .flatMap(s => s.enabledMechanisms ?? []) ++ .filter(m => !seenRoles.has(m.role) && seenRoles.add(m.role)); + + const selectedMechanism = this.selectedMechanism ?? + mechanisms.find(m => isSelectable(m)) ?? +@@ -546,14 +564,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); + } + +- _enableFallbackMechanisms() { +- if (!this._authServicesSSSDSwitchable || !this._authServicesLegacy) +- return false; +- +- return this._authServicesLegacy.updateEnabledRoles( +- this._authServicesSSSDSwitchable.unsupportedRoles); +- } +- + async _waitPendingMessages({task}) { + try { + await this._handlePendingMessages(); +-- +2.55.0 + + +From 3aae91592504fba330f5d63ec3f1739eac1138b4 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Sun, 19 Jul 2026 13:31:48 +0200 +Subject: [PATCH 58/72] gdm: Fix auth roles enablement by settings + +Don't use passkey and web login settings for role filtering. Settings +are only meant for enabling or disabling services not individual roles. + +Make authServices creation and role updates more agnostic: each +authServices starts with all its supported roles, then +_redistributeRoles cascades disabling roles already claimed by +higher-priority authServices. + +passkey and web login settings aren't used anymore. + +Part-of: +--- + js/gdm/authServices.js | 43 +++++++++++---------------- + js/gdm/authServicesLegacy.js | 16 +++++++++- + js/gdm/authServicesSSSDSwitchable.js | 10 +++---- + js/gdm/util.js | 44 +++++----------------------- + 4 files changed, 45 insertions(+), 68 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 137b07fb4..0cbe028d3 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -91,10 +91,6 @@ export class AuthServices extends GObject.Object { + static SupportedRoles = []; + static RoleToService = {}; + +- static supportsAny(roles) { +- return roles.some(r => this.SupportedRoles.includes(r)); +- } +- + static isEnabled(_settings) { + return true; + } +@@ -103,15 +99,16 @@ export class AuthServices extends GObject.Object { + super(); + params = Params.parse(params, { + client: null, +- enabledRoles: [], + allowedFailures: 3, + reauthOnly: false, ++ settings: null, + }); + + this._client = params.client; +- this._enabledRoles = params.enabledRoles; ++ this._enabledRoles = this.supportedRoles; + this._allowedFailures = params.allowedFailures; + this._reauthOnly = params.reauthOnly; ++ this._settings = params.settings; + + this._failCounter = 0; + this._activeServices = new Set(); +@@ -119,14 +116,11 @@ export class AuthServices extends GObject.Object { + + this._cancellable = null; + +- if (this.supportedRoles.includes(Constants.SMARTCARD_ROLE_NAME) && +- this._enabledRoles.includes(Constants.SMARTCARD_ROLE_NAME)) ++ if (this.supportedRoles.includes(Constants.SMARTCARD_ROLE_NAME)) + this._connectSmartcardManager(); +- if (this.supportedRoles.includes(Constants.PASSKEY_ROLE_NAME) && +- this._enabledRoles.includes(Constants.PASSKEY_ROLE_NAME)) ++ if (this.supportedRoles.includes(Constants.PASSKEY_ROLE_NAME)) + this._connectPasskeyDeviceManager(); +- if (this.supportedRoles.includes(Constants.FINGERPRINT_ROLE_NAME) && +- this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) ++ if (this.supportedRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) + this._connectFingerprintManager(); + } + +@@ -143,11 +137,7 @@ export class AuthServices extends GObject.Object { + } + + get supportedRoles() { +- return this.constructor.SupportedRoles; +- } +- +- get unsupportedRoles() { +- return this._handleGetUnsupportedRoles(); ++ return this._handleGetSupportedRoles(); + } + + selectChoice(serviceName, key) { +@@ -229,16 +219,17 @@ export class AuthServices extends GObject.Object { + this._handleClear(); + } + +- updateEnabledRoles(roles) { +- if (this._enabledRoles.length === roles.length && +- this._enabledRoles.every(r => roles.includes(r))) +- return false; ++ updateEnabledRoles({disableRoles}) { ++ const updatedRoles = this.supportedRoles ++ .filter(r => !disableRoles.includes(r)); + +- this._enabledRoles = roles; ++ if (updatedRoles.length === this._enabledRoles.length && ++ updatedRoles.every(r => this._enabledRoles.includes(r))) ++ return; + +- this._handleUpdateEnabledRoles(); ++ this._enabledRoles = updatedRoles; + +- return true; ++ this._handleUpdateEnabledRoles(); + } + + _clearUserVerifier() { +@@ -482,8 +473,8 @@ export class AuthServices extends GObject.Object { + } + } + +- _handleGetUnsupportedRoles() { +- return this._enabledRoles.filter(r => !this.supportedRoles.includes(r)); ++ _handleGetSupportedRoles() { ++ return this.constructor.SupportedRoles; + } + + _handleSelectChoice() {} +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 8faa82e0b..f98000c35 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -12,21 +12,28 @@ import {AuthServices} from './authServices.js'; + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + const FINGERPRINT_READY_TIMEOUT_MS = 500; + ++const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; ++const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; ++const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; ++ + const Mechanisms = [ + { + serviceName: Constants.PASSWORD_SERVICE_NAME, + role: Constants.PASSWORD_ROLE_NAME, + name: _('Password'), ++ setting: PASSWORD_AUTHENTICATION_KEY, + }, + { + serviceName: Constants.SMARTCARD_SERVICE_NAME, + role: Constants.SMARTCARD_ROLE_NAME, + name: _('Smartcard'), ++ setting: SMARTCARD_AUTHENTICATION_KEY, + }, + { + serviceName: Constants.FINGERPRINT_SERVICE_NAME, + role: Constants.FINGERPRINT_ROLE_NAME, + name: _('Fingerprint'), ++ setting: FINGERPRINT_AUTHENTICATION_KEY, + }, + ]; + +@@ -47,6 +54,12 @@ export class AuthServicesLegacy extends AuthServices { + GObject.registerClass(this); + } + ++ static isEnabled(settings) { ++ return settings.get_boolean(PASSWORD_AUTHENTICATION_KEY) || ++ settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY) || ++ settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY); ++ } ++ + constructor(params) { + super(params); + +@@ -150,7 +163,8 @@ export class AuthServicesLegacy extends AuthServices { + + _handleUpdateEnabledMechanisms() { + this._enabledMechanisms.push(...Mechanisms.filter(m => +- this._enabledRoles.includes(m.role) ++ this._enabledRoles.includes(m.role) && ++ this._settings.get_boolean(m.setting) + )); + + if (!this._fingerprintManager?.readerFound) { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index ee018e7b9..a88e971d3 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -107,13 +107,13 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + } + +- _handleGetUnsupportedRoles() { +- // When we couldn't get mechanisms (NOT_FOUND), assume all roles +- // are unsupported so they cascade to lower-priority authServices. ++ _handleGetSupportedRoles() { ++ // When we couldn't get mechanisms (NOT_FOUND), we don't support any ++ // role so they cascade to lower-priority authServices. + if (this._mechanismsStatus === MechanismsStatus.NOT_FOUND) +- return this._enabledRoles; ++ return []; + +- return super._handleGetUnsupportedRoles(); ++ return super._handleGetSupportedRoles(); + } + + _handleReset() { +diff --git a/js/gdm/util.js b/js/gdm/util.js +index f7abecbfe..301dcd0f8 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -14,11 +14,6 @@ import {AuthServicesSSSDSwitchable} from './authServicesSSSDSwitchable.js'; + const CLONE_FADE_ANIMATION_TIME = 250; + + export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +-export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; +-export const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; +-export const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; +-export const PASSKEY_AUTHENTICATION_KEY = 'enable-passkey-authentication'; +-export const WEB_AUTHENTICATION_KEY = 'enable-web-authentication'; + export const BANNER_MESSAGE_KEY = 'banner-message-enable'; + export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; + export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; +@@ -440,30 +435,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + _updateAuthServices() { +- const enabledRoles = []; +- +- if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY)) +- enabledRoles.push(Constants.PASSWORD_ROLE_NAME); +- if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) +- enabledRoles.push(Constants.SMARTCARD_ROLE_NAME); +- if (this._settings.get_boolean(PASSKEY_AUTHENTICATION_KEY)) +- enabledRoles.push(Constants.PASSKEY_ROLE_NAME); +- if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) +- enabledRoles.push(Constants.FINGERPRINT_ROLE_NAME); +- if (this._settings.get_boolean(WEB_AUTHENTICATION_KEY)) +- enabledRoles.push(Constants.WEB_LOGIN_ROLE_NAME); +- + const enabledAuthServicesClasses = AuthServicesClasses + .filter(C => C.isEnabled(this._settings)); + +- if (JSON.stringify(enabledRoles) === JSON.stringify(this._enabledRoles) && +- enabledAuthServicesClasses.length === this._enabledAuthServicesClasses?.length && ++ if (enabledAuthServicesClasses.length === this._enabledAuthServicesClasses?.length && + enabledAuthServicesClasses.every(c => this._enabledAuthServicesClasses.includes(c))) + return; + +- this._enabledRoles = enabledRoles; + this._enabledAuthServicesClasses = enabledAuthServicesClasses; +- + this._createAuthServices(); + } + +@@ -474,21 +453,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + client: this._client, + allowedFailures: this.allowedFailures, + reauthOnly: this._reauthOnly, ++ settings: this._settings, + }; + +- this._enabledAuthServicesClasses +- .filter(AuthServicesClass => +- AuthServicesClass.supportsAny(this._enabledRoles)) +- .forEach(AuthServicesClass => { +- const enabledRoles = +- this._authServices.at(-1)?.unsupportedRoles ?? +- this._enabledRoles; +- this._authServices.push(new AuthServicesClass({ +- ...params, +- enabledRoles, +- })); +- }); ++ this._enabledAuthServicesClasses.forEach(AuthServicesClass => { ++ this._authServices.push(new AuthServicesClass(params)); ++ }); + ++ this._redistributeRoles(); + this._connectAuthServices(); + } + +@@ -532,13 +504,13 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + this._redistributingRoles = true; + +- // Each authServices receives the roles unsupported by the one ++ // Each authServices disables the roles supported by the one + // before it, cascading down the priority chain + const authServices = this._authServices; + for (let i = 1; i < authServices.length; i++) { + const prev = authServices[i - 1]; + const current = authServices[i]; +- current.updateEnabledRoles(prev.unsupportedRoles); ++ current.updateEnabledRoles({disableRoles: prev.supportedRoles}); + } + + this._redistributingRoles = false; +-- +2.55.0 + + +From 174ab6cda1c291f62f5311147188b98ef0f53ab8 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Sun, 19 Jul 2026 15:25:00 +0200 +Subject: [PATCH 59/72] gdm: Centralize role properties in authServices base + class + +Move role-specific knowledge out of external callers and into a +central RoleProperties map in the AuthServices base class. + +Properties are injected into mechanism objects by +_updateEnabledMechanisms() so subclasses and consumers don't need +to interpret roles directly. Only truthy properties are set: + + - selectable: mechanism can be chosen from the auth menu + - preemptiveInput: allows typing before PAM prompts (retry flow) + - hint: clock hint text on the lock screen (e.g. 'Insert smartcard') + - iconName: indicator icon for non-selectable mechanisms + - description: indicator label for non-selectable mechanisms + +This removes isSelectable() and getNonSelectableIconName() from +util.js, and eliminates role constant imports from authPrompt.js +and unlockDialog.js. Adding a new role now only requires an entry +in RoleProperties. + +Part-of: +--- + js/gdm/authPrompt.js | 3 +-- + js/gdm/authServices.js | 27 +++++++++++++++++++++++++ + js/gdm/authServicesLegacy.js | 3 ++- + js/gdm/loginDialog.js | 2 +- + js/gdm/util.js | 38 +----------------------------------- + js/ui/unlockDialog.js | 34 +++++++------------------------- + 6 files changed, 39 insertions(+), 68 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index afc5e0f00..3beb04f4d 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -9,7 +9,6 @@ import St from 'gi://St'; + import * as Animation from '../ui/animation.js'; + import * as AuthList from './authList.js'; + import * as Batch from './batch.js'; +-import * as Constants from './constants.js'; + import * as GdmUtil from './util.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Params from '../misc/params.js'; +@@ -555,7 +554,7 @@ export const AuthPrompt = GObject.registerClass({ + + // Only allow instant retrying with password authentication. + // The rest of authentications will retry through the reset flow. +- if (canRetry && this._userVerifier.selectedMechanism?.role === Constants.PASSWORD_ROLE_NAME) { ++ if (canRetry && this._userVerifier.selectedMechanism?.preemptiveInput) { + this.verificationStatus = AuthPromptStatus.VERIFYING; + this._entry.text = ''; + this.startPreemptiveInput(); +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 0cbe028d3..d261bae98 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -19,6 +19,28 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); + Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); + Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply'); + ++export const RoleProperties = { ++ [Constants.PASSWORD_ROLE_NAME]: { ++ selectable: true, ++ preemptiveInput: true, ++ }, ++ [Constants.SMARTCARD_ROLE_NAME]: { ++ selectable: true, ++ hint: _('Insert smartcard'), ++ }, ++ [Constants.PASSKEY_ROLE_NAME]: { ++ selectable: true, ++ hint: _('Insert security key'), ++ }, ++ [Constants.WEB_LOGIN_ROLE_NAME]: { ++ selectable: true, ++ }, ++ [Constants.FINGERPRINT_ROLE_NAME]: { ++ iconName: 'fingerprint-auth-symbolic', ++ description: _('Unlock with fingerprint'), ++ }, ++}; ++ + export class AuthServices extends GObject.Object { + static [GObject.signals] = { + 'destroy': {}, +@@ -252,6 +274,11 @@ export class AuthServices extends GObject.Object { + + this._handleUpdateEnabledMechanisms(); + ++ this._enabledMechanisms = this._enabledMechanisms.map(m => ({ ++ ...m, ++ ...RoleProperties[m.role], ++ })); ++ + this.emit('mechanisms-changed'); + } + +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index f98000c35..a09e2751b 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -7,7 +7,7 @@ import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as OVirt from './oVirt.js'; + import * as Util from './util.js'; + import * as Vmware from './vmware.js'; +-import {AuthServices} from './authServices.js'; ++import {AuthServices, RoleProperties} from './authServices.js'; + + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + const FINGERPRINT_READY_TIMEOUT_MS = 500; +@@ -400,6 +400,7 @@ export class AuthServicesLegacy extends AuthServices { + this._selectedMechanism = { + serviceName: credentialManager.service, + role: Constants.PASSWORD_ROLE_NAME, ++ ...RoleProperties[Constants.PASSWORD_ROLE_NAME], + }; + this.emit('reset', {softReset: true}); + } +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 6d0708b1f..638fb3c54 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -1118,7 +1118,7 @@ export const LoginDialog = GObject.registerClass({ + return; + + for (const m of mechanisms) { +- if (GdmUtil.isSelectable(m)) { ++ if (m.selectable) { + this._authMenuButton.addItem({ + sectionName: _PRIMARY_LOGIN_METHOD_SECTION_NAME, + ...m, +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 301dcd0f8..5cc77a26c 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -99,42 +99,6 @@ export function cloneAndFadeOutActor(actor) { + return hold; + } + +-/** +- * @param {object} mechanism +- * @returns {boolean} +- */ +-export function isSelectable(mechanism) { +- switch (mechanism.role) { +- case Constants.PASSWORD_ROLE_NAME: +- case Constants.SMARTCARD_ROLE_NAME: +- case Constants.PASSKEY_ROLE_NAME: +- case Constants.WEB_LOGIN_ROLE_NAME: +- return true; +- case Constants.FINGERPRINT_ROLE_NAME: +- return false; +- default: +- throw new Error(`Failed checking mechanism is selectable: ${mechanism.role}`); +- } +-} +- +-/** +- * @param {object} mechanism +- * @returns {string} +- */ +-export function getNonSelectableIconName(mechanism) { +- // This is only used for non selectable mechanisms. +- // Currently only fingerprint is non selectable +- if (isSelectable(mechanism)) +- throw new Error(`Failed getting mechanism icon: ${mechanism.role}, is selectable`); +- +- switch (mechanism.role) { +- case Constants.FINGERPRINT_ROLE_NAME: +- return 'fingerprint-auth-symbolic'; +- default: +- throw new Error(`Failed getting mechanism icon: ${mechanism.role}`); +- } +-} +- + export class ShellUserVerifier extends Signals.EventEmitter { + constructor(client, params) { + super(); +@@ -530,7 +494,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + .filter(m => !seenRoles.has(m.role) && seenRoles.add(m.role)); + + const selectedMechanism = this.selectedMechanism ?? +- mechanisms.find(m => isSelectable(m)) ?? ++ mechanisms.find(m => m.selectable) ?? + {}; + + this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); +diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js +index d8d825da6..68331934d 100644 +--- a/js/ui/unlockDialog.js ++++ b/js/ui/unlockDialog.js +@@ -17,8 +17,6 @@ import * as SwipeTracker from './swipeTracker.js'; + import {formatDateWithCFormatString} from '../misc/dateUtils.js'; + import * as AuthMenuButton from '../gdm/authMenuButton.js'; + import * as AuthPrompt from '../gdm/authPrompt.js'; +-import * as GdmConstants from '../gdm/constants.js'; +-import * as GdmUtil from '../gdm/util.js'; + import {AuthPromptStatus} from '../gdm/authPrompt.js'; + import {MprisSource} from './mpris.js'; + import {MediaMessage} from './messageList.js'; +@@ -422,19 +420,16 @@ class UnlockDialogClock extends St.BoxLayout { + this._date.text = formatDateWithCFormatString(date, dateFormat); + } + +- selectAuthRole(roleName) { +- this._selectedAuthRole = roleName; ++ selectAuthHint(hint) { ++ this._authHint = hint; + this._updateHint(); + } + + _updateHint() { +- const selectedAuthRole = this._selectedAuthRole; + let text; + +- if (selectedAuthRole === GdmConstants.SMARTCARD_ROLE_NAME) +- text = _('Insert smartcard'); +- else if (selectedAuthRole === GdmConstants.PASSKEY_ROLE_NAME) +- text = _('Insert security key'); ++ if (this._authHint) ++ text = this._authHint; + else if (this._seat.touch_mode) + text = _('Swipe up'); + else +@@ -774,7 +769,7 @@ export const UnlockDialog = GObject.registerClass({ + + this._selectedAuthMechanism = authMechanism; + +- this._clock.selectAuthRole(authMechanism?.role); ++ this._clock.selectAuthHint(authMechanism?.hint); + } + + _createBackground(monitorIndex) { +@@ -960,17 +955,13 @@ export const UnlockDialog = GObject.registerClass({ + return; + + for (const m of mechanisms) { +- if (GdmUtil.isSelectable(m)) { ++ if (m.selectable) { + this._authMenuButton.addItem({ + sectionName: PRIMARY_UNLOCK_METHOD_SECTION_NAME, + ...m, + }); + } else { +- this._authIndicatorButton.addItem({ +- iconName: GdmUtil.getNonSelectableIconName(m), +- description: this._getUnlockDescription(m), +- ...m, +- }); ++ this._authIndicatorButton.addItem(m); + } + } + +@@ -980,17 +971,6 @@ export const UnlockDialog = GObject.registerClass({ + this._authIndicatorButton.updateDescriptionLabel(); + } + +- _getUnlockDescription(mechanism) { +- // This is only used for non selectable mechanisms. +- // Currently only fingerprint is non selectable +- switch (mechanism.role) { +- case GdmConstants.FINGERPRINT_ROLE_NAME: +- return _('Unlock with fingerprint'); +- default: +- throw new Error(`Failed getting unlock description: ${mechanism.role}`); +- } +- } +- + _escape() { + if (this._authPrompt && this.allowCancel) + this._authPrompt.cancel(); +-- +2.55.0 + + +From b1505b00570555e2d6f214eb2f921d12196517ca Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Sun, 19 Jul 2026 15:35:40 +0200 +Subject: [PATCH 60/72] gdm: Rename passkeyDeviceManager to fido2TokenManager + +The hardware device manager monitors FIDO2 tokens via udev, which +is not specific to the passkey authentication role. Rename to +reflect the actual device type being tracked. + +Part-of: +--- + js/gdm/authServices.js | 16 +++---- + js/gdm/authServicesSSSDSwitchable.js | 6 +-- + js/gdm/fido2TokenManager.js | 69 ++++++++++++++++++++++++++++ + js/gdm/passkeyDeviceManager.js | 69 ---------------------------- + js/js-resources.gresource.xml | 2 +- + 5 files changed, 81 insertions(+), 81 deletions(-) + create mode 100644 js/gdm/fido2TokenManager.js + delete mode 100644 js/gdm/passkeyDeviceManager.js + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index d261bae98..bf30b48f4 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -2,7 +2,7 @@ import * as Constants from './constants.js'; + import * as FingerprintManager from './fingerprintManager.js'; + import * as Params from '../misc/params.js'; + import {registerDestroyableType} from '../misc/signalTracker.js'; +-import * as PasskeyDeviceManager from './passkeyDeviceManager.js'; ++import * as Fido2TokenManager from './fido2TokenManager.js'; + import * as SmartcardManager from './smartcardManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Util from './util.js'; +@@ -141,7 +141,7 @@ export class AuthServices extends GObject.Object { + if (this.supportedRoles.includes(Constants.SMARTCARD_ROLE_NAME)) + this._connectSmartcardManager(); + if (this.supportedRoles.includes(Constants.PASSKEY_ROLE_NAME)) +- this._connectPasskeyDeviceManager(); ++ this._connectFido2TokenManager(); + if (this.supportedRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) + this._connectFingerprintManager(); + } +@@ -290,11 +290,11 @@ export class AuthServices extends GObject.Object { + this); + } + +- _connectPasskeyDeviceManager() { +- this._passkeyDeviceManager = PasskeyDeviceManager.getPasskeyDeviceManager(); +- this._passkeyDeviceManager.connectObject( +- 'passkey-inserted', () => this._handlePasskeyChanged(), +- 'passkey-removed', () => this._handlePasskeyChanged(), ++ _connectFido2TokenManager() { ++ this._fido2TokenManager = Fido2TokenManager.getFido2TokenManager(); ++ this._fido2TokenManager.connectObject( ++ 'fido2-token-inserted', () => this._handleFido2TokenChanged(), ++ 'fido2-token-removed', () => this._handleFido2TokenChanged(), + this); + } + +@@ -534,7 +534,7 @@ export class AuthServices extends GObject.Object { + + _handleSmartcardChanged() {} + +- _handlePasskeyChanged() {} ++ _handleFido2TokenChanged() {} + + _handleFingerprintChanged() {} + +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index a88e971d3..ecba7d87a 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -99,7 +99,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._startSmartcardLogin(); + break; + case Constants.PASSKEY_ROLE_NAME: +- this._startPasskeyLogin(); ++ this._startFido2TokenLogin(); + break; + case Constants.WEB_LOGIN_ROLE_NAME: + this._startWebLogin(); +@@ -220,7 +220,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('reset', {softReset: true, reuseEntryText: true}); + } + +- _handlePasskeyChanged() { ++ _handleFido2TokenChanged() { + if (!this._selectedMechanism || + !this._enabledMechanisms.some(({role}) => role === Constants.PASSKEY_ROLE_NAME)) + return; +@@ -401,7 +401,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + }; + } + +- _startPasskeyLogin() { ++ _startFido2TokenLogin() { + const { + serviceName, + keyConnected, initInstruction, +diff --git a/js/gdm/fido2TokenManager.js b/js/gdm/fido2TokenManager.js +new file mode 100644 +index 000000000..82ed2e094 +--- /dev/null ++++ b/js/gdm/fido2TokenManager.js +@@ -0,0 +1,69 @@ ++import GObject from 'gi://GObject'; ++import GUdev from 'gi://GUdev'; ++ ++let _fido2TokenManager = null; ++ ++/** ++ * @returns {Fido2TokenManager} ++ */ ++export function getFido2TokenManager() { ++ if (_fido2TokenManager == null) ++ _fido2TokenManager = new Fido2TokenManager(); ++ ++ return _fido2TokenManager; ++} ++ ++class Fido2TokenManager extends GObject.Object { ++ static [GObject.signals] = { ++ 'fido2-token-inserted': {param_types: [GObject.TYPE_JSOBJECT]}, ++ 'fido2-token-removed': {param_types: [GObject.TYPE_JSOBJECT]}, ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor() { ++ super(); ++ ++ this._insertedTokens = new Map(); ++ this._udevClient = new GUdev.Client({subsystems: ['hidraw']}); ++ ++ this._onLoaded(); ++ } ++ ++ get hasInsertedTokens() { ++ return this._insertedTokens.size > 0; ++ } ++ ++ _onLoaded() { ++ this._udevClient.query_by_subsystem('hidraw') ++ .forEach(d => this._addToken(d)); ++ ++ this._udevClient.connect('uevent', (_, action, device) => { ++ if (action === 'add') ++ this._addToken(device); ++ else if (action === 'remove') ++ this._removeToken(device); ++ }); ++ } ++ ++ _addToken(device) { ++ const sysfsPath = device.get_sysfs_path(); ++ const isFido = device.get_property_as_int('ID_FIDO_TOKEN') === 1; ++ if (!isFido || this._insertedTokens.has(sysfsPath)) ++ return; ++ ++ this._insertedTokens.set(sysfsPath, device); ++ this.emit('fido2-token-inserted', device); ++ } ++ ++ _removeToken(device) { ++ const sysfsPath = device.get_sysfs_path(); ++ if (!this._insertedTokens.has(sysfsPath)) ++ return; ++ ++ this._insertedTokens.delete(sysfsPath); ++ this.emit('fido2-token-removed', device); ++ } ++} +diff --git a/js/gdm/passkeyDeviceManager.js b/js/gdm/passkeyDeviceManager.js +deleted file mode 100644 +index 20799975a..000000000 +--- a/js/gdm/passkeyDeviceManager.js ++++ /dev/null +@@ -1,69 +0,0 @@ +-import GObject from 'gi://GObject'; +-import GUdev from 'gi://GUdev'; +- +-let _passkeyDeviceManager = null; +- +-/** +- * @returns {PasskeyDeviceManager} +- */ +-export function getPasskeyDeviceManager() { +- if (_passkeyDeviceManager == null) +- _passkeyDeviceManager = new PasskeyDeviceManager(); +- +- return _passkeyDeviceManager; +-} +- +-class PasskeyDeviceManager extends GObject.Object { +- static [GObject.signals] = { +- 'passkey-inserted': {param_types: [GObject.TYPE_JSOBJECT]}, +- 'passkey-removed': {param_types: [GObject.TYPE_JSOBJECT]}, +- }; +- +- static { +- GObject.registerClass(this); +- } +- +- constructor() { +- super(); +- +- this._insertedPasskeys = new Map(); +- this._udevClient = new GUdev.Client({subsystems: ['hidraw']}); +- +- this._onLoaded(); +- } +- +- get hasInsertedPasskeys() { +- return this._insertedPasskeys.size > 0; +- } +- +- _onLoaded() { +- this._udevClient.query_by_subsystem('hidraw') +- .forEach(d => this._addPasskey(d)); +- +- this._udevClient.connect('uevent', (_, action, device) => { +- if (action === 'add') +- this._addPasskey(device); +- else if (action === 'remove') +- this._removePasskey(device); +- }); +- } +- +- _addPasskey(device) { +- const sysfsPath = device.get_sysfs_path(); +- const isFido = device.get_property_as_int('ID_FIDO_TOKEN') === 1; +- if (!isFido || this._insertedPasskeys.has(sysfsPath)) +- return; +- +- this._insertedPasskeys.set(sysfsPath, device); +- this.emit('passkey-inserted', device); +- } +- +- _removePasskey(device) { +- const sysfsPath = device.get_sysfs_path(); +- if (!this._insertedPasskeys.has(sysfsPath)) +- return; +- +- this._insertedPasskeys.delete(sysfsPath); +- this.emit('passkey-removed', device); +- } +-} +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index ced2f28e2..7d5f4b039 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -13,7 +13,7 @@ + gdm/fingerprintManager.js + gdm/loginDialog.js + gdm/oVirt.js +- gdm/passkeyDeviceManager.js ++ gdm/fido2TokenManager.js + gdm/realmd.js + gdm/smartcardManager.js + gdm/util.js +-- +2.55.0 + + +From a65ee4a7d1ca1952b7aebfec9cb90734ad8e973a Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 20 Jul 2026 19:28:48 +0200 +Subject: [PATCH 61/72] gdm: Move device setups to authServices subclasses + +Part-of: +--- + js/gdm/authServices.js | 43 ---------------------- + js/gdm/authServicesLegacy.js | 55 ++++++++++++++++++++-------- + js/gdm/authServicesSSSDSwitchable.js | 53 +++++++++++++++++++-------- + 3 files changed, 76 insertions(+), 75 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index bf30b48f4..b22208a2a 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -1,9 +1,6 @@ + import * as Constants from './constants.js'; +-import * as FingerprintManager from './fingerprintManager.js'; + import * as Params from '../misc/params.js'; + import {registerDestroyableType} from '../misc/signalTracker.js'; +-import * as Fido2TokenManager from './fido2TokenManager.js'; +-import * as SmartcardManager from './smartcardManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Util from './util.js'; + import Gdm from 'gi://Gdm'; +@@ -137,13 +134,6 @@ export class AuthServices extends GObject.Object { + this._unavailableServices = new Set(); + + this._cancellable = null; +- +- if (this.supportedRoles.includes(Constants.SMARTCARD_ROLE_NAME)) +- this._connectSmartcardManager(); +- if (this.supportedRoles.includes(Constants.PASSKEY_ROLE_NAME)) +- this._connectFido2TokenManager(); +- if (this.supportedRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) +- this._connectFingerprintManager(); + } + + get selectedMechanism() { +@@ -282,33 +272,6 @@ export class AuthServices extends GObject.Object { + this.emit('mechanisms-changed'); + } + +- _connectSmartcardManager() { +- this._smartcardManager = SmartcardManager.getSmartcardManager(); +- this._smartcardManager.connectObject( +- 'smartcard-inserted', () => this._handleSmartcardChanged(), +- 'smartcard-removed', () => this._handleSmartcardChanged(), +- this); +- } +- +- _connectFido2TokenManager() { +- this._fido2TokenManager = Fido2TokenManager.getFido2TokenManager(); +- this._fido2TokenManager.connectObject( +- 'fido2-token-inserted', () => this._handleFido2TokenChanged(), +- 'fido2-token-removed', () => this._handleFido2TokenChanged(), +- this); +- } +- +- _connectFingerprintManager() { +- // Fingerprint can only work on lockscreen +- if (!this._reauthOnly) +- return; +- +- this._fingerprintManager = FingerprintManager.getFingerprintManager(); +- this._fingerprintManager.connectObject( +- 'reader-type-changed', () => this._handleFingerprintChanged(), +- this); +- } +- + _waitPendingMessages() { + const cancellable = this._cancellable; + const timeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, 10, +@@ -532,12 +495,6 @@ export class AuthServices extends GObject.Object { + `_handleUpdateEnabledMechanisms in ${this.constructor.name}`); + } + +- _handleSmartcardChanged() {} +- +- _handleFido2TokenChanged() {} +- +- _handleFingerprintChanged() {} +- + _handleOnInfo() {} + + _handleOnProblem() {} +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index a09e2751b..aebc7885f 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -2,8 +2,10 @@ import GLib from 'gi://GLib'; + import GObject from 'gi://GObject'; + + import * as Constants from './constants.js'; ++import * as FingerprintManager from './fingerprintManager.js'; + import {FingerprintReaderType} from './fingerprintManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; ++import * as SmartcardManager from './smartcardManager.js'; + import * as OVirt from './oVirt.js'; + import * as Util from './util.js'; + import * as Vmware from './vmware.js'; +@@ -63,6 +65,9 @@ export class AuthServicesLegacy extends AuthServices { + constructor(params) { + super(params); + ++ this._connectSmartcardManager(); ++ this._connectFingerprintManager(); ++ + this._updateEnabledMechanisms(); + + this._credentialManagers = {}; +@@ -177,22 +182,6 @@ export class AuthServicesLegacy extends AuthServices { + } + } + +- _handleSmartcardChanged() { +- if (this._selectedMechanism?.role !== Constants.SMARTCARD_ROLE_NAME || +- this._smartcardInProgress && this._smartcardManager.hasInsertedTokens()) +- return; +- +- this.emit('reset', {softReset: true}); +- } +- +- _handleFingerprintChanged() { +- if (!this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) +- return; +- +- this._updateEnabledMechanisms(); +- this.emit('reset', {softReset: true, reuseEntryText: true}); +- } +- + _handleOnInfo(serviceName, info) { + if (serviceName === this._selectedMechanism?.serviceName) { + this.emit('queue-message', { +@@ -396,6 +385,40 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('reset'); + } + ++ _connectSmartcardManager() { ++ this._smartcardManager = SmartcardManager.getSmartcardManager(); ++ this._smartcardManager.connectObject( ++ 'smartcard-inserted', () => this._onSmartcardChanged(), ++ 'smartcard-removed', () => this._onSmartcardChanged(), ++ this); ++ } ++ ++ _connectFingerprintManager() { ++ if (!this._reauthOnly) ++ return; ++ ++ this._fingerprintManager = FingerprintManager.getFingerprintManager(); ++ this._fingerprintManager.connectObject( ++ 'reader-type-changed', () => this._onFingerprintChanged(), ++ this); ++ } ++ ++ _onSmartcardChanged() { ++ if (this._selectedMechanism?.role !== Constants.SMARTCARD_ROLE_NAME || ++ this._smartcardInProgress && this._smartcardManager.hasInsertedTokens()) ++ return; ++ ++ this.emit('reset', {softReset: true}); ++ } ++ ++ _onFingerprintChanged() { ++ if (!this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) ++ return; ++ ++ this._updateEnabledMechanisms(); ++ this.emit('reset', {softReset: true, reuseEntryText: true}); ++ } ++ + _onCredentialManagerAuthenticated(credentialManager) { + this._selectedMechanism = { + serviceName: credentialManager.service, +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index ecba7d87a..d261d0012 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -2,7 +2,9 @@ import GLib from 'gi://GLib'; + import GObject from 'gi://GObject'; + + import * as Constants from './constants.js'; ++import * as Fido2TokenManager from './fido2TokenManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; ++import * as SmartcardManager from './smartcardManager.js'; + import * as Util from './util.js'; + import {AuthServices} from './authServices.js'; + +@@ -40,6 +42,9 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + constructor(params) { + super(params); + ++ this._connectSmartcardManager(); ++ this._connectFido2TokenManager(); ++ + this._mechanismsStatus = MechanismsStatus.WAITING; + } + +@@ -212,22 +217,6 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + }); + } + +- _handleSmartcardChanged() { +- if (!this._selectedMechanism || +- !this._enabledMechanisms.some(({role}) => role === Constants.SMARTCARD_ROLE_NAME)) +- return; +- +- this.emit('reset', {softReset: true, reuseEntryText: true}); +- } +- +- _handleFido2TokenChanged() { +- if (!this._selectedMechanism || +- !this._enabledMechanisms.some(({role}) => role === Constants.PASSKEY_ROLE_NAME)) +- return; +- +- this.emit('reset', {softReset: true, reuseEntryText: true}); +- } +- + _handleOnInfo(serviceName, info) { + if (!this._eventExpected()) + return; +@@ -483,4 +472,36 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + GLib.source_remove(this._webLoginTimeoutId); + this._webLoginTimeoutId = 0; + } ++ ++ _connectSmartcardManager() { ++ this._smartcardManager = SmartcardManager.getSmartcardManager(); ++ this._smartcardManager.connectObject( ++ 'smartcard-inserted', () => this._onSmartcardChanged(), ++ 'smartcard-removed', () => this._onSmartcardChanged(), ++ this); ++ } ++ ++ _connectFido2TokenManager() { ++ this._fido2TokenManager = Fido2TokenManager.getFido2TokenManager(); ++ this._fido2TokenManager.connectObject( ++ 'fido2-token-inserted', () => this._onFido2TokenChanged(), ++ 'fido2-token-removed', () => this._onFido2TokenChanged(), ++ this); ++ } ++ ++ _onSmartcardChanged() { ++ if (!this._selectedMechanism || ++ !this._enabledMechanisms.some(({role}) => role === Constants.SMARTCARD_ROLE_NAME)) ++ return; ++ ++ this.emit('reset', {softReset: true, reuseEntryText: true}); ++ } ++ ++ _onFido2TokenChanged() { ++ if (!this._selectedMechanism || ++ !this._enabledMechanisms.some(({role}) => role === Constants.PASSKEY_ROLE_NAME)) ++ return; ++ ++ this.emit('reset', {softReset: true, reuseEntryText: true}); ++ } + } +-- +2.55.0 + + +From 422c6fec320512ad36203018aa5d0e8d7c51addf Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Mon, 20 Jul 2026 20:02:22 +0200 +Subject: [PATCH 62/72] gdm: Remove constants module and add wiggle parameter + to message flow + +Delete constants.js and move its contents closer to where they are used: +role names become a Role enum exported from authServices.js, and service +name strings become local constants in each authServices subclass. + +The shouldWiggle flag, previously derived in authPrompt by checking the +service name, is now passed through the message queue from +authServicesLegacy. + +Part-of: +--- + js/gdm/authPrompt.js | 5 +-- + js/gdm/authServices.js | 21 ++++++--- + js/gdm/authServicesLegacy.js | 67 +++++++++++++++------------- + js/gdm/authServicesSSSDSwitchable.js | 67 ++++++++++++++-------------- + js/gdm/constants.js | 10 ----- + js/gdm/util.js | 21 ++++----- + js/js-resources.gresource.xml | 1 - + 7 files changed, 97 insertions(+), 95 deletions(-) + delete mode 100644 js/gdm/constants.js + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 3beb04f4d..0ba1a5de6 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -458,7 +458,7 @@ export const AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowMessage({serviceName, message, type, showMessageResolver}) { ++ _onShowMessage({message, type, shouldWiggle, showMessageResolver}) { + this.setMessage(message, type); + this.emit('prompted'); + +@@ -474,9 +474,6 @@ export const AuthPrompt = GObject.registerClass({ + this.updateSensitivity({sensitive: true}); + } + +- const shouldWiggle = type === GdmUtil.MessageType.ERROR && +- this._userVerifier.serviceIsFingerprint(serviceName); +- + const wigglePromise = shouldWiggle + ? wiggle(this._message, {duration: 65, wiggleCount: 3}) + : Promise.resolve(); +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index b22208a2a..3b58f4957 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -1,4 +1,3 @@ +-import * as Constants from './constants.js'; + import * as Params from '../misc/params.js'; + import {registerDestroyableType} from '../misc/signalTracker.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +@@ -16,23 +15,31 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_answer_query'); + Gio._promisify(Gdm.UserVerifierChoiceListProxy.prototype, 'call_select_choice'); + Gio._promisify(Gdm.UserVerifierCustomJSONProxy.prototype, 'call_reply'); + ++export const Role = { ++ PASSWORD: 'password', ++ SMARTCARD: 'smartcard', ++ FINGERPRINT: 'fingerprint', ++ PASSKEY: 'passkey', ++ WEB_LOGIN: 'eidp', ++}; ++ + export const RoleProperties = { +- [Constants.PASSWORD_ROLE_NAME]: { ++ [Role.PASSWORD]: { + selectable: true, + preemptiveInput: true, + }, +- [Constants.SMARTCARD_ROLE_NAME]: { ++ [Role.SMARTCARD]: { + selectable: true, + hint: _('Insert smartcard'), + }, +- [Constants.PASSKEY_ROLE_NAME]: { ++ [Role.PASSKEY]: { + selectable: true, + hint: _('Insert security key'), + }, +- [Constants.WEB_LOGIN_ROLE_NAME]: { ++ [Role.WEB_LOGIN]: { + selectable: true, + }, +- [Constants.FINGERPRINT_ROLE_NAME]: { ++ [Role.FINGERPRINT]: { + iconName: 'fingerprint-auth-symbolic', + description: _('Unlock with fingerprint'), + }, +@@ -45,12 +52,14 @@ export class AuthServices extends GObject.Object { + * @param {string} serviceName + * @param {string} message + * @param {MessageType} messageType ++ * @param {boolean} [wiggle] + */ + 'queue-message': {param_types: [GObject.TYPE_JSOBJECT]}, + /** + * @param {string} serviceName + * @param {string} message + * @param {MessageType} messageType ++ * @param {boolean} [wiggle] + */ + 'queue-priority-message': {param_types: [GObject.TYPE_JSOBJECT]}, + /** +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index aebc7885f..9707e95dd 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -1,7 +1,6 @@ + import GLib from 'gi://GLib'; + import GObject from 'gi://GObject'; + +-import * as Constants from './constants.js'; + import * as FingerprintManager from './fingerprintManager.js'; + import {FingerprintReaderType} from './fingerprintManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +@@ -9,31 +8,35 @@ import * as SmartcardManager from './smartcardManager.js'; + import * as OVirt from './oVirt.js'; + import * as Util from './util.js'; + import * as Vmware from './vmware.js'; +-import {AuthServices, RoleProperties} from './authServices.js'; ++import {AuthServices, Role, RoleProperties} from './authServices.js'; + + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + const FINGERPRINT_READY_TIMEOUT_MS = 500; + ++const PASSWORD_SERVICE_NAME = 'gdm-password'; ++const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; ++const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; ++ + const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; + const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; + const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; + + const Mechanisms = [ + { +- serviceName: Constants.PASSWORD_SERVICE_NAME, +- role: Constants.PASSWORD_ROLE_NAME, ++ serviceName: PASSWORD_SERVICE_NAME, ++ role: Role.PASSWORD, + name: _('Password'), + setting: PASSWORD_AUTHENTICATION_KEY, + }, + { +- serviceName: Constants.SMARTCARD_SERVICE_NAME, +- role: Constants.SMARTCARD_ROLE_NAME, ++ serviceName: SMARTCARD_SERVICE_NAME, ++ role: Role.SMARTCARD, + name: _('Smartcard'), + setting: SMARTCARD_AUTHENTICATION_KEY, + }, + { +- serviceName: Constants.FINGERPRINT_SERVICE_NAME, +- role: Constants.FINGERPRINT_ROLE_NAME, ++ serviceName: FINGERPRINT_SERVICE_NAME, ++ role: Role.FINGERPRINT, + name: _('Fingerprint'), + setting: FINGERPRINT_AUTHENTICATION_KEY, + }, +@@ -41,15 +44,15 @@ const Mechanisms = [ + + export class AuthServicesLegacy extends AuthServices { + static SupportedRoles = [ +- Constants.PASSWORD_ROLE_NAME, +- Constants.SMARTCARD_ROLE_NAME, +- Constants.FINGERPRINT_ROLE_NAME, ++ Role.PASSWORD, ++ Role.SMARTCARD, ++ Role.FINGERPRINT, + ]; + + static RoleToService = { +- [Constants.PASSWORD_ROLE_NAME]: Constants.PASSWORD_SERVICE_NAME, +- [Constants.SMARTCARD_ROLE_NAME]: Constants.SMARTCARD_SERVICE_NAME, +- [Constants.FINGERPRINT_ROLE_NAME]: Constants.FINGERPRINT_SERVICE_NAME, ++ [Role.PASSWORD]: PASSWORD_SERVICE_NAME, ++ [Role.SMARTCARD]: SMARTCARD_SERVICE_NAME, ++ [Role.FINGERPRINT]: FINGERPRINT_SERVICE_NAME, + }; + + static { +@@ -89,7 +92,7 @@ export class AuthServicesLegacy extends AuthServices { + if (serviceName !== this._selectedMechanism?.serviceName) + return; + +- if (this._selectedMechanism.role === Constants.SMARTCARD_ROLE_NAME) ++ if (this._selectedMechanism.role === Role.SMARTCARD) + this._smartcardInProgress = true; + + this._userVerifier.call_answer_query( +@@ -111,7 +114,7 @@ export class AuthServicesLegacy extends AuthServices { + // Username won't be needed when there's only one mechanism and is + // Smartcard, or if the selected mechanism is a credential manager + return !(this._enabledMechanisms.length === 1 && +- this._enabledMechanisms[0].role === Constants.SMARTCARD_ROLE_NAME || ++ this._enabledMechanisms[0].role === Role.SMARTCARD || + Object.keys(this._credentialManagers).includes(this._selectedMechanism?.serviceName)); + } + +@@ -136,7 +139,7 @@ export class AuthServicesLegacy extends AuthServices { + } + + _handleOnConversationStarted(serviceName) { +- if (serviceName !== Constants.FINGERPRINT_SERVICE_NAME || ++ if (serviceName !== FINGERPRINT_SERVICE_NAME || + this._fingerprintReadyTimeoutId !== 0) + return; + +@@ -151,7 +154,7 @@ export class AuthServicesLegacy extends AuthServices { + + _setFingerprintReady(ready) { + const mechanism = this._enabledMechanisms.find(m => +- m.role === Constants.FINGERPRINT_ROLE_NAME); ++ m.role === Role.FINGERPRINT); + + if (!mechanism || mechanism.ready === ready) + return; +@@ -174,7 +177,7 @@ export class AuthServicesLegacy extends AuthServices { + + if (!this._fingerprintManager?.readerFound) { + this._enabledMechanisms = this._enabledMechanisms.filter(m => +- m.role !== Constants.FINGERPRINT_ROLE_NAME); ++ m.role !== Role.FINGERPRINT); + } else { + // Mark fingerprint as not ready until service confirms + // it's working for this user +@@ -189,7 +192,7 @@ export class AuthServicesLegacy extends AuthServices { + message: info, + messageType: Util.MessageType.INFO, + }); +- } else if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ } else if (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName)) { + // We don't show fingerprint messages directly since it's + // not the main auth service. Instead we use the messages +@@ -210,16 +213,17 @@ export class AuthServicesLegacy extends AuthServices { + + _handleOnProblem(serviceName, problem) { + if (serviceName === this._selectedMechanism?.serviceName || +- (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName))) { + this.emit('queue-priority-message', { + serviceName, + message: problem, + messageType: Util.MessageType.ERROR, ++ wiggle: serviceName === FINGERPRINT_SERVICE_NAME, + }); + } + +- if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ if (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName)) { + // pam_fprintd allows the user to retry multiple (maybe even infinite! + // times before failing the authentication conversation. +@@ -277,7 +281,7 @@ export class AuthServicesLegacy extends AuthServices { + + _handleOnConversationStopped(serviceName) { + if (serviceName !== this._selectedMechanism?.serviceName && +- serviceName !== Constants.FINGERPRINT_SERVICE_NAME) ++ serviceName !== FINGERPRINT_SERVICE_NAME) + return; + + // If the login failed with the preauthenticated oVirt credentials +@@ -290,7 +294,7 @@ export class AuthServicesLegacy extends AuthServices { + return; + } + +- if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { ++ if (serviceName === FINGERPRINT_SERVICE_NAME) { + this._clearFingerprintSignalHandlers(); + if (this._unavailableServices.has(serviceName)) + this._setFingerprintReady(false); +@@ -309,7 +313,7 @@ export class AuthServicesLegacy extends AuthServices { + } + + _handleOnServiceUnavailable(serviceName, errorMessage) { +- if (serviceName !== Constants.FINGERPRINT_SERVICE_NAME || ++ if (serviceName !== FINGERPRINT_SERVICE_NAME || + !this._enabledMechanisms.some(m => m.serviceName === serviceName) || + !errorMessage) + return; +@@ -318,11 +322,12 @@ export class AuthServicesLegacy extends AuthServices { + serviceName, + message: errorMessage, + messageType: Util.MessageType.ERROR, ++ wiggle: serviceName === FINGERPRINT_SERVICE_NAME, + }); + } + + _handleVerificationFailed(serviceName) { +- if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ if (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName) && + this._fingerprintFailedId) + GLib.source_remove(this._fingerprintFailedId); +@@ -355,7 +360,7 @@ export class AuthServicesLegacy extends AuthServices { + + _handleCanStartService(serviceName) { + return serviceName === this._selectedMechanism?.serviceName || +- (serviceName === Constants.FINGERPRINT_SERVICE_NAME && ++ (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName) && + this._userName); + } +@@ -404,7 +409,7 @@ export class AuthServicesLegacy extends AuthServices { + } + + _onSmartcardChanged() { +- if (this._selectedMechanism?.role !== Constants.SMARTCARD_ROLE_NAME || ++ if (this._selectedMechanism?.role !== Role.SMARTCARD || + this._smartcardInProgress && this._smartcardManager.hasInsertedTokens()) + return; + +@@ -412,7 +417,7 @@ export class AuthServicesLegacy extends AuthServices { + } + + _onFingerprintChanged() { +- if (!this._enabledRoles.includes(Constants.FINGERPRINT_ROLE_NAME)) ++ if (!this._enabledRoles.includes(Role.FINGERPRINT)) + return; + + this._updateEnabledMechanisms(); +@@ -422,8 +427,8 @@ export class AuthServicesLegacy extends AuthServices { + _onCredentialManagerAuthenticated(credentialManager) { + this._selectedMechanism = { + serviceName: credentialManager.service, +- role: Constants.PASSWORD_ROLE_NAME, +- ...RoleProperties[Constants.PASSWORD_ROLE_NAME], ++ role: Role.PASSWORD, ++ ...RoleProperties[Role.PASSWORD], + }; + this.emit('reset', {softReset: true}); + } +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index d261d0012..cb5a62f05 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -1,12 +1,13 @@ + import GLib from 'gi://GLib'; + import GObject from 'gi://GObject'; + +-import * as Constants from './constants.js'; + import * as Fido2TokenManager from './fido2TokenManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as SmartcardManager from './smartcardManager.js'; + import * as Util from './util.js'; +-import {AuthServices} from './authServices.js'; ++import {AuthServices, Role} from './authServices.js'; ++ ++const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; + + const SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; + +@@ -18,17 +19,17 @@ const MechanismsStatus = { + + export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ +- Constants.PASSWORD_ROLE_NAME, +- Constants.SMARTCARD_ROLE_NAME, +- Constants.PASSKEY_ROLE_NAME, +- Constants.WEB_LOGIN_ROLE_NAME, ++ Role.PASSWORD, ++ Role.SMARTCARD, ++ Role.PASSKEY, ++ Role.WEB_LOGIN, + ]; + + static RoleToService = { +- [Constants.PASSWORD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, +- [Constants.SMARTCARD_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, +- [Constants.PASSKEY_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, +- [Constants.WEB_LOGIN_ROLE_NAME]: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ [Role.PASSWORD]: SWITCHABLE_AUTH_SERVICE_NAME, ++ [Role.SMARTCARD]: SWITCHABLE_AUTH_SERVICE_NAME, ++ [Role.PASSKEY]: SWITCHABLE_AUTH_SERVICE_NAME, ++ [Role.WEB_LOGIN]: SWITCHABLE_AUTH_SERVICE_NAME, + }; + + static { +@@ -52,7 +53,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (serviceName !== this._selectedMechanism?.serviceName) + return; + +- if (this._selectedMechanism.role === Constants.SMARTCARD_ROLE_NAME) { ++ if (this._selectedMechanism.role === Role.SMARTCARD) { + const certificates = this._selectedMechanism.certificates; + const cert = certificates.find(c => c.keyId === key); + this._selectedSmartcard = cert; +@@ -68,7 +69,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (serviceName !== this._selectedMechanism?.serviceName) + return; + +- if (this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && ++ if (this._selectedMechanism.role === Role.PASSWORD && + this._resettingPassword) { + this._userVerifier.call_answer_query(serviceName, + answer, +@@ -78,12 +79,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + let response; + switch (this._selectedMechanism.role) { +- case Constants.PASSWORD_ROLE_NAME: +- case Constants.SMARTCARD_ROLE_NAME: ++ case Role.PASSWORD: ++ case Role.SMARTCARD: + response = this._formatResponse(answer); + this._sendResponse(response); + break; +- case Constants.PASSKEY_ROLE_NAME: ++ case Role.PASSKEY: + response = this._formatResponse(answer); + this._sendResponse(response); + +@@ -97,16 +98,16 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + _handleSelectMechanism() { + switch (this._selectedMechanism?.role) { +- case Constants.PASSWORD_ROLE_NAME: ++ case Role.PASSWORD: + this._startPasswordLogin(); + break; +- case Constants.SMARTCARD_ROLE_NAME: ++ case Role.SMARTCARD: + this._startSmartcardLogin(); + break; +- case Constants.PASSKEY_ROLE_NAME: ++ case Role.PASSKEY: + this._startFido2TokenLogin(); + break; +- case Constants.WEB_LOGIN_ROLE_NAME: ++ case Role.WEB_LOGIN: + this._startWebLogin(); + break; + } +@@ -146,7 +147,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _handleOnCustomJSONRequest(serviceName, _protocol, _version, json) { +- if (serviceName !== Constants.SWITCHABLE_AUTH_SERVICE_NAME) ++ if (serviceName !== SWITCHABLE_AUTH_SERVICE_NAME) + return; + + let requestObject; +@@ -174,7 +175,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + _handleUpdateEnabledMechanisms() { + this._enabledMechanisms.push(...Object.keys(this._mechanisms) + .map(id => ({ +- serviceName: Constants.SWITCHABLE_AUTH_SERVICE_NAME, ++ serviceName: SWITCHABLE_AUTH_SERVICE_NAME, + id, + ...this._mechanisms[id], + })) +@@ -198,7 +199,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._clearWebLoginTimeout(); + + const webLoginMechanism = this._enabledMechanisms +- .find(m => m.role === Constants.WEB_LOGIN_ROLE_NAME); ++ .find(m => m.role === Role.WEB_LOGIN); + if (!webLoginMechanism) + return; + +@@ -208,7 +209,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + this._webLoginTimeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, + timeout, () => { +- if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) ++ if (this._selectedMechanism?.role !== Role.WEB_LOGIN) + webLoginMechanism.needsRefresh = true; + else + this.emit('reset', {softReset: true}); +@@ -224,7 +225,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + // sssd can't inform about expired password from JSON so it's needed + // to check the info message and handle the reset using the old flow + if (serviceName === this._selectedMechanism?.serviceName && +- this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && ++ this._selectedMechanism.role === Role.PASSWORD && + info.includes('Password expired. Change your password now')) + this._resettingPassword = true; + +@@ -261,7 +262,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + return; + + if (serviceName === this._selectedMechanism?.serviceName && +- this._selectedMechanism.role === Constants.PASSWORD_ROLE_NAME && ++ this._selectedMechanism.role === Role.PASSWORD && + this._resettingPassword) { + this.emit('ask-question', { + serviceName, +@@ -283,7 +284,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _handleCanStartService(serviceName) { +- return serviceName === Constants.SWITCHABLE_AUTH_SERVICE_NAME && ++ return serviceName === SWITCHABLE_AUTH_SERVICE_NAME && + this._mechanismsStatus === MechanismsStatus.WAITING; + } + +@@ -292,20 +293,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + let response; + switch (role) { +- case Constants.PASSWORD_ROLE_NAME: { ++ case Role.PASSWORD: { + response = {password: answer}; + break; + } +- case Constants.SMARTCARD_ROLE_NAME: { ++ case Role.SMARTCARD: { + const {tokenName, moduleName, keyId, label} = this._selectedSmartcard; + response = {pin: answer, tokenName, moduleName, keyId, label}; + break; + } +- case Constants.PASSKEY_ROLE_NAME: { ++ case Role.PASSKEY: { + response = {pin: answer, kerberos, cryptoChallenge}; + break; + } +- case Constants.WEB_LOGIN_ROLE_NAME: { ++ case Role.WEB_LOGIN: { + response = {}; + break; + } +@@ -456,7 +457,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _webLoginDone() { +- if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) ++ if (this._selectedMechanism?.role !== Role.WEB_LOGIN) + return; + + const response = this._formatResponse(); +@@ -491,7 +492,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + _onSmartcardChanged() { + if (!this._selectedMechanism || +- !this._enabledMechanisms.some(({role}) => role === Constants.SMARTCARD_ROLE_NAME)) ++ !this._enabledMechanisms.some(({role}) => role === Role.SMARTCARD)) + return; + + this.emit('reset', {softReset: true, reuseEntryText: true}); +@@ -499,7 +500,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + _onFido2TokenChanged() { + if (!this._selectedMechanism || +- !this._enabledMechanisms.some(({role}) => role === Constants.PASSKEY_ROLE_NAME)) ++ !this._enabledMechanisms.some(({role}) => role === Role.PASSKEY)) + return; + + this.emit('reset', {softReset: true, reuseEntryText: true}); +diff --git a/js/gdm/constants.js b/js/gdm/constants.js +deleted file mode 100644 +index a829d9866..000000000 +--- a/js/gdm/constants.js ++++ /dev/null +@@ -1,10 +0,0 @@ +-export const PASSWORD_ROLE_NAME = 'password'; +-export const SMARTCARD_ROLE_NAME = 'smartcard'; +-export const FINGERPRINT_ROLE_NAME = 'fingerprint'; +-export const PASSKEY_ROLE_NAME = 'passkey'; +-export const WEB_LOGIN_ROLE_NAME = 'eidp'; +- +-export const PASSWORD_SERVICE_NAME = 'gdm-password'; +-export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; +-export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; +-export const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 5cc77a26c..7fcb8df32 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -4,7 +4,6 @@ import GLib from 'gi://GLib'; + import * as Signals from '../misc/signals.js'; + + import * as Batch from './batch.js'; +-import * as Constants from './constants.js'; + import * as Main from '../ui/main.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Params from '../misc/params.js'; +@@ -271,9 +270,9 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._showMessageResolver = resolve; + + this.emit('show-message', { +- serviceName: message.serviceName, + message: message.text, + type: message.type, ++ shouldWiggle: message.wiggle, + showMessageResolver: this._showMessageResolver, + }); + +@@ -298,14 +297,20 @@ export class ShellUserVerifier extends Signals.EventEmitter { + GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout'); + } + +- _queueMessage({serviceName, message, messageType}) { ++ _queueMessage({serviceName, message, messageType, wiggle}) { + const interval = this._getIntervalForMessage(message); + +- this._messageQueue.push({serviceName, text: message, type: messageType, interval}); ++ this._messageQueue.push({ ++ serviceName, ++ text: message, ++ type: messageType, ++ wiggle, ++ interval, ++ }); + this._queueMessageTimeout(); + } + +- _queuePriorityMessage({serviceName, message, messageType}) { ++ _queuePriorityMessage({serviceName, message, messageType, wiggle}) { + const newQueue = this._messageQueue.filter(m => { + if (m.serviceName !== serviceName || m.type >= messageType) + return m.text !== message; +@@ -316,7 +321,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._clearMessageQueue(); + + this._messageQueue = newQueue; +- this._queueMessage({serviceName, message, messageType}); ++ this._queueMessage({serviceName, message, messageType, wiggle}); + } + + _clearMessageQueue() { +@@ -390,10 +395,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { + return proxies; + } + +- serviceIsFingerprint(serviceName) { +- return serviceName === Constants.FINGERPRINT_SERVICE_NAME; +- } +- + _onSettingsChanged() { + this._updateAuthServices(); + } +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 7d5f4b039..0359f6028 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -8,7 +8,6 @@ + gdm/authServicesLegacy.js + gdm/authServicesSSSDSwitchable.js + gdm/batch.js +- gdm/constants.js + gdm/credentialManager.js + gdm/fingerprintManager.js + gdm/loginDialog.js +-- +2.55.0 + + +From 4184d4760cedd2ec321969964b525a785ce46a76 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Wed, 11 Sep 2024 02:09:57 +0200 +Subject: [PATCH 63/72] gdm: Bind cancel button visibility to + verification-status + +It's depending on it, and the status may change during the execution so +bind the two values properly. + +Do the same with promptStep. + +Part-of: +--- + js/gdm/authPrompt.js | 39 ++++++++++++++++++++++----------------- + js/gdm/loginDialog.js | 4 ++-- + 2 files changed, 24 insertions(+), 19 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 0ba1a5de6..9e27a1250 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -55,6 +55,16 @@ export const AuthPrompt = GObject.registerClass({ + 'verification-complete': {}, + 'loading': {param_types: [GObject.TYPE_BOOLEAN]}, + }, ++ Properties: { ++ 'verification-status': GObject.ParamSpec.uint( ++ 'verification-status', 'verification-status', 'verification-status', ++ GObject.ParamFlags.READWRITE, ++ AuthPromptStatus.NOT_VERIFYING, AuthPromptStatus.VERIFICATION_IN_PROGRESS, 0), ++ 'prompt-step': GObject.ParamSpec.uint( ++ 'prompt-step', 'prompt-step', 'prompt-step', ++ GObject.ParamFlags.READWRITE, ++ 0, GLib.MAXUINT32, 0), ++ }, + }, class AuthPrompt extends St.BoxLayout { + _init(gdmClient, mode) { + super._init({ +@@ -71,7 +81,8 @@ export const AuthPrompt = GObject.registerClass({ + this._mode = mode; + this._defaultButtonWellActor = null; + this._cancelledRetries = 0; +- this._promptStep = 0; ++ ++ this.connect('notify::prompt-step', () => this._updateCancelButton()); + + this._idleMonitor = global.backend.get_core_idle_monitor(); + +@@ -327,7 +338,7 @@ export const AuthPrompt = GObject.registerClass({ + if (this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN) + return; + +- const cancelVisible = this._promptStep > 1; ++ const cancelVisible = this.promptStep > 1; + this.cancelButton.opacity = cancelVisible ? 255 : 0; + this.cancelButton.reactive = cancelVisible; + } +@@ -420,8 +431,7 @@ export const AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; +- this._updateCancelButton(); ++ this.promptStep++; + + const preemptiveAnswer = this._preemptiveAnswer; + this._clearPreemptiveState(); +@@ -448,8 +458,7 @@ export const AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; +- this._updateCancelButton(); ++ this.promptStep++; + + this._clearPreemptiveState(); + +@@ -486,7 +495,7 @@ export const AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; ++ this.promptStep++; + + this._clearPreemptiveState(); + +@@ -507,8 +516,7 @@ export const AuthPrompt = GObject.registerClass({ + this.clear(); + + this._queryingService = serviceName; +- this._promptStep++; +- this._updateCancelButton(); ++ this.promptStep++; + + this._webLoginParams = {message, url, code, buttons}; + +@@ -833,12 +841,10 @@ export const AuthPrompt = GObject.registerClass({ + if (invalidStatus.includes(this.verificationStatus)) + return false; + +- const oldPromptStep = this._promptStep; +- this._promptStep = 0; ++ const oldPromptStep = this.promptStep; ++ this.promptStep = 0; + if (!this._userVerifier.selectMechanism(mechanism)) +- this._promptStep = oldPromptStep; +- +- this._updateCancelButton(); ++ this.promptStep = oldPromptStep; + + return true; + } +@@ -853,8 +859,7 @@ export const AuthPrompt = GObject.registerClass({ + this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; + if (oldStatus !== AuthPromptStatus.VERIFICATION_IN_PROGRESS) + this._preemptiveAnswer = null; +- this._promptStep = 0; +- this._updateCancelButton(); ++ this.promptStep = 0; + + if (this._preemptiveAnswerWatchId) + this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); +@@ -948,7 +953,7 @@ export const AuthPrompt = GObject.registerClass({ + return; + + // If we're in a multi-step flow (step > 1), go back to step 1 instead of full reset +- if (this._promptStep > 1) { ++ if (this.promptStep > 1) { + this.reset({softReset: true}); + return; + } +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 638fb3c54..8f51ed59f 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -503,6 +503,8 @@ export const LoginDialog = GObject.registerClass({ + this._authPrompt.connect('verification-complete', this._onVerificationComplete.bind(this)); + this._authPrompt.connect('loading', this._onLoading.bind(this)); + this._authPrompt.connect('mechanisms-changed', this._onMechanismsChanged.bind(this)); ++ this._authPrompt.connectObject('notify::verification-status', ++ () => this._updateCancelButton(), this); + this._authPrompt.hide(); + this.add_child(this._authPrompt); + +@@ -1182,9 +1184,7 @@ export const LoginDialog = GObject.registerClass({ + this._updateSessions(); + this._authPrompt.clear(); + this._authPrompt.begin({userName: answer}); +- this._updateCancelButton(); + }); +- this._updateCancelButton(); + + this._authMenuButton.updateVisibility({visible: false}); + this._showPrompt(); +-- +2.55.0 + + +From 62c4c23eb631f4a837b3ee9b7d73e47653f088ff Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Sun, 26 Jul 2026 07:44:07 +0200 +Subject: [PATCH 64/72] gdm/authServices: Make it possible for a service to + handle the cancel button + +A service may behave differently when a cancel button is pressed, so let +the service handle it + +Part-of: +--- + js/gdm/authPrompt.js | 11 +++++++++-- + js/gdm/authServices.js | 2 ++ + js/gdm/util.js | 6 ++++++ + 3 files changed, 17 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 9e27a1250..95359354a 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -162,7 +162,7 @@ export const AuthPrompt = GObject.registerClass({ + + on_key_press_event(event) { + if (event.get_key_symbol() === Clutter.KEY_Escape) { +- this.cancel(); ++ this._handleCancel(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; +@@ -185,7 +185,7 @@ export const AuthPrompt = GObject.registerClass({ + y_align: Clutter.ActorAlign.CENTER, + icon_name: 'go-previous-symbolic', + }); +- this.cancelButton.connect('clicked', () => this.cancel()); ++ this.cancelButton.connect('clicked', () => this._handleCancel()); + this._updateCancelButton(); + this._mainBox.add_child(this.cancelButton); + +@@ -948,6 +948,13 @@ export const AuthPrompt = GObject.registerClass({ + }); + } + ++ _handleCancel() { ++ if (this._userVerifier.cancelRequested()) ++ return; ++ ++ this.cancel(); ++ } ++ + cancel() { + if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED) + return; +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 3b58f4957..a5adea6a1 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -219,6 +219,8 @@ export class AuthServices extends GObject.Object { + this._handleCancel(); + } + ++ cancelRequested() {} ++ + destroy() { + this.reset(); + this.clear(); +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 7fcb8df32..b984617f7 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -168,6 +168,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this.clear(); + } + ++ cancelRequested() { ++ const {selectedMechanism} = this; ++ return this._authServices.find(s => ++ s.selectedMechanism === selectedMechanism)?.cancelRequested() ?? false; ++ } ++ + cancel() { + this._authServices.forEach(s => s.cancel()); + +-- +2.55.0 + + +From 454c759840aa7830a8f630f4fbf16ff0f1f8cc44 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Sun, 26 Jul 2026 07:45:12 +0200 +Subject: [PATCH 65/72] gdm/authServicesSSSDSwitchable: Implement intelligent + cancel handling + +Override cancelRequested() to handle Escape/cancel based on the current +prompt state. Track the active prompt with a PromptStatus enum set at +each signal emission, enabling context-aware cancel behavior: stepping +back to a previous prompt when appropriate (without SSSD round-trips), +soft reset when needed, or falling back to the default hard reset. + +Co-authored-by: Joan Torres Lopez +Part-of: +--- + js/gdm/authPrompt.js | 18 ++--- + js/gdm/authServicesSSSDSwitchable.js | 111 +++++++++++++++++++++++++-- + 2 files changed, 113 insertions(+), 16 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 95359354a..fd9bc642b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -313,14 +313,7 @@ export const AuthPrompt = GObject.registerClass({ + this._mainBox.add_child(this._authButton); + + this._webLoginDialog = new WebLogin.WebLoginDialog(); +- this._webLoginDialog.connect('cancel', () => { +- if (this._webLoginDialog.isLoading) { +- this.reset({softReset: true}); +- } else { +- this._closeWebLoginDialog(); +- this.updateSensitivity({sensitive: true}); +- } +- }); ++ this._webLoginDialog.connect('cancel', () => this._handleCancel()); + this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); + this.add_child(this._webLoginDialog); + +@@ -949,8 +942,15 @@ export const AuthPrompt = GObject.registerClass({ + } + + _handleCancel() { +- if (this._userVerifier.cancelRequested()) ++ if (this._userVerifier.cancelRequested()) { ++ // We substract 2 because on cancel we'll receive a new prompt signal ++ // which increments promptStep, so that ends with a result of going ++ // back one step. ++ this.promptStep -= 2; ++ if (this.promptStep < 0) ++ this.promptStep = 0; + return; ++ } + + this.cancel(); + } +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index cb5a62f05..439cf6a77 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -17,6 +17,20 @@ const MechanismsStatus = { + FOUND: 2, + }; + ++const PromptStatus = { ++ NONE: 0, ++ PASSWORD_PROMPT: 1, ++ PASSWORD_WAITING: 2, ++ CERT_LIST_PROMPT: 3, ++ PIN_PROMPT: 4, ++ PIN_WAITING: 5, ++ INSERT_KEY_PROMPT: 6, ++ TOUCH_PROMPT: 7, ++ WEB_LOGIN_INTRO_PROMPT: 8, ++ WEB_LOGIN_DIALOG_PROMPT: 9, ++ WEB_LOGIN_DIALOG_WAITING: 10, ++}; ++ + export class AuthServicesSSSDSwitchable extends AuthServices { + static SupportedRoles = [ + Role.PASSWORD, +@@ -57,6 +71,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + const certificates = this._selectedMechanism.certificates; + const cert = certificates.find(c => c.keyId === key); + this._selectedSmartcard = cert; ++ this._promptStatus = PromptStatus.PIN_PROMPT; + this.emit('ask-question', { + serviceName, + question: cert.pinPrompt, +@@ -80,14 +95,20 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + let response; + switch (this._selectedMechanism.role) { + case Role.PASSWORD: ++ response = this._formatResponse(answer); ++ this._sendResponse(response); ++ this._promptStatus = PromptStatus.PASSWORD_WAITING; ++ break; + case Role.SMARTCARD: + response = this._formatResponse(answer); + this._sendResponse(response); ++ this._promptStatus = PromptStatus.PIN_WAITING; + break; + case Role.PASSKEY: + response = this._formatResponse(answer); + this._sendResponse(response); + ++ this._promptStatus = PromptStatus.TOUCH_PROMPT; + this.emit('show-choice-list', { + serviceName, + promptMessage: this._selectedMechanism.touchInstruction, +@@ -97,6 +118,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + _handleSelectMechanism() { ++ this._promptStatus = PromptStatus.NONE; ++ + switch (this._selectedMechanism?.role) { + case Role.PASSWORD: + this._startPasswordLogin(); +@@ -134,6 +157,68 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + } + ++ cancelRequested() { ++ if (!this._selectedMechanism) ++ return false; ++ ++ switch (this._selectedMechanism.role) { ++ case Role.PASSWORD: ++ // Waiting for authentication response: soft reset ++ if (this._promptStatus === PromptStatus.PASSWORD_WAITING) { ++ this.emit('reset', {softReset: true}); ++ return true; ++ } ++ // At password prompt: hard reset ++ return false; ++ ++ case Role.SMARTCARD: ++ // Waiting for PIN verification response: soft reset ++ if (this._promptStatus === PromptStatus.PIN_WAITING) { ++ this.emit('reset', {softReset: true}); ++ return true; ++ } ++ // At PIN entry after selecting from multiple certificates: ++ // step back to the certificate list ++ if (this._promptStatus === PromptStatus.PIN_PROMPT && ++ this._selectedMechanism.certificates.length > 1) { ++ this._selectedSmartcard = null; ++ this._startSmartcardLogin(); ++ return true; ++ } ++ // At certificate list or single-cert PIN: hard reset ++ return false; ++ ++ case Role.PASSKEY: ++ // At touch confirmation after PIN submission: soft reset ++ // back to PIN entry (PIN response was already sent to SSSD) ++ if (this._promptStatus === PromptStatus.TOUCH_PROMPT) { ++ this.emit('reset', {softReset: true}); ++ return true; ++ } ++ // At PIN entry or key insertion prompt: hard reset ++ return false; ++ ++ case Role.WEB_LOGIN: ++ // At login dialog after "Done" clicked: soft reset to retry ++ if (this._promptStatus === PromptStatus.WEB_LOGIN_DIALOG_WAITING) { ++ this.emit('reset', {softReset: true}); ++ return true; ++ } ++ // At login dialog with a login button available: ++ // step back to the login button ++ if (this._promptStatus === PromptStatus.WEB_LOGIN_DIALOG_PROMPT && ++ this._selectedMechanism.initPrompt) { ++ this._startWebLogin(); ++ return true; ++ } ++ // At login button or direct dialog (no initPrompt): hard reset ++ return false; ++ ++ default: ++ return false; ++ } ++ } ++ + _handleClear() { + this._mechanisms = null; + this._priorityList = null; +@@ -142,6 +227,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._selectedSmartcard = null; + + this._resettingPassword = false; ++ this._promptStatus = PromptStatus.NONE; + + this._clearWebLoginTimeout(); + } +@@ -346,6 +432,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + _startPasswordLogin() { + const {serviceName, prompt} = this._selectedMechanism; + ++ this._promptStatus = PromptStatus.PASSWORD_PROMPT; + this.emit('ask-question', {serviceName, question: prompt, secret: true}); + } + +@@ -354,6 +441,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + + if (certificates.length === 1) { + this._selectedSmartcard = certificates[0]; ++ this._promptStatus = PromptStatus.PIN_PROMPT; + this.emit('ask-question', { + serviceName, + question: certificates[0].pinPrompt, +@@ -370,6 +458,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + ? _('Insert Smartcard') + : _('Select Identity'); + ++ this._promptStatus = PromptStatus.CERT_LIST_PROMPT; + this.emit('show-choice-list', {serviceName, promptMessage, choiceList}); + } + +@@ -399,10 +488,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } = this._selectedMechanism; + + if (!keyConnected) { ++ this._promptStatus = PromptStatus.INSERT_KEY_PROMPT; + this.emit('show-choice-list', {serviceName, promptMessage: initInstruction}); + return; + } + ++ this._promptStatus = PromptStatus.PIN_PROMPT; + this.emit('ask-question', {serviceName, question: pinPrompt, secret: true}); + + if (pinAttempts <= 3 && pinAttempts > 0) { +@@ -437,15 +528,19 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + action: () => this._webLoginDone(), + }]; + +- const showWebLogin = () => this.emit('web-login', { +- serviceName, +- message: linkPrompt, +- url: uri, +- code, +- buttons, +- }); ++ const showWebLogin = () => { ++ this._promptStatus = PromptStatus.WEB_LOGIN_DIALOG_PROMPT; ++ this.emit('web-login', { ++ serviceName, ++ message: linkPrompt, ++ url: uri, ++ code, ++ buttons, ++ }); ++ }; + + if (initPrompt) { ++ this._promptStatus = PromptStatus.WEB_LOGIN_INTRO_PROMPT; + this.emit('show-button', { + serviceName, + label: initPrompt, +@@ -460,6 +555,8 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (this._selectedMechanism?.role !== Role.WEB_LOGIN) + return; + ++ this._promptStatus = PromptStatus.WEB_LOGIN_DIALOG_WAITING; ++ + const response = this._formatResponse(); + this._sendResponse(response); + +-- +2.55.0 + + +From e432b9cdbb757cedeba1d56aab1f8892e14e11cf Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Sun, 26 Jul 2026 07:56:47 +0200 +Subject: [PATCH 66/72] gdm/authServicesSSSDSwitchable: Disable the mechanisms + on service unavailable + +Part-of: +--- + js/gdm/authServicesSSSDSwitchable.js | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 439cf6a77..7286ec78c 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -369,6 +369,14 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._verificationFailed(serviceName, true); + } + ++ _handleOnServiceUnavailable(serviceName) { ++ if (serviceName !== SWITCHABLE_AUTH_SERVICE_NAME) ++ return; ++ ++ this._mechanismsStatus = MechanismsStatus.NOT_FOUND; ++ this.emit('mechanisms-changed'); ++ } ++ + _handleCanStartService(serviceName) { + return serviceName === SWITCHABLE_AUTH_SERVICE_NAME && + this._mechanismsStatus === MechanismsStatus.WAITING; +-- +2.55.0 + + +From 7bbf0905bdce299ea27f5ec287220b877e840450 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Sun, 26 Jul 2026 07:58:52 +0200 +Subject: [PATCH 67/72] gdm/authServicesSSSDSwitchable: Handle errors setting + the not found state + +Part-of: +--- + js/gdm/authServicesSSSDSwitchable.js | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 7286ec78c..3b462ad44 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -63,6 +63,18 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this._mechanismsStatus = MechanismsStatus.WAITING; + } + ++ async beginVerification(userName, userVerifierProxies) { ++ try { ++ await super.beginVerification(userName, userVerifierProxies); ++ } catch (e) { ++ if (e.serviceName !== SWITCHABLE_AUTH_SERVICE_NAME) ++ throw e; ++ ++ this._mechanismsStatus = MechanismsStatus.NOT_FOUND; ++ this.emit('mechanisms-changed'); ++ } ++ } ++ + _handleSelectChoice(serviceName, key) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; +-- +2.55.0 + + +From e3fa3b0407c85856334938d8008fb5c16bf4bdf1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Sun, 26 Jul 2026 08:02:46 +0200 +Subject: [PATCH 68/72] gdm/util: Allow an auth service to have multiple + mechanisms per role + +Each mechanism could actually define the role multiple times, but we +still want just one authService to provide a specific role + +Part-of: +--- + js/gdm/util.js | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/util.js b/js/gdm/util.js +index b984617f7..5c3dc4d37 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -497,8 +497,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + // keeping only the first mechanism per role + const seenRoles = new Set(); + const mechanisms = this._authServices +- .flatMap(s => s.enabledMechanisms ?? []) +- .filter(m => !seenRoles.has(m.role) && seenRoles.add(m.role)); ++ .flatMap(authServices => { ++ const serviceMechanisms = authServices.enabledMechanisms ?? []; ++ const visibleMechanisms = serviceMechanisms.filter(m => ++ !seenRoles.has(m.role)); ++ ++ serviceMechanisms.forEach(m => seenRoles.add(m.role)); ++ return visibleMechanisms; ++ }); + + const selectedMechanism = this.selectedMechanism ?? + mechanisms.find(m => m.selectable) ?? +-- +2.55.0 + + +From e9df31ea4600d759444e4fb5ada35936e375f690 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Mon, 27 Jul 2026 03:04:24 +0200 +Subject: [PATCH 69/72] gdm: Rename util into userVerifier + +We've been abusing of util.js long enough, it's a too generic name for +something that for many years is actually the core of the shell user +verifier class. + +So let's rename it as such first. +We do it without dropping the actual util.js content, to trick git to +generate a proper diff, making it easier to review, track changes and +blame them in future + +Part-of: +--- + js/gdm/authPrompt.js | 10 +++++----- + js/gdm/authServices.js | 8 ++++---- + js/gdm/authServicesLegacy.js | 10 +++++----- + js/gdm/authServicesSSSDSwitchable.js | 8 ++++---- + js/gdm/loginDialog.js | 2 +- + js/gdm/{util.js => userVerifier.js} | 0 + js/js-resources.gresource.xml | 2 +- + po/POTFILES.in | 1 + + 8 files changed, 21 insertions(+), 20 deletions(-) + rename js/gdm/{util.js => userVerifier.js} (100%) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index fd9bc642b..8143c2d5b 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -9,10 +9,10 @@ import St from 'gi://St'; + import * as Animation from '../ui/animation.js'; + import * as AuthList from './authList.js'; + import * as Batch from './batch.js'; +-import * as GdmUtil from './util.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Params from '../misc/params.js'; + import * as ShellEntry from '../ui/shellEntry.js'; ++import * as UserVerifier from './userVerifier.js'; + import * as UserWidget from '../ui/userWidget.js'; + import * as WebLogin from './webLogin.js'; + import {wiggle} from '../misc/animationUtils.js'; +@@ -143,7 +143,7 @@ export const AuthPrompt = GObject.registerClass({ + } + + _createUserVerifier(gdmClient, params) { +- return new GdmUtil.ShellUserVerifier(gdmClient, params); ++ return new UserVerifier.ShellUserVerifier(gdmClient, params); + } + + _onDestroy() { +@@ -467,7 +467,7 @@ export const AuthPrompt = GObject.registerClass({ + // If we're showing a message and no auth widget is currently visible, + // show the entry area to allow getting a preemptive answer + if (message && +- type < GdmUtil.MessageType.ERROR && ++ type < UserVerifier.MessageType.ERROR && + !this._entryArea.visible && + !this._authList.visible && + !this._authButton.visible && +@@ -748,12 +748,12 @@ export const AuthPrompt = GObject.registerClass({ + } + + setMessage(message, type) { +- if (type === GdmUtil.MessageType.ERROR) ++ if (type === UserVerifier.MessageType.ERROR) + this._message.add_style_class_name('login-dialog-message-warning'); + else + this._message.remove_style_class_name('login-dialog-message-warning'); + +- if (type === GdmUtil.MessageType.HINT) ++ if (type === UserVerifier.MessageType.HINT) + this._message.add_style_class_name('login-dialog-message-hint'); + else + this._message.remove_style_class_name('login-dialog-message-hint'); +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index a5adea6a1..aa379ca83 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -1,7 +1,7 @@ + import * as Params from '../misc/params.js'; + import {registerDestroyableType} from '../misc/signalTracker.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +-import * as Util from './util.js'; ++import {InitError, MessageType} from './userVerifier.js'; + import Gdm from 'gi://Gdm'; + import GLib from 'gi://GLib'; + import Gio from 'gi://Gio'; +@@ -366,7 +366,7 @@ export class AuthServices extends GObject.Object { + _onConversationStopped(serviceName) { + this._activeServices.delete(serviceName); + +- this.emit('filter-messages', {serviceName, messageType: Util.MessageType.ERROR}); ++ this.emit('filter-messages', {serviceName, messageType: MessageType.ERROR}); + + this._handleOnConversationStopped(serviceName); + } +@@ -378,7 +378,7 @@ export class AuthServices extends GObject.Object { + this.emit('queue-message', { + serviceName, + message: errorMessage, +- messageType: Util.MessageType.ERROR, ++ messageType: MessageType.ERROR, + }); + } + +@@ -466,7 +466,7 @@ export class AuthServices extends GObject.Object { + 'org.gnome.DisplayManager.SessionWorker.Error.ServiceUnavailable') + this._unavailableServices.add(serviceName); + +- throw new Util.InitError(e, ++ throw new InitError(e, + this._userName + ? `Failed to start ${serviceName} verification for user` + : `Failed to start ${serviceName} verification`, +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 9707e95dd..4738131cf 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -6,9 +6,9 @@ import {FingerprintReaderType} from './fingerprintManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as SmartcardManager from './smartcardManager.js'; + import * as OVirt from './oVirt.js'; +-import * as Util from './util.js'; + import * as Vmware from './vmware.js'; + import {AuthServices, Role, RoleProperties} from './authServices.js'; ++import {MessageType} from './userVerifier.js'; + + const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15; + const FINGERPRINT_READY_TIMEOUT_MS = 500; +@@ -190,7 +190,7 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('queue-message', { + serviceName, + message: info, +- messageType: Util.MessageType.INFO, ++ messageType: MessageType.INFO, + }); + } else if (serviceName === FINGERPRINT_SERVICE_NAME && + this._enabledMechanisms.some(m => m.serviceName === serviceName)) { +@@ -206,7 +206,7 @@ export class AuthServicesLegacy extends AuthServices { + // Translators: this message is shown below the password entry field + // to indicate the user can place their finger on the fingerprint reader instead + : _('(or place finger on reader)'), +- messageType: Util.MessageType.HINT, ++ messageType: MessageType.HINT, + }); + } + } +@@ -218,7 +218,7 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('queue-priority-message', { + serviceName, + message: problem, +- messageType: Util.MessageType.ERROR, ++ messageType: MessageType.ERROR, + wiggle: serviceName === FINGERPRINT_SERVICE_NAME, + }); + } +@@ -321,7 +321,7 @@ export class AuthServicesLegacy extends AuthServices { + this.emit('queue-message', { + serviceName, + message: errorMessage, +- messageType: Util.MessageType.ERROR, ++ messageType: MessageType.ERROR, + wiggle: serviceName === FINGERPRINT_SERVICE_NAME, + }); + } +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 3b462ad44..a46b1d1d4 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -4,8 +4,8 @@ import GObject from 'gi://GObject'; + import * as Fido2TokenManager from './fido2TokenManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as SmartcardManager from './smartcardManager.js'; +-import * as Util from './util.js'; + import {AuthServices, Role} from './authServices.js'; ++import {MessageType} from './userVerifier.js'; + + const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; + +@@ -331,7 +331,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('queue-message', { + serviceName, + message: info, +- messageType: Util.MessageType.INFO, ++ messageType: MessageType.INFO, + }); + } + } +@@ -344,7 +344,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('queue-priority-message', { + serviceName, + message: problem, +- messageType: Util.MessageType.ERROR, ++ messageType: MessageType.ERROR, + }); + } + } +@@ -521,7 +521,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('queue-message', { + serviceName, + message, +- messageType: Util.MessageType.INFO, ++ messageType: MessageType.INFO, + }); + } + } +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 8f51ed59f..3b5bf7c00 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -32,7 +32,7 @@ import * as AuthPrompt from './authPrompt.js'; + import * as Batch from './batch.js'; + import * as BoxPointer from '../ui/boxpointer.js'; + import * as CtrlAltTab from '../ui/ctrlAltTab.js'; +-import * as GdmUtil from './util.js'; ++import * as GdmUtil from './userVerifier.js'; + import * as Layout from '../ui/layout.js'; + import * as LoginManager from '../misc/loginManager.js'; + import * as Main from '../ui/main.js'; +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +new file mode 100644 +index 0000000..5c3dc4d +--- /dev/null ++++ b/js/gdm/userVerifier.js +@@ -0,0 +1,540 @@ ++import Clutter from 'gi://Clutter'; ++import Gio from 'gi://Gio'; ++import GLib from 'gi://GLib'; ++import * as Signals from '../misc/signals.js'; ++ ++import * as Batch from './batch.js'; ++import * as Main from '../ui/main.js'; ++import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; ++import * as Params from '../misc/params.js'; ++import {AuthServicesLegacy} from './authServicesLegacy.js'; ++import {AuthServicesSSSDSwitchable} from './authServicesSSSDSwitchable.js'; ++ ++const CLONE_FADE_ANIMATION_TIME = 250; ++ ++export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; ++export const BANNER_MESSAGE_KEY = 'banner-message-enable'; ++export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; ++export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; ++export const BANNER_MESSAGE_PATH_KEY = 'banner-message-path'; ++export const ALLOWED_FAILURES_KEY = 'allowed-failures'; ++ ++export const LOGO_KEY = 'logo'; ++export const DISABLE_USER_LIST_KEY = 'disable-user-list'; ++ ++// Give user 48ms to read each character of a PAM message ++// or 2 seconds, whichever is longer ++const USER_READ_TIME = 48; ++const USER_READ_TIME_MIN = 2000; ++const MESSAGE_TIME_MULTIPLIER = (() => { ++ const value = Number.parseFloat(GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER')); ++ return Number.isFinite(value) && value > 0 ? value : 1; ++})(); ++ ++/** ++ * Keep messages in order by priority ++ * ++ * @enum {number} ++ */ ++export const MessageType = { ++ NONE: 0, ++ HINT: 1, ++ INFO: 2, ++ ERROR: 3, ++}; ++ ++// Priority-ordered: earlier entries take precedence for shared roles. ++// Each authServices claims the roles it supports; unsupported roles ++// cascade to the next authServices in the array. ++const AuthServicesClasses = [ ++ AuthServicesSSSDSwitchable, ++ AuthServicesLegacy, ++]; ++ ++/** ++ * Error thrown during the authentication initialization phase. ++ * ++ * This error is emitted when requesting user verifier proxies or starting ++ * a service via beginVerification fails. It wraps the underlying error ++ * and provides context about which service failed. ++ */ ++export class InitError extends Error { ++ constructor(error, message, serviceName) { ++ super(message, {cause: error}); ++ this.serviceName = serviceName; ++ } ++} ++ ++/** ++ * @param {Clutter.Actor} actor ++ */ ++export function cloneAndFadeOutActor(actor) { ++ // Immediately hide actor so its sibling can have its space ++ // and position, but leave a non-reactive clone on-screen, ++ // so from the user's point of view it smoothly fades away ++ // and reveals its sibling. ++ actor.hide(); ++ ++ const clone = new Clutter.Clone({ ++ source: actor, ++ reactive: false, ++ }); ++ ++ Main.uiGroup.add_child(clone); ++ ++ let [x, y] = actor.get_transformed_position(); ++ clone.set_position(x, y); ++ ++ let hold = new Batch.Hold(); ++ clone.ease({ ++ opacity: 0, ++ duration: CLONE_FADE_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => { ++ clone.destroy(); ++ hold.release(); ++ }, ++ }); ++ return hold; ++} ++ ++export class ShellUserVerifier extends Signals.EventEmitter { ++ constructor(client, params) { ++ super(); ++ params = Params.parse(params, {reauthenticationOnly: false}); ++ this._reauthOnly = params.reauthenticationOnly; ++ ++ this._client = client; ++ this._cancellable = null; ++ this._authServices = []; ++ ++ this._messageQueue = []; ++ this._messageQueueTimeoutId = 0; ++ ++ this._settings = new Gio.Settings({schema_id: LOGIN_SCREEN_SCHEMA}); ++ this._settings.connect('changed', () => this._onSettingsChanged()); ++ this._updateAuthServices(); ++ } ++ ++ get hasPendingMessages() { ++ return !!this._messageQueue.length; ++ } ++ ++ get allowedFailures() { ++ return this._settings.get_int(ALLOWED_FAILURES_KEY); ++ } ++ ++ get currentMessage() { ++ return this._messageQueue ? this._messageQueue[0] : null; ++ } ++ ++ async begin(userName, hold) { ++ this._cancellable?.cancel(); ++ this._cancellable = new Gio.Cancellable(); ++ ++ try { ++ const proxies = await this._getUserVerifierProxies(userName, this._cancellable); ++ this._setUserVerifier(proxies.userVerifier); ++ for (const s of this._authServices) { ++ // eslint-disable-next-line no-await-in-loop ++ await s.beginVerification(userName, proxies); ++ } ++ } catch (e) { ++ if (e instanceof InitError) ++ this._reportInitError(e); ++ else ++ logErrorUnlessCancelled(e); ++ } ++ ++ hold?.release(); ++ } ++ ++ selectMechanism(mechanism) { ++ // Every authServices needs to update its selected mechanism ++ return this._authServices ++ .map(s => s.selectMechanism(mechanism)) ++ .some(Boolean); ++ } ++ ++ needsUsername() { ++ return this._authServices.some(s => s.needsUsername()); ++ } ++ ++ reset() { ++ this._authServices.forEach(s => s.reset()); ++ ++ this._userVerifier?.call_cancel_sync(null); ++ ++ this.clear(); ++ } ++ ++ cancelRequested() { ++ const {selectedMechanism} = this; ++ return this._authServices.find(s => ++ s.selectedMechanism === selectedMechanism)?.cancelRequested() ?? false; ++ } ++ ++ cancel() { ++ this._authServices.forEach(s => s.cancel()); ++ ++ this._userVerifier?.call_cancel_sync(null); ++ ++ this.clear(); ++ } ++ ++ clear() { ++ this._authServices.forEach(s => s.clear()); ++ this._redistributeRoles(); ++ ++ this._clearMessageQueue(); ++ ++ this._cancellable?.cancel(); ++ this._cancellable = null; ++ ++ this._clearUserVerifier(); ++ } ++ ++ _setUserVerifier(userVerifier) { ++ this._clearUserVerifier(); ++ this._userVerifier = userVerifier; ++ this._userVerifier.get_connection().connectObject( ++ 'closed', () => this._clearUserVerifier(), ++ this); ++ } ++ ++ _clearUserVerifier() { ++ this._userVerifier?.get_connection().disconnectObject(this); ++ this._userVerifier = null; ++ } ++ ++ destroy() { ++ this._authServices.forEach(s => s.destroy()); ++ this._authServices = []; ++ ++ this.cancel(); ++ ++ this._settings.run_dispose(); ++ this._settings = null; ++ } ++ ++ selectChoice(serviceName, key) { ++ this._authServices.forEach(s => s.selectChoice(serviceName, key)); ++ } ++ ++ async answerQuery(serviceName, answer) { ++ // Wait for pending messages to be displayed before answering to ++ // ensure no messages get lost ++ await this._handlePendingMessages().catch(logErrorUnlessCancelled); ++ ++ this._authServices.forEach(s => s.answerQuery(serviceName, answer)); ++ } ++ ++ addCredentialManager(serviceName, credentialManager) { ++ this._authServices.forEach(s => s.addCredentialManager(serviceName, credentialManager)); ++ } ++ ++ removeCredentialManager(serviceName) { ++ this._authServices.forEach(s => s.removeCredentialManager(serviceName)); ++ } ++ ++ _getIntervalForMessage(message) { ++ if (!message) ++ return 0; ++ ++ // We probably could be smarter here ++ return Math.max(message.length * USER_READ_TIME, USER_READ_TIME_MIN) * ++ MESSAGE_TIME_MULTIPLIER; ++ } ++ ++ _finishMessageQueue() { ++ if (!this.hasPendingMessages) ++ return; ++ ++ this._messageQueue = []; ++ ++ this.emit('no-more-messages'); ++ } ++ ++ _serviceHasPendingMessages(serviceName) { ++ return this._messageQueue.some(m => m.serviceName === serviceName); ++ } ++ ++ _filterServiceMessages({serviceName, messageType}) { ++ // This function allows to remove queued messages for the @serviceName ++ // whose type has lower priority than @messageType, replacing them ++ // with a null message that will lead to clearing the prompt once done. ++ if (this._serviceHasPendingMessages(serviceName)) ++ this._queuePriorityMessage({serviceName, messageType}); ++ } ++ ++ async _queueMessageTimeout() { ++ if (this._messageQueueTimeoutId !== 0 || this._showMessageResolver) ++ return; ++ ++ const message = this.currentMessage; ++ const {promise, resolve} = Promise.withResolvers(); ++ this._showMessageResolver = resolve; ++ ++ this.emit('show-message', { ++ message: message.text, ++ type: message.type, ++ shouldWiggle: message.wiggle, ++ showMessageResolver: this._showMessageResolver, ++ }); ++ ++ await promise.catch(logError); ++ if (!this._showMessageResolver) ++ return; ++ this._showMessageResolver = null; ++ ++ this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, ++ message.interval, () => { ++ this._messageQueueTimeoutId = 0; ++ ++ if (this._messageQueue.length > 1) { ++ this._messageQueue.shift(); ++ this._queueMessageTimeout(); ++ } else { ++ this._finishMessageQueue(); ++ } ++ ++ return GLib.SOURCE_REMOVE; ++ }); ++ GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout'); ++ } ++ ++ _queueMessage({serviceName, message, messageType, wiggle}) { ++ const interval = this._getIntervalForMessage(message); ++ ++ this._messageQueue.push({ ++ serviceName, ++ text: message, ++ type: messageType, ++ wiggle, ++ interval, ++ }); ++ this._queueMessageTimeout(); ++ } ++ ++ _queuePriorityMessage({serviceName, message, messageType, wiggle}) { ++ const newQueue = this._messageQueue.filter(m => { ++ if (m.serviceName !== serviceName || m.type >= messageType) ++ return m.text !== message; ++ return false; ++ }); ++ ++ if (!newQueue.includes(this.currentMessage)) ++ this._clearMessageQueue(); ++ ++ this._messageQueue = newQueue; ++ this._queueMessage({serviceName, message, messageType, wiggle}); ++ } ++ ++ _clearMessageQueue() { ++ this._finishMessageQueue(); ++ ++ if (this._messageQueueTimeoutId !== 0) { ++ GLib.source_remove(this._messageQueueTimeoutId); ++ this._messageQueueTimeoutId = 0; ++ } ++ ++ if (this._showMessageResolver) { ++ this._showMessageResolver(); ++ this._showMessageResolver = null; ++ } ++ ++ this.emit('show-message', {type: MessageType.NONE}); ++ } ++ ++ _reportInitError(initError) { ++ const {cause, message, serviceName} = initError; ++ ++ logError(cause, message); ++ ++ this._queueMessage({ ++ serviceName, ++ message: _('Authentication error'), ++ messageType: MessageType.ERROR, ++ }); ++ this._verificationFailed({serviceName}); ++ } ++ ++ async _getUserVerifierProxies(userName, cancellable) { ++ const proxies = {}; ++ ++ if (userName) { ++ try { ++ proxies.userVerifier = await this._client.open_reauthentication_channel( ++ userName, cancellable); ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ throw e; ++ if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) && ++ !this._reauthOnly) { ++ // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there ++ // is no session to reauthenticate. Fall back to performing ++ // verification from this login session ++ return this._getUserVerifierProxies(null, cancellable); ++ } ++ throw new InitError(e, 'Failed to open reauthentication channel'); ++ } ++ } else { ++ try { ++ proxies.userVerifier = await this._client.get_user_verifier( ++ cancellable); ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) ++ throw e; ++ throw new InitError(e, 'Failed to obtain user verifier'); ++ } ++ } ++ ++ try { ++ if (this._client.get_user_verifier_choice_list) ++ proxies.userVerifierChoiceList = await this._client.get_user_verifier_choice_list(); ++ if (this._client.get_user_verifier_custom_json) ++ proxies.userVerifierCustomJSON = await this._client.get_user_verifier_custom_json(); ++ } catch (e) { ++ throw new InitError(e, 'Failed to obtain user verifier extensions'); ++ } ++ ++ return proxies; ++ } ++ ++ _onSettingsChanged() { ++ this._updateAuthServices(); ++ } ++ ++ _updateAuthServices() { ++ const enabledAuthServicesClasses = AuthServicesClasses ++ .filter(C => C.isEnabled(this._settings)); ++ ++ if (enabledAuthServicesClasses.length === this._enabledAuthServicesClasses?.length && ++ enabledAuthServicesClasses.every(c => this._enabledAuthServicesClasses.includes(c))) ++ return; ++ ++ this._enabledAuthServicesClasses = enabledAuthServicesClasses; ++ this._createAuthServices(); ++ } ++ ++ _createAuthServices() { ++ this._clearAuthServices(); ++ ++ const params = { ++ client: this._client, ++ allowedFailures: this.allowedFailures, ++ reauthOnly: this._reauthOnly, ++ settings: this._settings, ++ }; ++ ++ this._enabledAuthServicesClasses.forEach(AuthServicesClass => { ++ this._authServices.push(new AuthServicesClass(params)); ++ }); ++ ++ this._redistributeRoles(); ++ this._connectAuthServices(); ++ } ++ ++ _clearAuthServices() { ++ this._authServices.forEach(s => s.destroy()); ++ this._authServices = []; ++ } ++ ++ _connectAuthServices() { ++ this._authServices.forEach(authServices => { ++ authServices.connectObject( ++ 'ask-question', (_, args) => this.emit('ask-question', args), ++ 'queue-message', (_, args) => this._queueMessage(args), ++ 'queue-priority-message', (_, args) => this._queuePriorityMessage(args), ++ 'wait-pending-messages', (_, args) => this._waitPendingMessages(args), ++ 'filter-messages', (_, args) => this._filterServiceMessages(args), ++ 'verification-failed', (_, args) => this._verificationFailed(args), ++ 'verification-complete', () => this.emit('verification-complete'), ++ 'reset', (_, args) => this.emit('reset', args), ++ 'show-choice-list', (_, args) => this.emit('show-choice-list', args), ++ 'show-button', (_, args) => this.emit('show-button', args), ++ 'mechanisms-changed', () => this._onMechanismsChanged(), ++ 'web-login', (_, args) => this.emit('web-login', args), ++ this); ++ }); ++ } ++ ++ _verificationFailed({serviceName, canRetry}) { ++ this._filterServiceMessages({serviceName, messageType: MessageType.ERROR}); ++ this.emit('verification-failed', {serviceName, canRetry}); ++ } ++ ++ get selectedMechanism() { ++ return this._authServices ++ .find(s => s.selectedMechanism)?.selectedMechanism ?? null; ++ } ++ ++ _redistributeRoles() { ++ if (this._authServices.length < 2) ++ return; ++ ++ this._redistributingRoles = true; ++ ++ // Each authServices disables the roles supported by the one ++ // before it, cascading down the priority chain ++ const authServices = this._authServices; ++ for (let i = 1; i < authServices.length; i++) { ++ const prev = authServices[i - 1]; ++ const current = authServices[i]; ++ current.updateEnabledRoles({disableRoles: prev.supportedRoles}); ++ } ++ ++ this._redistributingRoles = false; ++ } ++ ++ _onMechanismsChanged() { ++ if (this._redistributingRoles) ++ return; ++ ++ this._redistributeRoles(); ++ ++ // Collect mechanisms from all authServices in priority order, ++ // keeping only the first mechanism per role ++ const seenRoles = new Set(); ++ const mechanisms = this._authServices ++ .flatMap(authServices => { ++ const serviceMechanisms = authServices.enabledMechanisms ?? []; ++ const visibleMechanisms = serviceMechanisms.filter(m => ++ !seenRoles.has(m.role)); ++ ++ serviceMechanisms.forEach(m => seenRoles.add(m.role)); ++ return visibleMechanisms; ++ }); ++ ++ const selectedMechanism = this.selectedMechanism ?? ++ mechanisms.find(m => m.selectable) ?? ++ {}; ++ ++ this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); ++ } ++ ++ async _waitPendingMessages({task}) { ++ try { ++ await this._handlePendingMessages(); ++ task.return_boolean(true); ++ } catch (e) { ++ task.return_error(e); ++ } ++ } ++ ++ _handlePendingMessages() { ++ if (!this.hasPendingMessages) ++ return Promise.resolve(); ++ ++ const cancellable = this._cancellable; ++ return new Promise((resolve, reject) => { ++ let signalId = this.connect('no-more-messages', () => { ++ this.disconnect(signalId); ++ if (cancellable.is_cancelled()) ++ reject(new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled')); ++ else ++ resolve(); ++ }); ++ }); ++ } ++} +diff --git a/js/gdm/util.js b/js/gdm/util.js +deleted file mode 100644 +index 5c3dc4d..0000000 +--- a/js/gdm/util.js ++++ /dev/null +@@ -1,540 +0,0 @@ +-import Clutter from 'gi://Clutter'; +-import Gio from 'gi://Gio'; +-import GLib from 'gi://GLib'; +-import * as Signals from '../misc/signals.js'; +- +-import * as Batch from './batch.js'; +-import * as Main from '../ui/main.js'; +-import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; +-import * as Params from '../misc/params.js'; +-import {AuthServicesLegacy} from './authServicesLegacy.js'; +-import {AuthServicesSSSDSwitchable} from './authServicesSSSDSwitchable.js'; +- +-const CLONE_FADE_ANIMATION_TIME = 250; +- +-export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +-export const BANNER_MESSAGE_KEY = 'banner-message-enable'; +-export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; +-export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; +-export const BANNER_MESSAGE_PATH_KEY = 'banner-message-path'; +-export const ALLOWED_FAILURES_KEY = 'allowed-failures'; +- +-export const LOGO_KEY = 'logo'; +-export const DISABLE_USER_LIST_KEY = 'disable-user-list'; +- +-// Give user 48ms to read each character of a PAM message +-// or 2 seconds, whichever is longer +-const USER_READ_TIME = 48; +-const USER_READ_TIME_MIN = 2000; +-const MESSAGE_TIME_MULTIPLIER = (() => { +- const value = Number.parseFloat(GLib.getenv('GDM_MESSAGE_TIME_MULTIPLIER')); +- return Number.isFinite(value) && value > 0 ? value : 1; +-})(); +- +-/** +- * Keep messages in order by priority +- * +- * @enum {number} +- */ +-export const MessageType = { +- NONE: 0, +- HINT: 1, +- INFO: 2, +- ERROR: 3, +-}; +- +-// Priority-ordered: earlier entries take precedence for shared roles. +-// Each authServices claims the roles it supports; unsupported roles +-// cascade to the next authServices in the array. +-const AuthServicesClasses = [ +- AuthServicesSSSDSwitchable, +- AuthServicesLegacy, +-]; +- +-/** +- * Error thrown during the authentication initialization phase. +- * +- * This error is emitted when requesting user verifier proxies or starting +- * a service via beginVerification fails. It wraps the underlying error +- * and provides context about which service failed. +- */ +-export class InitError extends Error { +- constructor(error, message, serviceName) { +- super(message, {cause: error}); +- this.serviceName = serviceName; +- } +-} +- +-/** +- * @param {Clutter.Actor} actor +- */ +-export function cloneAndFadeOutActor(actor) { +- // Immediately hide actor so its sibling can have its space +- // and position, but leave a non-reactive clone on-screen, +- // so from the user's point of view it smoothly fades away +- // and reveals its sibling. +- actor.hide(); +- +- const clone = new Clutter.Clone({ +- source: actor, +- reactive: false, +- }); +- +- Main.uiGroup.add_child(clone); +- +- let [x, y] = actor.get_transformed_position(); +- clone.set_position(x, y); +- +- let hold = new Batch.Hold(); +- clone.ease({ +- opacity: 0, +- duration: CLONE_FADE_ANIMATION_TIME, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => { +- clone.destroy(); +- hold.release(); +- }, +- }); +- return hold; +-} +- +-export class ShellUserVerifier extends Signals.EventEmitter { +- constructor(client, params) { +- super(); +- params = Params.parse(params, {reauthenticationOnly: false}); +- this._reauthOnly = params.reauthenticationOnly; +- +- this._client = client; +- this._cancellable = null; +- this._authServices = []; +- +- this._messageQueue = []; +- this._messageQueueTimeoutId = 0; +- +- this._settings = new Gio.Settings({schema_id: LOGIN_SCREEN_SCHEMA}); +- this._settings.connect('changed', () => this._onSettingsChanged()); +- this._updateAuthServices(); +- } +- +- get hasPendingMessages() { +- return !!this._messageQueue.length; +- } +- +- get allowedFailures() { +- return this._settings.get_int(ALLOWED_FAILURES_KEY); +- } +- +- get currentMessage() { +- return this._messageQueue ? this._messageQueue[0] : null; +- } +- +- async begin(userName, hold) { +- this._cancellable?.cancel(); +- this._cancellable = new Gio.Cancellable(); +- +- try { +- const proxies = await this._getUserVerifierProxies(userName, this._cancellable); +- this._setUserVerifier(proxies.userVerifier); +- for (const s of this._authServices) { +- // eslint-disable-next-line no-await-in-loop +- await s.beginVerification(userName, proxies); +- } +- } catch (e) { +- if (e instanceof InitError) +- this._reportInitError(e); +- else +- logErrorUnlessCancelled(e); +- } +- +- hold?.release(); +- } +- +- selectMechanism(mechanism) { +- // Every authServices needs to update its selected mechanism +- return this._authServices +- .map(s => s.selectMechanism(mechanism)) +- .some(Boolean); +- } +- +- needsUsername() { +- return this._authServices.some(s => s.needsUsername()); +- } +- +- reset() { +- this._authServices.forEach(s => s.reset()); +- +- this._userVerifier?.call_cancel_sync(null); +- +- this.clear(); +- } +- +- cancelRequested() { +- const {selectedMechanism} = this; +- return this._authServices.find(s => +- s.selectedMechanism === selectedMechanism)?.cancelRequested() ?? false; +- } +- +- cancel() { +- this._authServices.forEach(s => s.cancel()); +- +- this._userVerifier?.call_cancel_sync(null); +- +- this.clear(); +- } +- +- clear() { +- this._authServices.forEach(s => s.clear()); +- this._redistributeRoles(); +- +- this._clearMessageQueue(); +- +- this._cancellable?.cancel(); +- this._cancellable = null; +- +- this._clearUserVerifier(); +- } +- +- _setUserVerifier(userVerifier) { +- this._clearUserVerifier(); +- this._userVerifier = userVerifier; +- this._userVerifier.get_connection().connectObject( +- 'closed', () => this._clearUserVerifier(), +- this); +- } +- +- _clearUserVerifier() { +- this._userVerifier?.get_connection().disconnectObject(this); +- this._userVerifier = null; +- } +- +- destroy() { +- this._authServices.forEach(s => s.destroy()); +- this._authServices = []; +- +- this.cancel(); +- +- this._settings.run_dispose(); +- this._settings = null; +- } +- +- selectChoice(serviceName, key) { +- this._authServices.forEach(s => s.selectChoice(serviceName, key)); +- } +- +- async answerQuery(serviceName, answer) { +- // Wait for pending messages to be displayed before answering to +- // ensure no messages get lost +- await this._handlePendingMessages().catch(logErrorUnlessCancelled); +- +- this._authServices.forEach(s => s.answerQuery(serviceName, answer)); +- } +- +- addCredentialManager(serviceName, credentialManager) { +- this._authServices.forEach(s => s.addCredentialManager(serviceName, credentialManager)); +- } +- +- removeCredentialManager(serviceName) { +- this._authServices.forEach(s => s.removeCredentialManager(serviceName)); +- } +- +- _getIntervalForMessage(message) { +- if (!message) +- return 0; +- +- // We probably could be smarter here +- return Math.max(message.length * USER_READ_TIME, USER_READ_TIME_MIN) * +- MESSAGE_TIME_MULTIPLIER; +- } +- +- _finishMessageQueue() { +- if (!this.hasPendingMessages) +- return; +- +- this._messageQueue = []; +- +- this.emit('no-more-messages'); +- } +- +- _serviceHasPendingMessages(serviceName) { +- return this._messageQueue.some(m => m.serviceName === serviceName); +- } +- +- _filterServiceMessages({serviceName, messageType}) { +- // This function allows to remove queued messages for the @serviceName +- // whose type has lower priority than @messageType, replacing them +- // with a null message that will lead to clearing the prompt once done. +- if (this._serviceHasPendingMessages(serviceName)) +- this._queuePriorityMessage({serviceName, messageType}); +- } +- +- async _queueMessageTimeout() { +- if (this._messageQueueTimeoutId !== 0 || this._showMessageResolver) +- return; +- +- const message = this.currentMessage; +- const {promise, resolve} = Promise.withResolvers(); +- this._showMessageResolver = resolve; +- +- this.emit('show-message', { +- message: message.text, +- type: message.type, +- shouldWiggle: message.wiggle, +- showMessageResolver: this._showMessageResolver, +- }); +- +- await promise.catch(logError); +- if (!this._showMessageResolver) +- return; +- this._showMessageResolver = null; +- +- this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, +- message.interval, () => { +- this._messageQueueTimeoutId = 0; +- +- if (this._messageQueue.length > 1) { +- this._messageQueue.shift(); +- this._queueMessageTimeout(); +- } else { +- this._finishMessageQueue(); +- } +- +- return GLib.SOURCE_REMOVE; +- }); +- GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout'); +- } +- +- _queueMessage({serviceName, message, messageType, wiggle}) { +- const interval = this._getIntervalForMessage(message); +- +- this._messageQueue.push({ +- serviceName, +- text: message, +- type: messageType, +- wiggle, +- interval, +- }); +- this._queueMessageTimeout(); +- } +- +- _queuePriorityMessage({serviceName, message, messageType, wiggle}) { +- const newQueue = this._messageQueue.filter(m => { +- if (m.serviceName !== serviceName || m.type >= messageType) +- return m.text !== message; +- return false; +- }); +- +- if (!newQueue.includes(this.currentMessage)) +- this._clearMessageQueue(); +- +- this._messageQueue = newQueue; +- this._queueMessage({serviceName, message, messageType, wiggle}); +- } +- +- _clearMessageQueue() { +- this._finishMessageQueue(); +- +- if (this._messageQueueTimeoutId !== 0) { +- GLib.source_remove(this._messageQueueTimeoutId); +- this._messageQueueTimeoutId = 0; +- } +- +- if (this._showMessageResolver) { +- this._showMessageResolver(); +- this._showMessageResolver = null; +- } +- +- this.emit('show-message', {type: MessageType.NONE}); +- } +- +- _reportInitError(initError) { +- const {cause, message, serviceName} = initError; +- +- logError(cause, message); +- +- this._queueMessage({ +- serviceName, +- message: _('Authentication error'), +- messageType: MessageType.ERROR, +- }); +- this._verificationFailed({serviceName}); +- } +- +- async _getUserVerifierProxies(userName, cancellable) { +- const proxies = {}; +- +- if (userName) { +- try { +- proxies.userVerifier = await this._client.open_reauthentication_channel( +- userName, cancellable); +- } catch (e) { +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) +- throw e; +- if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) && +- !this._reauthOnly) { +- // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there +- // is no session to reauthenticate. Fall back to performing +- // verification from this login session +- return this._getUserVerifierProxies(null, cancellable); +- } +- throw new InitError(e, 'Failed to open reauthentication channel'); +- } +- } else { +- try { +- proxies.userVerifier = await this._client.get_user_verifier( +- cancellable); +- } catch (e) { +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) +- throw e; +- throw new InitError(e, 'Failed to obtain user verifier'); +- } +- } +- +- try { +- if (this._client.get_user_verifier_choice_list) +- proxies.userVerifierChoiceList = await this._client.get_user_verifier_choice_list(); +- if (this._client.get_user_verifier_custom_json) +- proxies.userVerifierCustomJSON = await this._client.get_user_verifier_custom_json(); +- } catch (e) { +- throw new InitError(e, 'Failed to obtain user verifier extensions'); +- } +- +- return proxies; +- } +- +- _onSettingsChanged() { +- this._updateAuthServices(); +- } +- +- _updateAuthServices() { +- const enabledAuthServicesClasses = AuthServicesClasses +- .filter(C => C.isEnabled(this._settings)); +- +- if (enabledAuthServicesClasses.length === this._enabledAuthServicesClasses?.length && +- enabledAuthServicesClasses.every(c => this._enabledAuthServicesClasses.includes(c))) +- return; +- +- this._enabledAuthServicesClasses = enabledAuthServicesClasses; +- this._createAuthServices(); +- } +- +- _createAuthServices() { +- this._clearAuthServices(); +- +- const params = { +- client: this._client, +- allowedFailures: this.allowedFailures, +- reauthOnly: this._reauthOnly, +- settings: this._settings, +- }; +- +- this._enabledAuthServicesClasses.forEach(AuthServicesClass => { +- this._authServices.push(new AuthServicesClass(params)); +- }); +- +- this._redistributeRoles(); +- this._connectAuthServices(); +- } +- +- _clearAuthServices() { +- this._authServices.forEach(s => s.destroy()); +- this._authServices = []; +- } +- +- _connectAuthServices() { +- this._authServices.forEach(authServices => { +- authServices.connectObject( +- 'ask-question', (_, args) => this.emit('ask-question', args), +- 'queue-message', (_, args) => this._queueMessage(args), +- 'queue-priority-message', (_, args) => this._queuePriorityMessage(args), +- 'wait-pending-messages', (_, args) => this._waitPendingMessages(args), +- 'filter-messages', (_, args) => this._filterServiceMessages(args), +- 'verification-failed', (_, args) => this._verificationFailed(args), +- 'verification-complete', () => this.emit('verification-complete'), +- 'reset', (_, args) => this.emit('reset', args), +- 'show-choice-list', (_, args) => this.emit('show-choice-list', args), +- 'show-button', (_, args) => this.emit('show-button', args), +- 'mechanisms-changed', () => this._onMechanismsChanged(), +- 'web-login', (_, args) => this.emit('web-login', args), +- this); +- }); +- } +- +- _verificationFailed({serviceName, canRetry}) { +- this._filterServiceMessages({serviceName, messageType: MessageType.ERROR}); +- this.emit('verification-failed', {serviceName, canRetry}); +- } +- +- get selectedMechanism() { +- return this._authServices +- .find(s => s.selectedMechanism)?.selectedMechanism ?? null; +- } +- +- _redistributeRoles() { +- if (this._authServices.length < 2) +- return; +- +- this._redistributingRoles = true; +- +- // Each authServices disables the roles supported by the one +- // before it, cascading down the priority chain +- const authServices = this._authServices; +- for (let i = 1; i < authServices.length; i++) { +- const prev = authServices[i - 1]; +- const current = authServices[i]; +- current.updateEnabledRoles({disableRoles: prev.supportedRoles}); +- } +- +- this._redistributingRoles = false; +- } +- +- _onMechanismsChanged() { +- if (this._redistributingRoles) +- return; +- +- this._redistributeRoles(); +- +- // Collect mechanisms from all authServices in priority order, +- // keeping only the first mechanism per role +- const seenRoles = new Set(); +- const mechanisms = this._authServices +- .flatMap(authServices => { +- const serviceMechanisms = authServices.enabledMechanisms ?? []; +- const visibleMechanisms = serviceMechanisms.filter(m => +- !seenRoles.has(m.role)); +- +- serviceMechanisms.forEach(m => seenRoles.add(m.role)); +- return visibleMechanisms; +- }); +- +- const selectedMechanism = this.selectedMechanism ?? +- mechanisms.find(m => m.selectable) ?? +- {}; +- +- this.emit('mechanisms-changed', {mechanisms, selectedMechanism}); +- } +- +- async _waitPendingMessages({task}) { +- try { +- await this._handlePendingMessages(); +- task.return_boolean(true); +- } catch (e) { +- task.return_error(e); +- } +- } +- +- _handlePendingMessages() { +- if (!this.hasPendingMessages) +- return Promise.resolve(); +- +- const cancellable = this._cancellable; +- return new Promise((resolve, reject) => { +- let signalId = this.connect('no-more-messages', () => { +- this.disconnect(signalId); +- if (cancellable.is_cancelled()) +- reject(new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled')); +- else +- resolve(); +- }); +- }); +- } +-} +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 0359f6028..271b6c0e4 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -15,7 +15,7 @@ + gdm/fido2TokenManager.js + gdm/realmd.js + gdm/smartcardManager.js +- gdm/util.js ++ gdm/userVerifier.js + gdm/vmware.js + gdm/webLogin.js + +diff --git a/po/POTFILES.in b/po/POTFILES.in +index 08c23cbff..34be91f7c 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -16,6 +16,7 @@ js/gdm/authServicesLegacy.js + js/gdm/authServicesSSSDSwitchable.js + js/gdm/loginDialog.js + js/gdm/util.js ++js/gdm/userVerifier.js + js/gdm/webLogin.js + js/misc/breakManager.js + js/misc/brightnessManager.js +-- +2.55.0 + + +From 9cf6ab28d455c6351b7bc624e1a7bc6a7d3abe4b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Sun, 26 Jul 2026 10:40:09 +0200 +Subject: [PATCH 70/72] gdm: Move userVerifier settings and utilities to proper + files + +Leave to util only utility features, while settings keys and schemas are +now in a new settings file + +Part-of: +--- + js/gdm/authServicesLegacy.js | 17 ++++------ + js/gdm/authServicesSSSDSwitchable.js | 3 +- + js/gdm/loginDialog.js | 33 ++++++++++--------- + js/gdm/settings.js | 14 ++++++++ + js/gdm/userVerifier.js | 49 +--------------------------- + js/gdm/util.js | 39 ++++++++++++++++++++++ + js/js-resources.gresource.xml | 2 ++ + 7 files changed, 82 insertions(+), 75 deletions(-) + create mode 100644 js/gdm/settings.js + create mode 100644 js/gdm/util.js + +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 4738131cf..77f2e2551 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -4,6 +4,7 @@ import GObject from 'gi://GObject'; + import * as FingerprintManager from './fingerprintManager.js'; + import {FingerprintReaderType} from './fingerprintManager.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; ++import * as Settings from './settings.js'; + import * as SmartcardManager from './smartcardManager.js'; + import * as OVirt from './oVirt.js'; + import * as Vmware from './vmware.js'; +@@ -17,28 +18,24 @@ const PASSWORD_SERVICE_NAME = 'gdm-password'; + const SMARTCARD_SERVICE_NAME = 'gdm-smartcard'; + const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint'; + +-const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; +-const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; +-const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; +- + const Mechanisms = [ + { + serviceName: PASSWORD_SERVICE_NAME, + role: Role.PASSWORD, + name: _('Password'), +- setting: PASSWORD_AUTHENTICATION_KEY, ++ setting: Settings.PASSWORD_AUTHENTICATION_KEY, + }, + { + serviceName: SMARTCARD_SERVICE_NAME, + role: Role.SMARTCARD, + name: _('Smartcard'), +- setting: SMARTCARD_AUTHENTICATION_KEY, ++ setting: Settings.SMARTCARD_AUTHENTICATION_KEY, + }, + { + serviceName: FINGERPRINT_SERVICE_NAME, + role: Role.FINGERPRINT, + name: _('Fingerprint'), +- setting: FINGERPRINT_AUTHENTICATION_KEY, ++ setting: Settings.FINGERPRINT_AUTHENTICATION_KEY, + }, + ]; + +@@ -60,9 +57,9 @@ export class AuthServicesLegacy extends AuthServices { + } + + static isEnabled(settings) { +- return settings.get_boolean(PASSWORD_AUTHENTICATION_KEY) || +- settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY) || +- settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY); ++ return settings.get_boolean(Settings.PASSWORD_AUTHENTICATION_KEY) || ++ settings.get_boolean(Settings.FINGERPRINT_AUTHENTICATION_KEY) || ++ settings.get_boolean(Settings.SMARTCARD_AUTHENTICATION_KEY); + } + + constructor(params) { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index a46b1d1d4..296691d78 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -6,11 +6,10 @@ import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as SmartcardManager from './smartcardManager.js'; + import {AuthServices, Role} from './authServices.js'; + import {MessageType} from './userVerifier.js'; ++import {SWITCHABLE_AUTHENTICATION_KEY} from './settings.js'; + + const SWITCHABLE_AUTH_SERVICE_NAME = 'gdm-switchable-auth'; + +-const SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; +- + const MechanismsStatus = { + WAITING: 0, + NOT_FOUND: 1, +diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js +index 3b5bf7c00..37a0bff38 100644 +--- a/js/gdm/loginDialog.js ++++ b/js/gdm/loginDialog.js +@@ -32,7 +32,8 @@ import * as AuthPrompt from './authPrompt.js'; + import * as Batch from './batch.js'; + import * as BoxPointer from '../ui/boxpointer.js'; + import * as CtrlAltTab from '../ui/ctrlAltTab.js'; +-import * as GdmUtil from './userVerifier.js'; ++import * as GdmUtil from './util.js'; ++import * as Settings from './settings.js'; + import * as Layout from '../ui/layout.js'; + import * as LoginManager from '../misc/loginManager.js'; + import * as Main from '../ui/main.js'; +@@ -42,6 +43,7 @@ import * as PopupMenu from '../ui/popupMenu.js'; + import * as Realmd from './realmd.js'; + import * as UserWidget from '../ui/userWidget.js'; + import {QuickSettingsMenu} from '../ui/quickSettings.js'; ++import {MessageType} from './userVerifier.js'; + import * as A11y from '../ui/status/accessibility.js'; + + const _FADE_ANIMATION_TIME = 250; +@@ -460,25 +462,25 @@ export const LoginDialog = GObject.registerClass({ + } catch { + } + +- this._settings = new Gio.Settings({schema_id: GdmUtil.LOGIN_SCREEN_SCHEMA}); ++ this._settings = new Gio.Settings({schema_id: Settings.LOGIN_SCREEN_SCHEMA}); + +- this._settings.connect(`changed::${GdmUtil.BANNER_MESSAGE_KEY}`, ++ this._settings.connect(`changed::${Settings.BANNER_MESSAGE_KEY}`, + () => this._updateBanner().catch(logError)); +- this._settings.connect(`changed::${GdmUtil.BANNER_MESSAGE_TEXT_KEY}`, ++ this._settings.connect(`changed::${Settings.BANNER_MESSAGE_TEXT_KEY}`, + () => this._updateBanner().catch(logError)); +- this._settings.connect(`changed::${GdmUtil.BANNER_MESSAGE_SOURCE_KEY}`, ++ this._settings.connect(`changed::${Settings.BANNER_MESSAGE_SOURCE_KEY}`, + () => { + if (this._updateBannerMessageFile()) + this._updateBanner().catch(logError); + }); +- this._settings.connect(`changed::${GdmUtil.BANNER_MESSAGE_PATH_KEY}`, ++ this._settings.connect(`changed::${Settings.BANNER_MESSAGE_PATH_KEY}`, + () => { + if (this._updateBannerMessageFile()) + this._updateBanner().catch(logError); + }); +- this._settings.connect(`changed::${GdmUtil.DISABLE_USER_LIST_KEY}`, ++ this._settings.connect(`changed::${Settings.DISABLE_USER_LIST_KEY}`, + this._updateDisableUserList.bind(this)); +- this._settings.connect(`changed::${GdmUtil.LOGO_KEY}`, ++ this._settings.connect(`changed::${Settings.LOGO_KEY}`, + this._updateLogo.bind(this)); + + this._textureCache = St.TextureCache.get_default(); +@@ -906,7 +908,7 @@ export const LoginDialog = GObject.registerClass({ + } + + _updateDisableUserList() { +- let disableUserList = this._settings.get_boolean(GdmUtil.DISABLE_USER_LIST_KEY); ++ let disableUserList = this._settings.get_boolean(Settings.DISABLE_USER_LIST_KEY); + + // Disable user list when there are no users. + if (this._userListLoaded && this._userList.numItems() === 0) +@@ -938,8 +940,8 @@ export const LoginDialog = GObject.registerClass({ + } + + _updateBannerMessageFile() { +- const path = this._settings.get_string(GdmUtil.BANNER_MESSAGE_SOURCE_KEY) === 'file' +- ? this._settings.get_string(GdmUtil.BANNER_MESSAGE_PATH_KEY) ++ const path = this._settings.get_string(Settings.BANNER_MESSAGE_SOURCE_KEY) === 'file' ++ ? this._settings.get_string(Settings.BANNER_MESSAGE_PATH_KEY) + : null; + const file = path + ? Gio.File.new_for_path(path) +@@ -966,7 +968,7 @@ export const LoginDialog = GObject.registerClass({ + } + + async _getBannerText() { +- const enabled = this._settings.get_boolean(GdmUtil.BANNER_MESSAGE_KEY); ++ const enabled = this._settings.get_boolean(Settings.BANNER_MESSAGE_KEY); + if (!enabled) + return null; + +@@ -980,7 +982,7 @@ export const LoginDialog = GObject.registerClass({ + } + } + +- return this._settings.get_string(GdmUtil.BANNER_MESSAGE_TEXT_KEY); ++ return this._settings.get_string(Settings.BANNER_MESSAGE_TEXT_KEY); + } + + async _updateBanner() { +@@ -1026,7 +1028,7 @@ export const LoginDialog = GObject.registerClass({ + } + + _updateLogo() { +- let path = this._settings.get_string(GdmUtil.LOGO_KEY); ++ const path = this._settings.get_string(Settings.LOGO_KEY); + + this._logoFile = path ? Gio.file_new_for_path(path) : null; + this._updateLogoTexture(this._textureCache, this._logoFile); +@@ -1162,7 +1164,8 @@ export const LoginDialog = GObject.registerClass({ + + // Translators: this message is shown below the username entry field + // to clue the user in on how to login to the local network realm +- this._authPrompt.setMessage(_('(e.g., user or %s)').format(hint), GdmUtil.MessageType.HINT); ++ this._authPrompt.setMessage(_('(e.g., user or %s)').format(hint), ++ MessageType.HINT); + } + + _askForUsernameAndBeginVerification() { +diff --git a/js/gdm/settings.js b/js/gdm/settings.js +new file mode 100644 +index 000000000..bb44b0adb +--- /dev/null ++++ b/js/gdm/settings.js +@@ -0,0 +1,14 @@ ++export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; ++export const BANNER_MESSAGE_KEY = 'banner-message-enable'; ++export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; ++export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; ++export const BANNER_MESSAGE_PATH_KEY = 'banner-message-path'; ++export const ALLOWED_FAILURES_KEY = 'allowed-failures'; ++ ++export const LOGO_KEY = 'logo'; ++export const DISABLE_USER_LIST_KEY = 'disable-user-list'; ++ ++export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication'; ++export const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication'; ++export const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication'; ++export const SWITCHABLE_AUTHENTICATION_KEY = 'enable-switchable-authentication'; +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 5c3dc4d37..2b7e960d3 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -1,26 +1,12 @@ +-import Clutter from 'gi://Clutter'; + import Gio from 'gi://Gio'; + import GLib from 'gi://GLib'; + import * as Signals from '../misc/signals.js'; + +-import * as Batch from './batch.js'; +-import * as Main from '../ui/main.js'; + import {logErrorUnlessCancelled} from '../misc/errorUtils.js'; + import * as Params from '../misc/params.js'; + import {AuthServicesLegacy} from './authServicesLegacy.js'; + import {AuthServicesSSSDSwitchable} from './authServicesSSSDSwitchable.js'; +- +-const CLONE_FADE_ANIMATION_TIME = 250; +- +-export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen'; +-export const BANNER_MESSAGE_KEY = 'banner-message-enable'; +-export const BANNER_MESSAGE_SOURCE_KEY = 'banner-message-source'; +-export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text'; +-export const BANNER_MESSAGE_PATH_KEY = 'banner-message-path'; +-export const ALLOWED_FAILURES_KEY = 'allowed-failures'; +- +-export const LOGO_KEY = 'logo'; +-export const DISABLE_USER_LIST_KEY = 'disable-user-list'; ++import {LOGIN_SCREEN_SCHEMA, ALLOWED_FAILURES_KEY} from './settings.js'; + + // Give user 48ms to read each character of a PAM message + // or 2 seconds, whichever is longer +@@ -65,39 +51,6 @@ export class InitError extends Error { + } + } + +-/** +- * @param {Clutter.Actor} actor +- */ +-export function cloneAndFadeOutActor(actor) { +- // Immediately hide actor so its sibling can have its space +- // and position, but leave a non-reactive clone on-screen, +- // so from the user's point of view it smoothly fades away +- // and reveals its sibling. +- actor.hide(); +- +- const clone = new Clutter.Clone({ +- source: actor, +- reactive: false, +- }); +- +- Main.uiGroup.add_child(clone); +- +- let [x, y] = actor.get_transformed_position(); +- clone.set_position(x, y); +- +- let hold = new Batch.Hold(); +- clone.ease({ +- opacity: 0, +- duration: CLONE_FADE_ANIMATION_TIME, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => { +- clone.destroy(); +- hold.release(); +- }, +- }); +- return hold; +-} +- + export class ShellUserVerifier extends Signals.EventEmitter { + constructor(client, params) { + super(); +diff --git a/js/gdm/util.js b/js/gdm/util.js +new file mode 100644 +index 000000000..10f9131d9 +--- /dev/null ++++ b/js/gdm/util.js +@@ -0,0 +1,39 @@ ++import Clutter from 'gi://Clutter'; ++ ++import * as Batch from './batch.js'; ++import * as Main from '../ui/main.js'; ++ ++export const CLONE_FADE_ANIMATION_TIME = 250; ++ ++/** ++ * @param {Clutter.Actor} actor ++ */ ++export function cloneAndFadeOutActor(actor) { ++ // Immediately hide actor so its sibling can have its space ++ // and position, but leave a non-reactive clone on-screen, ++ // so from the user's point of view it smoothly fades away ++ // and reveals its sibling. ++ actor.hide(); ++ ++ const clone = new Clutter.Clone({ ++ source: actor, ++ reactive: false, ++ }); ++ ++ Main.uiGroup.add_child(clone); ++ ++ const [x, y] = actor.get_transformed_position(); ++ clone.set_position(x, y); ++ ++ const hold = new Batch.Hold(); ++ clone.ease({ ++ opacity: 0, ++ duration: CLONE_FADE_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => { ++ clone.destroy(); ++ hold.release(); ++ }, ++ }); ++ return hold; ++} +diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml +index 271b6c0e4..daff41d6a 100644 +--- a/js/js-resources.gresource.xml ++++ b/js/js-resources.gresource.xml +@@ -14,8 +14,10 @@ + gdm/oVirt.js + gdm/fido2TokenManager.js + gdm/realmd.js ++ gdm/settings.js + gdm/smartcardManager.js + gdm/userVerifier.js ++ gdm/util.js + gdm/vmware.js + gdm/webLogin.js + +-- +2.55.0 + + +From 5b3153aec9feec615a0fbd53a292c669e27fc5d1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Sun, 26 Jul 2026 10:06:35 +0200 +Subject: [PATCH 71/72] gdm/authServices: Use per-request callbacks to handle + services queries + +In the design previous to this change, we may end up handling a query +without proper context of what was the caller for it, and potentially +we may emit an answer to a service that did not perform the request. + +Move that responsibility to the signal emitters, by passing a callback +directly as part of the signal parameters, thus ensuring the answer +always reaches the mechanism that originated the query and giving +the prompt proper per-request life cycle. + +Part-of: +--- + js/gdm/authPrompt.js | 82 ++++++++++++++++++++-------- + js/gdm/authServices.js | 14 +---- + js/gdm/authServicesLegacy.js | 29 ++++++---- + js/gdm/authServicesSSSDSwitchable.js | 55 ++++++++++++------- + js/gdm/userVerifier.js | 16 +----- + 5 files changed, 117 insertions(+), 79 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 8143c2d5b..007c1538c 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -191,20 +191,7 @@ export const AuthPrompt = GObject.registerClass({ + + this._authList = new AuthList.AuthList(); + this._authList.hide(); +- this._authList.connect('activate', (list, key) => { +- this._authList.reactive = false; +- this._authList.ease({ +- opacity: 0, +- duration: MESSAGE_FADE_OUT_ANIMATION_TIME * 0.5, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => { +- this._authListTitle.child.text = ''; +- this._authList.clear(); +- this._authList.hide(); +- this._userVerifier.selectChoice(this._queryingService, key); +- }, +- }); +- }); ++ this._authListActivateId = 0; + this.add_child(this._authList); + + this._authListTitle = new St.Bin({ +@@ -310,6 +297,7 @@ export const AuthPrompt = GObject.registerClass({ + x_expand: true, + y_expand: true, + }); ++ this._authButton.connect('clicked', () => this._completePendingCallback()); + this._mainBox.add_child(this._authButton); + + this._webLoginDialog = new WebLogin.WebLoginDialog(); +@@ -381,7 +369,7 @@ export const AuthPrompt = GObject.registerClass({ + this.startSpinning({animate: true}); + + if (this._queryingService) { +- this._userVerifier.answerQuery(this._queryingService, this._entry.text); ++ this._completePendingCallback(this._entry.text); + } else { + this._preemptiveAnswer = this._entry.text; + +@@ -419,17 +407,37 @@ export const AuthPrompt = GObject.registerClass({ + this._capsLockWarningLabel.visible = secret; + } + +- _onAskQuestion({serviceName, question, secret}) { ++ _setPendingCallback(callback) { ++ if (this._pendingCallback) ++ throw new Error('A pending request is already active'); ++ this._pendingCallback = callback; ++ } ++ ++ _completePendingCallback(...args) { ++ if (!this._pendingCallback) ++ throw new Error('No pending request to complete'); ++ ++ const callback = this._pendingCallback; ++ this._pendingCallback = null; ++ ++ this._userVerifier.handlePendingMessages() ++ .then(() => callback(...args)) ++ .catch(logErrorUnlessCancelled); ++ } ++ ++ _onAskQuestion({serviceName, question, secret, answerHandler}) { + if (this._queryingService) + this.clear(); + + this._queryingService = serviceName; + this.promptStep++; + ++ this._setPendingCallback(answerHandler); ++ + const preemptiveAnswer = this._preemptiveAnswer; + this._clearPreemptiveState(); + if (preemptiveAnswer) { +- this._userVerifier.answerQuery(this._queryingService, preemptiveAnswer); ++ this._completePendingCallback(preemptiveAnswer); + return; + } + +@@ -446,7 +454,7 @@ export const AuthPrompt = GObject.registerClass({ + this.emit('prompted'); + } + +- _onShowChoiceList({serviceName, promptMessage, choiceList}) { ++ _onShowChoiceList({serviceName, promptMessage, choiceList, choiceHandler}) { + if (this._queryingService) + this.clear(); + +@@ -455,6 +463,9 @@ export const AuthPrompt = GObject.registerClass({ + + this._clearPreemptiveState(); + ++ this._connectAuthListActivate(); ++ this._setPendingCallback(choiceHandler); ++ + this.setChoiceList(promptMessage, choiceList); + this.updateSensitivity({sensitive: true}); + this.emit('prompted'); +@@ -492,10 +503,7 @@ export const AuthPrompt = GObject.registerClass({ + + this._clearPreemptiveState(); + +- const authButtonClickedId = this._authButton.connect('clicked', () => { +- this._authButton.disconnect(authButtonClickedId); +- callback(); +- }); ++ this._setPendingCallback(callback); + + this._authButton.set_label(label); + +@@ -670,8 +678,10 @@ export const AuthPrompt = GObject.registerClass({ + this._authListTitle.child.text = ''; + this._authList.clear(); + this._authList.hide(); ++ this._disconnectAuthListActivate(); + this._authButton.hide(); + this._closeWebLoginDialog(); ++ this._pendingCallback = null; + + [this._mainBox, this._webLoginDialog].forEach(widget => { + widget.opacity = 255; +@@ -695,6 +705,34 @@ export const AuthPrompt = GObject.registerClass({ + this.updateSensitivity({sensitive: true}); + } + ++ _connectAuthListActivate() { ++ if (this._authListActivateId) ++ return; ++ ++ this._authListActivateId = ++ this._authList.connect('activate', (list, key) => { ++ this._authList.reactive = false; ++ this._authList.ease({ ++ opacity: 0, ++ duration: MESSAGE_FADE_OUT_ANIMATION_TIME * 0.5, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => { ++ this._authListTitle.child.text = ''; ++ this._authList.clear(); ++ this._authList.hide(); ++ this._completePendingCallback(key); ++ }, ++ }); ++ }); ++ } ++ ++ _disconnectAuthListActivate() { ++ if (this._authListActivateId) { ++ this._authList.disconnect(this._authListActivateId); ++ this._authListActivateId = 0; ++ } ++ } ++ + _fadeInElement(element) { + if (element.visible) + return; +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index aa379ca83..102a22b92 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -81,6 +81,7 @@ export class AuthServices extends GObject.Object { + * @param {string} serviceName + * @param {string} question + * @param {boolean} [secret] ++ * @param {Function} answerHandler + */ + 'ask-question': {param_types: [GObject.TYPE_JSOBJECT]}, + /** +@@ -92,6 +93,7 @@ export class AuthServices extends GObject.Object { + * @param {string} serviceName + * @param {string} promptMessage + * @param {object} [choiceList] - keys map to {title: string} ++ * @param {Function} [choiceHandler] + */ + 'show-choice-list': {param_types: [GObject.TYPE_JSOBJECT]}, + /** +@@ -161,14 +163,6 @@ export class AuthServices extends GObject.Object { + return this._handleGetSupportedRoles(); + } + +- selectChoice(serviceName, key) { +- this._handleSelectChoice(serviceName, key); +- } +- +- answerQuery(serviceName, answer) { +- this._handleAnswerQuery(serviceName, answer); +- } +- + async beginVerification(userName, userVerifierProxies) { + this._cancellable?.cancel(); + this._cancellable = new Gio.Cancellable(); +@@ -478,10 +472,6 @@ export class AuthServices extends GObject.Object { + return this.constructor.SupportedRoles; + } + +- _handleSelectChoice() {} +- +- _handleAnswerQuery() {} +- + _handleBeginVerification() {} + + _handleSelectMechanism() { +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 77f2e2551..77f9cece6 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -77,14 +77,6 @@ export class AuthServicesLegacy extends AuthServices { + this._fingerprintReadyTimeoutId = 0; + } + +- _handleSelectChoice(serviceName, key) { +- if (serviceName !== this._selectedMechanism?.serviceName) +- return; +- +- this._userVerifierChoiceList.call_select_choice( +- serviceName, key, this._cancellable).catch(logErrorUnlessCancelled); +- } +- + _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; +@@ -253,7 +245,11 @@ export class AuthServicesLegacy extends AuthServices { + if (serviceName !== this._selectedMechanism?.serviceName) + return; + +- this.emit('ask-question', {serviceName, question}); ++ this.emit('ask-question', { ++ serviceName, ++ question, ++ answerHandler: answer => this._handleAnswerQuery(serviceName, answer), ++ }); + } + + _handleOnSecretInfoQuery(serviceName, secretQuestion) { +@@ -265,7 +261,7 @@ export class AuthServicesLegacy extends AuthServices { + token = this._credentialManagers[serviceName].token; + + if (token) { +- this.answerQuery(serviceName, token); ++ this._handleAnswerQuery(serviceName, token); + return; + } + +@@ -273,6 +269,7 @@ export class AuthServicesLegacy extends AuthServices { + serviceName, + question: secretQuestion, + secret: true, ++ answerHandler: answer => this._handleAnswerQuery(serviceName, answer), + }); + } + +@@ -348,7 +345,17 @@ export class AuthServicesLegacy extends AuthServices { + for (const [key, value] of Object.entries(list.deepUnpack())) + choiceList[key] = {title: value}; + +- this.emit('show-choice-list', {serviceName, promptMessage, choiceList}); ++ this.emit('show-choice-list', { ++ serviceName, ++ promptMessage, ++ choiceList, ++ choiceHandler: key => { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ this._userVerifierChoiceList.call_select_choice( ++ serviceName, key, this._cancellable).catch(logErrorUnlessCancelled); ++ }, ++ }); + } + + _handleGetCredentialManagerServices() { +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index 296691d78..cf6a4e1fb 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -74,23 +74,6 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + } + +- _handleSelectChoice(serviceName, key) { +- if (serviceName !== this._selectedMechanism?.serviceName) +- return; +- +- if (this._selectedMechanism.role === Role.SMARTCARD) { +- const certificates = this._selectedMechanism.certificates; +- const cert = certificates.find(c => c.keyId === key); +- this._selectedSmartcard = cert; +- this._promptStatus = PromptStatus.PIN_PROMPT; +- this.emit('ask-question', { +- serviceName, +- question: cert.pinPrompt, +- secret: true, +- }); +- } +- } +- + _handleAnswerQuery(serviceName, answer) { + if (serviceName !== this._selectedMechanism?.serviceName) + return; +@@ -365,6 +348,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + serviceName, + question: secretQuestion, + secret: true, ++ answerHandler: answer => this._handleAnswerQuery(serviceName, answer), + }); + } + } +@@ -452,7 +436,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + const {serviceName, prompt} = this._selectedMechanism; + + this._promptStatus = PromptStatus.PASSWORD_PROMPT; +- this.emit('ask-question', {serviceName, question: prompt, secret: true}); ++ this.emit('ask-question', { ++ serviceName, ++ question: prompt, ++ secret: true, ++ answerHandler: answer => this._handleAnswerQuery(serviceName, answer), ++ }); + } + + _startSmartcardLogin() { +@@ -465,6 +454,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + serviceName, + question: certificates[0].pinPrompt, + secret: true, ++ answerHandler: answer => this._handleAnswerQuery(serviceName, answer), + }); + return; + } +@@ -478,7 +468,27 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + : _('Select Identity'); + + this._promptStatus = PromptStatus.CERT_LIST_PROMPT; +- this.emit('show-choice-list', {serviceName, promptMessage, choiceList}); ++ this.emit('show-choice-list', { ++ serviceName, ++ promptMessage, ++ choiceList, ++ choiceHandler: key => { ++ if (serviceName !== this._selectedMechanism?.serviceName) ++ return; ++ ++ if (this._selectedMechanism.role === Role.SMARTCARD) { ++ const cert = this._selectedMechanism.certificates.find(c => c.keyId === key); ++ this._selectedSmartcard = cert; ++ this._promptStatus = PromptStatus.PIN_PROMPT; ++ this.emit('ask-question', { ++ serviceName, ++ question: cert.pinPrompt, ++ secret: true, ++ answerHandler: answer => this._handleAnswerQuery(serviceName, answer), ++ }); ++ } ++ }, ++ }); + } + + _parseCertInstruction(certInstruction) { +@@ -513,7 +523,12 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + } + + this._promptStatus = PromptStatus.PIN_PROMPT; +- this.emit('ask-question', {serviceName, question: pinPrompt, secret: true}); ++ this.emit('ask-question', { ++ serviceName, ++ question: pinPrompt, ++ secret: true, ++ answerHandler: answer => this._handleAnswerQuery(serviceName, answer), ++ }); + + if (pinAttempts <= 3 && pinAttempts > 0) { + const message = _('You have %d attempts left. If the passkey gets locked, you may not able to access your account.').format(pinAttempts); +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 2b7e960d3..183d0bed3 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -170,18 +170,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._settings = null; + } + +- selectChoice(serviceName, key) { +- this._authServices.forEach(s => s.selectChoice(serviceName, key)); +- } +- +- async answerQuery(serviceName, answer) { +- // Wait for pending messages to be displayed before answering to +- // ensure no messages get lost +- await this._handlePendingMessages().catch(logErrorUnlessCancelled); +- +- this._authServices.forEach(s => s.answerQuery(serviceName, answer)); +- } +- + addCredentialManager(serviceName, credentialManager) { + this._authServices.forEach(s => s.addCredentialManager(serviceName, credentialManager)); + } +@@ -468,14 +456,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + + async _waitPendingMessages({task}) { + try { +- await this._handlePendingMessages(); ++ await this.handlePendingMessages(); + task.return_boolean(true); + } catch (e) { + task.return_error(e); + } + } + +- _handlePendingMessages() { ++ handlePendingMessages() { + if (!this.hasPendingMessages) + return Promise.resolve(); + +-- +2.55.0 + + +From f6754a3fba6f7f6bc1985b36917d00df13e6c288 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 28 Jul 2026 16:39:32 +0200 +Subject: [PATCH 72/72] gdm/userVerifier: Add driver service to constrain + active auth services + +Add setDriverService() to allow callers to restrict which +AuthServices classes are instantiated by specifying a GDM +service name. When set, only classes whose hasService() returns +true for the given name are enabled. Setting it to null restores +the default behavior of enabling all settings-enabled classes. + +Part-of: +--- + js/gdm/authServices.js | 4 ++++ + js/gdm/userVerifier.js | 17 ++++++++++++++++- + 2 files changed, 20 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index 102a22b92..d0f7659a5 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -125,6 +125,10 @@ export class AuthServices extends GObject.Object { + return true; + } + ++ static hasService(serviceName) { ++ return Object.values(this.RoleToService).includes(serviceName); ++ } ++ + constructor(params) { + super(); + params = Params.parse(params, { +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 183d0bed3..02decbf86 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -60,6 +60,7 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._client = client; + this._cancellable = null; + this._authServices = []; ++ this._driverService = null; + + this._messageQueue = []; + this._messageQueueTimeoutId = 0; +@@ -113,6 +114,15 @@ export class ShellUserVerifier extends Signals.EventEmitter { + return this._authServices.some(s => s.needsUsername()); + } + ++ setDriverService(serviceName) { ++ if (this._driverService === serviceName) ++ return; ++ ++ this._driverService = serviceName; ++ this.reset(); ++ this._updateAuthServices(); ++ } ++ + reset() { + this._authServices.forEach(s => s.reset()); + +@@ -347,9 +357,14 @@ export class ShellUserVerifier extends Signals.EventEmitter { + } + + _updateAuthServices() { +- const enabledAuthServicesClasses = AuthServicesClasses ++ let enabledAuthServicesClasses = AuthServicesClasses + .filter(C => C.isEnabled(this._settings)); + ++ if (this._driverService) { ++ enabledAuthServicesClasses = enabledAuthServicesClasses ++ .filter(C => C.hasService(this._driverService)); ++ } ++ + if (enabledAuthServicesClasses.length === this._enabledAuthServicesClasses?.length && + enabledAuthServicesClasses.every(c => this._enabledAuthServicesClasses.includes(c))) + return; +-- +2.55.0 diff --git a/0001-data-Update-generated-stylesheets.patch b/0001-data-Update-generated-stylesheets.patch index 20caa6a..8d7a736 100644 --- a/0001-data-Update-generated-stylesheets.patch +++ b/0001-data-Update-generated-stylesheets.patch @@ -4,13 +4,13 @@ Date: Tue, 16 Apr 2024 20:49:40 +0200 Subject: [PATCH] data: Update generated stylesheets --- - data/theme/gnome-shell-dark.css | 492 +++++++++++++++++----- - data/theme/gnome-shell-high-contrast.css | 508 ++++++++++++++++++----- - data/theme/gnome-shell-light.css | 491 +++++++++++++++++----- - 3 files changed, 1161 insertions(+), 330 deletions(-) + data/theme/gnome-shell-dark.css | 495 +++++++++++++++++----- + data/theme/gnome-shell-high-contrast.css | 511 ++++++++++++++++++----- + data/theme/gnome-shell-light.css | 495 +++++++++++++++++----- + 3 files changed, 1171 insertions(+), 330 deletions(-) diff --git a/data/theme/gnome-shell-dark.css b/data/theme/gnome-shell-dark.css -index 82e68c5..e112db8 100644 +index 82e68c5..5a1340c 100644 --- a/data/theme/gnome-shell-dark.css +++ b/data/theme/gnome-shell-dark.css @@ -42,7 +42,9 @@ stage { @@ -18,7 +18,7 @@ index 82e68c5..e112db8 100644 transition-duration: 100ms; } -.login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item, .login-dialog .login-dialog-auth-list-item, .login-dialog-not-listed-button, .unlock-dialog-notifications-container .message StButton, -+.login-dialog .web-login-intro-button, ++.login-dialog .login-button, +.login-dialog .web-login-prompt-button, .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item, .login-dialog .login-dialog-auth-list-title, +.login-dialog .login-dialog-auth-list-item, .login-dialog-not-listed-button, .unlock-dialog-notifications-container .message StButton, .unlock-dialog-notifications-container .unlock-dialog-notification-source StButton, .unlock-dialog .button, @@ -30,7 +30,7 @@ index 82e68c5..e112db8 100644 .message .message-header .unlock-dialog .message-close-button, -.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .login-dialog-auth-list-item, #LookingGlassDialog .notebook-tab, .screenshot-ui-show-pointer-button, .screenshot-ui-type-button, #LookingGlassDebugFlags .lg-debug-flag-button, #LookingGlassPropertyInspector .lg-obj-inspector-close-button, -#LookingGlassPropertyInspector .lg-obj-inspector-button, #LookingGlassDialog > #Toolbar .lg-toolbar-button, .icon-button, .login-dialog-button.a11y-button, .login-dialog-button.cancel-button, .login-dialog-button.switch-user-button, .login-dialog-button.login-dialog-session-list-button, .message .message-header .message-expand-button, -+.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .web-login-intro-button, ++.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button, .unlock-dialog .login-dialog-auth-list-title, .unlock-dialog .login-dialog-auth-list-item, #LookingGlassDialog .notebook-tab, .screenshot-ui-show-pointer-button, .screenshot-ui-type-button, #LookingGlassDebugFlags .lg-debug-flag-button, #LookingGlassPropertyInspector .lg-obj-inspector-close-button, +#LookingGlassPropertyInspector .lg-obj-inspector-button, #LookingGlassDialog > #Toolbar .lg-toolbar-button, .icon-button, .login-dialog-button.next-button, .login-dialog-button.a11y-button, .login-dialog-button.cancel-button, .login-dialog-button.switch-user-button, .login-dialog-button.login-dialog-auth-menu-button, .login-dialog-button.login-dialog-session-list-button, .message .message-header .message-expand-button, .message .message-header .message-close-button, .message-notification-group .message-collapse-button, .calendar .calendar-month-header .pager-button, .button { @@ -285,7 +285,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button, -.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .login-dialog-auth-list-item { +.unlock-dialog .login-dialog-button.login-dialog-auth-menu-button, -+.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .web-login-intro-button, ++.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button, .unlock-dialog .login-dialog-auth-list-title, .unlock-dialog .login-dialog-auth-list-item { color: #fafafb; background-color: rgba(250, 250, 251, 0.1); } @@ -300,7 +300,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:focus, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .login-dialog-auth-list-item:focus { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:focus, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .web-login-intro-button:focus, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .login-button:focus, + .unlock-dialog .web-login-prompt-button:focus, .unlock-dialog .login-dialog-auth-list-title:focus, .unlock-dialog .login-dialog-auth-list-item:focus { color: #fafafb; box-shadow: inset 0 0 0 2px st-transparentize(st-mix(-st-accent-color, #ffffff, 60%), 0.2) !important; @@ -315,7 +315,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:focus:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .login-dialog-auth-list-item:focus:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:focus:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .web-login-intro-button:focus:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .login-button:focus:hover, + .unlock-dialog .web-login-prompt-button:focus:hover, .unlock-dialog .login-dialog-auth-list-title:focus:hover, .unlock-dialog .login-dialog-auth-list-item:focus:hover { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), rgba(250, 250, 251, 0.13), 5%); } .unlock-dialog-notifications-container .message StButton:hover, @@ -330,7 +330,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .login-dialog-auth-list-item:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .web-login-intro-button:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .login-button:hover, + .unlock-dialog .web-login-prompt-button:hover, .unlock-dialog .login-dialog-auth-list-title:hover, .unlock-dialog .login-dialog-auth-list-item:hover { color: #fafafb; background-color: rgba(250, 250, 251, 0.13); } @@ -345,7 +345,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:active, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .login-dialog-auth-list-item:active { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .web-login-intro-button:active, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .login-button:active, + .unlock-dialog .web-login-prompt-button:active, .unlock-dialog .login-dialog-auth-list-title:active, .unlock-dialog .login-dialog-auth-list-item:active { color: #fafafb; background-color: rgba(250, 250, 251, 0.16); } @@ -360,7 +360,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:active:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .login-dialog-auth-list-item:active:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .web-login-intro-button:active:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .login-button:active:hover, + .unlock-dialog .web-login-prompt-button:active:hover, .unlock-dialog .login-dialog-auth-list-title:active:hover, .unlock-dialog .login-dialog-auth-list-item:active:hover { background-color: rgba(250, 250, 251, 0.19); } .unlock-dialog-notifications-container .message StButton:active:focus, @@ -375,7 +375,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:active:focus, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .login-dialog-auth-list-item:active:focus { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active:focus, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .web-login-intro-button:active:focus, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .login-button:active:focus, + .unlock-dialog .web-login-prompt-button:active:focus, .unlock-dialog .login-dialog-auth-list-title:active:focus, .unlock-dialog .login-dialog-auth-list-item:active:focus { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), rgba(250, 250, 251, 0.16), 5%); } .unlock-dialog-notifications-container .message StButton:checked, @@ -390,7 +390,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .login-dialog-auth-list-item:checked { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .web-login-intro-button:checked, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .login-button:checked, + .unlock-dialog .web-login-prompt-button:checked, .unlock-dialog .login-dialog-auth-list-title:checked, .unlock-dialog .login-dialog-auth-list-item:checked { color: #fafafb; background-color: st-lighten(st-mix(#fafafb, #36363a, 9%), 8%); } @@ -405,7 +405,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .login-dialog-auth-list-item:checked:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .web-login-intro-button:checked:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .login-button:checked:hover, + .unlock-dialog .web-login-prompt-button:checked:hover, .unlock-dialog .login-dialog-auth-list-title:checked:hover, .unlock-dialog .login-dialog-auth-list-item:checked:hover { background-color: st-lighten(st-lighten(st-mix(#fafafb, #36363a, 9%), 8%), 4%); } .unlock-dialog-notifications-container .message StButton:checked:active, @@ -420,7 +420,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked:active, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .login-dialog-auth-list-item:checked:active { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked:active, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .web-login-intro-button:checked:active, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .login-button:checked:active, + .unlock-dialog .web-login-prompt-button:checked:active, .unlock-dialog .login-dialog-auth-list-title:checked:active, .unlock-dialog .login-dialog-auth-list-item:checked:active { background-color: st-lighten(st-lighten(st-mix(#fafafb, #36363a, 9%), 8%), 9%); } .unlock-dialog-notifications-container .message StButton:insensitive, @@ -435,7 +435,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog .login-dialog-button.switch-user-button:insensitive, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .login-dialog-auth-list-item:insensitive { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:insensitive, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .web-login-intro-button:insensitive, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .login-button:insensitive, + .unlock-dialog .web-login-prompt-button:insensitive, .unlock-dialog .login-dialog-auth-list-title:insensitive, .unlock-dialog .login-dialog-auth-list-item:insensitive { color: st-transparentize(#fafafb, 0.5); - background-color: st-darken(st-mix(#fafafb, #36363a, 9%), 3%); } @@ -567,20 +567,18 @@ index 82e68c5..e112db8 100644 .login-dialog, .unlock-dialog { color: #fafafb; } -@@ -3042,21 +3090,88 @@ StScrollBar { +@@ -3042,21 +3090,86 @@ StScrollBar { color: #fafafb; } .login-dialog .login-dialog-prompt-layout, .unlock-dialog .login-dialog-prompt-layout { - width: 25em; + width: 30em; -+ margin-top: 80px; } ++ margin-top: 80px; + spacing: 9px; } + .login-dialog .login-dialog-prompt-layout.web-login-active, + .unlock-dialog .login-dialog-prompt-layout.web-login-active { + width: 37.5em; + margin-top: 0; } -+ .login-dialog .login-dialog-input-well, -+ .unlock-dialog .login-dialog-input-well { - spacing: 9px; } + .login-dialog .login-dialog-prompt-entry-area, + .unlock-dialog .login-dialog-prompt-entry-area { + margin: 0.5em 20px; } @@ -627,19 +625,19 @@ index 82e68c5..e112db8 100644 + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { + font-size: 0.909em; + font-weight: bold; -+ color: #fafafb; } ++ color: #ffffff; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { -+ color: #fafafb; ++ color: #ffffff; + icon-size: 1.091em; + margin-right: 8px; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box { + spacing: 3px; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-name { -+ color: #fafafb; ++ color: #ffffff; + font-size: 1em; + font-weight: bold; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { -+ color: #fafafb; ++ color: #ffffff; + font-size: 0.909em; } + +.login-dialog-auth-menu-button-indicator { @@ -660,7 +658,7 @@ index 82e68c5..e112db8 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3118,49 +3233,104 @@ StScrollBar { +@@ -3118,49 +3231,106 @@ StScrollBar { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-lighten(#222226, 9%), 5%); } .login-dialog-auth-list-view { @@ -732,6 +730,8 @@ index 82e68c5..e112db8 100644 + padding: 9px; + margin-bottom: 4px; + margin-right: 4px; } ++ .login-dialog .login-dialog-auth-list-item.compact { ++ min-height: 1.3em; } .unlock-dialog .login-dialog-auth-list-item { - border-radius: 9.6px; @@ -751,10 +751,7 @@ index 82e68c5..e112db8 100644 +.login-dialog-auth-list-title-label { + padding: 6px; + text-align: center; } - --.login-dialog-auth-list-label:ltr { -- padding-left: 15px; -- text-align: left; } ++ +.login-dialog-auth-list-item-title, +.login-dialog-auth-list-item-subtitle { + text-align: center; @@ -762,7 +759,10 @@ index 82e68c5..e112db8 100644 + +.login-dialog-auth-list-item-title { + color: #fafafb; } -+ + +-.login-dialog-auth-list-label:ltr { +- padding-left: 15px; +- text-align: left; } +.login-dialog-auth-list-item-subtitle { + color: #c1c1ce; + font-weight: 500; } @@ -801,7 +801,7 @@ index 82e68c5..e112db8 100644 .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .user-icon { border: 2px solid transparent; } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .login-dialog-timed-login-indicator { -@@ -3200,6 +3373,104 @@ StScrollBar { +@@ -3200,6 +3373,107 @@ StScrollBar { .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:logged-in .user-icon StIcon { background-color: st-transparentize(-st-accent-color, 0.7); } @@ -840,63 +840,66 @@ index 82e68c5..e112db8 100644 + padding-right: 27px; + spacing: 1.75em; } + -+.web-login-button-label { -+ font-size: 1.182em; -+ color: #fafafb; -+ min-width: 12em; -+ text-align: center; -+ font-weight: bold; } -+ -+.login-dialog .web-login-intro-button, ++.login-dialog .login-button, +.login-dialog .web-login-prompt-button { + color: #fafafb; + background-color: st-mix(#fafafb, #222226, 9%); ++ font-size: 1.182em; ++ color: #fafafb; ++ text-align: center; ++ font-weight: bold; + border-radius: 32px; } -+ .login-dialog .web-login-intro-button:selected, .login-dialog .web-login-intro-button:focus, ++ .login-dialog .login-button:selected, .login-dialog .login-button:focus, + .login-dialog .web-login-prompt-button:selected, + .login-dialog .web-login-prompt-button:focus { + color: #fafafb; + box-shadow: inset 0 0 0 2px st-transparentize(st-mix(-st-accent-color, #ffffff, 60%), 0.2) !important; + background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(#fafafb, #222226, 9%), 5%); } -+ .login-dialog .web-login-intro-button:selected:hover, .login-dialog .web-login-intro-button:focus:hover, ++ .login-dialog .login-button:selected:hover, .login-dialog .login-button:focus:hover, + .login-dialog .web-login-prompt-button:selected:hover, + .login-dialog .web-login-prompt-button:focus:hover { + background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-lighten(st-mix(#fafafb, #222226, 9%), 4%), 5%); } -+ .login-dialog .web-login-intro-button:hover, ++ .login-dialog .login-button:hover, + .login-dialog .web-login-prompt-button:hover { + color: #fafafb; + background-color: st-lighten(st-mix(#fafafb, #222226, 9%), 4%); } -+ .login-dialog .web-login-intro-button:active, ++ .login-dialog .login-button:active, + .login-dialog .web-login-prompt-button:active { + color: #fafafb; + background-color: st-lighten(st-mix(#fafafb, #222226, 9%), 9%); } -+ .login-dialog .web-login-intro-button:active:hover, ++ .login-dialog .login-button:active:hover, + .login-dialog .web-login-prompt-button:active:hover { + background-color: st-lighten(st-lighten(st-mix(#fafafb, #222226, 9%), 9%), 4%); } -+ .login-dialog .web-login-intro-button:active:focus, ++ .login-dialog .login-button:active:focus, + .login-dialog .web-login-prompt-button:active:focus { + background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-lighten(st-mix(#fafafb, #222226, 9%), 9%), 5%); } -+ .login-dialog .web-login-intro-button:insensitive, ++ .login-dialog .login-button:insensitive, + .login-dialog .web-login-prompt-button:insensitive { + color: st-transparentize(#fafafb, 0.5); + background-color: st-darken(st-mix(#fafafb, #222226, 9%), 3%); } + -+.login-dialog .web-login-intro-button { ++.login-dialog .login-button { + padding: 0; -+ margin: 0.2em 20px; } ++ margin: 0.2em 20px; ++ min-width: 12em; } + +.login-dialog .web-login-prompt-button { + padding: 15px 24px; + margin: 24px 8px; + width: 8em; } + -+.unlock-dialog .web-login-intro-button, ++.unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button { ++ font-size: 1.182em; ++ color: #fafafb; ++ text-align: center; ++ font-weight: bold; + border-radius: 32px; } + -+.unlock-dialog .web-login-intro-button { ++.unlock-dialog .login-button { + padding: 0; -+ margin: 0.2em 20px; } ++ margin: 0.2em 20px; ++ min-width: 12em; } + +.unlock-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -906,7 +909,7 @@ index 82e68c5..e112db8 100644 .unlock-dialog { background-color: transparent; } -@@ -3212,7 +3483,8 @@ StScrollBar { +@@ -3212,7 +3486,8 @@ StScrollBar { .unlock-dialog-clock { color: #fafafb; @@ -917,7 +920,7 @@ index 82e68c5..e112db8 100644 font-size: 6.546em; font-weight: 800; } diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css -index b69823f..bc41454 100644 +index b69823f..9dcb6f8 100644 --- a/data/theme/gnome-shell-high-contrast.css +++ b/data/theme/gnome-shell-high-contrast.css @@ -42,7 +42,9 @@ stage { @@ -925,7 +928,7 @@ index b69823f..bc41454 100644 transition-duration: 100ms; } -.login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item, .login-dialog .login-dialog-auth-list-item, .login-dialog-not-listed-button, .unlock-dialog-notifications-container .message StButton, -+.login-dialog .web-login-intro-button, ++.login-dialog .login-button, +.login-dialog .web-login-prompt-button, .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item, .login-dialog .login-dialog-auth-list-title, +.login-dialog .login-dialog-auth-list-item, .login-dialog-not-listed-button, .unlock-dialog-notifications-container .message StButton, .unlock-dialog-notifications-container .unlock-dialog-notification-source StButton, .unlock-dialog .button, @@ -937,7 +940,7 @@ index b69823f..bc41454 100644 .message .message-header .unlock-dialog .message-close-button, -.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .login-dialog-auth-list-item, #LookingGlassDialog .notebook-tab, .screenshot-ui-show-pointer-button, .screenshot-ui-type-button, #LookingGlassDebugFlags .lg-debug-flag-button, #LookingGlassPropertyInspector .lg-obj-inspector-close-button, -#LookingGlassPropertyInspector .lg-obj-inspector-button, #LookingGlassDialog > #Toolbar .lg-toolbar-button, .icon-button, .login-dialog-button.a11y-button, .login-dialog-button.cancel-button, .login-dialog-button.switch-user-button, .login-dialog-button.login-dialog-session-list-button, .message .message-header .message-expand-button, -+.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .web-login-intro-button, ++.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button, .unlock-dialog .login-dialog-auth-list-title, .unlock-dialog .login-dialog-auth-list-item, #LookingGlassDialog .notebook-tab, .screenshot-ui-show-pointer-button, .screenshot-ui-type-button, #LookingGlassDebugFlags .lg-debug-flag-button, #LookingGlassPropertyInspector .lg-obj-inspector-close-button, +#LookingGlassPropertyInspector .lg-obj-inspector-button, #LookingGlassDialog > #Toolbar .lg-toolbar-button, .icon-button, .login-dialog-button.next-button, .login-dialog-button.a11y-button, .login-dialog-button.cancel-button, .login-dialog-button.switch-user-button, .login-dialog-button.login-dialog-auth-menu-button, .login-dialog-button.login-dialog-session-list-button, .message .message-header .message-expand-button, .message .message-header .message-close-button, .message-notification-group .message-collapse-button, .calendar .calendar-month-header .pager-button, .button { @@ -1240,7 +1243,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button, -.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .login-dialog-auth-list-item { +.unlock-dialog .login-dialog-button.login-dialog-auth-menu-button, -+.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .web-login-intro-button, ++.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button, .unlock-dialog .login-dialog-auth-list-title, .unlock-dialog .login-dialog-auth-list-item { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); color: #ffffff; @@ -1255,7 +1258,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:focus, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .login-dialog-auth-list-item:focus { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:focus, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .web-login-intro-button:focus, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .login-button:focus, + .unlock-dialog .web-login-prompt-button:focus, .unlock-dialog .login-dialog-auth-list-title:focus, .unlock-dialog .login-dialog-auth-list-item:focus { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); color: #ffffff; @@ -1270,7 +1273,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:focus:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .login-dialog-auth-list-item:focus:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:focus:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .web-login-intro-button:focus:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .login-button:focus:hover, + .unlock-dialog .web-login-prompt-button:focus:hover, .unlock-dialog .login-dialog-auth-list-title:focus:hover, .unlock-dialog .login-dialog-auth-list-item:focus:hover { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(rgba(255, 255, 255, 0.13), #ffffff, 87%), 5%); } .unlock-dialog-notifications-container .message StButton:hover, @@ -1285,7 +1288,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .login-dialog-auth-list-item:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .web-login-intro-button:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .login-button:hover, + .unlock-dialog .web-login-prompt-button:hover, .unlock-dialog .login-dialog-auth-list-title:hover, .unlock-dialog .login-dialog-auth-list-item:hover { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); color: #ffffff; @@ -1300,7 +1303,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:active, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .login-dialog-auth-list-item:active { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .web-login-intro-button:active, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .login-button:active, + .unlock-dialog .web-login-prompt-button:active, .unlock-dialog .login-dialog-auth-list-title:active, .unlock-dialog .login-dialog-auth-list-item:active { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); color: #ffffff; @@ -1315,7 +1318,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:active:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .login-dialog-auth-list-item:active:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .web-login-intro-button:active:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .login-button:active:hover, + .unlock-dialog .web-login-prompt-button:active:hover, .unlock-dialog .login-dialog-auth-list-title:active:hover, .unlock-dialog .login-dialog-auth-list-item:active:hover { background-color: rgba(255, 255, 255, 0.19); } .unlock-dialog-notifications-container .message StButton:active:focus, @@ -1330,7 +1333,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:active:focus, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .login-dialog-auth-list-item:active:focus { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active:focus, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .web-login-intro-button:active:focus, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .login-button:active:focus, + .unlock-dialog .web-login-prompt-button:active:focus, .unlock-dialog .login-dialog-auth-list-title:active:focus, .unlock-dialog .login-dialog-auth-list-item:active:focus { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(rgba(255, 255, 255, 0.16), #ffffff, 87%), 5%); } .unlock-dialog-notifications-container .message StButton:checked, @@ -1345,7 +1348,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .login-dialog-auth-list-item:checked { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .web-login-intro-button:checked, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .login-button:checked, + .unlock-dialog .web-login-prompt-button:checked, .unlock-dialog .login-dialog-auth-list-title:checked, .unlock-dialog .login-dialog-auth-list-item:checked { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); color: #ffffff; @@ -1360,7 +1363,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .login-dialog-auth-list-item:checked:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .web-login-intro-button:checked:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .login-button:checked:hover, + .unlock-dialog .web-login-prompt-button:checked:hover, .unlock-dialog .login-dialog-auth-list-title:checked:hover, .unlock-dialog .login-dialog-auth-list-item:checked:hover { background-color: st-lighten(st-lighten(st-mix(#ffffff, #1a1a1a, 9%), 8%), 4%); } .unlock-dialog-notifications-container .message StButton:checked:active, @@ -1375,7 +1378,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked:active, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .login-dialog-auth-list-item:checked:active { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked:active, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .web-login-intro-button:checked:active, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .login-button:checked:active, + .unlock-dialog .web-login-prompt-button:checked:active, .unlock-dialog .login-dialog-auth-list-title:checked:active, .unlock-dialog .login-dialog-auth-list-item:checked:active { background-color: st-lighten(st-lighten(st-mix(#ffffff, #1a1a1a, 9%), 8%), 9%); } .unlock-dialog-notifications-container .message StButton:insensitive, @@ -1390,7 +1393,7 @@ index b69823f..bc41454 100644 .unlock-dialog .login-dialog-button.switch-user-button:insensitive, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .login-dialog-auth-list-item:insensitive { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:insensitive, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .web-login-intro-button:insensitive, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .login-button:insensitive, + .unlock-dialog .web-login-prompt-button:insensitive, .unlock-dialog .login-dialog-auth-list-title:insensitive, .unlock-dialog .login-dialog-auth-list-item:insensitive { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); color: st-transparentize(#ffffff, 0.5); @@ -1526,20 +1529,18 @@ index b69823f..bc41454 100644 .login-dialog, .unlock-dialog { color: #ffffff; } -@@ -3372,21 +3420,88 @@ StScrollBar { +@@ -3372,21 +3420,86 @@ StScrollBar { color: #ffffff; } .login-dialog .login-dialog-prompt-layout, .unlock-dialog .login-dialog-prompt-layout { - width: 25em; + width: 30em; -+ margin-top: 80px; } ++ margin-top: 80px; + spacing: 9px; } + .login-dialog .login-dialog-prompt-layout.web-login-active, + .unlock-dialog .login-dialog-prompt-layout.web-login-active { + width: 37.5em; + margin-top: 0; } -+ .login-dialog .login-dialog-input-well, -+ .unlock-dialog .login-dialog-input-well { - spacing: 9px; } + .login-dialog .login-dialog-prompt-entry-area, + .unlock-dialog .login-dialog-prompt-entry-area { + margin: 0.5em 20px; } @@ -1619,7 +1620,7 @@ index b69823f..bc41454 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3455,53 +3570,112 @@ StScrollBar { +@@ -3455,53 +3568,114 @@ StScrollBar { background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(st-lighten(#000000, 9%), #ffffff, 87%), 5%); } .login-dialog-auth-list-view { @@ -1699,6 +1700,8 @@ index b69823f..bc41454 100644 + padding: 9px; + margin-bottom: 4px; + margin-right: 4px; } ++ .login-dialog .login-dialog-auth-list-item.compact { ++ min-height: 1.3em; } .unlock-dialog .login-dialog-auth-list-item { - border-radius: 9.6px; @@ -1772,7 +1775,7 @@ index b69823f..bc41454 100644 .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .user-icon { border: 2px solid transparent; } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .login-dialog-timed-login-indicator { -@@ -3545,6 +3726,112 @@ StScrollBar { +@@ -3545,6 +3726,115 @@ StScrollBar { .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:logged-in .user-icon StIcon { background-color: st-transparentize(-st-accent-color, 0.7); } @@ -1811,47 +1814,44 @@ index b69823f..bc41454 100644 + padding-right: 27px; + spacing: 1.75em; } + -+.web-login-button-label { -+ font-size: 1.182em; -+ color: #ffffff; -+ min-width: 12em; -+ text-align: center; -+ font-weight: bold; } -+ -+.login-dialog .web-login-intro-button, ++.login-dialog .login-button, +.login-dialog .web-login-prompt-button { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); + color: #ffffff; + background-color: st-mix(st-mix(#ffffff, #000000, 9%), #ffffff, 87%); ++ font-size: 1.182em; ++ color: #ffffff; ++ text-align: center; ++ font-weight: bold; + border-radius: 32px; } -+ .login-dialog .web-login-intro-button:selected, .login-dialog .web-login-intro-button:focus, ++ .login-dialog .login-button:selected, .login-dialog .login-button:focus, + .login-dialog .web-login-prompt-button:selected, + .login-dialog .web-login-prompt-button:focus { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); + color: #ffffff; + box-shadow: inset 0 0 0 2px st-transparentize(st-mix(-st-accent-color, #ffffff, 60%), 0.1) !important; + background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(st-mix(#ffffff, #000000, 9%), #ffffff, 87%), 5%); } -+ .login-dialog .web-login-intro-button:selected:hover, .login-dialog .web-login-intro-button:focus:hover, ++ .login-dialog .login-button:selected:hover, .login-dialog .login-button:focus:hover, + .login-dialog .web-login-prompt-button:selected:hover, + .login-dialog .web-login-prompt-button:focus:hover { + background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(st-lighten(st-mix(#ffffff, #000000, 9%), 4%), #ffffff, 87%), 5%); } -+ .login-dialog .web-login-intro-button:hover, ++ .login-dialog .login-button:hover, + .login-dialog .web-login-prompt-button:hover { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); + color: #ffffff; + background-color: st-mix(st-lighten(st-mix(#ffffff, #000000, 9%), 4%), #ffffff, 87%); } -+ .login-dialog .web-login-intro-button:active, ++ .login-dialog .login-button:active, + .login-dialog .web-login-prompt-button:active { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); + color: #ffffff; + background-color: st-mix(st-lighten(st-mix(#ffffff, #000000, 9%), 9%), #ffffff, 87%); } -+ .login-dialog .web-login-intro-button:active:hover, ++ .login-dialog .login-button:active:hover, + .login-dialog .web-login-prompt-button:active:hover { + background-color: st-lighten(st-lighten(st-mix(#ffffff, #000000, 9%), 9%), 4%); } -+ .login-dialog .web-login-intro-button:active:focus, ++ .login-dialog .login-button:active:focus, + .login-dialog .web-login-prompt-button:active:focus { + background-color: st-mix(st-mix(-st-accent-color, #ffffff, 60%), st-mix(st-lighten(st-mix(#ffffff, #000000, 9%), 9%), #ffffff, 87%), 5%); } -+ .login-dialog .web-login-intro-button:insensitive, ++ .login-dialog .login-button:insensitive, + .login-dialog .web-login-prompt-button:insensitive { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); + color: st-transparentize(#ffffff, 0.5); @@ -1860,22 +1860,28 @@ index b69823f..bc41454 100644 + box-shadow: none; + border: none; } + -+.login-dialog .web-login-intro-button { ++.login-dialog .login-button { + padding: 0; -+ margin: 0.2em 20px; } ++ margin: 0.2em 20px; ++ min-width: 12em; } + +.login-dialog .web-login-prompt-button { + padding: 15px 24px; + margin: 24px 8px; + width: 8em; } + -+.unlock-dialog .web-login-intro-button, ++.unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button { ++ font-size: 1.182em; ++ color: #ffffff; ++ text-align: center; ++ font-weight: bold; + border-radius: 32px; } + -+.unlock-dialog .web-login-intro-button { ++.unlock-dialog .login-button { + padding: 0; -+ margin: 0.2em 20px; } ++ margin: 0.2em 20px; ++ min-width: 12em; } + +.unlock-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -1885,7 +1891,7 @@ index b69823f..bc41454 100644 .unlock-dialog { background-color: transparent; } -@@ -3557,7 +3844,8 @@ StScrollBar { +@@ -3557,7 +3847,8 @@ StScrollBar { .unlock-dialog-clock { color: #ffffff; @@ -1896,7 +1902,7 @@ index b69823f..bc41454 100644 font-size: 6.546em; font-weight: 800; } diff --git a/data/theme/gnome-shell-light.css b/data/theme/gnome-shell-light.css -index 2b67846..c3288bb 100644 +index 2b67846..d74cfb3 100644 --- a/data/theme/gnome-shell-light.css +++ b/data/theme/gnome-shell-light.css @@ -42,7 +42,9 @@ stage { @@ -1904,7 +1910,7 @@ index 2b67846..c3288bb 100644 transition-duration: 100ms; } -.login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item, .login-dialog .login-dialog-auth-list-item, .login-dialog-not-listed-button, .unlock-dialog-notifications-container .message StButton, -+.login-dialog .web-login-intro-button, ++.login-dialog .login-button, +.login-dialog .web-login-prompt-button, .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item, .login-dialog .login-dialog-auth-list-title, +.login-dialog .login-dialog-auth-list-item, .login-dialog-not-listed-button, .unlock-dialog-notifications-container .message StButton, .unlock-dialog-notifications-container .unlock-dialog-notification-source StButton, .unlock-dialog .button, @@ -1916,7 +1922,7 @@ index 2b67846..c3288bb 100644 .message .message-header .unlock-dialog .message-close-button, -.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .login-dialog-auth-list-item, #LookingGlassDialog .notebook-tab, .screenshot-ui-show-pointer-button, .screenshot-ui-type-button, #LookingGlassDebugFlags .lg-debug-flag-button, #LookingGlassPropertyInspector .lg-obj-inspector-close-button, -#LookingGlassPropertyInspector .lg-obj-inspector-button, #LookingGlassDialog > #Toolbar .lg-toolbar-button, .icon-button, .login-dialog-button.a11y-button, .login-dialog-button.cancel-button, .login-dialog-button.switch-user-button, .login-dialog-button.login-dialog-session-list-button, .message .message-header .message-expand-button, -+.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .web-login-intro-button, ++.unlock-dialog .screenshot-ui-show-pointer-button, .unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button, .unlock-dialog .login-dialog-auth-list-title, .unlock-dialog .login-dialog-auth-list-item, #LookingGlassDialog .notebook-tab, .screenshot-ui-show-pointer-button, .screenshot-ui-type-button, #LookingGlassDebugFlags .lg-debug-flag-button, #LookingGlassPropertyInspector .lg-obj-inspector-close-button, +#LookingGlassPropertyInspector .lg-obj-inspector-button, #LookingGlassDialog > #Toolbar .lg-toolbar-button, .icon-button, .login-dialog-button.next-button, .login-dialog-button.a11y-button, .login-dialog-button.cancel-button, .login-dialog-button.switch-user-button, .login-dialog-button.login-dialog-auth-menu-button, .login-dialog-button.login-dialog-session-list-button, .message .message-header .message-expand-button, .message .message-header .message-close-button, .message-notification-group .message-collapse-button, .calendar .calendar-month-header .pager-button, .button { @@ -2171,7 +2177,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button, -.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .login-dialog-auth-list-item { +.unlock-dialog .login-dialog-button.login-dialog-auth-menu-button, -+.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .web-login-intro-button, ++.unlock-dialog .login-dialog-button.login-dialog-session-list-button, .unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button, .unlock-dialog .login-dialog-auth-list-title, .unlock-dialog .login-dialog-auth-list-item { color: #fafafb; background-color: rgba(250, 250, 251, 0.1); } @@ -2186,7 +2192,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:focus, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .login-dialog-auth-list-item:focus { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:focus, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .web-login-intro-button:focus, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus, .unlock-dialog .login-button:focus, + .unlock-dialog .web-login-prompt-button:focus, .unlock-dialog .login-dialog-auth-list-title:focus, .unlock-dialog .login-dialog-auth-list-item:focus { color: #fafafb; box-shadow: inset 0 0 0 2px st-transparentize(-st-accent-color, 0.2) !important; @@ -2201,7 +2207,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:focus:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .login-dialog-auth-list-item:focus:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:focus:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .web-login-intro-button:focus:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:focus:hover, .unlock-dialog .login-button:focus:hover, + .unlock-dialog .web-login-prompt-button:focus:hover, .unlock-dialog .login-dialog-auth-list-title:focus:hover, .unlock-dialog .login-dialog-auth-list-item:focus:hover { background-color: st-mix(-st-accent-color, rgba(250, 250, 251, 0.13), 5%); } .unlock-dialog-notifications-container .message StButton:hover, @@ -2216,7 +2222,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .login-dialog-auth-list-item:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .web-login-intro-button:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:hover, .unlock-dialog .login-button:hover, + .unlock-dialog .web-login-prompt-button:hover, .unlock-dialog .login-dialog-auth-list-title:hover, .unlock-dialog .login-dialog-auth-list-item:hover { color: #fafafb; background-color: rgba(250, 250, 251, 0.13); } @@ -2231,7 +2237,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:active, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .login-dialog-auth-list-item:active { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .web-login-intro-button:active, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active, .unlock-dialog .login-button:active, + .unlock-dialog .web-login-prompt-button:active, .unlock-dialog .login-dialog-auth-list-title:active, .unlock-dialog .login-dialog-auth-list-item:active { color: #fafafb; background-color: rgba(250, 250, 251, 0.16); } @@ -2246,7 +2252,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:active:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .login-dialog-auth-list-item:active:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .web-login-intro-button:active:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:hover, .unlock-dialog .login-button:active:hover, + .unlock-dialog .web-login-prompt-button:active:hover, .unlock-dialog .login-dialog-auth-list-title:active:hover, .unlock-dialog .login-dialog-auth-list-item:active:hover { background-color: rgba(250, 250, 251, 0.19); } .unlock-dialog-notifications-container .message StButton:active:focus, @@ -2261,7 +2267,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:active:focus, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .login-dialog-auth-list-item:active:focus { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:active:focus, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .web-login-intro-button:active:focus, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:active:focus, .unlock-dialog .login-button:active:focus, + .unlock-dialog .web-login-prompt-button:active:focus, .unlock-dialog .login-dialog-auth-list-title:active:focus, .unlock-dialog .login-dialog-auth-list-item:active:focus { background-color: st-mix(-st-accent-color, rgba(250, 250, 251, 0.16), 5%); } .unlock-dialog-notifications-container .message StButton:checked, @@ -2276,7 +2282,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .login-dialog-auth-list-item:checked { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .web-login-intro-button:checked, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked, .unlock-dialog .login-button:checked, + .unlock-dialog .web-login-prompt-button:checked, .unlock-dialog .login-dialog-auth-list-title:checked, .unlock-dialog .login-dialog-auth-list-item:checked { color: #fafafb; background-color: st-darken(st-mix(#fafafb, #fafafb, 12%), 8%); } @@ -2291,7 +2297,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked:hover, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .login-dialog-auth-list-item:checked:hover { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked:hover, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .web-login-intro-button:checked:hover, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:hover, .unlock-dialog .login-button:checked:hover, + .unlock-dialog .web-login-prompt-button:checked:hover, .unlock-dialog .login-dialog-auth-list-title:checked:hover, .unlock-dialog .login-dialog-auth-list-item:checked:hover { background-color: st-darken(st-darken(st-mix(#fafafb, #fafafb, 12%), 8%), 4%); } .unlock-dialog-notifications-container .message StButton:checked:active, @@ -2306,7 +2312,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:checked:active, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .login-dialog-auth-list-item:checked:active { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:checked:active, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .web-login-intro-button:checked:active, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:checked:active, .unlock-dialog .login-button:checked:active, + .unlock-dialog .web-login-prompt-button:checked:active, .unlock-dialog .login-dialog-auth-list-title:checked:active, .unlock-dialog .login-dialog-auth-list-item:checked:active { background-color: st-darken(st-darken(st-mix(#fafafb, #fafafb, 12%), 8%), 9%); } .unlock-dialog-notifications-container .message StButton:insensitive, @@ -2321,7 +2327,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog .login-dialog-button.switch-user-button:insensitive, - .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .login-dialog-auth-list-item:insensitive { + .unlock-dialog .login-dialog-button.login-dialog-auth-menu-button:insensitive, -+ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .web-login-intro-button:insensitive, ++ .unlock-dialog .login-dialog-button.login-dialog-session-list-button:insensitive, .unlock-dialog .login-button:insensitive, + .unlock-dialog .web-login-prompt-button:insensitive, .unlock-dialog .login-dialog-auth-list-title:insensitive, .unlock-dialog .login-dialog-auth-list-item:insensitive { color: st-transparentize(#fafafb, 0.6); - background-color: st-lighten(st-mix(#fafafb, #fafafb, 12%), 3%); } @@ -2439,7 +2445,7 @@ index 2b67846..c3288bb 100644 .app-folder-dialog .message .message-header .message-close-button:active:focus, .message .message-header .app-folder-dialog .message-close-button:active:focus, .app-folder-dialog .screenshot-ui-show-pointer-button:active:focus { background-color: st-mix(-st-accent-color, st-lighten(st-mix(#fafafb, #38383b, 12%), 9%), 5%); } -@@ -3033,6 +3074,12 @@ StScrollBar { +@@ -3033,6 +3074,13 @@ StScrollBar { background-color: st-lighten(-st-accent-color, 5%); color: st-lighten(-st-accent-fg-color, 5%); } @@ -2447,25 +2453,24 @@ index 2b67846..c3288bb 100644 + border-radius: 4px; + border-width: 1em; + background-color: #e9e9ea; -+ border-color: #e9e9ea; } ++ border-color: #e9e9ea; ++ color: #222226; } + .login-dialog, .unlock-dialog { color: #fafafb; } -@@ -3043,21 +3090,88 @@ StScrollBar { +@@ -3043,21 +3091,86 @@ StScrollBar { color: #fafafb; } .login-dialog .login-dialog-prompt-layout, .unlock-dialog .login-dialog-prompt-layout { - width: 25em; + width: 30em; -+ margin-top: 80px; } ++ margin-top: 80px; + spacing: 9px; } + .login-dialog .login-dialog-prompt-layout.web-login-active, + .unlock-dialog .login-dialog-prompt-layout.web-login-active { + width: 37.5em; + margin-top: 0; } -+ .login-dialog .login-dialog-input-well, -+ .unlock-dialog .login-dialog-input-well { - spacing: 9px; } + .login-dialog .login-dialog-prompt-entry-area, + .unlock-dialog .login-dialog-prompt-entry-area { + margin: 0.5em 20px; } @@ -2512,19 +2517,19 @@ index 2b67846..c3288bb 100644 + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-section-label { + font-size: 0.909em; + font-weight: bold; -+ color: #fafafb; } ++ color: #222226; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-icon { -+ color: #fafafb; ++ color: #222226; + icon-size: 1.091em; + margin-right: 8px; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box { + spacing: 3px; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-name { -+ color: #fafafb; ++ color: #222226; + font-size: 1em; + font-weight: bold; } + .login-dialog-auth-menu-button-popup .login-dialog-auth-menu-item-box-description { -+ color: #fafafb; ++ color: #222226; + font-size: 0.909em; } + +.login-dialog-auth-menu-button-indicator { @@ -2545,7 +2550,7 @@ index 2b67846..c3288bb 100644 .conflicting-session-dialog-content { spacing: 20px; } -@@ -3119,49 +3233,104 @@ StScrollBar { +@@ -3119,49 +3232,106 @@ StScrollBar { background-color: st-mix(-st-accent-color, st-lighten(#222226, 9%), 5%); } .login-dialog-auth-list-view { @@ -2617,6 +2622,8 @@ index 2b67846..c3288bb 100644 + padding: 9px; + margin-bottom: 4px; + margin-right: 4px; } ++ .login-dialog .login-dialog-auth-list-item.compact { ++ min-height: 1.3em; } .unlock-dialog .login-dialog-auth-list-item { - border-radius: 9.6px; @@ -2626,19 +2633,13 @@ index 2b67846..c3288bb 100644 + padding: 9px; + margin-bottom: 4px; + margin-right: 4px; } - --.login-dialog-auth-list-label:ltr { -- padding-left: 15px; -- text-align: left; } ++ +.unlock-dialog .login-dialog-auth-list-title { + background-color: transparent !important; + color: #fafafb !important; + padding: 0; + margin: 0; } - --.login-dialog-auth-list-label:rtl { -- padding-right: 15px; -- text-align: right; } ++ +.login-dialog-auth-list-title-label { + padding: 6px; + text-align: center; } @@ -2650,11 +2651,17 @@ index 2b67846..c3288bb 100644 + +.login-dialog-auth-list-item-title { + color: #fafafb; } -+ + +-.login-dialog-auth-list-label:ltr { +- padding-left: 15px; +- text-align: left; } +.login-dialog-auth-list-item-subtitle { + color: #c1c1ce; + font-weight: 500; } -+ + +-.login-dialog-auth-list-label:rtl { +- padding-right: 15px; +- text-align: right; } +.login-dialog-item-icon { + width: 1.3em; + height: 1.3em; @@ -2676,7 +2683,7 @@ index 2b67846..c3288bb 100644 .login-dialog-user-list-view { width: 25em; -@@ -3190,6 +3359,9 @@ StScrollBar { +@@ -3190,6 +3360,9 @@ StScrollBar { background-color: st-lighten(st-lighten(st-mix(#fafafb, #222226, 12%), 9%), 4%); } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:active:focus { background-color: st-mix(-st-accent-color, st-lighten(st-mix(#fafafb, #222226, 12%), 9%), 5%); } @@ -2686,7 +2693,7 @@ index 2b67846..c3288bb 100644 .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .user-icon { border: 2px solid transparent; } .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item .login-dialog-timed-login-indicator { -@@ -3201,6 +3373,104 @@ StScrollBar { +@@ -3201,6 +3374,107 @@ StScrollBar { .login-dialog-user-list-view .login-dialog-user-list .login-dialog-user-list-item:logged-in .user-icon StIcon { background-color: st-transparentize(-st-accent-color, 0.7); } @@ -2725,63 +2732,66 @@ index 2b67846..c3288bb 100644 + padding-right: 27px; + spacing: 1.75em; } + -+.web-login-button-label { -+ font-size: 1.182em; -+ color: #fafafb; -+ min-width: 12em; -+ text-align: center; -+ font-weight: bold; } -+ -+.login-dialog .web-login-intro-button, ++.login-dialog .login-button, +.login-dialog .web-login-prompt-button { + color: #fafafb; + background-color: st-mix(#fafafb, #222226, 12%); ++ font-size: 1.182em; ++ color: #fafafb; ++ text-align: center; ++ font-weight: bold; + border-radius: 32px; } -+ .login-dialog .web-login-intro-button:selected, .login-dialog .web-login-intro-button:focus, ++ .login-dialog .login-button:selected, .login-dialog .login-button:focus, + .login-dialog .web-login-prompt-button:selected, + .login-dialog .web-login-prompt-button:focus { + color: #fafafb; + box-shadow: inset 0 0 0 2px st-transparentize(-st-accent-color, 0.2) !important; + background-color: st-mix(-st-accent-color, st-mix(#fafafb, #222226, 12%), 5%); } -+ .login-dialog .web-login-intro-button:selected:hover, .login-dialog .web-login-intro-button:focus:hover, ++ .login-dialog .login-button:selected:hover, .login-dialog .login-button:focus:hover, + .login-dialog .web-login-prompt-button:selected:hover, + .login-dialog .web-login-prompt-button:focus:hover { + background-color: st-mix(-st-accent-color, st-lighten(st-mix(#fafafb, #222226, 12%), 4%), 5%); } -+ .login-dialog .web-login-intro-button:hover, ++ .login-dialog .login-button:hover, + .login-dialog .web-login-prompt-button:hover { + color: #fafafb; + background-color: st-lighten(st-mix(#fafafb, #222226, 12%), 4%); } -+ .login-dialog .web-login-intro-button:active, ++ .login-dialog .login-button:active, + .login-dialog .web-login-prompt-button:active { + color: #fafafb; + background-color: st-lighten(st-mix(#fafafb, #222226, 12%), 9%); } -+ .login-dialog .web-login-intro-button:active:hover, ++ .login-dialog .login-button:active:hover, + .login-dialog .web-login-prompt-button:active:hover { + background-color: st-lighten(st-lighten(st-mix(#fafafb, #222226, 12%), 9%), 4%); } -+ .login-dialog .web-login-intro-button:active:focus, ++ .login-dialog .login-button:active:focus, + .login-dialog .web-login-prompt-button:active:focus { + background-color: st-mix(-st-accent-color, st-lighten(st-mix(#fafafb, #222226, 12%), 9%), 5%); } -+ .login-dialog .web-login-intro-button:insensitive, ++ .login-dialog .login-button:insensitive, + .login-dialog .web-login-prompt-button:insensitive { + color: st-transparentize(#fafafb, 0.6); + background-color: st-darken(st-mix(#fafafb, #222226, 12%), 3%); } + -+.login-dialog .web-login-intro-button { ++.login-dialog .login-button { + padding: 0; -+ margin: 0.2em 20px; } ++ margin: 0.2em 20px; ++ min-width: 12em; } + +.login-dialog .web-login-prompt-button { + padding: 15px 24px; + margin: 24px 8px; + width: 8em; } + -+.unlock-dialog .web-login-intro-button, ++.unlock-dialog .login-button, +.unlock-dialog .web-login-prompt-button { ++ font-size: 1.182em; ++ color: #fafafb; ++ text-align: center; ++ font-weight: bold; + border-radius: 32px; } + -+.unlock-dialog .web-login-intro-button { ++.unlock-dialog .login-button { + padding: 0; -+ margin: 0.2em 20px; } ++ margin: 0.2em 20px; ++ min-width: 12em; } + +.unlock-dialog .web-login-prompt-button { + padding: 15px 24px; @@ -2791,7 +2801,7 @@ index 2b67846..c3288bb 100644 .unlock-dialog { background-color: transparent; } -@@ -3213,7 +3483,8 @@ StScrollBar { +@@ -3213,7 +3487,8 @@ StScrollBar { .unlock-dialog-clock { color: #fafafb; @@ -2802,5 +2812,5 @@ index 2b67846..c3288bb 100644 font-size: 6.546em; font-weight: 800; } -- -2.54.0 +2.52.0 diff --git a/0001-gdm-Work-around-failing-fingerprint-auth.patch b/0001-gdm-Work-around-failing-fingerprint-auth.patch deleted file mode 100644 index 92b6768..0000000 --- a/0001-gdm-Work-around-failing-fingerprint-auth.patch +++ /dev/null @@ -1,96 +0,0 @@ -From 5293ee585f876169e31f3b6af737a42d30302f71 Mon Sep 17 00:00:00 2001 -From: Benjamin Berg -Date: Wed, 28 Apr 2021 16:50:03 +0200 -Subject: [PATCH] gdm: Work around failing fingerprint auth - -On Fedora we have the problem that fingerprint auth fails immediately if -the PAM configuration has not been updated and no prints are enrolled. - -So, consider a verification failure within one second to be a service -failure instead. ---- - js/gdm/authServicesLegacy.js | 47 ++++++++++++++++++++++++------------ - 1 file changed, 32 insertions(+), 15 deletions(-) - -diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 7b5739f14..99c034f64 100644 ---- a/js/gdm/authServicesLegacy.js -+++ b/js/gdm/authServicesLegacy.js -@@ -57,6 +57,7 @@ export class AuthServicesLegacy extends AuthServices { - this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager()); - - this._fingerprintReadyTimeoutId = 0; -+ this._fprintStartTime = -1; - } - - _handleSelectChoice(serviceName, key) { -@@ -114,18 +115,21 @@ export class AuthServicesLegacy extends AuthServices { - } - - _handleOnConversationStarted(serviceName) { -- if (serviceName !== Constants.FINGERPRINT_SERVICE_NAME || -- this._fingerprintReadyTimeoutId !== 0) -- return; -- -- this._fingerprintReadyTimeoutId = GLib.timeout_add( -- GLib.PRIORITY_DEFAULT, -- FINGERPRINT_READY_TIMEOUT_MS, -- () => { -- this._fingerprintReadyTimeoutId = 0; -- this._setFingerprintReady(true); -- return GLib.SOURCE_REMOVE; -- }); -+ if (serviceName === Constants.FINGERPRINT_SERVICE_NAME) { -+ this._fprintStartTime = GLib.get_monotonic_time(); -+ -+ -+ if (this._fingerprintReadyTimeoutId === 0) { -+ this._fingerprintReadyTimeoutId = GLib.timeout_add( -+ GLib.PRIORITY_DEFAULT, -+ FINGERPRINT_READY_TIMEOUT_MS, -+ () => { -+ this._fingerprintReadyTimeoutId = 0; -+ this._setFingerprintReady(true); -+ return GLib.SOURCE_REMOVE; -+ }); -+ } -+ } - } - - _setFingerprintReady(ready) { -@@ -231,6 +235,7 @@ export class AuthServicesLegacy extends AuthServices { - - this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, - FINGERPRINT_ERROR_TIMEOUT_WAIT, () => { -+ log("Generating _verificationFailed!"); - this._fingerprintFailedId = 0; - if (!this._cancellable.is_cancelled()) - this._verificationFailed(serviceName, false); -@@ -312,9 +317,21 @@ export class AuthServicesLegacy extends AuthServices { - - _handleVerificationFailed(serviceName) { - if (serviceName === Constants.FINGERPRINT_SERVICE_NAME && -- this._enabledMechanisms.some(m => m.serviceName === serviceName) && -- this._fingerprintFailedId) -- GLib.source_remove(this._fingerprintFailedId); -+ this._enabledMechanisms.some(m => m.serviceName === serviceName)) { -+ if (this._fingerprintFailedId) -+ GLib.source_remove(this._fingerprintFailedId); -+ -+ // On Fedora we have the problem that fingerprint auth fails -+ // immediately if the PAM configuration has not been updated and no -+ // prints are enrolled. -+ // So, consider a verification failure within one second to be a service -+ // failure instead. -+ if (this._fprintStartTime > GLib.get_monotonic_time() - GLib.USEC_PER_SEC) { -+ log("Fingerprint service failed almost immediately, considering it unavailable."); -+ log("Please fix your configuration by running: authselect select --force sssd with-fingerprint with-silent-lastlog"); -+ this._onServiceUnavailable(serviceName, null); -+ } -+ } - } - - _handleOnVerificationComplete(serviceName) { --- -2.54.0 - diff --git a/0002-post-changes-for-passwordless-gdm-backport.patch b/0002-post-changes-for-passwordless-gdm-backport.patch deleted file mode 100644 index f25a593..0000000 --- a/0002-post-changes-for-passwordless-gdm-backport.patch +++ /dev/null @@ -1,163 +0,0 @@ -From 5b24694b73b58a77db1e017d2c97bfc673236572 Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Tue, 2 Jun 2026 11:43:10 +0200 -Subject: [PATCH] Change inputWell by this - -In this version inputWell doesn't exist yet. ---- - js/gdm/authPrompt.js | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js -index e511b3881..3f2765f9c 100644 ---- a/js/gdm/authPrompt.js -+++ b/js/gdm/authPrompt.js -@@ -196,7 +196,7 @@ export const AuthPrompt = GObject.registerClass({ - }, - }); - }); -- this._inputWell.add_child(this._authList); -+ this.add_child(this._authList); - - // Use an insensitive button for the auth list title - // to get the same style as the auth list buttons -@@ -315,7 +315,7 @@ export const AuthPrompt = GObject.registerClass({ - } - }); - this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading)); -- this._inputWell.add_child(this._webLoginDialog); -+ this.add_child(this._webLoginDialog); - - // center elements inside _mainBox between the cancel - // button on the left and this spacer on the right --- -2.54.0 - -From 848071303afba7aa58486b643e5631a0d75d40b9 Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Tue, 2 Jun 2026 12:07:45 +0200 -Subject: [PATCH] Use Object.keys(this._sections) instead of - this._sections.keys() - ---- - js/gdm/authMenuButton.js | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js -index cbc1d7676..6a697cbc2 100644 ---- a/js/gdm/authMenuButton.js -+++ b/js/gdm/authMenuButton.js -@@ -252,7 +252,7 @@ export class AuthMenuButton extends St.Button { - this._items.delete(itemKey); - }); - -- this._sections.keys().forEach(sectionName => { -+ Object.keys(this._sections).forEach(sectionName => { - const itemsInSection = this._findItems({sectionName}); - if (itemsInSection.length === 0) { - const section = this._sections.get(sectionName); --- -2.54.0 - -From 8a987a7b32a1e3edbb04f9a9ff3077fac7d6a151 Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Tue, 2 Jun 2026 12:52:09 +0200 -Subject: [PATCH] Don't use _once timeout variants - ---- - js/gdm/authServices.js | 7 +++++-- - js/gdm/authServicesLegacy.js | 3 ++- - js/gdm/authServicesSSSDSwitchable.js | 3 ++- - 3 files changed, 9 insertions(+), 4 deletions(-) - -diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 2726d960a..410f0bc16 100644 ---- a/js/gdm/authServices.js -+++ b/js/gdm/authServices.js -@@ -260,8 +260,11 @@ export class AuthServices extends GObject.Object { - - _waitPendingMessages() { - const cancellable = this._cancellable; -- const timeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, 10, -- () => cancellable.cancel()); -+ const timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, -+ () => { -+ cancellable.cancel(); -+ return GLib.SOURCE_REMOVE; -+ }); - - const {promise, resolve, reject} = Promise.withResolvers(); - const task = Gio.Task.new(this, cancellable, () => { -diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js -index 132c5307c..7b5739f14 100644 ---- a/js/gdm/authServicesLegacy.js -+++ b/js/gdm/authServicesLegacy.js -@@ -118,12 +118,13 @@ export class AuthServicesLegacy extends AuthServices { - this._fingerprintReadyTimeoutId !== 0) - return; - -- this._fingerprintReadyTimeoutId = GLib.timeout_add_once( -+ this._fingerprintReadyTimeoutId = GLib.timeout_add( - GLib.PRIORITY_DEFAULT, - FINGERPRINT_READY_TIMEOUT_MS, - () => { - this._fingerprintReadyTimeoutId = 0; - this._setFingerprintReady(true); -+ return GLib.SOURCE_REMOVE; - }); - } - -diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js -index 4c8eb1ae1..f98c86e09 100644 ---- a/js/gdm/authServicesSSSDSwitchable.js -+++ b/js/gdm/authServicesSSSDSwitchable.js -@@ -197,7 +197,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - if (!timeout) - return; - -- this._webLoginTimeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, -+ this._webLoginTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, - timeout, () => { - if (this._selectedMechanism?.role !== Constants.WEB_LOGIN_ROLE_NAME) - webLoginMechanism.needsRefresh = true; -@@ -205,6 +205,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { - this.emit('reset', {softReset: true}); - - this._webLoginTimeoutId = 0; -+ return GLib.SOURCE_REMOVE; - }); - } - --- -2.54.0 - -From 0198f88dc153e80041ab32fb20f6294e1440a77d Mon Sep 17 00:00:00 2001 -From: Joan Torres Lopez -Date: Tue, 9 Jun 2026 09:57:09 +0200 -Subject: [PATCH] authServices: Use alternative to Promise.withResolvers() - -It doesn't exist in this version ---- - js/gdm/authServices.js | 6 +++++- - 1 file changed, 5 insertions(+), 1 deletion(-) - -diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js -index 410f0bc16..fbea348fe 100644 ---- a/js/gdm/authServices.js -+++ b/js/gdm/authServices.js -@@ -266,7 +266,11 @@ export class AuthServices extends GObject.Object { - return GLib.SOURCE_REMOVE; - }); - -- const {promise, resolve, reject} = Promise.withResolvers(); -+ let resolve, reject; -+ const promise = new Promise((res, rej) => { -+ resolve = res; -+ reject = rej; -+ }); - const task = Gio.Task.new(this, cancellable, () => { - try { - const res = task.propagate_boolean(); --- -2.54.0 - diff --git a/gnome-shell.spec b/gnome-shell.spec index fe00fc5..147d066 100644 --- a/gnome-shell.spec +++ b/gnome-shell.spec @@ -40,12 +40,9 @@ Patch: disable-unlock-entry-until-question.patch Patch: 0001-main-Register-session-with-GDM-on-startup.patch Patch: 0001-loginManager-Update-RegisterSession.patch # Passwordless work -# https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3212 +Patch: pre-changes-for-passwordless-gdm-backport.patch Patch: 0001-Support-for-web-login-and-unified-auth-mechanism.patch -Patch: 0002-post-changes-for-passwordless-gdm-backport.patch -# Some users might have a broken PAM config, so we really need this -# downstream patch to stop trying on configuration errors. -Patch: 0001-gdm-Work-around-failing-fingerprint-auth.patch +Patch: post-changes-for-passwordless-gdm-backport.patch # Extensions Patch: 0001-extensionDownloader-Refuse-to-override-system-extens.patch diff --git a/post-changes-for-passwordless-gdm-backport.patch b/post-changes-for-passwordless-gdm-backport.patch new file mode 100644 index 0000000..98960a0 --- /dev/null +++ b/post-changes-for-passwordless-gdm-backport.patch @@ -0,0 +1,278 @@ +From 45dd4808e26dabd03c25c126e97b22a20b4bcfa5 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 2 Jun 2026 12:07:45 +0200 +Subject: [PATCH 1/5] Use Object.keys(this._sections) instead of + this._sections.keys() + +--- + js/gdm/authMenuButton.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index 5aaa45fd3..e2c516585 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -252,7 +252,7 @@ export class AuthMenuButton extends St.Button { + this._items.delete(itemKey); + }); + +- this._sections.keys().forEach(sectionName => { ++ Object.keys(this._sections).forEach(sectionName => { + const itemsInSection = this._findItems({sectionName}); + if (itemsInSection.length === 0) { + const section = this._sections.get(sectionName); +-- +2.55.0 + + +From 2f4e4d9bff76e6b4db951890b82282cc7a01921a Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 2 Jun 2026 12:52:09 +0200 +Subject: [PATCH 2/5] Don't use _once timeout variants + +--- + js/gdm/authServices.js | 7 +++++-- + js/gdm/authServicesLegacy.js | 3 ++- + js/gdm/authServicesSSSDSwitchable.js | 3 ++- + 3 files changed, 9 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index d0f7659a5..e13d4106e 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -283,8 +283,11 @@ export class AuthServices extends GObject.Object { + + _waitPendingMessages() { + const cancellable = this._cancellable; +- const timeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, 10, +- () => cancellable.cancel()); ++ const timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, ++ () => { ++ cancellable.cancel(); ++ return GLib.SOURCE_REMOVE; ++ }); + + const {promise, resolve, reject} = Promise.withResolvers(); + const task = Gio.Task.new(this, cancellable, () => { +diff --git a/js/gdm/authServicesLegacy.js b/js/gdm/authServicesLegacy.js +index 77f9cece6..0e5c525de 100644 +--- a/js/gdm/authServicesLegacy.js ++++ b/js/gdm/authServicesLegacy.js +@@ -132,12 +132,13 @@ export class AuthServicesLegacy extends AuthServices { + this._fingerprintReadyTimeoutId !== 0) + return; + +- this._fingerprintReadyTimeoutId = GLib.timeout_add_once( ++ this._fingerprintReadyTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + FINGERPRINT_READY_TIMEOUT_MS, + () => { + this._fingerprintReadyTimeoutId = 0; + this._setFingerprintReady(true); ++ return GLib.SOURCE_REMOVE; + }); + } + +diff --git a/js/gdm/authServicesSSSDSwitchable.js b/js/gdm/authServicesSSSDSwitchable.js +index cf6a4e1fb..0271d98f2 100644 +--- a/js/gdm/authServicesSSSDSwitchable.js ++++ b/js/gdm/authServicesSSSDSwitchable.js +@@ -287,7 +287,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + if (!timeout) + return; + +- this._webLoginTimeoutId = GLib.timeout_add_seconds_once(GLib.PRIORITY_DEFAULT, ++ this._webLoginTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, + timeout, () => { + if (this._selectedMechanism?.role !== Role.WEB_LOGIN) + webLoginMechanism.needsRefresh = true; +@@ -295,6 +295,7 @@ export class AuthServicesSSSDSwitchable extends AuthServices { + this.emit('reset', {softReset: true}); + + this._webLoginTimeoutId = 0; ++ return GLib.SOURCE_REMOVE; + }); + } + +-- +2.55.0 + + +From 398fd1ad475e2092977cb210e12d3403bc0fdbaa Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 9 Jun 2026 09:57:09 +0200 +Subject: [PATCH 3/5] gdm: Use alternative to Promise.withResolvers() + +It doesn't exist in this version +--- + js/gdm/authServices.js | 6 +++++- + js/gdm/userVerifier.js | 6 +++++- + 2 files changed, 10 insertions(+), 2 deletions(-) + +diff --git a/js/gdm/authServices.js b/js/gdm/authServices.js +index e13d4106e..e5cf21c96 100644 +--- a/js/gdm/authServices.js ++++ b/js/gdm/authServices.js +@@ -289,7 +289,11 @@ export class AuthServices extends GObject.Object { + return GLib.SOURCE_REMOVE; + }); + +- const {promise, resolve, reject} = Promise.withResolvers(); ++ let resolve, reject; ++ const promise = new Promise((res, rej) => { ++ resolve = res; ++ reject = rej; ++ }); + const task = Gio.Task.new(this, cancellable, () => { + try { + const res = task.propagate_boolean(); +diff --git a/js/gdm/userVerifier.js b/js/gdm/userVerifier.js +index 02decbf86..27777c6f1 100644 +--- a/js/gdm/userVerifier.js ++++ b/js/gdm/userVerifier.js +@@ -223,7 +223,11 @@ export class ShellUserVerifier extends Signals.EventEmitter { + return; + + const message = this.currentMessage; +- const {promise, resolve} = Promise.withResolvers(); ++ let resolve, reject; ++ const promise = new Promise((res, rej) => { ++ resolve = res; ++ reject = rej; ++ }); + this._showMessageResolver = resolve; + + this.emit('show-message', { +-- +2.55.0 + + +From dda52fc6a0e12af8dc3b6eca5f853923c311d837 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Thu, 6 Aug 2026 23:58:46 +0200 +Subject: [PATCH 4/5] gdm: Use old ButtonMask enum + +In this version it still used as ONE, TWO and THREE +--- + js/gdm/authList.js | 2 +- + js/gdm/authMenuButton.js | 2 +- + js/gdm/authPrompt.js | 2 +- + js/gdm/webLogin.js | 2 +- + 4 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/js/gdm/authList.js b/js/gdm/authList.js +index 81c204e7f..8e035da91 100644 +--- a/js/gdm/authList.js ++++ b/js/gdm/authList.js +@@ -70,7 +70,7 @@ class ItemIcon extends St.Button { + constructor(iconName, iconTitle, iconSubtitle) { + super({ + style_class: 'login-dialog-item-icon', +- button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, ++ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + iconName, + }); + +diff --git a/js/gdm/authMenuButton.js b/js/gdm/authMenuButton.js +index e2c516585..ca9a0b9d1 100644 +--- a/js/gdm/authMenuButton.js ++++ b/js/gdm/authMenuButton.js +@@ -149,7 +149,7 @@ export class AuthMenuButton extends St.Button { + style_class: 'login-dialog-button login-dialog-auth-menu-button', + can_focus: true, + accessible_role: Atk.Role.MENU, +- button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, ++ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + }); + this.bind_property('reactive', + this, 'can-focus', +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 007c1538c..7e69525f9 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -291,7 +291,7 @@ export const AuthPrompt = GObject.registerClass({ + + this._authButton = new St.Button({ + style_class: 'login-button', +- button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, ++ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + can_focus: true, + x_align: Clutter.ActorAlign.CENTER, + x_expand: true, +diff --git a/js/gdm/webLogin.js b/js/gdm/webLogin.js +index 3b6496296..34c371b8d 100644 +--- a/js/gdm/webLogin.js ++++ b/js/gdm/webLogin.js +@@ -200,7 +200,7 @@ export class WebLoginDialog extends St.Widget { + style_class: 'web-login-prompt-button', + can_focus: true, + accessible_name: b.label, +- button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY, ++ button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + child: new St.Label({ + text: b.label, + style_class: 'login-button-label', +-- +2.55.0 + +From 5e3b6d53a6e34e27e6c38d517fe51cfe52b2b96e Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Fri, 7 Aug 2026 11:42:37 +0200 +Subject: [PATCH 5/5] style: Fix color styles to adapt when on light-mode + (classic-mode) + +--- + data/theme/gnome-shell-sass/widgets/_login-lock.scss | 8 ++++---- + data/theme/gnome-shell-sass/widgets/_qr-code.scss | 1 + + 2 files changed, 5 insertions(+), 4 deletions(-) + +diff --git a/data/theme/gnome-shell-sass/widgets/_login-lock.scss b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +index 04f24a789..fbcb4d20f 100644 +--- a/data/theme/gnome-shell-sass/widgets/_login-lock.scss ++++ b/data/theme/gnome-shell-sass/widgets/_login-lock.scss +@@ -92,12 +92,12 @@ $_gdm_dialog_width: 25em; + &-label { + @include fontsize($base_font_size - 1); + font-weight: bold; +- color: $_gdm_fg; ++ color: $fg_color; + } + } + + .login-dialog-auth-menu-item-icon { +- color: $_gdm_fg; ++ color: $fg_color; + icon-size: $scalable_icon_size; + margin-right: $base_margin * 2; + } +@@ -106,13 +106,13 @@ $_gdm_dialog_width: 25em; + spacing: $base_padding * .5; + + &-name { +- color: $_gdm_fg; ++ color: $fg_color; + @include fontsize($base_font_size); + font-weight: bold; + } + + &-description { +- color: $_gdm_fg; ++ color: $fg_color; + @include fontsize($base_font_size - 1); + } + } +diff --git a/data/theme/gnome-shell-sass/widgets/_qr-code.scss b/data/theme/gnome-shell-sass/widgets/_qr-code.scss +index da04425bd..98ecf7b0f 100644 +--- a/data/theme/gnome-shell-sass/widgets/_qr-code.scss ++++ b/data/theme/gnome-shell-sass/widgets/_qr-code.scss +@@ -6,6 +6,7 @@ + $qrcode_bg_color: mix($fg_color, $bg_color, 8%); + background-color: $qrcode_bg_color; + border-color: $qrcode_bg_color; ++ color: $fg_color; + } @else { + background-color: $system_fg_color; + border-color: $system_fg_color; +-- +2.55.0 + diff --git a/pre-changes-for-passwordless-gdm-backport.patch b/pre-changes-for-passwordless-gdm-backport.patch new file mode 100644 index 0000000..6171c45 --- /dev/null +++ b/pre-changes-for-passwordless-gdm-backport.patch @@ -0,0 +1,217 @@ + +From 1c91eaa7fb93536a1e3e2695d856721d403171f6 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 5 May 2026 17:26:27 +0200 +Subject: [PATCH 1/3] animationUtils: Skip no-op wiggle to avoid cancelling + pending transitions + +When wiggle() is called with duration 0, the async easeAsync calls +yield between microtasks, leaving pending operations that remove +transitions on the same property. This becomes a problem when two +setMessage() calls happen in the same call stack (e.g. via +_queuePriorityMessage): the first fires a no-op wiggle(dur:0) and +the second starts the real wiggle(dur:65). When microtasks drain, +the first wiggle's pending easeAsync calls overwrite the second +wiggle's active transition, effectively cancelling the real animation. + +Return early when duration is 0, since the animation is a no-op anyway. + +Fixes: fdadbb7a5a2 ("animationUtils: Use easeAsync() for wiggle") +Closes: https://gitlab.gnome.org/GNOME/gnome-shell/-/work_items/9207 +Part-of: +--- + js/misc/animationUtils.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/js/misc/animationUtils.js b/js/misc/animationUtils.js +index cf6889609..9d065cf78 100644 +--- a/js/misc/animationUtils.js ++++ b/js/misc/animationUtils.js +@@ -84,7 +84,7 @@ export function ensureActorVisibleInScrollView(scrollView, actor) { + * @param {number} params.wiggleCount - the number of times to wiggle the actor + */ + export function wiggle(actor, params) { +- if (!St.Settings.get().enable_animations) ++ if (!St.Settings.get().enable_animations || params?.duration === 0) + return; + + params = Params.parse(params, { +-- +2.55.0 + + +From b5a37cbdd89eeed44966455ba22b0f10a3f25091 Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 5 May 2026 21:32:06 +0200 +Subject: [PATCH 2/3] authPrompt: Call wiggle directly in _onShowMessage + +Part-of: +--- + js/gdm/authPrompt.js | 13 +++++-------- + 1 file changed, 5 insertions(+), 8 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 3b4a2f798..f05856416 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -394,21 +394,20 @@ export const AuthPrompt = GObject.registerClass({ + } + + _onShowMessage(_userVerifier, serviceName, message, type) { +- let wiggleParameters = {duration: 0}; ++ this.setMessage(message, type); ++ this.emit('prompted'); + + if (type === GdmUtil.MessageType.ERROR && + this._userVerifier.serviceIsFingerprint(serviceName)) { + // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue +- wiggleParameters = { ++ const wiggleParameters = { + duration: 65, + wiggleCount: 3, + }; + this._userVerifier.increaseCurrentMessageTimeout( + wiggleParameters.duration * (wiggleParameters.wiggleCount + 2)); ++ wiggle(this._message, wiggleParameters); + } +- +- this.setMessage(message, type, wiggleParameters); +- this.emit('prompted'); + } + + _onVerificationFailed(userVerifier, serviceName, canRetry) { +@@ -587,7 +586,7 @@ export const AuthPrompt = GObject.registerClass({ + }); + } + +- setMessage(message, type, wiggleParameters = {duration: 0}) { ++ setMessage(message, type) { + if (type === GdmUtil.MessageType.ERROR) + this._message.add_style_class_name('login-dialog-message-warning'); + else +@@ -607,8 +606,6 @@ export const AuthPrompt = GObject.registerClass({ + } else { + this._message.opacity = 0; + } +- +- wiggle(this._message, wiggleParameters); + } + + updateSensitivity(sensitive) { +-- +2.55.0 + + +From 1e3f937a6ccfc8ea79c80d78df121102cffbdeff Mon Sep 17 00:00:00 2001 +From: Joan Torres Lopez +Date: Tue, 5 May 2026 17:28:18 +0200 +Subject: [PATCH 3/3] authPrompt: Await wiggle via show-message promise + resolver + +Pass promise resolver through the show-message signal so the +message queue waits for the wiggle animation to finish before scheduling +the next message timeout. This replaces the previous approach of manually +increasing the timeout duration to account for the wiggle. + +Part-of: +--- + js/gdm/authPrompt.js | 21 +++++++++------------ + js/gdm/util.js | 27 +++++++++++++++++---------- + 2 files changed, 26 insertions(+), 22 deletions(-) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index f05856416..b1e8202bd 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -393,21 +393,18 @@ export const AuthPrompt = GObject.registerClass({ + this.reset(); + } + +- _onShowMessage(_userVerifier, serviceName, message, type) { ++ _onShowMessage(_userVerifier, serviceName, message, type, showMessageResolver) { + this.setMessage(message, type); + this.emit('prompted'); + +- if (type === GdmUtil.MessageType.ERROR && +- this._userVerifier.serviceIsFingerprint(serviceName)) { +- // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue +- const wiggleParameters = { +- duration: 65, +- wiggleCount: 3, +- }; +- this._userVerifier.increaseCurrentMessageTimeout( +- wiggleParameters.duration * (wiggleParameters.wiggleCount + 2)); +- wiggle(this._message, wiggleParameters); +- } ++ const shouldWiggle = type === GdmUtil.MessageType.ERROR && ++ this._userVerifier.serviceIsFingerprint(serviceName); ++ ++ const wigglePromise = shouldWiggle ++ ? wiggle(this._message, {duration: 65, wiggleCount: 3}) ++ : Promise.resolve(); ++ ++ showMessageResolver?.(wigglePromise); + } + + _onVerificationFailed(userVerifier, serviceName, canRetry) { +diff --git a/js/gdm/util.js b/js/gdm/util.js +index 4b0f763f0..e529f84fa 100644 +--- a/js/gdm/util.js ++++ b/js/gdm/util.js +@@ -262,11 +262,6 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this.emit('no-more-messages'); + } + +- increaseCurrentMessageTimeout(interval) { +- if (!this._messageQueueTimeoutId && interval > 0) +- this._currentMessageExtraInterval = interval; +- } +- + _serviceHasPendingMessages(serviceName) { + return this._messageQueue.some(m => m.serviceName === serviceName); + } +@@ -279,17 +274,23 @@ export class ShellUserVerifier extends Signals.EventEmitter { + this._queuePriorityMessage(serviceName, null, messageType); + } + +- _queueMessageTimeout() { +- if (this._messageQueueTimeoutId !== 0) ++ async _queueMessageTimeout() { ++ if (this._messageQueueTimeoutId !== 0 || this._showMessageResolver) + return; + + const message = this.currentMessage; ++ const {promise, resolve} = Promise.withResolvers(); ++ this._showMessageResolver = resolve; + +- delete this._currentMessageExtraInterval; +- this.emit('show-message', message.serviceName, message.text, message.type); ++ this.emit('show-message', message.serviceName, message.text, message.type, this._showMessageResolver); ++ ++ await promise.catch(logError); ++ if (!this._showMessageResolver) ++ return; ++ this._showMessageResolver = null; + + this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, +- message.interval + (this._currentMessageExtraInterval | 0), () => { ++ message.interval, () => { + this._messageQueueTimeoutId = 0; + + if (this._messageQueue.length > 1) { +@@ -332,6 +333,12 @@ export class ShellUserVerifier extends Signals.EventEmitter { + GLib.source_remove(this._messageQueueTimeoutId); + this._messageQueueTimeoutId = 0; + } ++ ++ if (this._showMessageResolver) { ++ this._showMessageResolver(); ++ this._showMessageResolver = null; ++ } ++ + this.emit('show-message', null, null, MessageType.NONE); + } + +-- +2.55.0 +